diff --git a/config/alfresco/application-context.xml b/config/alfresco/application-context.xml index b4377cf42e..5ca124e1c2 100644 --- a/config/alfresco/application-context.xml +++ b/config/alfresco/application-context.xml @@ -43,5 +43,9 @@ --> + + + + diff --git a/source/java/org/alfresco/repo/module/ModuleManagementTool.java b/source/java/org/alfresco/repo/module/ModuleManagementTool.java new file mode 100644 index 0000000000..c1d8134330 --- /dev/null +++ b/source/java/org/alfresco/repo/module/ModuleManagementTool.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.module; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.VersionNumber; +import org.apache.log4j.Logger; + +import de.schlichtherle.io.DefaultRaesZipDetector; +import de.schlichtherle.io.File; +import de.schlichtherle.io.FileInputStream; +import de.schlichtherle.io.FileOutputStream; +import de.schlichtherle.io.ZipControllerException; +import de.schlichtherle.io.ZipDetector; +import de.schlichtherle.io.ZipWarningException; + +/** + * @author Roy Wetherall + */ +public class ModuleManagementTool +{ + public static Logger logger = Logger.getLogger("org.alfresco.repo.extension.ModuleManagementTool"); + + private static final String DEFAULT_FILE_MAPPING_PROPERTIES = "org/alfresco/repo/module/default-file-mapping.properties"; + private static final String MODULE_DIR = "/WEB-INF/classes/alfresco/module"; + + private static final String DELIMITER = ":"; + + private static final String PROP_ID = "module.id"; + private static final String PROP_TITLE = "module.title"; + private static final String PROP_DESCRIPTION = "module.description"; + private static final String PROP_VERSION = "module.version"; + + private static final String MOD_ADD_FILE = "add"; + private static final String MOD_UPDATE_FILE = "update"; + private static final String MOD_MK_DIR = "mkdir"; + + private static final String OP_INSTALL = "install"; + + private ZipDetector defaultDetector; + + private Properties fileMappingProperties; + + private boolean verbose = false; + + public ModuleManagementTool() + { + // Create the default zip detector + this.defaultDetector = new DefaultRaesZipDetector("amp|war"); + + // Load the default file mapping properties + this.fileMappingProperties = new Properties(); + InputStream is = this.getClass().getClassLoader().getResourceAsStream(DEFAULT_FILE_MAPPING_PROPERTIES); + try + { + this.fileMappingProperties.load(is); + } + catch (IOException exception) + { + throw new ModuleManagementToolException("Unable to load default extension file mapping properties.", exception); + } + } + + public boolean isVerbose() + { + return verbose; + } + + public void setVerbose(boolean verbose) + { + this.verbose = verbose; + } + + public void installModule(String ampFileLocation, String warFileLocation) + { + try + { + // Load the extension properties + File installingPropertiesFile = new File(ampFileLocation + "/module.properties", this.defaultDetector); + if (installingPropertiesFile.exists() == false) + { + throw new ModuleManagementToolException("Extension properties are not present in the AMP. Check that a valid module.properties file is present."); + } + Properties installingProperties = new Properties(); + installingProperties.load(new FileInputStream(installingPropertiesFile)); + + // Get the intalling extension version + String installingVersionString = installingProperties.getProperty(PROP_VERSION); + if (installingVersionString == null || installingVersionString.length() == 0) + { + throw new ModuleManagementToolException("The version number has not been specified in the module properties found in the AMP."); + } + VersionNumber installingVersion = new VersionNumber(installingVersionString); + + // Get the installed directory + File installDir = getInstalledDir(warFileLocation); + + // Look for a previously installed version of this extension + File installedExtensionPropertiesFile = new File(installDir.getPath() + "/" + getModuleDetailsFileName(installingProperties.getProperty(PROP_ID)), this.defaultDetector); + if (installedExtensionPropertiesFile.exists() == true) + { + Properties installedExtensionProperties = new Properties(); + InputStream is = new FileInputStream(installedExtensionPropertiesFile); + installedExtensionProperties.load(is); + + // Get the installed version + VersionNumber installedVersion = new VersionNumber(installedExtensionProperties.getProperty(PROP_VERSION)); + int compareValue = installedVersion.compareTo(installingVersion); + if (compareValue == -1) + { + // Trying to update the extension, old files need to cleaned before we proceed + cleanWAR(warFileLocation, installedExtensionProperties); + } + else if (compareValue == 0) + { + // Trying to install the same extension version again + verboseMessage("WARNING: This version of this module is already installed in the WAR"); + throw new ModuleManagementToolException("This version of this module is alreay installed. Use the 'force' parameter if you want to overwrite the current installation."); + } + else if (compareValue == 1) + { + // Trying to install an earlier version of the extension + verboseMessage("WARNING: A later version of this module is already installed in the WAR"); + throw new ModuleManagementToolException("An earlier version of this module is already installed. You must first unistall the current version before installing this version of the module."); + } + + } + + // TODO check for any additional file mapping propeties supplied in the AEP file + + // Copy the files from the AEP file into the WAR file + Map modifications = new HashMap(50); + for (Map.Entry entry : this.fileMappingProperties.entrySet()) + { + modifications.putAll(copyToWar(ampFileLocation, warFileLocation, (String)entry.getKey(), (String)entry.getValue())); + } + + // Copy the properties file into the war + if (installedExtensionPropertiesFile.exists() == false) + { + installedExtensionPropertiesFile.createNewFile(); + } + InputStream is = new FileInputStream(installingPropertiesFile); + try + { + installedExtensionPropertiesFile.catFrom(is); + } + finally + { + is.close(); + } + + // Create and add the modifications file to the war + writeModificationToFile(installDir.getPath() + "/" + getModuleModificationFileName(installingProperties.getProperty(PROP_ID)), modifications); + + // Update the zip file's + File.update(); + } + catch (ZipWarningException ignore) + { + // Only instances of the class ZipWarningException exist in the chain of + // exceptions. We choose to ignore this. + } + catch (ZipControllerException exception) + { + // At least one exception occured which is not just a ZipWarningException. + // This is a severe situation that needs to be handled. + throw new ModuleManagementToolException("A Zip error was encountered during deployment of the AEP into the WAR", exception); + } + catch (IOException exception) + { + throw new ModuleManagementToolException("An IO error was encountered during deployment of the AEP into the WAR", exception); + } + } + + private void cleanWAR(String warFileLocation, Properties installedExtensionProperties) + { + // Get the currently installed modifications + Map modifications = readModificationsFromFile(warFileLocation + "/" + getModuleModificationFileName(installedExtensionProperties.getProperty(PROP_ID))); + + for (Map.Entry modification : modifications.entrySet()) + { + String modType = modification.getValue(); + if (MOD_ADD_FILE.equals(modType) == true) + { + // Remove file + } + else if (MOD_UPDATE_FILE.equals(modType) == true) + { + // Remove file + // Replace with back-up + } + else if (MOD_MK_DIR.equals(modType) == true) + { + // Add to list of dir's to remove at the end + } + } + } + + private Map copyToWar(String aepFileLocation, String warFileLocation, String sourceDir, String destinationDir) + throws IOException + { + Map result = new HashMap(10); + + String sourceLocation = aepFileLocation + sourceDir; + File aepConfig = new File(sourceLocation, this.defaultDetector); + + for (java.io.File sourceChild : aepConfig.listFiles()) + { + String destinationFileLocation = warFileLocation + destinationDir + "/" + sourceChild.getName(); + File destinationChild = new File(destinationFileLocation, this.defaultDetector); + if (sourceChild.isFile() == true) + { + boolean createFile = false; + if (destinationChild.exists() == false) + { + destinationChild.createNewFile(); + createFile = true; + } + FileInputStream fis = new FileInputStream(sourceChild); + try + { + destinationChild.catFrom(fis); + } + finally + { + fis.close(); + } + + if (createFile == true) + { + result.put(destinationDir + "/" + sourceChild.getName(), MOD_ADD_FILE); + this.verboseMessage("File added: " + destinationDir + "/" + sourceChild.getName()); + } + else + { + result.put(destinationDir + "/" + sourceChild.getName(), MOD_UPDATE_FILE); + this.verboseMessage("File updated:" + destinationDir + "/" + sourceChild.getName()); + } + } + else + { + boolean mkdir = false; + if (destinationChild.exists() == false) + { + destinationChild.mkdir(); + mkdir = true; + } + + Map subResult = copyToWar(aepFileLocation, warFileLocation, sourceDir + "/" + sourceChild.getName(), + destinationDir + "/" + sourceChild.getName()); + result.putAll(subResult); + + if (mkdir == true) + { + result.put(destinationDir + "/" + sourceChild.getName(), MOD_MK_DIR); + this.verboseMessage("Directory added: " + destinationDir + "/" + sourceChild.getName()); + } + } + } + + return result; + } + + private File getInstalledDir(String warFileLocation) + { + // Check for the installed directory in the WAR file + File installedDir = new File(warFileLocation + MODULE_DIR, this.defaultDetector); + if (installedDir.exists() == false) + { + installedDir.mkdir(); + } + return installedDir; + } + + public void disableModule(String moduleId, String warLocation) + { + System.out.println("Currently unsupported ..."); + } + + public void enableModule(String moduleId, String warLocation) + { + System.out.println("Currently unsupported ..."); + } + + public void uninstallModule(String moduleId, String warLocation) + { + System.out.println("Currently unsupported ..."); + } + + public void listModules(String warLocation) + { + System.out.println("Currently unsupported ..."); + } + + private void verboseMessage(String message) + { + if (this.verbose == true) + { + System.out.println(message); + } + } + + private void writeModificationToFile(String fileLocation, Map modifications) + throws IOException + { + File file = new File(fileLocation, this.defaultDetector); + if (file.exists() == false) + { + file.createNewFile(); + } + FileOutputStream os = new FileOutputStream(file); + try + { + for (Map.Entry mod : modifications.entrySet()) + { + String output = mod.getValue() + DELIMITER + mod.getKey() + "\n"; + os.write(output.getBytes()); + } + } + finally + { + os.close(); + } + } + + private Map readModificationsFromFile(String fileLocation) + { + Map modifications = new HashMap(50); + + File file = new File(fileLocation, this.defaultDetector); + try + { + BufferedReader reader = new BufferedReader(new FileReader(file)); + try + { + String line = reader.readLine(); + while (line != null) + { + line = reader.readLine(); + String[] modification = line.split(DELIMITER); + modifications.put(modification[1], modification[0]); + } + } + finally + { + reader.close(); + } + } + catch(FileNotFoundException exception) + { + throw new ModuleManagementToolException("The module file install file '" + fileLocation + "' does not exist"); + } + catch(IOException exception) + { + throw new ModuleManagementToolException("Error whilst reading file '" + fileLocation); + } + + return modifications; + } + + private String getModuleDetailsFileName(String moduleId) + { + return "module-" + moduleId + ".install"; + } + + private String getModuleModificationFileName(String moduleId) + { + return "module-" + moduleId + "-modifications.install"; + } + + /** + * @param args + */ + public static void main(String[] args) + { + if (args.length >= 1) + { + ModuleManagementTool manager = new ModuleManagementTool(); + + String operation = args[0]; + if (operation.equals(OP_INSTALL) == true && args.length >= 3) + { + String aepFileLocation = args[1]; + String warFileLocation = args[2]; + + manager.installModule(aepFileLocation, warFileLocation); + } + else + { + outputUsage(); + } + } + else + { + outputUsage(); + } + } + + private static void outputUsage() + { + System.out.println("output useage ..."); + } + +} diff --git a/source/java/org/alfresco/repo/module/ModuleManagementToolException.java b/source/java/org/alfresco/repo/module/ModuleManagementToolException.java new file mode 100644 index 0000000000..59edd6b7cb --- /dev/null +++ b/source/java/org/alfresco/repo/module/ModuleManagementToolException.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.module; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Module Management Tool Exception class + * + * @author Roy Wetherall + */ +public class ModuleManagementToolException extends AlfrescoRuntimeException +{ + /** + * Serial version UID + */ + private static final long serialVersionUID = -4329693103965834085L; + + public ModuleManagementToolException(String msgId) + { + super(msgId); + } + + public ModuleManagementToolException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + public ModuleManagementToolException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } + + public ModuleManagementToolException(String msgId, Throwable cause) + { + super(msgId, cause); + } +} diff --git a/source/java/org/alfresco/repo/module/ModuleManagementToolTest.java b/source/java/org/alfresco/repo/module/ModuleManagementToolTest.java new file mode 100644 index 0000000000..f942bfc6f5 --- /dev/null +++ b/source/java/org/alfresco/repo/module/ModuleManagementToolTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.module; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.util.FileCopyUtils; + +import de.schlichtherle.io.DefaultRaesZipDetector; +import de.schlichtherle.io.FileOutputStream; +import de.schlichtherle.io.ZipDetector; + +/** + * @author Roy Wetherall + */ +public class ModuleManagementToolTest extends TestCase +{ + private ModuleManagementTool manager = new ModuleManagementTool(); + + ZipDetector defaultDetector = new DefaultRaesZipDetector("amp|war"); + + public void testBasicInstall() + throws Exception + { + manager.setVerbose(true); + + String warLocation = getFileLocation(".war", "module/test.war"); + String ampLocation = getFileLocation(".amp", "module/test.amp"); + + System.out.println(warLocation); + + // Initial install of module + this.manager.installModule(ampLocation, warLocation); + + // Check that the war has been modified correctly + List files = new ArrayList(10); + files.add("/WEB-INF/classes/alfresco/module/module-test.install"); + files.add("/WEB-INF/classes/alfresco/module/module-test-modifications.install"); + files.add("/WEB-INF/lib/test.jar"); + files.add("/WEB-INF/classes/alfresco/module/test/module-context.xml"); + files.add("/WEB-INF/classes/alfresco/module/test"); + files.add("/WEB-INF/licenses/license.txt"); + files.add("/scripts/test.js"); + files.add("/images/test.jpg"); + files.add("/jsp/test.jsp"); + files.add("/css/test.css"); + checkForFileExistance(warLocation, files); + + // Try and install same version + try + { + this.manager.installModule(ampLocation, warLocation); + fail("The module is already installed so an exception should have been raised since we are not forcing an overwite"); + } + catch(ModuleManagementToolException exception) + { + // Pass + } + + // Install a later version + // TODO + + // Try and install and earlier version + // TODO + + + } + + private String getFileLocation(String extension, String location) + throws IOException + { + File file = File.createTempFile("moduleManagementToolTest-", extension); + InputStream is = this.getClass().getClassLoader().getResourceAsStream(location); + OutputStream os = new FileOutputStream(file); + FileCopyUtils.copy(is, os); + return file.getPath(); + } + + private void checkForFileExistance(String warLocation, List files) + { + for (String file : files) + { + File file0 = new de.schlichtherle.io.File(warLocation + file, this.defaultDetector); + assertTrue("The file/dir " + file + " does not exist in the WAR.", file0.exists()); + } + } +} diff --git a/source/java/org/alfresco/repo/module/default-file-mapping.properties b/source/java/org/alfresco/repo/module/default-file-mapping.properties new file mode 100644 index 0000000000..785ecceb06 --- /dev/null +++ b/source/java/org/alfresco/repo/module/default-file-mapping.properties @@ -0,0 +1,8 @@ +# The default AEP => WAR file mappings +/config=/WEB-INF/classes +/lib=/WEB-INF/lib +/licenses=/WEB-INF/licenses +/web/jsp=/jsp +/web/css=/css +/web/images=/images +/web/scripts=/scripts \ No newline at end of file diff --git a/source/java/org/alfresco/service/cmr/module/ModuleDetails.java b/source/java/org/alfresco/service/cmr/module/ModuleDetails.java new file mode 100644 index 0000000000..363c55c8b0 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/module/ModuleDetails.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.module; + +import org.alfresco.util.VersionNumber; + +/** + * Module details, contains the details of an installed alfresco + * module. + * + * @author Roy Wetherall + */ +public class ModuleDetails +{ + private String id; + private VersionNumber version; + private String title; + private String description; + + public ModuleDetails(String id, VersionNumber version, String title, String description) + { + this.id = id; + this.version = version; + this.title = title; + } + + public String getId() + { + return id; + } + + public VersionNumber getVersion() + { + return version; + } + + public String getTitle() + { + return title; + } + + public String getDescription() + { + return description; + } +} diff --git a/source/java/org/alfresco/service/cmr/module/ModuleService.java b/source/java/org/alfresco/service/cmr/module/ModuleService.java new file mode 100644 index 0000000000..c7d3b177dc --- /dev/null +++ b/source/java/org/alfresco/service/cmr/module/ModuleService.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.service.cmr.module; + +import java.util.List; + +/** + * Module service. Provides information about the currently installed alfresco + * modules. + * + * @author Roy Wetherall + */ +public interface ModuleService +{ + public ModuleDetails getModule(String moduleId); + + public List getAllModules(); +} diff --git a/source/test-resources/module/test.amp b/source/test-resources/module/test.amp new file mode 100644 index 0000000000..d9915a2691 Binary files /dev/null and b/source/test-resources/module/test.amp differ diff --git a/source/test-resources/module/test.war b/source/test-resources/module/test.war new file mode 100644 index 0000000000..5549ecc3a5 Binary files /dev/null and b/source/test-resources/module/test.war differ