1 /*******************************************************************************
2  *  Copyright (c) 2005, 2017 IBM Corporation and others.
3  *
4  *  This program and the accompanying materials
5  *  are made available under the terms of the Eclipse Public License 2.0
6  *  which accompanies this distribution, and is available at
7  *  https://www.eclipse.org/legal/epl-2.0/
8  *
9  *  SPDX-License-Identifier: EPL-2.0
10  *
11  *  Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     G&H Softwareentwicklung GmbH - internationalization implementation (bug 150933)
14  *     Michael Seele -  remove offline-allowed  (bug 153403)
15  *******************************************************************************/
16 
17 package org.eclipse.pde.internal.build.tasks;
18 
19 import java.io.*;
20 import java.util.*;
21 import java.util.zip.ZipEntry;
22 import java.util.zip.ZipFile;
23 import javax.xml.parsers.*;
24 import org.xml.sax.*;
25 import org.xml.sax.helpers.DefaultHandler;
26 
27 /**
28  *
29  * @since 3.1
30  */
31 public class JNLPGenerator extends DefaultHandler {
32 
33 	private SAXParser parser;
34 	private final File featureRoot;
35 
36 	private final String codebase;
37 	private final String j2se;
38 
39 	/**
40 	 * id = ???
41 	 * version = jnlp.version
42 	 * label = information.title
43 	 * provider-name = information.vendor
44 	 * image = information.icon
45 	 * feature.description = information.description
46 	 * feature.includes = extension
47 	 * feature.plugin = jar
48 	 */
49 	private final static SAXParserFactory parserFactory = SAXParserFactory.newInstance();
50 	private PrintWriter out;
51 	private String destination;
52 	private String provider;
53 	private String label;
54 	private String version;
55 	private String id;
56 	private String description;
57 	private boolean resourceWritten = false;
58 	private String currentOS = null;
59 	private String currentArch = null;
60 	private Locale locale = null;
61 	private PropertyResourceBundle nlsBundle = null;
62 	private final boolean generateOfflineAllowed;
63 	private Config[] configs;
64 
65 	/**
66 	 * For testing purposes only.
67 	 */
main(String[] args)68 	public static void main(String[] args) {
69 		JNLPGenerator generator = new JNLPGenerator(args[0], args[1], args[2], args[3]);
70 		generator.process();
71 	}
72 
73 	/**
74 	 * Constructs a feature parser.
75 	 */
JNLPGenerator(String feature, String destination, String codebase, String j2se)76 	public JNLPGenerator(String feature, String destination, String codebase, String j2se) {
77 		this(feature, destination, codebase, j2se, Locale.getDefault(), true, null);
78 	}
79 
80 	/**
81 	 * Constructs a feature parser.
82 	 */
JNLPGenerator(String feature, String destination, String codebase, String j2se, Locale locale, boolean generateOfflineAllowed, String configs)83 	public JNLPGenerator(String feature, String destination, String codebase, String j2se, Locale locale, boolean generateOfflineAllowed, String configs) {
84 		super();
85 		this.featureRoot = new File(feature);
86 		this.destination = destination;
87 		this.codebase = codebase;
88 		this.j2se = j2se;
89 		this.locale = locale;
90 		this.generateOfflineAllowed = generateOfflineAllowed;
91 		try {
92 			parserFactory.setNamespaceAware(true);
93 			parser = parserFactory.newSAXParser();
94 		} catch (ParserConfigurationException e) {
95 			System.out.println(e);
96 		} catch (SAXException e) {
97 			System.out.println(e);
98 		}
99 		setConfigInfo(configs);
100 	}
101 
102 	/**
103 	 * Parses the specified url and constructs a feature
104 	 */
process()105 	public void process() {
106 		InputStream in = null;
107 		final String FEATURE_XML = "feature.xml"; //$NON-NLS-1$
108 
109 		try {
110 			ZipFile featureArchive = null;
111 			InputStream nlsStream = null;
112 			if (featureRoot.isFile()) {
113 				featureArchive = new ZipFile(featureRoot);
114 				nlsStream = getNLSStream(featureArchive);
115 				ZipEntry featureXML = featureArchive.getEntry(FEATURE_XML);
116 				in = featureArchive.getInputStream(featureXML);
117 			} else {
118 				nlsStream = getNLSStream(this.featureRoot);
119 				in = new BufferedInputStream(new FileInputStream(new File(featureRoot, FEATURE_XML)));
120 			}
121 			try {
122 				if (nlsStream != null) {
123 					nlsBundle = new PropertyResourceBundle(nlsStream);
124 					nlsStream.close();
125 				}
126 			} catch (IOException e) {
127 				// do nothing
128 			}
129 			try {
130 				parser.parse(new InputSource(in), this);
131 				writeResourceEpilogue();
132 				writeEpilogue();
133 			} catch (SAXException e) {
134 				//Ignore the exception
135 			} finally {
136 				in.close();
137 				if (out != null)
138 					out.close();
139 				if (featureArchive != null)
140 					featureArchive.close();
141 			}
142 		} catch (IOException e) {
143 			//Ignore the exception
144 		}
145 	}
146 
147 	/**
148 	 * Search for nls properties files and return the stream if files are found.
149 	 * First try to load the default properties file, then one with the default
150 	 * locale settings and if nothing matches, return the stream of the first
151 	 * properties file found.
152 	 */
getNLSStream(File root)153 	private InputStream getNLSStream(File root) {
154 		String appendix = ".properties"; //$NON-NLS-1$
155 		String[] potentials = createNLSPotentials();
156 
157 		Map<String, File> validEntries = new HashMap<>();
158 		File[] files = root.listFiles();
159 		for (int i = 0; i < files.length; i++) {
160 			String filename = files[i].getName();
161 			if (filename.endsWith(appendix)) {
162 				validEntries.put(filename, files[i]);
163 			}
164 		}
165 		InputStream stream = null;
166 		if (validEntries.size() > 0) {
167 			for (int i = 0; i < potentials.length; i++) {
168 				File file = validEntries.get(potentials[i]);
169 				if (file != null) {
170 					try {
171 						stream = new BufferedInputStream(new FileInputStream(file));
172 						break;
173 					} catch (IOException e) {
174 						// do nothing
175 					}
176 				}
177 			}
178 			if (stream == null) {
179 				File file = validEntries.values().iterator().next();
180 				try {
181 					stream = new BufferedInputStream(new FileInputStream(file));
182 				} catch (IOException e) {
183 					// do nothing
184 				}
185 			}
186 		}
187 		return stream;
188 	}
189 
190 	/**
191 	 * Search for nls properties files and return the stream if files are found.
192 	 * First try to load the default properties file, then one with the default
193 	 * locale settings and if nothing matches, return the stream of the first
194 	 * founded properties file.
195 	 */
getNLSStream(ZipFile featureArchive)196 	private InputStream getNLSStream(ZipFile featureArchive) {
197 		String appendix = ".properties"; //$NON-NLS-1$
198 		String[] potentials = createNLSPotentials();
199 
200 		Map<String, ZipEntry> validEntries = new HashMap<>();
201 		for (Enumeration<? extends ZipEntry> enumeration = featureArchive.entries(); enumeration.hasMoreElements();) {
202 			ZipEntry entry = enumeration.nextElement();
203 			String entryName = entry.getName();
204 			if (entryName.endsWith(appendix)) {
205 				validEntries.put(entryName, entry);
206 			}
207 		}
208 		InputStream stream = null;
209 		if (validEntries.size() > 0) {
210 			for (int i = 0; i < potentials.length; i++) {
211 				ZipEntry entry = validEntries.get(potentials[i]);
212 				if (entry != null) {
213 					try {
214 						stream = featureArchive.getInputStream(entry);
215 						break;
216 					} catch (IOException e) {
217 						// do nothing
218 					}
219 				}
220 			}
221 			if (stream == null) {
222 				ZipEntry entry = validEntries.values().iterator().next();
223 				try {
224 					stream = featureArchive.getInputStream(entry);
225 				} catch (IOException e) {
226 					// do nothing
227 				}
228 			}
229 		}
230 		return stream;
231 	}
232 
createNLSPotentials()233 	private String[] createNLSPotentials() {
234 		String suffix = "feature"; //$NON-NLS-1$
235 		String appendix = ".properties"; //$NON-NLS-1$
236 
237 		String language = locale.getLanguage();
238 		String country = locale.getCountry();
239 		String variant = locale.getVariant();
240 
241 		String potential1 = '_' + language + '_' + country + '_' + variant;
242 		String potential2 = '_' + language + '_' + country;
243 		String potential3 = '_' + language;
244 		String potential4 = ""; //$NON-NLS-1$
245 
246 		String[] potentials = new String[] {potential1, potential2, potential3, potential4};
247 		for (int i = 0; i < potentials.length; i++) {
248 			potentials[i] = suffix + potentials[i] + appendix;
249 		}
250 		return potentials;
251 	}
252 
253 	@Override
startElement(String uri, String localName, String qName, Attributes attributes)254 	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
255 		try {
256 			if ("feature".equals(localName)) { //$NON-NLS-1$
257 				processFeature(attributes);
258 			} else if ("includes".equals(localName)) { //$NON-NLS-1$
259 				processIncludes(attributes);
260 			} else if ("description".equals(localName)) { //$NON-NLS-1$
261 				processDescription(attributes);
262 			} else if ("plugin".equals(localName)) { //$NON-NLS-1$
263 				processPlugin(attributes);
264 			}
265 		} catch (IOException e) {
266 			throw new SAXException(e);
267 		}
268 	}
269 
processPlugin(Attributes attributes)270 	private void processPlugin(Attributes attributes) throws IOException {
271 		writePrologue();
272 		String pluginId = attributes.getValue("id"); //$NON-NLS-1$
273 		String pluginVersion = attributes.getValue("version"); //$NON-NLS-1$
274 		String os = attributes.getValue("os"); //$NON-NLS-1$
275 		String ws = attributes.getValue("ws"); //$NON-NLS-1$
276 		String arch = attributes.getValue("arch"); //$NON-NLS-1$
277 		if (isValidEnvironment(os, ws, arch)) {
278 			writeResourcePrologue(os, ws, arch);
279 			out.println("\t\t<jar href=\"plugins/" + pluginId + "_" + pluginVersion + ".jar\"/>"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
280 		}
281 	}
282 
writeResourceEpilogue()283 	private void writeResourceEpilogue() {
284 		if (!resourceWritten)
285 			return;
286 		out.println("\t</resources>"); //$NON-NLS-1$
287 		resourceWritten = false;
288 		currentOS = null;
289 	}
290 
writeResourcePrologue(String os, String ws, String arch)291 	private void writeResourcePrologue(String os, String ws, String arch) {
292 		if (os == null)
293 			os = ws;
294 		os = convertOS(os);
295 		arch = convertArch(arch);
296 		if (resourceWritten && osMatch(os) && archMatch(arch))
297 			return;
298 		if (resourceWritten)
299 			writeResourceEpilogue();
300 		out.println("\t<resources" + (os == null ? "" : " os=\"" + os + "\"") + (arch == null ? "" : " arch=\"" + arch + "\"") + ">"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$//$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$//$NON-NLS-7$ //$NON-NLS-8$
301 		resourceWritten = true;
302 		currentOS = os;
303 		currentArch = arch;
304 	}
305 
convertOS(String os)306 	private String convertOS(String os) {
307 		if (os == null)
308 			return null;
309 		if ("freebsd".equalsIgnoreCase(os)) //$NON-NLS-1$
310 			return "FreeBSD"; //$NON-NLS-1$
311 		if ("win32".equalsIgnoreCase(os)) //$NON-NLS-1$
312 			return "Windows"; //$NON-NLS-1$
313 		if ("macosx".equalsIgnoreCase(os)) //$NON-NLS-1$
314 			return "Mac"; //$NON-NLS-1$
315 		if ("linux".equalsIgnoreCase(os)) //$NON-NLS-1$
316 			return "Linux"; //$NON-NLS-1$
317 		if ("solaris".equalsIgnoreCase(os)) //$NON-NLS-1$
318 			return "Solaris"; //$NON-NLS-1$
319 		if ("hpux".equalsIgnoreCase(os)) //$NON-NLS-1$
320 			return "HP-UX"; //$NON-NLS-1$
321 		if ("aix".equalsIgnoreCase(os)) //$NON-NLS-1$
322 			return "AIX"; //$NON-NLS-1$
323 		return os;
324 	}
325 
osMatch(String os)326 	private boolean osMatch(String os) {
327 		if (os == currentOS)
328 			return true;
329 		if (os == null)
330 			return false;
331 		return os.equals(currentOS);
332 	}
333 
convertArch(String arch)334 	private String convertArch(String arch) {
335 		if (arch == null)
336 			return null;
337 
338 		if ("x86_64".equals(arch))//$NON-NLS-1$
339 			return "x86_64"; //$NON-NLS-1$
340 
341 		return arch;
342 	}
343 
archMatch(String arch)344 	private boolean archMatch(String arch) {
345 		if (arch == currentOS)
346 			return true;
347 		if (arch == null)
348 			return false;
349 		return arch.equals(currentArch);
350 	}
351 
processDescription(Attributes attributes)352 	private void processDescription(Attributes attributes) {
353 		// ignoring for now
354 	}
355 
processIncludes(Attributes attributes)356 	private void processIncludes(Attributes attributes) throws IOException {
357 		writePrologue();
358 		String inclusionId = attributes.getValue("id"); //$NON-NLS-1$
359 		String inclusionVersion = attributes.getValue("version"); //$NON-NLS-1$
360 		String name = attributes.getValue("name"); //$NON-NLS-1$
361 		String os = attributes.getValue("os"); //$NON-NLS-1$
362 		String ws = attributes.getValue("ws"); //$NON-NLS-1$
363 		String arch = attributes.getValue("arch"); //$NON-NLS-1$
364 		if (isValidEnvironment(os, ws, arch)) {
365 			writeResourcePrologue(os, ws, arch);
366 			out.print("\t\t<extension ");//$NON-NLS-1$
367 			if (name != null)
368 				out.print("name=\"" + name + "\" "); //$NON-NLS-1$ //$NON-NLS-2$
369 			if (inclusionId != null) {
370 				out.print("href=\"features/" + inclusionId); //$NON-NLS-1$
371 				if (inclusionVersion != null)
372 					out.print('_' + inclusionVersion);
373 				out.print(".jnlp\" "); //$NON-NLS-1$
374 			}
375 			out.println("/>"); //$NON-NLS-1$
376 		}
377 	}
378 
processFeature(Attributes attributes)379 	private void processFeature(Attributes attributes) {
380 		id = attributes.getValue("id"); //$NON-NLS-1$
381 		version = attributes.getValue("version"); //$NON-NLS-1$
382 		label = processNLS(attributes.getValue("label")); //$NON-NLS-1$
383 		provider = processNLS(attributes.getValue("provider-name")); //$NON-NLS-1$
384 	}
385 
386 	/**
387 	 * Search for a human readable string in the feature.properties file(s) if
388 	 * the given string is a translateable key.
389 	 *
390 	 * @param string a translateable key or a normal string(nothing is done)
391 	 *
392 	 * @return a translateabled string or the given string if it is not a
393 	 *         translateable key
394 	 */
processNLS(String string)395 	private String processNLS(String string) {
396 		if (string == null)
397 			return null;
398 		string = string.trim();
399 		if (!string.startsWith("%")) { //$NON-NLS-1$
400 			return string;
401 		}
402 		if (string.startsWith("%%")) { //$NON-NLS-1$
403 			return string.substring(1);
404 		}
405 		int index = string.indexOf(" "); //$NON-NLS-1$
406 		String key = index == -1 ? string : string.substring(0, index);
407 		String dflt = index == -1 ? string : string.substring(index + 1);
408 		if (nlsBundle == null) {
409 			return dflt;
410 		}
411 		try {
412 			return nlsBundle.getString(key.substring(1));
413 		} catch (MissingResourceException e) {
414 			return dflt;
415 		}
416 	}
417 
writePrologue()418 	private void writePrologue() throws IOException {
419 		if (out != null)
420 			return;
421 		if (destination == null) {
422 			featureRoot.getParentFile();
423 			destination = featureRoot.getParent() + '/';
424 		}
425 		if (destination.endsWith("/") || destination.endsWith("\\")) //$NON-NLS-1$  //$NON-NLS-2$
426 			destination = new File(featureRoot.getParentFile(), id + "_" + version + ".jnlp").getAbsolutePath(); //$NON-NLS-1$ //$NON-NLS-2$
427 		out = new PrintWriter(new BufferedOutputStream(new FileOutputStream(destination)));
428 		writePrologue();
429 		out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
430 		out.print("<jnlp spec=\"1.0+\" "); //$NON-NLS-1$
431 		if (codebase != null)
432 			out.print("codebase=\"" + codebase); //$NON-NLS-1$
433 		out.println("\">"); //$NON-NLS-1$
434 		out.println("\t<information>"); //$NON-NLS-1$
435 		if (label != null)
436 			out.println("\t\t<title>" + label + "</title>"); //$NON-NLS-1$ //$NON-NLS-2$
437 		if (provider != null)
438 			out.println("\t\t<vendor>" + provider + "</vendor>"); //$NON-NLS-1$ //$NON-NLS-2$
439 		if (description != null)
440 			out.println("\t\t<description>" + description + "</description>"); //$NON-NLS-1$ //$NON-NLS-2$
441 		if (generateOfflineAllowed)
442 			out.println("\t\t<offline-allowed/>"); //$NON-NLS-1$
443 		out.println("\t</information>"); //$NON-NLS-1$
444 		out.println("\t<security>"); //$NON-NLS-1$
445 		out.println("\t\t<all-permissions/>"); //$NON-NLS-1$
446 		out.println("\t</security>"); //$NON-NLS-1$
447 		out.println("\t<component-desc/>"); //$NON-NLS-1$
448 		out.println("\t<resources>"); //$NON-NLS-1$
449 		out.println("\t\t<j2se version=\"" + j2se + "\" />"); //$NON-NLS-1$ //$NON-NLS-2$
450 		out.println("\t</resources>"); //$NON-NLS-1$
451 	}
452 
writeEpilogue()453 	private void writeEpilogue() {
454 		out.println("</jnlp>"); //$NON-NLS-1$
455 	}
456 
isMatching(String candidateValues, String siteValues)457 	private boolean isMatching(String candidateValues, String siteValues) {
458 		if (candidateValues == null)
459 			return true;
460 		if (siteValues == null)
461 			return false;
462 		if ("*".equals(candidateValues)) //$NON-NLS-1$
463 			return true;
464 		if ("".equals(candidateValues)) //$NON-NLS-1$
465 			return true;
466 		StringTokenizer siteTokens = new StringTokenizer(siteValues, ","); //$NON-NLS-1$
467 		//$NON-NLS-1$
468 		while (siteTokens.hasMoreTokens()) {
469 			StringTokenizer candidateTokens = new StringTokenizer(candidateValues, ","); //$NON-NLS-1$
470 			String siteValue = siteTokens.nextToken();
471 			while (candidateTokens.hasMoreTokens()) {
472 				if (siteValue.equalsIgnoreCase(candidateTokens.nextToken()))
473 					return true;
474 			}
475 		}
476 		return false;
477 	}
478 
isValidEnvironment(String os, String ws, String arch)479 	private boolean isValidEnvironment(String os, String ws, String arch) {
480 		if (configs.length == 0)
481 			return true;
482 		for (int i = 0; i < configs.length; i++) {
483 			if (isMatching(os, configs[i].getOs()) && isMatching(ws, configs[i].getWs()) && isMatching(arch, configs[i].getArch()))
484 				return true;
485 		}
486 		return false;
487 	}
488 
setConfigInfo(String spec)489 	private void setConfigInfo(String spec) {
490 		if (spec != null && spec.startsWith("$")) { //$NON-NLS-1$
491 			configs = new Config[0];
492 			return;
493 		}
494 		if (spec == null) {
495 			configs = new Config[] {Config.genericConfig()};
496 			return;
497 		}
498 		StringTokenizer tokens = new StringTokenizer(spec, "&"); //$NON-NLS-1$
499 		int configNbr = tokens.countTokens();
500 		ArrayList<Config> configInfos = new ArrayList<>(configNbr);
501 		while (tokens.hasMoreElements()) {
502 			String aConfig = tokens.nextToken();
503 			StringTokenizer configTokens = new StringTokenizer(aConfig, ","); //$NON-NLS-1$
504 			if (configTokens.countTokens() == 3) {
505 				Config toAdd = new Config(configTokens.nextToken().trim(), configTokens.nextToken().trim(), configTokens.nextToken().trim());
506 				if (toAdd.equals(Config.genericConfig()))
507 					toAdd = Config.genericConfig();
508 				configInfos.add(toAdd);
509 			}
510 		}
511 		if (configInfos.size() == 0)
512 			configInfos.add(Config.genericConfig());
513 		configs = configInfos.toArray(new Config[configInfos.size()]);
514 	}
515 }
516