View Javadoc
1   package com.github.sbugat.rundeckmonitor;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.net.URL;
6   import java.nio.file.DirectoryStream;
7   import java.nio.file.FileSystems;
8   import java.nio.file.Files;
9   import java.nio.file.NoSuchFileException;
10  import java.nio.file.Path;
11  import java.nio.file.Paths;
12  import java.util.zip.ZipEntry;
13  import java.util.zip.ZipInputStream;
14  
15  import javax.swing.JOptionPane;
16  
17  import org.eclipse.egit.github.core.Repository;
18  import org.eclipse.egit.github.core.RepositoryTag;
19  import org.eclipse.egit.github.core.client.GitHubClient;
20  import org.eclipse.egit.github.core.service.RepositoryService;
21  import org.slf4j.ext.XLogger;
22  import org.slf4j.ext.XLoggerFactory;
23  
24  import com.github.sbugat.rundeckmonitor.tools.EnvironmentTools;
25  
26  /**
27   * Simple generic version checker on GitHub, inpect target jar and local jar build date to determinated if an update is available. If one is found, download the full jar and replace the original jar via a double restart.
28   * 
29   * The checker use an independant thread to check and download the file. The main thread have to check is the download is done and order to restart the program.
30   * 
31   * @author Sylvain Bugat
32   * 
33   */
34  public final class VersionChecker implements Runnable {
35  
36  	/** SLF4J XLogger. */
37  	private static final XLogger LOG = XLoggerFactory.getXLogger(VersionChecker.class);
38  
39  	/** Precalculated one megabyte for indicating download size. */
40  	private static final int ONE_MEGABYTE = 1_024 * 1_024;
41  
42  	/** Jar extension. */
43  	private static final String JAR_EXTENSION = ".jar"; //$NON-NLS-1$
44  	/** Tmp extension. */
45  	private static final String TMP_EXTENSION = ".tmp"; //$NON-NLS-1$
46  	/** Executable extension on Windows. */
47  	private static final String WINDOWS_EXE_EXTENSION = ".exe"; //$NON-NLS-1$
48  	/** Java home property. */
49  	private static final String JAVA_HOME_PROPERTY = "java.home"; //$NON-NLS-1$
50  	/** Target directory in zipball releases. */
51  	private static final String TARGET_DIRECTORY = "target"; //$NON-NLS-1$
52  
53  	/** bin subdirectory and java executable. */
54  	private static final String BIN_DIRECTORY_AND_JAVA = "bin" + FileSystems.getDefault().getSeparator() + "java"; //$NON-NLS-1$ //$NON-NLS-2$
55  
56  	/** Jar argument for java reloading. */
57  	private static final String JAR_ARGUMENT = "-jar"; //$NON-NLS-1$
58  
59  	/** Root URL of the GitHub project to update. */
60  	private final String gitHubUser;
61  	/** GitHub repository. */
62  	private final String gitHubRepository;
63  
64  	/** Maven artifact identifier. */
65  	private final String mavenArtifactId;
66  
67  	/** Suffix of the full jar including all dependencies. */
68  	private final String jarWithDependenciesSuffix;
69  
70  	/** Indicate if the download is completed. */
71  	private boolean downloadDone;
72  
73  	/** Indicate if the version checker is disabled. */
74  	private boolean versionCheckerDisabled;
75  
76  	/** Name of the downloaded jar. */
77  	private String downloadedJar;
78  
79  	/**
80  	 * Initialize the version checker with jar artifact and suffixnames and path to GitHub.
81  	 * 
82  	 * @param gitHubUserArg GitHub user
83  	 * @param gitHubRepositoryArg GitHub repository
84  	 * @param mavenArtifactIdArg maven artifact id
85  	 * @param jarWithDependenciesSuffixArg suffix of the full jar including all dependencies
86  	 */
87  	public VersionChecker(final String gitHubUserArg, final String gitHubRepositoryArg, final String mavenArtifactIdArg, final String jarWithDependenciesSuffixArg) {
88  
89  		gitHubUser = gitHubUserArg;
90  		gitHubRepository = gitHubRepositoryArg;
91  
92  		mavenArtifactId = mavenArtifactIdArg;
93  		jarWithDependenciesSuffix = jarWithDependenciesSuffixArg;
94  	}
95  
96  	/**
97  	 * Background thread launched to check the version on GitHub.
98  	 */
99  	@Override
100 	public void run() {
101 
102 		LOG.entry();
103 
104 		final String currentJar = currentJar();
105 
106 		if (null == currentJar) {
107 			LOG.exit();
108 			return;
109 		}
110 
111 		try {
112 
113 			final GitHubClient gitHubClient = new GitHubClient();
114 
115 			final RepositoryService rs = new RepositoryService(gitHubClient);
116 			final Repository repository = rs.getRepository(gitHubUser, gitHubRepository);
117 
118 			final String currentVersion = 'v' + currentJar.replaceFirst("^" + mavenArtifactId + '-', "").replaceFirst(jarWithDependenciesSuffix + ".*$", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
119 			RepositoryTag recentRelease = null;
120 			for (final RepositoryTag tag : rs.getTags(repository)) {
121 
122 				if (null != recentRelease && tag.getName().compareTo(recentRelease.getName()) > 0) {
123 					recentRelease = tag;
124 				}
125 				else if (tag.getName().compareTo(currentVersion) > 0) {
126 					recentRelease = tag;
127 				}
128 			}
129 
130 			if (null == recentRelease) {
131 				LOG.exit();
132 				return;
133 			}
134 
135 			if (!findAndDownloadReleaseJar(recentRelease, true)) {
136 				findAndDownloadReleaseJar(recentRelease, false);
137 			}
138 
139 			LOG.exit();
140 		}
141 		catch (final Exception e) {
142 
143 			// Ignore any error during update process
144 			// Just delete the temporary file
145 			cleanOldAndTemporaryJar();
146 			LOG.exit(e);
147 		}
148 	}
149 
150 	/**
151 	 * Find a jar in release and if it is a newer version, ask to download it.
152 	 * 
153 	 * @param release GitHub last release to use
154 	 * @param withDependenciesSuffix indicate if the jar to download have a dependencies suffix
155 	 * @return true if a new release jar has been found
156 	 * @throws IOException in case of reading error
157 	 */
158 	private boolean findAndDownloadReleaseJar(final RepositoryTag release, final boolean withDependenciesSuffix) throws IOException {
159 
160 		LOG.entry(release, withDependenciesSuffix);
161 
162 		final String jarSuffix;
163 		if (withDependenciesSuffix) {
164 			jarSuffix = jarWithDependenciesSuffix;
165 		}
166 		else {
167 			jarSuffix = ""; //$NON-NLS-1$
168 		}
169 		try (final InputStream remoteJarInputStream = new URL(release.getZipballUrl()).openStream()) {
170 
171 			final ZipInputStream zis = new ZipInputStream(remoteJarInputStream);
172 
173 			ZipEntry entry = zis.getNextEntry();
174 
175 			while (null != entry) {
176 
177 				if (entry.getName().matches(".*/" + TARGET_DIRECTORY + '/' + mavenArtifactId + "-[0-9\\.]*" + jarSuffix + JAR_EXTENSION)) { //$NON-NLS-1$ //$NON-NLS-2$
178 
179 					final Object[] options = { "Yes", "No", "Never ask me again" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
180 					final int confirmDialogChoice = JOptionPane.showOptionDialog(null, "An update is available, download it? (" + entry.getCompressedSize() / ONE_MEGABYTE + "MB)", "Rundeck Monitor update found!", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
181 					if (JOptionPane.YES_OPTION == confirmDialogChoice) {
182 
183 						final String jarFileBaseName = entry.getName().replaceFirst("^.*/", ""); //$NON-NLS-1$ //$NON-NLS-2$
184 
185 						downloadFile(zis, jarFileBaseName + TMP_EXTENSION);
186 						Files.move(Paths.get(jarFileBaseName + TMP_EXTENSION), Paths.get(jarFileBaseName));
187 
188 						downloadedJar = jarFileBaseName;
189 						downloadDone = true;
190 					}
191 					else if (JOptionPane.CANCEL_OPTION == confirmDialogChoice) {
192 
193 						versionCheckerDisabled = true;
194 					}
195 
196 					LOG.exit(true);
197 					return true;
198 				}
199 
200 				entry = zis.getNextEntry();
201 			}
202 		}
203 
204 		LOG.exit(false);
205 		return false;
206 	}
207 
208 	/**
209 	 * Restart the RunDeck monitor and use the newer jar file.
210 	 * 
211 	 * @return true if a java file has been launched on the new jar file
212 	 */
213 	public boolean restart() {
214 
215 		LOG.entry();
216 
217 		if (Files.exists(Paths.get(downloadedJar))) {
218 
219 			String javaExecutable = null;
220 			try {
221 
222 				javaExecutable = getJavaExecutable();
223 				final ProcessBuilder processBuilder = new ProcessBuilder(javaExecutable, JAR_ARGUMENT, downloadedJar);
224 				processBuilder.start();
225 
226 				LOG.exit(true);
227 				return true;
228 			}
229 			catch (final IOException e) {
230 
231 				// Ignore any error during restart process
232 				LOG.error("Error during restarting process {} with arguments: {} {}", javaExecutable, JAR_ARGUMENT, downloadedJar, e); //$NON-NLS-1$
233 			}
234 		}
235 
236 		LOG.exit(false);
237 		return false;
238 	}
239 
240 	/**
241 	 * Clean the old jar and any existing temporary file.
242 	 */
243 	public void cleanOldAndTemporaryJar() {
244 
245 		LOG.entry();
246 
247 		final String currentJar = currentJar();
248 
249 		try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(Paths.get("."))) { //$NON-NLS-1$
250 
251 			for (final Path path : directoryStream) {
252 
253 				final String fileName = path.getFileName().toString();
254 				if (fileName.startsWith(mavenArtifactId)) {
255 
256 					if (fileName.endsWith(JAR_EXTENSION) && null != currentJar && currentJar.compareTo(fileName) > 0) {
257 
258 						deleteJar(path);
259 					}
260 					else if (fileName.endsWith(JAR_EXTENSION + TMP_EXTENSION)) {
261 
262 						deleteJar(path);
263 					}
264 				}
265 			}
266 
267 			LOG.exit();
268 		}
269 		catch (final IOException e) {
270 			// Ignore any error during the delete process
271 			LOG.exit(e);
272 		}
273 	}
274 
275 	/**
276 	 * Delete a jar file.
277 	 * 
278 	 * @param jarFileToDelete file to delete
279 	 */
280 	private static void deleteJar(final Path jarFileToDelete) {
281 
282 		LOG.entry();
283 
284 		if (Files.exists(jarFileToDelete)) {
285 
286 			try {
287 				Files.delete(jarFileToDelete);
288 				LOG.exit();
289 			}
290 			catch (final IOException e) {
291 
292 				// Ignore any error during the delete process
293 				LOG.exit(e);
294 			}
295 		}
296 	}
297 
298 	/**
299 	 * Return the current executed jar.
300 	 * 
301 	 * @return the name of the current executed jar
302 	 */
303 	private String currentJar() {
304 
305 		LOG.entry();
306 
307 		String currentJar = null;
308 		try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(Paths.get("."))) { //$NON-NLS-1$
309 
310 			for (final Path path : directoryStream) {
311 
312 				final String fileName = path.getFileName().toString();
313 				if (fileName.startsWith(mavenArtifactId) && fileName.endsWith(JAR_EXTENSION) && (null == currentJar || currentJar.compareTo(fileName) < 0)) {
314 
315 					currentJar = fileName;
316 				}
317 			}
318 		}
319 		catch (final IOException e) {
320 			// Ignore any error during the process
321 			currentJar = null;
322 		}
323 
324 		LOG.exit(currentJar);
325 		return currentJar;
326 	}
327 
328 	/**
329 	 * Get the download status.
330 	 * 
331 	 * @return true if the download has been done
332 	 */
333 	public boolean isDownloadDone() {
334 
335 		return downloadDone;
336 	}
337 
338 	/**
339 	 * Enable the version checker.
340 	 */
341 	public void resetVersionCheckerDisabled() {
342 
343 		versionCheckerDisabled = false;
344 	}
345 
346 	/**
347 	 * Get the state of the version checker. True, if the user have disabled it.
348 	 * 
349 	 * @return true if the version checker is disabled
350 	 */
351 	public boolean isversionCheckerDisabled() {
352 
353 		return versionCheckerDisabled;
354 	}
355 
356 	/**
357 	 * Download a file/URL and write it to a destination file.
358 	 * 
359 	 * @param inputStream source stream
360 	 * @param destinationFile destination file
361 	 * @throws IOException in case of copy error
362 	 */
363 	private static void downloadFile(final InputStream inputStream, final String destinationFile) throws IOException {
364 
365 		Files.copy(inputStream, Paths.get(destinationFile));
366 	}
367 
368 	/**
369 	 * Get the java executable.
370 	 * 
371 	 * @return absolte path to the java executable
372 	 * @throws NoSuchFileException if the java executable is not found
373 	 */
374 	private static String getJavaExecutable() throws NoSuchFileException {
375 
376 		LOG.entry();
377 
378 		final String javaDirectory = System.getProperty(JAVA_HOME_PROPERTY);
379 
380 		if (javaDirectory == null) {
381 			throw new IllegalStateException(JAVA_HOME_PROPERTY);
382 		}
383 
384 		final String javaExecutableFilePath;
385 		// Add .exe extension on Windows OS
386 		if (EnvironmentTools.isWindows()) {
387 			javaExecutableFilePath = javaDirectory + FileSystems.getDefault().getSeparator() + BIN_DIRECTORY_AND_JAVA + WINDOWS_EXE_EXTENSION;
388 		}
389 		else {
390 			javaExecutableFilePath = javaDirectory + FileSystems.getDefault().getSeparator() + BIN_DIRECTORY_AND_JAVA;
391 		}
392 
393 		// Check if the executable exists and is executable
394 		final Path javaExecutablePath = Paths.get(javaExecutableFilePath);
395 		if (!Files.exists(javaExecutablePath) || !Files.isExecutable(javaExecutablePath)) {
396 
397 			final NoSuchFileException exception = new NoSuchFileException(javaExecutableFilePath);
398 			LOG.exit(exception);
399 			throw exception;
400 		}
401 
402 		LOG.exit(javaExecutableFilePath);
403 		return javaExecutableFilePath;
404 	}
405 }