001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.plugin;
017
018import org.apache.commons.io.FileUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.config.property.Config;
021import org.kuali.rice.kew.api.WorkflowRuntimeException;
022
023import java.io.BufferedInputStream;
024import java.io.BufferedOutputStream;
025import java.io.File;
026import java.io.FileOutputStream;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.net.MalformedURLException;
030import java.net.URL;
031import java.util.Enumeration;
032import java.util.zip.ZipEntry;
033import java.util.zip.ZipFile;
034
035
036/**
037 * Loads a plugin from a zip file on the file system.
038 *
039 * @author Kuali Rice Team (rice.collab@kuali.org)
040 */
041public class ZipFilePluginLoader extends BasePluginLoader {
042        private static final Logger LOG = Logger.getLogger(ZipFilePluginLoader.class);
043
044        private final File pluginZipFile;
045    private final File extractionDirectory;
046    private long zipFileLastModified = -1;
047    private boolean loadFailed = false;
048
049    private static String validatePluginZipFile(File pluginZipFile) {
050        PluginUtils.validatePluginZipFile(pluginZipFile);
051        String fileName = pluginZipFile.getName();
052        int indexOf = fileName.lastIndexOf(".zip");
053        return fileName.substring(0, indexOf);
054    }
055
056    public ZipFilePluginLoader(File pluginZipFile, File sharedPluginDirectory, ClassLoader parentClassLoader, Config parentConfig) {
057        super(validatePluginZipFile(pluginZipFile), sharedPluginDirectory, parentClassLoader, parentConfig);
058        this.pluginZipFile = pluginZipFile;
059        this.extractionDirectory = determineExtractionDirectory(getSimplePluginName(), pluginZipFile);
060    }
061    
062        public boolean isModified() {
063                long currentZipFileLastModified = pluginZipFile.lastModified();
064                if (zipFileLastModified == -1) {
065                        zipFileLastModified = currentZipFileLastModified;
066                        return false;
067                } else if (currentZipFileLastModified > zipFileLastModified) {
068                        return !isZipFileStillBeingModified();
069                }
070                return false;
071        }
072
073        protected boolean isZipFileStillBeingModified() {
074                long lastModified = pluginZipFile.lastModified();
075                long size = pluginZipFile.length();
076                // sleep for a fraction of a second and then check again if the values have changed
077                try {
078                        Thread.sleep(100);
079                } catch (InterruptedException e) {}
080                if (lastModified != pluginZipFile.lastModified()) {
081                        return true;
082                }
083                if (size != pluginZipFile.length()) {
084                        return true;
085                }
086                return false;
087        }
088
089        public boolean isRemoved() {
090                return pluginZipFile != null && !pluginZipFile.exists();
091        }
092
093    @Override
094        public Plugin load() throws Exception {
095        try {
096            updateLastModified();
097            extractPluginFiles();
098            Plugin plugin = super.load();
099            loadFailed = false;
100            return plugin;
101        } catch (Exception e) {
102            loadFailed = true;
103            throw e;
104        }
105        }
106
107    protected File determineExtractionDirectory(String pluginName, File pluginZipFile) {
108                return new File(pluginZipFile.getParentFile(), pluginName);
109        }
110
111    /**
112     * Extracts the plugin files if necessary.
113     */
114    protected void extractPluginFiles() throws Exception {
115        if (isExtractNeeded()) {
116                // first, delete the current copy of the extracted plugin
117                if (extractionDirectory.exists()) {
118                        // TODO how to handle locked files under windows?!?  This will throw an IOException in this case.
119                        FileUtils.deleteDirectory(extractionDirectory);
120                }
121                if (!extractionDirectory.mkdir()) {
122                        throw new WorkflowRuntimeException("Could not create the extraction directory for the plugin: " + extractionDirectory.getAbsolutePath());
123                }
124                ZipFile zipFile = new ZipFile(pluginZipFile, ZipFile.OPEN_READ);
125                for (Enumeration entries = zipFile.entries(); entries.hasMoreElements();) {
126                        ZipEntry entry = (ZipEntry)entries.nextElement();
127                                File entryFile = new File(extractionDirectory + java.io.File.separator + entry.getName());
128                        if (entry.isDirectory()) { // if its a directory, create it
129                                if (!entryFile.mkdir()) {
130                                        throw new WorkflowRuntimeException("Failed to create directory: " + entryFile.getAbsolutePath());
131                                }
132                                continue;
133                        }
134                        InputStream is = null;
135                        OutputStream os = null;
136                        try {
137                                is = new BufferedInputStream(zipFile.getInputStream(entry)); // get the input stream
138                                os = new BufferedOutputStream(new FileOutputStream(entryFile));
139                                while (is.available() > 0) {  // write contents of 'is' to 'fos'
140                                        os.write(is.read());
141                                }
142                        } finally {
143                                if (os != null) {
144                                        os.close();
145                                }
146                                if (is != null) {
147                                        is.close();
148                                }
149                        }
150                }
151        }
152    }
153    
154    /**
155     * @return the loadFailed
156     */
157    public boolean isLoadFailed() {
158        return this.loadFailed;
159    }
160
161    /**
162     * An extract is required if the plugin has been modified or the last modified date of the zip file
163     * is later than the last modified date of the extraction directory.
164     */
165    protected boolean isExtractNeeded() {
166        return isModified() || pluginZipFile.lastModified() > extractionDirectory.lastModified();
167    }
168
169    protected void updateLastModified() {
170        zipFileLastModified = pluginZipFile.lastModified();
171    }
172
173        protected PluginClassLoader createPluginClassLoader() throws MalformedURLException {
174        LOG.info(getLogPrefix() + " Initiating loading of plugin from file system: " + extractionDirectory.getPath());
175        LOG.info(getLogPrefix() + " Absolute path on file system is: " + extractionDirectory.getAbsolutePath());
176        /* MalformedURLException should technically never be thrown as the URLs are coming from (presumably)
177         * valid File objects
178         */
179        return new PluginClassLoader(parentClassLoader, sharedPluginDirectory, extractionDirectory);
180    }
181
182    protected URL getPluginConfigURL() throws PluginException, MalformedURLException {
183        File pluginConfigFile = new File(extractionDirectory, pluginConfigPath);
184        if (!pluginConfigFile.exists() || !pluginConfigFile.isFile()) {
185            throw new PluginException(getLogPrefix() + " Could not locate the plugin config file at path " + pluginConfigFile.getAbsolutePath());
186        }
187        return pluginConfigFile.toURI().toURL();
188    }
189}