org.openmrs.module.moduledistro.api.impl.ModuleDistroServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.moduledistro.api.impl.ModuleDistroServiceImpl.java

Source

/**
 * 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.moduledistro.api.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.servlet.ServletContext;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.module.Module;
import org.openmrs.module.ModuleFactory;
import org.openmrs.module.ModuleUtil;
import org.openmrs.module.moduledistro.api.ModuleDistroService;
import org.openmrs.module.moduledistro.api.db.ModuleDistroDAO;
import org.openmrs.module.web.WebModuleUtil;
import org.openmrs.util.OpenmrsUtil;

/**
 * It is a default implementation of {@link ModuleDistroService}.
 */
public class ModuleDistroServiceImpl extends BaseOpenmrsService implements ModuleDistroService {

    protected final Log log = LogFactory.getLog(this.getClass());

    private ModuleDistroDAO dao;

    /**
      * @param dao the dao to set
      */
    public void setDao(ModuleDistroDAO dao) {
        this.dao = dao;
    }

    /**
     * @see org.openmrs.module.moduledistro.api.ModuleDistroService#uploadDistro(java.io.File)
     */
    @Override
    public List<String> uploadDistro(File distributionZip, ServletContext servletContext) {
        // get all omods included in the zip file, by their original filename
        List<UploadedModule> includedOmods = new ArrayList<ModuleDistroServiceImpl.UploadedModule>();

        ZipFile zf = null;
        try {
            zf = new ZipFile(distributionZip);
            for (@SuppressWarnings("rawtypes")
            Enumeration e = zf.entries(); e.hasMoreElements();) {
                ZipEntry entry = (ZipEntry) e.nextElement();
                if (entry.getName().endsWith("/"))
                    continue;
                if (!entry.getName().endsWith(".omod")) {
                    throw new RuntimeException("This ZIP is only allowed to contain omod files, but this contains: "
                            + entry.getName());
                }
                File file = File.createTempFile("distributionOmod", ".omod");
                file.deleteOnExit();
                FileUtils.copyInputStreamToFile(zf.getInputStream(entry), file);
                String originalName = simpleFilename(entry.getName());
                includedOmods.add(new UploadedModule(originalName, file));
            }
        } catch (IOException ex) {
            // TODO something prettier
            throw new RuntimeException("Error reading zip file", ex);
        } finally {
            try {
                zf.close();
            } catch (Exception ex) {
            }
        }

        // determine which omods we want to install
        for (UploadedModule candidate : includedOmods) {
            try {
                log.debug("about to inspect " + candidate);
                populateFields(candidate);
                log.debug("inspected " + candidate);
            } catch (IOException ex) {
                throw new RuntimeException("Error inspecting " + candidate.getOriginalFilename(), ex);
            }
        }

        // apply those actions (and log them)
        List<String> log = new ArrayList<String>();
        List<ModuleAction> actions = determineActions(includedOmods);

        while (!actions.isEmpty()) {
            ModuleAction action = removeNextAction(actions);

            if (Action.SKIP.equals(action.getAction())) {
                UploadedModule info = (UploadedModule) action.getTarget();
                log.add(info.getOriginalFilename() + ": skipped because " + info.getSkipReason());

            } else if (Action.STOP.equals(action.getAction())) {
                Module module = (Module) action.getTarget();
                module.clearStartupError();
                List<Module> dependentModulesStopped = ModuleFactory.stopModule(module, false, true);
                for (Module depMod : dependentModulesStopped) {
                    if (servletContext != null)
                        WebModuleUtil.stopModule(depMod, servletContext);
                    log.add("Stopped depended module " + depMod.getModuleId() + " version " + depMod.getVersion());

                    // if any modules were stopped that we're not already planning to start, we need to start them
                    if (!scheduledToStart(actions, depMod.getModuleId())) {
                        actions.add(new ModuleAction(Action.START, depMod));
                    }
                }
                if (servletContext != null)
                    WebModuleUtil.stopModule(module, servletContext);
                log.add("Stopped " + module.getModuleId() + " version " + module.getVersion());

            } else if (Action.REMOVE.equals(action.getAction())) {
                Module module = (Module) action.getTarget();
                ModuleFactory.unloadModule(module);
                log.add("Removed " + module.getModuleId() + " version " + module.getVersion());

            } else if (Action.INSTALL.equals(action.getAction())) {
                UploadedModule info = (UploadedModule) action.getTarget();
                File inserted;
                try {
                    inserted = ModuleUtil.insertModuleFile(new FileInputStream(info.getData()),
                            info.getOriginalFilename());
                } catch (FileNotFoundException ex) {
                    throw new RuntimeException("Unexpected FileNotFoundException", ex);
                }
                Module loaded = ModuleFactory.loadModule(inserted);
                log.add("Installed " + info.getModuleId() + " version " + info.getModuleVersion());

                // if we installed a module, we also need to start it later
                if (!scheduledToStart(actions, loaded.getModuleId())) {
                    actions.add(new ModuleAction(Action.START, loaded));
                }

            } else if (Action.START.equals(action.getAction())) {
                Module module = (Module) action.getTarget();
                // TODO document a core bug, that the next line does not throw the promised ModuleException
                ModuleFactory.startModule(module);
                if (module.getStartupErrorMessage() != null)
                    throw new RuntimeException(
                            "Failed to start module " + module + " because of: " + module.getStartupErrorMessage());
                if (servletContext != null)
                    WebModuleUtil.startModule(module, servletContext, false); // TODO figure out how to delay context refresh
                log.add("Started " + module.getModuleId() + " version " + module.getVersion());

            } else {
                throw new RuntimeException(
                        "Programming Error: don't know how to handle action: " + action.getAction());
            }
        }

        return log;
    }

