001    /**
002     * Copyright 2005-2013 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     */
016    package org.kuali.rice.kew.plugin;
017    
018    import org.apache.commons.io.FileUtils;
019    import org.apache.log4j.Logger;
020    import org.kuali.rice.core.api.config.property.Config;
021    import org.kuali.rice.kew.api.WorkflowRuntimeException;
022    
023    import java.io.BufferedInputStream;
024    import java.io.BufferedOutputStream;
025    import java.io.File;
026    import java.io.FileOutputStream;
027    import java.io.InputStream;
028    import java.io.OutputStream;
029    import java.net.MalformedURLException;
030    import java.net.URL;
031    import java.util.Enumeration;
032    import java.util.zip.ZipEntry;
033    import 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     */
041    public 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    }