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 }