    /**
      * Removes the element from the list which should executed next, and returns it.
      * (We can't just use Collections.sort, or a PriorityQueue because I don't think sorting via
      * pairwise comparison can correctly determine module startup order.)
      * 
      * @param actions
      * @return
      */
    private ModuleAction removeNextAction(List<ModuleAction> actions) {
        if (actions.size() == 0)
            return null;

        Collections.sort(actions, new Comparator<ModuleAction>() {
            @Override
            public int compare(ModuleAction left, ModuleAction right) {
                return left.getAction().compareTo(right.getAction());
            }
        });

        Action nextAction = actions.get(0).getAction();
        if (!Action.START.equals(nextAction)) {
            // for every category except for starting modules, order doesn't matter
            return actions.remove(0);
        } else {
            // find a module that has all its dependencies started already
            for (Iterator<ModuleAction> iter = actions.iterator(); iter.hasNext();) {
                ModuleAction candidate = iter.next();
                if (candidate.getAction().equals(Action.START)
                        && requiredModulesStarted((Module) candidate.getTarget())) {
                    iter.remove();
                    return candidate;
                }
            }
            // if we couldn't find any startable modules, throw an error
            List<String> moduleIds = new ArrayList<String>();
            for (ModuleAction candidate : actions) {
                if (candidate.getAction().equals(Action.START))
                    moduleIds.add(((Module) candidate.getTarget()).getModuleId());
            }
            throw new RuntimeException(
                    "Cannot start any of the following modules because they all depend on non-started modules: "
                            + OpenmrsUtil.join(moduleIds, ", "));
        }
    }

    /**
      * copied from ModuleFactory in 1.9.x
      */
    private boolean requiredModulesStarted(Module module) {
        for (String reqModPackage : module.getRequiredModules()) {
            boolean started = false;
            for (Module mod : ModuleFactory.getStartedModules()) {
                if (mod.getPackageName().equals(reqModPackage)) {
                    String reqVersion = module.getRequiredModuleVersion(reqModPackage);
                    if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0)
                        started = true;
                    break;
                }
            }

            if (!started)
                return false;
        }

