Gridarta Editor
Updater.java
Go to the documentation of this file.
1 /*
2  * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
3  * Copyright (C) 2000-2015 The Gridarta Developers.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 package net.sf.gridarta.updater;
21 
22 import java.awt.Component;
23 import java.io.File;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InterruptedIOException;
28 import java.io.OutputStream;
29 import java.net.Proxy;
30 import java.net.URL;
31 import java.net.URLConnection;
32 import java.net.UnknownHostException;
33 import java.util.MissingResourceException;
34 import java.util.PropertyResourceBundle;
35 import java.util.ResourceBundle;
36 import java.util.prefs.Preferences;
37 import javax.swing.JOptionPane;
38 import javax.swing.ProgressMonitor;
39 import javax.swing.ProgressMonitorInputStream;
40 import net.sf.gridarta.MainControl;
43 import net.sf.gridarta.utils.Exiter;
44 import net.sf.japi.swing.action.ActionBuilder;
45 import net.sf.japi.swing.action.ActionBuilderFactory;
46 import org.apache.log4j.Category;
47 import org.apache.log4j.Logger;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50 
58 public class Updater implements Runnable {
59 
63  @NotNull
64  private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
65 
69  @NotNull
70  private static final Category LOG = Logger.getLogger(Updater.class);
71 
75  @NotNull
76  private static final Preferences PREFERENCES = Preferences.userNodeForPackage(MainControl.class);
77 
81  @NotNull
82  public static final String LAST_UPDATE_KEY = "UpdateTimestamp";
83 
87  @Nullable
88  private final Component parentComponent;
89 
93  @NotNull
94  private final Exiter exiter;
95 
99  @NotNull
100  private final String updateFileName;
101 
105  private static final int BUF_SIZE = 4096;
106 
113  public Updater(@Nullable final Component parentComponent, @NotNull final Exiter exiter, @NotNull final String updateFileName) {
114  this.parentComponent = parentComponent;
115  this.exiter = exiter;
116  this.updateFileName = updateFileName;
117  if (parentComponent != null) {
118  parentComponent.setEnabled(false);
119  }
120  }
121 
122  @Override
123  public void run() {
124  try {
125  final String propUrl = ACTION_BUILDER.getString("update.url");
126  if (propUrl == null) {
127  return;
128  }
129 
130  try (InputStream pin = openStream(propUrl)) {
131  final ResourceBundle updateBundle = new PropertyResourceBundle(pin);
132  final String downloadUrl = updateBundle.getString("update.url");
133  if (downloadUrl == null) {
134  ACTION_BUILDER.showMessageDialog(parentComponent, "updateError", "invalid server response: update.url is missing");
135  return;
136  }
137  final VersionInfo update = new VersionInfo(updateBundle, "update");
139  try {
140  active = new VersionInfo(ResourceBundle.getBundle("build"), "build");
141  } catch (final MissingResourceException e) {
142  ACTION_BUILDER.showMessageDialog(parentComponent, "updateActiveVersionUnavailable");
143  }
144  PREFERENCES.putLong(LAST_UPDATE_KEY, System.currentTimeMillis());
145  if (active == null || update.isNewerThan(active)) {
146  if (askIfUserWantsUpdate(active, update, propUrl, downloadUrl)) {
147  downloadAndInstallUpdate(downloadUrl);
148  }
149  } else {
150  noNewUpdate(active, update, propUrl, downloadUrl);
151  }
152  }
153  } catch (final UnknownHostException e) {
154  ACTION_BUILDER.showMessageDialog(parentComponent, "updateError", e.getLocalizedMessage());
155  } catch (final IOException e) {
156  ACTION_BUILDER.showMessageDialog(parentComponent, "updateError", e);
157  } finally {
158  if (parentComponent != null) {
159  parentComponent.setEnabled(true);
160  }
161  }
162  }
163 
173  private boolean askIfUserWantsUpdate(@Nullable final VersionInfo active, @NotNull final VersionInfo update, @NotNull final String propUrl, @NotNull final String downloadUrl) {
174  return ACTION_BUILDER.showConfirmDialog(parentComponent, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, "updateAvailable", updateFileName, active == null ? "?" : active.version, update.version, active == null ? "?" : active.developer, update.developer, active == null ? "?" : active.timestamp, update.timestamp, propUrl, downloadUrl) == JOptionPane.YES_OPTION;
175  }
176 
184  private void noNewUpdate(@Nullable final VersionInfo active, @NotNull final VersionInfo update, @NotNull final String propUrl, @NotNull final String downloadUrl) {
185  ACTION_BUILDER.showMessageDialog(parentComponent, "updateUnavailable", active == null ? "?" : active.version, update.version, active == null ? "?" : active.developer, update.developer, active == null ? "?" : active.timestamp, update.timestamp, propUrl, downloadUrl);
186  }
187 
192  private void downloadAndInstallUpdate(@NotNull final String url) {
193  final File download = new File(updateFileName + ".tmp"); // TODO: print error message if file already exists
194  final File backup = new File(updateFileName + ".bak");
195  final File orig = new File(updateFileName);
196  try {
197  try (InputStream in = openStream(url)) {
198  try (OutputStream out = new FileOutputStream(download)) {
199  final byte[] buf = new byte[BUF_SIZE];
200  while (true) {
201  final int bytesRead = in.read(buf);
202  if (bytesRead == -1) {
203  break;
204  }
205  out.write(buf, 0, bytesRead);
206  }
207  out.close();
208  if (/* !backup.delete() || */ !orig.renameTo(backup)) {
209  ACTION_BUILDER.showMessageDialog(parentComponent, "updateFailedNoBackup", updateFileName);
210  } else if (!download.renameTo(orig)) {
211  backup.renameTo(orig);
212  ACTION_BUILDER.showMessageDialog(parentComponent, "updateFailedNoDownload");
213  } else {
214  ACTION_BUILDER.showMessageDialog(parentComponent, "updateRestart", updateFileName);
215  exiter.doExit(0);
216  }
217  }
218  }
219  } catch (final InterruptedIOException e) {
220  ACTION_BUILDER.showMessageDialog(parentComponent, "updateAborted");
221  } catch (final Exception e) {
222  LOG.warn("updateError", e);
223  ACTION_BUILDER.showMessageDialog(parentComponent, "updateError", e);
224  }
225  }
226 
233  @NotNull
234  // The stream is closed by caller
235  private InputStream openStream(@NotNull final String url) throws IOException {
236  final Proxy proxy = NetPreferences.getProxy();
237  final URLConnection con = new URL(url).openConnection(proxy);
238  final ProgressMonitorInputStream stream = new ProgressMonitorInputStream(parentComponent, ActionBuilderUtils.getString(ACTION_BUILDER, "updateProgress.title"), con.getInputStream());
239  final ProgressMonitor monitor = stream.getProgressMonitor();
240  monitor.setMaximum(con.getContentLength());
241  monitor.setNote(ActionBuilderUtils.getString(ACTION_BUILDER, "updateProgress"));
242  monitor.setMillisToDecideToPopup(10);
243  monitor.setMillisToPopup(10);
244  return stream;
245  }
246 
250  private static class VersionInfo {
251 
256  @NotNull
257  private final String version;
258 
262  @NotNull
263  private final String timestamp;
264 
268  @NotNull
269  private final String developer;
270 
274  @NotNull
275  private static final VersionInfo UNAVAILABLE = new VersionInfo();
276 
280  private VersionInfo() {
281  this("unavailable", "unavailable", "unavailable");
282  }
283 
290  private VersionInfo(@NotNull final String version, @NotNull final String timestamp, @NotNull final String developer) {
291  this.version = version;
292  this.timestamp = timestamp;
293  this.developer = developer;
294  }
295 
304  private VersionInfo(@NotNull final ResourceBundle bundle, @NotNull final String prefix) {
305  this(bundle.getString(prefix + ".number"), bundle.getString(prefix + ".tstamp"), bundle.getString(prefix + ".developer"));
306  }
307 
314  @SuppressWarnings("ObjectEquality")
315  boolean isNewerThan(@NotNull final VersionInfo other) {
316  return this != UNAVAILABLE && (other == UNAVAILABLE || timestamp.compareTo(other.timestamp) > 0);
317  }
318 
319  }
320 
321 }
static Proxy getProxy()
Returns the currently preferred proxy.
static final int BUF_SIZE
Buffer size.
Definition: Updater.java:105
Graphical User Interface of Gridarta.
VersionInfo(@NotNull final String version, @NotNull final String timestamp, @NotNull final String developer)
Private constructor to map the strings.
Definition: Updater.java:290
final Component parentComponent
The parentComponent to show dialogs on.
Definition: Updater.java:88
static final String LAST_UPDATE_KEY
Preferences key for last update.
Definition: Updater.java:82
This class handles updating the map editor.
Definition: Updater.java:58
final String updateFileName
The file to update.
Definition: Updater.java:100
Class for holding version information and quickly comparing it.
Definition: Updater.java:250
static String getString(@NotNull final ActionBuilder actionBuilder, @NotNull final String key, @NotNull final String defaultValue)
Returns the value of a key.
Base package of all Gridarta classes.
void doExit(int returnCode)
Exits the application.
InputStream openStream(@NotNull final String url)
Opens an InputStream on a URL.
Definition: Updater.java:235
Exits the application.
Definition: Exiter.java:28
final String developer
Update information: Developer that created the update version.
Definition: Updater.java:269
Updater(@Nullable final Component parentComponent, @NotNull final Exiter exiter, @NotNull final String updateFileName)
Create a new instance.
Definition: Updater.java:113
This package contains the preferences ui modules.
void noNewUpdate(@Nullable final VersionInfo active, @NotNull final VersionInfo update, @NotNull final String propUrl, @NotNull final String downloadUrl)
Tell the user there is no update.
Definition: Updater.java:184
Interface used as preferences location.
static final Preferences PREFERENCES
Preferences.
Definition: Updater.java:76
static final VersionInfo UNAVAILABLE
Special Version "unavailable".
Definition: Updater.java:275
Preferences Module for networking preferences.
void downloadAndInstallUpdate(@NotNull final String url)
Download and install an update.
Definition: Updater.java:192
final Exiter exiter
The Exiter for terminating the application.
Definition: Updater.java:94
final String version
Update information: Version of update version, usually the build number.
Definition: Updater.java:257
VersionInfo(@NotNull final ResourceBundle bundle, @NotNull final String prefix)
Create update information from a ResourceBundle.
Definition: Updater.java:304
VersionInfo()
Private constructor used for unavailable versions.
Definition: Updater.java:280
Utility class for ActionBuilder related functions.
final String timestamp
Update information: Time stamp of update version.
Definition: Updater.java:263
boolean askIfUserWantsUpdate(@Nullable final VersionInfo active, @NotNull final VersionInfo update, @NotNull final String propUrl, @NotNull final String downloadUrl)
Ask the user whether he wants to update.
Definition: Updater.java:173
static final ActionBuilder ACTION_BUILDER
Action Builder to create Actions.
Definition: Updater.java:64
static final Category LOG
Logger.
Definition: Updater.java:70