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}