        return true;
    }

    /**
      * @param actions
      * @param moduleId
      * @return whether actions contains a START action for the given moduleId
      */
    private boolean scheduledToStart(Collection<ModuleAction> actions, String moduleId) {
        for (ModuleAction a : actions) {
            try {
                if (a.getAction().equals(Action.START)
                        && PropertyUtils.getProperty(a.getTarget(), "moduleId").equals(moduleId))
                    return true;
            } catch (Exception ex) {
                throw new RuntimeException("Cannot determine moduleId of " + a.getTarget());
            }
        }
        return false;
    }

    /**
      * Given a list of include omods, that have been inspected and their ModuleInfo fields populated, determine
      * what specific actions to take
      * 
      * @param includedOmods
      * @return
      */
    private List<ModuleAction> determineActions(List<UploadedModule> includedOmods) {
        // using a LinkedList here because we'll treat this as a queue and remove elements from the head
        List<ModuleAction> ret = new LinkedList<ModuleAction>();

        for (UploadedModule candidate : includedOmods) {
            log.debug("Looking at " + candidate);
            if (Action.SKIP.equals(candidate.getAction())) {
                ret.add(new ModuleAction(Action.SKIP, candidate));
            } else if (Action.START.equals(candidate.getAction())) {
                ret.add(new ModuleAction(Action.START, candidate.getExisting()));
                // TODO see if we need to start any dependent modules
            } else if (Action.INSTALL.equals(candidate.getAction())) {
                ret.add(new ModuleAction(Action.INSTALL, candidate));
            } else if (Action.UPGRADE.equals(candidate.getAction())) {
                ret.add(new ModuleAction(Action.STOP, candidate.getExisting()));
                ret.add(new ModuleAction(Action.REMOVE, candidate.getExisting()));
                ret.add(new ModuleAction(Action.INSTALL, candidate));
            } else {
                throw new RuntimeException("Programming error: don't know how to handle action "
                        + candidate.getAction() + " on " + candidate);
            }
        }

        return ret;
    }

    /**
      * Populates the moduleId, moduleVersion, action, and skipVersion fields on candidate
      * 
      * @param candidate
     * @throws IOException
     *
     * @should read the id and version from config.xml 
      */
    public void populateFields(UploadedModule candidate) throws IOException {
        JarFile jar = new JarFile(candidate.getData());
        ZipEntry entry = jar.getEntry("config.xml");
        if (entry == null) {
            throw new IOException("Cannot find config.xml");
        }
        StringWriter sw = new StringWriter();
        IOUtils.copy(jar.getInputStream(entry), sw);
        String configXml = sw.toString();

        String moduleId = null;
        {
            Matcher matcher = Pattern.compile("<id>(.+?)</id>").matcher(configXml);
            if (!matcher.find())
                throw new IOException("Cannot find <id>...</id> in config.xml");
            moduleId = matcher.group(1).trim();
        }

        String moduleVersion = null;
        {
            Matcher matcher = Pattern.compile("<version>(.+?)</version>").matcher(configXml);
            if (!matcher.find())
                throw new IOException("Cannot find <version>...</version> in config.xml");
            moduleVersion = matcher.group(1).trim();
        }

        candidate.setModuleId(moduleId);
        candidate.setModuleVersion(moduleVersion);

        Module existing = ModuleFactory.getModuleById(candidate.getModuleId());
        if (existing == null) {
            candidate.setAction(Action.INSTALL);
            return;
        } else { // there's an existing module
            candidate.setExisting(existing);

            if (shouldInstallNewVersion(candidate.getModuleVersion(), existing.getVersion())) {
                candidate.setAction(Action.UPGRADE);
            } else {
                candidate.setAction(Action.SKIP);
                candidate.setSkipReason(
                        "an equivalent or newer version is already installed: (" + existing.getVersion() + ")");
            }

            // if the module is up-to-date, but not running, we need to start it 
            if (!existing.isStarted() && candidate.getAction().equals(Action.SKIP)) {
                candidate.setAction(Action.START);
            }
        }

    }

    /**
      * public for testing
      * 
      * @param candidateVersion
      * @param existingVersion
      * @return whether candidateVersion should be installed over existingVersion
      * 
      * @should return false for 1.0-SNAPSHOT versus 1.0
      * @should return true for 1.0 versus 1.0-SNAPSHOT
      * @should return true for 1.0-SNAPSHOT versus 1.0-SNAPSHOT
      * @should return true for 1.0 versus 0.9.5
      * @should return false for 0.9.5 versus 1.0
      * @should return true for 1.0-SNAPSHOT versus 0.9
      * @should return false for 1.0-SNAPSHOT versus 1.1
      */
    public boolean shouldInstallNewVersion(String candidateVersion, String existingVersion) {
        int test = ModuleUtil.compareVersion(candidateVersion, existingVersion);

        // ModuleUtil.compareVersion treats 1.3-SNAPSHOT as being equals to 1.3, but if there's a
        // snapshot version installed, we want to overwrite that with any "same" version.

        if (test < 0) {
            return false;
        } else if (test > 0 || existingVersion.contains("-SNAPSHOT")) {
            return true;
        } else { // test == 0 && !existingVersion.contains("-SNAPSHOT")
            return false;
        }
    }

    /**
      * @param name
      * @return if name has any slashes, return what's after them
      */
    private String simpleFilename(String name) {
        if (name.indexOf('/') >= 0)
            name = name.substring(name.indexOf('/') + 1);
        if (name.indexOf('\\') >= 0)
            name = name.substring(name.indexOf('\\') + 1);
        return name;
    }

    /**
     * The order here is important
     */
    public enum Action {
        SKIP, STOP, REMOVE, INSTALL, START, UPGRADE
    }

    public class ModuleAction {
        private Action action;
        private Object target;

        public ModuleAction(Action action, UploadedModule target) {
            this.action = action;
            this.target = target;
        }

        public ModuleAction(Action action, Module target) {
            this.action = action;
            this.target = target;
        }

        /**
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            try {
                return action + " " + PropertyUtils.getProperty(target, "moduleId");
            } catch (Exception ex) {
                return action + " " + target;
            }
        }

        /**
         * @return the action
         */
        public Action getAction() {
            return action;
        }

        /**
         * @param action the action to set
         */
        public void setAction(Action action) {
            this.action = action;
        }

        /**
         * @return the target
         */
        public Object getTarget() {
            return target;
        }

        /**
         * @param target the target to set
         */
        public void setTarget(Object target) {
            this.target = target;
        }

    }

    public class UploadedModule {
        private String originalFilename;
        private File data;
        private String moduleId;
        private String moduleVersion;
        private Module existing;
        private Action action;
        private String skipReason;

        /**
          * @param originalFilename
          * @param data
          */
        public UploadedModule(String originalFilename, File data) {
            this.originalFilename = originalFilename;
            this.data = data;
        }

        /**
         * @see java.lang.Object#toString()
         */
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(originalFilename + " (" + data.getAbsolutePath() + ") -> " + moduleId + " v" + moduleVersion
                    + " ");
            if (existing != null)
                sb.append("already loaded with v" + existing.getVersion() + " ");
            sb.append("action=" + action);
            if (skipReason != null)
                sb.append(" skip because " + skipReason);
            return sb.toString();
        }

        /**
         * @return the originalFilename
         */
        public String getOriginalFilename() {
            return originalFilename;
        }

        /**
         * @param originalFilename the originalFilename to set
         */
        public void setOriginalFilename(String originalFilename) {
            this.originalFilename = originalFilename;
        }

        /**
         * @return the data
         */
        public File getData() {
            return data;
        }

        /**
         * @param data the data to set
         */
        public void setData(File data) {
            this.data = data;
        }

        /**
         * @return the moduleId
         */
        public String getModuleId() {
            return moduleId;
        }

        /**
         * @param moduleId the moduleId to set
         */
        public void setModuleId(String moduleId) {
            this.moduleId = moduleId;
        }

        /**
         * @return the moduleVersion
         */
        public String getModuleVersion() {
            return moduleVersion;
        }

        /**
         * @param moduleVersion the moduleVersion to set
         */
        public void setModuleVersion(String moduleVersion) {
            this.moduleVersion = moduleVersion;
        }

        /**
         * @return the skipReason
         */
        public String getSkipReason() {
            return skipReason;
        }

        /**
         * @param skipReason the skipReason to set
         */
        public void setSkipReason(String skipReason) {
            this.skipReason = skipReason;
        }

        /**
         * @return the action
         */
        public Action getAction() {
            return action;
        }

        /**
         * @param action the action to set
         */
        public void setAction(Action action) {
            this.action = action;
        }

        /**
         * @return the existing
         */
        public Module getExisting() {
            return existing;
        }

        /**
         * @param existing the existing to set
         */
        public void setExisting(Module existing) {
            this.existing = existing;
        }

    }

}