1 package fr.ifremer.quadrige3.ui.updater;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import javax.swing.JOptionPane;
25 import java.io.*;
26 import java.net.URL;
27 import java.net.URLConnection;
28 import java.nio.charset.Charset;
29 import java.nio.charset.StandardCharsets;
30 import java.nio.file.*;
31 import java.nio.file.attribute.BasicFileAttributes;
32 import java.nio.file.attribute.PosixFilePermission;
33 import java.text.DateFormat;
34 import java.text.MessageFormat;
35 import java.util.*;
36 import java.util.ResourceBundle.Control;
37 import java.util.stream.Collectors;
38
39 import static java.nio.file.FileVisitResult.CONTINUE;
40 import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
41 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
42
43
44
45
46
47
48 public class Updater {
49
50 private static final String NEW_DIR = "NEW";
51 private static final String OLD_DIR = "OLD";
52 private static final String JRE_DIR = "jre";
53 private static final String LAUNCHER_DIR = "launcher";
54 private static final String UPDATE_RUNTIME_FILENAME = "update_runtime";
55 private static final String UPDATE_RUNTIME_WINDOWS_CMD = "cmd /c start \"" + UPDATE_RUNTIME_FILENAME + "\" \"%s\"";
56 private static final String UPDATE_RUNTIME_UNIX_CMD = "./%s";
57 private static final String EXTERNAL_BACKUP_CMD = "external_backup";
58 private static final String BIN_WINDOWS_EXT = ".exe";
59 private static final String BATCH_WINDOWS_EXT = ".bat";
60 private static final String BATCH_UNIX_EXT = ".sh";
61
62 private static final String LAUNCHER_PROPERTIES = "launcher.properties";
63 private static final String PROPERTY_NAME = "NAME";
64
65 private static final String APP_DIR = "application";
66 private static final String CONFIG_DIR = "config";
67 private static final String CONFIG_EXT = ".config";
68 private static final String I18N_DIR = "i18n";
69 private static final String HELP_DIR = "help";
70 private static final String PLUGINS_DIR = "plugins";
71 private static final String DB_CACHE_DIR = "data/dbcache";
72 private static final String VERSION_FILE = "version.appup";
73
74 private static final int NORMAL_EXIT_CODE = 0;
75 private static final int ERROR_EXIT_CODE = 1;
76 private static final int STOP_EXIT_CODE = 90;
77
78 private Path basePath;
79 private Properties launcherProperties;
80
81 private static ResourceBundle bundle;
82
83
84
85
86
87
88 public static void main(String[] args) {
89
90
91 bundle = ResourceBundle.getBundle("i18n/updater", new UTF8Control());
92
93
94 Updater updater = new Updater();
95 int exitCode = updater.execute();
96
97
98 System.exit(exitCode);
99
100 }
101
102
103
104
105
106
107 private int execute() {
108 try {
109
110
111 basePath = Paths.get(System.getProperty("user.dir"));
112
113
114 launcherProperties = readLauncherProperties();
115
116
117 launchRuntimeUpdate();
118
119
120 launchUpdate();
121
122 } catch (IOException ex) {
123 JOptionPane.showMessageDialog(null, ex.toString(), getTitle(), JOptionPane.ERROR_MESSAGE);
124 ex.printStackTrace(System.err);
125 return STOP_EXIT_CODE;
126 } catch (Exception ex) {
127 ex.printStackTrace(System.err);
128 return ERROR_EXIT_CODE;
129 }
130 return NORMAL_EXIT_CODE;
131 }
132
133 private void launchUpdate() throws IOException {
134
135 System.out.println(getString("updater.started", DateFormat.getInstance().format(new Date())));
136
137
138 boolean appUpdated = updateModule(basePath, APP_DIR);
139 updateModule(basePath, I18N_DIR);
140 updateModule(basePath, HELP_DIR);
141
142
143 updateConfig(basePath);
144
145
146 updatePlugins();
147
148
149 updateOtherFiles();
150
151
152 cleanObsoleteFiles();
153 cleanPath(basePath.resolve(NEW_DIR));
154 cleanPath(basePath.resolve(PLUGINS_DIR).resolve(NEW_DIR));
155
156 cleanPath(basePath.resolve(OLD_DIR));
157
158 if (appUpdated) {
159
160 cleanPath(basePath.resolve(DB_CACHE_DIR));
161 }
162
163 System.out.println(getString("updater.ended", DateFormat.getInstance().format(new Date())));
164
165 }
166
167 private boolean updateModule(Path basePath, String moduleName) throws IOException {
168
169
170 Path modulePath = basePath.resolve(moduleName);
171 Path moduleNewPath = basePath.resolve(NEW_DIR).resolve(moduleName);
172
173 if (Files.isDirectory(moduleNewPath)) {
174
175
176 cleanPath(modulePath);
177
178
179 String newVersion = getVersion(moduleNewPath);
180 System.out.println(getString("updater.install", moduleName, newVersion));
181 moveDirectory(moduleNewPath, modulePath);
182
183 return true;
184 }
185
186 return false;
187 }
188
189 private void updatePlugins() throws IOException {
190
191
192 Path pluginsPath = basePath.resolve(PLUGINS_DIR);
193 Path pluginsNewPath = pluginsPath.resolve(NEW_DIR);
194
195 if (Files.isDirectory(pluginsNewPath)) {
196
197 DirectoryStream<Path> stream = Files.newDirectoryStream(pluginsNewPath, entry -> Files.isDirectory(entry));
198
199 List<String> pluginNames = new ArrayList<>();
200 for (Path path : stream) {
201 pluginNames.add(path.getFileName().toString());
202 }
203
204
205 for (String pluginName : pluginNames) {
206 updateModule(pluginsPath, pluginName);
207 }
208 }
209
210 }
211
212 private void updateConfig(Path basePath) throws IOException {
213
214 String configFileName = getName().toLowerCase() + CONFIG_EXT;
215 Path configPath = basePath.resolve(CONFIG_DIR);
216 Path configFilePath = configPath.resolve(configFileName);
217 Path configNewPath = basePath.resolve(NEW_DIR).resolve(CONFIG_DIR);
218 Path configNewFilePath = configNewPath.resolve(configFileName);
219
220 if (Files.isRegularFile(configNewFilePath)) {
221
222 String newVersion = getVersion(configNewPath);
223 System.out.println(getString("updater.install", CONFIG_DIR, newVersion));
224
225 if (Files.isRegularFile(configFilePath)) {
226
227
228 Properties actualProperties = new Properties();
229 Properties newProperties = new Properties();
230 try (Reader actualPropertiesReader = Files.newBufferedReader(configFilePath, StandardCharsets.UTF_8);
231 Reader newPropertiesReader = Files.newBufferedReader(configNewFilePath, StandardCharsets.UTF_8)) {
232
233
234 actualProperties.load(actualPropertiesReader);
235
236
237 newProperties.load(newPropertiesReader);
238 }
239
240
241 newProperties.forEach((key, newValue) -> actualProperties.setProperty((String) key, (String) newValue));
242
243 try (Writer actualPropertiesWriter = Files.newBufferedWriter(configFilePath, StandardCharsets.UTF_8)) {
244
245
246 actualProperties.store(actualPropertiesWriter, null);
247 }
248
249 } else {
250
251
252 Files.createDirectories(configFilePath.getParent());
253 Files.copy(configNewFilePath, configFilePath);
254 }
255
256
257 Files.copy(configNewFilePath.resolveSibling(VERSION_FILE), configFilePath.resolveSibling(VERSION_FILE), REPLACE_EXISTING);
258 }
259 }
260
261 private Properties readLauncherProperties() throws Exception {
262
263 Path localLauncherFile = basePath.resolve(LAUNCHER_PROPERTIES);
264 Path applicationLauncherFile = basePath.resolve(APP_DIR).resolve(LAUNCHER_PROPERTIES);
265
266 if (!Files.exists(localLauncherFile) && !Files.exists(applicationLauncherFile)) {
267 throw new IOException(getString("updater.fileNotExists", LAUNCHER_PROPERTIES));
268 }
269
270 Properties p1 = new Properties();
271 if (Files.isReadable(applicationLauncherFile)) {
272 try (Reader reader = new FileReader(applicationLauncherFile.toFile())) {
273 p1.load(reader);
274 }
275 }
276
277 Properties p = new Properties(p1);
278 if (Files.isReadable(localLauncherFile)) {
279 try (Reader reader = new FileReader(localLauncherFile.toFile())) {
280 p.load(reader);
281 }
282 }
283
284
285 assertPropertyExists(p, PROPERTY_NAME);
286
287 return p;
288 }
289
290 @SuppressWarnings("SameParameterValue")
291 private void assertPropertyExists(Properties properties, String propertyName) {
292 String property = properties.getProperty(propertyName);
293 if (property == null || property.trim().length() == 0)
294 throw new NullPointerException(getString("updater.propertyNotFound", propertyName, LAUNCHER_PROPERTIES));
295 }
296
297 private String getName() {
298 return launcherProperties.getProperty(PROPERTY_NAME);
299 }
300
301 private String getTitle() {
302 return getString("updater.title", getName());
303 }
304
305 private String getVersion(Path path) throws IOException {
306
307
308 Path versionFile = path.resolve(VERSION_FILE);
309 if (!Files.exists(versionFile)) {
310 return "0";
311 }
312 List<String> lines = Files.readAllLines(versionFile, StandardCharsets.UTF_8);
313 if (lines.isEmpty()) {
314 return "0";
315 }
316 return lines.get(0);
317 }
318
319 private void updateOtherFiles() throws IOException {
320
321
322 Path extBackupPath = basePath.resolve(APP_DIR).resolve(EXTERNAL_BACKUP_CMD + (isWindowsOS() ? BATCH_WINDOWS_EXT : BATCH_UNIX_EXT));
323 if (Files.exists(extBackupPath)) {
324 Files.move(extBackupPath, extBackupPath.getParent().getParent().resolve(extBackupPath.getFileName()), REPLACE_EXISTING);
325
326 if (!isWindowsOS()) {
327 Set<PosixFilePermission> perms = new HashSet<>();
328
329 perms.add(PosixFilePermission.OWNER_READ);
330 perms.add(PosixFilePermission.OWNER_WRITE);
331 perms.add(PosixFilePermission.OWNER_EXECUTE);
332
333 perms.add(PosixFilePermission.GROUP_READ);
334 perms.add(PosixFilePermission.GROUP_WRITE);
335 perms.add(PosixFilePermission.GROUP_EXECUTE);
336
337 perms.add(PosixFilePermission.OTHERS_READ);
338 perms.add(PosixFilePermission.OTHERS_EXECUTE);
339 Files.setPosixFilePermissions(basePath.resolve(EXTERNAL_BACKUP_CMD + BATCH_UNIX_EXT), perms);
340 }
341 }
342
343 }
344
345 private void copyDirectory(Path sourcePath, Path targetPath) throws IOException {
346
347 Files.walkFileTree(sourcePath, new TreeCopier(sourcePath, targetPath));
348 }
349
350 private void moveDirectory(Path sourcePath, Path targetPath) throws IOException {
351
352
353 copyDirectory(sourcePath, targetPath);
354 cleanPath(sourcePath);
355 }
356
357 private void cleanPath(Path path) throws IOException {
358
359 cleanPath(path, null);
360 }
361
362 private void cleanPath(Path path, String glob) throws IOException {
363
364 if (Files.isDirectory(path)) {
365 Files.walkFileTree(path, new RecursiveDeleteFileVisitor(glob));
366 }
367 }
368
369 private void cleanObsoleteFiles() throws IOException {
370
371 if (isWindowsOS()) {
372
373
374 Files.deleteIfExists(basePath.resolve(getName() + BATCH_WINDOWS_EXT));
375 Files.deleteIfExists(basePath.resolve(APP_DIR).resolve("launch" + BATCH_WINDOWS_EXT));
376
377
378 cleanPath(basePath, "*" + BATCH_UNIX_EXT);
379 } else {
380
381
382 Files.deleteIfExists(basePath.resolve(APP_DIR).resolve("launch" + BATCH_UNIX_EXT));
383
384
385 cleanPath(basePath, "*" + BATCH_WINDOWS_EXT);
386 cleanPath(basePath, "*" + BIN_WINDOWS_EXT);
387 }
388
389
390 Files.deleteIfExists(basePath.resolve(APP_DIR).resolve(getName() + BIN_WINDOWS_EXT));
391 Files.deleteIfExists(basePath.resolve(APP_DIR).resolve(getName() + BATCH_UNIX_EXT));
392 cleanPath(basePath.resolve(APP_DIR).resolve("launcher"));
393
394 }
395
396 private void launchRuntimeUpdate() throws IOException {
397
398 if (checkRuntimeUpdate()) {
399
400
401 Runtime.getRuntime().exec(String.format(
402 isWindowsOS() ? UPDATE_RUNTIME_WINDOWS_CMD : UPDATE_RUNTIME_UNIX_CMD,
403 getRuntimeUpdater().toFile().getAbsolutePath()));
404 System.exit(STOP_EXIT_CODE);
405 }
406 }
407
408 private boolean checkRuntimeUpdate() throws IOException {
409
410
411 Files.deleteIfExists(getRuntimeUpdater());
412
413 if (Files.isDirectory(basePath.resolve(NEW_DIR).resolve(JRE_DIR))
414 || Files.isDirectory(basePath.resolve(NEW_DIR).resolve(LAUNCHER_DIR))) {
415
416
417 generateRuntimeUpdater();
418 System.out.println(getString("updater.runtimeUpdate", getRuntimeUpdaterFilename()));
419 return true;
420 }
421 return false;
422 }
423
424 private Path getRuntimeUpdater() {
425
426
427 return basePath.resolve(getRuntimeUpdaterFilename());
428 }
429
430 private String getRuntimeUpdaterFilename() {
431
432 return UPDATE_RUNTIME_FILENAME + (isWindowsOS() ? BATCH_WINDOWS_EXT : BATCH_UNIX_EXT);
433 }
434
435 private boolean isWindowsOS() {
436
437 return System.getProperty("os.name").startsWith("Windows");
438 }
439
440 private void generateRuntimeUpdater() throws IOException {
441
442
443 InputStream inputStream = getClass().getClassLoader().getResourceAsStream(getRuntimeUpdaterFilename());
444
445
446 List<String> scriptContent = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream))).lines()
447 .map(line -> line.replaceAll("@NAME@", getName())).collect(Collectors.toList());
448
449
450 Files.write(getRuntimeUpdater(), scriptContent, Charset.defaultCharset(), StandardOpenOption.CREATE_NEW);
451 }
452
453 static class TreeCopier implements FileVisitor<Path> {
454 private final Path source;
455 private final Path target;
456
457 TreeCopier(Path source, Path target) {
458 this.source = source;
459 this.target = target;
460 }
461
462 @Override
463 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
464
465 Path newDir = target.resolve(source.relativize(dir));
466 try {
467 Files.createDirectories(newDir);
468 } catch (IOException x) {
469 System.err.format("Unable to create: %s: %s%n", newDir, x);
470 return SKIP_SUBTREE;
471 }
472 return CONTINUE;
473 }
474
475 @Override
476 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
477
478 try {
479 Files.copy(file, target.resolve(source.relativize(file)), REPLACE_EXISTING);
480 } catch (IOException e) {
481 System.err.format("Unable to copy: %s: %s%n", file, e);
482 }
483 return CONTINUE;
484 }
485
486 @Override
487 public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
488 return CONTINUE;
489 }
490
491 @Override
492 public FileVisitResult visitFileFailed(Path file, IOException exc) {
493 if (exc instanceof FileSystemLoopException) {
494 System.err.println("cycle detected: " + file);
495 } else {
496 System.err.format("Unable to copy: %s: %s%n", file, exc);
497 }
498 return CONTINUE;
499 }
500 }
501
502 private class RecursiveDeleteFileVisitor extends SimpleFileVisitor<Path> {
503
504 PathMatcher matcher;
505
506 RecursiveDeleteFileVisitor(String glob) {
507 super();
508
509
510 matcher = glob == null ? null : basePath.getFileSystem().getPathMatcher("glob:" + glob);
511 }
512
513 @Override
514 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
515
516
517 if (matcher == null || matcher.matches(file.getFileName())) {
518 Files.deleteIfExists(file);
519
520 }
521 return CONTINUE;
522 }
523
524 @Override
525 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
526
527
528 if (matcher == null) {
529 try {
530 Files.deleteIfExists(dir);
531 } catch (DirectoryNotEmptyException e) {
532
533 try {
534 Thread.sleep(500);
535 } catch (InterruptedException ignored) {
536 }
537 Files.deleteIfExists(dir);
538 }
539 }
540
541 return CONTINUE;
542 }
543
544 }
545
546 private static String getString(String resourceName, Object... params) {
547 return MessageFormat.format(bundle.getString(resourceName), params);
548 }
549
550 private static class UTF8Control extends Control {
551
552 @Override
553 public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IOException {
554
555 String bundleName = toBundleName(baseName, locale);
556 String resourceName = toResourceName(bundleName, "properties");
557 ResourceBundle bundle = null;
558 InputStream stream = null;
559 if (reload) {
560 URL url = loader.getResource(resourceName);
561 if (url != null) {
562 URLConnection connection = url.openConnection();
563 if (connection != null) {
564 connection.setUseCaches(false);
565 stream = connection.getInputStream();
566 }
567 }
568 } else {
569 stream = loader.getResourceAsStream(resourceName);
570 }
571 if (stream != null) {
572 try {
573
574 bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8));
575 } finally {
576 stream.close();
577 }
578 }
579 return bundle;
580 }
581
582 }
583 }