source: josm/trunk/src/org/openstreetmap/josm/plugins/PluginInformation.java@ 7509

Last change on this file since 7509 was 7294, checked in by Don-vip, 10 years ago

fix #10242 - catch NoClassDefFoundError when ClassNotFoundException is catched

  • Property svn:eol-style set to native
File size: 20.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.plugins;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Image;
7import java.awt.image.BufferedImage;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.IOException;
11import java.io.InputStream;
12import java.lang.reflect.Constructor;
13import java.lang.reflect.InvocationTargetException;
14import java.net.MalformedURLException;
15import java.net.URL;
16import java.text.MessageFormat;
17import java.util.ArrayList;
18import java.util.Collection;
19import java.util.LinkedList;
20import java.util.List;
21import java.util.Map;
22import java.util.TreeMap;
23import java.util.jar.Attributes;
24import java.util.jar.JarInputStream;
25import java.util.jar.Manifest;
26
27import javax.swing.ImageIcon;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.data.Version;
31import org.openstreetmap.josm.tools.ImageProvider;
32import org.openstreetmap.josm.tools.LanguageInfo;
33import org.openstreetmap.josm.tools.Utils;
34
35/**
36 * Encapsulate general information about a plugin. This information is available
37 * without the need of loading any class from the plugin jar file.
38 *
39 * @author imi
40 */
41public class PluginInformation {
42
43 /** The plugin jar file. */
44 public File file = null;
45 /** The plugin name. */
46 public String name = null;
47 /** The lowest JOSM version required by this plugin (from plugin list). **/
48 public int mainversion = 0;
49 /** The lowest JOSM version required by this plugin (from locally available jar). **/
50 public int localmainversion = 0;
51 /** The plugin class name. */
52 public String className = null;
53 public boolean oldmode = false;
54 /** The list of required plugins, separated by ';' (from plugin list). */
55 public String requires = null;
56 /** The list of required plugins, separated by ';' (from locally available jar). */
57 public String localrequires = null;
58 /** The plugin link (for documentation). */
59 public String link = null;
60 /** The plugin description. */
61 public String description = null;
62 /** Determines if the plugin must be loaded early or not. */
63 public boolean early = false;
64 /** The plugin author. */
65 public String author = null;
66 /** The plugin stage, determining the loading sequence order of plugins. */
67 public int stage = 50;
68 /** The plugin version (from plugin list). **/
69 public String version = null;
70 /** The plugin version (from locally available jar). **/
71 public String localversion = null;
72 /** The plugin download link. */
73 public String downloadlink = null;
74 public String iconPath;
75 /** The plugin icon. */
76 public ImageIcon icon;
77 public List<URL> libraries = new LinkedList<>();
78 public final Map<String, String> attr = new TreeMap<>();
79
80 private static final ImageIcon emptyIcon = new ImageIcon(new BufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB));
81
82 /**
83 * Creates a plugin information object by reading the plugin information from
84 * the manifest in the plugin jar.
85 *
86 * The plugin name is derived from the file name.
87 *
88 * @param file the plugin jar file
89 * @throws PluginException if reading the manifest fails
90 */
91 public PluginInformation(File file) throws PluginException{
92 this(file, file.getName().substring(0, file.getName().length()-4));
93 }
94
95 /**
96 * Creates a plugin information object for the plugin with name {@code name}.
97 * Information about the plugin is extracted from the manifest file in the plugin jar
98 * {@code file}.
99 * @param file the plugin jar
100 * @param name the plugin name
101 * @throws PluginException thrown if reading the manifest file fails
102 */
103 public PluginInformation(File file, String name) throws PluginException {
104 if (!PluginHandler.isValidJar(file)) {
105 throw new PluginException(name, tr("Invalid jar file ''{0}''", file));
106 }
107 this.name = name;
108 this.file = file;
109 try (
110 FileInputStream fis = new FileInputStream(file);
111 JarInputStream jar = new JarInputStream(fis)
112 ) {
113 Manifest manifest = jar.getManifest();
114 if (manifest == null)
115 throw new PluginException(name, tr("The plugin file ''{0}'' does not include a Manifest.", file.toString()));
116 scanManifest(manifest, false);
117 libraries.add(0, Utils.fileToURL(file));
118 } catch (IOException e) {
119 throw new PluginException(name, e);
120 }
121 }
122
123 /**
124 * Creates a plugin information object by reading plugin information in Manifest format
125 * from the input stream {@code manifestStream}.
126 *
127 * @param manifestStream the stream to read the manifest from
128 * @param name the plugin name
129 * @param url the download URL for the plugin
130 * @throws PluginException thrown if the plugin information can't be read from the input stream
131 */
132 public PluginInformation(InputStream manifestStream, String name, String url) throws PluginException {
133 this.name = name;
134 try {
135 Manifest manifest = new Manifest();
136 manifest.read(manifestStream);
137 if(url != null) {
138 downloadlink = url;
139 }
140 scanManifest(manifest, url != null);
141 } catch (IOException e) {
142 throw new PluginException(name, e);
143 }
144 }
145
146 /**
147 * Updates the plugin information of this plugin information object with the
148 * plugin information in a plugin information object retrieved from a plugin
149 * update site.
150 *
151 * @param other the plugin information object retrieved from the update site
152 */
153 public void updateFromPluginSite(PluginInformation other) {
154 this.mainversion = other.mainversion;
155 this.className = other.className;
156 this.requires = other.requires;
157 this.link = other.link;
158 this.description = other.description;
159 this.early = other.early;
160 this.author = other.author;
161 this.stage = other.stage;
162 this.version = other.version;
163 this.downloadlink = other.downloadlink;
164 this.icon = other.icon;
165 this.iconPath = other.iconPath;
166 this.libraries = other.libraries;
167 this.attr.clear();
168 this.attr.putAll(other.attr);
169 }
170
171 /**
172 * Updates the plugin information of this plugin information object with the
173 * plugin information in a plugin information object retrieved from a plugin
174 * jar.
175 *
176 * @param other the plugin information object retrieved from the jar file
177 * @since 5601
178 */
179 public void updateFromJar(PluginInformation other) {
180 updateLocalInfo(other);
181 if (other.icon != null) {
182 this.icon = other.icon;
183 }
184 this.early = other.early;
185 this.className = other.className;
186 this.libraries = other.libraries;
187 this.stage = other.stage;
188 }
189
190 private final void scanManifest(Manifest manifest, boolean oldcheck) {
191 String lang = LanguageInfo.getLanguageCodeManifest();
192 Attributes attr = manifest.getMainAttributes();
193 className = attr.getValue("Plugin-Class");
194 String s = attr.getValue(lang+"Plugin-Link");
195 if (s == null) {
196 s = attr.getValue("Plugin-Link");
197 }
198 if (s != null) {
199 try {
200 new URL(s);
201 } catch (MalformedURLException e) {
202 Main.info(tr("Invalid URL ''{0}'' in plugin {1}", s, name));
203 s = null;
204 }
205 }
206 link = s;
207 requires = attr.getValue("Plugin-Requires");
208 s = attr.getValue(lang+"Plugin-Description");
209 if (s == null) {
210 s = attr.getValue("Plugin-Description");
211 if (s != null) {
212 try {
213 s = tr(s);
214 } catch (IllegalArgumentException e) {
215 Main.info(tr("Invalid plugin description ''{0}'' in plugin {1}", s, name));
216 }
217 }
218 } else {
219 s = MessageFormat.format(s, (Object[]) null);
220 }
221 description = s;
222 early = Boolean.parseBoolean(attr.getValue("Plugin-Early"));
223 String stageStr = attr.getValue("Plugin-Stage");
224 stage = stageStr == null ? 50 : Integer.parseInt(stageStr);
225 version = attr.getValue("Plugin-Version");
226 s = attr.getValue("Plugin-Mainversion");
227 if (s != null) {
228 try {
229 mainversion = Integer.parseInt(s);
230 } catch(NumberFormatException e) {
231 Main.warn(tr("Invalid plugin main version ''{0}'' in plugin {1}", s, name));
232 }
233 } else {
234 Main.warn(tr("Missing plugin main version in plugin {0}", name));
235 }
236 author = attr.getValue("Author");
237 iconPath = attr.getValue("Plugin-Icon");
238 if (iconPath != null && file != null) {
239 // extract icon from the plugin jar file
240 icon = new ImageProvider(iconPath).setArchive(file).setMaxWidth(24).setMaxHeight(24).setOptional(true).get();
241 }
242 if (oldcheck && mainversion > Version.getInstance().getVersion()) {
243 int myv = Version.getInstance().getVersion();
244 for (Map.Entry<Object, Object> entry : attr.entrySet()) {
245 try {
246 String key = ((Attributes.Name)entry.getKey()).toString();
247 if (key.endsWith("_Plugin-Url")) {
248 int mv = Integer.parseInt(key.substring(0,key.length()-11));
249 if (mv <= myv && (mv > mainversion || mainversion > myv)) {
250 String v = (String)entry.getValue();
251 int i = v.indexOf(';');
252 if (i > 0) {
253 downloadlink = v.substring(i+1);
254 mainversion = mv;
255 version = v.substring(0,i);
256 oldmode = true;
257 }
258 }
259 }
260 }
261 catch(Exception e) {
262 Main.error(e);
263 }
264 }
265 }
266
267 String classPath = attr.getValue(Attributes.Name.CLASS_PATH);
268 if (classPath != null) {
269 for (String entry : classPath.split(" ")) {
270 File entryFile;
271 if (new File(entry).isAbsolute() || file == null) {
272 entryFile = new File(entry);
273 } else {
274 entryFile = new File(file.getParent(), entry);
275 }
276
277 libraries.add(Utils.fileToURL(entryFile));
278 }
279 }
280 for (Object o : attr.keySet()) {
281 this.attr.put(o.toString(), attr.getValue(o.toString()));
282 }
283 }
284
285 /**
286 * Replies the description as HTML document, including a link to a web page with
287 * more information, provided such a link is available.
288 *
289 * @return the description as HTML document
290 */
291 public String getDescriptionAsHtml() {
292 StringBuilder sb = new StringBuilder();
293 sb.append("<html><body>");
294 sb.append(description == null ? tr("no description available") : description);
295 if (link != null) {
296 sb.append(" <a href=\"").append(link).append("\">").append(tr("More info...")).append("</a>");
297 }
298 if (downloadlink != null && !downloadlink.startsWith("http://svn.openstreetmap.org/applications/editors/josm/dist/")
299 && !downloadlink.startsWith("http://trac.openstreetmap.org/browser/applications/editors/josm/dist/")) {
300 sb.append("<p>&nbsp;</p><p>"+tr("<b>Plugin provided by an external source:</b> {0}", downloadlink)+"</p>");
301 }
302 sb.append("</body></html>");
303 return sb.toString();
304 }
305
306 /**
307 * Loads and instantiates the plugin.
308 *
309 * @param klass the plugin class
310 * @return the instantiated and initialized plugin
311 * @throws PluginException if the plugin cannot be loaded or instanciated
312 */
313 public PluginProxy load(Class<?> klass) throws PluginException {
314 try {
315 Constructor<?> c = klass.getConstructor(PluginInformation.class);
316 Object plugin = c.newInstance(this);
317 return new PluginProxy(plugin, this);
318 } catch(NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
319 throw new PluginException(name, e);
320 }
321 }
322
323 /**
324 * Loads the class of the plugin.
325 *
326 * @param classLoader the class loader to use
327 * @return the loaded class
328 * @throws PluginException if the class cannot be loaded
329 */
330 public Class<?> loadClass(ClassLoader classLoader) throws PluginException {
331 if (className == null)
332 return null;
333 try {
334 return Class.forName(className, true, classLoader);
335 } catch (NoClassDefFoundError | ClassNotFoundException | ClassCastException e) {
336 throw new PluginException(name, e);
337 }
338 }
339
340 /**
341 * Try to find a plugin after some criterias. Extract the plugin-information
342 * from the plugin and return it. The plugin is searched in the following way:
343 *<ol>
344 *<li>first look after an MANIFEST.MF in the package org.openstreetmap.josm.plugins.&lt;plugin name&gt;
345 * (After removing all fancy characters from the plugin name).
346 * If found, the plugin is loaded using the bootstrap classloader.</li>
347 *<li>If not found, look for a jar file in the user specific plugin directory
348 * (~/.josm/plugins/&lt;plugin name&gt;.jar)</li>
349 *<li>If not found and the environment variable JOSM_RESOURCES + "/plugins/" exist, look there.</li>
350 *<li>Try for the java property josm.resources + "/plugins/" (set via java -Djosm.plugins.path=...)</li>
351 *<li>If the environment variable ALLUSERSPROFILE and APPDATA exist, look in
352 * ALLUSERSPROFILE/&lt;the last stuff from APPDATA&gt;/JOSM/plugins.
353 * (*sic* There is no easy way under Windows to get the All User's application
354 * directory)</li>
355 *<li>Finally, look in some typical unix paths:<ul>
356 * <li>/usr/local/share/josm/plugins/</li>
357 * <li>/usr/local/lib/josm/plugins/</li>
358 * <li>/usr/share/josm/plugins/</li>
359 * <li>/usr/lib/josm/plugins/</li></ul></li>
360 *</ol>
361 * If a plugin class or jar file is found earlier in the list but seem not to
362 * be working, an PluginException is thrown rather than continuing the search.
363 * This is so JOSM can detect broken user-provided plugins and do not go silently
364 * ignore them.
365 *
366 * The plugin is not initialized. If the plugin is a .jar file, it is not loaded
367 * (only the manifest is extracted). In the classloader-case, the class is
368 * bootstraped (e.g. static {} - declarations will run. However, nothing else is done.
369 *
370 * @param pluginName The name of the plugin (in all lowercase). E.g. "lang-de"
371 * @return Information about the plugin or <code>null</code>, if the plugin
372 * was nowhere to be found.
373 * @throws PluginException In case of broken plugins.
374 */
375 public static PluginInformation findPlugin(String pluginName) throws PluginException {
376 String name = pluginName;
377 name = name.replaceAll("[-. ]", "");
378 try (InputStream manifestStream = PluginInformation.class.getResourceAsStream("/org/openstreetmap/josm/plugins/"+name+"/MANIFEST.MF")) {
379 if (manifestStream != null) {
380 return new PluginInformation(manifestStream, pluginName, null);
381 }
382 } catch (IOException e) {
383 Main.warn(e);
384 }
385
386 Collection<String> locations = getPluginLocations();
387
388 for (String s : locations) {
389 File pluginFile = new File(s, pluginName + ".jar");
390 if (pluginFile.exists()) {
391 return new PluginInformation(pluginFile);
392 }
393 }
394 return null;
395 }
396
397 /**
398 * Returns all possible plugin locations.
399 * @return all possible plugin locations.
400 */
401 public static Collection<String> getPluginLocations() {
402 Collection<String> locations = Main.pref.getAllPossiblePreferenceDirs();
403 Collection<String> all = new ArrayList<>(locations.size());
404 for (String s : locations) {
405 all.add(s+"plugins");
406 }
407 return all;
408 }
409
410 /**
411 * Replies true if the plugin with the given information is most likely outdated with
412 * respect to the referenceVersion.
413 *
414 * @param referenceVersion the reference version. Can be null if we don't know a
415 * reference version
416 *
417 * @return true, if the plugin needs to be updated; false, otherweise
418 */
419 public boolean isUpdateRequired(String referenceVersion) {
420 if (this.downloadlink == null) return false;
421 if (this.version == null && referenceVersion!= null)
422 return true;
423 if (this.version != null && !this.version.equals(referenceVersion))
424 return true;
425 return false;
426 }
427
428 /**
429 * Replies true if this this plugin should be updated/downloaded because either
430 * it is not available locally (its local version is null) or its local version is
431 * older than the available version on the server.
432 *
433 * @return true if the plugin should be updated
434 */
435 public boolean isUpdateRequired() {
436 if (this.downloadlink == null) return false;
437 if (this.localversion == null) return true;
438 return isUpdateRequired(this.localversion);
439 }
440
441 protected boolean matches(String filter, String value) {
442 if (filter == null) return true;
443 if (value == null) return false;
444 return value.toLowerCase().contains(filter.toLowerCase());
445 }
446
447 /**
448 * Replies true if either the name, the description, or the version match (case insensitive)
449 * one of the words in filter. Replies true if filter is null.
450 *
451 * @param filter the filter expression
452 * @return true if this plugin info matches with the filter
453 */
454 public boolean matches(String filter) {
455 if (filter == null) return true;
456 String[] words = filter.split("\\s+");
457 for (String word: words) {
458 if (matches(word, name)
459 || matches(word, description)
460 || matches(word, version)
461 || matches(word, localversion))
462 return true;
463 }
464 return false;
465 }
466
467 /**
468 * Replies the name of the plugin.
469 * @return The plugin name
470 */
471 public String getName() {
472 return name;
473 }
474
475 /**
476 * Sets the name
477 * @param name
478 */
479 public void setName(String name) {
480 this.name = name;
481 }
482
483 /**
484 * Replies the plugin icon, scaled to 24x24 pixels.
485 * @return the plugin icon, scaled to 24x24 pixels.
486 */
487 public ImageIcon getScaledIcon() {
488 if (icon == null)
489 return emptyIcon;
490 return new ImageIcon(icon.getImage().getScaledInstance(24, 24, Image.SCALE_SMOOTH));
491 }
492
493 @Override
494 public final String toString() {
495 return getName();
496 }
497
498 private static List<String> getRequiredPlugins(String pluginList) {
499 List<String> requiredPlugins = new ArrayList<>();
500 if (pluginList != null) {
501 for (String s : pluginList.split(";")) {
502 String plugin = s.trim();
503 if (!plugin.isEmpty()) {
504 requiredPlugins.add(plugin);
505 }
506 }
507 }
508 return requiredPlugins;
509 }
510
511 /**
512 * Replies the list of plugins required by the up-to-date version of this plugin.
513 * @return List of plugins required. Empty if no plugin is required.
514 * @since 5601
515 */
516 public List<String> getRequiredPlugins() {
517 return getRequiredPlugins(requires);
518 }
519
520 /**
521 * Replies the list of plugins required by the local instance of this plugin.
522 * @return List of plugins required. Empty if no plugin is required.
523 * @since 5601
524 */
525 public List<String> getLocalRequiredPlugins() {
526 return getRequiredPlugins(localrequires);
527 }
528
529 /**
530 * Updates the local fields ({@link #localversion}, {@link #localmainversion}, {@link #localrequires})
531 * to values contained in the up-to-date fields ({@link #version}, {@link #mainversion}, {@link #requires})
532 * of the given PluginInformation.
533 * @param info The plugin information to get the data from.
534 * @since 5601
535 */
536 public void updateLocalInfo(PluginInformation info) {
537 if (info != null) {
538 this.localversion = info.version;
539 this.localmainversion = info.mainversion;
540 this.localrequires = info.requires;
541 }
542 }
543}
Note: See TracBrowser for help on using the repository browser.