/** * The contents of this file are subject to the OpenMRS Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * Copyright (C) OpenMRS, LLC. All Rights Reserved. */ package org.openmrs.module; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Vector; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.GlobalProperty; import org.openmrs.Privilege; import org.openmrs.api.context.Context; import org.openmrs.util.OpenmrsUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * This class will parse a file into an org.openmrs.module.Module object */ public class ModuleFileParser { private Log log = LogFactory.getLog(this.getClass()); private File moduleFile = null; /** * List out all of the possible version numbers for config files that openmrs has DTDs for. * These are usually stored at http://resources.openmrs.org/doctype/config-x.x.dt */ private static List<String> validConfigVersions = new ArrayList<String>(); static { validConfigVersions.add("1.0"); validConfigVersions.add("1.1"); validConfigVersions.add("1.2"); validConfigVersions.add("1.3"); } /** * Constructor * * @param moduleFile the module (jar)file that will be parsed */ public ModuleFileParser(File moduleFile) { if (moduleFile == null) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.fileCannotBeNull")); if (!moduleFile.getName().endsWith(".omod")) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.invalidFileExtension"), moduleFile.getName()); this.moduleFile = moduleFile; } /** * Convenience constructor to parse the given inputStream file into an omod. <br/> * This copies the stream into a temporary file just so things can be parsed.<br/> * * @param inputStream the inputStream pointing to an omod file */ public ModuleFileParser(InputStream inputStream) { FileOutputStream outputStream = null; try { moduleFile = File.createTempFile("moduleUpgrade", "omod"); outputStream = new FileOutputStream(moduleFile); OpenmrsUtil.copyFile(inputStream, outputStream); } catch (FileNotFoundException e) { throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.cannotCreateFile"), e); } catch (IOException e) { throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.cannotCreateFile"), e); } finally { try { inputStream.close(); } catch (Exception e) { /* pass */} try { outputStream.close(); } catch (Exception e) { /* pass */} } } /** * Get the module * * @return new module object */ public Module parse() throws ModuleException { Module module = null; JarFile jarfile = null; InputStream configStream = null; try { try { jarfile = new JarFile(moduleFile); } catch (IOException e) { throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.cannotGetJarFile"), moduleFile.getName(), e); } // look for config.xml in the root of the module ZipEntry config = jarfile.getEntry("config.xml"); if (config == null) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.noConfigFile"), moduleFile.getName()); // get a config file stream try { configStream = jarfile.getInputStream(config); } catch (IOException e) { throw new ModuleException(Context.getMessageSourceService().getMessage( "Module.error.cannotGetConfigFileStream"), moduleFile.getName(), e); } // turn the config file into an xml document Document configDoc = null; try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); db.setEntityResolver(new EntityResolver() { public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { // When asked to resolve external entities (such as a // DTD) we return an InputSource // with no data at the end, causing the parser to ignore // the DTD. return new InputSource(new StringReader("")); } }); configDoc = db.parse(configStream); } catch (Exception e) { log.error("Error parsing config.xml: " + configStream.toString(), e); OutputStream out = null; String output = ""; try { out = new ByteArrayOutputStream(); // Now copy bytes from the URL to the output stream byte[] buffer = new byte[4096]; int bytes_read; while ((bytes_read = configStream.read(buffer)) != -1) out.write(buffer, 0, bytes_read); output = out.toString(); } catch (Exception e2) { log.warn("Another error parsing config.xml", e2); } finally { try { out.close(); } catch (Exception e3) {} } log.error("config.xml content: " + output); throw new ModuleException( Context.getMessageSourceService().getMessage("Module.error.cannotParseConfigFile"), moduleFile .getName(), e); } Element rootNode = configDoc.getDocumentElement(); String configVersion = rootNode.getAttribute("configVersion").trim(); if (!validConfigVersions.contains(configVersion)) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.invalidConfigVersion", new Object[] { configVersion }, Context.getLocale()), moduleFile.getName()); String name = getElement(rootNode, configVersion, "name").trim(); String moduleId = getElement(rootNode, configVersion, "id").trim(); String packageName = getElement(rootNode, configVersion, "package").trim(); String author = getElement(rootNode, configVersion, "author").trim(); String desc = getElement(rootNode, configVersion, "description").trim(); String version = getElement(rootNode, configVersion, "version").trim(); // do some validation if (name == null || name.length() == 0) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.nameCannotBeEmpty"), moduleFile.getName()); if (moduleId == null || moduleId.length() == 0) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.idCannotBeEmpty"), name); if (packageName == null || packageName.length() == 0) throw new ModuleException(Context.getMessageSourceService().getMessage("Module.error.packageCannotBeEmpty"), name); // look for log4j.xml in the root of the module Document log4jDoc = null; try { ZipEntry log4j = jarfile.getEntry("log4j.xml"); if (log4j != null) { InputStream log4jStream = jarfile.getInputStream(log4j); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); db.setEntityResolver(new EntityResolver() { public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { // When asked to resolve external entities (such as // a // DTD) we return an InputSource // with no data at the end, causing the parser to // ignore // the DTD. return new InputSource(new StringReader("")); } }); log4jDoc = db.parse(log4jStream); } } catch (Exception e) {} // create the module object module = new Module(name, moduleId, packageName, author, desc, version); // find and load the activator class module.setActivatorName(getElement(rootNode, configVersion, "activator").trim()); module.setRequireDatabaseVersion(getElement(rootNode, configVersion, "require_database_version").trim()); module.setRequireOpenmrsVersion(getElement(rootNode, configVersion, "require_version").trim()); module.setUpdateURL(getElement(rootNode, configVersion, "updateURL").trim()); module.setRequiredModulesMap(getRequiredModules(rootNode, configVersion)); module.setAwareOfModulesMap(getAwareOfModules(rootNode, configVersion)); module.setAdvicePoints(getAdvice(rootNode, configVersion, module)); module.setExtensionNames(getExtensions(rootNode, configVersion)); module.setPrivileges(getPrivileges(rootNode, configVersion)); module.setGlobalProperties(getGlobalProperties(rootNode, configVersion)); module.setMessages(getMessages(rootNode, configVersion, jarfile)); module.setMappingFiles(getMappingFiles(rootNode, configVersion, jarfile)); module.setConfig(configDoc); module.setLog4j(log4jDoc); module.setMandatory(getMandatory(rootNode, configVersion, jarfile)); module.setFile(moduleFile); } finally { try { jarfile.close(); } catch (Exception e) { log.warn("Unable to close jarfile: " + jarfile, e); } if (configStream != null) { try { configStream.close(); } catch (Exception io) { log.error("Error while closing config stream for module: " + moduleFile.getAbsolutePath(), io); } } } return module; } /** * Generic method to get a module tag * * @param root * @param version * @param tag * @return */ private String getElement(Element root, String version, String tag) { if (root.getElementsByTagName(tag).getLength() > 0) return root.getElementsByTagName(tag).item(0).getTextContent(); return ""; } /** * load in required modules list * * @param root element in the xml doc object * @param version of the config file * @return map from module package name to required version * @since 1.5 */ private Map<String, String> getRequiredModules(Element root, String version) { NodeList requiredModulesParents = root.getElementsByTagName("require_modules"); Map<String, String> packageNamesToVersion = new HashMap<String, String>(); // TODO test require_modules section if (requiredModulesParents.getLength() > 0) { Node requiredModulesParent = requiredModulesParents.item(0); NodeList requiredModules = requiredModulesParent.getChildNodes(); int i = 0; while (i < requiredModules.getLength()) { Node n = requiredModules.item(i); if (n != null && "require_module".equals(n.getNodeName())) { NamedNodeMap attributes = n.getAttributes(); Node versionNode = attributes.getNamedItem("version"); String reqVersion = versionNode == null ? null : versionNode.getNodeValue(); packageNamesToVersion.put(n.getTextContent().trim(), reqVersion); } i++; } } return packageNamesToVersion; } /** * load in list of modules we are aware of. * * @param root element in the xml doc object * @param version of the config file * @return map from module package name to aware of version * @since 1.9 */ private Map<String, String> getAwareOfModules(Element root, String version) { NodeList awareOfModulesParents = root.getElementsByTagName("aware_of_modules"); Map<String, String> packageNamesToVersion = new HashMap<String, String>(); // TODO test aware_of_modules section if (awareOfModulesParents.getLength() > 0) { Node awareOfModulesParent = awareOfModulesParents.item(0); NodeList awareOfModules = awareOfModulesParent.getChildNodes(); int i = 0; while (i < awareOfModules.getLength()) { Node n = awareOfModules.item(i); if (n != null && "aware_of_module".equals(n.getNodeName())) { NamedNodeMap attributes = n.getAttributes(); Node versionNode = attributes.getNamedItem("version"); String awareOfVersion = versionNode == null ? null : versionNode.getNodeValue(); packageNamesToVersion.put(n.getTextContent().trim(), awareOfVersion); } i++; } } return packageNamesToVersion; } /** * load in advicePoints * * @param root * @param version * @return */ private List<AdvicePoint> getAdvice(Element root, String version, Module mod) { List<AdvicePoint> advicePoints = new Vector<AdvicePoint>(); NodeList advice = root.getElementsByTagName("advice"); if (advice.getLength() > 0) { log.debug("# advice: " + advice.getLength()); int i = 0; while (i < advice.getLength()) { Node node = advice.item(i); NodeList nodes = node.getChildNodes(); int x = 0; String point = "", adviceClass = ""; while (x < nodes.getLength()) { Node childNode = nodes.item(x); if ("point".equals(childNode.getNodeName())) point = childNode.getTextContent().trim(); else if ("class".equals(childNode.getNodeName())) adviceClass = childNode.getTextContent().trim(); x++; } log.debug("point: " + point + " class: " + adviceClass); // point and class are required if (point.length() > 0 && adviceClass.length() > 0) { advicePoints.add(new AdvicePoint(mod, point, adviceClass)); } else log.warn("'point' and 'class' are required for advice. Given '" + point + "' and '" + adviceClass + "'"); i++; } } return advicePoints; } /** * load in extensions * * @param root * @param configVersion * @return */ private IdentityHashMap<String, String> getExtensions(Element root, String configVersion) { IdentityHashMap<String, String> extensions = new IdentityHashMap<String, String>(); NodeList extensionNodes = root.getElementsByTagName("extension"); if (extensionNodes.getLength() > 0) { log.debug("# extensions: " + extensionNodes.getLength()); int i = 0; while (i < extensionNodes.getLength()) { Node node = extensionNodes.item(i); NodeList nodes = node.getChildNodes(); int x = 0; String point = "", extClass = ""; while (x < nodes.getLength()) { Node childNode = nodes.item(x); if ("point".equals(childNode.getNodeName())) point = childNode.getTextContent().trim(); else if ("class".equals(childNode.getNodeName())) extClass = childNode.getTextContent().trim(); x++; } log.debug("point: " + point + " class: " + extClass); // point and class are required if (point.length() > 0 && extClass.length() > 0) { if (point.indexOf(Extension.extensionIdSeparator) != -1) log.warn("Point id contains illegal character: '" + Extension.extensionIdSeparator + "'"); else { extensions.put(point, extClass); } } else log .warn("'point' and 'class' are required for extensions. Given '" + point + "' and '" + extClass + "'"); i++; } } return extensions; } /** * load in messages * * @param root * @param configVersion * @return */ private Map<String, Properties> getMessages(Element root, String configVersion, JarFile jarfile) { Map<String, Properties> messages = new HashMap<String, Properties>(); NodeList messageNodes = root.getElementsByTagName("messages"); if (messageNodes.getLength() > 0) { log.debug("# message nodes: " + messageNodes.getLength()); int i = 0; while (i < messageNodes.getLength()) { Node node = messageNodes.item(i); NodeList nodes = node.getChildNodes(); int x = 0; String lang = "", file = ""; while (x < nodes.getLength()) { Node childNode = nodes.item(x); if ("lang".equals(childNode.getNodeName())) lang = childNode.getTextContent().trim(); else if ("file".equals(childNode.getNodeName())) file = childNode.getTextContent().trim(); x++; } log.debug("lang: " + lang + " file: " + file); // lang and file are required if (lang.length() > 0 && file.length() > 0) { InputStream inStream = null; try { ZipEntry entry = jarfile.getEntry(file); if (entry == null) throw new ModuleException(Context.getMessageSourceService().getMessage( "Module.error.noMessagePropsFile", new Object[] { file, lang }, Context.getLocale())); inStream = jarfile.getInputStream(entry); Properties props = new Properties(); OpenmrsUtil.loadProperties(props, inStream); messages.put(lang, props); } catch (IOException e) { log.warn("Unable to load properties: " + file); } finally { if (inStream != null) { try { inStream.close(); } catch (IOException io) { log.error("Error while closing property input stream for module: " + moduleFile.getAbsolutePath(), io); } } } } else log.warn("'lang' and 'file' are required for extensions. Given '" + lang + "' and '" + file + "'"); i++; } } return messages; } /** * load in required privileges * * @param root * @param version * @return */ private List<Privilege> getPrivileges(Element root, String version) { List<Privilege> privileges = new Vector<Privilege>(); NodeList privNodes = root.getElementsByTagName("privilege"); if (privNodes.getLength() > 0) { log.debug("# privileges: " + privNodes.getLength()); int i = 0; while (i < privNodes.getLength()) { Node node = privNodes.item(i); NodeList nodes = node.getChildNodes(); int x = 0; String name = "", description = ""; while (x < nodes.getLength()) { Node childNode = nodes.item(x); if ("name".equals(childNode.getNodeName())) name = childNode.getTextContent().trim(); else if ("description".equals(childNode.getNodeName())) description = childNode.getTextContent().trim(); x++; } log.debug("name: " + name + " description: " + description); // name and desc are required if (name.length() > 0 && description.length() > 0) privileges.add(new Privilege(name, description)); else log.warn("'name' and 'description' are required for privileges. Given '" + name + "' and '" + description + "'"); i++; } } return privileges; } /** * load in required global properties and defaults * * @param root * @param version * @return */ private List<GlobalProperty> getGlobalProperties(Element root, String version) { List<GlobalProperty> properties = new Vector<GlobalProperty>(); NodeList propNodes = root.getElementsByTagName("globalProperty"); if (propNodes.getLength() > 0) { log.debug("# global props: " + propNodes.getLength()); int i = 0; while (i < propNodes.getLength()) { Node node = propNodes.item(i); NodeList nodes = node.getChildNodes(); int x = 0; String property = "", defaultValue = "", description = ""; while (x < nodes.getLength()) { Node childNode = nodes.item(x); if ("property".equals(childNode.getNodeName())) property = childNode.getTextContent().trim(); else if ("defaultValue".equals(childNode.getNodeName())) defaultValue = childNode.getTextContent(); else if ("description".equals(childNode.getNodeName())) description = childNode.getTextContent().trim(); x++; } log.debug("property: " + property + " defaultValue: " + defaultValue + " description: " + description); // remove tabs from description and trim start/end whitespace if (description != null) description = description.replaceAll(" ", "").trim(); // name is required if (property.length() > 0) properties.add(new GlobalProperty(property, defaultValue, description)); else log.warn("'property' is required for global properties. Given '" + property + "'"); i++; } } return properties; } /** * Load in the defined mapping file names * * @param rootNode * @param configVersion * @param jarfile * @return */ private List<String> getMappingFiles(Element rootNode, String configVersion, JarFile jarfile) { String mappingString = getElement(rootNode, configVersion, "mappingFiles"); List<String> mappings = new Vector<String>(); for (String s : mappingString.split("\\s")) { String s2 = s.trim(); if (s2.length() > 0) mappings.add(s2); } return mappings; } /** * Looks for the "<mandatory>" element in the config file and returns true if the value is * exactly "true". * * @param rootNode * @param configVersion * @param jarfile * @return true if the mandatory element is set to true */ private boolean getMandatory(Element rootNode, String configVersion, JarFile jarfile) { if (Double.parseDouble(configVersion) >= 1.3) { String mandatory = getElement(rootNode, configVersion, "mandatory").trim(); return "true".equalsIgnoreCase(mandatory); } return false; // this module has an older config file } }