// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.opendata.core.modules; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Dimension; import java.awt.Image; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import javax.swing.ImageIcon; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.plugins.opendata.OdPlugin; import org.openstreetmap.josm.plugins.opendata.core.OdConstants; import org.openstreetmap.josm.plugins.opendata.core.util.OdUtils; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; import org.openstreetmap.josm.tools.LanguageInfo; /** * Encapsulate general information about a module. This information is available * without the need of loading any class from the module jar file. */ public class ModuleInformation { public File file = null; public String name = null; public String className = null; public String link = null; public String description = null; public String author = null; public String version = null; public String localversion = null; public String downloadlink = null; public String iconPath; public ImageIcon icon; public List<URL> libraries = new LinkedList<>(); public final Map<String, String> attr = new TreeMap<>(); /** * Creates a module information object by reading the module information from * the manifest in the module jar. * * The module name is derived from the file name. * * @param file the module jar file * @throws ModuleException if reading the manifest fails */ /*public ModuleInformation(File file) throws ModuleException { this(file, file.getName().substring(0, file.getName().length()-4)); }*/ /** * Creates a module information object for the module with name {@code name}. * Information about the module is extracted from the manifest file in the module jar * {@code file}. * @param file the module jar * @param name the module name * @throws ModuleException thrown if reading the manifest file fails */ public ModuleInformation(File file, String name) throws ModuleException { this.name = name; this.file = file; try ( FileInputStream fis = new FileInputStream(file); JarInputStream jar = new JarInputStream(fis); ) { Manifest manifest = jar.getManifest(); if (manifest == null) throw new ModuleException(name, tr("The module file ''{0}'' does not include a Manifest.", file.toString())); scanManifest(manifest); libraries.add(0, fileToURL(file)); } catch (IOException e) { throw new ModuleException(name, e); } } /** * Creates a module information object by reading module information in Manifest format * from the input stream {@code manifestStream}. * * @param manifestStream the stream to read the manifest from * @param name the module name * @param url the download URL for the module * @throws ModuleException thrown if the module information can't be read from the input stream */ public ModuleInformation(InputStream manifestStream, String name, String url) throws ModuleException { this.name = name; try { Manifest manifest = new Manifest(); manifest.read(manifestStream); if (url != null) { downloadlink = url; } scanManifest(manifest); } catch (IOException e) { throw new ModuleException(name, e); } } /** * Updates the module information of this module information object with the * module information in a module information object retrieved from a module * update site. * * @param other the module information object retrieved from the update site */ public void updateFromModuleSite(ModuleInformation other) { this.className = other.className; this.link = other.link; this.description = other.description; this.author = other.author; this.version = other.version; this.downloadlink = other.downloadlink; this.icon = other.icon; this.iconPath = other.iconPath; this.libraries = other.libraries; this.attr.clear(); this.attr.putAll(other.attr); } private static ImageIcon extractIcon(String iconPath, File jarFile, boolean suppressWarnings) { return new ImageProvider(iconPath).setArchive(jarFile).setMaxWidth(24).setMaxHeight(24).setOptional(true) .setSuppressWarnings(suppressWarnings).get(); } private void scanManifest(Manifest manifest) { String lang = LanguageInfo.getLanguageCodeManifest(); Attributes attr = manifest.getMainAttributes(); className = attr.getValue("Module-Class"); String s = attr.getValue(lang+"Module-Link"); if (s == null) { s = attr.getValue("Module-Link"); } if (s != null) { try { @SuppressWarnings("unused") URL url = new URL(s); } catch (MalformedURLException e) { Main.error(tr("Invalid URL ''{0}'' in module {1}", s, name)); s = null; } } link = s; s = attr.getValue(lang+"Module-Description"); if (s == null) { s = attr.getValue("Module-Description"); if (s != null) { s = tr(s); } } description = s; version = attr.getValue("Module-Version"); author = attr.getValue("Author"); iconPath = attr.getValue("Module-Icon"); if (iconPath != null && file != null) { // extract icon from the module jar file icon = extractIcon(iconPath, file, true); // if not found, extract icon from the plugin jar file if (icon == null) { icon = extractIcon(iconPath, OdPlugin.getInstance().getPluginInformation().file, true); } if (icon == null) { Main.error("Unable to load module icon: "+iconPath); } } String classPath = attr.getValue(Attributes.Name.CLASS_PATH); if (classPath != null) { for (String entry : classPath.split(" ")) { File entryFile; if (new File(entry).isAbsolute() || file == null) { entryFile = new File(entry); } else { entryFile = new File(file.getParent(), entry); } libraries.add(fileToURL(entryFile)); } } for (Object o : attr.keySet()) { this.attr.put(o.toString(), attr.getValue(o.toString())); } } /** * Replies the description as HTML document, including a link to a web page with * more information, provided such a link is available. * * @return the description as HTML document */ public String getDescriptionAsHtml() { StringBuilder sb = new StringBuilder(); sb.append("<html><body>"); sb.append(description == null ? tr("no description available") : description); if (link != null) { sb.append(" <a href=\"").append(link).append("\">").append(tr("More info...")).append("</a>"); } if (downloadlink != null && !downloadlink.startsWith(OdConstants.OSM_SITE+"dist/")) { sb.append("<p> </p><p>"+tr("<b>Module provided by an external source:</b> {0}", downloadlink)+"</p>"); } sb.append("</body></html>"); return sb.toString(); } /** * Load and instantiate the module * * @param the module class * @return the instantiated and initialized module */ public Module load(Class<? extends Module> klass) throws ModuleException { try { return klass.getConstructor(ModuleInformation.class).newInstance(this); } catch (Exception t) { throw new ModuleException(name, t); } } /** * Load the class of the module * * @param classLoader the class loader to use * @return the loaded class */ @SuppressWarnings("unchecked") public Class<? extends Module> loadClass(ClassLoader classLoader) throws ModuleException { if (className == null) return null; try { return (Class<? extends Module>) Class.forName(className, true, classLoader); } catch (Exception t) { throw new ModuleException(name, t); } } public static URL fileToURL(File f) { try { return f.toURI().toURL(); } catch (MalformedURLException ex) { Main.warn(ex.getMessage()); return null; } } public static Collection<String> getModuleLocations() { Collection<String> locations = Main.pref.getAllPossiblePreferenceDirs(); Collection<String> all = new ArrayList<>(locations.size()); for (String s : locations) { all.add(s+"plugins/opendata/modules"); } return all; } /** * Replies true if the module with the given information is most likely outdated with * respect to the referenceVersion. * * @param referenceVersion the reference version. Can be null if we don't know a * reference version * * @return true, if the module needs to be updated; false, otherweise */ public boolean isUpdateRequired(String referenceVersion) { if (this.downloadlink == null) return false; if (this.version == null && referenceVersion != null) return true; if (this.version != null && !this.version.equals(referenceVersion)) return true; return false; } /** * Replies true if this this module should be updated/downloaded because either * it is not available locally (its local version is null) or its local version is * older than the available version on the server. * * @return true if the module should be updated */ public boolean isUpdateRequired() { if (this.downloadlink == null) return false; if (this.localversion == null) return true; return isUpdateRequired(this.localversion); } protected boolean matches(String filter, String value) { if (filter == null) return true; if (value == null) return false; return value.toLowerCase().contains(filter.toLowerCase()); } /** * Replies true if either the name, the description, or the version match (case insensitive) * one of the words in filter. Replies true if filter is null. * * @param filter the filter expression * @return true if this module info matches with the filter */ public boolean matches(String filter) { if (filter == null) return true; String[] words = filter.split("\\s+"); for (String word: words) { if (matches(word, name) || matches(word, description) || matches(word, version) || matches(word, localversion)) return true; } return false; } /** * Replies the name of the module */ public String getName() { return name; } public ImageIcon getScaledIcon() { Dimension dim = ImageSizes.MENU.getImageDimension(); ImageIcon iconToScale = icon != null ? icon : OdUtils.getImageIcon(OdConstants.ICON_EMPTY_24); return new ImageIcon(iconToScale.getImage().getScaledInstance(dim.width, dim.height, Image.SCALE_SMOOTH)); } }