diff --git a/core/.gitattributes b/core/.gitattributes new file mode 100644 index 0000000000..8303247a39 --- /dev/null +++ b/core/.gitattributes @@ -0,0 +1,8 @@ +.* eol=crlf +*.html eol=crlf +*.java eol=crlf +*.txt eol=crlf +*.css eol=crlf +*.xml eol=crlf +*.js eol=crlf +*.properties eol=crlf \ No newline at end of file diff --git a/core/.gitbugtraq b/core/.gitbugtraq new file mode 100644 index 0000000000..bacffb702d --- /dev/null +++ b/core/.gitbugtraq @@ -0,0 +1,4 @@ +# For SmartGit +[bugtraq "jira"] + url = https://issues.alfresco.com/jira/browse/%BUGID% + logRegex = ([A-Z]+-\\d+) diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000000..9346843361 --- /dev/null +++ b/core/.gitignore @@ -0,0 +1,34 @@ +*.class + +# Eclipse +.classpath +.settings +.project + +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +target +*.log +*.log.* + +# Mobile Tools for Java (J2ME) + +.mtj +.tmp/ + +# Package Files # + +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +hs_err_pid* diff --git a/core/.travis.settings.xml b/core/.travis.settings.xml new file mode 100644 index 0000000000..dffe7fbfe1 --- /dev/null +++ b/core/.travis.settings.xml @@ -0,0 +1,10 @@ + + + + + alfresco-public + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + diff --git a/core/.travis.yml b/core/.travis.yml new file mode 100644 index 0000000000..203105794c --- /dev/null +++ b/core/.travis.yml @@ -0,0 +1,52 @@ +dist: trusty +sudo: required +language: java +jdk: + - openjdk11 + +cache: + directories: + - $HOME/.m2 + +branches: + only: + - master + - /support\/.*/ + +install: travis_retry mvn install -DskipTests=true -B -V + +stages: + - test + - release + +jobs: + include: + - stage: test + name: "Build and test" + script: travis_retry mvn test + - name: "WhiteSource scan" + # only on SP branches or master and if it is not a PR + if: fork = false AND (branch = master OR branch =~ /support\/SP\/.*/) AND type != pull_request + script: + # Download the latest version of WhiteSource Unified Agent + - curl -LJO https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar + # Run WhiteSource Unified Agent + - java -jar wss-unified-agent.jar -apiKey ${WHITESOURCE_API_KEY} -c .wss-unified-agent.config + - name: "Source Clear Scan" + # only on SP branches or master and if it is not a PR + if: fork = false AND (branch = master OR branch =~ /support\/SP\/.*/) AND type != pull_request + script: skip + addons: + srcclr: true + - stage: release + name: "Push to Nexus" + if: fork = false AND (branch = master OR branch =~ /support\/.*/) AND type != pull_request AND commit_message !~ /\[no-release\]/ + before_install: + - "cp .travis.settings.xml $HOME/.m2/settings.xml" + script: + # Use full history for release + - git checkout -B "${TRAVIS_BRANCH}" + # Add email to link commits to user + - git config user.email "${GIT_EMAIL}" + # Skip building of release commits + - mvn --batch-mode -q -DscmCommentPrefix="[maven-release-plugin][skip ci] " -Dusername="${GIT_USERNAME}" -Dpassword="${GIT_PASSWORD}" -DskipTests -Darguments=-DskipTests release:clean release:prepare release:perform diff --git a/core/.whitesource b/core/.whitesource new file mode 100644 index 0000000000..f056952141 --- /dev/null +++ b/core/.whitesource @@ -0,0 +1,8 @@ +{ + "generalSettings": { + "shouldScanRepo": true + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure" + } +} \ No newline at end of file diff --git a/core/.wss-unified-agent.config b/core/.wss-unified-agent.config new file mode 100644 index 0000000000..6f9cb57795 --- /dev/null +++ b/core/.wss-unified-agent.config @@ -0,0 +1,228 @@ +#################################################################### +# WhiteSource Unified-Agent configuration file +#################################################################### +########################################## +# GENERAL SCAN MODE: Files and Package Managers +########################################## + +checkPolicies=true +forceCheckAllDependencies=true +forceUpdate=true +forceUpdate.failBuildOnPolicyViolation=true +offline=false +#ignoreSourceFiles=true +#scanComment= +#updateInventory=false +#resolveAllDependencies=false +#failErrorLevel=ALL +#requireKnownSha1=false +#generateScanReport=true +#scanReportTimeoutMinutes=10 +#excludeDependenciesFromNodes=.*commons-io.*,.*maven-model + +#projectPerFolder=true +#projectPerFolderIncludes= +#projectPerFolderExcludes= + +#wss.connectionTimeoutMinutes=60 +# Change the below URL to your WhiteSource server. +# Use the 'WhiteSource Server URL' which can be retrieved +# from your 'Profile' page on the 'Server URLs' panel. +# Then, add the '/agent' path to it. +wss.url=https://saas.whitesourcesoftware.com/agent + +#npm.resolveDependencies=false +#npm.ignoreSourceFiles=false +#npm.includeDevDependencies=true +#npm.runPreStep=true +#npm.ignoreNpmLsErrors=true +#npm.ignoreScripts=true +#npm.yarnProject=true +#npm.accessToken= +#npm.identifyByNameAndVersion=true + +#bower.resolveDependencies=false +#bower.ignoreSourceFiles=true +#bower.runPreStep=true + +#nuget.resolvePackagesConfigFiles=false +#nuget.resolveCsProjFiles=false +#nuget.resolveDependencies=false +#nuget.restoreDependencies=true +#nuget.ignoreSourceFiles=true +#nuget.runPreStep=true +#nuget.resolveNuspecFiles=false + +#python.resolveDependencies=false +#python.ignoreSourceFiles=false +#python.ignorePipInstallErrors=true +#python.installVirtualenv=true +#python.resolveHierarchyTree=false +#python.requirementsFileIncludes=requirements.txt +#python.resolveSetupPyFiles=true +#python.runPipenvPreStep=true +#python.pipenvDevDependencies=true +#python.IgnorePipenvInstallErrors=true + +#maven.ignoredScopes=test provided +maven.resolveDependencies=true +#maven.ignoreSourceFiles=true +#maven.aggregateModules=true +maven.ignorePomModules=false +#maven.runPreStep=true +#maven.ignoreMvnTreeErrors=true +#maven.environmentPath= +#maven.m2RepositoryPath= + +#gradle.ignoredScopes= +#gradle.resolveDependencies=false +#gradle.runAssembleCommand=false +#gradle.runPreStep=true +#gradle.ignoreSourceFiles=true +#gradle.aggregateModules=true +#gradle.preferredEnvironment=wrapper +#gradle.localRepositoryPath= + +#paket.resolveDependencies=false +#paket.ignoredGroups= +#paket.ignoreSourceFiles=false +#paket.runPreStep=true +#paket.exePath= + +#go.resolveDependencies=false +#go.collectDependenciesAtRuntime=true +#go.dependencyManager= +#go.ignoreSourceFiles=true +#go.glide.ignoreTestPackages=false +#go.gogradle.enableTaskAlias=true + +#ruby.resolveDependencies = false +#ruby.ignoreSourceFiles = false +#ruby.installMissingGems = true +#ruby.runBundleInstall = true +#ruby.overwriteGemFile = true + +#sbt.resolveDependencies=false +#sbt.ignoreSourceFiles=true +#sbt.aggregateModules=true +#sbt.runPreStep=true +#sbt.targetFolder= + +#php.resolveDependencies=false +#php.runPreStep=true +#php.includeDevDependencies=true + +#html.resolveDependencies=false + +#cocoapods.resolveDependencies=false +#cocoapods.runPreStep=true +#cocoapods.ignoreSourceFiles=false + +#hex.resolveDependencies=false +#hex.runPreStep=true +#hex.ignoreSourceFiles=false +#hex.aggregateModules=true + +################################## +# Organization tokens: +################################## +apiKey= + +#userKey is required if WhiteSource administrator has enabled "Enforce user level access" option +#userKey= + +projectName=alfresco-core +projectVersion= +projectToken= + +productName=ACS Community +productVersion= +productToken= +#updateType=APPEND +#requesterEmail=user@provider.com + +######################################################################################### +# Includes/Excludes Glob patterns - PLEASE USE ONLY ONE EXCLUDE LINE AND ONE INCLUDE LINE +######################################################################################### +#includes=**/*.c **/*.cc **/*.cp **/*.cpp **/*.cxx **/*.c++ **/*.h **/*.hpp **/*.hxx + +#includes=**/*.m **/*.mm **/*.js **/*.php +includes=**/*.jar +#includes=**/*.gem **/*.rb +#includes=**/*.dll **/*.cs **/*.nupkg +#includes=**/*.tgz **/*.deb **/*.gzip **/*.rpm **/*.tar.bz2 +#includes=**/*.zip **/*.tar.gz **/*.egg **/*.whl **/*.py + +## Exclude file extensions or specific directories by adding **/*. or **/** +excludes=**/*sources.jar **/*javadoc.jar + +case.sensitive.glob=false +followSymbolicLinks=true + +################################## +# Archive properties +################################## +#archiveExtractionDepth=2 +#archiveIncludes=**/*.war **/*.ear +#archiveExcludes=**/*sources.jar + +################################## +# Proxy settings +################################## +#proxy.host= +#proxy.port= +#proxy.user= +#proxy.pass= + +################################## +# SCM settings +################################## +#scm.type= +#scm.user= +#scm.pass= +#scm.ppk= +#scm.url= +#scm.branch= +#scm.tag= +#scm.npmInstall= +#scm.npmInstallTimeoutMinutes= +#scm.repositoriesFile= + +############################################## +# SCAN MODE: Linux package manager settings +############################################## +#scanPackageManager=true + +################################## +# SCAN MODE: Docker images +################################## +#docker.scanImages=true +#docker.includes=.*.* +#docker.excludes= +#docker.pull.enable=true +#docker.pull.images=.*.* +#docker.pull.maxImages=10 +#docker.pull.tags=.*.* +#docker.pull.digest= +#docker.delete.force=true +#docker.login.sudo=false + +#docker.aws.enable=true +#docker.aws.registryIds= + +################################## +# SCAN MODE: Docker containers +################################## +#docker.scanContainers=true +#docker.containerIncludes=.*.* +#docker.containerExcludes= + +################################ +# Serverless settings +################################ +#serverless.provider= +#serverless.scanFunctions=true +#serverless.includes= +#serverless.excludes= +#serverless.region= +#serverless.maxFunctions=10 diff --git a/core/CONTRIBUTING.md b/core/CONTRIBUTING.md new file mode 100644 index 0000000000..8d059e27ca --- /dev/null +++ b/core/CONTRIBUTING.md @@ -0,0 +1,16 @@ +### Contributing +Thanks for your interest in contributing to this project! + +The following is a set of guidelines for contributing to this library. Most of them will make the life of the reviewer easier and therefore decrease the time required for the patch be included in the next version. + +Because this project forms a part of Alfresco Content Services, the guidelines are hosted in the [Alfresco Social Community](http://community.alfresco.com/community/ecm) where they can be referenced from multiple projects. + +Read an [overview on how this project is goverened](https://community.alfresco.com/docs/DOC-6385-project-overview-repository). + +You can report an issue in the ALF project of the [Alfresco issue tracker](http://issues.alfresco.com). + +Read [instructions for a good issue report](https://community.alfresco.com/docs/DOC-6263-reporting-an-issue). + +Read [instructions for making a contribution](https://community.alfresco.com/docs/DOC-6269-submitting-contributions). + +Please follow [the coding standards](https://community.alfresco.com/docs/DOC-4658-coding-standards). diff --git a/core/LICENSE.txt b/core/LICENSE.txt new file mode 100644 index 0000000000..65c5ca88a6 --- /dev/null +++ b/core/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000000..13462dd056 --- /dev/null +++ b/core/README.md @@ -0,0 +1,42 @@ +### Alfresco Core +[![Build Status](https://travis-ci.com/Alfresco/alfresco-core.svg?branch=master)](https://travis-ci.com/Alfresco/alfresco-core) + +Alfresco Core is a library packaged as a jar file which is part of [Alfresco Content Services Repository](https://community.alfresco.com/docs/DOC-6385-project-overview-repository). +The library contains the following: +* Various helpers and utils +* Canned queries interface and supporting classes +* Generic encryption supporting classes + +Version 7 of the library uses Spring 5, Quartz 2.3 and does not have Hibernate dependency. + +### Building and testing +The project can be built and tested by running Maven command: +~~~ +mvn clean install +~~~ + +### Artifacts +The artifacts can be obtained by: +* downloading from [Alfresco repository](https://artifacts.alfresco.com/nexus/content/groups/public) +* getting as Maven dependency by adding the dependency to your pom file: +~~~ + + org.alfresco + alfresco-core + version + +~~~ +and Alfresco repository: +~~~ + + alfresco-maven-repo + https://artifacts.alfresco.com/nexus/content/groups/public + +~~~ +The SNAPSHOT version of the artifact is **never** published. + +### Old version history +The history for older versions can be found in [Alfresco SVN](https://svn.alfresco.com/repos/alfresco-open-mirror/services/alfresco-core/) + +### Contributing guide +Please use [this guide](CONTRIBUTING.md) to make a contribution to the project. diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000000..00a51e925d --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,206 @@ + + 4.0.0 + + + org.alfresco + alfresco-super-pom + 12 + + alfresco-core + 7.33 + Alfresco Core + Alfresco core libraries and utils + + + scm:git:https://github.com/Alfresco/alfresco-core.git + scm:git:https://github.com/Alfresco/alfresco-core.git + https://github.com/Alfresco/alfresco-core + 7.33 + + + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/repositories/releases + + + + + 5.1.15.RELEASE + 7.14 + 11 + + + + + commons-codec + commons-codec + 1.14 + + + commons-httpclient + commons-httpclient + 3.1-HTTPCLIENT-1265 + + + commons-logging + commons-logging + 1.2 + + + commons-io + commons-io + 2.6 + + + org.apache.commons + commons-math3 + 3.6.1 + + + org.safehaus.jug + jug + 2.0.0 + asl + + + log4j + log4j + 1.2.17 + + + org.json + json + 20160212 + + + org.springframework + spring-orm + ${dependency.spring.version} + + + + + org.springframework + spring-jcl + + + + + org.springframework + spring-context + ${dependency.spring.version} + + + org.quartz-scheduler + quartz + 2.3.2 + + + + + com.mchange + * + + + + + org.alfresco.surf + spring-surf-core-configservice + ${dependency.surf.version} + + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-impl + 2.3.2 + + + com.sun.xml.bind + jaxb-core + 2.3.0.1 + + + org.codehaus.guessencoding + guessencoding + 1.4 + + + javax.transaction + jta + 1.1 + + + joda-time + joda-time + 2.10.5 + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + + + org.slf4j + slf4j-log4j12 + 1.7.30 + test + + + junit + junit + 4.13 + test + + + org.mockito + mockito-core + 3.2.4 + test + + + commons-dbcp + commons-dbcp + 1.4-DBCP330 + test + + + + + + + + maven-release-plugin + + true + @{project.version} + + + + + + + + maven-jar-plugin + 3.2.0 + + + + test-jar + + + + + + + + diff --git a/core/src/main/java/org/alfresco/api/AlfrescoPublicApi.java b/core/src/main/java/org/alfresco/api/AlfrescoPublicApi.java new file mode 100644 index 0000000000..c3f66179a8 --- /dev/null +++ b/core/src/main/java/org/alfresco/api/AlfrescoPublicApi.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to denote a class or method as part + * of the public API. When a class or method is so designated then + * we will not change it within a release in a way that would make + * it no longer backwardly compatible with an earlier version within + * the release. + * + * @author Greg Melahn + */ +@Target( {ElementType.TYPE,ElementType.METHOD} ) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AlfrescoPublicApi +{ +} diff --git a/core/src/main/java/org/alfresco/config/AlfrescoPropertiesPersister.java b/core/src/main/java/org/alfresco/config/AlfrescoPropertiesPersister.java new file mode 100644 index 0000000000..b3d1ff7622 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/AlfrescoPropertiesPersister.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Enumeration; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.StringUtils; + +/** + * Simple extension to the{@link DefaultPropertiesPersister} to strip trailing whitespace + * from incoming properties. + * + * @author shane frensley + * @see org.springframework.util.DefaultPropertiesPersister + */ +public class AlfrescoPropertiesPersister extends DefaultPropertiesPersister +{ + + private static Log logger = LogFactory.getLog(AlfrescoPropertiesPersister.class); + + @Override + public void load(Properties props, InputStream is) throws IOException + { + super.load(props, is); + strip(props); + } + + @Override + public void load(Properties props, Reader reader) throws IOException + { + super.load(props, reader); + strip(props); + } + + public void loadFromXml(Properties props, InputStream is) throws IOException + { + super.loadFromXml(props, is); + strip(props); + } + + private void strip(Properties props) + { + for (Enumeration keys = props.keys(); keys.hasMoreElements();) + { + String key = (String) keys.nextElement(); + String val = StringUtils.trimTrailingWhitespace(props.getProperty(key)); + if (logger.isTraceEnabled()) + { + logger.trace("Trimmed trailing whitespace for property " + key + " = " + val); + } + props.setProperty(key, val); + } + } +} diff --git a/core/src/main/java/org/alfresco/config/JndiObjectFactoryBean.java b/core/src/main/java/org/alfresco/config/JndiObjectFactoryBean.java new file mode 100644 index 0000000000..9a2a90205d --- /dev/null +++ b/core/src/main/java/org/alfresco/config/JndiObjectFactoryBean.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.sql.Connection; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +/** + * An extended version of JndiObjectFactoryBean that actually tests a JNDI data source before falling back to its + * default object. Allows continued backward compatibility with old-style datasource configuration. + * + * @author dward + */ +public class JndiObjectFactoryBean extends org.springframework.jndi.JndiObjectFactoryBean +{ + + @Override + protected Object lookup() throws NamingException + { + Object candidate = super.lookup(); + if (candidate instanceof DataSource) + { + Connection con = null; + try + { + con = ((DataSource) candidate).getConnection(); + } + catch (Exception e) + { + NamingException e1 = new NamingException("Unable to get connection from " + getJndiName()); + e1.setRootCause(e); + throw e1; + } + finally + { + try + { + if (con != null) + { + con.close(); + } + } + catch (Exception e) + { + } + } + } + return candidate; + } +} diff --git a/core/src/main/java/org/alfresco/config/JndiPropertiesFactoryBean.java b/core/src/main/java/org/alfresco/config/JndiPropertiesFactoryBean.java new file mode 100644 index 0000000000..70453690e0 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/JndiPropertiesFactoryBean.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.util.Properties; + +import javax.naming.NamingException; + +import org.springframework.jndi.JndiTemplate; + +/** + * An extended {@link SystemPropertiesFactoryBean} that allows properties to be set through JNDI entries in + * java:comp/env/properties/*. The precedence given to system properties is still as per the superclass. + * + * @author dward + */ +public class JndiPropertiesFactoryBean extends SystemPropertiesFactoryBean +{ + private JndiTemplate jndiTemplate = new JndiTemplate(); + + @Override + protected void resolveMergedProperty(String propertyName, Properties props) + { + try + { + Object value = this.jndiTemplate.lookup("java:comp/env/properties/" + propertyName); + if (value != null) + { + String stringValue = value.toString(); + if (stringValue.length() > 0) + { + // Unfortunately, JBoss 4 wrongly expects every env-entry declared in web.xml to have an + // env-entry-value (even though these are meant to be decided on deployment!). So we treat the empty + // string as null. + props.setProperty(propertyName, stringValue); + } + } + } + catch (NamingException e) + { + // Fall back to merged value in props + } + } +} diff --git a/core/src/main/java/org/alfresco/config/JndiPropertyPlaceholderConfigurer.java b/core/src/main/java/org/alfresco/config/JndiPropertyPlaceholderConfigurer.java new file mode 100644 index 0000000000..b775ad0110 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/JndiPropertyPlaceholderConfigurer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.util.Properties; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.jndi.JndiTemplate; + +/** + * An extended {@link PropertyPlaceholderConfigurer} that allows properties to be set through JNDI entries in + * java:comp/env/properties/*. The precedence given to system properties is still as per the superclass. + * + * @author dward + */ +public class JndiPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer +{ + private JndiTemplate jndiTemplate = new JndiTemplate(); + + @Override + protected String resolvePlaceholder(String placeholder, Properties props) + { + String result = null; + try + { + Object value = this.jndiTemplate.lookup("java:comp/env/properties/" + placeholder); + if (value != null) + { + result = value.toString(); + } + } + catch (NamingException e) + { + } + // Unfortunately, JBoss 4 wrongly expects every env-entry declared in web.xml to have an env-entry-value (even + // though these are meant to be decided on deployment!). So we treat the empty string as null. + return result == null || result.length() == 0 ? super.resolvePlaceholder(placeholder, props) : result; + } +} diff --git a/core/src/main/java/org/alfresco/config/NonBlockingLazyInitTargetSource.java b/core/src/main/java/org/alfresco/config/NonBlockingLazyInitTargetSource.java new file mode 100644 index 0000000000..b7d3f33720 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/NonBlockingLazyInitTargetSource.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource; +import org.springframework.beans.BeansException; + +/** + * A non-blocking version of LazyInitTargetSource. + * + * @author dward + */ +public class NonBlockingLazyInitTargetSource extends AbstractBeanFactoryBasedTargetSource +{ + + private static final long serialVersionUID = 4509578245779492037L; + private Object target; + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + public Object getTarget() throws BeansException + { + this.lock.readLock().lock(); + try + { + if (this.target != null) + { + return this.target; + } + } + finally + { + this.lock.readLock().unlock(); + } + this.lock.writeLock().lock(); + try + { + if (this.target == null) + { + this.target = getBeanFactory().getBean(getTargetBeanName()); + } + return this.target; + } + finally + { + this.lock.writeLock().unlock(); + } + } +} diff --git a/core/src/main/java/org/alfresco/config/PathMatchingHelper.java b/core/src/main/java/org/alfresco/config/PathMatchingHelper.java new file mode 100644 index 0000000000..d0f1c09ab6 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/PathMatchingHelper.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.io.IOException; +import java.net.URL; +import java.util.Set; + +import org.springframework.core.io.Resource; +import org.springframework.util.PathMatcher; + +/** + * An interface for plug ins to JBossEnabledResourcePatternResolver that avoids direct dependencies on + * application server specifics. + * + * @author dward + */ +public interface PathMatchingHelper +{ + /** + * Indicates whether this helper is capable of searching the given URL (i.e. its protocol is supported). + * + * @param rootURL + * the root url to be searched + * @return true if this helper is capable of searching the given URL + */ + public boolean canHandle(URL rootURL); + + /** + * Gets the resource at the given URL. + * + * @param url URL + * @return the resource at the given URL + * @throws IOException + * for any error + */ + public Resource getResource(URL url) throws IOException; + + /** + * Gets the set of resources under the given URL whose path matches the given sub pattern. + * + * @param matcher + * the matcher + * @param rootURL + * the root URL to be searched + * @param subPattern + * the ant-style pattern to match + * @return the set of matching resources + * @throws IOException + * for any error + */ + public Set getResources(PathMatcher matcher, URL rootURL, String subPattern) throws IOException; +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/config/SystemPropertiesFactoryBean.java b/core/src/main/java/org/alfresco/config/SystemPropertiesFactoryBean.java new file mode 100644 index 0000000000..f9ebb16658 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/SystemPropertiesFactoryBean.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.core.Constants; + +/** + * Like the parent PropertiesFactoryBean, but overrides or augments the resulting property set with values + * from VM system properties. As with the Spring {@link PropertyPlaceholderConfigurer} the following modes are + * supported: + *
    + *
  • SYSTEM_PROPERTIES_MODE_NEVER: Don't use system properties at all.
  • + *
  • SYSTEM_PROPERTIES_MODE_FALLBACK: Fallback to a system property only for undefined properties.
  • + *
  • SYSTEM_PROPERTIES_MODE_OVERRIDE: (DEFAULT)Use a system property if it is available.
  • + *
+ * Note that system properties will only be included in the property set if defaults for the property have already been + * defined using {@link #setProperties(Properties)} or {@link #setLocations(org.springframework.core.io.Resource[])} or + * their names have been included explicitly in the set passed to {@link #setSystemProperties(Set)}. + * + * @author Derek Hulley + */ +public class SystemPropertiesFactoryBean extends PropertiesFactoryBean +{ + private static final Constants constants = new Constants(PropertyPlaceholderConfigurer.class); + + private int systemPropertiesMode = PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE; + private Set systemProperties = Collections.emptySet(); + + /** + * Set the system property mode by the name of the corresponding constant, e.g. "SYSTEM_PROPERTIES_MODE_OVERRIDE". + * + * @param constantName + * name of the constant + * @throws java.lang.IllegalArgumentException + * if an invalid constant was specified + * @see #setSystemPropertiesMode + */ + public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException + { + this.systemPropertiesMode = SystemPropertiesFactoryBean.constants.asNumber(constantName).intValue(); + } + + /** + * Set how to check system properties. + * + * @see PropertyPlaceholderConfigurer#setSystemPropertiesMode(int) + */ + public void setSystemPropertiesMode(int systemPropertiesMode) + { + this.systemPropertiesMode = systemPropertiesMode; + } + + /** + * Set the names of the properties that can be considered for overriding. + * + * @param systemProperties + * a set of properties that can be fetched from the system properties + */ + public void setSystemProperties(Set systemProperties) + { + this.systemProperties = systemProperties; + } + + @SuppressWarnings("unchecked") + @Override + protected Properties mergeProperties() throws IOException + { + // First do the default merge + Properties props = super.mergeProperties(); + + // Now resolve all the merged properties + if (this.systemPropertiesMode == PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_NEVER) + { + // If we are in never mode, we don't refer to system properties at all + for (String systemProperty : (Set) (Set) props.keySet()) + { + resolveMergedProperty(systemProperty, props); + } + } + else + { + // Otherwise, we allow unset properties to drift through from the systemProperties set and potentially set + // ones to be overriden by system properties + Set propNames = new HashSet((Set) (Set) props.keySet()); + propNames.addAll(this.systemProperties); + for (String systemProperty : propNames) + { + resolveMergedProperty(systemProperty, props); + if (this.systemPropertiesMode == PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_FALLBACK + && props.containsKey(systemProperty)) + { + // It's already there + continue; + } + // Get the system value and assign if present + String systemPropertyValue = System.getProperty(systemProperty); + if (systemPropertyValue != null) + { + props.put(systemProperty, systemPropertyValue); + } + } + } + return props; + } + + /** + * Override hook. Allows subclasses to resolve a merged property from an alternative source, whilst still respecting + * the chosen system property fallback path. + * + * @param systemProperty String + * @param props Properties + */ + protected void resolveMergedProperty(String systemProperty, Properties props) + { + } +} diff --git a/core/src/main/java/org/alfresco/config/SystemPropertiesSetterBean.java b/core/src/main/java/org/alfresco/config/SystemPropertiesSetterBean.java new file mode 100644 index 0000000000..a00fe869d5 --- /dev/null +++ b/core/src/main/java/org/alfresco/config/SystemPropertiesSetterBean.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Takes a set of properties and pushes them into the Java environment. Usually, VM properties + * are required by the system (see {@link SystemPropertiesFactoryBean} and + * Spring's PropertyPlaceholderConfigurer); sometimes it is necessary to take properties + * available to Spring and push them onto the VM. + *

+ * For simplicity, the system property, if present, will NOT be modified. Also, property placeholders + * (${...}), empty values and null values will be ignored. + *

+ * Use the {@link #init()} method to push the properties. + * + * @author Derek Hulley + * @since V3.1 + */ +public class SystemPropertiesSetterBean +{ + private static Log logger = LogFactory.getLog(SystemPropertiesSetterBean.class); + + private Map propertyMap; + + SystemPropertiesSetterBean() + { + propertyMap = new HashMap(3); + } + + /** + * Set the properties that will be pushed onto the JVM. + * + * @param propertyMap a map of property name to property value + */ + public void setPropertyMap(Map propertyMap) + { + this.propertyMap = propertyMap; + } + + public void init() + { + for (Map.Entry entry : propertyMap.entrySet()) + { + String name = entry.getKey(); + String value = entry.getValue(); + // Some values can be ignored + if (value == null || value.length() == 0) + { + continue; + } + if (value.startsWith("${") && value.endsWith("}")) + { + continue; + } + // Check the system properties + if (System.getProperty(name) != null) + { + // It was already there + if (logger.isDebugEnabled()) + { + logger.debug("\n" + + "Not pushing up system property: \n" + + " Property: " + name + "\n" + + " Value already present: " + System.getProperty(name) + "\n" + + " Value provided: " + value); + } + continue; + } + System.setProperty(name, value); + } + } +} diff --git a/core/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java b/core/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java new file mode 100644 index 0000000000..cceb32c944 --- /dev/null +++ b/core/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encoding; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @since 2.1 + * @author Derek Hulley + */ +public abstract class AbstractCharactersetFinder implements CharactersetFinder +{ + private static Log logger = LogFactory.getLog(AbstractCharactersetFinder.class); + private static boolean isDebugEnabled = logger.isDebugEnabled(); + + private int bufferSize; + + public AbstractCharactersetFinder() + { + this.bufferSize = 8192; + } + + /** + * Set the maximum number of bytes to read ahead when attempting to determine the characterset. + * Most characterset detectors are efficient and can process 8K of buffered data very quickly. + * Some, may need to be constrained a bit. + * + * @param bufferSize the number of bytes - default 8K. + */ + public void setBufferSize(int bufferSize) + { + this.bufferSize = bufferSize; + } + + /** + * {@inheritDoc} + *

+ * The input stream is checked to ensure that it supports marks, after which + * a buffer is extracted, leaving the stream in its original state. + */ + public final Charset detectCharset(InputStream is) + { + // Only support marking streams + if (!is.markSupported()) + { + throw new IllegalArgumentException("The InputStream must support marks. Wrap the stream in a BufferedInputStream."); + } + try + { + int bufferSize = getBufferSize(); + if (bufferSize < 0) + { + throw new RuntimeException("The required buffer size may not be negative: " + bufferSize); + } + // Mark the stream for just a few more than we actually will need + is.mark(bufferSize); + // Create a buffer to hold the data + byte[] buffer = new byte[bufferSize]; + // Fill it + int read = is.read(buffer); + // Create an appropriately sized buffer + if (read > -1 && read < buffer.length) + { + byte[] copyBuffer = new byte[read]; + System.arraycopy(buffer, 0, copyBuffer, 0, read); + buffer = copyBuffer; + } + // Detect + return detectCharset(buffer); + } + catch (IOException e) + { + // Attempt a reset + throw new AlfrescoRuntimeException("IOException while attempting to detect charset encoding.", e); + } + finally + { + try { is.reset(); } catch (Throwable ee) {} + } + } + + public final Charset detectCharset(byte[] buffer) + { + try + { + Charset charset = detectCharsetImpl(buffer); + // Done + if (isDebugEnabled) + { + if (charset == null) + { + // Read a few characters for debug purposes + logger.debug("\n" + + "Failed to identify stream character set: \n" + + " Guessed 'chars': " + Arrays.toString(buffer)); + } + else + { + // Read a few characters for debug purposes + logger.debug("\n" + + "Identified character set from stream:\n" + + " Charset: " + charset + "\n" + + " Detected chars: " + new String(buffer, charset.name())); + } + } + return charset; + } + catch (Throwable e) + { + logger.error("IOException while attempting to detect charset encoding.", e); + return null; + } + } + + /** + * Some implementations may only require a few bytes to do detect the stream type, + * whilst others may be more efficient with larger buffers. In either case, the + * number of bytes actually present in the buffer cannot be enforced. + *

+ * Only override this method if there is a very compelling reason to adjust the buffer + * size, and then consider handling the {@link #setBufferSize(int)} method by issuing a + * warning. This will prevent users from setting the buffer size when it has no effect. + * + * @return Returns the maximum desired size of the buffer passed + * to the {@link CharactersetFinder#detectCharset(byte[])} method. + * + * @see #setBufferSize(int) + */ + protected int getBufferSize() + { + return bufferSize; + } + + /** + * Worker method for implementations to override. All exceptions will be reported and + * absorbed and null returned. + *

+ * The interface contract is that the data buffer must not be altered in any way. + * + * @param buffer the buffer of data no bigger than the requested + * {@linkplain #getBufferSize() best buffer size}. This can, + * very efficiently, be turned into an InputStream using a + * ByteArrayInputStream. + * @return Returns the charset or null if an accurate conclusion + * is not possible + * @throws Exception Any exception, checked or not + */ + protected abstract Charset detectCharsetImpl(byte[] buffer) throws Exception; +} diff --git a/core/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java b/core/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java new file mode 100644 index 0000000000..d4908c0320 --- /dev/null +++ b/core/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encoding; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Byte Order Marker encoding detection. + * + * @since 2.1 + * @author Pacific Northwest National Lab + * @author Derek Hulley + */ +public class BomCharactersetFinder extends AbstractCharactersetFinder +{ + private static Log logger = LogFactory.getLog(BomCharactersetFinder.class); + + @Override + public void setBufferSize(int bufferSize) + { + logger.warn("Setting the buffersize has no effect for charset finder: " + BomCharactersetFinder.class.getName()); + } + + /** + * @return Returns 64 + */ + @Override + protected int getBufferSize() + { + return 64; + } + + /** + * Just searches the Byte Order Marker, i.e. the first three characters for a sign of + * the encoding. + */ + protected Charset detectCharsetImpl(byte[] buffer) throws Exception + { + Charset charset = null; + ByteArrayInputStream bis = null; + try + { + bis = new ByteArrayInputStream(buffer); + bis.mark(3); + char[] byteHeader = new char[3]; + InputStreamReader in = new InputStreamReader(bis); + int bytesRead = in.read(byteHeader); + bis.reset(); + + if (bytesRead < 2) + { + // ASCII + charset = Charset.forName("Cp1252"); + } + else if ( + byteHeader[0] == 0xFE && + byteHeader[1] == 0xFF) + { + // UCS-2 Big Endian + charset = Charset.forName("UTF-16BE"); + } + else if ( + byteHeader[0] == 0xFF && + byteHeader[1] == 0xFE) + { + // UCS-2 Little Endian + charset = Charset.forName("UTF-16LE"); + } + else if ( + bytesRead >= 3 && + byteHeader[0] == 0xEF && + byteHeader[1] == 0xBB && + byteHeader[2] == 0xBF) + { + // UTF-8 + charset = Charset.forName("UTF-8"); + } + else + { + // No idea + charset = null; + } + // Done + return charset; + } + finally + { + if (bis != null) + { + try { bis.close(); } catch (Throwable e) {} + } + } + } +} diff --git a/core/src/main/java/org/alfresco/encoding/CharactersetFinder.java b/core/src/main/java/org/alfresco/encoding/CharactersetFinder.java new file mode 100644 index 0000000000..365b55bcf0 --- /dev/null +++ b/core/src/main/java/org/alfresco/encoding/CharactersetFinder.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encoding; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Interface for classes that are able to read a text-based input stream and determine + * the character encoding. + *

+ * There are quite a few libraries that do this, but none are perfect. It is therefore + * necessary to abstract the implementation to allow these finders to be configured in + * as required. + *

+ * Implementations should have a default constructor and be completely thread safe and + * stateless. This will allow them to be constructed and held indefinitely to do the + * decoding work. + *

+ * Where the encoding cannot be determined, it is left to the client to decide what to do. + * Some implementations may guess and encoding or use a default guess - it is up to the + * implementation to specify the behaviour. + * + * @since 2.1 + * @author Derek Hulley + */ +public interface CharactersetFinder +{ + /** + * Attempt to detect the character set encoding for the give input stream. The input + * stream will not be altered or closed by this method, and must therefore support + * marking. If the input stream available doesn't support marking, then it can be wrapped with + * a {@link BufferedInputStream}. + *

+ * The current state of the stream will be restored before the method returns. + * + * @param is an input stream that must support marking + * @return Returns the encoding of the stream, + * or null if encoding cannot be identified + */ + public Charset detectCharset(InputStream is); + + /** + * Attempt to detect the character set encoding for the given buffer. + * + * @param buffer the first n bytes of the character stream + * @return Returns the encoding of the buffer, + * or null if encoding cannot be identified + */ + public Charset detectCharset(byte[] buffer); +} diff --git a/core/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java b/core/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java new file mode 100644 index 0000000000..92cd35f1c6 --- /dev/null +++ b/core/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encoding; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; + +import com.glaforge.i18n.io.CharsetToolkit; + +/** + * Uses the Guess Encoding + * library. + * + * @since 2.1 + * @author Derek Hulley + */ +public class GuessEncodingCharsetFinder extends AbstractCharactersetFinder +{ + /** Dummy charset to detect the default guess */ + private static final Charset DUMMY_CHARSET = new DummyCharset(); + + @Override + protected Charset detectCharsetImpl(byte[] buffer) throws Exception + { + CharsetToolkit charsetToolkit = new CharsetToolkit(buffer, DUMMY_CHARSET); + charsetToolkit.setEnforce8Bit(true); // Force the default instead of a guess + Charset charset = charsetToolkit.guessEncoding(); + if (charset == DUMMY_CHARSET) + { + return null; + } + else + { + return charset; + } + } + + /** + * A dummy charset to detect a default hit. + */ + public static class DummyCharset extends Charset + { + DummyCharset() + { + super("dummy", new String[] {}); + } + + @Override + public boolean contains(Charset cs) + { + throw new UnsupportedOperationException(); + } + + @Override + public CharsetDecoder newDecoder() + { + throw new UnsupportedOperationException(); + } + + @Override + public CharsetEncoder newEncoder() + { + throw new UnsupportedOperationException(); + } + + } +} diff --git a/core/src/main/java/org/alfresco/encryption/AbstractEncryptor.java b/core/src/main/java/org/alfresco/encryption/AbstractEncryptor.java new file mode 100644 index 0000000000..865df534a4 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/AbstractEncryptor.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.security.AlgorithmParameters; +import java.security.InvalidKeyException; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.SealedObject; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.Pair; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Basic support for encryption engines. + * + * @since 4.0 + */ +public abstract class AbstractEncryptor implements Encryptor +{ + protected static final Log logger = LogFactory.getLog(Encryptor.class); + protected String cipherAlgorithm; + protected String cipherProvider; + + protected KeyProvider keyProvider; + + /** + * Constructs with defaults + */ + protected AbstractEncryptor() + { + } + + /** + * @param keyProvider provides encryption keys based on aliases + */ + public void setKeyProvider(KeyProvider keyProvider) + { + this.keyProvider = keyProvider; + } + + public KeyProvider getKeyProvider() + { + return keyProvider; + } + + public void init() + { + PropertyCheck.mandatory(this, "keyProvider", keyProvider); + } + + /** + * Factory method to be written by implementations to construct and initialize + * physical ciphering objects. + * + * @param keyAlias the key alias + * @param params algorithm-specific parameters + * @param mode the cipher mode + * @return Cipher + */ + protected abstract Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode); + + /** + * {@inheritDoc} + */ + @Override + public Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input) + { + Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE); + if (cipher == null) + { + return new Pair(input, null); + } + try + { + byte[] output = cipher.doFinal(input); + params = cipher.getParameters(); + return new Pair(output, params); + } + catch (Throwable e) + { +// cipher.init(Cipher.ENCRYPT_MODE, key, params); + throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e); + } + } + + protected void resetCipher() + { + + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input) + { + Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE); + if (cipher == null) + { + return input; + } + try + { + return cipher.doFinal(input); + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream decrypt(String keyAlias, AlgorithmParameters params, InputStream input) + { + Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE); + if (cipher == null) + { + return input; + } + + try + { + return new CipherInputStream(input, cipher); + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e); + } + } + + /** + * {@inheritDoc} + *

+ * Serializes and {@link #encrypt(String, AlgorithmParameters, byte[]) encrypts} the input data. + */ + @Override + public Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input) + { + try + { + ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(input); + byte[] unencrypted = bos.toByteArray(); + return encrypt(keyAlias, params, unencrypted); + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Failed to serialize or encrypt object", e); + } + } + + /** + * {@inheritDoc} + *

+ * {@link #decrypt(String, AlgorithmParameters, byte[]) Decrypts} and deserializes the input data + */ + @Override + public Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input) + { + try + { + byte[] unencrypted = decrypt(keyAlias, params, input); + ByteArrayInputStream bis = new ByteArrayInputStream(unencrypted); + ObjectInputStream ois = new ObjectInputStream(bis); + Object obj = ois.readObject(); + return obj; + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Failed to deserialize or decrypt object", e); + } + } + + @Override + public Serializable sealObject(String keyAlias, AlgorithmParameters params, Serializable input) + { + if (input == null) + { + return null; + } + Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE); + if (cipher == null) + { + return input; + } + try + { + return new SealedObject(input, cipher); + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Failed to seal object", e); + } + } + + @Override + public Serializable unsealObject(String keyAlias, Serializable input) throws InvalidKeyException + { + if (input == null) + { + return input; + } + // Don't unseal it if it is not sealed + if (!(input instanceof SealedObject)) + { + return input; + } + // Get the Key, rather than a Cipher + Key key = keyProvider.getKey(keyAlias); + if (key == null) + { + // The client will be expecting to unseal the object + throw new IllegalStateException("No key matching " + keyAlias + ". Cannot unseal object."); + } + // Unseal it using the key + SealedObject sealedInput = (SealedObject) input; + try + { + Serializable output = (Serializable) sealedInput.getObject(key); + // Done + return output; + } + catch(InvalidKeyException e) + { + // let these through, can be useful to client code to know this is the cause + throw e; + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Failed to unseal object", e); + } + } + + public void setCipherAlgorithm(String cipherAlgorithm) + { + this.cipherAlgorithm = cipherAlgorithm; + } + + public String getCipherAlgorithm() + { + return this.cipherAlgorithm; + } + + public void setCipherProvider(String cipherProvider) + { + this.cipherProvider = cipherProvider; + } + + public String getCipherProvider() + { + return this.cipherProvider; + } + + /** + * {@inheritDoc} + */ + @Override + public AlgorithmParameters decodeAlgorithmParameters(byte[] encoded) + { + try + { + AlgorithmParameters p = null; + String algorithm = "DESede"; + if(getCipherProvider() != null) + { + p = AlgorithmParameters.getInstance(algorithm, getCipherProvider()); + } + else + { + p = AlgorithmParameters.getInstance(algorithm); + } + p.init(encoded); + return p; + } + catch(Exception e) + { + throw new AlfrescoRuntimeException("", e); + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java b/core/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java new file mode 100644 index 0000000000..297f28c355 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +/** + * Basic support for key providers + *

+ * TODO: This class will provide the alias name mapping so that use-cases can be mapped + * to different alias names in the keystore. + * + * @author Derek Hulley + * @since 4.0 + */ +public abstract class AbstractKeyProvider implements KeyProvider +{ + /* + * Not a useless class. + */ +} diff --git a/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java new file mode 100644 index 0000000000..a017ce984a --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.Key; +import java.util.Set; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; + +/** + * Manages a Java Keystore for Alfresco, including caching keys where appropriate. + * + * @since 4.0 + * + */ +public interface AlfrescoKeyStore +{ + public static final String KEY_KEYSTORE_PASSWORD = "keystore.password"; + + /** + * The name of the keystore. + * + * @return the name of the keystore. + */ + public String getName(); + + /** + * Backup the keystore to the backup location. Write the keys to the backup keystore. + */ + public void backup(); + + /** + * The key store parameters. + * + * @return KeyStoreParameters + */ + public KeyStoreParameters getKeyStoreParameters(); + + /** + * The backup key store parameters. + * + * @return * @return + + */ + public KeyStoreParameters getBackupKeyStoreParameters(); + + /** + * Does the underlying key store exist? + * + * @return true if it exists, false otherwise + */ + public boolean exists(); + + /** + * Return the key with the given key alias. + * + * @param keyAlias String + * @return Key + */ + public Key getKey(String keyAlias); + + /** + * Return the timestamp (in ms) of when the key was last loaded from the keystore on disk. + * + * @param keyAlias String + * @return long + */ + public long getKeyTimestamp(String keyAlias); + + /** + * Return the backup key with the given key alias. + * + * @param keyAlias String + * @return Key + */ + public Key getBackupKey(String keyAlias); + + /** + * Return all key aliases in the key store. + * + * @return Set + */ + public Set getKeyAliases(); + + /** + * Create an array of key managers from keys in the key store. + * + * @return KeyManager[] + */ + public KeyManager[] createKeyManagers(); + + /** + * Create an array of trust managers from certificates in the key store. + * + * @return TrustManager[] + */ + public TrustManager[] createTrustManagers(); + + /** + * Create the key store if it doesn't exist. + * A key for each key alias will be written to the keystore on disk, either from the cached keys or, if not present, a key will be generated. + */ + public void create(); + + /** + * Reload the keys from the key store. + */ + public void reload() throws InvalidKeystoreException, MissingKeyException; + + /** + * Check that the keys in the key store are valid i.e. that they match those registered. + */ + public void validateKeys() throws InvalidKeystoreException, MissingKeyException; + +} diff --git a/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java new file mode 100644 index 0000000000..085bec231a --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java @@ -0,0 +1,1100 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESedeKeySpec; +import javax.management.openmbean.KeyAlreadyExistsException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.alfresco.encryption.EncryptionKeysRegistry.KEY_STATUS; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This wraps a Java Keystore and caches the encryption keys. It manages the loading and caching of the encryption keys + * and their registration with and validation against the encryption key registry. + * + * @since 4.0 + * + */ +public class AlfrescoKeyStoreImpl implements AlfrescoKeyStore +{ + private static final Log logger = LogFactory.getLog(AlfrescoKeyStoreImpl.class); + + protected KeyStoreParameters keyStoreParameters; + protected KeyStoreParameters backupKeyStoreParameters; + protected KeyResourceLoader keyResourceLoader; + protected EncryptionKeysRegistry encryptionKeysRegistry; + + protected KeyMap keys; + protected KeyMap backupKeys; + protected final WriteLock writeLock; + protected final ReadLock readLock; + + private Set keysToValidate; + protected boolean validateKeyChanges = false; + + public AlfrescoKeyStoreImpl() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + writeLock = lock.writeLock(); + readLock = lock.readLock(); + this.keys = new KeyMap(); + this.backupKeys = new KeyMap(); + } + + public AlfrescoKeyStoreImpl(KeyStoreParameters keyStoreParameters, KeyResourceLoader keyResourceLoader) + { + this(); + + this.keyResourceLoader = keyResourceLoader; + this.keyStoreParameters = keyStoreParameters; + + safeInit(); + } + + public void init() + { + writeLock.lock(); + try + { + safeInit(); + } + finally + { + writeLock.unlock(); + } + } + + public void setEncryptionKeysRegistry( + EncryptionKeysRegistry encryptionKeysRegistry) + { + this.encryptionKeysRegistry = encryptionKeysRegistry; + } + + public void setValidateKeyChanges(boolean validateKeyChanges) + { + this.validateKeyChanges = validateKeyChanges; + } + + public void setKeysToValidate(Set keysToValidate) + { + this.keysToValidate = keysToValidate; + } + + public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters) + { + this.keyStoreParameters = keyStoreParameters; + } + + public void setBackupKeyStoreParameters( + KeyStoreParameters backupKeyStoreParameters) + { + this.backupKeyStoreParameters = backupKeyStoreParameters; + } + + public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader) + { + this.keyResourceLoader = keyResourceLoader; + } + + public KeyStoreParameters getKeyStoreParameters() + { + return keyStoreParameters; + } + + public KeyStoreParameters getBackupKeyStoreParameters() + { + return backupKeyStoreParameters; + } + + public KeyResourceLoader getKeyResourceLoader() + { + return keyResourceLoader; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() + { + return keyStoreParameters.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void validateKeys() throws InvalidKeystoreException, MissingKeyException + { + validateKeys(keys, backupKeys); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean exists() + { + return keyStoreExists(getKeyStoreParameters().getLocation()); + } + + /** + * {@inheritDoc} + */ + @Override + public void reload() throws InvalidKeystoreException, MissingKeyException + { + KeyMap keys = loadKeyStore(getKeyStoreParameters()); + KeyMap backupKeys = loadKeyStore(getBackupKeyStoreParameters()); + + validateKeys(keys, backupKeys); + + // all ok, reload the keys + writeLock.lock(); + try + { + this.keys = keys; + this.backupKeys = backupKeys; + } + finally + { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Set getKeyAliases() + { + return new HashSet(keys.getKeyAliases()); + } + + /** + * {@inheritDoc} + */ + @Override + public void backup() + { + writeLock.lock(); + try + { + for(String keyAlias : keys.getKeyAliases()) + { + backupKeys.setKey(keyAlias, keys.getKey(keyAlias)); + } + createKeyStore(backupKeyStoreParameters, backupKeys); + } + finally + { + writeLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void create() + { + createKeyStore(keyStoreParameters, keys); + } + + /** + * {@inheritDoc} + */ + @Override + public Key getKey(String keyAlias) + { + readLock.lock(); + try + { + return keys.getCachedKey(keyAlias).getKey(); + } + finally + { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public long getKeyTimestamp(String keyAlias) + { + readLock.lock(); + try + { + CachedKey cachedKey = keys.getCachedKey(keyAlias); + return cachedKey.getTimestamp(); + } + finally + { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Key getBackupKey(String keyAlias) + { + readLock.lock(); + try + { + return backupKeys.getCachedKey(keyAlias).getKey(); + } + finally + { + readLock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public KeyManager[] createKeyManagers() + { + KeyInfoManager keyInfoManager = null; + + try + { + keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation()); + KeyStore ks = loadKeyStore(keyStoreParameters, keyInfoManager); + + logger.debug("Initializing key managers"); + KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + String keyStorePassword = keyInfoManager.getKeyStorePassword(); + kmfactory.init(ks, keyStorePassword != null ? keyStorePassword.toCharArray(): null); + return kmfactory.getKeyManagers(); + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to create key manager", e); + } + finally + { + if(keyInfoManager != null) + { + keyInfoManager.clear(); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public TrustManager[] createTrustManagers() + { + KeyInfoManager keyInfoManager = null; + + try + { + keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation()); + KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager); + + logger.debug("Initializing trust managers"); + TrustManagerFactory tmfactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmfactory.init(ks); + return tmfactory.getTrustManagers(); + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to create key manager", e); + } + finally + { + if(keyInfoManager != null) + { + keyInfoManager.clear(); + } + } + } + + protected String getKeyMetaDataFileLocation() + { + return keyStoreParameters.getKeyMetaDataFileLocation(); + } + + protected InputStream getKeyStoreStream(String location) throws FileNotFoundException + { + if(location == null) + { + return null; + } + return keyResourceLoader.getKeyStore(location); + } + + protected OutputStream getKeyStoreOutStream() throws FileNotFoundException + { + return new FileOutputStream(getKeyStoreParameters().getLocation()); + } + + protected KeyInfoManager getKeyInfoManager(String metadataFileLocation) throws FileNotFoundException, IOException + { + return new KeyInfoManager(metadataFileLocation, keyResourceLoader); + } + + protected KeyMap cacheKeys(KeyStore ks, KeyInfoManager keyInfoManager) + throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException + { + KeyMap keys = new KeyMap(); + + // load and cache the keys + for(Entry keyEntry : keyInfoManager.getKeyInfo().entrySet()) + { + String keyAlias = keyEntry.getKey(); + + KeyInformation keyInfo = keyInfoManager.getKeyInformation(keyAlias); + String passwordStr = keyInfo != null ? keyInfo.getPassword() : null; + + // Null is an acceptable value (means no key) + Key key = null; + + // Attempt to get the key + key = ks.getKey(keyAlias, passwordStr == null ? null : passwordStr.toCharArray()); + if(key != null) + { + keys.setKey(keyAlias, key); + } + // Key loaded + if (logger.isDebugEnabled()) + { + logger.debug( + "Retrieved key from keystore: \n" + + " Location: " + getKeyStoreParameters().getLocation() + "\n" + + " Provider: " + getKeyStoreParameters().getProvider() + "\n" + + " Type: " + getKeyStoreParameters().getType() + "\n" + + " Alias: " + keyAlias + "\n" + + " Password?: " + (passwordStr != null)); + + Certificate[] certs = ks.getCertificateChain(keyAlias); + if(certs != null) + { + logger.debug("Certificate chain '" + keyAlias + "':"); + for(int c = 0; c < certs.length; c++) + { + if(certs[c] instanceof X509Certificate) + { + X509Certificate cert = (X509Certificate)certs[c]; + logger.debug(" Certificate " + (c + 1) + ":"); + logger.debug(" Subject DN: " + cert.getSubjectDN()); + logger.debug(" Signature Algorithm: " + cert.getSigAlgName()); + logger.debug(" Valid from: " + cert.getNotBefore() ); + logger.debug(" Valid until: " + cert.getNotAfter()); + logger.debug(" Issuer: " + cert.getIssuerDN()); + } + } + } + } + } + + return keys; + } + + protected KeyStore initialiseKeyStore(String type, String provider) + { + KeyStore ks = null; + + try + { + if(provider == null || provider.equals("")) + { + ks = KeyStore.getInstance(type); + } + else + { + ks = KeyStore.getInstance(type, provider); + } + + ks.load(null, null); + + return ks; + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to intialise key store", e); + } + } + + protected KeyStore loadKeyStore(KeyStoreParameters keyStoreParameters, KeyInfoManager keyInfoManager) + { + String pwdKeyStore = null; + + try + { + KeyStore ks = initialiseKeyStore(keyStoreParameters.getType(), keyStoreParameters.getProvider()); + + // Load it up + InputStream is = getKeyStoreStream(keyStoreParameters.getLocation()); + if (is != null) + { + try + { + // Get the keystore password + pwdKeyStore = keyInfoManager.getKeyStorePassword(); + ks.load(is, pwdKeyStore == null ? null : pwdKeyStore.toCharArray()); + } + finally + { + try {is.close(); } catch (Throwable e) {} + } + } + else + { + // this is ok, the keystore will contain no keys. + logger.warn("Keystore file doesn't exist: " + keyStoreParameters.getLocation()); + } + + return ks; + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to load key store: " + keyStoreParameters.getLocation(), e); + } + finally + { + pwdKeyStore = null; + } + } + + /** + * Initializes class + */ + private void safeInit() + { + PropertyCheck.mandatory(this, "location", getKeyStoreParameters().getLocation()); + + // Make sure we choose the default type, if required + if(getKeyStoreParameters().getType() == null) + { + keyStoreParameters.setType(KeyStore.getDefaultType()); + } + + writeLock.lock(); + try + { + keys = loadKeyStore(keyStoreParameters); + backupKeys = loadKeyStore(backupKeyStoreParameters); + } + finally + { + writeLock.unlock(); + } + } + + private KeyMap loadKeyStore(KeyStoreParameters keyStoreParameters) + { + InputStream is = null; + KeyInfoManager keyInfoManager = null; + KeyStore ks = null; + + if(keyStoreParameters == null) + { + // empty key map + return new KeyMap(); + } + + try + { + keyInfoManager = getKeyInfoManager(keyStoreParameters.getKeyMetaDataFileLocation()); + ks = loadKeyStore(keyStoreParameters, keyInfoManager); + // Loaded + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException( + "Failed to initialize keystore: \n" + + " Location: " + getKeyStoreParameters().getLocation() + "\n" + + " Provider: " + getKeyStoreParameters().getProvider() + "\n" + + " Type: " + getKeyStoreParameters().getType(), + e); + } + finally + { + if(keyInfoManager != null) + { + keyInfoManager.clearKeyStorePassword(); + } + + if (is != null) + { + try + { + is.close(); + } + catch (Throwable e) + { + + } + } + } + + try + { + // cache the keys from the keystore + KeyMap keys = cacheKeys(ks, keyInfoManager); + + if(logger.isDebugEnabled()) + { + logger.debug( + "Initialized keystore: \n" + + " Location: " + getKeyStoreParameters().getLocation() + "\n" + + " Provider: " + getKeyStoreParameters().getProvider() + "\n" + + " Type: " + getKeyStoreParameters().getType() + "\n" + + keys.numKeys() + " keys found"); + } + + return keys; + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException( + "Failed to retrieve keys from keystore: \n" + + " Location: " + getKeyStoreParameters().getLocation() + "\n" + + " Provider: " + getKeyStoreParameters().getProvider() + "\n" + + " Type: " + getKeyStoreParameters().getType() + "\n", + e); + } + finally + { + // Clear key information + keyInfoManager.clear(); + } + } + + protected void createKey(String keyAlias) + { + KeyInfoManager keyInfoManager = null; + + try + { + keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation()); + Key key = getSecretKey(keyInfoManager.getKeyInformation(keyAlias)); + encryptionKeysRegistry.registerKey(keyAlias, key); + keys.setKey(keyAlias, key); + + KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager); + ks.setKeyEntry(keyAlias, key, keyInfoManager.getKeyInformation(keyAlias).getPassword().toCharArray(), null); + OutputStream keyStoreOutStream = getKeyStoreOutStream(); + ks.store(keyStoreOutStream, keyInfoManager.getKeyStorePassword().toCharArray()); + // Workaround for MNT-15005 + keyStoreOutStream.close(); + + logger.info("Created key: " + keyAlias + "\n in key store: \n" + + " Location: " + getKeyStoreParameters().getLocation() + "\n" + + " Provider: " + getKeyStoreParameters().getProvider() + "\n" + + " Type: " + getKeyStoreParameters().getType()); + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException( + "Failed to create key: " + keyAlias + "\n in key store: \n" + + " Location: " + getKeyStoreParameters().getLocation() + "\n" + + " Provider: " + getKeyStoreParameters().getProvider() + "\n" + + " Type: " + getKeyStoreParameters().getType(), + e); + } + finally + { + if(keyInfoManager != null) + { + keyInfoManager.clear(); + } + } + } + + protected void createKeyStore(KeyStoreParameters keyStoreParameters, KeyMap keys) + { + KeyInfoManager keyInfoManager = null; + + try + { + if(!keyStoreExists(keyStoreParameters.getLocation())) + { + keyInfoManager = getKeyInfoManager(keyStoreParameters.getKeyMetaDataFileLocation()); + KeyStore ks = initialiseKeyStore(keyStoreParameters.getType(), keyStoreParameters.getProvider()); + + String keyStorePassword = keyInfoManager.getKeyStorePassword(); + if(keyStorePassword == null) + { + throw new AlfrescoRuntimeException("Key store password is null for keystore at location " + + getKeyStoreParameters().getLocation() + + ", key store meta data location" + getKeyMetaDataFileLocation()); + } + + for(String keyAlias : keys.getKeyAliases()) + { + KeyInformation keyInfo = keyInfoManager.getKeyInformation(keyAlias); + + Key key = keys.getKey(keyAlias); + if(key == null) + { + logger.warn("Key with alias " + keyAlias + " is null when creating keystore at location " + keyStoreParameters.getLocation()); + } + else + { + ks.setKeyEntry(keyAlias, key, keyInfo.getPassword().toCharArray(), null); + } + } + +// try +// { +// throw new Exception("Keystore creation: " + ); +// } +// catch(Throwable e) +// { +// logger.debug(e.getMessage()); +// e.printStackTrace(); +// } + + OutputStream keyStoreOutStream = getKeyStoreOutStream(); + ks.store(keyStoreOutStream, keyStorePassword.toCharArray()); + // Workaround for MNT-15005 + keyStoreOutStream.close(); + } + else + { + logger.warn("Can't create key store " + keyStoreParameters.getLocation() + ", already exists."); + } + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException( + "Failed to create keystore: \n" + + " Location: " + keyStoreParameters.getLocation() + "\n" + + " Provider: " + keyStoreParameters.getProvider() + "\n" + + " Type: " + keyStoreParameters.getType(), + e); + } + finally + { + if(keyInfoManager != null) + { + keyInfoManager.clear(); + } + } + } + + /* + * For testing + */ +// void createBackup() +// { +// createKeyStore(backupKeyStoreParameters, backupKeys); +// } + + private byte[] generateKeyData() + { + try + { + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN]; + random.nextBytes(bytes); + return bytes; + } + catch(Exception e) + { + throw new RuntimeException("Unable to generate secret key", e); + } + } + + protected Key getSecretKey(KeyInformation keyInformation) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException + { + byte[] keyData = keyInformation.getKeyData(); + + if(keyData == null) + { + if(keyInformation.getKeyAlgorithm().equals("DESede")) + { + // no key data provided, generate key data automatically + keyData = generateKeyData(); + } + else + { + throw new AlfrescoRuntimeException("Unable to generate secret key: key algorithm is not DESede and no keyData provided"); + } + } + + DESedeKeySpec keySpec = new DESedeKeySpec(keyData); + SecretKeyFactory kf = SecretKeyFactory.getInstance(keyInformation.getKeyAlgorithm()); + SecretKey secretKey = kf.generateSecret(keySpec); + return secretKey; + } + + void importPrivateKey(String keyAlias, String keyPassword, InputStream fl, InputStream certstream) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException, KeyStoreException + { + KeyInfoManager keyInfoManager = null; + + writeLock.lock(); + try + { + keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation()); + KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager); + + // loading Key + byte[] keyBytes = new byte[fl.available()]; + KeyFactory kf = KeyFactory.getInstance("RSA"); + fl.read(keyBytes, 0, fl.available()); + fl.close(); + PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(keyBytes); + PrivateKey key = kf.generatePrivate(keysp); + + // loading CertificateChain + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + @SuppressWarnings("rawtypes") + Collection c = cf.generateCertificates(certstream) ; + Certificate[] certs = new Certificate[c.toArray().length]; + + certs = (Certificate[])c.toArray(new Certificate[0]); + + // storing keystore + ks.setKeyEntry(keyAlias, key, keyPassword.toCharArray(), certs); + + if(logger.isDebugEnabled()) + { + logger.debug("Key and certificate stored."); + logger.debug("Alias:"+ keyAlias); + } + OutputStream keyStoreOutStream = getKeyStoreOutStream(); + ks.store(keyStoreOutStream, keyPassword.toCharArray()); + // Workaround for MNT-15005 + keyStoreOutStream.close(); + } + finally + { + if(keyInfoManager != null) + { + keyInfoManager.clear(); + } + + writeLock.unlock(); + } + } + + public boolean backupExists() + { + return keyStoreExists(getBackupKeyStoreParameters().getLocation()); + } + + protected boolean keyStoreExists(String location) + { + try + { + InputStream is = getKeyStoreStream(location); + if (is == null) + { + return false; + } + else + { + try { is.close(); } catch (Throwable e) {} + return true; + } + } + catch(FileNotFoundException e) + { + return false; + } + } + + /* + * Validates the keystore keys against the key registry, throwing exceptions if the keys have been unintentionally changed. + * + * For each key to validate: + * + * (i) no main key, no backup key, the key is registered for the main keystore -> error, must re-instate the keystore + * (ii) no main key, no backup key, the key is not registered -> create the main key store and register the key + * (iii) main key exists but is not registered -> register the key + * (iv) main key exists, no backup key, the key is registered -> check that the key has not changed - if it has, throw an exception + * (v) main key exists, backup key exists, the key is registered -> check in the registry that the backup key has not changed and then re-register main key + */ + protected void validateKeys(KeyMap keys, KeyMap backupKeys) throws InvalidKeystoreException, MissingKeyException + { + if(!validateKeyChanges) + { + return; + } + + writeLock.lock(); + try + { + // check for the existence of a key store first + for(String keyAlias : keysToValidate) + { + if(keys.getKey(keyAlias) == null) + { + if(backupKeys.getKey(keyAlias) == null) + { + if(encryptionKeysRegistry.isKeyRegistered(keyAlias)) + { + // The key is registered and neither key nor backup key exist -> throw + // an exception indicating that the key is missing and the keystore should + // be re-instated. + throw new MissingKeyException(keyAlias, getKeyStoreParameters().getLocation()); + } + else + { + // Neither the key nor the backup key exist, so create the key + createKey(keyAlias); + } + } + } + else + { + if(!encryptionKeysRegistry.isKeyRegistered(keyAlias)) + { + // The key is not registered, so register it + encryptionKeysRegistry.registerKey(keyAlias, keys.getKey(keyAlias)); + } + else if(backupKeys.getKey(keyAlias) == null && encryptionKeysRegistry.checkKey(keyAlias, keys.getKey(keyAlias)) == KEY_STATUS.CHANGED) + { + // A key has been changed, indicating that the keystore has been un-intentionally changed. + // Note: this will halt the application bootstrap. + throw new InvalidKeystoreException("The key with alias " + keyAlias + " has been changed, re-instate the previous keystore"); + } + else if(backupKeys.getKey(keyAlias) != null && encryptionKeysRegistry.isKeyRegistered(keyAlias)) + { + // Both key and backup key exist and the key is registered. + if(encryptionKeysRegistry.checkKey(keyAlias, backupKeys.getKey(keyAlias)) == KEY_STATUS.OK) + { + // The registered key is the backup key so lets re-register the key in the main key store. + // Unregister the existing (now backup) key and re-register the main key. + encryptionKeysRegistry.unregisterKey(keyAlias); + encryptionKeysRegistry.registerKey(keyAlias, keys.getKey(keyAlias)); + } + } + } + } + } + finally + { + writeLock.unlock(); + } + } + + public static class KeyInformation + { + protected String alias; + protected byte[] keyData; + protected String password; + protected String keyAlgorithm; + + public KeyInformation(String alias, byte[] keyData, String password, String keyAlgorithm) + { + super(); + this.alias = alias; + this.keyData = keyData; + this.password = password; + this.keyAlgorithm = keyAlgorithm; + } + + public String getAlias() + { + return alias; + } + + public byte[] getKeyData() + { + return keyData; + } + + public String getPassword() + { + return password; + } + + public String getKeyAlgorithm() + { + return keyAlgorithm; + } + } + + /* + * Caches key meta data information such as password, seed. + * + */ + public static class KeyInfoManager + { + private KeyResourceLoader keyResourceLoader; + private String metadataFileLocation; + private Properties keyProps; + private String keyStorePassword = null; + private Map keyInfo; + + /** + * For testing. + * + * @param passwords + */ + KeyInfoManager(Map passwords, KeyResourceLoader keyResourceLoader) + { + this.keyResourceLoader = keyResourceLoader; + keyInfo = new HashMap(2); + for(Map.Entry password : passwords.entrySet()) + { + keyInfo.put(password.getKey(), new KeyInformation(password.getKey(), null, password.getValue(), null)); + } + } + + KeyInfoManager(String metadataFileLocation, KeyResourceLoader keyResourceLoader) throws IOException, FileNotFoundException + { + this.keyResourceLoader = keyResourceLoader; + this.metadataFileLocation = metadataFileLocation; + keyInfo = new HashMap(2); + loadKeyMetaData(); + } + + public Map getKeyInfo() + { + // TODO defensively copy + return keyInfo; + } + + /** + * Set the map of key meta data (including passwords to access the keystore). + *

+ * Where required, null values must be inserted into the map to indicate the presence + * of a key that is not protected by a password. They entry for {@link #KEY_KEYSTORE_PASSWORD} + * is required if the keystore is password protected. + */ + protected void loadKeyMetaData() throws IOException, FileNotFoundException + { + keyProps = keyResourceLoader.loadKeyMetaData(metadataFileLocation); + if(keyProps != null) + { + String aliases = keyProps.getProperty("aliases"); + if(aliases == null) + { + throw new AlfrescoRuntimeException("Passwords file must contain an aliases key"); + } + + this.keyStorePassword = keyProps.getProperty(KEY_KEYSTORE_PASSWORD); + + StringTokenizer st = new StringTokenizer(aliases, ","); + while(st.hasMoreTokens()) + { + String keyAlias = st.nextToken(); + keyInfo.put(keyAlias, loadKeyInformation(keyAlias)); + } + } + else + { + // TODO + //throw new FileNotFoundException("Cannot find key metadata file " + getKeyMetaDataFileLocation()); + } + } + + public void clear() + { + this.keyStorePassword = null; + if(this.keyProps != null) + { + this.keyProps.clear(); + } + } + + public void removeKeyInformation(String keyAlias) + { + this.keyProps.remove(keyAlias); + } + + protected KeyInformation loadKeyInformation(String keyAlias) + { + String keyPassword = keyProps.getProperty(keyAlias + ".password"); + String keyData = keyProps.getProperty(keyAlias + ".keyData"); + String keyAlgorithm = keyProps.getProperty(keyAlias + ".algorithm"); + + byte[] keyDataBytes = null; + if(keyData != null && !keyData.equals("")) + { + keyDataBytes = Base64.decodeBase64(keyData); + } + KeyInformation keyInfo = new KeyInformation(keyAlias, keyDataBytes, keyPassword, keyAlgorithm); + return keyInfo; + } + + public String getKeyStorePassword() + { + return keyStorePassword; + } + + public void clearKeyStorePassword() + { + this.keyStorePassword = null; + } + + public KeyInformation getKeyInformation(String keyAlias) + { + return keyInfo.get(keyAlias); + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/CachedKey.java b/core/src/main/java/org/alfresco/encryption/CachedKey.java new file mode 100644 index 0000000000..9d24ee0598 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/CachedKey.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.Key; + +/** + * + * Represents a loaded, cached encryption key. The key can be null. + * + * @since 4.0 + * + */ +public class CachedKey +{ + public static CachedKey NULL = new CachedKey(null, null); + + private Key key; + private long timestamp; + + CachedKey(Key key, Long timestamp) + { + this.key = key; + this.timestamp = (timestamp != null ? timestamp.longValue() : -1); + } + + public CachedKey(Key key) + { + super(); + this.key = key; + this.timestamp = System.currentTimeMillis(); + } + + public Key getKey() + { + return key; + } + + public long getTimestamp() + { + return timestamp; + } +} diff --git a/core/src/main/java/org/alfresco/encryption/DecryptingInputStream.java b/core/src/main/java/org/alfresco/encryption/DecryptingInputStream.java new file mode 100644 index 0000000000..97a0e5bf7f --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/DecryptingInputStream.java @@ -0,0 +1,355 @@ +/* + * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.encryption; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SecureRandom; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * An input stream that encrypts data produced by a {@link EncryptingOutputStream}. A lightweight yet secure hybrid + * encryption scheme is used. A random symmetric key is decrypted using the receiver's private key. The supplied data is + * then decrypted using the symmetric key and read on a streaming basis. When the end of the stream is reached or the + * stream is closed, a HMAC checksum of the entire stream contents is validated. + */ +public class DecryptingInputStream extends InputStream +{ + + /** The wrapped stream. */ + private final DataInputStream wrapped; + + /** The input cipher. */ + private final Cipher inputCipher; + + /** The MAC generator. */ + private final Mac mac; + + /** Internal buffer for MAC computation. */ + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024); + + /** A DataOutputStream on top of our interal buffer. */ + private final DataOutputStream dataStr = new DataOutputStream(this.buffer); + + /** The current unencrypted data block. */ + private byte[] currentDataBlock; + + /** The next encrypted data block. (could be the HMAC checksum) */ + private byte[] nextDataBlock; + + /** Have we read to the end of the underlying stream?. */ + private boolean isAtEnd; + + /** Our current position within currentDataBlock. */ + private int currentDataPos; + + /** + * Constructs a DecryptingInputStream using default symmetric encryption parameters. + * + * @param wrapped + * the input stream to decrypt + * @param privKey + * the receiver's private key for decrypting the symmetric key + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws NoSuchAlgorithmException + * the no such algorithm exception + * @throws NoSuchPaddingException + * the no such padding exception + * @throws InvalidKeyException + * the invalid key exception + * @throws IllegalBlockSizeException + * the illegal block size exception + * @throws BadPaddingException + * the bad padding exception + * @throws InvalidAlgorithmParameterException + * the invalid algorithm parameter exception + * @throws NoSuchProviderException + * the no such provider exception + */ + public DecryptingInputStream(final InputStream wrapped, final PrivateKey privKey) throws IOException, + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, + BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException + { + this(wrapped, privKey, "AES", "CBC", "PKCS5PADDING"); + } + + /** + * Constructs a DecryptingInputStream. + * + * @param wrapped + * the input stream to decrypt + * @param privKey + * the receiver's private key for decrypting the symmetric key + * @param algorithm + * encryption algorithm (e.g. "AES") + * @param mode + * encryption mode (e.g. "CBC") + * @param padding + * padding scheme (e.g. "PKCS5PADDING") + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws NoSuchAlgorithmException + * the no such algorithm exception + * @throws NoSuchPaddingException + * the no such padding exception + * @throws InvalidKeyException + * the invalid key exception + * @throws IllegalBlockSizeException + * the illegal block size exception + * @throws BadPaddingException + * the bad padding exception + * @throws InvalidAlgorithmParameterException + * the invalid algorithm parameter exception + * @throws NoSuchProviderException + * the no such provider exception + */ + public DecryptingInputStream(final InputStream wrapped, final PrivateKey privKey, final String algorithm, + final String mode, final String padding) throws IOException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, + InvalidAlgorithmParameterException, NoSuchProviderException + { + // Initialise a secure source of randomness + this.wrapped = new DataInputStream(wrapped); + final SecureRandom secRand = SecureRandom.getInstance("SHA1PRNG"); + + // Set up RSA + final Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING"); + rsa.init(Cipher.DECRYPT_MODE, privKey, secRand); + + // Read and decrypt the symmetric key + final SecretKey symKey = new SecretKeySpec(rsa.doFinal(readBlock()), algorithm); + + // Read and decrypt initialisation vector + final byte[] keyIV = rsa.doFinal(readBlock()); + + // Set up cipher for decryption + this.inputCipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding); + this.inputCipher.init(Cipher.DECRYPT_MODE, symKey, new IvParameterSpec(keyIV)); + + // Read and decrypt the MAC key + final SecretKey macKey = new SecretKeySpec(this.inputCipher.doFinal(readBlock()), "HMACSHA1"); + + // Set up HMAC + this.mac = Mac.getInstance("HMACSHA1"); + this.mac.init(macKey); + + // Always read a block ahead so we can intercept the HMAC block + this.nextDataBlock = readBlock(false); + } + + /** + * Reads the next block of data, adding it to the HMAC checksum. Strips the header recording the number of bytes in + * the block. + * + * @return the data block, or null if the end of the stream has been reached + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private byte[] readBlock() throws IOException + { + return readBlock(true); + } + + /** + * Reads the next block of data, optionally adding it to the HMAC checksum. Strips the header recording the number + * of bytes in the block. + * + * @param updateMac + * should the block be added to the HMAC checksum? + * @return the data block, or null if the end of the stream has been reached + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private byte[] readBlock(final boolean updateMac) throws IOException + { + int len; + try + { + len = this.wrapped.readInt(); + } + catch (final EOFException e) + { + return null; + } + final byte[] in = new byte[len]; + this.wrapped.readFully(in); + if (updateMac) + { + macBlock(in); + } + return in; + } + + /** + * Updates the HMAC checksum with the given data block. + * + * @param block + * the block + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private void macBlock(final byte[] block) throws IOException + { + this.dataStr.writeInt(block.length); + this.dataStr.write(block); + // If we don't have the MAC key yet, buffer up until we do + if (this.mac != null) + { + this.dataStr.flush(); + final byte[] bytes = this.buffer.toByteArray(); + this.buffer.reset(); + this.mac.update(bytes); + } + } + + /* + * (non-Javadoc) + * @see java.io.InputStream#read() + */ + @Override + public int read() throws IOException + { + final byte[] buf = new byte[1]; + int bytesRead; + while ((bytesRead = read(buf)) == 0) + { + ; + } + return bytesRead == -1 ? -1 : buf[0] & 0xFF; + } + + /* + * (non-Javadoc) + * @see java.io.InputStream#read(byte[]) + */ + @Override + public int read(final byte b[]) throws IOException + { + return read(b, 0, b.length); + } + + /* + * (non-Javadoc) + * @see java.io.InputStream#read(byte[], int, int) + */ + @Override + public int read(final byte b[], int off, final int len) throws IOException + { + if (b == null) + { + throw new NullPointerException(); + } + else if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0) + { + throw new IndexOutOfBoundsException(); + } + else if (len == 0) + { + return 0; + } + + int bytesToRead = len; + OUTER: while (bytesToRead > 0) + { + // Fetch another block if necessary + while (this.currentDataBlock == null || this.currentDataPos >= this.currentDataBlock.length) + { + byte[] newDataBlock; + // We're right at the end of the last block so finish + if (this.isAtEnd) + { + this.currentDataBlock = this.nextDataBlock = null; + break OUTER; + } + // We've already read the last block so validate the MAC code + else if ((newDataBlock = readBlock(false)) == null) + { + if (!MessageDigest.isEqual(this.mac.doFinal(), this.nextDataBlock)) + { + throw new IOException("Invalid HMAC"); + } + // We still have what's left in the cipher to read + try + { + this.currentDataBlock = this.inputCipher.doFinal(); + } + catch (final GeneralSecurityException e) + { + throw new RuntimeException(e); + } + this.isAtEnd = true; + } + // We have an ordinary data block to MAC and decrypt + else + { + macBlock(this.nextDataBlock); + this.currentDataBlock = this.inputCipher.update(this.nextDataBlock); + this.nextDataBlock = newDataBlock; + } + this.currentDataPos = 0; + } + final int bytesRead = Math.min(bytesToRead, this.currentDataBlock.length - this.currentDataPos); + System.arraycopy(this.currentDataBlock, this.currentDataPos, b, off, bytesRead); + bytesToRead -= bytesRead; + off += bytesRead; + this.currentDataPos += bytesRead; + } + return bytesToRead == len ? -1 : len - bytesToRead; + } + + /* + * (non-Javadoc) + * @see java.io.InputStream#available() + */ + @Override + public int available() throws IOException + { + return this.currentDataBlock == null ? 0 : this.currentDataBlock.length - this.currentDataPos; + } + + /* + * (non-Javadoc) + * @see java.io.InputStream#close() + */ + @Override + public void close() throws IOException + { + // Read right to the end, just to ensure the MAC code is valid! + if (this.nextDataBlock != null) + { + final byte[] skipBuff = new byte[1024]; + while (read(skipBuff) != -1) + { + ; + } + } + this.wrapped.close(); + this.dataStr.close(); + } + +} diff --git a/core/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java b/core/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java new file mode 100644 index 0000000000..6bceef2a9d --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.AlgorithmParameters; +import java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.encryption.MACUtils.MACInput; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.IPUtils; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.Base64; +import org.springframework.util.FileCopyUtils; + +/** + * Various encryption utility methods. + * + * @since 4.0 + */ +public class DefaultEncryptionUtils implements EncryptionUtils +{ + // Logger + protected static Log logger = LogFactory.getLog(Encryptor.class); + + protected static String HEADER_ALGORITHM_PARAMETERS = "XAlfresco-algorithmParameters"; + protected static String HEADER_MAC = "XAlfresco-mac"; + protected static String HEADER_TIMESTAMP = "XAlfresco-timestamp"; + + protected Encryptor encryptor; + protected MACUtils macUtils; + protected long messageTimeout; // ms + protected String remoteIP; + protected String localIP; + + public DefaultEncryptionUtils() + { + try + { + this.localIP = InetAddress.getLocalHost().getHostAddress(); + } + catch(Exception e) + { + throw new AlfrescoRuntimeException("Unable to initialise EncryptionUtils", e); + } + } + + public String getRemoteIP() + { + return remoteIP; + } + + public void setRemoteIP(String remoteIP) + { + try + { + this.remoteIP = IPUtils.getRealIPAddress(remoteIP); + } + catch (UnknownHostException e) + { + throw new AlfrescoRuntimeException("Failed to get server IP address", e); + } + } + + /** + * Get the local registered IP address for authentication purposes + * + * @return String + */ + protected String getLocalIPAddress() + { + return localIP; + } + + public void setMessageTimeout(long messageTimeout) + { + this.messageTimeout = messageTimeout; + } + + public void setEncryptor(Encryptor encryptor) + { + this.encryptor = encryptor; + } + + public void setMacUtils(MACUtils macUtils) + { + this.macUtils = macUtils; + } + + protected void setRequestMac(HttpMethod method, byte[] mac) + { + if(mac == null) + { + throw new AlfrescoRuntimeException("Mac cannot be null"); + } + method.setRequestHeader(HEADER_MAC, Base64.encodeBytes(mac)); + } + + /** + * Set the MAC on the HTTP response + * + * @param response HttpServletResponse + * @param mac byte[] + */ + protected void setMac(HttpServletResponse response, byte[] mac) + { + if(mac == null) + { + throw new AlfrescoRuntimeException("Mac cannot be null"); + } + + response.setHeader(HEADER_MAC, Base64.encodeBytes(mac)); + } + + /** + * Get the MAC (Message Authentication Code) on the HTTP request + * + * @param req HttpServletRequest + * @return the MAC + * @throws IOException + */ + protected byte[] getMac(HttpServletRequest req) throws IOException + { + String header = req.getHeader(HEADER_MAC); + if(header != null) + { + return Base64.decode(header); + } + else + { + return null; + } + } + + /** + * Get the MAC (Message Authentication Code) on the HTTP response + * + * @param res HttpMethod + * @return the MAC + * @throws IOException + */ + protected byte[] getResponseMac(HttpMethod res) throws IOException + { + Header header = res.getResponseHeader(HEADER_MAC); + if(header != null) + { + return Base64.decode(header.getValue()); + } + else + { + return null; + } + } + + /** + * Set the timestamp on the HTTP request + * @param method HttpMethod + * @param timestamp (ms, in UNIX time) + */ + protected void setRequestTimestamp(HttpMethod method, long timestamp) + { + method.setRequestHeader(HEADER_TIMESTAMP, String.valueOf(timestamp)); + } + + /** + * Set the timestamp on the HTTP response + * @param res HttpServletResponse + * @param timestamp (ms, in UNIX time) + */ + protected void setTimestamp(HttpServletResponse res, long timestamp) + { + res.setHeader(HEADER_TIMESTAMP, String.valueOf(timestamp)); + } + + /** + * Get the timestamp on the HTTP response + * + * @param method HttpMethod + * @return timestamp (ms, in UNIX time) + * @throws IOException + */ + protected Long getResponseTimestamp(HttpMethod method) throws IOException + { + Header header = method.getResponseHeader(HEADER_TIMESTAMP); + if(header != null) + { + return Long.valueOf(header.getValue()); + } + else + { + return null; + } + } + + /** + * Get the timestamp on the HTTP request + * + * @param method HttpServletRequest + * @return timestamp (ms, in UNIX time) + * @throws IOException + */ + protected Long getTimestamp(HttpServletRequest method) throws IOException + { + String header = method.getHeader(HEADER_TIMESTAMP); + if(header != null) + { + return Long.valueOf(header); + } + else + { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setRequestAlgorithmParameters(HttpMethod method, AlgorithmParameters params) throws IOException + { + if(params != null) + { + method.setRequestHeader(HEADER_ALGORITHM_PARAMETERS, Base64.encodeBytes(params.getEncoded())); + } + } + + /** + * Set the algorithm parameters header on the HTTP response + * + * @param response HttpServletResponse + * @param params AlgorithmParameters + * @throws IOException + */ + protected void setAlgorithmParameters(HttpServletResponse response, AlgorithmParameters params) throws IOException + { + if(params != null) + { + response.setHeader(HEADER_ALGORITHM_PARAMETERS, Base64.encodeBytes(params.getEncoded())); + } + } + + /** + * Decode cipher algorithm parameters from the HTTP method + * + * @param method HttpMethod + * @return decoded algorithm parameters + * @throws IOException + */ + protected AlgorithmParameters decodeAlgorithmParameters(HttpMethod method) throws IOException + { + Header header = method.getResponseHeader(HEADER_ALGORITHM_PARAMETERS); + if(header != null) + { + byte[] algorithmParams = Base64.decode(header.getValue()); + AlgorithmParameters algorithmParameters = encryptor.decodeAlgorithmParameters(algorithmParams); + return algorithmParameters; + } + else + { + return null; + } + } + + /** + * Decode cipher algorithm parameters from the HTTP method + * + * @param req + * @return decoded algorithm parameters + * @throws IOException + */ + protected AlgorithmParameters decodeAlgorithmParameters(HttpServletRequest req) throws IOException + { + String header = req.getHeader(HEADER_ALGORITHM_PARAMETERS); + if(header != null) + { + byte[] algorithmParams = Base64.decode(header); + AlgorithmParameters algorithmParameters = encryptor.decodeAlgorithmParameters(algorithmParams); + return algorithmParameters; + } + else + { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] decryptResponseBody(HttpMethod method) throws IOException + { + // TODO fileoutputstream if content is especially large? + InputStream body = method.getResponseBodyAsStream(); + if(body != null) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + FileCopyUtils.copy(body, out); + + AlgorithmParameters params = decodeAlgorithmParameters(method); + if(params != null) + { + byte[] decrypted = encryptor.decrypt(KeyProvider.ALIAS_SOLR, params, out.toByteArray()); + return decrypted; + } + else + { + throw new AlfrescoRuntimeException("Unable to decrypt response body, missing encryption algorithm parameters"); + } + } + else + { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] decryptBody(HttpServletRequest req) throws IOException + { + if(req.getMethod().equals("POST")) + { + InputStream bodyStream = req.getInputStream(); + if(bodyStream != null) + { + // expect algorithParameters header + AlgorithmParameters p = decodeAlgorithmParameters(req); + + // decrypt the body + InputStream in = encryptor.decrypt(KeyProvider.ALIAS_SOLR, p, bodyStream); + return IOUtils.toByteArray(in); + } + else + { + return null; + } + } + else + { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean authenticateResponse(HttpMethod method, String remoteIP, byte[] decryptedBody) + { + try + { + byte[] expectedMAC = getResponseMac(method); + Long timestamp = getResponseTimestamp(method); + if(timestamp == null) + { + return false; + } + remoteIP = IPUtils.getRealIPAddress(remoteIP); + return authenticate(expectedMAC, new MACInput(decryptedBody, timestamp.longValue(), remoteIP)); + } + catch(Exception e) + { + throw new RuntimeException("Unable to authenticate HTTP response", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean authenticate(HttpServletRequest req, byte[] decryptedBody) + { + try + { + byte[] expectedMAC = getMac(req); + Long timestamp = getTimestamp(req); + if(timestamp == null) + { + return false; + } + String ipAddress = IPUtils.getRealIPAddress(req.getRemoteAddr()); + return authenticate(expectedMAC, new MACInput(decryptedBody, timestamp.longValue(), ipAddress)); + } + catch(Exception e) + { + throw new AlfrescoRuntimeException("Unable to authenticate HTTP request", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setRequestAuthentication(HttpMethod method, byte[] message) throws IOException + { + long requestTimestamp = System.currentTimeMillis(); + + // add MAC header + byte[] mac = macUtils.generateMAC(KeyProvider.ALIAS_SOLR, new MACInput(message, requestTimestamp, getLocalIPAddress())); + + if(logger.isDebugEnabled()) + { + logger.debug("Setting MAC " + Arrays.toString(mac) + " on HTTP request " + method.getPath()); + logger.debug("Setting timestamp " + requestTimestamp + " on HTTP request " + method.getPath()); + } + + setRequestMac(method, mac); + + // prevent replays + setRequestTimestamp(method, requestTimestamp); + } + + /** + * {@inheritDoc} + */ + @Override + public void setResponseAuthentication(HttpServletRequest httpRequest, HttpServletResponse httpResponse, + byte[] responseBody, AlgorithmParameters params) throws IOException + { + long responseTimestamp = System.currentTimeMillis(); + byte[] mac = macUtils.generateMAC(KeyProvider.ALIAS_SOLR, + new MACInput(responseBody, responseTimestamp, getLocalIPAddress())); + + if(logger.isDebugEnabled()) + { + logger.debug("Setting MAC " + Arrays.toString(mac) + " on HTTP response to request " + httpRequest.getRequestURI()); + logger.debug("Setting timestamp " + responseTimestamp + " on HTTP response to request " + httpRequest.getRequestURI()); + } + + setAlgorithmParameters(httpResponse, params); + setMac(httpResponse, mac); + + // prevent replays + setTimestamp(httpResponse, responseTimestamp); + } + + protected boolean authenticate(byte[] expectedMAC, MACInput macInput) + { + // check the MAC and, if valid, check that the timestamp is under the threshold and that the remote IP is + // the expected IP + boolean authorized = macUtils.validateMAC(KeyProvider.ALIAS_SOLR, expectedMAC, macInput) && + validateTimestamp(macInput.getTimestamp()); + return authorized; + } + + protected boolean validateTimestamp(long timestamp) + { + long currentTime = System.currentTimeMillis(); + return((currentTime - timestamp) < messageTimeout); + } + +} diff --git a/core/src/main/java/org/alfresco/encryption/DefaultEncryptor.java b/core/src/main/java/org/alfresco/encryption/DefaultEncryptor.java new file mode 100644 index 0000000000..630fa3d56b --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/DefaultEncryptor.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.PropertyCheck; + +/** + * @author Derek Hulley + * @since 4.0 + */ +public class DefaultEncryptor extends AbstractEncryptor +{ + private boolean cacheCiphers = true; + private final ThreadLocal> threadCipher; + + /** + * Default constructor for IOC + */ + public DefaultEncryptor() + { + threadCipher = new ThreadLocal>(); + } + + /** + * Convenience constructor for tests + */ + /* package */ DefaultEncryptor(KeyProvider keyProvider, String cipherAlgorithm, String cipherProvider) + { + this(); + setKeyProvider(keyProvider); + setCipherAlgorithm(cipherAlgorithm); + setCipherProvider(cipherProvider); + } + + public void init() + { + super.init(); + PropertyCheck.mandatory(this, "cipherAlgorithm", cipherAlgorithm); + } + + public void setCacheCiphers(boolean cacheCiphers) + { + this.cacheCiphers = cacheCiphers; + } + + protected Cipher createCipher(int mode, String algorithm, String provider, Key key, AlgorithmParameters params) + throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException + { + Cipher cipher = null; + + if (cipherProvider == null) + { + cipher = Cipher.getInstance(algorithm); + } + else + { + cipher = Cipher.getInstance(algorithm, provider); + } + cipher.init(mode, key, params); + + return cipher; + } + + protected Cipher getCachedCipher(String keyAlias, int mode, AlgorithmParameters params, Key key) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidAlgorithmParameterException + { + CachedCipher cipherInfo = null; + Cipher cipher = null; + + Map ciphers = threadCipher.get(); + if(ciphers == null) + { + ciphers = new HashMap(5); + threadCipher.set(ciphers); + } + cipherInfo = ciphers.get(new CipherKey(keyAlias, mode)); + if(cipherInfo == null) + { + cipher = createCipher(mode, cipherAlgorithm, cipherProvider, key, params); + ciphers.put(new CipherKey(keyAlias, mode), new CachedCipher(cipher, key)); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Cipher constructed: alias=" + keyAlias + "; mode=" + mode + ": " + cipher); + } + } + else + { + // the key has changed, re-construct the cipher + if(cipherInfo.getKey() != key) + { + // key has changed, rendering the cached cipher out of date. Re-create the cipher with + // the new key. + cipher = createCipher(mode, cipherAlgorithm, cipherProvider, key, params); + ciphers.put(new CipherKey(keyAlias, mode), new CachedCipher(cipher, key)); + } + else + { + cipher = cipherInfo.getCipher(); + } + } + + return cipher; + } + + @Override + public Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode) + { + Cipher cipher = null; + + // Get the encryption key + Key key = keyProvider.getKey(keyAlias); + if(key == null) + { + // No encryption possible + return null; + } + + try + { + if(cacheCiphers) + { + cipher = getCachedCipher(keyAlias, mode, params, key); + } + else + { + cipher = createCipher(mode, cipherAlgorithm, cipherProvider, key, params); + } + } + catch (Exception e) + { + throw new AlfrescoRuntimeException( + "Failed to construct cipher: alias=" + keyAlias + "; mode=" + mode, + e); + } + + return cipher; + } + + public boolean keyAvailable(String keyAlias) + { + return keyProvider.getKey(keyAlias) != null; + } + + private static class CipherKey + { + private String keyAlias; + private int mode; + + public CipherKey(String keyAlias, int mode) + { + super(); + this.keyAlias = keyAlias; + this.mode = mode; + } + + public String getKeyAlias() + { + return keyAlias; + } + + public int getMode() + { + return mode; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + + ((keyAlias == null) ? 0 : keyAlias.hashCode()); + result = prime * result + mode; + return result; + } + + @Override + public boolean equals(Object obj) + { + if(this == obj) + { + return true; + } + + if(!(obj instanceof CipherKey)) + { + return false; + } + + CipherKey other = (CipherKey)obj; + if(keyAlias == null) + { + if (other.keyAlias != null) + { + return false; + } + } + else if(!keyAlias.equals(other.keyAlias)) + { + return false; + } + + if(mode != other.mode) + { + return false; + } + + return true; + } + } + + /* + * Stores a cipher and the key used to construct it. + */ + private static class CachedCipher + { + private Key key; + private Cipher cipher; + + public CachedCipher(Cipher cipher, Key key) + { + super(); + this.cipher = cipher; + this.key = key; + } + + public Cipher getCipher() + { + return cipher; + } + + public Key getKey() + { + return key; + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/DefaultFallbackEncryptor.java b/core/src/main/java/org/alfresco/encryption/DefaultFallbackEncryptor.java new file mode 100644 index 0000000000..7bf33ca8b4 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/DefaultFallbackEncryptor.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.InputStream; +import java.io.Serializable; +import java.security.AlgorithmParameters; +import java.security.InvalidKeyException; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.Pair; + +/** + * The fallback encryptor provides a fallback mechanism for decryption, first using the default + * encryption keys and, if they fail (perhaps because they have been changed), falling back + * to a backup set of keys. + * + * Note that encryption will be performed only using the default encryption keys. + * + * @since 4.0 + */ +public class DefaultFallbackEncryptor implements FallbackEncryptor +{ + private Encryptor fallback; + private Encryptor main; + + public DefaultFallbackEncryptor() + { + } + + public DefaultFallbackEncryptor(Encryptor main, Encryptor fallback) + { + this(); + this.main = main; + this.fallback = fallback; + } + + public void setFallback(Encryptor fallback) + { + this.fallback = fallback; + } + + public void setMain(Encryptor main) + { + this.main = main; + } + + /** + * {@inheritDoc} + */ + @Override + public Pair encrypt(String keyAlias, + AlgorithmParameters params, byte[] input) + { + // Note: encrypt supported only for main encryptor + Pair ret = main.encrypt(keyAlias, params, input); + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] decrypt(String keyAlias, AlgorithmParameters params, + byte[] input) + { + byte[] ret; + + // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated), + // fall back to fallback encryptor. + try + { + ret = main.decrypt(keyAlias, params, input); + } + catch(Throwable e) + { + ret = fallback.decrypt(keyAlias, params, input); + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream decrypt(String keyAlias, AlgorithmParameters params, + InputStream in) + { + InputStream ret; + + // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated), + // fall back to fallback encryptor. + try + { + ret = main.decrypt(keyAlias, params, in); + } + catch(Throwable e) + { + ret = fallback.decrypt(keyAlias, params, in); + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public Pair encryptObject(String keyAlias, + AlgorithmParameters params, Object input) + { + // Note: encrypt supported only for main encryptor + Pair ret = main.encryptObject(keyAlias, params, input); + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public Object decryptObject(String keyAlias, AlgorithmParameters params, + byte[] input) + { + Object ret; + + // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated), + // fall back to fallback encryptor. + try + { + ret = main.decryptObject(keyAlias, params, input); + } + catch(Throwable e) + { + ret = fallback.decryptObject(keyAlias, params, input); + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public Serializable sealObject(String keyAlias, AlgorithmParameters params, + Serializable input) + { + // Note: encrypt supported only for main encryptor + Serializable ret = main.sealObject(keyAlias, params, input); + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public Serializable unsealObject(String keyAlias, Serializable input) + throws InvalidKeyException + { + Serializable ret; + + // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated), + // fall back to fallback encryptor. + try + { + ret = main.unsealObject(keyAlias, input); + } + catch(Throwable e) + { + ret = fallback.unsealObject(keyAlias, input); + } + + return ret; + + } + + /** + * {@inheritDoc} + */ + @Override + public AlgorithmParameters decodeAlgorithmParameters(byte[] encoded) + { + AlgorithmParameters ret; + + try + { + ret = main.decodeAlgorithmParameters(encoded); + } + catch(AlfrescoRuntimeException e) + { + ret = fallback.decodeAlgorithmParameters(encoded); + } + + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean keyAvailable(String keyAlias) + { + return main.keyAvailable(keyAlias); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean backupKeyAvailable(String keyAlias) + { + return fallback.keyAvailable(keyAlias); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/encryption/EncryptingOutputStream.java b/core/src/main/java/org/alfresco/encryption/EncryptingOutputStream.java new file mode 100644 index 0000000000..7096da7b18 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/EncryptingOutputStream.java @@ -0,0 +1,252 @@ +/* + * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.encryption; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +/** + * An output stream that encrypts data to another output stream. A lightweight yet secure hybrid encryption scheme is + * used. A random symmetric key is generated and encrypted using the receiver's public key. The supplied data is then + * encrypted using the symmetric key and sent to the underlying stream on a streaming basis. An HMAC checksum is also + * computed on an ongoing basis and appended to the output when the stream is closed. This class can be used in + * conjunction with {@link DecryptingInputStream} to transport data securely. + */ +public class EncryptingOutputStream extends OutputStream +{ + /** The wrapped stream. */ + private final OutputStream wrapped; + + /** The output cipher. */ + private final Cipher outputCipher; + + /** The MAC generator. */ + private final Mac mac; + + /** Internal buffer for MAC computation. */ + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024); + + /** A DataOutputStream on top of our interal buffer. */ + private final DataOutputStream dataStr = new DataOutputStream(this.buffer); + + /** + * Constructs an EncryptingOutputStream using default symmetric encryption parameters. + * + * @param wrapped + * outputstream to store the encrypted data + * @param receiverKey + * the receiver's public key for encrypting the symmetric key + * @param rand + * a secure source of randomness + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws NoSuchAlgorithmException + * the no such algorithm exception + * @throws NoSuchPaddingException + * the no such padding exception + * @throws InvalidKeyException + * the invalid key exception + * @throws BadPaddingException + * the bad padding exception + * @throws IllegalBlockSizeException + * the illegal block size exception + */ + public EncryptingOutputStream(final OutputStream wrapped, final PublicKey receiverKey, final SecureRandom rand) + throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException + { + this(wrapped, receiverKey, "AES", rand, 128, "CBC", "PKCS5PADDING"); + } + + /** + * Constructs an EncryptingOutputStream. + * + * @param wrapped + * outputstream to store the encrypted data + * @param receiverKey + * the receiver's public key for encrypting the symmetric key + * @param algorithm + * symmetric encryption algorithm (e.g. "AES") + * @param rand + * a secure source of randomness + * @param strength + * the key size in bits (e.g. 128) + * @param mode + * encryption mode (e.g. "CBC") + * @param padding + * padding scheme (e.g. "PKCS5PADDING") + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws NoSuchAlgorithmException + * the no such algorithm exception + * @throws NoSuchPaddingException + * the no such padding exception + * @throws InvalidKeyException + * the invalid key exception + * @throws BadPaddingException + * the bad padding exception + * @throws IllegalBlockSizeException + * the illegal block size exception + */ + public EncryptingOutputStream(final OutputStream wrapped, final PublicKey receiverKey, final String algorithm, + final SecureRandom rand, final int strength, final String mode, final String padding) throws IOException, + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, + BadPaddingException + { + // Initialise + this.wrapped = wrapped; + + // Generate a random symmetric key + final KeyGenerator keyGen = KeyGenerator.getInstance(algorithm); + keyGen.init(strength, rand); + final Key symKey = keyGen.generateKey(); + + // Instantiate Symmetric cipher for encryption. + this.outputCipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding); + this.outputCipher.init(Cipher.ENCRYPT_MODE, symKey, rand); + + // Set up HMAC + this.mac = Mac.getInstance("HMACSHA1"); + final byte[] macKeyBytes = new byte[20]; + rand.nextBytes(macKeyBytes); + final Key macKey = new SecretKeySpec(macKeyBytes, "HMACSHA1"); + this.mac.init(macKey); + + // Set up RSA to encrypt symmetric key + final Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING"); + rsa.init(Cipher.ENCRYPT_MODE, receiverKey, rand); + + // Write the header + + // Write out an RSA-encrypted block for the key of the cipher. + writeBlock(rsa.doFinal(symKey.getEncoded())); + + // Write out RSA-encrypted Initialisation Vector block + writeBlock(rsa.doFinal(this.outputCipher.getIV())); + + // Write out key for HMAC. + writeBlock(this.outputCipher.doFinal(macKey.getEncoded())); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#write(int) + */ + @Override + public void write(final int b) throws IOException + { + write(new byte[] + { + (byte) b + }, 0, 1); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#write(byte[]) + */ + @Override + public void write(final byte b[]) throws IOException + { + write(b, 0, b.length); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#write(byte[], int, int) + */ + @Override + public void write(final byte b[], final int off, final int len) throws IOException + { + if (b == null) + { + throw new NullPointerException(); + } + else if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0) + { + throw new IndexOutOfBoundsException(); + } + else if (len == 0) + { + return; + } + final byte[] out = this.outputCipher.update(b, off, len); // Encrypt data. + if (out != null && out.length > 0) + { + writeBlock(out); + } + } + + /** + * Writes a block of data, preceded by its length, and adds it to the HMAC checksum. + * + * @param out + * the data to be written. + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private void writeBlock(final byte[] out) throws IOException + { + this.dataStr.writeInt(out.length); // Write length. + this.dataStr.write(out); // Write encrypted data. + this.dataStr.flush(); + final byte[] block = this.buffer.toByteArray(); + this.buffer.reset(); + this.mac.update(block); + this.wrapped.write(block); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#flush() + */ + @Override + public void flush() throws IOException + { + this.wrapped.flush(); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#close() + */ + @Override + public void close() throws IOException + { + try + { + // Write the last block + writeBlock(this.outputCipher.doFinal()); + } + catch (final GeneralSecurityException e) + { + throw new RuntimeException(e); + } + // Write the MAC code + writeBlock(this.mac.doFinal()); + this.wrapped.close(); + this.dataStr.close(); + } + +} diff --git a/core/src/main/java/org/alfresco/encryption/EncryptionKeysRegistry.java b/core/src/main/java/org/alfresco/encryption/EncryptionKeysRegistry.java new file mode 100644 index 0000000000..bc58421067 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/EncryptionKeysRegistry.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.Key; +import java.util.List; +import java.util.Set; + +/** + * Stores registered encryption keys. + * + * @since 4.0 + * + */ +public interface EncryptionKeysRegistry +{ + public static enum KEY_STATUS + { + OK, CHANGED, MISSING; + }; + + /** + * Is the key with alias 'keyAlias' registered? + * @param keyAlias String + * @return boolean + */ + public boolean isKeyRegistered(String keyAlias); + + /** + * Register the key. + * + * @param keyAlias String + * @param key Key + */ + public void registerKey(String keyAlias, Key key); + + /** + * Unregister the key. + * + * @param keyAlias String + */ + public void unregisterKey(String keyAlias); + + /** + * Check the validity of the key against the registry. + * + * @param keyAlias String + * @param key Key + * @return KEY_STATUS + */ + public KEY_STATUS checkKey(String keyAlias, Key key); + + /** + * Remove the set of keys from the registry. + * + * @param keys Set + */ + public void removeRegisteredKeys(Set keys); + + /** + * Return those keys in the set that have been registered. + * + * @param keys Set + * @return List + */ + public List getRegisteredKeys(Set keys); +} diff --git a/core/src/main/java/org/alfresco/encryption/EncryptionUtils.java b/core/src/main/java/org/alfresco/encryption/EncryptionUtils.java new file mode 100644 index 0000000000..d8211b3c45 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/EncryptionUtils.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.IOException; +import java.security.AlgorithmParameters; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.httpclient.HttpMethod; + +/** + * Various encryption utility methods. + * + * @since 4.0 + */ +public interface EncryptionUtils +{ + /** + * Decrypt the response body of the http method + * + * @param method + * @return decrypted response body + * @throws IOException + */ + public byte[] decryptResponseBody(HttpMethod method) throws IOException; + + /** + * Decrypt the body of the http request + * + * @param req + * @return decrypted response body + * @throws IOException + */ + public byte[] decryptBody(HttpServletRequest req) throws IOException; + + /** + * Authenticate the http method response: validate the MAC, check that the remote IP is + * as expected and that the timestamp is recent. + * + * @param method + * @param remoteIP + * @param decryptedBody + * @return true if the method reponse is authentic, false otherwise + */ + public boolean authenticateResponse(HttpMethod method, String remoteIP, byte[] decryptedBody); + + /** + * Authenticate the http request: validate the MAC, check that the remote IP is + * as expected and that the timestamp is recent. + * + * @param req + * @param decryptedBody + * @return true if the method request is authentic, false otherwise + */ + public boolean authenticate(HttpServletRequest req, byte[] decryptedBody); + + /** + * Encrypt the http method request body + * + * @param method + * @param message + * @throws IOException + */ + public void setRequestAuthentication(HttpMethod method, byte[] message) throws IOException; + + /** + * Sets authentication headers on the HTTP response. + * + * @param httpRequest + * @param httpResponse + * @param responseBody + * @param params + * @throws IOException + */ + public void setResponseAuthentication(HttpServletRequest httpRequest, HttpServletResponse httpResponse, + byte[] responseBody, AlgorithmParameters params) throws IOException; + + /** + * Set the algorithm parameters header on the method request + * + * @param method + * @param params + * @throws IOException + */ + public void setRequestAlgorithmParameters(HttpMethod method, AlgorithmParameters params) throws IOException; +} diff --git a/core/src/main/java/org/alfresco/encryption/Encryptor.java b/core/src/main/java/org/alfresco/encryption/Encryptor.java new file mode 100644 index 0000000000..dc58db43e3 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/Encryptor.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.InputStream; +import java.io.Serializable; +import java.security.AlgorithmParameters; +import java.security.InvalidKeyException; + +import org.alfresco.util.Pair; + +/** + * Interface providing methods to encrypt and decrypt data. + * + * @since 4.0 + */ +public interface Encryptor +{ + /** + * Encrypt some bytes + * + * @param keyAlias the encryption key alias + * @param input the data to encrypt + * @return the encrypted data and parameters used + */ + Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input); + + /** + * Decrypt some bytes + * + * @param keyAlias the encryption key alias + * @param input the data to decrypt + * @return the unencrypted data + */ + byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input); + + /** + * Decrypt an input stream + * + * @param keyAlias the encryption key alias + * @param in the data to decrypt + * @return the unencrypted data + */ + InputStream decrypt(String keyAlias, AlgorithmParameters params, InputStream in); + + /** + * Encrypt an object + * + * @param keyAlias the encryption key alias + * @param input the object to write to bytes + * @return the encrypted data and parameters used + */ + Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input); + + /** + * Decrypt data as an object + * + * @param keyAlias the encryption key alias + * @param input the data to decrypt + * @return the unencrypted data deserialized + */ + Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input); + + /** + * Convenience method to seal on object up cryptographically. + *

+ * Note that the original object may be returned directly if there is no key associated with + * the alias. + * + * @param keyAlias the encryption key alias + * @param input the object to encrypt and seal + * @return the sealed object that can be decrypted with the original key + */ + Serializable sealObject(String keyAlias, AlgorithmParameters params, Serializable input); + + /** + * Convenience method to unseal on object sealed up cryptographically. + *

+ * Note that the algorithm parameters not provided on the assumption that a symmetric key + * algorithm is in use - only the key is required for unsealing. + *

+ * Note that the original object may be returned directly if there is no key associated with + * the alias or if the input object is not a SealedObject. + * + * @param keyAlias the encryption key alias + * @param input the object to decrypt and unseal + * @return the original unsealed object that was encrypted with the original key + * @throws IllegalStateException if the key alias is not valid and the input is a + * SealedObject + */ + Serializable unsealObject(String keyAlias, Serializable input) throws InvalidKeyException; + + /** + * Decodes encoded cipher algorithm parameters + * + * @param encoded the encoded cipher algorithm parameters + * @return the decoded cipher algorithmParameters + */ + AlgorithmParameters decodeAlgorithmParameters(byte[] encoded); + + boolean keyAvailable(String keyAlias); +} diff --git a/core/src/main/java/org/alfresco/encryption/FallbackEncryptor.java b/core/src/main/java/org/alfresco/encryption/FallbackEncryptor.java new file mode 100644 index 0000000000..a5428fcadd --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/FallbackEncryptor.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +/** + * A fallback encryptor provides a fallback mechanism for decryption, first using the default + * encryption keys and, if they fail (perhaps because they have been changed), falling back + * to a backup set of keys. + * + * Note that encryption will be performed only using the default encryption keys. + * + * @since 4.0 + */ +public interface FallbackEncryptor extends Encryptor +{ + /** + * Is the backup key available in order to fall back to? + * + * @return boolean + */ + boolean backupKeyAvailable(String keyAlias); +} diff --git a/core/src/main/java/org/alfresco/encryption/GenerateSecretKey.java b/core/src/main/java/org/alfresco/encryption/GenerateSecretKey.java new file mode 100644 index 0000000000..8e10773505 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/GenerateSecretKey.java @@ -0,0 +1,47 @@ +package org.alfresco.encryption; + +import java.security.SecureRandom; + +import javax.crypto.spec.DESedeKeySpec; + +import org.apache.commons.codec.binary.Base64; + +/** + * + * Generate a secret key for use by the repository. + * + * @since 4.0 + * + */ +public class GenerateSecretKey +{ + public byte[] generateKeyData() + { + try + { + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + random.setSeed(System.currentTimeMillis()); + byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN]; + random.nextBytes(bytes); + return bytes; + } + catch(Exception e) + { + throw new RuntimeException("Unable to generate secret key", e); + } + } + + public static void main(String args[]) + { + try + { + GenerateSecretKey gen = new GenerateSecretKey(); + byte[] bytes = gen.generateKeyData(); + System.out.print(Base64.encodeBase64String(bytes)); + } + catch(Throwable e) + { + e.printStackTrace(); + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/InvalidKeystoreException.java b/core/src/main/java/org/alfresco/encryption/InvalidKeystoreException.java new file mode 100644 index 0000000000..4048f9c0bc --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/InvalidKeystoreException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +/** + * + * @since 4.0 + * + */ +public class InvalidKeystoreException extends Exception +{ + private static final long serialVersionUID = -1324791685965572313L; + + public InvalidKeystoreException(String message) + { + super(message); + } +} diff --git a/core/src/main/java/org/alfresco/encryption/KeyMap.java b/core/src/main/java/org/alfresco/encryption/KeyMap.java new file mode 100644 index 0000000000..74e9e2cec7 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/KeyMap.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.Key; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A simple map of key aliases to keys. Each key has an associated timestamp indicating + * when it was last loaded from the keystore on disk. + * + * @since 4.0 + * + */ +public class KeyMap +{ + private Map keys; + + public KeyMap() + { + this.keys = new HashMap(5); + } + + public KeyMap(Map keys) + { + super(); + this.keys = keys; + } + + public int numKeys() + { + return keys.size(); + } + + public Set getKeyAliases() + { + return keys.keySet(); + } + + // always returns an instance; if null will return a CachedKey.NULL + public CachedKey getCachedKey(String keyAlias) + { + CachedKey cachedKey = keys.get(keyAlias); + return (cachedKey != null ? cachedKey : CachedKey.NULL); + } + + public Key getKey(String keyAlias) + { + return getCachedKey(keyAlias).getKey(); + } + + public void setKey(String keyAlias, Key key) + { + keys.put(keyAlias, new CachedKey(key)); + } +} diff --git a/core/src/main/java/org/alfresco/encryption/KeyProvider.java b/core/src/main/java/org/alfresco/encryption/KeyProvider.java new file mode 100644 index 0000000000..a24a398953 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/KeyProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.Key; + +/** + * A key provider returns the secret keys for different use cases. + * + * @since 4.0 + */ +public interface KeyProvider +{ + // TODO: Allow the aliases to be configured i.e. include an alias mapper + /** + * Constant representing the keystore alias for keys to encrypt/decrypt node metadata + */ + public static final String ALIAS_METADATA = "metadata"; + + /** + * Constant representing the keystore alias for keys to encrypt/decrypt SOLR transfer data + */ + public static final String ALIAS_SOLR = "solr"; + + /** + * Get an encryption key if available. + * + * @param keyAlias the key alias + * @return the encryption key and a timestamp of when it was last changed + */ + public Key getKey(String keyAlias); +} diff --git a/core/src/main/java/org/alfresco/encryption/KeyResourceLoader.java b/core/src/main/java/org/alfresco/encryption/KeyResourceLoader.java new file mode 100644 index 0000000000..f32325ed80 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/KeyResourceLoader.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Manages key resources (key store and key store passwords) + * + * @since 4.0 + * + */ +public interface KeyResourceLoader +{ + /** + * Loads and returns an InputStream of the key store at the configured location. + * If the file cannot be found this method returns null. + * + * @return InputStream + * @throws FileNotFoundException + */ + public InputStream getKeyStore(String keyStoreLocation) throws FileNotFoundException; + + /** + * Loads key metadata from the configured passwords file location. + * + * Note that the passwords are not cached locally. + * If the file cannot be found this method returns null. + * + * @return Properties + * @throws IOException + */ + public Properties loadKeyMetaData(String keyMetaDataFileLocation) throws IOException, FileNotFoundException; +} diff --git a/core/src/main/java/org/alfresco/encryption/KeyStoreParameters.java b/core/src/main/java/org/alfresco/encryption/KeyStoreParameters.java new file mode 100644 index 0000000000..5b8ebe4504 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/KeyStoreParameters.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import org.alfresco.util.PropertyCheck; + +/** + * Stores Java keystore initialisation parameters. + * + * @since 4.0 + * + */ +public class KeyStoreParameters +{ + private String name; + private String type; + private String provider; + private String keyMetaDataFileLocation; + private String location; + + public KeyStoreParameters() + { + } + + public KeyStoreParameters(String name, String type, String keyStoreProvider, + String keyMetaDataFileLocation, String location) + { + super(); + this.name = name; + this.type = type; + this.provider = keyStoreProvider; + this.keyMetaDataFileLocation = keyMetaDataFileLocation; + this.location = location; + } + + public void init() + { + if (!PropertyCheck.isValidPropertyString(getLocation())) + { + setLocation(null); + } + if (!PropertyCheck.isValidPropertyString(getProvider())) + { + setProvider(null); + } + if (!PropertyCheck.isValidPropertyString(getType())) + { + setType(null); + } + if (!PropertyCheck.isValidPropertyString(getKeyMetaDataFileLocation())) + { + setKeyMetaDataFileLocation(null); + } + } + + public String getName() + { + return name; + } + + public String getType() + { + return type; + } + + public String getProvider() + { + return provider; + } + + public String getKeyMetaDataFileLocation() + { + return keyMetaDataFileLocation; + } + + public String getLocation() + { + return location; + } + + public void setName(String name) + { + this.name = name; + } + + public void setType(String type) + { + this.type = type; + } + + public void setProvider(String provider) + { + this.provider = provider; + } + + public void setKeyMetaDataFileLocation(String keyMetaDataFileLocation) + { + this.keyMetaDataFileLocation = keyMetaDataFileLocation; + } + + public void setLocation(String location) + { + this.location = location; + } +} diff --git a/core/src/main/java/org/alfresco/encryption/KeysReport.java b/core/src/main/java/org/alfresco/encryption/KeysReport.java new file mode 100644 index 0000000000..49f10fce81 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/KeysReport.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.util.List; + +/** + * A report on which keys have changed and which keys have not changed. + * + * @since 4.0 + * + */ +public class KeysReport +{ + private List keysChanged; + private List keysUnchanged; + + public KeysReport(List keysChanged, List keysUnchanged) + { + super(); + this.keysChanged = keysChanged; + this.keysUnchanged = keysUnchanged; + } + + public List getKeysChanged() + { + return keysChanged; + } + + public List getKeysUnchanged() + { + return keysUnchanged; + } +} diff --git a/core/src/main/java/org/alfresco/encryption/KeystoreKeyProvider.java b/core/src/main/java/org/alfresco/encryption/KeystoreKeyProvider.java new file mode 100644 index 0000000000..dcc2a5c960 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/KeystoreKeyProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.security.Key; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * Provides system-wide secret keys for symmetric database encryption from a key store + * in the filesystem. Just wraps a key store. + * + * @author Derek Hulley + * @since 4.0 + */ +public class KeystoreKeyProvider extends AbstractKeyProvider +{ + private static final Log logger = LogFactory.getLog(KeystoreKeyProvider.class); + + private AlfrescoKeyStore keyStore; + private boolean useBackupKeys = false; + + /** + * Constructs the provider with required defaults + */ + public KeystoreKeyProvider() + { + } + + public KeystoreKeyProvider(KeyStoreParameters keyStoreParameters, KeyResourceLoader keyResourceLoader) + { + this(); + this.keyStore = new AlfrescoKeyStoreImpl(keyStoreParameters, keyResourceLoader); + init(); + } + + public void setUseBackupKeys(boolean useBackupKeys) + { + this.useBackupKeys = useBackupKeys; + } + + /** + * + * @param keyStore + */ + public KeystoreKeyProvider(AlfrescoKeyStore keyStore) + { + this(); + this.keyStore = keyStore; + init(); + } + + public void setKeyStore(AlfrescoKeyStore keyStore) + { + this.keyStore = keyStore; + } + + public void init() + { + } + + /** + * {@inheritDoc} + */ + @Override + public Key getKey(String keyAlias) + { + if(useBackupKeys) + { + return keyStore.getBackupKey(keyAlias); + } + else + { + return keyStore.getKey(keyAlias); + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/MACUtils.java b/core/src/main/java/org/alfresco/encryption/MACUtils.java new file mode 100644 index 0000000000..e7f7eaad6b --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/MACUtils.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.Key; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.crypto.Mac; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides support for generating and checking MACs (Message Authentication Codes) using Alfresco's + * secret keys. + * + * @since 4.0 + * + */ +public class MACUtils +{ + private static Log logger = LogFactory.getLog(Encryptor.class); + private static byte SEPARATOR = 0; + + private final ThreadLocal threadMac; + + private KeyProvider keyProvider; + private String macAlgorithm; + + /** + * Default constructor for IOC + */ + public MACUtils() + { + threadMac = new ThreadLocal(); + } + + public void setKeyProvider(KeyProvider keyProvider) + { + this.keyProvider = keyProvider; + } + + public void setMacAlgorithm(String macAlgorithm) + { + this.macAlgorithm = macAlgorithm; + } + + protected Mac getMac(String keyAlias) throws Exception + { + Mac mac = threadMac.get(); + if(mac == null) + { + mac = Mac.getInstance(macAlgorithm); + + threadMac.set(mac); + } + Key key = keyProvider.getKey(keyAlias); + if(key == null) + { + throw new AlfrescoRuntimeException("Unexpected null key for key alias " + keyAlias); + } + mac.init(key); + return mac; + } + + protected byte[] longToByteArray(long l) throws IOException + { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeLong(l); + dos.flush(); + return bos.toByteArray(); + } + + public byte[] generateMAC(String keyAlias, MACInput macInput) + { + try + { + InputStream fullMessage = macInput.getMACInput(); + + if(logger.isDebugEnabled()) + { + logger.debug("Generating MAC for " + macInput + "..."); + } + + Mac mac = getMac(keyAlias); + + byte[] buf = new byte[1024]; + int len; + while((len = fullMessage.read(buf, 0, 1024)) != -1) + { + mac.update(buf, 0, len); + } + byte[] newMAC = mac.doFinal(); + + if(logger.isDebugEnabled()) + { + logger.debug("...done. MAC is " + Arrays.toString(newMAC)); + } + + return newMAC; + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Failed to generate MAC", e); + } + } + + /** + * Compares the expectedMAC against the MAC generated from + * Assumes message has been decrypted + * @param keyAlias String + * @param expectedMAC byte[] + * @param macInput MACInput + * @return boolean + */ + public boolean validateMAC(String keyAlias, byte[] expectedMAC, MACInput macInput) + { + try + { + byte[] mac = generateMAC(keyAlias, macInput); + + if(logger.isDebugEnabled()) + { + logger.debug("Validating expected MAC " + Arrays.toString(expectedMAC) + " against mac " + Arrays.toString(mac) + " for MAC input " + macInput + "..."); + } + + boolean areEqual = Arrays.equals(expectedMAC, mac); + + if(logger.isDebugEnabled()) + { + logger.debug(areEqual ? "...MAC validation succeeded." : "...MAC validation failed."); + } + + return areEqual; + } + catch (Exception e) + { + throw new AlfrescoRuntimeException("Failed to validate MAC", e); + } + } + + /** + * Represents the information to be fed into the MAC generator + * + * @since 4.0 + * + */ + public static class MACInput + { + // The message, may be null + private InputStream message; + private long timestamp; + private String ipAddress; + + public MACInput(byte[] message, long timestamp, String ipAddress) + { + this.message = (message != null ? new ByteArrayInputStream(message) : null); + this.timestamp = timestamp; + this.ipAddress = ipAddress; + } + + public InputStream getMessage() + { + return message; + } + + public long getTimestamp() + { + return timestamp; + } + + public String getIpAddress() + { + return ipAddress; + } + + public InputStream getMACInput() throws IOException + { + List inputStreams = new ArrayList(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(bytes); + out.writeUTF(ipAddress); + out.writeByte(SEPARATOR); + out.writeLong(timestamp); + inputStreams.add(new ByteArrayInputStream(bytes.toByteArray())); + + if(message != null) + { + inputStreams.add(message); + } + + return new MessageInputStream(inputStreams); + } + + public String toString() + { + StringBuilder sb = new StringBuilder("MACInput["); + sb.append("timestamp: ").append(getTimestamp()); + sb.append("ipAddress: ").append(getIpAddress()); + return sb.toString(); + } + } + + private static class MessageInputStream extends InputStream + { + private List input; + private InputStream activeInputStream; + private int currentStream = 0; + + public MessageInputStream(List input) + { + this.input = input; + this.currentStream = 0; + this.activeInputStream = input.get(currentStream); + } + + @Override + public void close() throws IOException + { + IOException firstIOException = null; + + for(InputStream in : input) + { + try + { + in.close(); + } + catch(IOException e) + { + if(firstIOException == null) + { + firstIOException = e; + } + } + } + + if(firstIOException != null) + { + throw firstIOException; + } + + } + + @Override + public int read() throws IOException + { + int i = activeInputStream.read(); + if(i == -1) + { + currentStream++; + if(currentStream >= input.size()) + { + return -1; + } + else + { + activeInputStream = input.get(currentStream); + i = activeInputStream.read(); + } + } + + return i; + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/MissingKeyException.java b/core/src/main/java/org/alfresco/encryption/MissingKeyException.java new file mode 100644 index 0000000000..ed55eebb78 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/MissingKeyException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +/** + * + * @since 4.0 + * + */ +public class MissingKeyException extends Exception +{ + private static final long serialVersionUID = -7843412242954504581L; + + private String keyAlias; + private String keyStoreLocation; + + public MissingKeyException(String message) + { + super(message); + } + + public MissingKeyException(String keyAlias, String keyStoreLocation) + { + // TODO i18n + super("Key " + keyAlias + " is missing from keystore " + keyStoreLocation); + } + + public String getKeyAlias() + { + return keyAlias; + } + + public String getKeyStoreLocation() + { + return keyStoreLocation; + } +} diff --git a/core/src/main/java/org/alfresco/encryption/SpringKeyResourceLoader.java b/core/src/main/java/org/alfresco/encryption/SpringKeyResourceLoader.java new file mode 100644 index 0000000000..cf4b538b9c --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/SpringKeyResourceLoader.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.Resource; +import org.springframework.util.ResourceUtils; + +/** + * Loads key resources (key store and key store passwords) from the Spring classpath. + * + * @since 4.0 + * + */ +public class SpringKeyResourceLoader implements KeyResourceLoader, ApplicationContextAware +{ + /** + * The application context might not be available, in which case the usual URL + * loading is used. + */ + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + /** + * Helper method to switch between application context resource loading or + * simpler current classloader resource loading. + */ + private InputStream getSafeInputStream(String location) + { + try + { + final InputStream is; + if (applicationContext != null) + { + Resource resource = applicationContext.getResource(location); + if (resource.exists()) + { + is = new BufferedInputStream(resource.getInputStream()); + } + else + { + // Fall back to conventional loading + File file = ResourceUtils.getFile(location); + if (file.exists()) + { + is = new BufferedInputStream(new FileInputStream(file)); + } + else + { + is = null; + } + } + } + else + { + // Load conventionally (i.e. we are in a unit test) + File file = ResourceUtils.getFile(location); + if (file.exists()) + { + is = new BufferedInputStream(new FileInputStream(file)); + } + else + { + is = null; + } + } + + return is; + } + catch (IOException e) + { + return null; + } + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getKeyStore(String keyStoreLocation) + { + if (keyStoreLocation == null) + { + return null; + } + return getSafeInputStream(keyStoreLocation); + } + + /** + * {@inheritDoc} + */ + @Override + public Properties loadKeyMetaData(String keyMetaDataFileLocation) throws IOException + { + if (keyMetaDataFileLocation == null) + { + return null; + } + + try + { + InputStream is = getSafeInputStream(keyMetaDataFileLocation); + if (is == null) + { + return null; + } + else + { + try + { + Properties p = new Properties(); + p.load(is); + return p; + } + finally + { + try { is.close(); } catch (Throwable e) {} + } + } + } + catch(FileNotFoundException e) + { + return null; + } + } +} diff --git a/core/src/main/java/org/alfresco/encryption/ssl/AuthSSLInitializationError.java b/core/src/main/java/org/alfresco/encryption/ssl/AuthSSLInitializationError.java new file mode 100644 index 0000000000..f2259dc10e --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/ssl/AuthSSLInitializationError.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption.ssl; + +/** + *

+ * Signals fatal error in initialization of {@link AuthSSLProtocolSocketFactory}. + *

+ * + *

+ * Adapted from code here: http://svn.apache.org/viewvc/httpcomponents/oac.hc3x/trunk/src/contrib/org/apache/commons/httpclient/contrib/ssl/AuthSSLX509TrustManager.java?revision=608014&view=co + *

+ * + * @since 4.0 + */ +public class AuthSSLInitializationError extends Error +{ + private static final long serialVersionUID = 8135341334029823112L; + + /** + * Creates a new AuthSSLInitializationError. + */ + public AuthSSLInitializationError() + { + super(); + } + + /** + * Creates a new AuthSSLInitializationError with the specified message. + * + * @param message error message + */ + public AuthSSLInitializationError(String message) + { + super(message); + } +} diff --git a/core/src/main/java/org/alfresco/encryption/ssl/AuthSSLProtocolSocketFactory.java b/core/src/main/java/org/alfresco/encryption/ssl/AuthSSLProtocolSocketFactory.java new file mode 100644 index 0000000000..d3a08e6b3f --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/ssl/AuthSSLProtocolSocketFactory.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption.ssl; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.security.KeyStore; + +import javax.net.SocketFactory; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; + +import org.alfresco.encryption.AlfrescoKeyStore; +import org.alfresco.encryption.KeyResourceLoader; +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.httpclient.ConnectTimeoutException; +import org.apache.commons.httpclient.params.HttpConnectionParams; +import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

+ * Mutual Authentication against an Alfresco repository. + * + * AuthSSLProtocolSocketFactory can be used to validate the identity of the HTTPS + * server against a list of trusted certificates and to authenticate to the HTTPS + * server using a private key. + *

+ * + *

+ * Adapted from code here: http://svn.apache.org/viewvc/httpcomponents/oac.hc3x/trunk/src/contrib/org/apache/commons/httpclient/contrib/ssl/AuthSSLX509TrustManager.java?revision=608014&view=co + *

+ * + *

+ * AuthSSLProtocolSocketFactory will enable server authentication when supplied with + * a {@link KeyStore truststore} file containing one or several trusted certificates. + * The client secure socket will reject the connection during the SSL session handshake + * if the target HTTPS server attempts to authenticate itself with a non-trusted + * certificate. + *

+ * + *

+ * AuthSSLProtocolSocketFactory will enable client authentication when supplied with + * a {@link KeyStore keystore} file containg a private key/public certificate pair. + * The client secure socket will use the private key to authenticate itself to the target + * HTTPS server during the SSL session handshake if requested to do so by the server. + * The target HTTPS server will in its turn verify the certificate presented by the client + * in order to establish client's authenticity + *

+ * + * + * @since 4.0 + */ +public class AuthSSLProtocolSocketFactory implements SecureProtocolSocketFactory +{ + /** Log object for this class. */ + private static final Log logger = LogFactory.getLog(AuthSSLProtocolSocketFactory.class); + + private SSLContext sslcontext = null; + + private AlfrescoKeyStore keyStore = null; + private AlfrescoKeyStore trustStore = null; + + /** + * Constructor for AuthSSLProtocolSocketFactory. Either a keystore or truststore file + * must be given. Otherwise SSL context initialization error will result. + * + * @param sslKeyStore SSL parameters to use. + * @param keyResourceLoader loads key resources from an arbitrary source e.g. classpath + */ + public AuthSSLProtocolSocketFactory(AlfrescoKeyStore sslKeyStore, AlfrescoKeyStore sslTrustStore, KeyResourceLoader keyResourceLoader) + { + super(); + this.keyStore = sslKeyStore; + this.trustStore = sslTrustStore; + } + + private SSLContext createSSLContext() + { + KeyManager[] keymanagers = keyStore.createKeyManagers();; + TrustManager[] trustmanagers = trustStore.createTrustManagers(); + + try + { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(keymanagers, trustmanagers, null); + return sslcontext; + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to create SSL context", e); + } + } + + private SSLContext getSSLContext() + { + try + { + if(this.sslcontext == null) + { + this.sslcontext = createSSLContext(); + } + return this.sslcontext; + } + catch(Throwable e) + { + throw new AlfrescoRuntimeException("Unable to create SSL context", e); + } + } + + /** + * Attempts to get a new socket connection to the given host within the given time limit. + *

+ * To circumvent the limitations of older JREs that do not support connect timeout a + * controller thread is executed. The controller thread attempts to create a new socket + * within the given limit of time. If socket constructor does not return until the + * timeout expires, the controller terminates and throws an {@link ConnectTimeoutException} + *

+ * + * @param host the host name/IP + * @param port the port on the host + * @param localAddress the local host name/IP to bind the socket to + * @param localPort the port on the local machine + * @param params {@link HttpConnectionParams Http connection parameters} + * + * @return Socket a new socket + * + * @throws IOException if an I/O error occurs while creating the socket + * @throws UnknownHostException if the IP address of the host cannot be + * determined + */ + public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort, + final HttpConnectionParams params) throws IOException, UnknownHostException, ConnectTimeoutException + { + SSLSocket sslSocket = null; + + if(params == null) + { + throw new IllegalArgumentException("Parameters may not be null"); + } + int timeout = params.getConnectionTimeout(); + SocketFactory socketfactory = getSSLContext().getSocketFactory(); + if(timeout == 0) + { + sslSocket = (SSLSocket)socketfactory.createSocket(host, port, localAddress, localPort); + } + else + { + sslSocket = (SSLSocket)socketfactory.createSocket(); + SocketAddress localaddr = new InetSocketAddress(localAddress, localPort); + SocketAddress remoteaddr = new InetSocketAddress(host, port); + sslSocket.bind(localaddr); + sslSocket.connect(remoteaddr, timeout); + } + + return sslSocket; + } + + /** + * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int) + */ + public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) + throws IOException, UnknownHostException + { + SSLSocket sslSocket = (SSLSocket)getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort); + return sslSocket; + } + + /** + * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int) + */ + public Socket createSocket(String host, int port) throws IOException, UnknownHostException + { + SSLSocket sslSocket = (SSLSocket)getSSLContext().getSocketFactory().createSocket(host, port); + return sslSocket; + } + + /** + * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean) + */ + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) + throws IOException, UnknownHostException + { + SSLSocket sslSocket = (SSLSocket)getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose); + return sslSocket; + } +} diff --git a/core/src/main/java/org/alfresco/encryption/ssl/SSLEncryptionParameters.java b/core/src/main/java/org/alfresco/encryption/ssl/SSLEncryptionParameters.java new file mode 100644 index 0000000000..1252193e94 --- /dev/null +++ b/core/src/main/java/org/alfresco/encryption/ssl/SSLEncryptionParameters.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.encryption.ssl; + +import org.alfresco.encryption.KeyStoreParameters; + +/** + * + * @since 4.0 + * + */ +public class SSLEncryptionParameters +{ + private KeyStoreParameters keyStoreParameters; + private KeyStoreParameters trustStoreParameters; + + /** + * Default constructor (for use by Spring) + */ + public SSLEncryptionParameters() + { + super(); + } + + public SSLEncryptionParameters(KeyStoreParameters keyStoreParameters, KeyStoreParameters trustStoreParameters) + { + super(); + this.keyStoreParameters = keyStoreParameters; + this.trustStoreParameters = trustStoreParameters; + } + + public KeyStoreParameters getKeyStoreParameters() + { + return keyStoreParameters; + } + + public KeyStoreParameters getTrustStoreParameters() + { + return trustStoreParameters; + } + + public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters) + { + this.keyStoreParameters = keyStoreParameters; + } + + public void setTrustStoreParameters(KeyStoreParameters trustStoreParameters) + { + this.trustStoreParameters = trustStoreParameters; + } +} diff --git a/core/src/main/java/org/alfresco/error/AlfrescoRuntimeException.java b/core/src/main/java/org/alfresco/error/AlfrescoRuntimeException.java new file mode 100644 index 0000000000..af0f059b88 --- /dev/null +++ b/core/src/main/java/org/alfresco/error/AlfrescoRuntimeException.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.error; + +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.extensions.surf.util.I18NUtil; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * I18n'ed runtime exception thrown by Alfresco code. + * + * @author gavinc + */ +@AlfrescoPublicApi +public class AlfrescoRuntimeException extends RuntimeException +{ + /** + * Serial version UUID + */ + private static final long serialVersionUID = 3787143176461219632L; + + private static final String MESSAGE_DELIMITER = " "; + + private String msgId; + private transient Object[] msgParams = null; + + /** + * Helper factory method making use of variable argument numbers + */ + public static AlfrescoRuntimeException create(String msgId, Object ...objects) + { + return new AlfrescoRuntimeException(msgId, objects); + } + + /** + * Helper factory method making use of variable argument numbers + */ + public static AlfrescoRuntimeException create(Throwable cause, String msgId, Object ...objects) + { + return new AlfrescoRuntimeException(msgId, objects, cause); + } + + /** + * Utility to convert a general Throwable to a RuntimeException. No conversion is done if the + * throwable is already a RuntimeException. + * + * @see #create(Throwable, String, Object...) + */ + public static RuntimeException makeRuntimeException(Throwable e, String msgId, Object ...objects) + { + if (e instanceof RuntimeException) + { + return (RuntimeException) e; + } + // Convert it + return AlfrescoRuntimeException.create(e, msgId, objects); + } + + /** + * Constructor + * + * @param msgId the message id + */ + public AlfrescoRuntimeException(String msgId) + { + super(resolveMessage(msgId, null)); + this.msgId = msgId; + } + + /** + * Constructor + * + * @param msgId the message id + * @param msgParams the message parameters + */ + public AlfrescoRuntimeException(String msgId, Object[] msgParams) + { + super(resolveMessage(msgId, msgParams)); + this.msgId = msgId; + this.msgParams = msgParams; + } + + /** + * Constructor + * + * @param msgId the message id + * @param cause the exception cause + */ + public AlfrescoRuntimeException(String msgId, Throwable cause) + { + super(resolveMessage(msgId, null), cause); + this.msgId = msgId; + } + + /** + * Constructor + * + * @param msgId the message id + * @param msgParams the message parameters + * @param cause the exception cause + */ + public AlfrescoRuntimeException(String msgId, Object[] msgParams, Throwable cause) + { + super(resolveMessage(msgId, msgParams), cause); + this.msgId = msgId; + this.msgParams = msgParams; + } + + /** + * @return the msgId + */ + public String getMsgId() + { + return msgId; + } + + /** + * @return the msgParams + */ + public Object[] getMsgParams() + { + return msgParams; + } + + /** + * @return the numericalId + */ + public String getNumericalId() + { + return getMessage().split(MESSAGE_DELIMITER)[0]; + } + + /** + * Resolves the message id to the localised string. + *

+ * If a localised message can not be found then the message Id is + * returned. + * + * @param messageId the message Id + * @param params message parameters + * @return the localised message (or the message id if none found) + */ + private static String resolveMessage(String messageId, Object[] params) + { + String message = I18NUtil.getMessage(messageId, params); + if (message == null) + { + // If a localized string cannot be found then return the messageId and the params + message = messageId; + if (params != null) + { + message += " - " + Arrays.toString(params); + } + } + return buildErrorLogNumber(message); + } + + /** + * Generate an error log number - based on MMDDXXXX - where M is month, + * D is day and X is an atomic integer count. + * + * @param message Message to prepend the error log number to + * + * @return message with error log number prefix + */ + private static String buildErrorLogNumber(String message) + { + // ensure message is not null + if (message == null) + { + message= ""; + } + + Date today = new Date(); + StringBuilder buf = new StringBuilder(message.length() + 10); + padInt(buf, today.getMonth(), 2); + padInt(buf, today.getDate(), 2); + padInt(buf, errorCounter.getAndIncrement(), 4); + buf.append(MESSAGE_DELIMITER); + buf.append(message); + return buf.toString(); + } + + /** + * Helper to zero pad a number to specified length + */ + private static void padInt(StringBuilder buffer, int value, int length) + { + String strValue = Integer.toString(value); + for (int i = length - strValue.length(); i > 0; i--) + { + buffer.append('0'); + } + buffer.append(strValue); + } + + private static AtomicInteger errorCounter = new AtomicInteger(); + + /** + * Get the root cause. + */ + public Throwable getRootCause() + { + Throwable cause = this; + for (Throwable tmp = this; tmp != null ; tmp = cause.getCause()) + { + cause = tmp; + } + return cause; + } +} diff --git a/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java b/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java new file mode 100644 index 0000000000..8ff83c2452 --- /dev/null +++ b/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.error; + +/** + * Helper class to provide information about exception stacks. + * + * @author Derek Hulley + */ +public class ExceptionStackUtil +{ + /** + * Searches through the exception stack of the given throwable to find any instance + * of the possible cause. The top-level throwable will also be tested. + * + * @param throwable the exception condition to search + * @param possibleCauses the types of the exception conditions of interest + * @return Returns the first instance that matches one of the given + * possible types, or null if there is nothing in the stack + */ + public static Throwable getCause(Throwable throwable, Class ... possibleCauses) + { + while (throwable != null) + { + for (Class possibleCauseClass : possibleCauses) + { + Class throwableClass = throwable.getClass(); + if (possibleCauseClass.isAssignableFrom(throwableClass)) + { + // We have a match + return throwable; + } + } + // There was no match, so dig deeper + Throwable cause = throwable.getCause(); + throwable = (throwable == cause) ? null : cause; + } + // Nothing found + return null; + } +} diff --git a/core/src/main/java/org/alfresco/error/StackTraceUtil.java b/core/src/main/java/org/alfresco/error/StackTraceUtil.java new file mode 100644 index 0000000000..1291226afe --- /dev/null +++ b/core/src/main/java/org/alfresco/error/StackTraceUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.error; + + +/** + * Helper class around outputting stack traces. + * + * @author Derek Hulley + */ +public class StackTraceUtil +{ + /** + * Builds a message with the stack trace of the form: + *

+     *    SOME MESSAGE:
+     *       Started at:
+     *          com.package...
+     *          com.package...
+     *          ...
+     * 
+ * + * @param msg the initial error message + * @param stackTraceElements the stack trace elements + * @param sb the buffer to append to + * @param maxDepth the maximum number of trace elements to output. 0 or less means output all. + */ + public static void buildStackTrace( + String msg, + StackTraceElement[] stackTraceElements, + StringBuilder sb, + int maxDepth) + { + String lineEnding = System.getProperty("line.separator", "\n"); + + sb.append(msg).append(" ").append(lineEnding) + .append(" Started at: ").append(lineEnding); + for (int i = 0; i < stackTraceElements.length; i++) + { + if (i > maxDepth && maxDepth > 0) + { + sb.append(" ..."); + break; + } + sb.append(" ").append(stackTraceElements[i]); + if (i < stackTraceElements.length - 1) + { + sb.append(lineEnding); + } + } + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/AbstractHttpClient.java b/core/src/main/java/org/alfresco/httpclient/AbstractHttpClient.java new file mode 100644 index 0000000000..0c6e6b6406 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/AbstractHttpClient.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +import java.io.IOException; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpConnectionManager; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.HeadMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public abstract class AbstractHttpClient implements AlfrescoHttpClient +{ + private static final Log logger = LogFactory.getLog(AlfrescoHttpClient.class); + + public static final String ALFRESCO_DEFAULT_BASE_URL = "/alfresco"; + + public static final int DEFAULT_SAVEPOST_BUFFER = 4096; + + // Remote Server access + protected HttpClient httpClient = null; + + private String baseUrl = ALFRESCO_DEFAULT_BASE_URL; + + public AbstractHttpClient(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + protected HttpClient getHttpClient() + { + return httpClient; + } + + /** + * @return the baseUrl + */ + public String getBaseUrl() + { + return baseUrl; + } + + /** + * @param baseUrl the baseUrl to set + */ + public void setBaseUrl(String baseUrl) + { + this.baseUrl = baseUrl; + } + + private boolean isRedirect(HttpMethod method) + { + switch (method.getStatusCode()) { + case HttpStatus.SC_MOVED_TEMPORARILY: + case HttpStatus.SC_MOVED_PERMANENTLY: + case HttpStatus.SC_SEE_OTHER: + case HttpStatus.SC_TEMPORARY_REDIRECT: + if (method.getFollowRedirects()) { + return true; + } else { + return false; + } + default: + return false; + } + } + + /** + * Send Request to the repository + */ + protected HttpMethod sendRemoteRequest(Request req) throws AuthenticationException, IOException + { + if (logger.isDebugEnabled()) + { + logger.debug(""); + logger.debug("* Request: " + req.getMethod() + " " + req.getFullUri() + (req.getBody() == null ? "" : "\n" + new String(req.getBody(), "UTF-8"))); + } + + HttpMethod method = createMethod(req); + + // execute method + executeMethod(method); + + // Deal with redirect + if(isRedirect(method)) + { + Header locationHeader = method.getResponseHeader("location"); + if (locationHeader != null) + { + String redirectLocation = locationHeader.getValue(); + method.setURI(new URI(redirectLocation, true)); + httpClient.executeMethod(method); + } + } + + return method; + } + + protected long executeMethod(HttpMethod method) throws HttpException, IOException + { + // execute method + + long startTime = System.currentTimeMillis(); + + // TODO: Pool, and sent host configuration and state on execution + getHttpClient().executeMethod(method); + + return System.currentTimeMillis() - startTime; + } + + protected HttpMethod createMethod(Request req) throws IOException + { + StringBuilder url = new StringBuilder(128); + url.append(baseUrl); + url.append("/service/"); + url.append(req.getFullUri()); + + // construct method + HttpMethod httpMethod = null; + String method = req.getMethod(); + if(method.equalsIgnoreCase("GET")) + { + GetMethod get = new GetMethod(url.toString()); + httpMethod = get; + httpMethod.setFollowRedirects(true); + } + else if(method.equalsIgnoreCase("POST")) + { + PostMethod post = new PostMethod(url.toString()); + httpMethod = post; + ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(req.getBody(), req.getType()); + if (req.getBody().length > DEFAULT_SAVEPOST_BUFFER) + { + post.getParams().setBooleanParameter(HttpMethodParams.USE_EXPECT_CONTINUE, true); + } + post.setRequestEntity(requestEntity); + // Note: not able to automatically follow redirects for POST, this is handled by sendRemoteRequest + } + else if(method.equalsIgnoreCase("HEAD")) + { + HeadMethod head = new HeadMethod(url.toString()); + httpMethod = head; + httpMethod.setFollowRedirects(true); + } + else + { + throw new AlfrescoRuntimeException("Http Method " + method + " not supported"); + } + + if (req.getHeaders() != null) + { + for (Map.Entry header : req.getHeaders().entrySet()) + { + httpMethod.setRequestHeader(header.getKey(), header.getValue()); + } + } + + return httpMethod; + } + + /* (non-Javadoc) + * @see org.alfresco.httpclient.AlfrescoHttpClient#close() + */ + @Override + public void close() + { + if(httpClient != null) + { + HttpConnectionManager connectionManager = httpClient.getHttpConnectionManager(); + if(connectionManager instanceof MultiThreadedHttpConnectionManager) + { + ((MultiThreadedHttpConnectionManager)connectionManager).shutdown(); + } + } + + } + + + +} diff --git a/core/src/main/java/org/alfresco/httpclient/AlfrescoHttpClient.java b/core/src/main/java/org/alfresco/httpclient/AlfrescoHttpClient.java new file mode 100644 index 0000000000..9629cff1ce --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/AlfrescoHttpClient.java @@ -0,0 +1,30 @@ +package org.alfresco.httpclient; + +import java.io.IOException; + +/** + * + * @since 4.0 + * + */ +public interface AlfrescoHttpClient +{ + /** + * Send Request to the repository + */ + public Response sendRequest(Request req) throws AuthenticationException, IOException; + + + /** + * Set the base url to alfresco + * - normally /alfresco + * @param baseUrl + */ + public void setBaseUrl(String baseUrl); + + + /** + * + */ + public void close(); +} diff --git a/core/src/main/java/org/alfresco/httpclient/AuthenticationException.java b/core/src/main/java/org/alfresco/httpclient/AuthenticationException.java new file mode 100644 index 0000000000..5f5b084803 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/AuthenticationException.java @@ -0,0 +1,21 @@ +package org.alfresco.httpclient; + +import org.apache.commons.httpclient.HttpMethod; + +public class AuthenticationException extends Exception +{ + private static final long serialVersionUID = -407003742855571557L; + + private HttpMethod method; + + public AuthenticationException(HttpMethod method) + { + this.method = method; + } + + public HttpMethod getMethod() + { + return method; + } + +} diff --git a/core/src/main/java/org/alfresco/httpclient/GetRequest.java b/core/src/main/java/org/alfresco/httpclient/GetRequest.java new file mode 100644 index 0000000000..1960850972 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/GetRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +/** + * HTTP GET Request + * + * @since 4.0 + */ +public class GetRequest extends Request +{ + public GetRequest(String uri) + { + super("get", uri); + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/HeadRequest.java b/core/src/main/java/org/alfresco/httpclient/HeadRequest.java new file mode 100644 index 0000000000..9b8499ffa2 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/HeadRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +/** + * HTTP HEAD request + * + * @since 4.0 + */ +public class HeadRequest extends Request +{ + public HeadRequest(String uri) + { + super("head", uri); + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java b/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java new file mode 100644 index 0000000000..45a70358e4 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/HttpClientFactory.java @@ -0,0 +1,791 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.AlgorithmParameters; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.alfresco.encryption.AlfrescoKeyStore; +import org.alfresco.encryption.AlfrescoKeyStoreImpl; +import org.alfresco.encryption.EncryptionUtils; +import org.alfresco.encryption.Encryptor; +import org.alfresco.encryption.KeyProvider; +import org.alfresco.encryption.KeyResourceLoader; +import org.alfresco.encryption.KeyStoreParameters; +import org.alfresco.encryption.ssl.AuthSSLProtocolSocketFactory; +import org.alfresco.encryption.ssl.SSLEncryptionParameters; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.Pair; +import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpHost; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.HttpVersion; +import org.apache.commons.httpclient.HttpsURL; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.SimpleHttpConnectionManager; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.httpclient.cookie.CookiePolicy; +import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.params.DefaultHttpParams; +import org.apache.commons.httpclient.params.DefaultHttpParamsFactory; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.commons.httpclient.params.HttpConnectionParams; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.commons.httpclient.params.HttpParams; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.commons.httpclient.util.DateUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A factory to create HttpClients and AlfrescoHttpClients based on the setting of the 'secureCommsType' property. + * + * @since 4.0 + */ +public class HttpClientFactory +{ + public static enum SecureCommsType + { + HTTPS, NONE; + + public static SecureCommsType getType(String type) + { + if(type.equalsIgnoreCase("https")) + { + return HTTPS; + } + else if(type.equalsIgnoreCase("none")) + { + return NONE; + } + else + { + throw new IllegalArgumentException("Invalid communications type"); + } + } + }; + + private static final Log logger = LogFactory.getLog(HttpClientFactory.class); + + private SSLEncryptionParameters sslEncryptionParameters; + private KeyResourceLoader keyResourceLoader; + private SecureCommsType secureCommsType; + + // for md5 http client (no longer used but kept for now) + private KeyStoreParameters keyStoreParameters; + private MD5EncryptionParameters encryptionParameters; + + private String host; + private int port; + private int sslPort; + + private AlfrescoKeyStore sslKeyStore; + private AlfrescoKeyStore sslTrustStore; + private ProtocolSocketFactory sslSocketFactory; + + private int maxTotalConnections = 40; + + private int maxHostConnections = 40; + + private Integer socketTimeout = null; + + private int connectionTimeout = 0; + + public HttpClientFactory() + { + } + + public HttpClientFactory(SecureCommsType secureCommsType, SSLEncryptionParameters sslEncryptionParameters, + KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters, + MD5EncryptionParameters encryptionParameters, String host, int port, int sslPort, int maxTotalConnections, + int maxHostConnections, int socketTimeout) + { + this.secureCommsType = secureCommsType; + this.sslEncryptionParameters = sslEncryptionParameters; + this.keyResourceLoader = keyResourceLoader; + this.keyStoreParameters = keyStoreParameters; + this.encryptionParameters = encryptionParameters; + this.host = host; + this.port = port; + this.sslPort = sslPort; + this.maxTotalConnections = maxTotalConnections; + this.maxHostConnections = maxHostConnections; + this.socketTimeout = socketTimeout; + init(); + } + + public void init() + { + this.sslKeyStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getKeyStoreParameters(), keyResourceLoader); + this.sslTrustStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getTrustStoreParameters(), keyResourceLoader); + this.sslSocketFactory = new AuthSSLProtocolSocketFactory(sslKeyStore, sslTrustStore, keyResourceLoader); + + // Setup the Apache httpclient library to use our concurrent HttpParams factory + DefaultHttpParams.setHttpParamsFactory(new NonBlockingHttpParamsFactory()); + } + + public void setHost(String host) + { + this.host = host; + } + + public String getHost() + { + return host; + } + + public void setPort(int port) + { + this.port = port; + } + + public int getPort() + { + return port; + } + + public void setSslPort(int sslPort) + { + this.sslPort = sslPort; + } + + public boolean isSSL() + { + return secureCommsType == SecureCommsType.HTTPS; + } + + public void setSecureCommsType(String type) + { + try + { + this.secureCommsType = SecureCommsType.getType(type); + } + catch(IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("", e); + } + } + + public void setSSLEncryptionParameters(SSLEncryptionParameters sslEncryptionParameters) + { + this.sslEncryptionParameters = sslEncryptionParameters; + } + + public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters) + { + this.keyStoreParameters = keyStoreParameters; + } + + public void setEncryptionParameters(MD5EncryptionParameters encryptionParameters) + { + this.encryptionParameters = encryptionParameters; + } + + public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader) + { + this.keyResourceLoader = keyResourceLoader; + } + + /** + * @return the maxTotalConnections + */ + public int getMaxTotalConnections() + { + return maxTotalConnections; + } + + /** + * @param maxTotalConnections the maxTotalConnections to set + */ + public void setMaxTotalConnections(int maxTotalConnections) + { + this.maxTotalConnections = maxTotalConnections; + } + + /** + * @return the maxHostConnections + */ + public int getMaxHostConnections() + { + return maxHostConnections; + } + + /** + * @param maxHostConnections the maxHostConnections to set + */ + public void setMaxHostConnections(int maxHostConnections) + { + this.maxHostConnections = maxHostConnections; + } + + /** + * Sets the default socket timeout (SO_TIMEOUT) in milliseconds which is the + * timeout for waiting for data. A timeout value of zero is interpreted as an infinite + * timeout. + * + * @param socketTimeout Timeout in milliseconds + */ + public void setSocketTimeout(Integer socketTimeout) + { + this.socketTimeout = socketTimeout; + } + + /** + * Attempts to connect to a server will timeout after this period (millis). + * Default is zero (the timeout is not used). + * + * @param connectionTimeout time in millis. + */ + public void setConnectionTimeout(int connectionTimeout) + { + this.connectionTimeout = connectionTimeout; + } + + protected HttpClient constructHttpClient() + { + MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); + HttpClient httpClient = new HttpClient(connectionManager); + HttpClientParams params = httpClient.getParams(); + params.setBooleanParameter(HttpConnectionParams.TCP_NODELAY, true); + params.setBooleanParameter(HttpConnectionParams.STALE_CONNECTION_CHECK, true); + if (socketTimeout != null) + { + params.setSoTimeout(socketTimeout); + } + HttpConnectionManagerParams connectionManagerParams = httpClient.getHttpConnectionManager().getParams(); + connectionManagerParams.setMaxTotalConnections(maxTotalConnections); + connectionManagerParams.setDefaultMaxConnectionsPerHost(maxHostConnections); + connectionManagerParams.setConnectionTimeout(connectionTimeout); + + return httpClient; + } + + protected HttpClient getHttpsClient() + { + return getHttpsClient(host, sslPort); + } + + protected HttpClient getHttpsClient(String httpsHost, int httpsPort) + { + // Configure a custom SSL socket factory that will enforce mutual authentication + HttpClient httpClient = constructHttpClient(); + // Default port is 443 for the HostFactory, when including customised port (like 8983) the port name is skipped from "getHostURL" string + HttpHostFactory hostFactory = new HttpHostFactory(new Protocol("https", sslSocketFactory, HttpsURL.DEFAULT_PORT)); + httpClient.setHostConfiguration(new HostConfigurationWithHostFactory(hostFactory)); + httpClient.getHostConfiguration().setHost(httpsHost, httpsPort, "https"); + return httpClient; + } + + protected HttpClient getDefaultHttpClient() + { + return getDefaultHttpClient(host, port); + } + + protected HttpClient getDefaultHttpClient(String httpHost, int httpPort) + { + HttpClient httpClient = constructHttpClient(); + httpClient.getHostConfiguration().setHost(httpHost, httpPort); + return httpClient; + } + + protected AlfrescoHttpClient getAlfrescoHttpsClient() + { + AlfrescoHttpClient repoClient = new HttpsClient(getHttpsClient()); + return repoClient; + } + + protected AlfrescoHttpClient getAlfrescoHttpClient() + { + AlfrescoHttpClient repoClient = new DefaultHttpClient(getDefaultHttpClient()); + return repoClient; + } + + protected HttpClient getMD5HttpClient(String host, int port) + { + HttpClient httpClient = constructHttpClient(); + httpClient.getHostConfiguration().setHost(host, port); + return httpClient; + } + + + public AlfrescoHttpClient getRepoClient(String host, int port) + { + AlfrescoHttpClient repoClient = null; + + if(secureCommsType == SecureCommsType.HTTPS) + { + repoClient = getAlfrescoHttpsClient(); + } + else if(secureCommsType == SecureCommsType.NONE) + { + repoClient = getAlfrescoHttpClient(); + } + else + { + throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); + } + + return repoClient; + } + + public HttpClient getHttpClient() + { + HttpClient httpClient = null; + + if(secureCommsType == SecureCommsType.HTTPS) + { + httpClient = getHttpsClient(); + } + else if(secureCommsType == SecureCommsType.NONE) + { + httpClient = getDefaultHttpClient(); + } + else + { + throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); + } + + return httpClient; + } + + public HttpClient getHttpClient(String host, int port) + { + HttpClient httpClient = null; + + if(secureCommsType == SecureCommsType.HTTPS) + { + httpClient = getHttpsClient(host, port); + } + else if(secureCommsType == SecureCommsType.NONE) + { + httpClient = getDefaultHttpClient(host, port); + } + else + { + throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'"); + } + + return httpClient; + } + + + + /** + * A secure client connection to the repository. + * + * @since 4.0 + * + */ + class HttpsClient extends AbstractHttpClient + { + public HttpsClient(HttpClient httpClient) + { + super(httpClient); + } + + /** + * Send Request to the repository + */ + public Response sendRequest(Request req) throws AuthenticationException, IOException + { + HttpMethod method = super.sendRemoteRequest(req); + return new HttpMethodResponse(method); + } + } + + /** + * Simple HTTP client to connect to the Alfresco server. Simply wraps a HttpClient. + * + * @since 4.0 + */ + class DefaultHttpClient extends AbstractHttpClient + { + public DefaultHttpClient(HttpClient httpClient) + { + super(httpClient); + } + + /** + * Send Request to the repository + */ + public Response sendRequest(Request req) throws AuthenticationException, IOException + { + HttpMethod method = super.sendRemoteRequest(req); + return new HttpMethodResponse(method); + } + } + + + + static class SecureHttpMethodResponse extends HttpMethodResponse + { + protected HostConfiguration hostConfig; + protected EncryptionUtils encryptionUtils; + // Need to get as a byte array because we need to read the request twice, once for authentication + // and again by the web service. + protected byte[] decryptedBody; + + public SecureHttpMethodResponse(HttpMethod method, HostConfiguration hostConfig, + EncryptionUtils encryptionUtils) throws AuthenticationException, IOException + { + super(method); + this.hostConfig = hostConfig; + this.encryptionUtils = encryptionUtils; + + if(method.getStatusCode() == HttpStatus.SC_OK) + { + this.decryptedBody = encryptionUtils.decryptResponseBody(method); + // authenticate the response + if(!authenticate()) + { + throw new AuthenticationException(method); + } + } + } + + protected boolean authenticate() throws IOException + { + return encryptionUtils.authenticateResponse(method, hostConfig.getHost(), decryptedBody); + } + + public InputStream getContentAsStream() throws IOException + { + if(decryptedBody != null) + { + return new ByteArrayInputStream(decryptedBody); + } + else + { + return null; + } + } + } + + private static class HttpHostFactory + { + private Map protocols; + + public HttpHostFactory(Protocol httpsProtocol) + { + protocols = new HashMap(2); + protocols.put("https", httpsProtocol); + } + + /** Get a host for the given parameters. This method need not be thread-safe. */ + public HttpHost getHost(String host, int port, String scheme) + { + if(scheme == null) + { + scheme = "http"; + } + Protocol protocol = protocols.get(scheme); + if(protocol == null) + { + protocol = Protocol.getProtocol("http"); + if(protocol == null) + { + throw new IllegalArgumentException("Unrecognised scheme parameter"); + } + } + + return new HttpHost(host, port, protocol); + } + } + + private static class HostConfigurationWithHostFactory extends HostConfiguration + { + private final HttpHostFactory factory; + + public HostConfigurationWithHostFactory(HttpHostFactory factory) + { + this.factory = factory; + } + + public synchronized void setHost(String host, int port, String scheme) + { + setHost(factory.getHost(host, port, scheme)); + } + + public synchronized void setHost(String host, int port) + { + setHost(factory.getHost(host, port, "http")); + } + + @SuppressWarnings("unused") + public synchronized void setHost(URI uri) + { + try { + setHost(uri.getHost(), uri.getPort(), uri.getScheme()); + } catch(URIException e) { + throw new IllegalArgumentException(e.toString()); + } + } + } + + /** + * An extension of the DefaultHttpParamsFactory that uses a RRW lock pattern rather than + * full synchronization around the parameter CRUD - to avoid locking on many reads. + * + * @author Kevin Roast + */ + public static class NonBlockingHttpParamsFactory extends DefaultHttpParamsFactory + { + private volatile HttpParams httpParams; + + /* (non-Javadoc) + * @see org.apache.commons.httpclient.params.DefaultHttpParamsFactory#getDefaultParams() + */ + @Override + public HttpParams getDefaultParams() + { + if (httpParams == null) + { + synchronized (this) + { + if (httpParams == null) + { + httpParams = createParams(); + } + } + } + + return httpParams; + } + + /** + * NOTE: This is a copy of the code in {@link DefaultHttpParamsFactory} + * Unfortunately this is required because although the factory pattern allows the + * override of the default param creation, it does not allow the class of the actual + * HttpParam implementation to be changed. + */ + @Override + protected HttpParams createParams() + { + HttpClientParams params = new NonBlockingHttpParams(null); + + params.setParameter(HttpMethodParams.USER_AGENT, "Spring Surf via Apache HttpClient/3.1"); + params.setVersion(HttpVersion.HTTP_1_1); + params.setConnectionManagerClass(SimpleHttpConnectionManager.class); + params.setCookiePolicy(CookiePolicy.IGNORE_COOKIES); + params.setHttpElementCharset("US-ASCII"); + params.setContentCharset("ISO-8859-1"); + params.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler()); + + List datePatterns = Arrays.asList( + new String[] { + DateUtil.PATTERN_RFC1123, + DateUtil.PATTERN_RFC1036, + DateUtil.PATTERN_ASCTIME, + "EEE, dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MMM-yyyy HH-mm-ss z", + "EEE, dd MMM yy HH:mm:ss z", + "EEE dd-MMM-yyyy HH:mm:ss z", + "EEE dd MMM yyyy HH:mm:ss z", + "EEE dd-MMM-yyyy HH-mm-ss z", + "EEE dd-MMM-yy HH:mm:ss z", + "EEE dd MMM yy HH:mm:ss z", + "EEE,dd-MMM-yy HH:mm:ss z", + "EEE,dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MM-yyyy HH:mm:ss z", + } + ); + params.setParameter(HttpMethodParams.DATE_PATTERNS, datePatterns); + + String agent = null; + try + { + agent = System.getProperty("httpclient.useragent"); + } + catch (SecurityException ignore) + { + } + if (agent != null) + { + params.setParameter(HttpMethodParams.USER_AGENT, agent); + } + + String preemptiveDefault = null; + try + { + preemptiveDefault = System.getProperty("httpclient.authentication.preemptive"); + } + catch (SecurityException ignore) + { + } + if (preemptiveDefault != null) + { + preemptiveDefault = preemptiveDefault.trim().toLowerCase(); + if (preemptiveDefault.equals("true")) + { + params.setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, Boolean.TRUE); + } + else if (preemptiveDefault.equals("false")) + { + params.setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, Boolean.FALSE); + } + } + + String defaultCookiePolicy = null; + try + { + defaultCookiePolicy = System.getProperty("apache.commons.httpclient.cookiespec"); + } + catch (SecurityException ignore) + { + } + if (defaultCookiePolicy != null) + { + if ("COMPATIBILITY".equalsIgnoreCase(defaultCookiePolicy)) + { + params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); + } + else if ("NETSCAPE_DRAFT".equalsIgnoreCase(defaultCookiePolicy)) + { + params.setCookiePolicy(CookiePolicy.NETSCAPE); + } + else if ("RFC2109".equalsIgnoreCase(defaultCookiePolicy)) + { + params.setCookiePolicy(CookiePolicy.RFC_2109); + } + } + + return params; + } + } + + /** + * @author Kevin Roast + */ + public static class NonBlockingHttpParams extends HttpClientParams + { + private HashMap parameters = new HashMap(8); + private ReadWriteLock paramLock = new ReentrantReadWriteLock(); + + public NonBlockingHttpParams() + { + super(); + } + + public NonBlockingHttpParams(HttpParams defaults) + { + super(defaults); + } + + @Override + public Object getParameter(final String name) + { + // See if the parameter has been explicitly defined + Object param = null; + paramLock.readLock().lock(); + try + { + param = this.parameters.get(name); + } + finally + { + paramLock.readLock().unlock(); + } + if (param == null) + { + // If not, see if defaults are available + HttpParams defaults = getDefaults(); + if (defaults != null) + { + // Return default parameter value + param = defaults.getParameter(name); + } + } + return param; + } + + @Override + public void setParameter(final String name, final Object value) + { + paramLock.writeLock().lock(); + try + { + this.parameters.put(name, value); + } + finally + { + paramLock.writeLock().unlock(); + } + } + + @Override + public boolean isParameterSetLocally(final String name) + { + paramLock.readLock().lock(); + try + { + return (this.parameters.get(name) != null); + } + finally + { + paramLock.readLock().unlock(); + } + } + + @Override + public void clear() + { + paramLock.writeLock().lock(); + try + { + this.parameters.clear(); + } + finally + { + paramLock.writeLock().unlock(); + } + } + + @Override + public Object clone() throws CloneNotSupportedException + { + NonBlockingHttpParams clone = (NonBlockingHttpParams)super.clone(); + paramLock.readLock().lock(); + try + { + clone.parameters = (HashMap) this.parameters.clone(); + } + finally + { + paramLock.readLock().unlock(); + } + clone.setDefaults(getDefaults()); + return clone; + } + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/HttpMethodResponse.java b/core/src/main/java/org/alfresco/httpclient/HttpMethodResponse.java new file mode 100644 index 0000000000..4bf7a9bbbe --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/HttpMethodResponse.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpMethod; + +/** + * + * @since 4.0 + * + */ +public class HttpMethodResponse implements Response +{ + protected HttpMethod method; + + public HttpMethodResponse(HttpMethod method) throws IOException + { + this.method = method; + } + + public void release() + { + method.releaseConnection(); + } + + public InputStream getContentAsStream() throws IOException + { + return method.getResponseBodyAsStream(); + } + + public String getContentType() + { + return getHeader("Content-Type"); + } + + public String getHeader(String name) + { + Header header = method.getResponseHeader(name); + return (header != null) ? header.getValue() : null; + } + + public int getStatus() + { + return method.getStatusCode(); + } + +} diff --git a/core/src/main/java/org/alfresco/httpclient/MD5EncryptionParameters.java b/core/src/main/java/org/alfresco/httpclient/MD5EncryptionParameters.java new file mode 100644 index 0000000000..158b060ad7 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/MD5EncryptionParameters.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +/** + * + * @since 4.0 + * + */ +public class MD5EncryptionParameters +{ + private String cipherAlgorithm; + private long messageTimeout; + private String macAlgorithm; + + public MD5EncryptionParameters() + { + + } + + public MD5EncryptionParameters(String cipherAlgorithm, + Long messageTimeout, String macAlgorithm) + { + this.cipherAlgorithm = cipherAlgorithm; + this.messageTimeout = messageTimeout; + this.macAlgorithm = macAlgorithm; + } + + public String getCipherAlgorithm() + { + return cipherAlgorithm; + } + + public void setCipherAlgorithm(String cipherAlgorithm) + { + this.cipherAlgorithm = cipherAlgorithm; + } + + public long getMessageTimeout() + { + return messageTimeout; + } + + public String getMacAlgorithm() + { + return macAlgorithm; + } + + public void setMessageTimeout(long messageTimeout) + { + this.messageTimeout = messageTimeout; + } + + public void setMacAlgorithm(String macAlgorithm) + { + this.macAlgorithm = macAlgorithm; + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/PostRequest.java b/core/src/main/java/org/alfresco/httpclient/PostRequest.java new file mode 100644 index 0000000000..08133a12ad --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/PostRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +import java.io.UnsupportedEncodingException; + +/** + * HTTP POST Request + * + * @since 4.0 + */ +public class PostRequest extends Request +{ + public PostRequest(String uri, String post, String contentType) + throws UnsupportedEncodingException + { + super("post", uri); + setBody(getEncoding() == null ? post.getBytes() : post.getBytes(getEncoding())); + setType(contentType); + } + + public PostRequest(String uri, byte[] post, String contentType) + { + super("post", uri); + setBody(post); + setType(contentType); + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/Request.java b/core/src/main/java/org/alfresco/httpclient/Request.java new file mode 100644 index 0000000000..a6328535c5 --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/Request.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +import java.util.Map; + +/** + * + * @since 4.0 + * + */ +public class Request +{ + private String method; + private String uri; + private Map args; + private Map headers; + private byte[] body; + private String encoding = "UTF-8"; + private String contentType; + + public Request(Request req) + { + this.method = req.method; + this.uri= req.uri; + this.args = req.args; + this.headers = req.headers; + this.body = req.body; + this.encoding = req.encoding; + this.contentType = req.contentType; + } + + public Request(String method, String uri) + { + this.method = method; + this.uri = uri; + } + + public String getMethod() + { + return method; + } + + public String getUri() + { + return uri; + } + + public String getFullUri() + { + // calculate full uri + String fullUri = uri == null ? "" : uri; + if (args != null && args.size() > 0) + { + char prefix = (uri.indexOf('?') == -1) ? '?' : '&'; + for (Map.Entry arg : args.entrySet()) + { + fullUri += prefix + arg.getKey() + "=" + (arg.getValue() == null ? "" : arg.getValue()); + prefix = '&'; + } + } + + return fullUri; + } + + public Request setArgs(Map args) + { + this.args = args; + return this; + } + + public Map getArgs() + { + return args; + } + + public Request setHeaders(Map headers) + { + this.headers = headers; + return this; + } + + public Map getHeaders() + { + return headers; + } + + public Request setBody(byte[] body) + { + this.body = body; + return this; + } + + public byte[] getBody() + { + return body; + } + + public Request setEncoding(String encoding) + { + this.encoding = encoding; + return this; + } + + public String getEncoding() + { + return encoding; + } + + public Request setType(String contentType) + { + this.contentType = contentType; + return this; + } + + public String getType() + { + return contentType; + } +} diff --git a/core/src/main/java/org/alfresco/httpclient/Response.java b/core/src/main/java/org/alfresco/httpclient/Response.java new file mode 100644 index 0000000000..59dbae23bf --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/Response.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.httpclient; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @since 4.0 + * + */ +public interface Response +{ + public InputStream getContentAsStream() throws IOException; + + public String getHeader(String name); + + public String getContentType(); + + public int getStatus(); + +// public Long getRequestDuration(); + + public void release(); +} diff --git a/core/src/main/java/org/alfresco/httpclient/SecureHttpClient.java b/core/src/main/java/org/alfresco/httpclient/SecureHttpClient.java new file mode 100644 index 0000000000..93c93bcd6b --- /dev/null +++ b/core/src/main/java/org/alfresco/httpclient/SecureHttpClient.java @@ -0,0 +1,149 @@ +package org.alfresco.httpclient; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.AlgorithmParameters; + +import org.alfresco.encryption.EncryptionUtils; +import org.alfresco.encryption.Encryptor; +import org.alfresco.encryption.KeyProvider; +import org.alfresco.encryption.KeyResourceLoader; +import org.alfresco.util.Pair; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Simple HTTP client to connect to the Alfresco server. + * + * @since 4.0 + */ +public class SecureHttpClient //extends AbstractHttpClient +{ +// private static final Log logger = LogFactory.getLog(SecureHttpClient.class); +// +// private Encryptor encryptor; +// private EncryptionUtils encryptionUtils; +// private EncryptionService encryptionService; +// private EncryptionParameters encryptionParameters; +// +// /** +// * For testing purposes. +// * +// * @param solrResourceLoader +// * @param alfrescoHost +// * @param alfrescoPort +// * @param encryptionParameters +// */ +// public SecureHttpClient(HttpClientFactory httpClientFactory, String host, int port, EncryptionService encryptionService) +// { +// super(httpClientFactory, host, port); +// this.encryptionUtils = encryptionService.getEncryptionUtils(); +// this.encryptor = encryptionService.getEncryptor(); +// this.encryptionService = encryptionService; +// this.encryptionParameters = encryptionService.getEncryptionParameters(); +// } +// +// public SecureHttpClient(HttpClientFactory httpClientFactory, KeyResourceLoader keyResourceLoader, String host, int port, +// EncryptionParameters encryptionParameters) +// { +// super(httpClientFactory, host, port); +// this.encryptionParameters = encryptionParameters; +// this.encryptionService = new EncryptionService(alfrescoHost, alfrescoPort, keyResourceLoader, encryptionParameters); +// this.encryptionUtils = encryptionService.getEncryptionUtils(); +// this.encryptor = encryptionService.getEncryptor(); +// } +// +// protected HttpMethod createMethod(Request req) throws IOException +// { +// byte[] message = null; +// HttpMethod method = super.createMethod(req); +// +// if(req.getMethod().equalsIgnoreCase("POST")) +// { +// message = req.getBody(); +// // encrypt body +// Pair encrypted = encryptor.encrypt(KeyProvider.ALIAS_SOLR, null, message); +// encryptionUtils.setRequestAlgorithmParameters(method, encrypted.getSecond()); +// +// ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(encrypted.getFirst(), "application/octet-stream"); +// ((PostMethod)method).setRequestEntity(requestEntity); +// } +// +// encryptionUtils.setRequestAuthentication(method, message); +// +// return method; +// } +// +// protected HttpMethod sendRemoteRequest(Request req) throws AuthenticationException, IOException +// { +// HttpMethod method = super.sendRemoteRequest(req); +// +// // check that the request returned with an ok status +// if(method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) +// { +// throw new AuthenticationException(method); +// } +// +// return method; +// } +// +// /** +// * Send Request to the repository +// */ +// public Response sendRequest(Request req) throws AuthenticationException, IOException +// { +// HttpMethod method = super.sendRemoteRequest(req); +// return new SecureHttpMethodResponse(method, httpClient.getHostConfiguration(), encryptionUtils); +// } +// +// public static class SecureHttpMethodResponse extends HttpMethodResponse +// { +// protected HostConfiguration hostConfig; +// protected EncryptionUtils encryptionUtils; +// // Need to get as a byte array because we need to read the request twice, once for authentication +// // and again by the web service. +// protected byte[] decryptedBody; +// +// public SecureHttpMethodResponse(HttpMethod method, HostConfiguration hostConfig, +// EncryptionUtils encryptionUtils) throws AuthenticationException, IOException +// { +// super(method); +// this.hostConfig = hostConfig; +// this.encryptionUtils = encryptionUtils; +// +// if(method.getStatusCode() == HttpStatus.SC_OK) +// { +// this.decryptedBody = encryptionUtils.decryptResponseBody(method); +// // authenticate the response +// if(!authenticate()) +// { +// throw new AuthenticationException(method); +// } +// } +// } +// +// protected boolean authenticate() throws IOException +// { +// return encryptionUtils.authenticateResponse(method, hostConfig.getHost(), decryptedBody); +// } +// +// public InputStream getContentAsStream() throws IOException +// { +// if(decryptedBody != null) +// { +// return new ByteArrayInputStream(decryptedBody); +// } +// else +// { +// return null; +// } +// } +// } + +} diff --git a/core/src/main/java/org/alfresco/i18n/ResourceBundleBootstrapComponent.java b/core/src/main/java/org/alfresco/i18n/ResourceBundleBootstrapComponent.java new file mode 100644 index 0000000000..0939129cf3 --- /dev/null +++ b/core/src/main/java/org/alfresco/i18n/ResourceBundleBootstrapComponent.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.i18n; + +import java.util.List; + +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Resource bundle bootstrap component. + *

+ * Provides a convenient way to make resource bundles available via Spring config. + * + * @author Roy Wetherall + */ +public class ResourceBundleBootstrapComponent +{ + /** + * Set the resource bundles to be registered. This should be a list of resource + * bundle base names whose content will be made available across the repository. + * + * @param resourceBundles the resource bundles + */ + public void setResourceBundles(List resourceBundles) + { + for (String resourceBundle : resourceBundles) + { + I18NUtil.registerResourceBundle(resourceBundle); + } + } +} diff --git a/core/src/main/java/org/alfresco/processor/Processor.java b/core/src/main/java/org/alfresco/processor/Processor.java new file mode 100644 index 0000000000..52c7e61c1a --- /dev/null +++ b/core/src/main/java/org/alfresco/processor/Processor.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.processor; + +/** + * Interface for Proccessor classes - such as Template or Scripting Processors. + * + * @author Roy Wetherall + */ +public interface Processor +{ + /** + * Get the name of the processor + * + * @return the name of the processor + */ + public String getName(); + + /** + * The file extension that the processor is associated with, null if none. + * + * @return the extension + */ + public String getExtension(); + + /** + * Registers a processor extension with the processor + * + * @param processorExtension the process extension + */ + public void registerProcessorExtension(ProcessorExtension processorExtension); +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/processor/ProcessorExtension.java b/core/src/main/java/org/alfresco/processor/ProcessorExtension.java new file mode 100644 index 0000000000..c9dbb97e07 --- /dev/null +++ b/core/src/main/java/org/alfresco/processor/ProcessorExtension.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.processor; + +/** + * Interface to represent a server side script implementation + * + * @author Roy Wetherall + */ +public interface ProcessorExtension +{ + /** + * Returns the name of the extension + * + * @return the name of the extension + */ + String getExtensionName(); +} diff --git a/core/src/main/java/org/alfresco/query/AbstractCachingCannedQueryFactory.java b/core/src/main/java/org/alfresco/query/AbstractCachingCannedQueryFactory.java new file mode 100644 index 0000000000..832ee3a350 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/AbstractCachingCannedQueryFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.List; + +/** + * Caching support extension for {@link CannedQueryFactory} implementations. + *

+ * Depending on the parameters provided, this class may choose to pick up existing results + * and re-use them for later page requests; the client will not have knowledge of the + * shortcuts. + * + * TODO: This is work-in-progress + * + * @author Derek Hulley + * @since 4.0 + */ +public abstract class AbstractCachingCannedQueryFactory extends AbstractCannedQueryFactory +{ + /** + * Base implementation that provides a caching facade around the query. + * + * @return a decoraded facade query that will cache query results for later paging requests + */ + @Override + public final CannedQuery getCannedQuery(CannedQueryParameters parameters) + { + throw new UnsupportedOperationException(); + } + + /** + * Derived classes must implement this method to provide the raw query that supports the given + * parameters. All requests must be serviced without any further caching in order to prevent + * duplicate caching. + * + * @param parameters the query parameters as given by the client + * @return the query that will generate the results + */ + protected abstract CannedQuery getCannedQueryImpl(CannedQueryParameters parameters); + + private class CannedQueryCacheFacade extends AbstractCannedQuery + { + private final AbstractCannedQuery delegate; + + private CannedQueryCacheFacade(CannedQueryParameters params, AbstractCannedQuery delegate) + { + super(params); + this.delegate = delegate; + } + + @Override + protected List queryAndFilter(CannedQueryParameters parameters) + { + // Copy the parameters and remove all references to paging. + // The underlying query will return full or filtered results (possibly also sorted) + // but will not apply page limitations + + throw new UnsupportedOperationException(); + } + } +} diff --git a/core/src/main/java/org/alfresco/query/AbstractCannedQuery.java b/core/src/main/java/org/alfresco/query/AbstractCannedQuery.java new file mode 100644 index 0000000000..a8effe90fc --- /dev/null +++ b/core/src/main/java/org/alfresco/query/AbstractCannedQuery.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.GUID; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; + +/** + * Basic support for canned query implementations. + * + * @author Derek Hulley + * @since 4.0 + */ +public abstract class AbstractCannedQuery implements CannedQuery +{ + private final CannedQueryParameters parameters; + private final String queryExecutionId; + private CannedQueryResults results; + + /** + * Construct the canned query given the original parameters applied. + *

+ * A random GUID query execution ID will be generated. + * + * @param parameters the original query parameters + */ + protected AbstractCannedQuery(CannedQueryParameters parameters) + { + ParameterCheck.mandatory("parameters", parameters); + this.parameters = parameters; + this.queryExecutionId = GUID.generate(); + } + + @Override + public CannedQueryParameters getParameters() + { + return parameters; + } + + @Override + public String toString() + { + return "AbstractCannedQuery [parameters=" + parameters + ", class=" + this.getClass() + "]"; + } + + @Override + public synchronized final CannedQueryResults execute() + { + // Check that we are not requerying + if (results != null) + { + throw new IllegalStateException( + "This query instance has already by used." + + " It can only be used to query once."); + } + + // Get the raw query results + List rawResults = queryAndFilter(parameters); + if (rawResults == null) + { + throw new AlfrescoRuntimeException("Execution returned 'null' results"); + } + + // Apply sorting + if (isApplyPostQuerySorting()) + { + rawResults = applyPostQuerySorting(rawResults, parameters.getSortDetails()); + } + + // Apply permissions + if (isApplyPostQueryPermissions()) + { + // Work out the number of results required + int requestedCount = parameters.getResultsRequired(); + rawResults = applyPostQueryPermissions(rawResults, requestedCount); + } + + // Get total count + final Pair totalCount = getTotalResultCount(rawResults); + + // Apply paging + CannedQueryPageDetails pagingDetails = parameters.getPageDetails(); + List> pages = Collections.singletonList(rawResults); + if (isApplyPostQueryPaging()) + { + pages = applyPostQueryPaging(rawResults, pagingDetails); + } + + // Construct results object + final List> finalPages = pages; + + // Has more items beyond requested pages ? ... ie. at least one more page (with at least one result) + final boolean hasMoreItems = (rawResults.size() > pagingDetails.getResultsRequiredForPaging()); + + results = new CannedQueryResults() + { + @Override + public CannedQuery getOriginatingQuery() + { + return AbstractCannedQuery.this; + } + + @Override + public String getQueryExecutionId() + { + return queryExecutionId; + } + + @Override + public Pair getTotalResultCount() + { + if (parameters.getTotalResultCountMax() > 0) + { + return totalCount; + } + else + { + throw new IllegalStateException("Total results were not requested in parameters."); + } + } + + @Override + public int getPagedResultCount() + { + int finalPagedCount = 0; + for (List page : finalPages) + { + finalPagedCount += page.size(); + } + return finalPagedCount; + } + + @Override + public int getPageCount() + { + return finalPages.size(); + } + + @Override + public R getSingleResult() + { + if (finalPages.size() != 1 && finalPages.get(0).size() != 1) + { + throw new IllegalStateException("There must be exactly one page of one result available."); + } + return finalPages.get(0).get(0); + } + + @Override + public List getPage() + { + if (finalPages.size() != 1) + { + throw new IllegalStateException("There must be exactly one page of results available."); + } + return finalPages.get(0); + } + + @Override + public List> getPages() + { + return finalPages; + } + + @Override + public boolean hasMoreItems() + { + return hasMoreItems; + } + }; + return results; + } + + /** + * Implement the basic query, returning either filtered or all results. + *

+ * The implementation may optimally select, filter, sort and apply permissions. + * If not, however, the subsequent post-query methods + * ({@link #applyPostQuerySorting(List, CannedQuerySortDetails)}, + * {@link #applyPostQueryPermissions(List, int)} and + * {@link #applyPostQueryPaging(List, CannedQueryPageDetails)}) can + * be used to trim the results as required. + * + * @param parameters the full parameters to be used for execution + */ + protected abstract List queryAndFilter(CannedQueryParameters parameters); + + /** + * Override to get post-query calls to do sorting. + * + * @return true to get a post-query call to sort (default false) + */ + protected boolean isApplyPostQuerySorting() + { + return false; + } + + /** + * Called before {@link #applyPostQueryPermissions(List, int)} to allow the results to be sorted prior to permission checks. + * Note that the query implementation may optimally sort results during retrieval, in which case this method does not need to be implemented. + * + * @param results the results to sort + * @param sortDetails details of the sorting requirements + * @return the results according to the new sort order + */ + protected List applyPostQuerySorting(List results, CannedQuerySortDetails sortDetails) + { + throw new UnsupportedOperationException("Override this method if post-query sorting is required."); + } + + /** + * Override to get post-query calls to apply permission filters. + * + * @return true to get a post-query call to apply permissions (default false) + */ + protected boolean isApplyPostQueryPermissions() + { + return false; + } + + /** + * Called after the query to filter out results based on permissions. + * Note that the query implementation may optimally only select results + * based on available privileges, in which case this method does not need to be implemented. + *

+ * Permission evaluations should continue until the requested number of results are retrieved + * or all available results have been examined. + * + * @param results the results to apply permissions to + * @param requestedCount the minimum number of results to pass the permission checks + * in order to fully satisfy the paging requirements + * @return the remaining results (as a single "page") after permissions have been applied + */ + protected List applyPostQueryPermissions(List results, int requestedCount) + { + throw new UnsupportedOperationException("Override this method if post-query filtering is required."); + } + + /** + * Get the total number of available results after querying, filtering, sorting and permission checking. + *

+ * The default implementation assumes that the given results are the final total possible. + * + * @param results the results after filtering and sorting, but before paging + * @return pair representing (a) the total number of results and + * (b) the estimated (or actual) number of maximum results + * possible for this query. + * + * @see CannedQueryParameters#getTotalResultCountMax() + */ + protected Pair getTotalResultCount(List results) + { + Integer size = results.size(); + return new Pair(size, size); + } + + /** + * Override to get post-query calls to do pull out paged results. + * + * @return true to get a post-query call to page (default true) + */ + protected boolean isApplyPostQueryPaging() + { + return true; + } + + /** + * Called after the {@link #applyPostQuerySorting(List, CannedQuerySortDetails) sorting phase} to pull out results specific + * to the required pages. Note that the query implementation may optimally + * create page-specific results, in which case this method does not need to be implemented. + *

+ * The base implementation assumes that results are not paged and that the current results + * are all the available results i.e. that paging still needs to be applied. + * + * @param results full results (all or excess pages) + * @param pageDetails details of the paging requirements + * @return the specific page of results as per the query parameters + */ + protected List> applyPostQueryPaging(List results, CannedQueryPageDetails pageDetails) + { + int skipResults = pageDetails.getSkipResults(); + int pageSize = pageDetails.getPageSize(); + int pageCount = pageDetails.getPageCount(); + int pageNumber = pageDetails.getPageNumber(); + + int availableResults = results.size(); + int totalResults = pageSize * pageCount; + int firstResult = skipResults + ((pageNumber-1) * pageSize); // first of window + + List> pages = new ArrayList>(pageCount); + + // First some shortcuts + if (skipResults == 0 && pageSize > availableResults) + { + return Collections.singletonList(results); // Requesting more results in one page than are available + } + else if (firstResult > availableResults) + { + return pages; // Start of first page is after all results + } + + // Build results + Iterator iterator = results.listIterator(firstResult); + int countTotal = 0; + List page = new ArrayList(Math.min(results.size(), pageSize)); // Prevent memory blow-out + pages.add(page); + while (iterator.hasNext() && countTotal < totalResults) + { + if (page.size() == pageSize) + { + // Create a page and add it to the results + page = new ArrayList(pageSize); + pages.add(page); + } + R next = iterator.next(); + page.add(next); + + countTotal++; + } + + // Done + return pages; + } +} diff --git a/core/src/main/java/org/alfresco/query/AbstractCannedQueryFactory.java b/core/src/main/java/org/alfresco/query/AbstractCannedQueryFactory.java new file mode 100644 index 0000000000..86efc92e93 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/AbstractCannedQueryFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.registry.NamedObjectRegistry; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; + +/** + * Basic services for {@link CannedQueryFactory} implementations. + * + * @author Derek Hulley + * @since 4.0 + */ +public abstract class AbstractCannedQueryFactory implements CannedQueryFactory, InitializingBean, BeanNameAware +{ + private String name; + @SuppressWarnings("rawtypes") + private NamedObjectRegistry registry; + + /** + * Set the name with which to {@link #setRegistry(NamedObjectRegistry) register} + * @param name the name of the bean + */ + public void setBeanName(String name) + { + this.name = name; + } + + /** + * Set the registry with which to register + */ + @SuppressWarnings("rawtypes") + public void setRegistry(NamedObjectRegistry registry) + { + this.registry = registry; + } + + /** + * Registers the instance + */ + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "name", name); + PropertyCheck.mandatory(this, "registry", registry); + + registry.register(name, this); + } + + /** + * Helper method to construct a unique query execution ID based on the + * instance of the factory and the parameters provided. + * + * @param parameters the query parameters + * @return a unique query instance ID + */ + protected String getQueryExecutionId(CannedQueryParameters parameters) + { + // Create a GUID + String uuid = name + "-" + GUID.generate(); + return uuid; + } + + /** + * {@inheritDoc} + */ + @Override + public CannedQuery getCannedQuery(Object parameterBean, int skipResults, int pageSize, String queryExecutionId) + { + return getCannedQuery(new CannedQueryParameters(parameterBean, skipResults, pageSize, queryExecutionId)); + } +} diff --git a/core/src/main/java/org/alfresco/query/CannedQuery.java b/core/src/main/java/org/alfresco/query/CannedQuery.java new file mode 100644 index 0000000000..f6b7b55cd5 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +/** + * Interface for named query implementations. These are queries that encapsulate varying + * degrees of functionality, but ultimately provide support for paging results. + *

+ * Note that each instance of the query is stateful and cannot be reused. + * + * @param the query result type + * + * @author Derek Hulley + * @since 4.0 + */ +public interface CannedQuery +{ + /** + * Get the original parameters used to generate the query. + * + * @return the parameters used to obtain the named query. + */ + CannedQueryParameters getParameters(); + + /** + * Execute the named query, which was provided to support the + * {@link #getParameters() parameters} originally provided. + *

+ * Note: This method can only be used once; to requery, get a new + * instance from the {@link CannedQueryFactory factory}. + * + * @return the query results + * + * @throws IllegalStateException on second and subsequent calls to this method + */ + CannedQueryResults execute(); +} diff --git a/core/src/main/java/org/alfresco/query/CannedQueryException.java b/core/src/main/java/org/alfresco/query/CannedQueryException.java new file mode 100644 index 0000000000..1950c169fd --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQueryException.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Exception generated by failures to execute canned queries. + * + * @author Derek Hulley + * @since 4.0 + */ +public class CannedQueryException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = -4985399145374964458L; + + /** + * @param msg the message + */ + public CannedQueryException(String msg) + { + super(msg); + } + + /** + * @param msg the message + * @param cause the exception cause + */ + public CannedQueryException(String msg, Throwable cause) + { + super(msg, cause); + } + + /** + * @param msgId the message id + * @param msgParams the message parameters + */ + public CannedQueryException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + /** + * @param msgId the message id + * @param msgParams the message parameters + * @param cause the exception cause + */ + public CannedQueryException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } +} diff --git a/core/src/main/java/org/alfresco/query/CannedQueryFactory.java b/core/src/main/java/org/alfresco/query/CannedQueryFactory.java new file mode 100644 index 0000000000..56e7716026 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQueryFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +/** + * Interface for factory implementations for producing instances of {@link CannedQuery} + * based on all the query parameters. + * + * @param the query result type + * + * @author Derek Hulley, janv + * @since 4.0 + */ +public interface CannedQueryFactory +{ + /** + * Retrieve an instance of a {@link CannedQuery} based on the full range of + * available parameters. + * + * @param parameters the full query parameters + * @return an implementation that will execute the query + */ + CannedQuery getCannedQuery(CannedQueryParameters parameters); + + /** + * Retrieve an instance of a {@link CannedQuery} based on limited parameters. + * + * @param parameterBean the values that the query will be based on or null + * if not relevant to the query + * @param skipResults results to skip before page + * @param pageSize the size of page - ie. max items (if skipResults = 0) + * @param queryExecutionId ID of a previously-executed query to be used during follow-up + * page requests - null if not available + * @return an implementation that will execute the query + */ + CannedQuery getCannedQuery(Object parameterBean, int skipResults, int pageSize, String queryExecutionId); +} diff --git a/core/src/main/java/org/alfresco/query/CannedQueryPageDetails.java b/core/src/main/java/org/alfresco/query/CannedQueryPageDetails.java new file mode 100644 index 0000000000..56520b3a5e --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQueryPageDetails.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +/** + * Details for canned queries supporting paged results. + *

+ * Results are {@link #skipResults skipped}, chopped into pages of + * {@link #pageSize appropriate size} before the {@link #pageCount start page} + * and {@link #pageNumber number} are returned. + * + * @author Derek Hulley + * @since 4.0 + */ +public class CannedQueryPageDetails +{ + public static final int DEFAULT_SKIP_RESULTS = 0; + public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE; + public static final int DEFAULT_PAGE_NUMBER = 1; + public static final int DEFAULT_PAGE_COUNT = 1; + + private final int skipResults; + private final int pageSize; + private final int pageNumber; + private final int pageCount; + + /** + * Construct with defaults + *

    + *
  • skipResults: {@link #DEFAULT_SKIP_RESULTS}
  • + *
  • pageSize: {@link #DEFAULT_PAGE_SIZE}
  • + *
  • pageNumber: {@link #DEFAULT_PAGE_NUMBER}
  • + *
  • pageCount: {@link #DEFAULT_PAGE_COUNT}
  • + *
+ */ + public CannedQueryPageDetails() + { + this(DEFAULT_SKIP_RESULTS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_COUNT); + } + + /** + * Construct with defaults + *
    + *
  • pageNumber: {@link #DEFAULT_PAGE_NUMBER}
  • + *
  • pageCount: {@link #DEFAULT_PAGE_COUNT}
  • + *
+ * @param skipResults results to skip before page one + * (default {@link #DEFAULT_SKIP_RESULTS}) + * @param pageSize the size of each page + * (default {@link #DEFAULT_PAGE_SIZE}) + */ + public CannedQueryPageDetails(int skipResults, int pageSize) + { + this (skipResults, pageSize, DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_COUNT); + } + + /** + * @param skipResults results to skip before page one + * (default {@link #DEFAULT_SKIP_RESULTS}) + * @param pageSize the size of each page + * (default {@link #DEFAULT_PAGE_SIZE}) + * @param pageNumber the first page number to return + * (default {@link #DEFAULT_PAGE_NUMBER}) + * @param pageCount the number of pages to return + * (default {@link #DEFAULT_PAGE_COUNT}) + */ + public CannedQueryPageDetails(int skipResults, int pageSize, int pageNumber, int pageCount) + { + this.skipResults = skipResults; + this.pageSize = pageSize; + this.pageNumber = pageNumber; + this.pageCount = pageCount; + + // Do some checks + if (skipResults < 0) + { + throw new IllegalArgumentException("Cannot skip fewer than 0 results."); + } + if (pageSize < 1) + { + throw new IllegalArgumentException("pageSize must be greater than zero."); + } + if (pageNumber < 1) + { + throw new IllegalArgumentException("pageNumber must be greater than zero."); + } + if (pageCount < 1) + { + throw new IllegalArgumentException("pageCount must be greater than zero."); + } + } + + /** + * Helper constructor to transform a paging request into the Canned Query form. + * + * @param pagingRequest the paging details + */ + public CannedQueryPageDetails(PagingRequest pagingRequest) + { + this(pagingRequest.getSkipCount(), pagingRequest.getMaxItems()); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("NamedQueryPageDetails ") + .append("[skipResults=").append(skipResults) + .append(", pageSize=").append(pageSize) + .append(", pageCount=").append(pageCount) + .append(", pageNumber=").append(pageNumber) + .append("]"); + return sb.toString(); + } + + /** + * Get the number of query results to skip before applying further page parameters + * @return results to skip before page one + */ + public int getSkipResults() + { + return skipResults; + } + + /** + * Get the size of each page + * @return the size of each page + */ + public int getPageSize() + { + return pageSize; + } + + /** + * Get the first page number to return + * @return the first page number to return + */ + public int getPageNumber() + { + return pageNumber; + } + + /** + * Get the total number of pages to return + * @return the number of pages to return + */ + public int getPageCount() + { + return pageCount; + } + + /** + * Calculate the number of results that would be required to satisy this paging request. + * Note that the skip size can significantly increase this number even if the page sizes + * are small. + * + * @return the number of results required for proper paging + */ + public int getResultsRequiredForPaging() + { + int tmp = skipResults + pageCount * pageSize; + if(tmp < 0) + { + // overflow + return Integer.MAX_VALUE; + } + else + { + return tmp; + } + } +} diff --git a/core/src/main/java/org/alfresco/query/CannedQueryParameters.java b/core/src/main/java/org/alfresco/query/CannedQueryParameters.java new file mode 100644 index 0000000000..7554ac2862 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQueryParameters.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +/** + * Parameters defining the {@link CannedQuery named query} to execute. + *

+ * The implementations of the underlying queries may be vastly different + * depending on seemingly-minor variations in the parameters; only set the + * parameters that are required. + * + * @author Derek Hulley + * @since 4.0 + */ +public class CannedQueryParameters +{ + public static final int DEFAULT_TOTAL_COUNT_MAX = 0; // default 0 => don't request total count + + private final Object parameterBean; + private final CannedQueryPageDetails pageDetails; + private final CannedQuerySortDetails sortDetails; + private final int totalResultCountMax; + private final String queryExecutionId; + + /** + *

    + *
  • pageDetails: null
  • + *
  • sortDetails: null
  • + *
  • totalResultCountMax: 0
  • + *
  • queryExecutionId: null
  • + *
+ * + */ + public CannedQueryParameters(Object parameterBean) + { + this (parameterBean, null, null, DEFAULT_TOTAL_COUNT_MAX, null); + } + + /** + * Defaults: + *
    + *
  • pageDetails.pageNumber: 1
  • + *
  • pageDetails.pageCount: 1
  • + *
  • totalResultCountMax: 0
  • + *
+ * + */ + public CannedQueryParameters( + Object parameterBean, + int skipResults, + int pageSize, + String queryExecutionId) + { + this ( + parameterBean, + new CannedQueryPageDetails(skipResults, pageSize, CannedQueryPageDetails.DEFAULT_PAGE_NUMBER, CannedQueryPageDetails.DEFAULT_PAGE_COUNT), + null, + DEFAULT_TOTAL_COUNT_MAX, + queryExecutionId); + } + + /** + * Defaults: + *
    + *
  • totalResultCountMax: 0
  • + *
  • queryExecutionId: null
  • + *
+ * + */ + public CannedQueryParameters( + Object parameterBean, + CannedQueryPageDetails pageDetails, + CannedQuerySortDetails sortDetails) + { + this (parameterBean, pageDetails, sortDetails, DEFAULT_TOTAL_COUNT_MAX, null); + } + + /** + * Construct all the parameters for executing a named query, using values from the + * {@link PagingRequest}. + * + * @param parameterBean the values that the query will be based on or null + * if not relevant to the query + * @param sortDetails the type of sorting to be applied or null for none + * @param pagingRequest the type of paging to be applied or null for none + */ + public CannedQueryParameters( + Object parameterBean, + CannedQuerySortDetails sortDetails, + PagingRequest pagingRequest) + { + this ( + parameterBean, + pagingRequest == null ? null : new CannedQueryPageDetails(pagingRequest), + sortDetails, + pagingRequest == null ? 0 : pagingRequest.getRequestTotalCountMax(), + pagingRequest == null ? null : pagingRequest.getQueryExecutionId()); + } + + /** + * Construct all the parameters for executing a named query. Note that the allowable values + * for the arguments depend on the specific query being executed. + * + * @param parameterBean the values that the query will be based on or null + * if not relevant to the query + * @param pageDetails the type of paging to be applied or null for none + * @param sortDetails the type of sorting to be applied or null for none + * @param totalResultCountMax greater than zero if the query should not only return the required rows + * but should also return the total number of possible rows up to + * the given maximum. + * @param queryExecutionId ID of a previously-executed query to be used during follow-up + * page requests - null if not available + */ + @SuppressWarnings("unchecked") + public CannedQueryParameters( + Object parameterBean, + CannedQueryPageDetails pageDetails, + CannedQuerySortDetails sortDetails, + int totalResultCountMax, + String queryExecutionId) + { + if (totalResultCountMax < 0) + { + throw new IllegalArgumentException("totalResultCountMax cannot be negative."); + } + + this.parameterBean = parameterBean; + this.pageDetails = pageDetails == null ? new CannedQueryPageDetails() : pageDetails; + this.sortDetails = sortDetails == null ? new CannedQuerySortDetails() : sortDetails; + this.totalResultCountMax = totalResultCountMax; + this.queryExecutionId = queryExecutionId; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("NamedQueryParameters ") + .append("[parameterBean=").append(parameterBean) + .append(", pageDetails=").append(pageDetails) + .append(", sortDetails=").append(sortDetails) + .append(", requestTotalResultCountMax=").append(totalResultCountMax) + .append(", queryExecutionId=").append(queryExecutionId) + .append("]"); + return sb.toString(); + } + + public String getQueryExecutionId() + { + return queryExecutionId; + } + + /** + * @return the sort details (never null) + */ + public CannedQuerySortDetails getSortDetails() + { + return sortDetails; + } + + /** + * @return the query paging details (never null) + */ + public CannedQueryPageDetails getPageDetails() + { + return pageDetails; + } + + /** + * @return if > 0 then the query should not only return the required rows but should + * also return the total count (number of possible rows) up to the given max + * if 0 then query does not need to return the total count + */ + public int getTotalResultCountMax() + { + return totalResultCountMax; + } + + /** + * Helper method to get the total number of query results that need to be obtained in order + * to satisfy the {@link #getPageDetails() paging requirements}, the + * maximum result count ... and an extra to provide + * 'hasMore' functionality. + * + * @return the minimum number of results required before pages can be created + */ + public int getResultsRequired() + { + int resultsForPaging = pageDetails.getResultsRequiredForPaging(); + if (resultsForPaging < Integer.MAX_VALUE) // Add one for 'hasMore' + { + resultsForPaging++; + } + int maxRequired = Math.max(totalResultCountMax, resultsForPaging); + return maxRequired; + } + + /** + * @return parameterBean the values that the query will be based on or null + * if not relevant to the query + */ + public Object getParameterBean() + { + return parameterBean; + } +} diff --git a/core/src/main/java/org/alfresco/query/CannedQueryResults.java b/core/src/main/java/org/alfresco/query/CannedQueryResults.java new file mode 100644 index 0000000000..1dda96309e --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQueryResults.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.List; + +/** + * Interface for results returned by {@link CannedQuery canned queries}. + * + * @author Derek Hulley, janv + * @since 4.0 + */ +public interface CannedQueryResults extends PagingResults +{ + /** + * Get the instance of the query that generated these results. + * + * @return the query that generated these results. + */ + CannedQuery getOriginatingQuery(); + + /** + * Get the total number of results available within the pages of this result. + * The count excludes results chopped out by the paging process i.e. it is only + * the count of results physically obtainable through this instance. + * + * @return number of results available in the pages + */ + int getPagedResultCount(); + + /** + * Get the number of pages available + * + * @return the number of pages available + */ + int getPageCount(); + + /** + * Get a single result if there is only one result expected. + * + * @return a single result + * @throws IllegalStateException if the query returned more than one result + */ + R getSingleResult(); + + /** + * Get the paged results + * + * @return a list of paged results + */ + List> getPages(); +} diff --git a/core/src/main/java/org/alfresco/query/CannedQuerySortDetails.java b/core/src/main/java/org/alfresco/query/CannedQuerySortDetails.java new file mode 100644 index 0000000000..134c4a176f --- /dev/null +++ b/core/src/main/java/org/alfresco/query/CannedQuerySortDetails.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.alfresco.util.Pair; + +/** + * Details for canned queries supporting sorted results + * + * @author Derek Hulley + * @since 4.0 + */ +public class CannedQuerySortDetails +{ + /** + * Sort ordering for the sort pairs. + * @author Derek Hulley + * @since 4.0 + */ + public static enum SortOrder + { + ASCENDING, + DESCENDING + } + + private final List> sortPairs; + + /** + * Construct the sort details with a variable number of sort pairs. + *

+ * Sorting is done by:
+ * key: the key type to sort on
+ * sortOrder: the ordering of values associated with the key
+ * + * @param sortPairs the sort pairs, which will be applied in order + */ + public CannedQuerySortDetails(Pair ... sortPairs) + { + this.sortPairs = Collections.unmodifiableList(Arrays.asList(sortPairs)); + } + + /** + * Construct the sort details from a list of sort pairs. + *

+ * Sorting is done by:
+ * key: the key type to sort on
+ * sortOrder: the ordering of values associated with the key
+ * + * @param sortPairs the sort pairs, which will be applied in order + */ + public CannedQuerySortDetails(List> sortPairs) + { + this.sortPairs = Collections.unmodifiableList(sortPairs); + } + + @Override + public String toString() + { + return "CannedQuerySortDetails [sortPairs=" + sortPairs + "]"; + } + + /** + * Get the sort definitions. The instance will become unmodifiable after this has been called. + */ + public List> getSortPairs() + { + return Collections.unmodifiableList(sortPairs); + } +} diff --git a/core/src/main/java/org/alfresco/query/EmptyCannedQueryResults.java b/core/src/main/java/org/alfresco/query/EmptyCannedQueryResults.java new file mode 100644 index 0000000000..547b419a7f --- /dev/null +++ b/core/src/main/java/org/alfresco/query/EmptyCannedQueryResults.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.util.Pair; + +/** + * An always empty {@link CannedQueryResults}, used when you know + * you can short circuit a query when no results are found. + * + * @author Nick Burch + * @since 4.0 + */ +public class EmptyCannedQueryResults extends EmptyPagingResults implements CannedQueryResults +{ + private CannedQuery query; + + public EmptyCannedQueryResults(CannedQuery query) + { + this.query = query; + } + + @Override + public CannedQuery getOriginatingQuery() { + return query; + } + + @Override + public int getPageCount() { + return 0; + } + + @Override + public int getPagedResultCount() { + return 0; + } + + @Override + public List> getPages() { + return Collections.emptyList(); + } + + @Override + public R getSingleResult() { + return null; + } +} diff --git a/core/src/main/java/org/alfresco/query/EmptyPagingResults.java b/core/src/main/java/org/alfresco/query/EmptyPagingResults.java new file mode 100644 index 0000000000..995605adf5 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/EmptyPagingResults.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.util.Pair; + +/** + * An always empty {@link PagingResults}, used when you know + * you can short circuit a query when no results are found. + * + * @author Nick Burch + * @since 4.0 + */ +public class EmptyPagingResults implements PagingResults +{ + /** + * Returns an empty page + */ + public List getPage() + { + return Collections.emptyList(); + } + + /** + * No more items remain + */ + public boolean hasMoreItems() + { + return false; + } + + /** + * There are no results + */ + public Pair getTotalResultCount() + { + return new Pair(0,0); + } + + /** + * There is no unique query ID, as no query was done + */ + public String getQueryExecutionId() + { + return null; + } +} diff --git a/core/src/main/java/org/alfresco/query/ListBackedPagingResults.java b/core/src/main/java/org/alfresco/query/ListBackedPagingResults.java new file mode 100644 index 0000000000..a2833cc67d --- /dev/null +++ b/core/src/main/java/org/alfresco/query/ListBackedPagingResults.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.util.Pair; + +/** + * Wraps a list of items as a {@link PagingResults}, used typically when + * migrating from a full listing system to a paged one. + * + * @author Nick Burch + * @since Odin + */ +public class ListBackedPagingResults implements PagingResults +{ + private List results; + private int size; + private boolean hasMore; + + public ListBackedPagingResults(List list) + { + this.results = Collections.unmodifiableList(list); + + // No more items remain, the page is everything + size = list.size(); + hasMore = false; + } + public ListBackedPagingResults(List list, PagingRequest paging) + { + // Excerpt + int start = paging.getSkipCount(); + int end = Math.min(list.size(), start + paging.getMaxItems()); + if (paging.getMaxItems() == 0) + { + end = list.size(); + } + + this.results = Collections.unmodifiableList( + list.subList(start, end)); + this.size = list.size(); + this.hasMore = ! (list.size() == end); + } + + /** + * Returns the whole set of results as one page + */ + public List getPage() + { + return results; + } + + public boolean hasMoreItems() + { + return hasMore; + } + + /** + * We know exactly how many results there are + */ + public Pair getTotalResultCount() + { + return new Pair(size, size); + } + + /** + * There is no unique query ID, as no query was done + */ + public String getQueryExecutionId() + { + return null; + } +} diff --git a/core/src/main/java/org/alfresco/query/PageDetails.java b/core/src/main/java/org/alfresco/query/PageDetails.java new file mode 100644 index 0000000000..c26fec0634 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/PageDetails.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +/** + * Stores paging details based on a PagingRequest. + * + * @author steveglover + * + */ +public class PageDetails +{ + private boolean hasMoreItems = false; + private int pageSize; + private int skipCount; + private int maxItems; + private int end; + + public PageDetails(int pageSize, boolean hasMoreItems, int skipCount, int maxItems, int end) + { + super(); + this.hasMoreItems = hasMoreItems; + this.pageSize = pageSize; + this.skipCount = skipCount; + this.maxItems = maxItems; + this.end = end; + } + + public int getSkipCount() + { + return skipCount; + } + + public int getMaxItems() + { + return maxItems; + } + + public int getEnd() + { + return end; + } + + public boolean hasMoreItems() + { + return hasMoreItems; + } + + public int getPageSize() + { + return pageSize; + } + + public static PageDetails getPageDetails(PagingRequest pagingRequest, int totalSize) + { + int skipCount = pagingRequest.getSkipCount(); + int maxItems = pagingRequest.getMaxItems(); + int end = skipCount + maxItems; + int pageSize = -1; + if(end < 0 || end > totalSize) + { + // overflow or greater than the total + end = totalSize; + pageSize = end - skipCount; + } + else + { + pageSize = maxItems; + } + if(pageSize < 0) + { + pageSize = 0; + } + boolean hasMoreItems = end < totalSize; + return new PageDetails(pageSize, hasMoreItems, skipCount, maxItems, end); + } +} diff --git a/core/src/main/java/org/alfresco/query/PagingRequest.java b/core/src/main/java/org/alfresco/query/PagingRequest.java new file mode 100644 index 0000000000..a35639e45f --- /dev/null +++ b/core/src/main/java/org/alfresco/query/PagingRequest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Simple wrapper for single page request (with optional request for total count up to a given max) + * + * @author janv + * @since 4.0 + */ +@AlfrescoPublicApi +public class PagingRequest +{ + private int skipCount = CannedQueryPageDetails.DEFAULT_SKIP_RESULTS; + private int maxItems; + + private int requestTotalCountMax = 0; // request total count up to a given max (0 => do not request total count) + private String queryExecutionId; + + /** + * Construct a page request + * + * @param maxItems the maximum number of items per page + */ + public PagingRequest(int maxItems) + { + this.maxItems = maxItems; + } + + /** + * Construct a page request + * + * @param maxItems the maximum number of items per page + * @param skipCount the number of items to skip before the first page + */ + public PagingRequest(int skipCount, int maxItems) + { + this.skipCount = skipCount; + this.maxItems = maxItems; + } + + /** + * Construct a page request + * + * @param maxItems the maximum number of items per page + * @param queryExecutionId a query execution ID associated with ealier paged requests + */ + public PagingRequest(int maxItems, String queryExecutionId) + { + setMaxItems(maxItems); + this.queryExecutionId = queryExecutionId; + } + + /** + * Construct a page request + * + * @param skipCount the number of items to skip before the first page + * @param maxItems the maximum number of items per page + * @param queryExecutionId a query execution ID associated with ealier paged requests + */ + public PagingRequest(int skipCount, int maxItems, String queryExecutionId) + { + setSkipCount(skipCount); + setMaxItems(maxItems); + this.queryExecutionId = queryExecutionId; + } + + /** + * Results to skip before retrieving the page. Usually a multiple of page size (ie. page size * num pages to skip). + * Default is 0. + * + * @return the number of results to skip before the page + */ + public int getSkipCount() + { + return skipCount; + } + + /** + * Change the skip count. Must be called before the paging query is run. + */ + protected void setSkipCount(int skipCount) + { + this.skipCount = (skipCount < 0 ? CannedQueryPageDetails.DEFAULT_SKIP_RESULTS : skipCount); + } + + /** + * Size of the page - if skip count is 0 then return up to max items. + * + * @return the maximum size of the page + */ + public int getMaxItems() + { + return maxItems; + } + + /** + * Change the size of the page. Must be called before the paging query is run. + */ + protected void setMaxItems(int maxItems) + { + this.maxItems = (maxItems < 0 ? CannedQueryPageDetails.DEFAULT_PAGE_SIZE : maxItems); + } + + /** + * Get requested total count (up to a given maximum). + */ + public int getRequestTotalCountMax() + { + return requestTotalCountMax; + } + + /** + * Set request total count (up to a given maximum). Default is 0 => do not request total count (which allows possible query optimisation). + * + * @param requestTotalCountMax + */ + public void setRequestTotalCountMax(int requestTotalCountMax) + { + this.requestTotalCountMax = requestTotalCountMax; + } + + /** + * Get a unique ID associated with these query results. This must be available before and + * after execution i.e. it must depend on the type of query and the query parameters + * rather than the execution results. Client has the option to pass this back as a hint when + * paging. + * + * @return a unique ID associated with the query execution results + */ + public String getQueryExecutionId() + { + return queryExecutionId; + } + + /** + * Change the unique query ID for the results. Must be called before the paging query is run. + */ + protected void setQueryExecutionId(String queryExecutionId) + { + this.queryExecutionId = queryExecutionId; + } +} diff --git a/core/src/main/java/org/alfresco/query/PagingResults.java b/core/src/main/java/org/alfresco/query/PagingResults.java new file mode 100644 index 0000000000..8cfb2e87b8 --- /dev/null +++ b/core/src/main/java/org/alfresco/query/PagingResults.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.List; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.util.Pair; + +/** + * Marker interface for single page of results + * + * @author janv + * @since 4.0 + */ +@AlfrescoPublicApi +public interface PagingResults +{ + /** + * Get the page of results. + * + * @return the results - possibly empty but never null + */ + public List getPage(); + + /** + * True if more items on next page. + *

+ * Note: could also return true if page was cutoff/trimmed for some reason + * (eg. due to permission checks of large page of requested max items) + * + * @return true if more items (eg. on next page)
+ * - true => at least one more page (or incomplete page - if cutoff)
+ * - false => last page (or incomplete page - if cutoff) + */ + public boolean hasMoreItems(); + + /** + * Get the total result count assuming no paging applied. This value will only be available if + * the query supports it and the client requested it. By default, it is not requested. + *

+ * Returns result as an approx "range" pair + *

    + *
  • null (or lower is null): unknown total count (or not requested by the client).
  • + *
  • lower = upper : total count should be accurate
  • + *
  • lower < upper : total count is an approximation ("about") - somewhere in the given range (inclusive)
  • + *
  • upper is null : total count is "more than" lower (upper is unknown)
  • + *
+ * + * @return Returns the total results as a range (all results, including the paged results returned) + */ + public Pair getTotalResultCount(); + + /** + * Get a unique ID associated with these query results. This must be available before and + * after execution i.e. it must depend on the type of query and the query parameters + * rather than the execution results. Client has the option to pass this back as a hint when + * paging. + * + * @return a unique ID associated with the query execution results + */ + public String getQueryExecutionId(); +} diff --git a/core/src/main/java/org/alfresco/query/PermissionedResults.java b/core/src/main/java/org/alfresco/query/PermissionedResults.java new file mode 100644 index 0000000000..b31613890c --- /dev/null +++ b/core/src/main/java/org/alfresco/query/PermissionedResults.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +/** + * Marker interface to show that permissions have already been applied to the results (and possibly cutoff) + * + * @author janv + * @since 4.0 + */ +public interface PermissionedResults +{ + /** + * @return true - if permissions have been applied to the results + */ + public boolean permissionsApplied(); + + /** + * @return true - if permission checks caused results to be cutoff (either due to max count or max time) + */ + public boolean hasMoreItems(); +} diff --git a/core/src/main/java/org/alfresco/scripts/ScriptException.java b/core/src/main/java/org/alfresco/scripts/ScriptException.java new file mode 100644 index 0000000000..6fb5b6ac04 --- /dev/null +++ b/core/src/main/java/org/alfresco/scripts/ScriptException.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.scripts; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * @author Kevin Roast + */ +public class ScriptException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 1739480648583299623L; + + /** + * @param msgId String + */ + public ScriptException(String msgId) + { + super(msgId); + } + + /** + * @param msgId String + * @param cause Throwable + */ + public ScriptException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + /** + * @param msgId String + * @param params Object[] + */ + public ScriptException(String msgId, Object[] params) + { + super(msgId, params); + } + + /** + * @param msgId String + * @param msgParams Object[] + * @param cause Throwable + */ + public ScriptException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } +} diff --git a/core/src/main/java/org/alfresco/scripts/ScriptResourceHelper.java b/core/src/main/java/org/alfresco/scripts/ScriptResourceHelper.java new file mode 100644 index 0000000000..06be2ef1f0 --- /dev/null +++ b/core/src/main/java/org/alfresco/scripts/ScriptResourceHelper.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.scripts; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; + +/** + * @author Kevin Roast + */ +public class ScriptResourceHelper +{ + private static final String SCRIPT_ROOT = "_root"; + private static final String IMPORT_PREFIX = " + * Multiple includes of the same resource are dealt with correctly and nested includes of scripts + * is fully supported. + *

+ * Note that for performance reasons the script import directive syntax and placement in the file + * is very strict. The import lines must always be first in the file - even before any comments. + * Immediately that the script service detects a non-import line it will assume the rest of the + * file is executable script and no longer attempt to search for any further import directives. Therefore + * all imports should be at the top of the script, one following the other, in the correct syntax and + * with no comments present - the only separators valid between import directives is white space. + * + * @param script The script content to resolve imports in + * + * @return a valid script with all nested includes resolved into a single script instance + */ + public static String resolveScriptImports(String script, ScriptResourceLoader loader, Log logger) + { + // use a linked hashmap to preserve order of includes - the key in the collection is used + // to resolve multiple includes of the same scripts and therefore cyclic includes also + Map scriptlets = new LinkedHashMap(8, 1.0f); + + // perform a recursive resolve of all script imports + recurseScriptImports(SCRIPT_ROOT, script, loader, scriptlets, logger); + + if (scriptlets.size() == 1) + { + // quick exit for single script with no includes + if (logger.isTraceEnabled()) + logger.trace("Script content resolved to:\r\n" + script); + + return script; + } + else + { + // calculate total size of buffer required for the script and all includes + int length = 0; + for (String scriptlet : scriptlets.values()) + { + length += scriptlet.length(); + } + // append the scripts together to make a single script + StringBuilder result = new StringBuilder(length); + for (String scriptlet : scriptlets.values()) + { + result.append(scriptlet); + } + + if (logger.isTraceEnabled()) + logger.trace("Script content resolved to:\r\n" + result.toString()); + + return result.toString(); + } + } + + /** + * Recursively resolve imports in the specified scripts, adding the imports to the + * specific list of scriplets to combine later. + * + * @param location Script location - used to ensure duplicates are not added + * @param script The script to recursively resolve imports for + * @param scripts The collection of scriplets to execute with imports resolved and removed + */ + private static void recurseScriptImports( + String location, String script, ScriptResourceLoader loader, Map scripts, Log logger) + { + int index = 0; + // skip any initial whitespace + for (; index') + { + // found end of import line - so we have a resource path + String resource = script.substring(resourceStart, index); + + if (logger.isDebugEnabled()) + logger.debug("Found script resource import: " + resource); + + if (scripts.containsKey(resource) == false) + { + // load the script resource (and parse any recursive includes...) + String includedScript = loader.loadScriptResource(resource); + if (includedScript != null) + { + if (logger.isDebugEnabled()) + logger.debug("Succesfully located script '" + resource + "'"); + recurseScriptImports(resource, includedScript, loader, scripts, logger); + } + } + else + { + if (logger.isDebugEnabled()) + logger.debug("Note: already imported resource: " + resource); + } + + // continue scanning this script for additional includes + // skip the last two characters of the import directive + for (index += 2; index"); + } + else + { + throw new ScriptException( + "Malformed 'import' line - must be first in file, no comments and strictly of the form:" + + "\r\n"); + } + } + else + { + // no (further) includes found - include the original script content + if (logger.isDebugEnabled()) + logger.debug("Imports resolved, adding resource '" + location); + if (logger.isTraceEnabled()) + logger.trace(script); + scripts.put(location, script); + } + } +} diff --git a/core/src/main/java/org/alfresco/scripts/ScriptResourceLoader.java b/core/src/main/java/org/alfresco/scripts/ScriptResourceLoader.java new file mode 100644 index 0000000000..dfa4683794 --- /dev/null +++ b/core/src/main/java/org/alfresco/scripts/ScriptResourceLoader.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.scripts; + +/** + * @author Kevin Roast + */ +public interface ScriptResourceLoader +{ + public String loadScriptResource(String resource); +} diff --git a/core/src/main/java/org/alfresco/util/ArgumentHelper.java b/core/src/main/java/org/alfresco/util/ArgumentHelper.java new file mode 100644 index 0000000000..fff0df53ec --- /dev/null +++ b/core/src/main/java/org/alfresco/util/ArgumentHelper.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class to assist in extracting program arguments. + * + * @author Derek Hulley + * @since V2.1-A + */ +public class ArgumentHelper +{ + private String usage; + private Map args; + + public static Map ripArgs(String ... args) + { + Map argsMap = new HashMap(5); + for (String arg : args) + { + int index = arg.indexOf('='); + if (!arg.startsWith("--") || index < 0 || index == arg.length() - 1) + { + // Ignore it + continue; + } + String name = arg.substring(2, index); + String value = arg.substring(index + 1, arg.length()); + argsMap.put(name, value); + } + return argsMap; + } + + public ArgumentHelper(String usage, String[] args) + { + this.usage = usage; + this.args = ArgumentHelper.ripArgs(args); + } + + /** + * @throws IllegalArgumentException if the argument doesn't match the requirements. + */ + public String getStringValue(String arg, boolean mandatory, boolean nonEmpty) + { + String value = args.get(arg); + if (value == null && mandatory) + { + throw new IllegalArgumentException("Argument '" + arg + "' is required."); + } + else if (value != null && value.length() == 0 && nonEmpty) + { + throw new IllegalArgumentException("Argument '" + arg + "' may not be empty."); + } + return value; + } + + /** + * @return Returns the value assigned or the minimum value if the parameter was not present + * @throws IllegalArgumentException if the argument doesn't match the requirements. + */ + public int getIntegerValue(String arg, boolean mandatory, int minValue, int maxValue) + { + String valueStr = args.get(arg); + if (valueStr == null) + { + if (mandatory) + { + throw new IllegalArgumentException("Argument '" + arg + "' is required."); + } + else + { + return minValue; + } + } + // Now convert + try + { + int value = Integer.parseInt(valueStr); + if (value < minValue || value > maxValue) + { + throw new IllegalArgumentException("Argument '" + arg + "' must be in range " + minValue + " to " + maxValue + "."); + } + return value; + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("Argument '" + arg + "' must be a valid integer."); + } + } + + public void printUsage() + { + System.out.println(usage); + } +} diff --git a/core/src/main/java/org/alfresco/util/BridgeTable.java b/core/src/main/java/org/alfresco/util/BridgeTable.java new file mode 100644 index 0000000000..04b582293f --- /dev/null +++ b/core/src/main/java/org/alfresco/util/BridgeTable.java @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Generic bridge table support with optional reference counting to allow multiple membership for an object via several + * relationships. + * + * @author Andy + */ +public class BridgeTable +{ + HashMap>> descendants = new HashMap>>(); + + HashMap>> ancestors = new HashMap>>(); + + ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + public void addLink(T parent, T child) + { + readWriteLock.writeLock().lock(); + try + { + addDescendants(parent, child); + addAncestors(parent, child); + } + finally + { + readWriteLock.writeLock().unlock(); + } + + } + + public void addLink(Pair link) + { + addLink(link.getFirst(), link.getSecond()); + } + + public void addLinks(Collection> links) + { + for (Pair link : links) + { + addLink(link); + } + } + + public void removeLink(T parent, T child) + { + readWriteLock.writeLock().lock(); + try + { + removeDescendants(parent, child); + removeAncestors(parent, child); + } + finally + { + readWriteLock.writeLock().unlock(); + } + + } + + public void removeLink(Pair link) + { + removeLink(link.getFirst(), link.getSecond()); + } + + public void removeLinks(Collection> links) + { + for (Pair link : links) + { + removeLink(link); + } + } + + public HashSet getDescendants(T node) + { + return getDescendants(node, 1, Integer.MAX_VALUE); + } + + public HashSet getDescendants(T node, int position) + { + return getDescendants(node, position, position); + } + + public HashSet getDescendants(T node, int start, int end) + { + HashSet answer = new HashSet(); + HashMap> found = descendants.get(node); + if (found != null) + { + for (Integer key : found.keySet()) + { + if ((key.intValue() >= start) && (key.intValue() <= end)) + { + HashMap asd = found.get(key); + answer.addAll(asd.keySet()); + } + } + } + return answer; + } + + public HashSet getAncestors(T node) + { + return getAncestors(node, 1, Integer.MAX_VALUE); + } + + public HashSet getAncestors(T node, int position) + { + return getAncestors(node, position, position); + } + + public HashSet getAncestors(T node, int start, int end) + { + HashSet answer = new HashSet(); + HashMap> found = ancestors.get(node); + if (found != null) + { + for (Integer key : found.keySet()) + { + if ((key.intValue() >= start) && (key.intValue() <= end)) + { + HashMap asd = found.get(key); + answer.addAll(asd.keySet()); + } + } + } + return answer; + } + + /** + * @param parent T + * @param child T + */ + private void addDescendants(T parent, T child) + { + HashMap> parentsDescendants = descendants.get(parent); + if (parentsDescendants == null) + { + parentsDescendants = new HashMap>(); + descendants.put(parent, parentsDescendants); + } + + HashMap> childDescendantsToAdd = descendants.get(child); + + // add all the childs children to the parents descendants + + add(childDescendantsToAdd, Integer.valueOf(0), parentsDescendants, child); + + // add childs descendants to all parents ancestors at the correct depth + + HashMap> ancestorsToFixUp = ancestors.get(parent); + + if (ancestorsToFixUp != null) + { + for (Integer ancestorPosition : ancestorsToFixUp.keySet()) + { + HashMap ancestorsToFixUpAtPosition = ancestorsToFixUp.get(ancestorPosition); + for (T ancestorToFixUpAtPosition : ancestorsToFixUpAtPosition.keySet()) + { + HashMap> ancestorDescendants = descendants.get(ancestorToFixUpAtPosition); + add(childDescendantsToAdd, ancestorPosition, ancestorDescendants, child); + } + } + } + } + + /** + * @param parent T + * @param child T + */ + private void removeDescendants(T parent, T child) + { + HashMap> parentsDescendants = descendants.get(parent); + if (parentsDescendants == null) + { + return; + } + + HashMap> childDescendantsToRemove = descendants.get(child); + + // add all the childs children to the parents descendants + + remove(childDescendantsToRemove, Integer.valueOf(0), parentsDescendants, child); + + // add childs descendants to all parents ancestors at the correct depth + + HashMap> ancestorsToFixUp = ancestors.get(parent); + + if (ancestorsToFixUp != null) + { + for (Integer ancestorPosition : ancestorsToFixUp.keySet()) + { + HashMap ancestorsToFixUpAtPosition = ancestorsToFixUp.get(ancestorPosition); + for (T ancestorToFixUpAtPosition : ancestorsToFixUpAtPosition.keySet()) + { + HashMap> ancestorDescendants = descendants.get(ancestorToFixUpAtPosition); + remove(childDescendantsToRemove, ancestorPosition, ancestorDescendants, child); + } + } + } + } + + /** + * @param parent T + * @param child T + */ + private void removeAncestors(T parent, T child) + { + HashMap> childsAncestors = ancestors.get(child); + if (childsAncestors == null) + { + return; + } + + HashMap> parentAncestorsToRemove = ancestors.get(parent); + + // add all the childs children to the parents descendants + + remove(parentAncestorsToRemove, Integer.valueOf(0), childsAncestors, parent); + + // add childs descendants to all parents ancestors at the correct depth + + HashMap> decendantsToFixUp = descendants.get(child); + + if (decendantsToFixUp != null) + { + for (Integer descendantPosition : decendantsToFixUp.keySet()) + { + HashMap decendantsToFixUpAtPosition = decendantsToFixUp.get(descendantPosition); + for (T descendantToFixUpAtPosition : decendantsToFixUpAtPosition.keySet()) + { + HashMap> descendantAncestors = ancestors.get(descendantToFixUpAtPosition); + remove(parentAncestorsToRemove, descendantPosition, descendantAncestors, parent); + } + } + } + } + + /** + * @param toAdd HashMap> + * @param position Integer + * @param target HashMap> + * @param node T + */ + private void add(HashMap> toAdd, Integer position, HashMap> target, T node) + { + // add direct child + Integer directKey = Integer.valueOf(position.intValue() + 1); + HashMap direct = target.get(directKey); + if (direct == null) + { + direct = new HashMap(); + target.put(directKey, direct); + } + Counter counter = direct.get(node); + if (counter == null) + { + counter = new Counter(); + direct.put(node, counter); + } + counter.increment(); + + if (toAdd != null) + { + for (Integer depth : toAdd.keySet()) + { + Integer newKey = Integer.valueOf(position.intValue() + depth.intValue() + 1); + HashMap toAddAtDepth = toAdd.get(depth); + HashMap targetAtDepthPlusOne = target.get(newKey); + if (targetAtDepthPlusOne == null) + { + targetAtDepthPlusOne = new HashMap(); + target.put(newKey, targetAtDepthPlusOne); + } + + for (T key : toAddAtDepth.keySet()) + { + Counter counterToAdd = toAddAtDepth.get(key); + Counter counterToAddTo = targetAtDepthPlusOne.get(key); + if (counterToAddTo == null) + { + counterToAddTo = new Counter(); + targetAtDepthPlusOne.put(key, counterToAddTo); + } + counterToAddTo.add(counterToAdd); + } + } + } + } + + /** + * @param toRemove HashMap> + * @param position Integer + * @param target HashMap> + * @param node T + */ + private void remove(HashMap> toRemove, Integer position, HashMap> target, T node) + { + // remove direct child + Integer directKey = Integer.valueOf(position.intValue() + 1); + HashMap direct = target.get(directKey); + if (direct != null) + { + Counter counter = direct.get(node); + if (counter != null) + { + counter.decrement(); + if (counter.getCount() == 0) + { + direct.remove(node); + } + } + + } + + if (toRemove != null) + { + for (Integer depth : toRemove.keySet()) + { + Integer newKey = Integer.valueOf(position.intValue() + depth.intValue() + 1); + HashMap toRemoveAtDepth = toRemove.get(depth); + HashMap targetAtDepthPlusOne = target.get(newKey); + if (targetAtDepthPlusOne != null) + { + for (T key : toRemoveAtDepth.keySet()) + { + Counter counterToRemove = toRemoveAtDepth.get(key); + Counter counterToRemoveFrom = targetAtDepthPlusOne.get(key); + if (counterToRemoveFrom != null) + { + counterToRemoveFrom.remove(counterToRemove); + if (counterToRemoveFrom.getCount() == 0) + { + targetAtDepthPlusOne.remove(key); + } + } + } + } + } + } + } + + /** + * @param parent T + * @param child T + */ + private void addAncestors(T parent, T child) + { + HashMap> childsAncestors = ancestors.get(child); + if (childsAncestors == null) + { + childsAncestors = new HashMap>(); + ancestors.put(child, childsAncestors); + } + + HashMap> parentAncestorsToAdd = ancestors.get(parent); + + // add all the childs children to the parents descendants + + add(parentAncestorsToAdd, Integer.valueOf(0), childsAncestors, parent); + + // add childs descendants to all parents ancestors at the correct depth + + HashMap> descenantsToFixUp = descendants.get(child); + + if (descenantsToFixUp != null) + { + for (Integer descendantPosition : descenantsToFixUp.keySet()) + { + HashMap descenantsToFixUpAtPosition = descenantsToFixUp.get(descendantPosition); + for (T descenantToFixUpAtPosition : descenantsToFixUpAtPosition.keySet()) + { + HashMap> descendatAncestors = ancestors.get(descenantToFixUpAtPosition); + add(parentAncestorsToAdd, descendantPosition, descendatAncestors, parent); + } + } + } + } + + public int size() + { + return ancestors.size(); + } + + private static class Counter + { + int count = 0; + + void increment() + { + count++; + } + + void decrement() + { + count--; + } + + int getCount() + { + return count; + } + + void add(Counter other) + { + count += other.count; + } + + void remove(Counter other) + { + count -= other.count; + } + } + + /** + * @return Set + */ + public Set keySet() + { + return ancestors.keySet(); + } +} diff --git a/core/src/main/java/org/alfresco/util/CachingDateFormat.java b/core/src/main/java/org/alfresco/util/CachingDateFormat.java new file mode 100644 index 0000000000..c719c42276 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/CachingDateFormat.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2005-2018 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import static java.util.Arrays.stream; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.*; + +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +/** + * Provides thread safe means of obtaining a cached date formatter. + *

+ * The cached string-date mappings are stored in a WeakHashMap. + * + * @see java.text.DateFormat#setLenient(boolean) + * + * @author Derek Hulley + * @author Andrea Gazzarini + */ +public class CachingDateFormat extends SimpleDateFormat +{ + private static final long serialVersionUID = 3258415049197565235L; + public static final String UTC = "UTC"; + + public static final String FORMAT_FULL_GENERIC = "yyyy-MM-dd'T'HH:mm:ss"; + public static final String FORMAT_CMIS_SQL = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String FORMAT_SOLR = "yyyy-MM-dd'T'HH:mm:ss.SSSX"; + public static final String UTC_WITHOUT_MSECS = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + public static final String FORMAT_DATE_GENERIC = "yyyy-MM-dd"; + public static final String FORMAT_TIME_GENERIC = "HH:mm:ss"; + + public static final StringAndResolution[] LENIENT_FORMATS = + { + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ssZ", Calendar.SECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss", Calendar.SECOND), + new StringAndResolution("yyyy-MM-dd'T'HH:mmZ", Calendar.MINUTE), + new StringAndResolution("yyyy-MM-dd'T'HH:mm", Calendar.MINUTE), + new StringAndResolution("yyyy-MM-dd'T'HHZ", Calendar.HOUR_OF_DAY), + new StringAndResolution("yyyy-MM-dd'T'HH", Calendar.HOUR_OF_DAY), + new StringAndResolution("yyyy-MM-dd'T'Z", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MM-dd'T'", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MM-ddZ", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MM-dd", Calendar.DAY_OF_MONTH), + new StringAndResolution("yyyy-MMZ", Calendar.MONTH), + new StringAndResolution("yyyy-MM", Calendar.MONTH), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ssZ", Calendar.SECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss", Calendar.SECOND), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mmZ", Calendar.MINUTE), + new StringAndResolution( "yyyy-MMM-dd'T'HH:mm", Calendar.MINUTE), + new StringAndResolution( "yyyy-MMM-dd'T'HHZ", Calendar.HOUR_OF_DAY), + new StringAndResolution( "yyyy-MMM-dd'T'HH", Calendar.HOUR_OF_DAY), + new StringAndResolution( "yyyy-MMM-dd'T'Z",Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMM-dd'T'",Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMM-ddZ", Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMM-dd", Calendar.DAY_OF_MONTH), + new StringAndResolution( "yyyy-MMMZ", Calendar.MONTH), + new StringAndResolution( "yyyy-MMM", Calendar.MONTH), + new StringAndResolution("yyyyZ", Calendar.YEAR), + new StringAndResolution("yyyy", Calendar.YEAR) + }; + + static ThreadLocal S_LOCAL_DATE_FORMAT = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_FULL_GENERIC)); + + static ThreadLocal S_LOCAL_DATEONLY_FORMAT = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_DATE_GENERIC)); + + static ThreadLocal S_LOCAL_TIMEONLY_FORMAT = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_TIME_GENERIC)); + + static ThreadLocal S_LOCAL_CMIS_SQL_DATETIME = ThreadLocal.withInitial(() -> newDateFormat(FORMAT_CMIS_SQL)); + + static ThreadLocal S_LOCAL_SOLR_DATETIME = ThreadLocal.withInitial(()-> + { + CachingDateFormat formatter = newDateFormatWithLocale(FORMAT_SOLR, Locale.ENGLISH); + /* + SEARCH-1263 + Apache Solr only supports the ISO 8601 date format: + UTC and western locale are mandatory (only Arabic numerals (0123456789) are supported) + */ + formatter.setTimeZone(TimeZone.getTimeZone(UTC)); + return formatter; + }); + + static ThreadLocal S_UTC_DATETIME_WITHOUT_MSECS = ThreadLocal.withInitial(() -> + { + CachingDateFormat formatter = newDateFormatWithLocale(UTC_WITHOUT_MSECS, Locale.ENGLISH); + formatter.setTimeZone(TimeZone.getTimeZone(UTC)); + return formatter; + }); + + static ThreadLocal S_LENIENT_PARSERS = + ThreadLocal.withInitial(() -> + stream(LENIENT_FORMATS) + .map(format -> { + CachingDateFormat formatter = new CachingDateFormat(format.string); + formatter.setLenient(false); + return new SimpleDateFormatAndResolution(formatter, format.resolution); }) + .toArray(SimpleDateFormatAndResolution[]::new)); + + private Map cacheDates = new WeakHashMap<>(89); + + private CachingDateFormat(String pattern, Locale locale) + { + super(pattern, locale); + } + + private CachingDateFormat(String pattern) + { + super(pattern); + } + + @Override + public String toString() + { + return this.toPattern(); + } + + /** + * @param length + * the type of date format, e.g. {@link CachingDateFormat#LONG } + * @param locale + * the Locale that will be used to determine the + * date pattern + * + * @see #getDateFormat(String, boolean) + * @see CachingDateFormat#SHORT + * @see CachingDateFormat#MEDIUM + * @see CachingDateFormat#LONG + * @see CachingDateFormat#FULL + */ + public static SimpleDateFormat getDateFormat(int length, Locale locale, boolean lenient) + { + SimpleDateFormat dateFormat = (SimpleDateFormat) CachingDateFormat.getDateInstance(length, locale); + // extract the format string + String pattern = dateFormat.toPattern(); + // we have a pattern to use + return getDateFormat(pattern, lenient); + } + + /** + * @param dateLength + * the type of date format, e.g. {@link CachingDateFormat#LONG } + * @param timeLength + * the type of time format, e.g. {@link CachingDateFormat#LONG } + * @param locale + * the Locale that will be used to determine the + * date pattern + * + * @see #getDateFormat(String, boolean) + * @see CachingDateFormat#SHORT + * @see CachingDateFormat#MEDIUM + * @see CachingDateFormat#LONG + * @see CachingDateFormat#FULL + */ + public static SimpleDateFormat getDateTimeFormat(int dateLength, int timeLength, Locale locale, boolean lenient) + { + SimpleDateFormat dateFormat = (SimpleDateFormat) CachingDateFormat.getDateTimeInstance(dateLength, timeLength, locale); + // extract the format string + String pattern = dateFormat.toPattern(); + // we have a pattern to use + return getDateFormat(pattern, lenient); + } + + /** + * @param pattern + * the conversion pattern to use + * @param lenient + * true to allow the parser to extract the date in conceivable + * manner + * @return Returns a conversion-cacheing formatter for the given pattern, + * but the instance itself is not cached + */ + public static SimpleDateFormat getDateFormat(String pattern, boolean lenient) + { + // create an alfrescoDateFormat for cacheing purposes + SimpleDateFormat dateFormat = new CachingDateFormat(pattern); + // set leniency + dateFormat.setLenient(lenient); + // done + return dateFormat; + } + + /** + * Returns a thread-safe formatter for the generic date/time format. + * + * @see #FORMAT_FULL_GENERIC + * @return a thread-safe formatter for the generic date/time format. + */ + public static SimpleDateFormat getDateFormat() + { + return S_LOCAL_DATE_FORMAT.get(); + } + + /** + * Returns a thread-safe formatter for the cmis sql datetime format. + * + * @see #FORMAT_CMIS_SQL + * @return a thread-safe formatter for the cmis sql datetime format. + */ + public static SimpleDateFormat getCmisSqlDatetimeFormat() + { + return S_LOCAL_CMIS_SQL_DATETIME.get(); + } + + /** + * Returns a thread-safe formatter for the Solr ISO 8601 datetime format (without the msecs part). + * + * @see #UTC_WITHOUT_MSECS + * @return Returns a thread-safe formatter for the Solr ISO 8601 datetime format (without the msecs part). + */ + public static SimpleDateFormat getSolrDatetimeFormatWithoutMsecs() + { + return S_UTC_DATETIME_WITHOUT_MSECS.get(); + } + + /** + * Returns a thread-safe formatter for the Solr ISO 8601 datetime format. + * + * @see #FORMAT_SOLR + * @return a thread-safe formatter for the Solr ISO 8601 datetime format + */ + public static SimpleDateFormat getSolrDatetimeFormat() + { + return S_LOCAL_SOLR_DATETIME.get(); + } + + /** + * @return Returns a thread-safe formatter for the generic date format + * + * @see #FORMAT_DATE_GENERIC + */ + public static SimpleDateFormat getDateOnlyFormat() + { + return S_LOCAL_DATEONLY_FORMAT.get(); + } + + /** + * Returns a thread-safe formatter for the generic time format. + * + * @see #FORMAT_TIME_GENERIC + * @return a thread-safe formatter for the generic time format. + */ + public static SimpleDateFormat getTimeOnlyFormat() + { + return S_LOCAL_TIMEONLY_FORMAT.get(); + } + + /** + * Parses and caches date strings. + * + * @see java.text.DateFormat#parse(java.lang.String, + * java.text.ParsePosition) + */ + public Date parse(String text, ParsePosition pos) + { + Date cached = cacheDates.get(text); + if (cached == null) + { + Date date = super.parse(text, pos); + if ((date != null) && (pos.getIndex() == text.length())) + { + cacheDates.put(text, date); + return (Date) date.clone(); + } + else + { + return date; + } + } + else + { + pos.setIndex(text.length()); + return (Date) cached.clone(); + } + } + + public static Pair lenientParse(String text, int minimumResolution) throws ParseException + { + DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); + try + { + Date parsed = fmt.parseDateTime(text).toDate(); + return new Pair(parsed, Calendar.MILLISECOND); + } + catch(IllegalArgumentException e) + { + // Nothing to be done here + } + + SimpleDateFormatAndResolution[] formatters = getLenientFormatters(); + for(SimpleDateFormatAndResolution formatter : formatters) + { + if(formatter.resolution >= minimumResolution) + { + ParsePosition pp = new ParsePosition(0); + Date parsed = formatter.simpleDateFormat.parse(text, pp); + if ((pp.getIndex() < text.length()) || (parsed == null)) + { + continue; + } + return new Pair(parsed, formatter.resolution); + } + } + + throw new ParseException("Unknown date format", 0); + } + + public static SimpleDateFormatAndResolution[] getLenientFormatters() + { + return S_LENIENT_PARSERS.get(); + } + + public static class StringAndResolution + { + String string; + int resolution; + + /** + * @return the resolution + */ + public int getResolution() + { + return resolution; + } + + /** + * @param resolution the resolution to set + */ + public void setResolution(int resolution) + { + this.resolution = resolution; + } + + StringAndResolution(String string, int resolution) + { + this.string = string; + this.resolution = resolution; + } + } + + public static class SimpleDateFormatAndResolution + { + SimpleDateFormat simpleDateFormat; + int resolution; + + SimpleDateFormatAndResolution(SimpleDateFormat simpleDateFormat, int resolution) + { + this.simpleDateFormat = simpleDateFormat; + this.resolution = resolution; + } + + /** + * @return the simpleDateFormat + */ + public SimpleDateFormat getSimpleDateFormat() + { + return simpleDateFormat; + } + + /** + * @return the resolution + */ + public int getResolution() + { + return resolution; + } + + } + + /** + * Creates a new non-lenient {@link CachingDateFormat} instance. + * + * @param pattern the date / datetime pattern. + * @return new non-lenient {@link CachingDateFormat} instance. + */ + private static CachingDateFormat newDateFormat(String pattern) + { + CachingDateFormat formatter = new CachingDateFormat(pattern); + formatter.setLenient(false); + return formatter; + } + + /** + * Creates a new non-lenient localised {@link CachingDateFormat} instance. + * + * @param pattern the date / datetime pattern. + * @param locale the locale. + * @return new non-lenient {@link CachingDateFormat} instance. + */ + private static CachingDateFormat newDateFormatWithLocale(String pattern, Locale locale) + { + CachingDateFormat formatter = new CachingDateFormat(pattern, locale); + formatter.setLenient(false); + return formatter; + } +} diff --git a/core/src/main/java/org/alfresco/util/Content.java b/core/src/main/java/org/alfresco/util/Content.java new file mode 100644 index 0000000000..d507d9c747 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/Content.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + + +/** + * Content + * + * @author dcaruana + */ +public interface Content +{ + /** + * Gets content as a string + * + * @return content as a string + * @throws IOException + */ + public String getContent() throws IOException; + + /** + * Gets the content mimetype + * + * @return mimetype + */ + public String getMimetype(); + + /** + * Gets the content encoding + * + * @return encoding + */ + public String getEncoding(); + + /** + * Gets the content length (in bytes) + * + * @return length + */ + public long getSize(); + + /** + * Gets the content input stream + * + * @return input stream + */ + public InputStream getInputStream(); + + /** + * Gets the content reader (which is sensitive to encoding) + * + * @return Reader + */ + public Reader getReader() throws IOException; +} diff --git a/core/src/main/java/org/alfresco/util/Convert.java b/core/src/main/java/org/alfresco/util/Convert.java new file mode 100644 index 0000000000..5074ff9691 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/Convert.java @@ -0,0 +1,809 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.alfresco.encoding.CharactersetFinder; +import org.alfresco.encoding.GuessEncodingCharsetFinder; +import org.alfresco.util.exec.RuntimeExec; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; + +/** + * Utility to convert text files. + *

+ * Check the usage options with the --help option. + *

+ * Here are some examples of how to use the main method: + *

    + *
  • + * --help
    + * Produce the help output. + *
  • + *
  • + * --dry-run --encoding=UTF-8 --line-ending=WINDOWS --match="(.java|.xml|.jsp|.properties)$" --ignore="(.svn|classes)" "w:\"
    + * Find all source (.java, .xml, .jsp and .properties) files in directory "w:\".
    + * List files and show which would change when converting to CR-LF (Windows) line endings.
    + * Where auto-detection of the file is ambiguous, assume UTF-8. + *
  • + *
  • + * --encoding=UTF-8 --line-ending=WINDOWS --match="(.java|.xml|.jsp|.properties)$" --ignore="(.svn|classes)" "w:\"
    + * Find all source (.java, .xml, .jsp and .properties) files in directory "w:\". Recurse into subdirectories.
    + * Convert files, where necessary, to have CR-LF (Windows) line endings.
    + * Where auto-detection of the file encoding is ambiguous, assume UTF-8.
    + * Backups (.bak) files will be created. + *
  • + *
  • + * --svn-update --no-backup --encoding=UTF-8 --line-ending=WINDOWS --match="(.java|.xml|.jsp|.properties)$" "w:\"
    + * Issue a 'svn status' command on directory "w:\" and match the regular expressions given to find files.
    + * Convert files, where necessary, to have CR-LF (Windows) line endings.
    + * Where auto-detection of the file encoding is ambiguous, assume UTF-8. Write out as UTF-8.
    + * No backups files will be created. + *
  • + *
+ * + * @author Derek Hulley + */ +public class Convert +{ + private static final String OPTION_HELP = "--help"; + private static final String OPTION_SVN_STATUS = "--svn-status"; + private static final String OPTION_MATCH = "--match="; + private static final String OPTION_IGNORE = "--ignore="; + private static final String OPTION_ENCODING= "--encoding="; + private static final String OPTION_LINE_ENDING = "--line-ending="; + private static final String OPTION_REPLACE_TABS= "--replace-tabs="; + private static final String OPTION_NO_RECURSE = "--no-recurse"; + private static final String OPTION_NO_BACKUP = "--no-backup"; + private static final String OPTION_DRY_RUN = "--dry-run"; + private static final String OPTION_VERBOSE = "--verbose"; + private static final String OPTION_QUIET = "--quiet"; + + private static final Set OPTIONS = new HashSet(13); + + static + { + OPTIONS.add(OPTION_HELP); + OPTIONS.add(OPTION_SVN_STATUS); + OPTIONS.add(OPTION_MATCH); + OPTIONS.add(OPTION_IGNORE); + OPTIONS.add(OPTION_ENCODING); + OPTIONS.add(OPTION_LINE_ENDING); + OPTIONS.add(OPTION_REPLACE_TABS); + OPTIONS.add(OPTION_NO_RECURSE); + OPTIONS.add(OPTION_NO_BACKUP); + OPTIONS.add(OPTION_DRY_RUN); + OPTIONS.add(OPTION_VERBOSE); + OPTIONS.add(OPTION_QUIET); + } + + /** + * @see GuessEncodingCharsetFinder + */ + private static final CharactersetFinder CHARACTER_ENCODING_FINDER = new GuessEncodingCharsetFinder(); + + private File startDir = null; + + private boolean svnStatus = false; + private boolean dryRun = false; + private Pattern matchPattern = null; + private Pattern ignorePattern = null; + private Charset charset = null; + private String lineEnding = null; + private Integer replaceTabs = null; + private boolean noRecurse = false; + private boolean noBackup = false; + private boolean verbose = false; + private boolean quiet = false; + + public static void main(String[] args) + { + if (args.length < 1) + { + printUsage(); + } + // Convert args to a list + List argList = new ArrayList(args.length); + List argListFixed = Arrays.asList(args); + argList.addAll(argListFixed); + // Extract all the options + Map optionValues = extractOptions(argList); + + // Check for help request + if (optionValues.containsKey(OPTION_HELP)) + { + printUsage(); + System.exit(0); + } + + // Check + if (argList.size() != 1) + { + printUsage(); + System.exit(1); + } + + // Get the directory to start in + File startDir = new File(argList.get(0)); + if (!startDir.exists() || !startDir.isDirectory()) + { + System.err.println("Convert: "); + System.err.println(" Unable to find directory: " + startDir); + System.err.flush(); + printUsage(); + System.exit(1); + } + + Convert convert = new Convert(optionValues, startDir); + convert.convert(); + } + + /** + * Private constructor for use by the main method. + */ + private Convert(Map optionValues, File startDir) + { + this.startDir = startDir; + + svnStatus = optionValues.containsKey(OPTION_SVN_STATUS); + dryRun = optionValues.containsKey(OPTION_DRY_RUN); + String match = optionValues.get(OPTION_MATCH); + String ignore = optionValues.get(OPTION_IGNORE); + String encoding = optionValues.get(OPTION_ENCODING); + lineEnding = optionValues.get(OPTION_LINE_ENDING); + noRecurse = optionValues.containsKey(OPTION_NO_RECURSE); + noBackup = optionValues.containsKey(OPTION_NO_BACKUP); + verbose = optionValues.containsKey(OPTION_VERBOSE); + quiet = optionValues.containsKey(OPTION_QUIET); + + // Check that the tab replacement count is correct + String replaceTabsStr = optionValues.get(OPTION_REPLACE_TABS); + if (replaceTabsStr != null) + { + try + { + replaceTabs = Integer.parseInt(replaceTabsStr); + } + catch (NumberFormatException e) + { + System.err.println("Convert: "); + System.err.println(" Unable to determine how many spaces to replace tabs with: " + replaceTabsStr); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + + // Check the match regex expressions + if (match == null) + { + match = ".*"; + } + try + { + matchPattern = Pattern.compile(match); + } + catch (Throwable e) + { + System.err.println("Convert: "); + System.err.println(" Unable to parse regular expression: " + match); + System.err.flush(); + printUsage(); + System.exit(1); + } + // Check the match regex expressions + if (ignore != null) + { + try + { + ignorePattern = Pattern.compile(ignore); + } + catch (Throwable e) + { + System.err.println("Convert: "); + System.err.println(" Unable to parse regular expression: " + ignore); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + // Check the encoding + if (encoding != null) + { + try + { + charset = Charset.forName(encoding); + } + catch (Throwable e) + { + System.err.println("Convert: "); + System.err.println(" Unknown encoding: " + encoding); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + + // Check line ending + if (lineEnding != null && !lineEnding.equals("WINDOWS") && !lineEnding.equals("UNIX")) + { + System.err.println("Convert: "); + System.err.println(" Line endings can be either WINDOWS or UNIX: " + lineEnding); + System.err.flush(); + printUsage(); + System.exit(1); + } + + // Check quiet/verbose match + if (verbose && quiet) + { + System.err.println("Convert: "); + System.err.println(" Cannot output in verbose and quiet mode."); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + + private void convert() + { + try + { + if (!quiet) + { + System.out.print("Converting files matching " + matchPattern); + System.out.print(ignorePattern == null ? "" : " but not " + ignorePattern); + System.out.println(dryRun ? " [DRY RUN]" : ""); + } + if (!svnStatus) + { + // Do a recursive pattern match + convertDir(startDir); + } + else + { + // Use SVN + convertSvn(startDir); + } + } + catch (Throwable e) + { + e.printStackTrace(); + System.err.flush(); + printUsage(); + System.exit(1); + } + finally + { + System.out.flush(); + } + } + + private void convertSvn(File currentDir) throws Throwable + { + RuntimeExec exec = new RuntimeExec(); + exec.setCommand(new String[] {"svn", "status", currentDir.toString()}); + ExecutionResult result = exec.execute(); + if (!result.getSuccess()) + { + System.out.println("svn status command failed:" + exec); + } + // Get the output + String dump = result.getStdOut(); + BufferedReader reader = null; + try + { + reader = new BufferedReader(new StringReader(dump)); + while (true) + { + String line = reader.readLine(); + if (line == null) + { + break; + } + // Only lines that start with "A" or "M" + if (!line.startsWith("A") && !line.startsWith("M")) + { + continue; + } + String filename = line.substring(7).trim(); + if (filename.length() < 1) + { + continue; + } + File file = new File(filename); + if (!file.exists()) + { + continue; + } + // We found one + convertFile(file); + } + } + finally + { + if (reader != null) + { + try { reader.close(); } catch (Throwable e) {} + } + } + } + + /** + * Recursive method to do the conversion work. + */ + private void convertDir(File currentDir) throws Throwable + { + // Get all children of the folder + File[] childFiles = currentDir.listFiles(); + for (File childFile : childFiles) + { + if (childFile.isDirectory()) + { + if (noRecurse) + { + // Don't enter the directory + continue; + } + // Recurse + convertDir(childFile); + } + else + { + convertFile(childFile); + } + } + } + + private void convertFile(File file) throws Throwable + { + // We have a file, but does the pattern match + String filePath = file.getAbsolutePath(); + if (matchPattern.matcher(filePath).find()) + { + // It matches, but must we ignore it? + if (ignorePattern != null && ignorePattern.matcher(filePath).find()) + { + // It is ignorable + return; + } + } + else + { + // It missed the primary positive match + return; + } + + // Ignore folders + if (file.isDirectory()) + { + return; + } + + if (file.length() > (1024 * 1024)) // 1MB. TODO: Make an option + { + System.out.println(" (Too big)"); + } + File backupFile = null; + try + { + // Read the source file into memory + byte[] fileBytes = readFileIntoMemory(file); + // Calculate the MD5 for the file + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(fileBytes); + byte[] fileMd5 = md5.digest(); + // Guess the charset now + Charset fileCharset = guessCharset(fileBytes, charset); + + byte[] convertedBytes = fileBytes; + byte[] sourceBytes = fileBytes; + byte[] convertedMd5 = fileMd5; + + // Convert the tabs + if (replaceTabs != null) + { + sourceBytes = convertTabs(sourceBytes, fileCharset, replaceTabs); + } + // Convert the charset + if (charset != null) + { + // TODO + // sourceBytes = convert ... + } + // Convert the line endings + if (lineEnding != null) + { + convertedBytes = convertLineEndings(sourceBytes, fileCharset, lineEnding); + } + boolean changed = false; + if (convertedBytes == fileBytes) + { + // Nothing done + } + else + { + // Recalculate the converted MD5 + md5 = MessageDigest.getInstance("MD5"); + md5.update(convertedBytes); + convertedMd5 = md5.digest(); + // Now compare + changed = !Arrays.equals(fileMd5, convertedMd5); + } + // Make a backup of the file if it changed + if (changed) + { + if (!noBackup && !dryRun) + { + String backupFilename = file.getAbsolutePath() + ".bak"; + File backupFilePre = new File(backupFilename); + // Write the original file contents to the backup file + writeMemoryIntoFile(fileBytes, backupFilePre); + // That being successful, we can now reference it + backupFile = backupFilePre; + } + if (!quiet) + { + System.out.println(" " + file + " "); + } + // Only write to the file if this is not a dry run + if (!dryRun) + { + // Now write the converted buffer to the original file + writeMemoryIntoFile(convertedBytes, file); + } + } + else + { + if (verbose) + { + System.out.println(" " + file + " "); + } + } + } + catch (Throwable e) + { + if (backupFile != null) + { + try + { + file.delete(); + backupFile.renameTo(file); + } + catch (Throwable ee) + { + System.err.println("Failed to restore backup file: " + backupFile); + ee.printStackTrace(); + } + } + throw e; + } + finally + { + if (!quiet || verbose) + { + System.out.flush(); + } + } + } + + /** + * Brute force guessing by doing charset conversions.
+ */ + private static Charset guessCharset(byte[] bytes, Charset charset) throws Exception + { + Charset guessedCharset = CHARACTER_ENCODING_FINDER.detectCharset(bytes); + if (guessedCharset == null) + { + return charset; + } + else + { + return guessedCharset; + } + } + + private static byte[] convertTabs(byte[] bytes, Charset charset, int replaceTabs) throws Exception + { + // The tab character + char tab = '\t'; + char space = ' '; + + // The output + StringBuilder sb = new StringBuilder(bytes.length); + + String charsetName = charset.name(); + // Using the charset, convert to a string + String str = new String(bytes, charsetName); + char[] chars = str.toCharArray(); + for (char c : chars) + { + if (c == tab) + { + // Replace the tab + for (int i = 0; i < replaceTabs; i++) + { + sb.append(space); + } + } + else + { + sb.append(c); + } + } + // Done + return sb.toString().getBytes(charsetName); + } + + private static final String EOF_CHECK = "--EOF-CHECK--"; + private static byte[] convertLineEndings(byte[] bytes, Charset charset, String lineEnding) throws Exception + { + String charsetName = charset.name(); + // Using the charset, convert to a string + BufferedReader reader = null; + StringBuilder sb = new StringBuilder(bytes.length); + try + { + String str = new String(bytes, charsetName); + str = str + EOF_CHECK; + reader = new BufferedReader(new StringReader(str)); + String line = reader.readLine(); + while (line != null) + { + // Ignore the newline check + boolean addLine = true; + if (line.equals(EOF_CHECK)) + { + break; + } + else if (line.endsWith(EOF_CHECK)) + { + int index = line.indexOf(EOF_CHECK); + line = line.substring(0, index); + addLine = false; + } + // Write the line back out + sb.append(line); + if (!addLine) + { + // No newline + } + else if (lineEnding.equalsIgnoreCase("UNIX")) + { + sb.append("\n"); + } + else + { + sb.append("\r\n"); + } + line = reader.readLine(); + } + } + finally + { + if (reader != null) + { + try { reader.close(); } catch (Throwable e) {} + } + } + // Done + return sb.toString().getBytes(charsetName); + } + + private static byte[] readFileIntoMemory(File file) throws Exception + { + InputStream is = null; + OutputStream os = null; + try + { + is = new BufferedInputStream(new FileInputStream(file)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(8192); + os = new BufferedOutputStream(baos); + byte[] buffer = new byte[1024]; + while (true) + { + int count = is.read(buffer); + if (count < 0) + { + break; + } + os.write(buffer, 0, count); + } + os.flush(); + byte[] memory = baos.toByteArray(); + return memory; + } + finally + { + if (is != null) + { + try { is.close(); } catch (Throwable e) {} + } + if (os != null) + { + try { os.close(); } catch (Throwable e) {} + } + } + } + + private static void writeMemoryIntoFile(byte[] bytes, File file) throws Exception + { + InputStream is = null; + OutputStream os = null; + try + { + is = new ByteArrayInputStream(bytes); + os = new BufferedOutputStream(new FileOutputStream(file)); + byte[] buffer = new byte[1024]; + while (true) + { + int count = is.read(buffer); + if (count < 0) + { + break; + } + os.write(buffer, 0, count); + } + os.flush(); + } + finally + { + if (is != null) + { + try { is.close(); } catch (Throwable e) {} + } + if (os != null) + { + try { os.close(); } catch (Throwable e) {} + } + } + } + + /** + * Extract all the options from the list of arguments. + * @param args the program arguments. This list will be modified. + * @return Returns a map of arguments and their values. Where the arguments have + * no values, an empty string is returned. + */ + private static Map extractOptions(List args) + { + Map optionValues = new HashMap(13); + // Iterate until we find a non-option + Iterator iterator = args.iterator(); + while (iterator.hasNext()) + { + String arg = iterator.next(); + boolean foundOption = false; + for (String option : OPTIONS) + { + if (!arg.startsWith(option)) + { + // It is a non-option + continue; + } + foundOption = true; + // We can remove the argument + iterator.remove(); + // Check if the option needs a value + if (option.endsWith("=")) + { + // Extract the option value + int index = arg.indexOf("="); + if (index == arg.length() - 1) + { + // There is nothing there, so we don't keep a value + } + else + { + String value = arg.substring(index + 1); + optionValues.put(option, value); + } + } + else + { + // Add the value to the map + String value = ""; + optionValues.put(option, value); + } + } + if (!foundOption) + { + // It is not an option + break; + } + } + // Done + return optionValues; + } + + public static void printUsage() + { + StringBuilder sb = new StringBuilder(1024); + sb.append("Usage: \n") + .append(" Convert [options] directory \n") + .append(" \n") + .append(" options: \n") + .append(" --help \n") + .append(" Print this help. \n") + .append(" --svn-status \n") + .append(" Execute a 'svn status' command against the directory and use the output for the file list. \n") + .append(" --match=?: \n") + .append(" A regular expression that all filenames must match. \n") + .append(" This argument can be escaped with double quotes, ie.g \"[a-zA-z0-9 ]\". \n") + .append(" The regular expression will be applied to the full path of the file. \n") + .append(" Name seperators will be '/' on Unix and ''\\'' on Windows systems. \n") + .append(" The default is \"--match=.*\", or match all files. \n") + .append(" --ignore=?: \n") + .append(" A regular expression that all filenames must not match. \n") + .append(" This argument can be escaped with double quotes, ie.g \"[a-zA-z0-9 ]\". \n") + .append(" The regular expression will be applied to the full path of the file. \n") + .append(" Name seperators will be '/' on Unix and ''\\'' on Windows systems. \n") + .append(" This option is not present by default. \n") + .append(" --encoding=? \n") + .append(" If not specified, the encoding of the files is left unchanged. \n") + .append(" Typical values would be UTF-8, UTF-16 or any java-recognized encoding string. \n") + .append(" --line-ending=? \n") + .append(" This can either be WINDOWS or UNIX. \n") + .append(" If not set, the line ending style is left unchanged. \n") + .append(" --replace-tabs=? \n") + .append(" Specify the number of spaces to insert in place of a tab. \n") + .append(" --no-recurse \n") + .append(" Do not recurse into subdirectories. \n") + .append(" --no-backup \n") + .append(" The default is to make a backup of all files prior to modification. \n") + .append(" With this option, no backups are made. \n") + .append(" --dry-run \n") + .append(" Do not modify or backup any files. \n") + .append(" No filesystem modifications are made. \n") + .append(" --verbose \n") + .append(" Dump all files checked to std.out. \n") + .append(" --quiet \n") + .append(" Don't dump anything to std.out. \n") + .append(" directory: \n") + .append(" The directory to start searching in. \n") + .append(" If the directory has spaces in it, then escape it with double quotes, e.g. \"C:\\Program Files\" \n") + .append(" \n") + .append("Details of the modifications being made are written to std.out. \n") + .append("Errors are written to std.err. \n") + .append("When used without any options, this program will behave like a FIND. \n"); + System.out.println(sb); + System.out.flush(); + } +} diff --git a/core/src/main/java/org/alfresco/util/Debug.java b/core/src/main/java/org/alfresco/util/Debug.java new file mode 100644 index 0000000000..0d3ddf834b --- /dev/null +++ b/core/src/main/java/org/alfresco/util/Debug.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.net.URL; + +/** + * Class containing debugging utility methods + * + * @author gavinc + */ +public class Debug +{ + /** + * Returns the location of the file that will be loaded for the given class name + * + * @param className The class to load + * @return The location of the file that will be loaded + * @throws ClassNotFoundException + */ + public static String whichClass(String className) throws ClassNotFoundException + { + String path = className; + + // prepare the resource path + if (path.startsWith("/") == false) + { + path = "/" + path; + } + path = path.replace('.', '/'); + path = path + ".class"; + + // get the location + URL url = Debug.class.getResource(path); + if (url == null) + { + throw new ClassNotFoundException(className); + } + + // format the result + String location = url.toExternalForm(); + if (location.startsWith("jar")) + { + location = location.substring(10, location.lastIndexOf("!")); + } + else if (location.startsWith("file:")) + { + location = location.substring(6); + } + + return location; + } + + /** + * Returns the class loader that will load the given class name + * + * @param className The class to load + * @return The class loader the class will be loaded in + * @throws ClassNotFoundException + */ + public static String whichClassLoader(String className) throws ClassNotFoundException + { + String result = "Could not determine class loader for " + className; + + Class clazz = Class.forName(className); + ClassLoader loader = clazz.getClassLoader(); + + if (loader != null) + { + result = clazz.getClassLoader().toString(); + } + + return result; + } + + /** + * Returns the class loader hierarchy that will load the given class name + * + * @param className The class to load + * @return The hierarchy of class loaders used to load the class + * @throws ClassNotFoundException + */ + public static String whichClassLoaderHierarchy(String className) throws ClassNotFoundException + { + StringBuffer buffer = new StringBuffer(); + Class clazz = Class.forName(className); + ClassLoader loader = clazz.getClassLoader(); + if (loader != null) + { + buffer.append(loader.toString()); + + ClassLoader parent = loader.getParent(); + while (parent != null) + { + buffer.append("\n-> ").append(parent.toString()); + parent = parent.getParent(); + } + } + else + { + buffer.append("Could not determine class loader for " + className); + } + + return buffer.toString(); + } +} diff --git a/core/src/main/java/org/alfresco/util/Deleter.java b/core/src/main/java/org/alfresco/util/Deleter.java new file mode 100644 index 0000000000..04778260b1 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/Deleter.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Utility to delete a file or directory recursively. + * @author britt + */ +public class Deleter +{ + private static final Log log = LogFactory.getLog(Deleter.class); + + /** + * Delete by path. + * @param path + */ + public static void Delete(String path) + { + File toDelete = new File(path); + Delete(toDelete); + } + + /** + * Delete by File. + * @param toDelete + */ + public static void Delete(File toDelete) + { + if (toDelete.isDirectory()) + { + File[] listing = toDelete.listFiles(); + for (File file : listing) + { + Delete(file); + } + } + toDelete.delete(); + } + + + /** + * Recursively deletes the parents of the specified file stopping when rootDir is reached. + * The file itself must have been deleted before calling this method - since only empty + * directories can be deleted. + *

+ * For example: deleteEmptyParents(new File("/tmp/a/b/c/d.txt"), "/tmp/a") + *

+ * Will delete directories c and b assuming that they are both empty. It will leave /tmp/a even if it is + * empty as this is the rootDir + * + * @param file The path of the file whose parent directories should be deleted. + * @param rootDir Top level directory where deletion should stop. Must be the canonical path + * to ensure correct comparisons. + */ + public static void deleteEmptyParents(File file, String rootDir) + { + File parent = file.getParentFile(); + boolean deleted = false; + do + { + try + { + if (parent.isDirectory() && !parent.getCanonicalPath().equals(rootDir)) + { + // Only an empty directory will successfully be deleted. + deleted = parent.delete(); + } + } + catch (IOException error) + { + log.error("Unable to construct canonical path for " + parent.getAbsolutePath()); + break; + } + + parent = parent.getParentFile(); + } + while(deleted); + } + + /** + * Same behaviour as for {@link Deleter#deleteEmptyParents(File, String)} but with the + * rootDir parameter specified as a {@link java.io.File} object. + * + * @see Deleter#deleteEmptyParents(File, String) + * @param file + * @param rootDir + */ + public static void deleteEmptyParents(File file, File rootDir) + { + try + { + deleteEmptyParents(file, rootDir.getCanonicalPath()); + } + catch (IOException e) + { + String msg = "Unable to convert rootDir to canonical form [rootDir=" + rootDir + "]"; + throw new RuntimeException(msg, e); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/DynamicallySizedThreadPoolExecutor.java b/core/src/main/java/org/alfresco/util/DynamicallySizedThreadPoolExecutor.java new file mode 100644 index 0000000000..91a5a513c6 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/DynamicallySizedThreadPoolExecutor.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This is an instance of {@link java.util.concurrent.ThreadPoolExecutor} which + * behaves how one would expect it to, even when faced with an unlimited + * queue. Unlike the default {@link java.util.concurrent.ThreadPoolExecutor}, it + * will add new Threads up to {@link #setMaximumPoolSize(int) maximumPoolSize} + * when there is lots of pending work, rather than only when the queue is full + * (which it often never will be, especially for unlimited queues) + * + * @author Nick Burch + */ +public class DynamicallySizedThreadPoolExecutor extends ThreadPoolExecutor +{ + private static Log logger = LogFactory.getLog(DynamicallySizedThreadPoolExecutor.class); + + private final ReentrantLock lock = new ReentrantLock(); + private int realCorePoolSize; + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, RejectedExecutionHandler handler) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); + this.realCorePoolSize = corePoolSize; + } + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + this.realCorePoolSize = corePoolSize; + } + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, ThreadFactory threadFactory) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + this.realCorePoolSize = corePoolSize; + } + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + this.realCorePoolSize = corePoolSize; + } + + @Override + public void setCorePoolSize(int corePoolSize) + { + this.realCorePoolSize = corePoolSize; + super.setCorePoolSize(corePoolSize); + } + + @Override + public void execute(Runnable command) + { + // Do we want to add another thread? + int threadCount = getPoolSize(); + if(logger.isDebugEnabled()) + { + logger.debug("Current pool size is " + threadCount + ", real core=" + realCorePoolSize + + ", current core=" + getCorePoolSize() + ", max=" + getMaximumPoolSize()); + } + + if(threadCount < getMaximumPoolSize()) + { + // We're not yet at the full thread count + + // Does the queue size warrant adding one? + // (If there are more than the maximum pool size of jobs pending, + // it's time to add another thread) + int queueSize = getQueue().size() + 1;// New job not yet added + if(queueSize >= getMaximumPoolSize()) + { + lock.lock(); + int currentCoreSize = getCorePoolSize(); + if(currentCoreSize < getMaximumPoolSize()) + { + super.setCorePoolSize(currentCoreSize+1); + + if(logger.isInfoEnabled()) + { + logger.info("Increased pool size to " + getCorePoolSize() + " from " + + currentCoreSize + " due to queue size of " + queueSize); + } + } + lock.unlock(); + } + } + + // Now run the actual work + super.execute(command); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) + { + // If the queue is looking empty, allow the pool to + // get rid of idle threads when it wants to + int threadCount = getPoolSize(); + if(threadCount == getMaximumPoolSize() && threadCount > realCorePoolSize) + { + int queueSize = getQueue().size(); + int currentCoreSize = getCorePoolSize(); + if(queueSize < 2 && currentCoreSize > realCorePoolSize) + { + // Almost out of work, allow the pool to reduce threads when + // required. Double checks the sizing inside a lock to avoid + // race conditions taking us below the real core size. + lock.lock(); + currentCoreSize = getCorePoolSize(); + if(currentCoreSize > realCorePoolSize) + { + super.setCorePoolSize(currentCoreSize-1); + + if(logger.isInfoEnabled()) + { + logger.info("Decreased pool size to " + getCorePoolSize() + " from " + + currentCoreSize + " (real core size is " + realCorePoolSize + + ") due to queue size of " + queueSize); + } + } + lock.unlock(); + } + } + } +} diff --git a/core/src/main/java/org/alfresco/util/EqualsHelper.java b/core/src/main/java/org/alfresco/util/EqualsHelper.java new file mode 100644 index 0000000000..7b7c4cc8a1 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/EqualsHelper.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Utility class providing helper methods for various types of equals functionality + * + * @author Derek Hulley + */ +@AlfrescoPublicApi +public class EqualsHelper +{ + /** + * Performs an equality check left.equals(right) after checking for null values + * + * @param left the Object appearing in the left side of an equals statement + * @param right the Object appearing in the right side of an equals statement + * @return Return true or false even if one or both of the objects are null + */ + public static boolean nullSafeEquals(Object left, Object right) + { + return (left == right) || (left != null && right != null && left.equals(right)); + } + + /** + * Performs an case-sensitive or case-insensitive equality check after checking for null values + * @param ignoreCase true to ignore case + */ + public static boolean nullSafeEquals(String left, String right, boolean ignoreCase) + { + if (ignoreCase) + { + return (left == right) || (left != null && right != null && left.equalsIgnoreCase(right)); + } + else + { + return (left == right) || (left != null && right != null && left.equals(right)); + } + } + + private static final int BUFFER_SIZE = 1024; + /** + * Performs a byte-level comparison between two streams. + * + * @param left the left stream. This is closed at the end of the operation. + * @param right an right stream. This is closed at the end of the operation. + * @return Returns true if the streams are identical to the last byte + */ + public static boolean binaryStreamEquals(InputStream left, InputStream right) throws IOException + { + try + { + if (left == right) + { + // The same stream! This is pretty pointless, but they are equal, nevertheless. + return true; + } + + byte[] leftBuffer = new byte[BUFFER_SIZE]; + byte[] rightBuffer = new byte[BUFFER_SIZE]; + while (true) + { + int leftReadCount = left.read(leftBuffer); + int rightReadCount = right.read(rightBuffer); + if (leftReadCount != rightReadCount) + { + // One stream ended before the other + return false; + } + else if (leftReadCount == -1) + { + // Both streams ended without any differences found + return true; + } + for (int i = 0; i < leftReadCount; i++) + { + if (leftBuffer[i] != rightBuffer[i]) + { + // We found a byte difference + return false; + } + } + } + // The only exits with 'return' statements, so there is no need for any code here + } + finally + { + try { left.close(); } catch (Throwable e) {} + try { right.close(); } catch (Throwable e) {} + } + } + + /** + * Compare two maps and generate a difference report between the actual and expected values. + * This method is particularly useful during unit tests as the result (if not null) + * can be appended to a failure message. + * + * @param actual the map in hand + * @param expected the map expected + * @return Returns a difference report or null if there were no + * differences. The message starts with a new line and is neatly + * formatted. + */ + public static String getMapDifferenceReport(Map actual, Map expected) + { + Map copyResult = new HashMap(actual); + + boolean failure = false; + + StringBuilder sb = new StringBuilder(1024); + sb.append("\nValues that don't match the expected values: "); + for (Map.Entry entry : expected.entrySet()) + { + Object key = entry.getKey(); + Object expectedValue = entry.getValue(); + Object resultValue = actual.get(key); + if (!EqualsHelper.nullSafeEquals(resultValue, expectedValue)) + { + sb.append("\n") + .append(" Key: ").append(key).append("\n") + .append(" Result: ").append(resultValue).append("\n") + .append(" Expected: ").append(expectedValue); + failure = true; + } + copyResult.remove(key); + } + sb.append("\nValues that are present but should not be: "); + for (Map.Entry entry : copyResult.entrySet()) + { + Object key = entry.getKey(); + Object resultValue = entry.getValue(); + sb.append("\n") + .append(" Key: ").append(key).append("\n") + .append(" Result: ").append(resultValue); + failure = true; + } + if (failure) + { + return sb.toString(); + } + else + { + return null; + } + } + + /** + * Enumeration for results returned by {@link EqualsHelper#getMapComparison(Map, Map) map comparisons}. + * + * @author Derek Hulley + * @since 3.3 + */ + public static enum MapValueComparison + { + /** The key was only present in the left map */ + LEFT_ONLY, + /** The key was only present in the right map */ + RIGHT_ONLY, + /** The key was present in both maps and the values were equal */ + EQUAL, + /** The key was present in both maps but not equal */ + NOT_EQUAL + } + + /** + * Compare two maps. + *

+ * The return codes that accompany the keys are: + *

    + *
  • {@link MapValueComparison#LEFT_ONLY}
  • + *
  • {@link MapValueComparison#RIGHT_ONLY}
  • + *
  • {@link MapValueComparison#EQUAL}
  • + *
  • {@link MapValueComparison#NOT_EQUAL}
  • + *
+ * + * @param the map key type + * @param the map value type + * @param left the left side of the comparison + * @param right the right side of the comparison + * @return Returns a map whose keys are a union of the two maps' keys, along with + * the value comparison result + */ + public static Map getMapComparison(Map left, Map right) + { + Set keys = new HashSet(left.size() + right.size()); + keys.addAll(left.keySet()); + keys.addAll(right.keySet()); + + Map diff = new HashMap(left.size() + right.size()); + + // Iterate over the keys and do the comparisons + for (K key : keys) + { + boolean leftHasKey = left.containsKey(key); + boolean rightHasKey = right.containsKey(key); + V leftValue = left.get(key); + V rightValue = right.get(key); + if (leftHasKey) + { + if (!rightHasKey) + { + diff.put(key, MapValueComparison.LEFT_ONLY); + } + else if (EqualsHelper.nullSafeEquals(leftValue, rightValue)) + { + diff.put(key, MapValueComparison.EQUAL); + } + else + { + diff.put(key, MapValueComparison.NOT_EQUAL); + } + } + else if (rightHasKey) + { + if (!leftHasKey) + { + diff.put(key, MapValueComparison.RIGHT_ONLY); + } + else if (EqualsHelper.nullSafeEquals(leftValue, rightValue)) + { + diff.put(key, MapValueComparison.EQUAL); + } + else + { + diff.put(key, MapValueComparison.NOT_EQUAL); + } + } + else + { + // How is it here? + } + } + + return diff; + } +} diff --git a/core/src/main/java/org/alfresco/util/ExpiringValueCache.java b/core/src/main/java/org/alfresco/util/ExpiringValueCache.java new file mode 100644 index 0000000000..d888628748 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/ExpiringValueCache.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.Serializable; + +/** + * Simple cache of a single Object value. + *

+ * The object placed in the cache will automatically be discarded after a timeout value. + * + * @author Kevin Roast + */ +public class ExpiringValueCache implements Serializable +{ + private static final long serialVersionUID = 1036233352030777619L; + + // default is to discard cached object after 1 minute + private final static long TIMEOUT_DEFAULT = 1000L*60L; + + private long timeout = TIMEOUT_DEFAULT; + private long snapshot = 0; + private T value; + + /** + * Default constructor. + * + * Uses the default timeout of 1 minute. + */ + public ExpiringValueCache() + { + } + + /** + * Constructor + * + * @param timeout Timeout in milliseconds before cached value is discarded + */ + public ExpiringValueCache(long timeout) + { + this.timeout = timeout; + } + + /** + * Put a value into the cache. The item will be return from the associated get() method + * until the timeout expires then null will be returned. + * + * @param value The object to store in the cache + */ + public void put(T value) + { + this.value = value; + this.snapshot = System.currentTimeMillis(); + } + + /** + * Get the cached object. The set item will be returned until it expires, then null will be returned. + * + * @return cached object or null if not set or expired. + */ + public T get() + { + if (snapshot + timeout < System.currentTimeMillis()) + { + this.value = null; + } + return this.value; + } + + /** + * Clear the cache value + */ + public void clear() + { + this.value = null; + } +} diff --git a/core/src/main/java/org/alfresco/util/FileFilterMode.java b/core/src/main/java/org/alfresco/util/FileFilterMode.java new file mode 100644 index 0000000000..3d7e488f91 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/FileFilterMode.java @@ -0,0 +1,115 @@ +package org.alfresco.util; + +public class FileFilterMode +{ + /** + * Clients for which specific hiding/visibility behaviour may be requested. + * Do not remove or change the order of + */ + public static enum Client + { + cifs, imap, webdav, nfs, script, webclient, ftp, cmis, admin; + + /** + * @deprecated Use {@link Client#valueOf(String)} + */ + @Deprecated + public static Client getClient(String clientStr) + { + if(clientStr.equals("cifs")) + { + return cifs; + } + else if(clientStr.equals("imap")) + { + return imap; + } + else if(clientStr.equals("webdav")) + { + return webdav; + } + else if(clientStr.equals("nfs")) + { + return nfs; + } + else if(clientStr.equals("ftp")) + { + return ftp; + } + else if(clientStr.equals("script")) + { + return script; + } + else if(clientStr.equals("webclient")) + { + return webclient; + } + else if(clientStr.equals("cmis")) + { + return cmis; + } + else if(clientStr.equals("admin")) + { + return admin; + } + else + { + throw new IllegalArgumentException(); + } + } + }; + + public static enum Mode + { + BASIC, ENHANCED; + }; + + private static ThreadLocal client = new ThreadLocal() + { + protected Client initialValue() { + return null; + } + }; + + public static void clearClient() + { + client.set(null); + } + + public static Client setClient(Client newClient) + { + Client oldClient = client.get(); + client.set(newClient); + + return oldClient; + } + + public static Mode getMode() + { + Client client = getClient(); + if(client == null) + { + return Mode.BASIC; + } + else + { + switch(client) + { + case cifs : + case nfs : + case ftp : + case webdav : + case cmis : + case admin : + return Mode.ENHANCED; + default: + return Mode.BASIC; + } + } + } + + public static Client getClient() + { + return client.get(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/GUID.java b/core/src/main/java/org/alfresco/util/GUID.java new file mode 100644 index 0000000000..227e928a5e --- /dev/null +++ b/core/src/main/java/org/alfresco/util/GUID.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.security.SecureRandom; +import java.util.Random; + +import org.safehaus.uuid.UUIDGenerator; +import org.alfresco.api.AlfrescoPublicApi; + +/** + * A wrapper class to serve up GUIDs + * + * @author kevinr + */ +@AlfrescoPublicApi +public final class GUID +{ + /** + * Private Constructor for GUID. + */ + private GUID() + { + } + +// protected static final char[] s_values = +// { +// '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', +// 'f' +// }; + + private static final SecureRandom[] SECURE_RANDOM_POOL = initSecureRandomArray(); + private static final int SECURE_RANDOM_POOL_MAX_ITEMS = 256; + private static final Random RANDOM = new Random(); + + + private static SecureRandom[] initSecureRandomArray() + { + SecureRandom[] array = new SecureRandom[SECURE_RANDOM_POOL_MAX_ITEMS]; + for (int i = 0; i < SECURE_RANDOM_POOL_MAX_ITEMS; i++) + { + array[i] = new SecureRandom(); + } + return array; + } + + /** + * Generates and returns a new GUID as a string based on a SecureRandom pool in other to avoid + * thread blocking in concurrent calls. + * + * @return String GUID + */ + public static String generate() + { + int randomInt = RANDOM.nextInt(SECURE_RANDOM_POOL_MAX_ITEMS); + return UUIDGenerator.getInstance().generateRandomBasedUUID(SECURE_RANDOM_POOL[randomInt]).toString(); + } + +// == Not sure if we need this functionality again (derekh) == +// +// /** +// * Convert a string with a guid inside into a byte[16] array +// * +// * @param str - the guid +// * @return - byte[16] containing the GUID +// * @throws InvalidGuidFormatException +// */ +// public static byte[] parseFromString(String str) throws InvalidGuidFormatException +// { +// byte[] data = new byte[16]; +// int dataPos = 0; +// +// byte bVal; +// int value = 0; +// int pos = 0; +// +// for(int i = 0; i < str.length(); i++) +// { +// char thisChar = str.charAt(i); +// +// int idx = 0; +// +// if(thisChar >= '0' && thisChar <= '9') +// { +// idx = thisChar - '0'; +// pos++; +// } +// else if(thisChar >= 'a' && thisChar <= 'f') +// { +// idx = thisChar - 'a' + 10; +// pos++; +// } +// else if(thisChar >= 'a' && thisChar <= 'f') +// { +// idx = thisChar - 'A' + 10; +// pos++; +// } +// else if(thisChar == '-' || thisChar == '{' || thisChar == '}') +// { +// // Doesn't matter +// } +// else +// { +// throw new InvalidGuidFormatException(); +// } +// +// try +// { +// if(pos == 1) +// value = idx; +// else if(pos == 2) +// { +// value = (value * 16) + idx; +// +// byte b = (byte) value; +// data[dataPos++] = b; +// +// pos = 0; +// } +// } +// catch(RuntimeException e) +// { +// // May occur if we go off the end of the data index +// throw new InvalidGuidFormatException(); +// } +// } +// +// return data; +// } +// +// /** +// * Convert a byte[16] containing a guid to a string representation +// * +// * @param data - the data +// * @return - the string +// */ +// public static String convertToString(byte[] data) +// { +// char[] output = new char[36]; +// int cPos = 0; +// +// for(int i = 0; i < 16; i++) +// { +// int v = data[i]; +// +// int lowVal = v & 0x000F; +// int hiVal = (v & 0x00F0) >> 4; +// +// output[cPos++] = s_values[hiVal]; +// output[cPos++] = s_values[lowVal]; +// +// if(cPos == 8 || cPos == 13 || cPos == 18 || cPos == 23) +// output[cPos++] = '-'; +// } +// +// return new String(output); +// } +} diff --git a/core/src/main/java/org/alfresco/util/IPUtils.java b/core/src/main/java/org/alfresco/util/IPUtils.java new file mode 100644 index 0000000000..1ddb9c928e --- /dev/null +++ b/core/src/main/java/org/alfresco/util/IPUtils.java @@ -0,0 +1,26 @@ +package org.alfresco.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class IPUtils +{ + /** + * Returns the "real" IP address represented by ipAddress. If ipAddress is a loopback + * address it is converted into the host's underlying IP address + * + * @param ipAddress String + * @return String + * @throws UnknownHostException + */ + public static String getRealIPAddress(String ipAddress) throws UnknownHostException + { + if(ipAddress.equals("localhost") || ipAddress.equals("127.0.0.1")) + { + // make sure we are using a "real" IP address + ipAddress = InetAddress.getLocalHost().getHostAddress(); + } + + return ipAddress; + } +} diff --git a/core/src/main/java/org/alfresco/util/ISO8601DateFormat.java b/core/src/main/java/org/alfresco/util/ISO8601DateFormat.java new file mode 100644 index 0000000000..5b8da9b283 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/ISO8601DateFormat.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.joda.time.*; +import org.joda.time.chrono.GJChronology; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + + +/** + * Formatting support for ISO 8601 dates + *

+ *    sYYYY-MM-DDThh:mm:ss.sssTZD
+ * 
+ * where: + *
    + *
  • sYYYY Four-digit year with optional leading positive (+) or negative (-) sign. + * A negative sign indicates a year BCE. The absence of a sign or the presence of a + * positive sign indicates a year CE (for example, -0055 would indicate the year 55 BCE, + * while +1969 and 1969 indicate the year 1969 CE).
  • + *
  • MM Two-digit month (01 = January, etc.)
  • + *
  • DD Two-digit day of month (01 through 31)
  • + *
  • hh Two digits of hour (00 through 23)
  • + *
  • mm Two digits of minute (00 through 59)
  • + *
  • ss.sss Seconds, to three decimal places (00.000 through 59.999)
  • + *
  • TZD Time zone designator (either Z for Zulu, i.e. UTC, or +hh:mm or -hh:mm, i.e. an offset from UTC)
  • + *
+ */ +@AlfrescoPublicApi +public class ISO8601DateFormat +{ + private static ThreadLocal> calendarThreadLocal = new ThreadLocal>(); + public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); + + /** + * Get a calendar object from cache. + * @param timezone timezone object to indicate the timezone to be used by the returned calendar object + * @return calendar object from cache or newly created (if cache is empty) + */ + public static Calendar getCalendar(TimeZone timezone) + { + if (calendarThreadLocal.get() == null) + { + calendarThreadLocal.set(new HashMap()); + } + + Calendar calendar = calendarThreadLocal.get().get(timezone); + if (calendar == null) + { + calendar = new GregorianCalendar(timezone); + calendarThreadLocal.get().put(timezone, calendar); + } + + return calendar; + } + + /** + * Get a calendar object from cache for the system default timezone. + * @return calendar object from cache or newly created (if cache is empty) + */ + public static Calendar getCalendar() + { + return getCalendar(TimeZone.getDefault()); + } + + /** + * Format date into ISO format (UCT0 / Zulu) + * + * @param isoDate the date to format + * @return the ISO Zulu timezone formatted string + */ + public static String format(Date isoDate) + { + Calendar calendar = getCalendar(UTC_TIMEZONE); + calendar.setTime(isoDate); + + // MNT-9790 + // org.joda.time.DateTime.DateTime take away some minutes from date before 1848 year at formatting. + // This behavior connected with acceptance of time zones based + // on the Greenwich meridian (it was in Great Britain, year 1848). + if (calendar.get(Calendar.YEAR) > 1847) + { + DateTime dt = new DateTime(isoDate, DateTimeZone.UTC); + return dt.toString(); + } + else + { + int val = 0; + StringBuilder formatted = new StringBuilder(28); + formatted.append(calendar.get(Calendar.YEAR)); + formatted.append('-'); + val = calendar.get(Calendar.MONTH) + 1; + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append('-'); + val = calendar.get(Calendar.DAY_OF_MONTH); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append('T'); + val = calendar.get(Calendar.HOUR_OF_DAY); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append(':'); + val = calendar.get(Calendar.MINUTE); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append(':'); + val = calendar.get(Calendar.SECOND); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append('.'); + val = calendar.get(Calendar.MILLISECOND); + if (val < 10) + { + formatted.append(val < 10 ? ("00" + val) : val); + } + else if (val >= 10 && val < 100) + { + formatted.append(val < 10 ? ("0" + val) : val); + } + else + { + formatted.append(val); + } + + // ALF-21965 We are now confident we are using UTC timezone with zero offset + formatted.append('Z'); + + return formatted.toString(); + } + } + + /** + * Normalise isoDate time to Zulu(UTC0) time-zone, removing any UTC offset. + * @param isoDate + * @return the ISO Zulu timezone formatted string + * e.g 2011-02-04T17:13:14.000+01:00 -> 2011-02-04T16:13:14.000Z + */ + public static String formatToZulu(String isoDate) + { + try + { + DateTime dt = new DateTime(isoDate, DateTimeZone.UTC); + return dt.toString(); + } catch (IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + } + + /** + * Parse date from ISO formatted string. + * The ISO8601 date must include TimeZone offset information + * + * @param isoDate ISO string to parse + * @return the date + * @throws AlfrescoRuntimeException if the parse failed + */ + public static Date parse(String isoDate) + { + return parseInternal(isoDate, null); + } + + /** + * Parse date from ISO formatted string, with an + * explicit timezone specified + * + * @param isoDate ISO string to parse + * @param timezone The TimeZone the date is in + * @return the date + * @throws AlfrescoRuntimeException if the parse failed + */ + public static Date parse(String isoDate, TimeZone timezone) + { + return parseInternal(isoDate, timezone); + } + + /** + * Parse date from ISO formatted string, either in the specified + * TimeZone, or with TimeZone information taken from the date + * + * @param isoDate ISO string to parse + * @param timezone The time zone, null means default time zone + * @return the date + * @throws AlfrescoRuntimeException if the parse failed + */ + public static Date parseInternal(String isoDate, TimeZone timezone) + { + try + { + // null time-zone defaults to the local time-zone + DateTimeZone dtz = DateTimeZone.forTimeZone(timezone); + try + { + Chronology chrono = GJChronology.getInstance(dtz); + DateTime dateTime = new DateTime(isoDate, chrono); + Date date = dateTime.toDate(); + return date; + } + catch (IllegalInstantException ie) + { + // The exception is thrown when a DateTime was created with a date-time inside the DST gap - a time that did not exist. + // Parse the date ignoring the time. + DateTimeFormatter parser = ISODateTimeFormat.dateTimeParser(); + LocalDate ldate = new LocalDate(parser.parseLocalDate(isoDate), dtz); + // Default to the first valid date-time of the day, not always 00:00 (because of DST). + DateTime dateT = ldate.toDateTimeAtStartOfDay(dtz); + Date date = dateT.toDate(); + return date; + } + } + catch (IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + } + + /** + * Checks whether or not the given ISO8601-formatted date-string contains a time-component + * instead of only the actual date. + * + * @param isoDate + * @return true, if time is present. + */ + public static boolean isTimeComponentDefined(String isoDate) + { + boolean defined = false; + + if(isoDate != null && isoDate.length() > 11) + { + // Find occurrence of T (sYYYY-MM-DDT..), sign is optional + int expectedLocation = 10; + if(isoDate.charAt(0) == '-' || isoDate.charAt(0) == '+') { + // Sign is included before year + expectedLocation++; + } + + defined = isoDate.length() >= expectedLocation && isoDate.charAt(expectedLocation) == 'T'; + } + + return defined; + } + + /** + * Parses the given ISO8601-formatted date-string, not taking into account the time-component. + * The time-information for the will be reset to zero. + * + * @param isoDate the day (formatted sYYYY-MM-DD) or a full date (sYYYY-MM-DDThh:mm:ss.sssTZD) + * @param timezone the timezone to use + * @return the parsed date + * + * @throws AlfrescoRuntimeException if the parsing failed. + */ + public static Date parseDayOnly(String isoDate, TimeZone timezone) + { + try + { + if(isoDate != null && isoDate.length() >= 10) + { + int offset = 0; + + // Sign can be included before year + boolean bc = false; + if(isoDate.charAt(0) == '-') + { + bc = true; + offset++; + } + else if(isoDate.charAt(0) == '+') + { + offset++; + } + + // Extract year + int year = Integer.parseInt(isoDate.substring(offset, offset += 4)); + if (isoDate.charAt(offset) != '-') + { + throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset)); + } + + // Extract month + int month = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + if (isoDate.charAt(offset) != '-') + { + throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset)); + } + + // Extract day + int day = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + + Calendar calendar = new GregorianCalendar(timezone); + calendar.setLenient(false); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + if(bc) + { + calendar.set(Calendar.ERA, GregorianCalendar.BC); + } + + return calendar.getTime(); + } + else + { + throw new AlfrescoRuntimeException("String passed is too short " + isoDate); + } + } + catch(IndexOutOfBoundsException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + catch(NumberFormatException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + } + + +} diff --git a/core/src/main/java/org/alfresco/util/InputStreamContent.java b/core/src/main/java/org/alfresco/util/InputStreamContent.java new file mode 100644 index 0000000000..7e497bf7b5 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/InputStreamContent.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; + +import org.springframework.util.FileCopyUtils; + + +/** + * Input Stream based Content + */ +public class InputStreamContent implements Content, Serializable +{ + private static final long serialVersionUID = -7729633986840536282L; + + private InputStream stream; + private String mimetype; + private String encoding; + + /** cached result - to ensure we only read it once */ + private String content; + + + /** + * Constructor + * + * @param stream content input stream + * @param mimetype content mimetype + */ + public InputStreamContent(InputStream stream, String mimetype, String encoding) + { + this.stream = stream; + this.mimetype = mimetype; + this.encoding = encoding; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getContent() + */ + public String getContent() + throws IOException + { + // ensure we only try to read the content once - as this method may be called several times + // but the inputstream can only be processed a single time + if (this.content == null) + { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + FileCopyUtils.copy(stream, os); // both streams are closed + byte[] bytes = os.toByteArray(); + // get the encoding for the string + String encoding = getEncoding(); + // create the string from the byte[] using encoding if necessary + this.content = (encoding == null) ? new String(bytes) : new String(bytes, encoding); + } + return this.content; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getInputStream() + */ + public InputStream getInputStream() + { + return stream; + } + + + public Reader getReader() + throws IOException + { + return (encoding == null) ? new InputStreamReader(stream) : new InputStreamReader(stream, encoding); + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getSize() + */ + public long getSize() + { + return -1; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getMimetype() + */ + public String getMimetype() + { + return mimetype; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getEncoding() + */ + public String getEncoding() + { + return encoding; + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/JMXUtils.java b/core/src/main/java/org/alfresco/util/JMXUtils.java new file mode 100644 index 0000000000..e64de9c850 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/JMXUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.Date; + +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; + +public class JMXUtils +{ + public static OpenType getOpenType(Object o) + { + if(o instanceof Long) + { + return SimpleType.LONG; + } + else if(o instanceof String) + { + return SimpleType.STRING; + } + else if(o instanceof Date) + { + return SimpleType.DATE; + } + else if(o instanceof Integer) + { + return SimpleType.INTEGER; + } + else if(o instanceof Boolean) + { + return SimpleType.BOOLEAN; + } + else if(o instanceof Double) + { + return SimpleType.DOUBLE; + } + else if(o instanceof Float) + { + return SimpleType.FLOAT; + } + else + { + throw new IllegalArgumentException(); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/LockHelper.java b/core/src/main/java/org/alfresco/util/LockHelper.java new file mode 100644 index 0000000000..5d64241a92 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/LockHelper.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +/** + * Helper to make trying for read-write locks simpler + * + * @author Derek Hulley + * @since 4.1.7 + */ +public class LockHelper +{ + /** + * Exception generated when a lock try is unsuccessful + * + * @author Derek Hulley + * @since 4.1.7 + */ + public static class LockTryException extends RuntimeException + { + private static final long serialVersionUID = -3629889029591630609L; + + public LockTryException(String msg) + { + super(msg); + } + } + + /** + * Try to get a lock in the given number of milliseconds or get an exception + * + * @param lock the lock to try + * @param timeoutMs the number of milliseconds to try + * @param useCase {@link String} value which specifies description of use case when lock is needed + * @throws LockTryException the exception if the time is exceeded or the thread is interrupted + */ + public static void tryLock(Lock lock, long timeoutMs, String useCase) throws LockTryException + { + boolean gotLock = false; + try + { + gotLock = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) + { + // Handled + } + if (!gotLock) + { + throw new LockTryException("Failed to get lock " + lock.getClass().getSimpleName() + " for " + useCase + " in " + timeoutMs + "ms."); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/LogAdapter.java b/core/src/main/java/org/alfresco/util/LogAdapter.java new file mode 100644 index 0000000000..ae27069ddd --- /dev/null +++ b/core/src/main/java/org/alfresco/util/LogAdapter.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import org.apache.commons.logging.Log; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Utility class to adapt a {@link Log} class. + * + * @since 4.2 + * + * @author Alan Davis + */ +@AlfrescoPublicApi +public abstract class LogAdapter implements Log +{ + final protected Log log; + + /** + * Constructor of an optional wrapped {@link Log}. + * @param log + */ + protected LogAdapter(Log log) + { + this.log = log; + } + + @Override + public void trace(Object arg0) + { + trace(arg0, null); + } + + @Override + public void trace(Object arg0, Throwable arg1) + { + if (log != null) + { + log.trace(arg0, arg1); + } + } + + @Override + public void debug(Object arg0) + { + debug(arg0, null); + } + + @Override + public void debug(Object arg0, Throwable arg1) + { + if (log != null) + { + log.debug(arg0, arg1); + } + } + + @Override + public void info(Object arg0) + { + info(arg0, null); + } + + @Override + public void info(Object arg0, Throwable arg1) + { + if (log != null) + { + log.info(arg0, arg1); + } + } + + @Override + public void warn(Object arg0) + { + warn(arg0, null); + } + + @Override + public void warn(Object arg0, Throwable arg1) + { + if (log != null) + { + log.warn(arg0, arg1); + } + } + + @Override + public void error(Object arg0) + { + error(arg0, null); + } + + @Override + public void error(Object arg0, Throwable arg1) + { + if (log != null) + { + log.error(arg0, arg1); + } + } + + @Override + public void fatal(Object arg0) + { + fatal(arg0, null); + } + + @Override + public void fatal(Object arg0, Throwable arg1) + { + if (log != null) + { + log.fatal(arg0, arg1); + } + } + + @Override + public boolean isTraceEnabled() + { + return log != null && log.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() + { + return log != null && log.isDebugEnabled(); + } + + @Override + public boolean isInfoEnabled() + { + return log != null && log.isInfoEnabled(); + } + + @Override + public boolean isWarnEnabled() + { + return log != null && log.isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled() + { + return log != null && log.isErrorEnabled(); + } + + @Override + public boolean isFatalEnabled() + { + return log != null && log.isFatalEnabled(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/LogTee.java b/core/src/main/java/org/alfresco/util/LogTee.java new file mode 100644 index 0000000000..512f95a5b8 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/LogTee.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import org.apache.commons.logging.Log; + +/** + * Utility class to split or 'tee' two {@link Log} classes. + * + * @since 4.2 + * + * @author Alan Davis + */ +public class LogTee extends LogAdapter +{ + protected Log log2; + + public LogTee(Log log1, Log log2) + { + super(log1); + this.log2 = log2; + } + + @Override + public void trace(Object arg0, Throwable arg1) + { + log.trace(arg0, arg1); + log2.trace(arg0, arg1); + } + + @Override + public void debug(Object arg0, Throwable arg1) + { + log.debug(arg0, arg1); + log2.debug(arg0, arg1); + } + + @Override + public void info(Object arg0, Throwable arg1) + { + log.info(arg0, arg1); + log2.info(arg0, arg1); + } + + @Override + public void warn(Object arg0, Throwable arg1) + { + log.warn(arg0, arg1); + log2.warn(arg0, arg1); + } + + @Override + public void error(Object arg0, Throwable arg1) + { + log.error(arg0, arg1); + log2.error(arg0, arg1); + } + + @Override + public void fatal(Object arg0, Throwable arg1) + { + log.fatal(arg0, arg1); + log2.fatal(arg0, arg1); + } + + @Override + public boolean isTraceEnabled() + { + return log.isTraceEnabled() || log2.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() + { + return log.isDebugEnabled() || log2.isDebugEnabled(); + } + + @Override + public boolean isInfoEnabled() + { + return log.isInfoEnabled() || log2.isInfoEnabled(); + } + + @Override + public boolean isWarnEnabled() + { + return log.isWarnEnabled() || log2.isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled() + { + return log.isErrorEnabled() || log2.isErrorEnabled(); + } + + @Override + public boolean isFatalEnabled() + { + return log.isFatalEnabled() || log2.isFatalEnabled(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/LogUtil.java b/core/src/main/java/org/alfresco/util/LogUtil.java new file mode 100644 index 0000000000..515a2938f9 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/LogUtil.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import org.springframework.extensions.surf.util.I18NUtil; +import org.apache.commons.logging.Log; + +/** + * Utility class to assist with I18N of log messages. + *

+ * Calls to this class should still be wrapped with the appropriate log level checks: + *

+ * if (logger.isDebugEnabled())
+ * {
+ *     LogUtil.debug(logger, MSG_EXECUTING_STATEMENT, sql);
+ * }
+ * 
+ * + * @see org.springframework.extensions.surf.util.I18NUtil + * @since 2.1 + * + * @author Derek Hulley + */ +public class LogUtil +{ + /** + * Log an I18Nized message to DEBUG. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void debug(Log logger, String messageKey, Object ... args) + { + logger.debug(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to INFO. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void info(Log logger, String messageKey, Object ... args) + { + logger.info(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to WARN. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void warn(Log logger, String messageKey, Object ... args) + { + logger.warn(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to ERROR. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void error(Log logger, String messageKey, Object ... args) + { + logger.error(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to ERROR with a given source error. + * + * @param logger the logger to use + * @param e the exception cause of the issue + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void error(Log logger, Throwable e, String messageKey, Object ... args) + { + logger.error(I18NUtil.getMessage(messageKey, args), e); + } +} diff --git a/core/src/main/java/org/alfresco/util/MD5.java b/core/src/main/java/org/alfresco/util/MD5.java new file mode 100644 index 0000000000..aee2761744 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/MD5.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; +import java.security.*; + +/** +* The MD5 utility class computes the MD5 digest (aka: "hash") of a block +* of data; an MD5 digest is a 32-char ASCII string. +* +* The synchronized/static function "Digest" is useful for situations where +* lock contention in the application is not expected to be an issue. +* +* The unsynchronized/non-static method "digest" is useful in a +* multi-threaded program that wanted to avoid locking by creating +* an MD5 object for exclusive use by a single thread. +* +* +*
+*  EXAMPLE 1:  Static usage
+*
+*      import org..alfresco.util.MD5;
+*      String x = MD5.Digest("hello".getBytes());
+*
+*
+*  EXAMPLE 2:  Per-thread non-static usage
+*
+*      import org..alfresco.util.MD5;
+*      MD5 md5 = new MD5();
+*      ...
+*      String x = md5.digest("hello".getBytes());
+*
+* 
+*/ +public class MD5 +{ + private static final byte[] ToHex_ = + { '0','1','2','3','4','5','6','7', + '8','9','a','b','c','d','e','f' + }; + + private MessageDigest md5_ = null; + + static private MessageDigest Md5_; + static + { + try { Md5_ = MessageDigest.getInstance("MD5");} // MD5 is supported + catch ( NoSuchAlgorithmException e ) {}; // safe to swallow + }; + + /** + * Constructor for use with the unsynchronized/non-static method + * "digest" method. Note that the "digest" function is not + * thread-safe, so if you want to use it, every thread must create + * its own MD5 instance. If you don't want to bother & are willing + * to deal with the potential for lock contention, use the synchronized + * static "Digest" function instead of creating an instance via this + * constructor. + */ + public MD5() + { + try { md5_ = MessageDigest.getInstance("MD5");} // MD5 is supported + catch ( NoSuchAlgorithmException e ) {}; // safe to swallow + } + + /** + * Thread-safe static digest (hashing) function. + * + * If you want to avoid lock contention, create an instance of MD5 + * per-thead, anc call the unsynchronized method 'digest' instead. + */ + public static synchronized String Digest(byte[] dataToHash) + { + Md5_.update(dataToHash, 0, dataToHash.length); + return HexStringFromBytes( Md5_.digest() ); + } + + /** + * Non-threadsafe MD5 digest (hashing) function + */ + public String digest(byte[] dataToHash) + { + md5_.update(dataToHash, 0, dataToHash.length); + return HexStringFromBytes( md5_.digest() ); + } + + private static String HexStringFromBytes(byte[] b) + { + byte [] hex_bytes = new byte[ b.length * 2 ]; + int i=0,j=0; + + for (i=0; i < b.length; i++) + { + hex_bytes[j] = ToHex_[ ( b[i] & 0x000000F0 ) >> 4 ] ; + hex_bytes[j+1] = ToHex_[ b[i] & 0x0000000F ]; + j+=2; + } + return new String( hex_bytes ); + } +} diff --git a/core/src/main/java/org/alfresco/util/MaxSizeMap.java b/core/src/main/java/org/alfresco/util/MaxSizeMap.java new file mode 100644 index 0000000000..bc402bca39 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/MaxSizeMap.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Map that ejects the last recently accessed or inserted element(s) to keep the size to a specified maximum. + * + * @param + * Key + * @param + * Value + */ +public class MaxSizeMap extends LinkedHashMap +{ + private static final long serialVersionUID = 3753219027867262507L; + + private final int maxSize; + + /** + * @param maxSize maximum size of the map. + * @param accessOrder true for access-order, false for insertion-order. + */ + public MaxSizeMap(int maxSize, boolean accessOrder) + { + super(maxSize * 2, 0.75f, accessOrder); + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) + { + return super.size() > this.maxSize; + } +} diff --git a/core/src/main/java/org/alfresco/util/OneToManyBiMap.java b/core/src/main/java/org/alfresco/util/OneToManyBiMap.java new file mode 100644 index 0000000000..5d49ec19ca --- /dev/null +++ b/core/src/main/java/org/alfresco/util/OneToManyBiMap.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; + +/** + * An extension of org.alfresco.util.OneToManyMap that stores the + * inverse mapping from a value to its key. + * + * @author Nick Smith + */ +public interface OneToManyBiMap extends OneToManyMap +{ + + /** + * Returns the key, if any, for the specified value. If the + * specified value does not exist within the map then this method returns + * null. + * + * @param value + * @return The key to the specified value or null. + */ + public abstract K getKey(V value); + + /** + * Removes the specified value from the OneToManyBiMap. If this was the only value associated with the key to this value, then the key is also removed. + * + * @param value The value to be removed. + * @return The key that is associated with the value to be removed. + */ + public abstract K removeValue(V value); + +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/OneToManyHashBiMap.java b/core/src/main/java/org/alfresco/util/OneToManyHashBiMap.java new file mode 100644 index 0000000000..57dd0937dd --- /dev/null +++ b/core/src/main/java/org/alfresco/util/OneToManyHashBiMap.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author Nick Smith + */ +public class OneToManyHashBiMap implements Map>, OneToManyBiMap +{ + // The 'forward' map. + private OneToManyHashMap map = new OneToManyHashMap(); + + // The inverse map. + private Map inverse = new HashMap(); + + public void clear() + { + map.clear(); + inverse.clear(); + } + + public boolean containsKey(Object key) + { + return map.containsKey(key); + } + + public boolean containsValue(Object value) + { + return map.containsValue(value); + } + + public boolean containsSingleValue(V value) + { + return inverse.containsKey(value); + } + + public Set>> entrySet() + { + return map.entrySet(); + } + + public Set> entries() + { + return map.entries(); + } + + public Set get(Object key) + { + return map.get(key); + } + + /* + * @see org.alfresco.util.OneToManyBiMap#getKey(V) + */ + public K getKey(V value) + { + return inverse.get(value); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public Set keySet() + { + return map.keySet(); + } + + public Set put(K key, Set values) + { + map.put(key, values); + for (V value : values) + { + inverse.put(value, key); + } + return null; + } + + public V putSingleValue(K key, V value) + { + inverse.put(value, key); + return map.putSingleValue(key, value); + } + + public void putAll(Map> m) + { + map.putAll(m); + for (Entry> entry : m.entrySet()) + { + K key = entry.getKey(); + for (V value : entry.getValue()) + { + inverse.put(value, key); + } + } + } + + public void putAllSingleValues(Map m) + { + map.putAllSingleValues(m); + for (Entry entry : m.entrySet()) + { + inverse.put(entry.getValue(), entry.getKey()); + } + } + + public Set remove(Object key) + { + Set values = map.remove(key); + for (V value : values) + { + inverse.remove(value); + } + return values; + } + + /* + * @see org.alfresco.util.OneToManyBiMap#removeValue(V) + */ + public K removeValue(V value) + { + K key = inverse.remove(value); + Set values = map.get(key); + values.remove(value); + if (values.size() == 0) map.remove(key); + return key; + } + + public int size() + { + return map.size(); + } + + public Collection> values() + { + return map.values(); + } + + public Collection flatValues() + { + return Collections.unmodifiableCollection(inverse.keySet()); + } + +} diff --git a/core/src/main/java/org/alfresco/util/OneToManyHashMap.java b/core/src/main/java/org/alfresco/util/OneToManyHashMap.java new file mode 100644 index 0000000000..da13105ca1 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/OneToManyHashMap.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +/** + * @author Nick Smith + */ +public class OneToManyHashMap implements Map>, OneToManyMap +{ + //Delegate map. + private final Map> map = new HashMap>(); + + public void clear() + { + map.clear(); + } + + public boolean containsKey(Object key) + { + return map.containsKey(key); + } + + public boolean containsValue(Object value) + { + return map.containsValue(value); + } + + /* + * @see org.alfresco.util.OneToManyMap#containsSingleValue(V) + */ + public boolean containsSingleValue(V value) + { + Collection> values = map.values(); + for (Set set : values) + { + if (set.contains(value)) return true; + + } + return false; + } + + public Set>> entrySet() + { + return map.entrySet(); + } + + /* + * @see org.alfresco.util.OneToManyMap#entries() + */ + public Set> entries() + { + Set> entries = new HashSet>(); + for (Entry> entry : map.entrySet()) + { + final K key = entry.getKey(); + final Set values = entry.getValue(); + for (final V value : values) + { + entries.add(new Entry() + { + + public K getKey() + { + return key; + } + + public V getValue() + { + return value; + } + + // Not Thread-safe! + public V setValue(V newValue) + { + throw new UnsupportedOperationException( + "Cannot modify the entries returned by " + + OneToManyHashMap.class.getName() + ".entries()!"); + } + }); + } + } + return entries; + } + + public Set get(Object key) + { + Set set = map.get(key); + if (set == null) set = new HashSet(); + return Collections.unmodifiableSet(set); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public Set keySet() + { + return map.keySet(); + } + + public Set put(K key, Set value) + { + return map.put(key, value); + } + + /* + * @see org.alfresco.util.OneToManyMap#putSingleValue(K, V) + */ + public V putSingleValue(K key, V value) + { + Set values = map.get(key); + if (values == null) + { + values = new HashSet(); + map.put(key, values); + } + values.add(value); + return value; + } + + public void putAll(Map> m) + { + map.putAll(m); + } + + /* + * @see org.alfresco.util.OneToManyMap#putAllSingleValues(java.util.Map) + */ + public void putAllSingleValues(Map m) + { + for (Entry entry : m.entrySet()) + { + putSingleValue(entry.getKey(), entry.getValue()); + } + } + + public Set remove(Object key) + { + return map.remove(key); + } + + public int size() + { + return map.size(); + } + + public Collection> values() + { + return map.values(); + } + + /* + * @see org.alfresco.util.OneToManyMap#flatValues() + */ + public Collection flatValues() + { + LinkedList flatValues = new LinkedList(); + for (Set values : map.values()) + { + flatValues.addAll(values); + } + return flatValues; + } +} diff --git a/core/src/main/java/org/alfresco/util/OneToManyMap.java b/core/src/main/java/org/alfresco/util/OneToManyMap.java new file mode 100644 index 0000000000..e678f2c43b --- /dev/null +++ b/core/src/main/java/org/alfresco/util/OneToManyMap.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * An extension of java.util.Map that represents a mapping + * from a key to a set of values. In addition to the standard + * java.util.Map methods this interface also provides several useful + * methods for directly accessing the values rather than having to access values + * via a java.util.Set + * + * @author Nick Smith + */ +public interface OneToManyMap extends Map> +{ + /** + * This method returns true if any of the value sets in the + * OneToManyMap contains an equivalent object to the value + * parameter, where equivalence is determined using the + * equals(Object) method. + * + * @param value The value being searched for. + * @return Returns true if any of the value sets contains a + * matching value, otherwise returns false + */ + public abstract boolean containsSingleValue(V value); + + /** + * This method is similar to the java.util.Map.entrySet() + * method, however the entries returned map from a key to a value, rather + * than from a key(K) to a value(V) rather than + * froma key(K) to a set of values(Set<V>).
+ * Note that the entries returned by this method do not support the method + * java.util.Map.Entry.setValue(V). + * + * @return The + * Set<Entry<K, V>> representing all the key-value pairs in the ManyToOneMap. + */ + public abstract Set> entries(); + + /** + * This method is similar to the method java.util.Map.put(K, V) + * , however it allows the user to add a single value to the map rather than + * adding a java.util.Set containing one or more values. If the + * specified key already has a set of values associated with it then the new + * value is added to this set. Otherwise a new set is created and the new + * value is added to that. + * + * @param key + * @param value + * @return returns the newly added value. + */ + public abstract V putSingleValue(K key, V value); + + /** + * This method is similar to java.utilMap.putAll(Map m), + * however the map specified is from keys to values instead of keys to sets + * of values. + * + * @param m A map containing the key-value mappings to be added to the + * ManyToOneMap. + */ + public abstract void putAllSingleValues(Map m); + + /** + * Returns a Collection of all the values in the map. Unlike + * values() the values are in a single flattened + * Collection<V> rather than a + * Collection<Set<V>>. + * + * @return All the values in the map as a flattened Collection. + */ + public abstract Collection flatValues(); + +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/PackageMarker.java b/core/src/main/java/org/alfresco/util/PackageMarker.java new file mode 100644 index 0000000000..c851c7947b --- /dev/null +++ b/core/src/main/java/org/alfresco/util/PackageMarker.java @@ -0,0 +1,22 @@ +package org.alfresco.util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This empty marker annotation is added to make sure .class files are actually generated + * for package-info.java files. This allow to speed up incremental compilation time, + * so that each build tool will be able to properly detect differences between sources and + * .class compiled files. + * + * See https://jira.codehaus.org/browse/MCOMPILER-205?focusedCommentId=326795&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-326795 + * for more details + * + * NOTE: This annotation should be added in each package-info.java file + * @author Gabriele Columbro + * + */ +@Retention(RetentionPolicy.SOURCE) +public @interface PackageMarker { + +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/Pair.java b/core/src/main/java/org/alfresco/util/Pair.java new file mode 100644 index 0000000000..60e5344692 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/Pair.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.io.ObjectInputStream.GetField; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Utility class for containing two things that aren't like each other + */ +@AlfrescoPublicApi +public final class Pair implements Serializable +{ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static final Pair NULL_PAIR = new Pair(null, null); + + @SuppressWarnings("unchecked") + public static final Pair nullPair() + { + return NULL_PAIR; + } + + private static final long serialVersionUID = -7406248421185630612L; + + /** + * The first member of the pair. + */ + private F first; + + /** + * The second member of the pair. + */ + private S second; + + /** + * Make a new one. + * + * @param first The first member. + * @param second The second member. + */ + public Pair(F first, S second) + { + this.first = first; + this.second = second; + } + + /** + * Get the first member of the tuple. + * @return The first member. + */ + public final F getFirst() + { + return first; + } + + /** + * Get the second member of the tuple. + * @return The second member. + */ + public final S getSecond() + { + return second; + } + + public final void setFirst(F first) + { + this.first = first; + } + + public final void setSecond(S second) + { + this.second = second; + } + + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (other == null || !(other instanceof Pair)) + { + return false; + } + Pair o = (Pair)other; + return EqualsHelper.nullSafeEquals(this.first, o.first) && + EqualsHelper.nullSafeEquals(this.second, o.second); + } + + @Override + public int hashCode() + { + return (first == null ? 0 : first.hashCode()) + (second == null ? 0 : second.hashCode()); + } + + @Override + public String toString() + { + return "(" + first + ", " + second + ")"; + } + + /** + * Ensure that previously-serialized instances don't fail due to the member name change. + */ + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException + { + GetField fields = is.readFields(); + if (fields.defaulted("first")) + { + // This is a pre-V3.3 + this.first = (F) fields.get("fFirst", null); + this.second = (S) fields.get("fSecond", null); + } + else + { + this.first = (F) fields.get("first", null); + this.second = (S) fields.get("second", null); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/ParameterCheck.java b/core/src/main/java/org/alfresco/util/ParameterCheck.java new file mode 100644 index 0000000000..efc932e0b7 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/ParameterCheck.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.Collection; + +/** + * Utility class to perform various common parameter checks + * + * @author gavinc + */ +public final class ParameterCheck +{ + /** + * Checks that the parameter with the given name has content i.e. it is not + * null + * + * @param strParamName Name of parameter to check + * @param object Value of the parameter to check + */ + public static final void mandatory(final String strParamName, final Object object) + { + // check that the object is not null + if (object == null) + { + throw new IllegalArgumentException(strParamName + " is a mandatory parameter"); + } + } + + /** + * Checks that the string parameter with the given name has content i.e. it + * is not null and not zero length + * + * @param strParamName Name of parameter to check + * @param strParamValue Value of the parameter to check + */ + public static final void mandatoryString(final String strParamName, final String strParamValue) + { + // check that the given string value has content + if (strParamValue == null || strParamValue.length() == 0) + { + throw new IllegalArgumentException(strParamName + " is a mandatory parameter"); + } + } + + /** + * Checks that the collection parameter contains at least one item. + * + * @param strParamName Name of parameter to check + * @param coll collection to check + */ + public static final void mandatoryCollection(final String strParamName, final Collection coll) + { + if (coll == null || coll.size() == 0) + { + throw new IllegalArgumentException(strParamName + " collection must contain at least one item"); + } + } + +} diff --git a/core/src/main/java/org/alfresco/util/PathMapper.java b/core/src/main/java/org/alfresco/util/PathMapper.java new file mode 100644 index 0000000000..3b537ebcc4 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/PathMapper.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A component that maps source data paths to target data paths. + *

+ * This class caches results and is thread-safe. + * + * @author Derek Hulley + * @since 3.2 + */ +public class PathMapper +{ + private static final Log logger = LogFactory.getLog(PathMapper.class); + + private final ReentrantReadWriteLock.ReadLock readLock; + private final ReentrantReadWriteLock.WriteLock writeLock; + + private boolean locked; + /** + * Used to lookup path translations + */ + private final Map> pathMaps; + /** + * Cached fine-grained path translations (derived data) + */ + private final Map> derivedPathMaps; + private final Map> derivedPathMapsPartial; + + /** + * Default constructor + */ + public PathMapper() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + + pathMaps = new HashMap>(37); + derivedPathMaps = new HashMap>(127); + derivedPathMapsPartial = new HashMap>(127); + } + + /** + * Locks the instance against further modifications. + */ + public void lock() + { + writeLock.lock(); + try + { + locked = true; + } + finally + { + writeLock.unlock(); + } + } + + public void clear() + { + writeLock.lock(); + try + { + if (locked) + { + throw new IllegalStateException("The PathMapper has been locked against further changes"); + } + pathMaps.clear(); + derivedPathMaps.clear(); + derivedPathMapsPartial.clear(); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Add a path mapping. + * + * @param sourcePath the source path + * @param targetPath the target path + */ + public void addPathMap(String sourcePath, String targetPath) + { + writeLock.lock(); + try + { + if (locked) + { + throw new IllegalStateException("The PathMapper has been locked against further changes"); + } + derivedPathMaps.clear(); + derivedPathMapsPartial.clear(); + Set targetPaths = pathMaps.get(sourcePath); + if (targetPaths == null) + { + targetPaths = new HashSet(5); + pathMaps.put(sourcePath, targetPaths); + } + targetPaths.add(targetPath); + } + finally + { + writeLock.unlock(); + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Added path map: " + sourcePath + " --> " + targetPath); + } + } + + /** + * Gets the remapped paths for the given source path, excluding any derivative + * paths i.e. does exact path matching only. + * + * @param sourcePath the source path + * @return Returns the target paths (never null) + */ + public Set getMappedPaths(String sourcePath) + { + readLock.lock(); + try + { + Set targetPaths = derivedPathMaps.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + } + finally + { + readLock.unlock(); + } + // We didn't find anything, so update the cache + writeLock.lock(); + try + { + return updateMappedPaths(sourcePath); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Gets the remapped paths for the given source path, including any derivative + * paths i.e. does partial path matching. + * + * @param sourcePath the source path + * @return Returns the target paths (never null) + */ + public Set getMappedPathsWithPartialMatch(String sourcePath) + { + readLock.lock(); + try + { + Set targetPaths = derivedPathMapsPartial.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + } + finally + { + readLock.unlock(); + } + // We didn't find anything, so update the cache + writeLock.lock(); + try + { + return updateMappedPathsPartial(sourcePath); + } + finally + { + writeLock.unlock(); + } + } + + public boolean isEmpty() + { + readLock.lock(); + try + { + return pathMaps.isEmpty(); + } + finally + { + readLock.unlock(); + } + } + + private Set updateMappedPaths(String sourcePath) + { + // Do a double-check + Set targetPaths = derivedPathMaps.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + targetPaths = new HashSet(17); + derivedPathMaps.put(sourcePath, targetPaths); + // Now remap it and build the target values + for (Map.Entry> entry : pathMaps.entrySet()) + { + String mapSourcePath = entry.getKey(); + Set mapTargetPaths = entry.getValue(); + // If the map source matches the source, then it's simple + if (mapSourcePath.equals(sourcePath)) + { + targetPaths.addAll(mapTargetPaths); + continue; + } + // It is not an exact match, so check if it starts with the source + int index = sourcePath.indexOf(mapSourcePath); + if (index != 0) + { + // It doesn't match the start, so ignore it + continue; + } + // Replace the beginning with the mapped targets + for (String mapTargetPath : mapTargetPaths) + { + if (mapTargetPath.equals(mapSourcePath)) + { + // Direct mapping, so shortcut + targetPaths.add(sourcePath); + } + else + { + String newPath = (mapTargetPath + sourcePath.substring(mapSourcePath.length())); + targetPaths.add(newPath); + } + } + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Cached path mapping: \n" + + " Source: " + sourcePath + "\n" + + " Targets: " + targetPaths); + } + return targetPaths; + } + + private Set updateMappedPathsPartial(String sourcePath) + { + // Do a double-check + Set targetPaths = derivedPathMapsPartial.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + targetPaths = new HashSet(17); + derivedPathMapsPartial.put(sourcePath, targetPaths); + // Now remap it and build the target values + for (Map.Entry> entry : pathMaps.entrySet()) + { + String mapSourcePath = entry.getKey(); + Set mapTargetPaths = entry.getValue(); + // It is not an exact match, so check if it starts with the source + int index = mapSourcePath.indexOf(sourcePath); + if (index != 0) + { + // It doesn't match the start, so ignore it + continue; + } + // Record the partial matches + targetPaths.addAll(mapTargetPaths); + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Cached path mapping (partial): \n" + + " Source: " + sourcePath + "\n" + + " Targets: " + targetPaths); + } + return targetPaths; + } + + public Map convertMap(Map valueMap) + { + Map resultMap = new HashMap(valueMap.size() * 2 + 1); + for (Map.Entry entry : valueMap.entrySet()) + { + String path = entry.getKey(); + V value = entry.getValue(); + Set mappedPaths = getMappedPaths(path); + for (String mappedPath : mappedPaths) + { + resultMap.put(mappedPath, value); + } + } + return resultMap; + } +} diff --git a/core/src/main/java/org/alfresco/util/PatternFilter.java b/core/src/main/java/org/alfresco/util/PatternFilter.java new file mode 100644 index 0000000000..1ec0a05c43 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/PatternFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . */ +package org.alfresco.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Matches a path against a set of regular expression filters + * + */ +public class PatternFilter +{ + private List patterns; + + /** + * A list of regular expressions that represent patterns of files. + * + * @param regexps list of regular expressions + * + * @see String#matches(java.lang.String) + */ + public void setPatterns(List regexps) + { + this.patterns = new ArrayList(regexps.size()); + for(String regexp : regexps) + { + this.patterns.add(Pattern.compile(regexp)); + } + } + + public boolean isFiltered(String path) + { + // check against all the regular expressions + boolean matched = false; + + for (Pattern regexp : patterns) + { + if(!regexp.matcher(path).matches()) + { + // it is not a match - try next one + continue; + } + else + { + matched = true; + break; + } + } + + return matched; + } +} diff --git a/core/src/main/java/org/alfresco/util/PropertyCheck.java b/core/src/main/java/org/alfresco/util/PropertyCheck.java new file mode 100644 index 0000000000..37c4158a0b --- /dev/null +++ b/core/src/main/java/org/alfresco/util/PropertyCheck.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Helper class for for use when checking properties. This class uses + * I18N for its messages. + * + * @author Derek Hulley + */ +public class PropertyCheck +{ + public static final String ERR_PROPERTY_NOT_SET = "system.err.property_not_set"; + + /** + * Checks that the property with the given name is not null. + * + * @param target the object on which the property must have been set + * @param propertyName the name of the property + * @param value of the property value + */ + public static void mandatory(Object target, String propertyName, Object value) + { + if (value == null) + { + throw new AlfrescoRuntimeException( + ERR_PROPERTY_NOT_SET, + new Object[] {propertyName, target, target.getClass()}); + } + } + + /** + * Checks that the given string is not: + *

    + *
  • null
  • + *
  • empty
  • + *
  • a placeholder of form '${...}'
  • + *
+ * + * @param value the value to check + * @return true if the checks all pass + */ + public static boolean isValidPropertyString(String value) + { + if (value == null || value.length() == 0) + { + return false; + } + if (value.startsWith("${") && value.endsWith("}")) + { + return false; + } + else + { + return true; + } + } + + /** + * Dig out the property name from a placeholder-style property of form + * ${prop.name}, which will yield prop.name. If the placeholders + * are not there, the value is returned directly. null values are + * not allowed, but empty strings are. + * + * @param value The property with or without property placeholders + * @return Returns the core property without the property placeholders + * ${ and }. + * @throws IllegalArgumentException if the value is null + */ + public static String getPropertyName(String value) + { + if (value == null) + { + throw new IllegalArgumentException("'value' is a required argument."); + } + if (!value.startsWith("${")) + { + return value; + } + if (!value.endsWith("}")) + { + return value; + } + int strLen = value.length(); + return value.substring(2, strLen - 1); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/ReadWriteLockExecuter.java b/core/src/main/java/org/alfresco/util/ReadWriteLockExecuter.java new file mode 100644 index 0000000000..1b9a9928da --- /dev/null +++ b/core/src/main/java/org/alfresco/util/ReadWriteLockExecuter.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Utility object that wraps read and write methods within the context of a + * {@link ReentrantReadWriteLock}. The callback's methods are best-suited + * to fetching values from a cache or protecting members that need lazy + * initialization. + *

+ * Client code should construct an instance of this class for each resource + * (or set of resources) that need to be protected. + * + * @author Derek Hulley + * @since 3.4 + */ +public abstract class ReadWriteLockExecuter +{ + private ReentrantReadWriteLock.ReadLock readLock; + private ReentrantReadWriteLock.WriteLock writeLock; + + /** + * Default constructor + */ + public ReadWriteLockExecuter() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + } + + /** + * Execute the read-only part of the work. + * + * @return Returns a value of interest or null if + * the {@link #getWithWriteLock()} method must be + * called + * @throws Throwable all checked exceptions are wrapped in a RuntimeException + */ + protected abstract T getWithReadLock() throws Throwable; + + /** + * Execute the write part of the work. + *

+ * NOTE: It is important to perform a double-check on the resource + * before assuming it is not null; there is a window between the {@link #getWithReadLock()} + * and the {@link #getWithWriteLock()} during which another thread may have populated + * the resource of interest. + * + * @return Returns the value of interest of null + * @throws Throwable all checked exceptions are wrapped in a RuntimeException + */ + protected abstract T getWithWriteLock() throws Throwable; + + public T execute() + { + T ret = null; + readLock.lock(); + try + { + ret = this.getWithReadLock(); + // We do the null check here so that less time is spent outside of the lock + if (ret != null) + { + return ret; + } + } + catch (RuntimeException e) + { + throw e; + } + catch (Throwable e) + { + throw new RuntimeException("Exception during 'getWithReadLock'", e); + } + finally + { + readLock.unlock(); + } + // If we got here, then we didn't get a result and need to go for the write lock + writeLock.lock(); + try + { + // The return value is not of interest to us + return this.getWithWriteLock(); + } + catch (RuntimeException e) + { + throw e; + } + catch (Throwable e) + { + throw new RuntimeException("Exception during 'getWithWriteLock'", e); + } + finally + { + writeLock.unlock(); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/ReflectionHelper.java b/core/src/main/java/org/alfresco/util/ReflectionHelper.java new file mode 100644 index 0000000000..6d06ea326b --- /dev/null +++ b/core/src/main/java/org/alfresco/util/ReflectionHelper.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Static Helper methods for instantiating objects from reflection. + * + * @author muzquiano + */ +public class ReflectionHelper +{ + private static Log logger = LogFactory.getLog(ReflectionHelper.class); + + private ReflectionHelper() + { + } + + /** + * Constructs a new object for the given class name. + * The construction takes no arguments. + * + * If an exception occurs during construction, null is returned. + * + * All exceptions are written to the Log instance for this class. + * + * @param className String + * @return Object + */ + public static Object newObject(String className) + { + Object o = null; + + try + { + Class clazz = Class.forName(className); + o = clazz.newInstance(); + } + catch (ClassNotFoundException cnfe) + { + logger.debug(cnfe); + } + catch (InstantiationException ie) + { + logger.debug(ie); + } + catch (IllegalAccessException iae) + { + logger.debug(iae); + } + return o; + } + + /** + * Constructs a new object for the given class name and with the given + * arguments. The arguments must be specified in terms of their Class[] + * types and their Object[] values. + * + * Example: + * + * String s = newObject("java.lang.String", new Class[] { String.class}, + * new String[] { "test"}); + * + * is equivalent to: + * + * String s = new String("test"); + * + * If an exception occurs during construction, null is returned. + * + * All exceptions are written to the Log instance for this class. + + * @param className String + * @param argTypes Class[] + * @param args Object[] + * @return Object + */ + public static Object newObject(String className, Class[] argTypes, Object[] args) + { + /** + * We have some mercy here - if they called and did not pass in any + * arguments, then we will call through to the pure newObject() method. + */ + if (args == null || args.length == 0) + { + return newObject(className); + } + + /** + * Try to build the object + * + * If an exception occurs, we log it and return null. + */ + Object o = null; + try + { + // base class + Class clazz = Class.forName(className); + + Constructor c = clazz.getDeclaredConstructor(argTypes); + o = c.newInstance(args); + } + catch (ClassNotFoundException cnfe) + { + logger.debug(cnfe); + } + catch (InstantiationException ie) + { + logger.debug(ie); + } + catch (IllegalAccessException iae) + { + logger.debug(iae); + } + catch (NoSuchMethodException nsme) + { + logger.debug(nsme); + } + catch (InvocationTargetException ite) + { + logger.debug(ite); + } + return o; + } + + /** + * Invokes a method on the given object by passing the given arguments + * into the method. + * + * @param obj Object + * @param method String + * @param argTypes Class[] + * @param args Object[] + * @return Object + */ + public static Object invoke(Object obj, String method, Class[] argTypes, Object[] args) + { + if (obj == null || method == null) + { + throw new IllegalArgumentException("Object and Method must be supplied."); + } + + /** + * Try to invoke the method. + * + * If the method is unable to be invoked, we log and return null. + */ + try + { + Method m = obj.getClass().getMethod(method, argTypes); + if(m != null) + { + return m.invoke(obj, args); + } + } + catch(NoSuchMethodException nsme) + { + logger.debug(nsme); + } + catch(IllegalAccessException iae) + { + logger.debug(iae); + } + catch(InvocationTargetException ite) + { + logger.debug(ite); + } + + return null; + } +} diff --git a/core/src/main/java/org/alfresco/util/SchedulerStarterBean.java b/core/src/main/java/org/alfresco/util/SchedulerStarterBean.java new file mode 100644 index 0000000000..d236e15fe6 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/SchedulerStarterBean.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; + +public class SchedulerStarterBean extends AbstractLifecycleBean +{ + protected final static Log log = LogFactory.getLog(SchedulerStarterBean.class); + + private Scheduler scheduler; + + @Override + protected void onBootstrap(ApplicationEvent event) + { + try + { + log.info("Scheduler started"); + scheduler.start(); + } + catch (SchedulerException e) + { + throw new AlfrescoRuntimeException("Scheduler failed to start", e); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // Nothing required + // This is done by the SchedulerFactoryBean.destroy() - DisposableBean + } + + public Scheduler getScheduler() + { + return scheduler; + } + + public void setScheduler(Scheduler scheduler) + { + this.scheduler = scheduler; + } + +} diff --git a/core/src/main/java/org/alfresco/util/SerializationUtils.java b/core/src/main/java/org/alfresco/util/SerializationUtils.java new file mode 100644 index 0000000000..0b58e62d74 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/SerializationUtils.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * NOTE: This utility class is a copy of org.apache.commons.lang3.SerializationUtils + * + * Please see http://issues.alfresco.com/jira/browse/ALF-5044 for why this is done. + * + * @author Apache Software Foundation + * @author Daniel L. Rall + * @author Jeff Varszegi + * @author Gary Gregory + * + *

+ * Assists with the serialization process and performs additional functionality + * based on serialization. + *

+ *

+ *

    + *
  • Deep clone using serialization + *
  • Serialize managing finally and IOException + *
  • Deserialize managing finally and IOException + *
+ * + *

+ * This class throws exceptions for invalid null inputs. Each + * method documents its behaviour in more detail. + *

+ * + *

+ * #ThreadSafe# + *

+ * + */ +public class SerializationUtils +{ + + /** + *

+ * SerializationUtils instances should NOT be constructed in standard + * programming. Instead, the class should be used as + * SerializationUtils.clone(object). + *

+ * + *

+ * This constructor is public to permit tools that require a JavaBean + * instance to operate. + *

+ */ + public SerializationUtils() + { + super(); + } + + // Clone + // ----------------------------------------------------------------------- + /** + *

+ * Deep clone an Object using serialization. + *

+ * + *

+ * This is many times slower than writing clone methods by hand on all + * objects in your object graph. However, for complex object graphs, or for + * those that don't support deep cloning this can be a simple alternative + * implementation. Of course all the objects must be + * Serializable. + *

+ * + * @param object + * the Serializable object to clone + * @return the cloned object + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static T clone(T object) + { + /* + * when we serialize and deserialize an object, it is reasonable to + * assume the deserialized object is of the same type as the original + * serialized object + */ + @SuppressWarnings("unchecked") + final T result = (T) deserialize(serialize(object)); + return result; + } + + // Serialize + // ----------------------------------------------------------------------- + /** + *

+ * Serializes an Object to the specified stream. + *

+ * + *

+ * The stream will be closed once the object is written. This avoids the + * need for a finally clause, and maybe also exception handling, in the + * application code. + *

+ * + *

+ * The stream passed in is not buffered internally within this method. This + * is the responsibility of your application if desired. + *

+ * + * @param obj + * the object to serialize to bytes, may be null + * @param outputStream + * the stream to write to, must not be null + * @throws IllegalArgumentException + * if outputStream is null + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static void serialize(Serializable obj, OutputStream outputStream) + { + if (outputStream == null) + { + throw new IllegalArgumentException("The OutputStream must not be null"); + } + ObjectOutputStream out = null; + try + { + // stream closed in the finally + out = new ObjectOutputStream(outputStream); + out.writeObject(obj); + + } catch (IOException ex) + { + throw new AlfrescoRuntimeException("Failed to serialize", ex); + } finally + { + try + { + if (out != null) + { + out.close(); + } + } catch (IOException ex) + { + // ignore close exception + } + } + } + + /** + *

+ * Serializes an Object to a byte array for + * storage/serialization. + *

+ * + * @param obj + * the object to serialize to bytes + * @return a byte[] with the converted Serializable + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static byte[] serialize(Serializable obj) + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + serialize(obj, baos); + return baos.toByteArray(); + } + + // Deserialize + // ----------------------------------------------------------------------- + /** + *

+ * Deserializes an Object from the specified stream. + *

+ * + *

+ * The stream will be closed once the object is written. This avoids the + * need for a finally clause, and maybe also exception handling, in the + * application code. + *

+ * + *

+ * The stream passed in is not buffered internally within this method. This + * is the responsibility of your application if desired. + *

+ * + * @param inputStream + * the serialized object input stream, must not be null + * @return the deserialized object + * @throws IllegalArgumentException + * if inputStream is null + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static Object deserialize(InputStream inputStream) + { + if (inputStream == null) + { + throw new IllegalArgumentException("The InputStream must not be null"); + } + ObjectInputStream in = null; + try + { + // stream closed in the finally + in = new ObjectInputStream(inputStream); + return in.readObject(); + + } catch (ClassNotFoundException ex) + { + throw new AlfrescoRuntimeException("Failed to deserialize", ex); + } catch (IOException ex) + { + throw new AlfrescoRuntimeException("Failed to deserialize", ex); + } finally + { + try + { + if (in != null) + { + in.close(); + } + } catch (IOException ex) + { + // ignore close exception + } + } + } + + /** + *

+ * Deserializes a single Object from an array of bytes. + *

+ * + * @param objectData + * the serialized object, must not be null + * @return the deserialized object + * @throws IllegalArgumentException + * if objectData is null + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static Object deserialize(byte[] objectData) + { + if (objectData == null) + { + throw new IllegalArgumentException("The byte[] must not be null"); + } + ByteArrayInputStream bais = new ByteArrayInputStream(objectData); + return deserialize(bais); + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/TempFileProvider.java b/core/src/main/java/org/alfresco/util/TempFileProvider.java new file mode 100644 index 0000000000..678515de40 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/TempFileProvider.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * A helper class that provides temporary files, providing a common point to clean + * them up. + * + *

+ * The contents of ALFRESCO_TEMP_FILE_DIR [%java.io.tmpdir%/Alfresco] are managed by this + * class. Temporary files and directories are cleaned by TempFileCleanerJob so that + * after a delay [default 1 hour] the contents of the alfresco temp dir, + * both files and directories are removed. + * + *

+ * Some temporary files may need to live longer than 1 hour. The temp file provider allows special sub folders which + * are cleaned less frequently. By default, files in the long life folders will remain for 24 hours + * unless cleaned by the application code earlier. + * + *

+ * The other contents of %java.io.tmpdir% are not touched by the cleaner job. + * + *

TempFileCleanerJob Job Data: protectHours, number of hours to keep temporary files, default 1 hour. + * + * @author derekh + * @author mrogers + */ +@AlfrescoPublicApi +public class TempFileProvider +{ + private static final int BUFFER_SIZE = 40 * 1024; + + /** + * subdirectory in the temp directory where Alfresco temporary files will go + */ + public static final String ALFRESCO_TEMP_FILE_DIR = "Alfresco"; + + /** + * The prefix for the long life temporary files. + */ + public static final String ALFRESCO_LONG_LIFE_FILE_DIR = "longLife"; + + /** the system property key giving us the location of the temp directory */ + public static final String SYSTEM_KEY_TEMP_DIR = "java.io.tmpdir"; + + private static final Log logger = LogFactory.getLog(TempFileProvider.class); + + private static int MAX_RETRIES = 3; + + /** + * Static class only + */ + private TempFileProvider() + { + } + + /** + * Get the Java Temp dir e.g. java.io.tempdir + * + * @return Returns the system temporary directory i.e. isDir == true + */ + public static File getSystemTempDir() + { + String systemTempDirPath = System.getProperty(SYSTEM_KEY_TEMP_DIR); + if (systemTempDirPath == null) + { + throw new AlfrescoRuntimeException("System property not available: " + SYSTEM_KEY_TEMP_DIR); + } + File systemTempDir = new File(systemTempDirPath); + if (logger.isDebugEnabled()) + { + logger.debug("Created system temporary directory: " + systemTempDir); + } + return systemTempDir; + } + + /** + * Get the Alfresco temp dir, by defaut %java.io.tempdir%/Alfresco. + * Will create the temp dir on the fly if it does not already exist. + * + * @return Returns a temporary directory, i.e. isDir == true + */ + public static File getTempDir() + { + return getTempDir(ALFRESCO_TEMP_FILE_DIR); + } + + /** + * Get the specified temp dir, %java.io.tempdir%/dirName. + * Will create the temp dir on the fly if it does not already exist. + * + * @param dirName the name of sub-directory in %java.io.tempdir% + * + * @return Returns a temporary directory, i.e. isDir == true + */ + public static File getTempDir(String dirName) + { + File systemTempDir = getSystemTempDir(); + // append the Alfresco directory + File tempDir = new File(systemTempDir, dirName); + // ensure that the temp directory exists + if (tempDir.exists()) + { + // nothing to do + } + else + { + // not there yet + if (!tempDir.mkdirs()) + { + // We didn't create it but perhaps it was made by some other thread + if (!tempDir.exists()) + { + // It's definitely not there + throw new AlfrescoRuntimeException("Failed to create temp directory: " + tempDir); + } + } + else + { + // This thread created it + if (logger.isDebugEnabled()) + { + logger.debug("Created temp directory: " + tempDir); + } + } + } + // done + return tempDir; + } + + /** + * creates a longer living temp dir. Files within the longer living + * temp dir will not be garbage collected as soon as "normal" temporary files. + * By default long life temp files will live for for 24 hours rather than 1 hour. + *

+ * Code using the longer life temporary files should be careful to clean up since + * abuse of this feature may result in out of memory/disk space errors. + * @param key can be blank in which case the system will generate a folder to be used by all processes + * or can be used to create a unique temporary folder name for a particular process. At the end of the process + * the client can simply delete the entire temporary folder. + * @return the long life temporary directory + */ + public static File getLongLifeTempDir(String key) + { + /** + * Long life temporary directories have a prefix at the start of the + * folder name. + */ + String folderName = ALFRESCO_LONG_LIFE_FILE_DIR + "_" + key; + + File tempDir = getTempDir(); + + // append the Alfresco directory + File longLifeDir = new File(tempDir, folderName); + // ensure that the temp directory exists + + if (longLifeDir.exists()) + { + if (logger.isDebugEnabled()) + { + logger.debug("Already exists: " + longLifeDir); + } + // nothing to do + return longLifeDir; + } + else + { + /** + * We need to create a temporary directory + * + * We may have a race condition here if more than one thread attempts to create + * the temp dir. + * + * mkdirs can't be synchronized + * See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4742723 + */ + for(int retry = 0; retry < MAX_RETRIES; retry++) + { + boolean created = longLifeDir.mkdirs(); + + if (created) + { + // Yes we created the temp dir + if (logger.isDebugEnabled()) + { + logger.debug("Created long life temp directory: " + longLifeDir); + } + return longLifeDir; + } + else + { + if(longLifeDir.exists()) + { + // created by another thread, but that's O.K. + if (logger.isDebugEnabled()) + { + logger.debug("Another thread created long life temp directory: " + longLifeDir); + } + return longLifeDir; + } + } + } + } + throw new AlfrescoRuntimeException("Failed to create temp directory: " + longLifeDir); + } + + public static File createTempFile(InputStream in, String namePrefix, String nameSufix) throws Exception + { + if (null == in) + { + return null; + } + + File file = createTempFile(namePrefix, nameSufix); + OutputStream out = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE); + try + { + byte[] buffer = new byte[BUFFER_SIZE]; + int i; + while ((i = in.read(buffer)) > -1) + { + out.write(buffer, 0, i); + } + } + catch (Exception e) + { + file.delete(); + throw e; + } + finally + { + in.close(); + out.flush(); + out.close(); + } + + return file; + } + + /** + * Is this a long life folder ? + * @param file + * @return true, this is a long life folder. + */ + private static boolean isLongLifeTempDir(File file) + { + if(file.isDirectory()) + { + if(file.getName().startsWith(ALFRESCO_LONG_LIFE_FILE_DIR)) + { + return true; + } + else + { + return false; + } + } + return false; + } + + /** + * Create a temp file in the alfresco temp dir. + * + * @return Returns a temp File that will be located in the + * Alfresco subdirectory of the default temp directory + * + * @see #ALFRESCO_TEMP_FILE_DIR + * @see File#createTempFile(java.lang.String, java.lang.String) + */ + public static File createTempFile(String prefix, String suffix) + { + File tempDir = TempFileProvider.getTempDir(); + // we have the directory we want to use + return createTempFile(prefix, suffix, tempDir); + } + + /** + * @return Returns a temp File that will be located in the + * given directory + * + * @see #ALFRESCO_TEMP_FILE_DIR + * @see File#createTempFile(java.lang.String, java.lang.String) + */ + public static File createTempFile(String prefix, String suffix, File directory) + { + try + { + File tempFile = File.createTempFile(prefix, suffix, directory); + if (logger.isDebugEnabled()) + { + logger.debug("Creating tmp file: " + tempFile); + } + return tempFile; + } catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to created temp file: \n" + + " prefix: " + prefix + "\n" + + " suffix: " + suffix + "\n" + + " directory: " + directory, + e); + } + } + + /** + * Cleans up all Alfresco temporary files that are older than the + * given number of hours. Subdirectories are emptied as well and all directories + * below the primary temporary subdirectory are removed. + *

+ * The job data must include a property protectHours, which is the + * number of hours to protect a temporary file from deletion since its last + * modification. + * + * @author Derek Hulley + */ + @AlfrescoPublicApi + public static class TempFileCleanerJob implements Job + { + public static final String KEY_PROTECT_HOURS = "protectHours"; + public static final String KEY_DIRECTORY_NAME = "directoryName"; + + /** + * Gets a list of all files in the {@link TempFileProvider#ALFRESCO_TEMP_FILE_DIR temp directory} + * and deletes all those that are older than the given number of hours. + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + // get the number of hours to protect the temp files + String strProtectHours = (String) context.getJobDetail().getJobDataMap().get(KEY_PROTECT_HOURS); + if (strProtectHours == null) + { + throw new JobExecutionException("Missing job data: " + KEY_PROTECT_HOURS); + } + int protectHours = -1; + try + { + protectHours = Integer.parseInt(strProtectHours); + } + catch (NumberFormatException e) + { + throw new JobExecutionException("Invalid job data " + KEY_PROTECT_HOURS + ": " + strProtectHours); + } + if (protectHours < 0 || protectHours > 8760) + { + throw new JobExecutionException("Hours to protect temp files must be 0 <= x <= 8760"); + } + + String directoryName = (String) context.getJobDetail().getJobDataMap().get(KEY_DIRECTORY_NAME); + + if (directoryName == null) + { + directoryName = ALFRESCO_TEMP_FILE_DIR; + } + + long now = System.currentTimeMillis(); + long aFewHoursBack = now - (3600L * 1000L * protectHours); + + long aLongTimeBack = now - (24 * 3600L * 1000L); + + File tempDir = TempFileProvider.getTempDir(directoryName); + int count = removeFiles(tempDir, aFewHoursBack, aLongTimeBack, false); // don't delete this directory + // done + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + count + " files from temp directory: " + tempDir); + } + } + + /** + * Removes all temporary files created before the given time. + *

+ * The delete will cascade down through directories as well. + * + * @param removeBefore only remove files created before this time + * @return Returns the number of files removed + */ + public static int removeFiles(long removeBefore) + { + File tempDir = TempFileProvider.getTempDir(); + return removeFiles(tempDir, removeBefore, removeBefore, false); + } + + /** + * @param directory the directory to clean out - the directory will optionally be removed + * @param removeBefore only remove files created before this time + * @param removeDir true if the directory must be removed as well, otherwise false + * @return Returns the number of files removed + */ + private static int removeFiles(File directory, long removeBefore, long longLifeBefore, boolean removeDir) + { + if (!directory.isDirectory()) + { + throw new IllegalArgumentException("Expected a directory to clear: " + directory); + } + // check if there is anything to to + if (!directory.exists()) + { + return 0; + } + // list all files + File[] files = directory.listFiles(); + int count = 0; + for (File file : files) + { + if (file.isDirectory()) + { + if(isLongLifeTempDir(file)) + { + // long life for this folder and its children + int countRemoved = removeFiles(file, longLifeBefore, longLifeBefore, true); + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + countRemoved + " files from temp directory: " + file); + } + } + else + { + // enter subdirectory and clean it out and remove itsynetics + int countRemoved = removeFiles(file, removeBefore, longLifeBefore, true); + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + countRemoved + " files from directory: " + file); + } + } + } + else + { + // it is a file - check the created time + if (file.lastModified() > removeBefore) + { + // file is not old enough + continue; + } + // it is a file - attempt a delete + try + { + if(logger.isDebugEnabled()) + { + logger.debug("Deleting temp file: " + file); + } + file.delete(); + count++; + } + catch (Throwable e) + { + logger.info("Failed to remove temp file: " + file); + } + } + } + // must we delete the directory we are in? + if (removeDir) + { + // the directory must be removed if empty + try + { + File[] listing = directory.listFiles(); + if(listing != null && listing.length == 0) + { + // directory is empty + if(logger.isDebugEnabled()) + { + logger.debug("Deleting empty directory: " + directory); + } + directory.delete(); + } + } + catch (Throwable e) + { + logger.info("Failed to remove temp directory: " + directory, e); + } + } + // done + return count; + } + } +} diff --git a/core/src/main/java/org/alfresco/util/TraceableThreadFactory.java b/core/src/main/java/org/alfresco/util/TraceableThreadFactory.java new file mode 100644 index 0000000000..b59900b1c6 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/TraceableThreadFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A thread factory that spawns threads that are statically visible. Each factory uses a unique + * thread group. All the groups that have been used can be fetched using + * {@link #getActiveThreadGroups()}, allowing iteration of the the threads in the group. + * + * @since 2.1 + * @author Derek Hulley + */ +public class TraceableThreadFactory implements ThreadFactory +{ + private static final AtomicInteger factoryNumber = new AtomicInteger(1); + private static List activeThreadGroups = Collections.synchronizedList(new ArrayList(1)); + + /** + * Get a list of thread groups registered by the factory. + * + * @return Returns a snapshot of thread groups + */ + public static List getActiveThreadGroups() + { + return activeThreadGroups; + } + + private final ThreadGroup group; + private String namePrefix; + private final AtomicInteger threadNumber; + private boolean threadDaemon; + private int threadPriority; + + + public TraceableThreadFactory() + { + this.group = new ThreadGroup("TraceableThreadGroup-" + factoryNumber.getAndIncrement()); + TraceableThreadFactory.activeThreadGroups.add(this.group); + + this.namePrefix = "TraceableThread-" + factoryNumber.getAndIncrement() + "-thread-"; + this.threadNumber = new AtomicInteger(1); + + this.threadDaemon = true; + this.threadPriority = Thread.NORM_PRIORITY; + } + + /** + * @param daemon true if all threads created must be daemon threads + */ + public void setThreadDaemon(boolean daemon) + { + this.threadDaemon = daemon; + } + + /** + * + * @param threadPriority the threads priority from 1 (lowest) to 10 (highest) + */ + public void setThreadPriority(int threadPriority) + { + this.threadPriority = threadPriority; + } + + public Thread newThread(Runnable r) + { + Thread thread = new Thread( + group, + r, + namePrefix + threadNumber.getAndIncrement(), + 0); + thread.setDaemon(threadDaemon); + thread.setPriority(threadPriority); + + return thread; + } + + public void setNamePrefix(String namePrefix) + { + this.namePrefix = namePrefix; + } + + public String getNamePrefix() + { + return this.namePrefix; + } + +} diff --git a/core/src/main/java/org/alfresco/util/TriggerBeanSPI.java b/core/src/main/java/org/alfresco/util/TriggerBeanSPI.java new file mode 100644 index 0000000000..e4c5d662fc --- /dev/null +++ b/core/src/main/java/org/alfresco/util/TriggerBeanSPI.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +/** + * @author Andy + * + */ +public interface TriggerBeanSPI +{ + + /** + * @return int + */ + int getRepeatCount(); + + /** + * @param repeatCount int + */ + void setRepeatCount(int repeatCount); + + /** + * @return long + */ + long getRepeatInterval(); + + /** + * @param repeatInterval long + */ + void setRepeatInterval(long repeatInterval); + + /** + * @param repeatIntervalMinutes long + */ + void setRepeatIntervalMinutes(long repeatIntervalMinutes); + + /** + * @return long + */ + long getStartDelay(); + + /** + * @param startDelay long + */ + void setStartDelay(long startDelay); + + /** + * @param startDelayMinutes long + */ + void setStartDelayMinutes(long startDelayMinutes); + +} diff --git a/core/src/main/java/org/alfresco/util/Triple.java b/core/src/main/java/org/alfresco/util/Triple.java new file mode 100644 index 0000000000..24785f0191 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/Triple.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.util; + +/** + * Utility class for containing three things that aren't like each other. + * + * @since 4.0 + */ +public final class Triple +{ + /** + * The first member of the triple. + */ + private final T first; + + /** + * The second member of the triple. + */ + private final U second; + + /** + * The third member of the triple. + */ + private final V third; + + /** + * Make a new one. + * + * @param first The first member. + * @param second The second member. + * @param third The third member. + */ + public Triple(final T first, final U second, final V third) + { + this.first = first; + this.second = second; + this.third = third; + } + + /** + * Get the first member of the tuple. + * @return The first member. + */ + public T getFirst() + { + return first; + } + + /** + * Get the second member of the tuple. + * @return The second member. + */ + public U getSecond() + { + return second; + } + + /** + * Get the third member of the tuple. + * @return The third member. + */ + public V getThird() + { + return third; + } + + /** + * Override of equals. + * @param other The thing to compare to. + * @return equality. + */ + public boolean equals(final Object other) + { + if (this == other) + { + return true; + } + + if (!(other instanceof Triple)) + { + return false; + } + + Triple o = (Triple)other; + return (first.equals(o.getFirst()) && + second.equals(o.getSecond()) && + third.equals(o.getThird())); + } + + /** + * Override of hashCode. + */ + public int hashCode() + { + return ((first == null ? 0 : first.hashCode()) + + (second == null ? 0 : second.hashCode()) + + (third == null ? 0 : third.hashCode())); + } + + /** + * @see java.lang.Object#toString() + */ + public String toString() + { + return "(" + first + ", " + second + ", " + third + ")"; + } +} diff --git a/core/src/main/java/org/alfresco/util/VersionNumber.java b/core/src/main/java/org/alfresco/util/VersionNumber.java new file mode 100644 index 0000000000..2c18bfba54 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/VersionNumber.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.Serializable; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Immutable class to encapsulate a version number string. + * + * A valid version number string can be made up of any number of numberical parts + * all delimited by '.'. + * + * @author Roy Wetherall + */ +@AlfrescoPublicApi +public final class VersionNumber implements Comparable, Serializable +{ + private static final long serialVersionUID = -1570247769786810251L; + + /** A convenient '0' version */ + public static final VersionNumber VERSION_ZERO = new VersionNumber("0"); + /** A convenient '999' version */ + public static final VersionNumber VERSION_BIG = new VersionNumber("999"); + + /** Version delimeter */ + private static final String DELIMITER = "\\."; + + /** Version parts */ + private final int[] parts; + + /** + * Constructror, expects a valid version string. + * + * A AlfrescoRuntimeException will be throw if an invalid version is encountered. + * + * @param version the version string + */ + public VersionNumber(String version) + { + // Split the version into its component parts + String[] versions = version.split(DELIMITER); + if (versions.length < 1) + { + throw new AlfrescoRuntimeException("The version string '" + version + "' is invalid."); + } + + try + { + // Set the parts of the version + int index = 0; + this.parts = new int[versions.length]; + for (String versionPart : versions) + { + int part = Integer.parseInt(versionPart); + this.parts[index] = part; + index++; + } + } + catch (NumberFormatException e) + { + throw new AlfrescoRuntimeException("The version string '" + version + "' is invalid."); + } + } + + /** + * Get the various parts of the version + * + * @return array containing the parts of the version + */ + public int[] getParts() + { + return this.parts.clone(); + } + + /** + * Compares the passed version to this. Determines whether they are equal, greater or less than this version. + * + * @param obj the other version number + * @return -1 if the passed version is less that this, 0 if they are equal, 1 if the passed version is greater + */ + public int compareTo(VersionNumber obj) + { + int result = 0; + + VersionNumber that = (VersionNumber)obj; + int length = 0; + if (this.parts.length > that.parts.length) + { + length = this.parts.length; + } + else + { + length = that.parts.length; + } + + for (int index = 0; index < length; index++) + { + int thisPart = this.getPart(index); + int thatPart = that.getPart(index); + + if (thisPart > thatPart) + { + result = 1; + break; + } + else if (thisPart < thatPart) + { + result = -1; + break; + } + } + + return result; + } + + /** + * Helper method to the the part based on the index, if an invalid index is supplied 0 is returned. + * + * @param index the index + * @return the part value, 0 if the index is invalid + */ + public int getPart(int index) + { + int result = 0; + if (index < this.parts.length) + { + result = this.parts[index]; + } + return result; + } + + /** + * Hash code implementation + */ + @Override + public int hashCode() + { + if (parts == null || parts.length == 0) + { + return 0; + } + else if (parts.length >= 2) + { + return parts[0] * 17 + parts[1]; + } + else + { + return parts[0]; + } + } + + /** + * Equals implementation + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (false == obj instanceof VersionNumber) + { + return false; + } + VersionNumber that = (VersionNumber) obj; + return this.compareTo(that) == 0; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (int part : parts) + { + if (!first) + { + sb.append("."); + } + first = false; + sb.append(part); + } + return sb.toString(); + } +} diff --git a/core/src/main/java/org/alfresco/util/VmShutdownListener.java b/core/src/main/java/org/alfresco/util/VmShutdownListener.java new file mode 100644 index 0000000000..8682b93c31 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/VmShutdownListener.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A class that keeps track of the VM shutdown status. It can be + * used by threads as a singleton to check if the + * VM shutdown status has been activated. + *

+ * NOTE: In order to prevent a proliferation of shutdown hooks, + * it is advisable to use instances as singletons only. + *

+ * This component should be used by long-running, but interruptable processes. + * + * @author Derek Hulley + */ +public class VmShutdownListener +{ + private Log logger; + private volatile boolean vmShuttingDown; + + /** + * Constructs this instance to listen to the VM shutdown call. + * + */ + public VmShutdownListener(final String name) + { + logger = LogFactory.getLog(VmShutdownListener.class); + + vmShuttingDown = false; + Runnable shutdownRunnable = new Runnable() + { + public void run() + { + vmShuttingDown = true; + if (logger.isDebugEnabled()) + { + logger.debug("VM shutdown detected by listener " + name); + } + }; + }; + Thread shutdownThread = new Thread(shutdownRunnable, "ShutdownListener-" + name); + Runtime.getRuntime().addShutdownHook(shutdownThread); + } + + /** + * @return Returns true if the VM shutdown signal was detected. + */ + public boolean isVmShuttingDown() + { + return vmShuttingDown; + } + + /** + * Message carrier to break out of loops using the callback. + * + * @author Derek Hulley + * @since 3.2.1 + */ + public static class VmShutdownException extends RuntimeException + { + private static final long serialVersionUID = -5876107469054587072L; + } +} diff --git a/core/src/main/java/org/alfresco/util/bean/BooleanBean.java b/core/src/main/java/org/alfresco/util/bean/BooleanBean.java new file mode 100644 index 0000000000..8d43227a1f --- /dev/null +++ b/core/src/main/java/org/alfresco/util/bean/BooleanBean.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.bean; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Interface that may be implemented to return a boolean value in Spring bean configuration. + */ +@AlfrescoPublicApi +public interface BooleanBean +{ + public boolean isTrue(); +} diff --git a/core/src/main/java/org/alfresco/util/bean/HierarchicalBeanLoader.java b/core/src/main/java/org/alfresco/util/bean/HierarchicalBeanLoader.java new file mode 100644 index 0000000000..f086229763 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/bean/HierarchicalBeanLoader.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.bean; + +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Factory bean to find beans using a class hierarchy to drive the lookup. The well-known + * placeholder {@link #DEFAULT_DIALECT_PLACEHOLDER} is replaced with successive class + * names starting from the {@link #setDialectClass(String) dialect class} and + * progressing up the hierarchy until the {@link #setDialectBaseClass(String) base class} + * is reached. The bean is looked up in the context at each point until the + * bean is found or the base of the class hierarchy is reached. + *

+ * For example assume bean names:
+ *

+ *    BEAN 1: contentDAO.org.hibernate.dialect.Dialect
+ *    BEAN 2: contentDAO.org.hibernate.dialect.MySQLInnoDBDialect
+ *    BEAN 3: propertyValueDAO.org.hibernate.dialect.Dialect
+ *    BEAN 4: propertyValueDAO.org.hibernate.dialect.MySQLDialect
+ * 
+ * and
+ *
+ *    dialectBaseClass = org.hibernate.dialect.Dialect
+ * 
+ * For dialect org.hibernate.dialect.MySQLInnoDBDialect the following will be returned:
+ *
+ *    contentDAO.bean.dialect == BEAN 2
+ *    propertyValueDAO.bean.dialect == BEAN 4
+ * 
+ * For dialectorg.hibernate.dialect.MySQLDBDialect the following will be returned:
+ *
+ *    contentDAO.bean.dialect == BEAN 1
+ *    propertyValueDAO.bean.dialect == BEAN 4
+ * 
+ * For dialectorg.hibernate.dialect.Dialect the following will be returned:
+ *
+ *    contentDAO.bean.dialect == BEAN 1
+ *    propertyValueDAO.bean.dialect == BEAN 3
+ * 
+ * + * @author Derek Hulley + * @since 3.2SP1 + */ +public class HierarchicalBeanLoader + implements InitializingBean, FactoryBean, ApplicationContextAware +{ + public static final String DEFAULT_DIALECT_PLACEHOLDER = "#bean.dialect#"; + public static final String DEFAULT_DIALECT_REGEX = "\\#bean\\.dialect\\#"; + + private ApplicationContext ctx; + private String targetBeanName; + private Class targetClass; + private String dialectBaseClass; + private String dialectClass; + + /** + * Create a new HierarchicalResourceLoader. + */ + public HierarchicalBeanLoader() + { + super(); + } + + /** + * The application context that this bean factory serves. + */ + public void setApplicationContext(ApplicationContext ctx) + { + this.ctx = ctx; + } + + /** + * @param targetBeanName the name of the target bean to return, + * including the {@link #DEFAULT_DIALECT_PLACEHOLDER} + * where the specific dialect must be replaced. + */ + public void setTargetBeanName(String targetBeanName) + { + this.targetBeanName = targetBeanName; + } + + /** + * Set the target class that will be returned by {@link #getObjectType()} + * + * @param targetClass the type that this factory returns + */ + public void setTargetClass(Class targetClass) + { + this.targetClass = targetClass; + } + + /** + * Set the class to be used during hierarchical dialect replacement. Searches for the + * configuration location will not go further up the hierarchy than this class. + * + * @param className the name of the class or interface + */ + public void setDialectBaseClass(String className) + { + this.dialectBaseClass = className; + } + + public void setDialectClass(String className) + { + this.dialectClass = className; + } + + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "targetBeanName", targetBeanName); + PropertyCheck.mandatory(this, "targetClass", targetClass); + PropertyCheck.mandatory(this, "dialectBaseClass", dialectBaseClass); + PropertyCheck.mandatory(this, "dialectClass", dialectClass); + } + + /** + * @return Returns {@link #setTargetClass(Class) target class} + */ + public Class getObjectType() + { + return targetClass; + } + + /** + * @return Returns true always + */ + public boolean isSingleton() + { + return true; + } + + /** + * Replaces the + */ + public Object getObject() throws Exception + { + if (dialectClass == null || dialectBaseClass == null) + { + ctx.getBean(targetBeanName); + } + + // If a property value has not been substituted, extract the property name and load from system + String dialectBaseClassStr = dialectBaseClass; + if (!PropertyCheck.isValidPropertyString(dialectBaseClass)) + { + String prop = PropertyCheck.getPropertyName(dialectBaseClass); + dialectBaseClassStr = System.getProperty(prop, dialectBaseClass); + } + String dialectClassStr = dialectClass; + if (!PropertyCheck.isValidPropertyString(dialectClass)) + { + String prop = PropertyCheck.getPropertyName(dialectClass); + dialectClassStr = System.getProperty(prop, dialectClass); + } + + Class dialectBaseClazz; + try + { + dialectBaseClazz = Class.forName(dialectBaseClassStr); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Dialect base class not found: " + dialectBaseClassStr); + } + Class dialectClazz; + try + { + dialectClazz = Class.forName(dialectClassStr); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Dialect class not found: " + dialectClassStr); + } + // Ensure that we are dealing with classes and not interfaces + if (!Object.class.isAssignableFrom(dialectBaseClazz)) + { + throw new RuntimeException( + "Dialect base class must be derived from java.lang.Object: " + + dialectBaseClazz.getName()); + } + if (!Object.class.isAssignableFrom(dialectClazz)) + { + throw new RuntimeException( + "Dialect class must be derived from java.lang.Object: " + + dialectClazz.getName()); + } + // We expect these to be in the same hierarchy + if (!dialectBaseClazz.isAssignableFrom(dialectClazz)) + { + throw new RuntimeException( + "Non-existent HierarchicalBeanLoader hierarchy: " + + dialectBaseClazz.getName() + " is not a superclass of " + dialectClazz); + } + + Class clazz = dialectClazz; + Object bean = null; + while (bean == null) + { + // Do replacement + String newBeanName = targetBeanName.replaceAll(DEFAULT_DIALECT_REGEX, clazz.getName()); + try + { + bean = ctx.getBean(newBeanName); + // Found it + break; + } + catch (NoSuchBeanDefinitionException e) + { + } + // Not found + bean = null; + // Are we at the base class? + if (clazz.equals(dialectBaseClazz)) + { + // We don't go any further + break; + } + // Move up the hierarchy + clazz = clazz.getSuperclass(); + if (clazz == null) + { + throw new RuntimeException( + "Non-existent HierarchicalBeanLoaderBean hierarchy: " + + dialectBaseClazz.getName() + " is not a superclass of " + dialectClazz); + } + } + return bean; + } +} diff --git a/core/src/main/java/org/alfresco/util/cache/AbstractAsynchronouslyRefreshedCache.java b/core/src/main/java/org/alfresco/util/cache/AbstractAsynchronouslyRefreshedCache.java new file mode 100644 index 0000000000..9f3749af32 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/AbstractAsynchronouslyRefreshedCache.java @@ -0,0 +1,741 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.cache; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.transaction.TransactionListener; +import org.alfresco.util.transaction.TransactionSupportUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * The base implementation for an asynchronously refreshed cache. + * + * Currently supports one value or a cache per key (such as tenant.) Implementors just need to provide buildCache(String key/tennnantId) + * + * @author Andy + * @since 4.1.3 + * + * @author mrogers + * MER 17/04/2014 Refactored to core and generalised tennancy + */ +public abstract class AbstractAsynchronouslyRefreshedCache + implements AsynchronouslyRefreshedCache, + RefreshableCacheListener, + Callable, + BeanNameAware, + InitializingBean, + TransactionListener +{ + private static final String RESOURCE_KEY_TXN_DATA = "AbstractAsynchronouslyRefreshedCache.TxnData"; + + private static Log logger = LogFactory.getLog(AbstractAsynchronouslyRefreshedCache.class); + + private enum RefreshState + { + IDLE, WAITING, RUNNING, DONE + }; + + private ThreadPoolExecutor threadPoolExecutor; + private AsynchronouslyRefreshedCacheRegistry registry; + + // State + + private List listeners = new LinkedList(); + protected final ReentrantReadWriteLock liveLock = new ReentrantReadWriteLock(); + private final ReentrantReadWriteLock refreshLock = new ReentrantReadWriteLock(); + private final ReentrantReadWriteLock runLock = new ReentrantReadWriteLock(); + protected HashMap live = new HashMap(); + private LinkedHashSet refreshQueue = new LinkedHashSet(); + private String cacheId; + private RefreshState refreshState = RefreshState.IDLE; + private String resourceKeyTxnData; + + @Override + public void register(RefreshableCacheListener listener) + { + listeners.add(listener); + } + + /** + * @param threadPoolExecutor + * the threadPoolExecutor to set + */ + public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) + { + this.threadPoolExecutor = threadPoolExecutor; + } + + /** + * @param registry + * the registry to set + */ + public void setRegistry(AsynchronouslyRefreshedCacheRegistry registry) + { + this.registry = registry; + } + + + public void init() + { + registry.register(this); + } + + @Override + public String toString() + { + return "AbstractAsynchronouslyRefreshedCache [cacheId=" + cacheId + "]"; + } + + @Override + public T get(String key) + { + liveLock.readLock().lock(); + try + { + if (live.get(key) != null) + { + if (logger.isTraceEnabled()) + { + logger.trace("get() from cache for key " + key + " on " + this); + } + return live.get(key); + } + } + finally + { + liveLock.readLock().unlock(); + } + + if (logger.isDebugEnabled()) + { + logger.debug("get() miss, scheduling and waiting for key " + key + " on " + this); + } + + // There was nothing to return so we build and return + Refresh refresh = null; + refreshLock.writeLock().lock(); + try + { + // Is there anything we can wait for + for (Refresh existing : refreshQueue) + { + if (existing.getKey().equals(key)) + { + if (logger.isDebugEnabled()) + { + logger.debug("get() found existing build to wait for on " + this); + } + refresh = existing; + } + } + + if (refresh == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("get() building from scratch on " + this); + } + refresh = new Refresh(key); + refreshQueue.add(refresh); + } + + } + finally + { + refreshLock.writeLock().unlock(); + } + submit(); + waitForBuild(refresh); + + return get(key); + } + + /** + * Use the current thread to build and put a new version of the cache entry before returning. + * @param key the cache key + */ + public void forceInChangesForThisUncommittedTransaction(String key) + { + if (logger.isDebugEnabled()) + { + logger.debug("Building cache for tenant " + key + " on " + this); + } + T cache = buildCache(key); + if (logger.isDebugEnabled()) + { + logger.debug("Cache built for tenant " + key + " on " + this); + } + + liveLock.writeLock().lock(); + try + { + live.put(key, cache); + } + finally + { + liveLock.writeLock().unlock(); + } + + broadcastEvent(new RefreshableCacheRefreshedEvent(cacheId, key)); + } + + protected void waitForBuild(Refresh refresh) + { + while (refresh.getState() != RefreshState.DONE) + { + synchronized (refresh) + { + try + { + refresh.wait(100); + } + catch (InterruptedException e) + { + } + } + } + } + + @Override + public void refresh(String key) + { + // String tenantId = tenantService.getCurrentUserDomain(); + if (logger.isDebugEnabled()) + { + logger.debug("Async cache refresh request for tenant " + key + " on " + this); + } + registry.broadcastEvent(new RefreshableCacheRefreshEvent(cacheId, key), true); + } + + @Override + public void onRefreshableCacheEvent(RefreshableCacheEvent refreshableCacheEvent) + { + // Ignore events not targeted for this cache + if (!refreshableCacheEvent.getCacheId().equals(cacheId)) + { + return; + } + if (logger.isDebugEnabled()) + { + logger.debug("Async cache onRefreshableCacheEvent " + refreshableCacheEvent + " on " + this); + } + + // If in a transaction delay the refresh until after it commits + + if (TransactionSupportUtil.getTransactionId() != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Async cache adding" + refreshableCacheEvent.getKey() + " to post commit list: " + this); + } + TransactionData txData = getTransactionData(); + txData.keys.add(refreshableCacheEvent.getKey()); + } + else + { + LinkedHashSet keys = new LinkedHashSet(); + keys.add(refreshableCacheEvent.getKey()); + queueRefreshAndSubmit(keys); + } + } + + /** + * To be used in a transaction only. + */ + private TransactionData getTransactionData() + { + TransactionData data = (TransactionData) TransactionSupportUtil.getResource(resourceKeyTxnData); + if (data == null) + { + data = new TransactionData(); + // create and initialize caches + data.keys = new LinkedHashSet(); + + // ensure that we get the transaction callbacks as we have bound the unique + // transactional caches to a common manager + // The synchronizations are not available after the txn is committed/rolled back + // the resources are still stored in org.alfresco.util.transaction.TransactionSupportUtil + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + TransactionSupportUtil.bindListener(this, 0); + } + TransactionSupportUtil.bindResource(resourceKeyTxnData, data); + } + return data; + } + + private void queueRefreshAndSubmit(LinkedHashSet tenantIds) + { + if((tenantIds == null) || (tenantIds.size() == 0)) + { + return; + } + refreshLock.writeLock().lock(); + try + { + for (String tenantId : tenantIds) + { + if (logger.isDebugEnabled()) + { + logger.debug("Async cache adding refresh to queue for tenant " + tenantId + " on " + this); + } + refreshQueue.add(new Refresh(tenantId)); + } + } + finally + { + refreshLock.writeLock().unlock(); + } + submit(); + } + + @Override + public boolean isUpToDate(String key) + { + refreshLock.readLock().lock(); + try + { + for(Refresh refresh : refreshQueue) + { + if(refresh.getKey().equals(key)) + { + return false; + } + } + if (TransactionSupportUtil.getTransactionId() != null) + { + return (!getTransactionData().keys.contains(key)); + } + else + { + return true; + } + } + finally + { + refreshLock.readLock().unlock(); + } + } + + /** + * Must be run with runLock.writeLock + */ + private Refresh getNextRefresh() + { + if (runLock.writeLock().isHeldByCurrentThread()) + { + for (Refresh refresh : refreshQueue) + { + if (refresh.state == RefreshState.WAITING) + { + return refresh; + } + } + return null; + } + else + { + throw new IllegalStateException("Method should not be called without holding the write lock: " + this); + } + + } + + /** + * Must be run with runLock.writeLock + */ + private int countWaiting() + { + int count = 0; + if (runLock.writeLock().isHeldByCurrentThread()) + { + refreshLock.readLock().lock(); + try + { + for (Refresh refresh : refreshQueue) + { + if (refresh.state == RefreshState.WAITING) + { + count++; + } + } + return count; + } + finally + { + refreshLock.readLock().unlock(); + } + } + else + { + throw new IllegalStateException("Method should not be called without holding the write lock: " + this); + } + + } + + private void submit() + { + runLock.writeLock().lock(); + try + { + if (refreshState == RefreshState.IDLE) + { + if (logger.isDebugEnabled()) + { + logger.debug("submit() scheduling job: " + this); + } + threadPoolExecutor.submit(this); + refreshState = RefreshState.WAITING; + } + } + finally + { + runLock.writeLock().unlock(); + } + } + + @Override + public Void call() + { + try + { + doCall(); + return null; + } + catch (Exception e) + { + logger.error("Cache update failed: " + this, e); + runLock.writeLock().lock(); + try + { + threadPoolExecutor.submit(this); + refreshState = RefreshState.WAITING; + } + finally + { + runLock.writeLock().unlock(); + } + return null; + } + } + + private void doCall() throws Exception + { + Refresh refresh = setUpRefresh(); + if (refresh == null) + { + return; + } + + if (logger.isDebugEnabled()) + { + logger.debug("Building cache for key" + refresh.getKey() + " on " + this); + } + + try + { + doRefresh(refresh); + } + catch (Exception e) + { + refresh.setState(RefreshState.WAITING); + throw e; + } + } + + private void doRefresh(Refresh refresh) + { + if (logger.isDebugEnabled()) + { + logger.debug("Building cache for tenant" + refresh.getKey() + ": " + this); + } + T cache = buildCache(refresh.getKey()); + if (logger.isDebugEnabled()) + { + logger.debug(".... cache built for tenant" + refresh.getKey()); + } + + liveLock.writeLock().lock(); + try + { + live.put(refresh.getKey(), cache); + } + finally + { + liveLock.writeLock().unlock(); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Cache entry updated for tenant" + refresh.getKey()); + } + + broadcastEvent(new RefreshableCacheRefreshedEvent(cacheId, refresh.key)); + + runLock.writeLock().lock(); + try + { + refreshLock.writeLock().lock(); + try + { + if (countWaiting() > 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("Rescheduling more work: " + this); + } + threadPoolExecutor.submit(this); + refreshState = RefreshState.WAITING; + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Nothing to do; going idle: " + this); + } + refreshState = RefreshState.IDLE; + } + refresh.setState(RefreshState.DONE); + refreshQueue.remove(refresh); + } + finally + { + refreshLock.writeLock().unlock(); + } + } + finally + { + runLock.writeLock().unlock(); + } + } + + private Refresh setUpRefresh() throws Exception + { + Refresh refresh = null; + runLock.writeLock().lock(); + try + { + if (refreshState == RefreshState.WAITING) + { + refreshLock.writeLock().lock(); + try + { + refresh = getNextRefresh(); + if (refresh != null) + { + refreshState = RefreshState.RUNNING; + refresh.setState(RefreshState.RUNNING); + return refresh; + } + else + { + refreshState = RefreshState.IDLE; + return null; + } + } + finally + { + refreshLock.writeLock().unlock(); + } + } + else + { + return null; + } + } + catch (Exception e) + { + if (refresh != null) + { + refresh.setState(RefreshState.WAITING); + } + throw e; + } + finally + { + runLock.writeLock().unlock(); + } + + } + + @Override + public void setBeanName(String name) + { + cacheId = name; + + } + + @Override + public String getCacheId() + { + return cacheId; + } + + /** + * Build the cache entry for the specific key. + * This method is called in a thread-safe manner i.e. it is only ever called by a single + * thread. + * + * @param key + * @return new Cache instance + */ + protected abstract T buildCache(String key); + + private static class Refresh + { + private String key; + + private volatile RefreshState state = RefreshState.WAITING; + + Refresh(String key) + { + this.key = key; + } + + /** + * @return the tenantId + */ + public String getKey() + { + return key; + } + + /** + * @return the state + */ + public RefreshState getState() + { + return state; + } + + /** + * @param state + * the state to set + */ + public void setState(RefreshState state) + { + this.state = state; + } + + @Override + public int hashCode() + { + // The bucked is determined by the tenantId alone - we are going to change the state + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Refresh other = (Refresh) obj; + if (state != other.state) + return false; + if (key == null) + { + if (other.key != null) + return false; + } + else if (!key.equals(other.key)) + return false; + return true; + } + + @Override + public String toString() + { + return "Refresh [key=" + key + ", state=" + state + ", hashCode()=" + hashCode() + "]"; + } + + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "threadPoolExecutor", threadPoolExecutor); + PropertyCheck.mandatory(this, "registry", registry); + registry.register(this); + + resourceKeyTxnData = RESOURCE_KEY_TXN_DATA + "." + cacheId; + + } + + public void broadcastEvent(RefreshableCacheEvent event) + { + if (logger.isDebugEnabled()) + { + logger.debug("Notifying cache listeners for " + getCacheId() + " " + event); + } + // If the system is up and running, broadcast the event immediately + for (RefreshableCacheListener listener : this.listeners) + { + listener.onRefreshableCacheEvent(event); + } + + } + + @Override + public void beforeCommit(boolean readOnly) + { + // Nothing + } + + @Override + public void beforeCompletion() + { + // Nothing + } + + @Override + public void afterCommit() + { + TransactionData txnData = getTransactionData(); + queueRefreshAndSubmit(txnData.keys); + } + + @Override + public void afterRollback() + { + // Nothing + } + + private static class TransactionData + { + LinkedHashSet keys; + } +} diff --git a/core/src/main/java/org/alfresco/util/cache/AbstractRefreshableCacheEvent.java b/core/src/main/java/org/alfresco/util/cache/AbstractRefreshableCacheEvent.java new file mode 100644 index 0000000000..d29a67a486 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/AbstractRefreshableCacheEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.cache; + +/** + * A generic event with the cache id and affected tenant + * + * @author Andy + */ +public abstract class AbstractRefreshableCacheEvent implements RefreshableCacheEvent +{ + private static final long serialVersionUID = 1324638640132648062L; + + private String cacheId; + private String key; + + AbstractRefreshableCacheEvent(String cacheId, String key) + { + this.cacheId = cacheId; + this.key = key; + } + + @Override + public String getCacheId() + { + return cacheId; + } + + @Override + public String getKey() + { + return key; + } + + @Override + public String toString() + { + return "AbstractRefreshableCacheEvent [cacheId=" + cacheId + ", tenantId=" + key + "]"; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((cacheId == null) ? 0 : cacheId.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AbstractRefreshableCacheEvent other = (AbstractRefreshableCacheEvent) obj; + if (cacheId == null) + { + if (other.cacheId != null) return false; + } + else if (!cacheId.equals(other.cacheId)) return false; + if (key == null) + { + if (other.key != null) return false; + } + else if (!key.equals(other.key)) return false; + return true; + } +} diff --git a/core/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCache.java b/core/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCache.java new file mode 100644 index 0000000000..27534d1ae0 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCache.java @@ -0,0 +1,20 @@ +package org.alfresco.util.cache; + +public interface AsynchronouslyRefreshedCache extends RefreshableCache +{ + /** + * Get the cache id + * + * @return the cache ID + */ + String getCacheId(); + + /** + * Determine if the cache is up to date + * + * @param key tennant id + * @return true if the cache is not currently refreshing itself + */ + boolean isUpToDate(String key); + +} diff --git a/core/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCacheRegistry.java b/core/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCacheRegistry.java new file mode 100644 index 0000000000..8f093de68e --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCacheRegistry.java @@ -0,0 +1,19 @@ +package org.alfresco.util.cache; + + +public interface AsynchronouslyRefreshedCacheRegistry +{ + /** + * Register a listener + * @param listener + */ + public void register(RefreshableCacheListener listener); + + /** + * Fire an event + * @param event + * @param toAll - true goes to all listeners, false only to listeners that have a matching cacheId + */ + public void broadcastEvent(RefreshableCacheEvent event, boolean toAll); + +} diff --git a/core/src/main/java/org/alfresco/util/cache/DefaultAsynchronouslyRefreshedCacheRegistry.java b/core/src/main/java/org/alfresco/util/cache/DefaultAsynchronouslyRefreshedCacheRegistry.java new file mode 100644 index 0000000000..1e368bab87 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/DefaultAsynchronouslyRefreshedCacheRegistry.java @@ -0,0 +1,74 @@ +package org.alfresco.util.cache; +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Base registry implementation + * + * @author Andy + */ +public class DefaultAsynchronouslyRefreshedCacheRegistry implements AsynchronouslyRefreshedCacheRegistry +{ + private static Log logger = LogFactory.getLog(DefaultAsynchronouslyRefreshedCacheRegistry.class); + + private List listeners = new LinkedList(); + + @Override + public void register(RefreshableCacheListener listener) + { + if(logger.isDebugEnabled()) + { + logger.debug("Listener added for " + listener.getCacheId()); + } + listeners.add(listener); + } + + public void broadcastEvent(RefreshableCacheEvent event, boolean toAll) + { + // If the system is up and running, broadcast the event immediately + for (RefreshableCacheListener listener : this.listeners) + { + if (toAll) + { + if(logger.isDebugEnabled()) + { + logger.debug("Delivering event (" + event + ") to listener (" + listener + ")."); + } + listener.onRefreshableCacheEvent(event); + } + else + { + if (listener.getCacheId().equals(event.getCacheId())) + { + if(logger.isDebugEnabled()) + { + logger.debug("Delivering event (" + event + ") to listener (" + listener + ")."); + } + listener.onRefreshableCacheEvent(event); + } + } + } + } +} + diff --git a/core/src/main/java/org/alfresco/util/cache/RefreshableCache.java b/core/src/main/java/org/alfresco/util/cache/RefreshableCache.java new file mode 100644 index 0000000000..e9db992308 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/RefreshableCache.java @@ -0,0 +1,29 @@ +package org.alfresco.util.cache; + + +public interface RefreshableCache +{ + /** + * Get the cache. + * If there is no cache value this call will block. + * If the underlying cache is being refreshed, the old cache value will be returned until the refresh is complete. + * + * @return T + */ + public T get(String key); + + /** + * Refresh the cache asynchronously. + */ + public void refresh(String key); + + /** + * Register to be informed when the cache is updated in the background. + * + * Note: it is up to the implementation to provide any transactional wrapping. + * Transactional wrapping is not required to invalidate a shared cache entry directly via a transactional cache + * @param listener RefreshableCacheListener + */ + void register(RefreshableCacheListener listener); + +} diff --git a/core/src/main/java/org/alfresco/util/cache/RefreshableCacheEvent.java b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheEvent.java new file mode 100644 index 0000000000..2f225a4f57 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheEvent.java @@ -0,0 +1,18 @@ +package org.alfresco.util.cache; + +import java.io.Serializable; + +public interface RefreshableCacheEvent extends Serializable +{ + /** + * Get the cache id + */ + public String getCacheId(); + + + /** + * Get the affected key/tenant id + */ + public String getKey(); + +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/cache/RefreshableCacheListener.java b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheListener.java new file mode 100644 index 0000000000..8a72faa8ef --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheListener.java @@ -0,0 +1,20 @@ +package org.alfresco.util.cache; + + +public interface RefreshableCacheListener +{ + /** + * Callback made when a cache refresh occurs + * + * @param refreshableCacheEvent the cache event + */ + public void onRefreshableCacheEvent(RefreshableCacheEvent refreshableCacheEvent); + + /** + * Cache id so broadcast can be constrained to matching caches + * + * @return the cache ID + */ + public String getCacheId(); + +} diff --git a/core/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshEvent.java b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshEvent.java new file mode 100644 index 0000000000..161cb3ea4e --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.cache; + +/** + * Describes an entry that is stale in the cache + * + * @author Andy + * + */ +public class RefreshableCacheRefreshEvent extends AbstractRefreshableCacheEvent +{ + /** + * @param cacheId + */ + RefreshableCacheRefreshEvent(String cacheId, String key) + { + super(cacheId, key); + } + + /** + * + */ + private static final long serialVersionUID = -8011932788039835334L; + +} diff --git a/core/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshedEvent.java b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshedEvent.java new file mode 100644 index 0000000000..ef9833a126 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshedEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.cache; + +/** + * Describes a new entry has been inserted in the cache. + * + * @author Andy + * + */ +public class RefreshableCacheRefreshedEvent extends AbstractRefreshableCacheEvent +{ + + /** + * + */ + private static final long serialVersionUID = 2352511592269578075L; + + /** + * @param cacheId + * @param key - the key/ tennant id + */ + RefreshableCacheRefreshedEvent(String cacheId, String key) + { + super(cacheId, key); + } + +} diff --git a/core/src/main/java/org/alfresco/util/collections/CollectionUtils.java b/core/src/main/java/org/alfresco/util/collections/CollectionUtils.java new file mode 100644 index 0000000000..fc82aa000a --- /dev/null +++ b/core/src/main/java/org/alfresco/util/collections/CollectionUtils.java @@ -0,0 +1,647 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util.collections; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.TreeSet; + +import org.alfresco.util.Pair; + +/** + * @author Nick Smith + * @author Neil Mc Erlean + * @since 4.0 + */ +public abstract class CollectionUtils +{ + public static boolean isEmpty(Map map) + { + if (map == null) + { + return true; + } + return map.isEmpty(); + } + + public static boolean isEmpty(Collection items) + { + if(items == null) + { + return true; + } + return items.isEmpty(); + } + + /** + * This method merges two sets returning the union of both sets. + * + * @param first first set. can be null. + * @param second second set. can be null. + * @return the union of both sets. will not be null + */ + public static Set nullSafeMerge(Set first, Set second) + { + return nullSafeMerge(first, second, false); + } + + /** + * This method merges two sets returning the union of both sets. + * + * @param first first set. can be null. + * @param second second set. can be null. + * @param emptyResultIsNull if the result is empty, should we return null? + * @return the union of both sets or null. + */ + public static Set nullSafeMerge(Set first, Set second, boolean emptyResultIsNull) + { + Set result = new HashSet(); + + if (first != null) result.addAll(first); + if (second != null) result.addAll(second); + + if (result.isEmpty() && emptyResultIsNull) + { + result = null; + } + return result; + } + + /** + * This method merges two maps returning the union of both maps. + * + * @param first first map. can be null. + * @param second second map. can be null. + * @return the union of both maps. will not be null + */ + public static Map nullSafeMerge(Map first, Map second) + { + return nullSafeMerge(first, second, false); + } + + /** + * This method merges two maps returning the union of both maps. + * + * @param first first map. can be null. + * @param second second map. can be null. + * @param emptyResultIsNull if the result is empty, should we return null? + * @return the union of both maps, or null. + */ + public static Map nullSafeMerge(Map first, Map second, boolean emptyResultIsNull) + { + Map result = new HashMap(); + + if (first != null) result.putAll(first); + if (second != null) result.putAll(second); + + if (result.isEmpty() && emptyResultIsNull) + { + result = null; + } + return result; + } + + /** + * This method joins two lists returning the a single list consisting of the first followed by the second. + * + * @param first first list. can be null. + * @param second second list. can be null. + * @return the concatenation of both lists. will not be null + */ + public static List nullSafeAppend(List first, List second) + { + return nullSafeAppend(first, second, false); + } + + /** + * This method joins two lists returning the a single list consisting of the first followed by the second. + * + * @param first first list. can be null. + * @param second second list. can be null. + * @param emptyResultIsNull if the result is empty, should we return null? + * @return the concatenation of both lists or null + */ + public static List nullSafeAppend(List first, List second, boolean emptyResultIsNull) + { + List result = new ArrayList(); + + if (first != null) result.addAll(first); + if (second != null) result.addAll(second); + + if (result.isEmpty() && emptyResultIsNull) + { + result = null; + } + return result; + } + + public static final Function TO_STRING_TRANSFORMER = new Function() + { + public String apply(Object value) + { + return value.toString(); + } + }; + + /** + * Converts a {@link Collection} of values of type F to a {@link Serializable} {@link List} of values of type T. + * Filters out all values converted to null. + * @param From type + * @param To type + * @param values the values to convert. + * @param transformer Used to convert values. + * @return List + */ + public static List transform(Collection values, Function transformer) + { + if(values == null || values.isEmpty()) + { + return new ArrayList(); + } + List results = new ArrayList(values.size()); + for (F value : values) + { + T result = transformer.apply(value); + if(result != null) + { + results.add(result); + } + } + return results; + } + + /** + * Converts a {@link Map} having keys of type F to a new {@link Map} instance having keys of type T. The object references + * in the value set are copied to the transformed map, thus reusing the same objects. + * @param From type + * @param To type + * @param The value type of the before and after maps. + * @param map the map to convert. + * @param transformer Used to convert keys. + * @return a new Map instance with transformed keys and unchanged values. These values will be the same object references. + */ + public static Map transformKeys(Map map, Function transformer) + { + if(map == null || map.isEmpty()) + { + return new HashMap(); + } + Map results = new HashMap(map.size()); + for (Entry entry : map.entrySet()) + { + T transformedKey = transformer.apply(entry.getKey()); + results.put(transformedKey, entry.getValue()); + } + return results; + } + + /** + * Converts a {@link Collection} of values of type F to a {@link Serializable} {@link List} of values of type T. + * Filters out all values converted to null. + * @param From type + * @param To type + * @param values the values to convert. + * @param transformer Used to convert values. + * @return List + */ + public static List transform(Function transformer, F... values) + { + if(values == null || values.length<1) + { + return new ArrayList(); + } + List results = new ArrayList(values.length); + for (F value : values) + { + T result = transformer.apply(value); + if(result != null) + { + results.add(result); + } + } + return results; + } + + public static List toListOfStrings(Collection values) + { + return transform(values, TO_STRING_TRANSFORMER); + } + + /** + * This utility method converts a vararg of Objects into a Set. + * + * @param objects the objects to be added to the set + * @return a Set of objects (any equal objects will of course not be duplicated) + * @throws ClassCastException if any of the supplied objects are not of type T. + */ + public static Set asSet(T... objects) + { + Set result = new HashSet<>(); + for (T obj : objects) + { + result.add(obj); + } + + return result; + } + + /** + * This utility method converts a vararg of Objects into a Set. + * + * @param clazz the Set type to return. + * @param objects the objects to be added to the set + * @return a Set of objects (any equal objects will of course not be duplicated) + * @throws ClassCastException if any of the supplied objects are not of type T. + */ + public static Set asSet(Class clazz, Object... objects) + { + Set result = new HashSet(); + for (Object obj : objects) + { + @SuppressWarnings("unchecked") + T cast = (T) obj; + result.add(cast); + } + + return result; + } + + /** + * Returns a filtered {@link List} of values. Only values for which filter.apply(T) returns true are included in the {@link List} or returned values. + * @param The type of the {@link Collection} + * @param values the {@link Collection} to be filtered. + * @param filter the {@link Function} used to filter the {@link Collection}. + * @return the filtered {@link List} of values. + */ + public static List filter(Collection values, final Function filter) + { + return transform(values, new Function() + { + public T apply(T value) + { + if(filter.apply(value)) + { + return value; + } + return null; + } + }); + } + + /** + * This method flattens the provided collection of collections of values into a single + * {@code List} object containing each of the elements from the provided sub-collections. + *

+ * For example, {@code flatten( [1, 2], [3], [], [4, 5, 6] )} would produce a List like {@code [1, 2, 3, 4, 5, 6]}. + * Here, "[]" represents any Java collection. + * + * @param the element type of the collections. Note that this must be the same for all collections. + * @param values a collection of collections of elements to be flattened. + * @return a List containing the flattened elements. + */ + public static List flatten(Collection> values) + { + List results = new ArrayList(); + for (Collection collection : values) + { + if (collection != null) { results.addAll(collection); } + } + return results; + } + + /** + * See {@link #flatten(Collection)} + * @param collections a vararg of Collection objects to be flattened into a list. + * @return A flat List containing the elements of the provided collections. + * @since 5.0 + */ + @SafeVarargs + public static List flatten(Collection... collections) + { + List> listOfCollections = Arrays.asList(collections); + return CollectionUtils.flatten(listOfCollections); + } + + public static List transformFlat(Collection values, Function> transformer) + { + return flatten(transform(values, transformer)); + } + + /** + * Finds the first value for which acceptor returns true. + * @param T + * @param values Collection + * @param acceptor Function + * @return returns accepted value or null. + */ + public static T findFirst(Collection values, Function acceptor) + { + if (values != null ) + { + for (T value : values) + { + if (acceptor.apply(value)) + { + return value; + } + } + } + return null; + } + + /** + * Returns an immutable Serializable Set containing the values. + * @param T + * @param values T... + * @return Set + */ + public static Set unmodifiableSet(T... values) + { + return unmodifiableSet(Arrays.asList(values)); + } + + /** + * Returns an immutable Serializable Set containing the values. + * @param T + * @param values Collection + * @return Set + */ + public static Set unmodifiableSet(Collection values) + { + TreeSet set = new TreeSet(values); + return Collections.unmodifiableSet(set); + } + + /** + * @param values Collection + * @param transformer Function + * @return Map + */ + public static Map transformToMap(Collection values, + Function transformer) + { + if(isEmpty(values)) + { + return Collections.emptyMap(); + } + HashMap results = new HashMap(values.size()); + for (F value : values) + { + T result = transformer.apply(value); + results.put(value, result); + } + return results; + } + + /** + * This method can be used to filter a Map. Any keys in the supplied map, for which the supplied {@link Function filter function} + * returns true, will be included in the resultant Map, else they will not. + * + * @param map the map whose entries are to be filtered. + * @param filter the filter function which is applied to the key. + * @return a filtered map. + */ + public static Map filterKeys(Map map, Function filter) + { + Map results = new HashMap(); + Set> entries = map.entrySet(); + for (Entry entry : entries) + { + K key = entry.getKey(); + if(filter.apply(key)) + { + results.put(key, entry.getValue()); + } + } + return results; + } + + public static Map transform(Map map, + Function, Pair> transformer ) + { + Map results = new HashMap(map.size()); + for (Entry entry : map.entrySet()) + { + Pair pair = transformer.apply(entry); + if(pair!=null) + { + TK key = pair.getFirst(); + if (key != null) + { + results.put(key, pair.getSecond()); + } + } + } + return results; + } + + public static Filter containsFilter(final Collection values) + { + return new Filter() + { + public Boolean apply(T value) + { + return values.contains(value); + } + }; + } + + /** + * This method returns a new ArrayList which is the intersection of the two List parameters, based on {@link Object#equals(Object) equality} + * of their elements. + * The intersection list will contain elements in the order they have in list1 and any references in the resultant list will be + * to elements within list1 also. + * + * @return a new ArrayList whose values represent the intersection of the two Lists. + */ + public static List intersect(List list1, List list2) + { + if (list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) + { + return Collections.emptyList(); + } + + List result = new ArrayList(); + result.addAll(list1); + + result.retainAll(list2); + + return result; + } + + /** + * This method returns a new HashMap which is the intersection of the two Map parameters, based on {@link Object#equals(Object) equality} + * of their entries. + * Any references in the resultant map will be to elements within map1. + * + * @return a new HashMap whose values represent the intersection of the two Maps. + */ + public static Map intersect(Map map1, Map map2) + { + if (map1 == null || map1.isEmpty() || map2 == null || map2.isEmpty()) + { + return Collections.emptyMap(); + } + + // We now know neither map is null. + Map result = new HashMap(); + for (Map.Entry item : map1.entrySet()) + { + V value = map2.get(item.getKey()); + if (value != null && value.equals(item.getValue())) + { + result.put(item.getKey(), item.getValue()); + } + } + + return result; + } + + /** + * This method returns a new HashSet which is the intersection of the two Set parameters, based on {@link Object#equals(Object) equality} + * of their elements. + * Any references in the resultant set will be to elements within set1. + * + * @return a new HashSet whose values represent the intersection of the two Sets. + */ + public static Set intersect(Set set1, Set set2) + { + if (set1 == null || set1.isEmpty() || set2 == null || set2.isEmpty()) + { + return Collections.emptySet(); + } + + Set result = new HashSet(); + result.addAll(set1); + + result.retainAll(set2); + + return result; + } + + /** + * Creates a new sorted map, based on the values from the given map and Comparator. + * + * @param map the map which needs to be sorted + * @param valueComparator the Comparator + * @return a new sorted map + */ + public static Map sortMapByValue(Map map, Comparator> valueComparator) + { + if (map == null) + { + return Collections.emptyMap(); + } + + List> entriesList = new LinkedList<>(map.entrySet()); + + // Sort based on the map's values + Collections.sort(entriesList, valueComparator); + + Map orderedMap = new LinkedHashMap<>(entriesList.size()); + for (Entry entry : entriesList) + { + orderedMap.put(entry.getKey(), entry.getValue()); + } + return orderedMap; + } + + /** + * This method offers convenient conversion from value-based comparators to entry-based comparators + * for use with {@link #sortMapByValue(Map, Comparator)} above. + *

+ * Call it like so: {@code CollectionUtils.toEntryComparator(valueComparator);} + * + * @param valueComparator a comparator which compares the value types from a Map. + * @return a comparator which takes Map.Entry objects from that Map and compares their values. + */ + public static Comparator> toEntryComparator(final Comparator valueComparator) + { + return new Comparator>() + { + @Override public int compare(Entry e1, Entry e2) + { + return valueComparator.compare(e1.getValue(), e2.getValue()); + } + }; + } + + /** + * This method returns a new List instance containing the same element objects as the provided + * list, but with the specified element having been moved left by the specified offset. + *

+ * If the offset would mean that the element would move beyond the start or end of the list, it will + * move only to the end. + * + * @param offset the number of places over which to move the specified element. + * @param element the element to be moved. + * @param list the list to be reordered. + * @return a new List instance containing the ordered elements. + * @throws NoSuchElementException if the list does not contain an element equal to the one specified. + */ + public static List moveLeft(int offset, T element, List list) + { + return moveRight(-offset, element, list); + } + + /** + * This method does the same as {@link #moveLeft(int, Object, List)} but it moves the specified element + * to the right instead of the left. + */ + public static List moveRight(int offset, T element, List list) + { + final int elementIndex = list.indexOf(element); + + if (elementIndex == -1) { throw new NoSuchElementException("Element not found in provided list."); } + + if (offset == 0) + { + return list; + } + else + { + int newElementIndex = elementIndex + offset; + + // Ensure that the element will not move off the end of the list. + if (newElementIndex >= list.size()) { newElementIndex = list.size() - 1; } + else if (newElementIndex < 0) { newElementIndex = 0; } + + List result = new ArrayList<>(list); + result.remove(element); + + result.add(newElementIndex, element); + + return result; + } + } +} diff --git a/core/src/main/java/org/alfresco/util/collections/EntryTransformer.java b/core/src/main/java/org/alfresco/util/collections/EntryTransformer.java new file mode 100644 index 0000000000..720db9b2b1 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/collections/EntryTransformer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util.collections; + +import java.util.Map.Entry; + +import org.alfresco.util.Pair; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public interface EntryTransformer extends Function, Pair> +{ + //NOOP +} diff --git a/core/src/main/java/org/alfresco/util/collections/Filter.java b/core/src/main/java/org/alfresco/util/collections/Filter.java new file mode 100644 index 0000000000..412811355f --- /dev/null +++ b/core/src/main/java/org/alfresco/util/collections/Filter.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util.collections; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public interface Filter extends Function +{ + //NOOP +} diff --git a/core/src/main/java/org/alfresco/util/collections/Function.java b/core/src/main/java/org/alfresco/util/collections/Function.java new file mode 100644 index 0000000000..0150caf352 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/collections/Function.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util.collections; + +/** + * + * @author Nick Smith + * @since 4.0 + * + * @param From type + * @param To type + */ +public interface Function +{ + /** + * Converts a value of type F to a result of type T. + * @param value F + * @return T + */ + T apply(F value); +} diff --git a/core/src/main/java/org/alfresco/util/collections/JsonUtils.java b/core/src/main/java/org/alfresco/util/collections/JsonUtils.java new file mode 100644 index 0000000000..de719834ff --- /dev/null +++ b/core/src/main/java/org/alfresco/util/collections/JsonUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util.collections; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.json.JSONArray; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public class JsonUtils +{ + + @SuppressWarnings("unchecked") + public static List transform(JSONArray values, Function transformer) + { + if(values == null || values.length()<1) + { + return Collections.emptyList(); + } + ArrayList results = new ArrayList(values.length()); + for (int i = 0; i < values.length(); i++) + { + T result = transformer.apply((F)values.opt(i)); + if(result != null) + { + results.add(result); + } + } + return results; + } + + public static List toListOfStrings(JSONArray values) + { + return transform(values, CollectionUtils.TO_STRING_TRANSFORMER); + } +} diff --git a/core/src/main/java/org/alfresco/util/exec/ExecParameterTokenizer.java b/core/src/main/java/org/alfresco/util/exec/ExecParameterTokenizer.java new file mode 100644 index 0000000000..38601c4cbb --- /dev/null +++ b/core/src/main/java/org/alfresco/util/exec/ExecParameterTokenizer.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; + +import org.alfresco.util.Pair; + +/** + * This class is used to tokenize strings used as parameters for {@link RuntimeExec} objects. + * Examples of such strings are as follows (ImageMagick-like parameters): + *

    + *
  • -font Helvetica -pointsize 50
  • + *
  • -font Helvetica -pointsize 50 -draw "circle 100,100 150,150"
  • + *
  • -font Helvetica -pointsize 50 -draw "gravity south fill black text 0,12 'CopyRight'"
  • + *
+ * The first is the simple case which would be parsed into Strings as follows: + * "-font", "Helvetica", "-pointsize", "50" + *

+ * The second is more complex in that it includes a quoted parameter, which would be parsed as a single String: + * "-font", "Helvetica", "-pointsize", "50", "circle 100,100 150,150" + * Note however that the quotation characters will be stripped from the token. + *

+ * The third shows an example with embedded quotation marks, which would parse to: + * "-font", "Helvetica", "-pointsize", "50", "gravity south fill black text 0,12 'CopyRight'" + * In this case, the embedded quotation marks (which must be different from those surrounding the parameter) + * are preserved in the extracted token. + *

+ * The class does not understand escaped quotes such as p1 p2 "a b c \"hello\" d" p4 + * + * @author Neil Mc Erlean + * @since 3.4.2 + */ +public class ExecParameterTokenizer +{ + /** + * The string to be tokenized. + */ + private final String str; + + /** + * The list of tokens, which will take account of quoted sections. + */ + private List tokens; + + public ExecParameterTokenizer(String str) + { + this.str = str; + } + + /** + * This method returns the tokens in a parameter string. + * Any tokens not contained within single or double quotes will be tokenized in the normal + * way i.e. by using whitespace separators and the standard StringTokenizer algorithm. + * Any tokens which are contained within single or double quotes will be returned as single + * String instances and will have their quote marks removed. + *

+ * See above for examples. + * + * @throws NullPointerException if the string to be tokenized was null. + */ + public List getAllTokens() + { + if (this.str == null) + { + throw new NullPointerException("Illegal null string cannot be tokenized."); + } + + if (tokens == null) + { + tokens = new ArrayList(); + + // Preserve original behaviour from RuntimeExec. + if (str.indexOf('\'') == -1 && str.indexOf('"') == -1) + { + // Contains no quotes. + for (StringTokenizer standardTokenizer = new StringTokenizer(str); standardTokenizer.hasMoreTokens(); ) + { + tokens.add(standardTokenizer.nextToken()); + } + } + else + { + // There are either single or double quotes or both. + // So we need to identify the quoted regions within the string. + List> quotedRegions = new ArrayList>(); + + for (Pair next = identifyNextQuotedRegion(str, 0); next != null; ) + { + quotedRegions.add(next); + next = identifyNextQuotedRegion(str, next.getSecond() + 1); + } + + // Now we've got a List of index pairs identifying the quoted regions. + // We need to get substrings of quoted and unquoted blocks, whilst maintaining order. + List substrings = getSubstrings(str, quotedRegions); + + for (Substring r : substrings) + { + tokens.addAll(r.getTokens()); + } + } + } + + return this.tokens; + } + + /** + * The substrings will be a list of quoted and unquoted substrings. + * The unquoted ones need to be further tokenized in the normal way. + * The quoted ones must not be tokenized, but need their quotes stripped off. + */ + private List getSubstrings(String str, List> quotedRegionIndices) + { + List result = new ArrayList(); + + int cursorPosition = 0; + for (Pair nextQuotedRegionIndices : quotedRegionIndices) + { + if (cursorPosition < nextQuotedRegionIndices.getFirst()) + { + int startIndexOfNextQuotedRegion = nextQuotedRegionIndices.getFirst() - 1; + result.add(new UnquotedSubstring(str.substring(cursorPosition, startIndexOfNextQuotedRegion))); + cursorPosition = startIndexOfNextQuotedRegion; + } + result.add(new QuotedSubstring(str.substring(nextQuotedRegionIndices.getFirst(), nextQuotedRegionIndices.getSecond()))); + cursorPosition = nextQuotedRegionIndices.getSecond(); + } + + // We've processed all the quoted regions, but there may be a final unquoted region + if (cursorPosition < str.length() - 1) + { + result.add(new UnquotedSubstring(str.substring(cursorPosition, str.length() - 1))); + } + + return result; + } + + private Pair identifyNextQuotedRegion(String str, int startingIndex) + { + int indexOfNextSingleQuote = str.indexOf('\'', startingIndex); + int indexOfNextDoubleQuote = str.indexOf('"', startingIndex); + + if (indexOfNextSingleQuote == -1 && indexOfNextDoubleQuote == -1) + { + // If there are no more quoted regions + return null; + } + else if (indexOfNextSingleQuote > -1 && indexOfNextDoubleQuote > -1) + { + // If there are both single and double quotes in the remainder of the string + // Then select the closest quote. + int indexOfNextQuote = Math.min(indexOfNextSingleQuote, indexOfNextDoubleQuote); + char quoteChar = str.charAt(indexOfNextQuote); + + return findIndexOfClosingQuote(str, indexOfNextQuote, quoteChar); + } + else + { + // Only one of the quote characters is present. + + int indexOfNextQuote = Math.max(indexOfNextSingleQuote, indexOfNextDoubleQuote); + char quoteChar = str.charAt(indexOfNextQuote); + + return findIndexOfClosingQuote(str, indexOfNextQuote, quoteChar); + } + } + + private Pair findIndexOfClosingQuote(String str, int indexOfStartingQuote, char quoteChar) + { + // So we know which type of quote char we're dealing with. Either ' or ". + // Now we need to find the closing quote. + int indexAfterClosingQuote = str.indexOf(quoteChar, indexOfStartingQuote + 1) + 1; // + 1 to search after opening quote. + 1 to give result including closing quote. + + if (indexAfterClosingQuote == 0) // -1 + 1 + { + // If no closing quote. + throw new IllegalArgumentException("No closing " + quoteChar + "quote in" + str); + } + + return new Pair(indexOfStartingQuote, indexAfterClosingQuote); + } + + /** + * Utility interface for a substring in a parameter string. + */ + public interface Substring + { + /** + * Gets all the tokens in a parameter string. + */ + public List getTokens(); + } + + /** + * A substring that is not surrounded by (single or double) quotes. + */ + public class UnquotedSubstring implements Substring + { + private final String regionString; + public UnquotedSubstring(String str) + { + this.regionString = str; + } + + public List getTokens() + { + StringTokenizer t = new StringTokenizer(regionString); + List result = new ArrayList(); + while (t.hasMoreTokens()) + { + result.add(t.nextToken()); + } + return result; + } + + public String toString() + { + return UnquotedSubstring.class.getSimpleName() + ": '" + regionString + '\''; + } + } + + /** + * A substring that is surrounded by (single or double) quotes. + */ + public class QuotedSubstring implements Substring + { + private final String regionString; + public QuotedSubstring(String str) + { + this.regionString = str; + } + + public List getTokens() + { + String stringWithoutQuotes = regionString.substring(1, regionString.length() -1); + return Arrays.asList(new String[] {stringWithoutQuotes}); + } + + public String toString() + { + return QuotedSubstring.class.getSimpleName() + ": '" + regionString + '\''; + } + } +} diff --git a/core/src/main/java/org/alfresco/util/exec/RuntimeExec.java b/core/src/main/java/org/alfresco/util/exec/RuntimeExec.java new file mode 100644 index 0000000000..3db9814d9b --- /dev/null +++ b/core/src/main/java/org/alfresco/util/exec/RuntimeExec.java @@ -0,0 +1,1005 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.Timer; +import java.util.TimerTask; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This acts as a session similar to the java.lang.Process, but + * logs the system standard and error streams. + *

+ * The bean can be configured to execute a command directly, or be given a map + * of commands keyed by the os.name Java system property. In this map, + * the default key that is used when no match is found is the + * {@link #KEY_OS_DEFAULT *} key. + *

+ * Use the {@link #setProcessDirectory(String) processDirectory} property to change the default location + * from which the command executes. The process's environment can be configured using the + * {@link #setProcessProperties(Map) processProperties} property. + *

+ * Commands may use placeholders, e.g. + *


+ *    find
+ *    -name
+ *    ${filename}
+ * 
+ * The filename property will be substituted for any supplied value prior to + * each execution of the command. Currently, no checks are made to get or check the + * properties contained within the command string. It is up to the client code to + * dynamically extract the properties required if the required properties are not + * known up front. + *

+ * Sometimes, a variable may contain several arguments. . In this case, the arguments + * need to be tokenized using a standard StringTokenizer. To force tokenization + * of a value, use: + *


+ *    SPLIT:${userArgs}
+ * 
+ * You should not use this just to split up arguments that are known to require tokenization + * up front. The SPLIT: directive works for the entire argument and will not do anything + * if it is not at the beginning of the argument. Do not use SPLIT: to break up arguments + * that are fixed, so avoid doing this: + *

+ *    SPLIT:ls -lih
+ * 
+ * Instead, break the command up explicitly: + *

+ *    ls
+ *    -lih
+ * 
+ * + * Tokenization of quoted parameter values is handled by {@link ExecParameterTokenizer}, which + * describes the support in more detail. + * + * @author Derek Hulley + */ +public class RuntimeExec +{ + /** the key to use when specifying a command for any other OS: * */ + public static final String KEY_OS_DEFAULT = "*"; + + private static final String KEY_OS_NAME = "os.name"; + private static final int BUFFER_SIZE = 1024; + private static final String VAR_OPEN = "${"; + private static final String VAR_CLOSE = "}"; + private static final String DIRECTIVE_SPLIT = "SPLIT:"; + + private static Log logger = LogFactory.getLog(RuntimeExec.class); + private static Log transformerDebugLogger = LogFactory.getLog("org.alfresco.repo.content.transform.TransformerDebug"); + + private String[] command; + private Charset charset; + private boolean waitForCompletion; + private Map defaultProperties; + private String[] processProperties; + private File processDirectory; + private Set errCodes; + private Timer timer = new Timer(true); + + /** + * Default constructor. Initialize this instance by setting individual properties. + */ + public RuntimeExec() + { + this.charset = Charset.defaultCharset(); + this.waitForCompletion = true; + defaultProperties = Collections.emptyMap(); + processProperties = null; + processDirectory = null; + + // set default error codes + this.errCodes = new HashSet(2); + errCodes.add(1); + errCodes.add(2); + } + + public String toString() + { + + StringBuffer sb = new StringBuffer(256); + sb.append("RuntimeExec:\n") + .append(" command: "); + if (command == null) + { + // command is 'null', so there's nothing to toString + sb.append("'null'\n"); + } + else + { + for (String cmdStr : command) + { + sb.append(cmdStr).append(" "); + } + sb.append("\n"); + } + sb.append(" env props: ").append(Arrays.toString(processProperties)).append("\n") + .append(" dir: ").append(processDirectory).append("\n") + .append(" os: ").append(System.getProperty(KEY_OS_NAME)).append("\n"); + return sb.toString(); + } + + /** + * Set the command to execute regardless of operating system + * + * @param command an array of strings representing the command (first entry) and arguments + * + * @since 3.0 + */ + public void setCommand(String[] command) + { + this.command = command; + } + + /** + * Sets the assumed charset of OUT and ERR streams generated by the executed command. + * This defaults to the system default charset: {@link Charset#defaultCharset()}. + * + * @param charsetCode a supported character set code + * @throws UnsupportedCharsetException if the characterset code is not recognised by Java + */ + public void setCharset(String charsetCode) + { + this.charset = Charset.forName(charsetCode); + } + + /** + * Set whether to wait for completion of the command or not. If there is no wait for completion, + * then the return value of out and err buffers cannot be relied upon as the + * command may still be in progress. Failure is therefore not possible unless the calling thread + * waits for execution. + * + * @param waitForCompletion true (default) is to wait for the command to exit, + * or false to just return an exit code of 0 and whatever + * output is available at that point. + * + * @since 2.1 + */ + public void setWaitForCompletion(boolean waitForCompletion) + { + this.waitForCompletion = waitForCompletion; + } + + /** + * Supply a choice of commands to execute based on a mapping from the os.name system + * property to the command to execute. The {@link #KEY_OS_DEFAULT *} key can be used + * to get a command where there is not direct match to the operating system key. + *

+ * Each command is an array of strings, the first of which represents the command and all subsequent + * entries in the array represent the arguments. All elements of the array will be checked for + * the presence of any substitution parameters (e.g. '{dir}'). The parameters can be set using the + * {@link #setDefaultProperties(Map) defaults} or by passing the substitution values into the + * {@link #execute(Map)} command. + *

+ * If parameters passed may be multiple arguments, or if the values provided in the map are themselves + * collections of arguments (not recommended), then prefix the value with SPLIT: to ensure that + * the value is tokenized before being passed to the command. Any values that are not split, will be + * passed to the command as single arguments. For example:
+ * 'SPLIT: dir . ..' becomes 'dir', '.' and '..'.
+ * 'SPLIT: dir ${path}' (if path is '. ..') becomes 'dir', '.' and '..'.
+ * The splitting occurs post-subtitution. Where the arguments are known, it is advisable to avoid + * SPLIT:. + * + * @param commandsByOS a map of command string arrays, keyed by operating system names + * + * @see #setDefaultProperties(Map) + * + * @since 3.0 + */ + public void setCommandsAndArguments(Map commandsByOS) + { + // get the current OS + String serverOs = System.getProperty(KEY_OS_NAME); + // attempt to find a match + String[] command = commandsByOS.get(serverOs); + if (command == null) + { + // go through the commands keys, looking for one that matches by regular expression matching + for (String osName : commandsByOS.keySet()) + { + // Ignore * options. It is dealt with later. + if (osName.equals(KEY_OS_DEFAULT)) + { + continue; + } + // Do regex match + if (serverOs.matches(osName)) + { + command = commandsByOS.get(osName); + break; + } + } + // if there is still no command, then check for the wildcard + if (command == null) + { + command = commandsByOS.get(KEY_OS_DEFAULT); + } + } + // check + if (command == null) + { + throw new AlfrescoRuntimeException( + "No command found for OS " + serverOs + " or '" + KEY_OS_DEFAULT + "': \n" + + " commands: " + commandsByOS); + } + this.command = command; + } + + /** + * Supply a choice of commands to execute based on a mapping from the os.name system + * property to the command to execute. The {@link #KEY_OS_DEFAULT *} key can be used + * to get a command where there is not direct match to the operating system key. + * + * @param commandsByOS a map of command string keyed by operating system names + * + * @deprecated Use {@link #setCommandsAndArguments(Map)} + */ + public void setCommandMap(Map commandsByOS) + { + // This is deprecated, so issue a warning + logger.warn( + "The bean RuntimeExec property 'commandMap' has been deprecated;" + + " use 'commandsAndArguments' instead. See https://issues.alfresco.com/jira/browse/ETHREEOH-579."); + Map fixed = new LinkedHashMap(7); + for (Map.Entry entry : commandsByOS.entrySet()) + { + String os = entry.getKey(); + String unparsedCmd = entry.getValue(); + StringTokenizer tokenizer = new StringTokenizer(unparsedCmd); + String[] cmd = new String[tokenizer.countTokens()]; + for (int i = 0; i < cmd.length; i++) + { + cmd[i] = tokenizer.nextToken(); + } + fixed.put(os, cmd); + } + setCommandsAndArguments(fixed); + } + + /** + * Set the default command-line properties to use when executing the command. + * These are properties that substitute variables defined in the command string itself. + * Properties supplied during execution will overwrite the default properties. + *

+ * null properties will be treated as an empty string for substitution + * purposes. + * + * @param defaultProperties property values + */ + public void setDefaultProperties(Map defaultProperties) + { + this.defaultProperties = defaultProperties; + } + + /** + * Set additional runtime properties (environment properties) that will used + * by the executing process. + *

+ * Any keys or properties that start and end with ${...} will be removed on the assumption + * that these are unset properties. null values are translated to empty strings. + * All keys and values are trimmed of leading and trailing whitespace. + * + * @param processProperties Runtime process properties + * + * @see Runtime#exec(String, String[], java.io.File) + */ + public void setProcessProperties(Map processProperties) + { + ArrayList processPropList = new ArrayList(processProperties.size()); + boolean hasPath = false; + String systemPath = System.getenv("PATH"); + for (Map.Entry entry : processProperties.entrySet()) + { + String key = entry.getKey(); + String value = entry.getValue(); + if (key == null) + { + continue; + } + if (value == null) + { + value = ""; + } + key = key.trim(); + value = value.trim(); + if (key.startsWith(VAR_OPEN) && key.endsWith(VAR_CLOSE)) + { + continue; + } + if (value.startsWith(VAR_OPEN) && value.endsWith(VAR_CLOSE)) + { + continue; + } + // If a path is specified, prepend it to the existing path + if (key.equals("PATH")) + { + if (systemPath != null && systemPath.length() > 0) + { + processPropList.add(key + "=" + value + File.pathSeparator + systemPath); + } + else + { + processPropList.add(key + "=" + value); + } + hasPath = true; + } + else + { + processPropList.add(key + "=" + value); + } + } + // If a path was not specified, inherit the current one + if (!hasPath && systemPath != null && systemPath.length() > 0) + { + processPropList.add("PATH=" + systemPath); + } + this.processProperties = processPropList.toArray(new String[processPropList.size()]); + } + + /** + * Adds a property to existed processProperties. + * Property should not be null or empty. + * If property with the same value already exists then no change is made. + * If property exists with a different value then old value is replaced with the new one. + * @param name - property name + * @param value - property value + */ + public void setProcessProperty(String name, String value) + { + boolean set = false; + + if (name == null || value == null) + return; + + name = name.trim(); + value = value.trim(); + + if (name.isEmpty() || value.isEmpty()) + return; + + String property = name + "=" + value; + + for (String prop : this.processProperties) + { + if (prop.equals(property)) + { + set = true; + break; + } + + if (prop.startsWith(name)) + { + String oldValue = prop.split("=")[1]; + prop.replace(oldValue, value); + set = true; + } + } + + if (!set) + { + String[] existedProperties = this.processProperties; + int epl = existedProperties.length; + String[] newProperties = Arrays.copyOf(existedProperties, epl + 1); + newProperties[epl] = property; + this.processProperties = newProperties; + set = true; + } + } + + + /** + * Set the runtime location from which the command is executed. + *

+ * If the value is an unsubsititued variable (${...}) then it is ignored. + * If the location is not visible at the time of setting, a warning is issued only. + * + * @param processDirectory the runtime location from which to execute the command + */ + public void setProcessDirectory(String processDirectory) + { + if (processDirectory.startsWith(VAR_OPEN) && processDirectory.endsWith(VAR_CLOSE)) + { + this.processDirectory = null; + } + else + { + this.processDirectory = new File(processDirectory); + if (!this.processDirectory.exists()) + { + logger.warn( + "The runtime process directory is not visible when setting property 'processDirectory': \n" + + this); + } + } + } + + /** + * A comma or space separated list of values that, if returned by the executed command, + * indicate an error value. This defaults to "1, 2". + * + * @param errCodesStr the error codes for the execution + */ + public void setErrorCodes(String errCodesStr) + { + errCodes.clear(); + StringTokenizer tokenizer = new StringTokenizer(errCodesStr, " ,"); + while(tokenizer.hasMoreElements()) + { + String errCodeStr = tokenizer.nextToken(); + // attempt to convert it to an integer + try + { + int errCode = Integer.parseInt(errCodeStr); + this.errCodes.add(errCode); + } + catch (NumberFormatException e) + { + throw new AlfrescoRuntimeException( + "Property 'errorCodes' must be comma-separated list of integers: " + errCodesStr); + } + } + } + + /** + * Executes the command using the default properties + * + * @see #execute(Map) + */ + public ExecutionResult execute() + { + return execute(defaultProperties); + } + + /** + * Executes the statement that this instance was constructed with. + * + * @param properties the properties that the command might be executed with. + * null properties will be treated as an empty string for substitution + * purposes. + * + * @return Returns the full execution results + */ + public ExecutionResult execute(Map properties) + { + return execute(properties, -1); + } + + /** + * Executes the statement that this instance was constructed with an optional + * timeout after which the command is asked to + * + * @param properties the properties that the command might be executed with. + * null properties will be treated as an empty string for substitution + * purposes. + * @param timeoutMs a timeout after which {@link Process#destroy()} is called. + * ignored if less than or equal to zero. Note this method does not guarantee + * to terminate the process (it is not a kill -9). + * + * @return Returns the full execution results + */ + public ExecutionResult execute(Map properties, final long timeoutMs) + { + int defaultFailureExitValue = errCodes.size() > 0 ? ((Integer)errCodes.toArray()[0]) : 1; + + // check that the command has been set + if (command == null) + { + throw new AlfrescoRuntimeException("Runtime command has not been set: \n" + this); + } + + // create the properties + Runtime runtime = Runtime.getRuntime(); + Process process = null; + String[] commandToExecute = null; + try + { + // execute the command with full property replacement + commandToExecute = getCommand(properties); + final Process thisProcess = runtime.exec(commandToExecute, processProperties, processDirectory); + process = thisProcess; + if (timeoutMs > 0) + { + final String[] command = commandToExecute; + timer.schedule(new TimerTask() + { + @Override + public void run() + { + // Only try to kill the process if it is still running + try + { + thisProcess.exitValue(); + } + catch (IllegalThreadStateException stillRunning) + { + if (transformerDebugLogger.isDebugEnabled()) + { + transformerDebugLogger.debug("Process has taken too long ("+ + (timeoutMs/1000)+" seconds). Killing process "+ + Arrays.deepToString(command)); + } + thisProcess.destroy(); + } + } + }, timeoutMs); + } + } + catch (IOException e) + { + // The process could not be executed here, so just drop out with an appropriate error state + String execOut = ""; + String execErr = e.getMessage(); + int exitValue = defaultFailureExitValue; + ExecutionResult result = new ExecutionResult(null, commandToExecute, errCodes, exitValue, execOut, execErr); + logFullEnvironmentDump(result); + return result; + } + + // create the stream gobblers + InputStreamReaderThread stdOutGobbler = new InputStreamReaderThread(process.getInputStream(), charset); + InputStreamReaderThread stdErrGobbler = new InputStreamReaderThread(process.getErrorStream(), charset); + + // start gobbling + stdOutGobbler.start(); + stdErrGobbler.start(); + + // wait for the process to finish + int exitValue = 0; + try + { + if (waitForCompletion) + { + exitValue = process.waitFor(); + } + } + catch (InterruptedException e) + { + // process was interrupted - generate an error message + stdErrGobbler.addToBuffer(e.toString()); + exitValue = defaultFailureExitValue; + } + + if (waitForCompletion) + { + // ensure that the stream gobblers get to finish + stdOutGobbler.waitForCompletion(); + stdErrGobbler.waitForCompletion(); + } + + // get the stream values + String execOut = stdOutGobbler.getBuffer(); + String execErr = stdErrGobbler.getBuffer(); + + // construct the return value + ExecutionResult result = new ExecutionResult(process, commandToExecute, errCodes, exitValue, execOut, execErr); + + // done + logFullEnvironmentDump(result); + return result; + } + + /** + * Dump the full environment in debug mode + */ + private void logFullEnvironmentDump(ExecutionResult result) + { + if (logger.isTraceEnabled()) + { + StringBuilder sb = new StringBuilder(); + sb.append(result); + + // Environment variables modified by Alfresco + if (processProperties != null && processProperties.length > 0) + { + sb.append("\n modified environment: "); + for (int i=0; i envVariables = System.getenv(); + for (Map.Entry entry : envVariables.entrySet()) + { + String name = entry.getKey(); + String value = entry.getValue(); + sb.append("\n "); + sb.append(name + "=" + value); + } + + logger.trace(sb); + } + else if (logger.isDebugEnabled()) + { + logger.debug(result); + } + + // close output stream (connected to input stream of native subprocess) + } + + /** + * @return Returns the command that will be executed if no additional properties + * were to be supplied + */ + public String[] getCommand() + { + return getCommand(defaultProperties); + } + + /** + * Get the command that will be executed post substitution. + *

+ * null properties will be treated as an empty string for substitution + * purposes. + * + * @param properties the properties that the command might be executed with + * @return Returns the command that will be executed should the additional properties + * be supplied + */ + public String[] getCommand(Map properties) + { + Map execProperties = null; + if (properties == defaultProperties) + { + // we are just using the default properties + execProperties = defaultProperties; + } + else + { + execProperties = new HashMap(defaultProperties); + // overlay the supplied properties + execProperties.putAll(properties); + } + // Perform the substitution for each element of the command + ArrayList adjustedCommandElements = new ArrayList(20); + for (int i = 0; i < command.length; i++) + { + StringBuilder sb = new StringBuilder(command[i]); + for (Map.Entry entry : execProperties.entrySet()) + { + String key = entry.getKey(); + String value = entry.getValue(); + // ignore null + if (value == null) + { + value = ""; + } + // progressively replace the property in the command + key = (VAR_OPEN + key + VAR_CLOSE); + int index = sb.indexOf(key); + while (index > -1) + { + // replace + sb.replace(index, index + key.length(), value); + // get the next one + index = sb.indexOf(key, index + 1); + } + } + String adjustedValue = sb.toString(); + // Now SPLIT: it + if (adjustedValue.startsWith(DIRECTIVE_SPLIT)) + { + String unsplitAdjustedValue = sb.substring(DIRECTIVE_SPLIT.length()); + + // There may be quoted arguments here (see ALF-7482) + ExecParameterTokenizer quoteAwareTokenizer = new ExecParameterTokenizer(unsplitAdjustedValue); + List tokens = quoteAwareTokenizer.getAllTokens(); + adjustedCommandElements.addAll(tokens); + } + else + { + adjustedCommandElements.add(adjustedValue); + } + } + // done + return adjustedCommandElements.toArray(new String[adjustedCommandElements.size()]); + } + + /** + * Object to carry the results of an execution to the caller. + * + * @author Derek Hulley + */ + public static class ExecutionResult + { + private final Process process; + private final String[] command; + private final Set errCodes; + private final int exitValue; + private final String stdOut; + private final String stdErr; + + /** + * + * @param process the process attached to Java - null is allowed + */ + private ExecutionResult( + final Process process, + final String[] command, + final Set errCodes, + final int exitValue, + final String stdOut, + final String stdErr) + { + this.process = process; + this.command = command; + this.errCodes = errCodes; + this.exitValue = exitValue; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + @Override + public String toString() + { + String out = stdOut.length() > 250 ? stdOut.substring(0, 250) : stdOut; + String err = stdErr.length() > 250 ? stdErr.substring(0, 250) : stdErr; + + StringBuilder sb = new StringBuilder(128); + sb.append("Execution result: \n") + .append(" os: ").append(System.getProperty(KEY_OS_NAME)).append("\n") + .append(" command: ");appendCommand(sb, command).append("\n") + .append(" succeeded: ").append(getSuccess()).append("\n") + .append(" exit code: ").append(exitValue).append("\n") + .append(" out: ").append(out).append("\n") + .append(" err: ").append(err); + return sb.toString(); + } + + /** + * Appends the command in a form that make running from the command line simpler. + * It is not a real attempt at making a command given all the operating system + * and shell options, but makes copy, paste and edit a bit simpler. + */ + private StringBuilder appendCommand(StringBuilder sb, String[] command) + { + boolean arg = false; + for (String element: command) + { + if (element == null) + { + continue; + } + + if (arg) + { + sb.append(' '); + } + else + { + arg = true; + } + + boolean escape = element.indexOf(' ') != -1 || element.indexOf('>') != -1; + if (escape) + { + sb.append("\""); + } + sb.append(element); + if (escape) + { + sb.append("\""); + } + } + return sb; + } + + /** + * A helper method to force a kill of the process that generated this result. This is + * useful in cases where the process started is not expected to exit, or doesn't exit + * quickly. If the {@linkplain RuntimeExec#setWaitForCompletion(boolean) "wait for completion"} + * flag is false then the process may still be running when this result is returned. + * + * @return + * true if the process was killed, otherwise false + */ + public boolean killProcess() + { + if (process == null) + { + return true; + } + try + { + process.destroy(); + return true; + } + catch (Throwable e) + { + logger.warn(e.getMessage()); + return false; + } + } + + /** + * @param exitValue the command exit value + * @return Returns true if the code is a listed failure code + * + * @see #setErrorCodes(String) + */ + private boolean isFailureCode(int exitValue) + { + return errCodes.contains((Integer)exitValue); + } + + /** + * @return Returns true if the command was deemed to be successful according to the + * failure codes returned by the execution. + */ + public boolean getSuccess() + { + return !isFailureCode(exitValue); + } + + public int getExitValue() + { + return exitValue; + } + + public String getStdOut() + { + return stdOut; + } + + public String getStdErr() + { + return stdErr; + } + } + + /** + * Gobbles an InputStream and writes it into a + * StringBuffer + *

+ * The reading of the input stream is buffered. + */ + public static class InputStreamReaderThread extends Thread + { + private final InputStream is; + private final Charset charset; + private final StringBuffer buffer; // we require the synchronization + private boolean completed; + + /** + * @param is an input stream to read - it will be wrapped in a buffer + * for reading + */ + public InputStreamReaderThread(InputStream is, Charset charset) + { + super(); + setDaemon(true); // must not hold up the VM if it is terminating + this.is = is; + this.charset = charset; + this.buffer = new StringBuffer(BUFFER_SIZE); + this.completed = false; + } + + public synchronized void run() + { + completed = false; + + byte[] bytes = new byte[BUFFER_SIZE]; + InputStream tempIs = null; + try + { + tempIs = new BufferedInputStream(is, BUFFER_SIZE); + int count = -2; + while (count != -1) + { + // do we have something previously read? + if (count > 0) + { + String toWrite = new String(bytes, 0, count, charset.name()); + buffer.append(toWrite); + } + // read the next set of bytes + count = tempIs.read(bytes); + } + // done + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Unable to read stream", e); + } + finally + { + // close the input stream + if (tempIs != null) + { + try + { + tempIs.close(); + } + catch (Exception e) + { + } + } + // The thread has finished consuming the stream + completed = true; + // Notify waiters + this.notifyAll(); // Note: Method is synchronized + } + } + + /** + * Waits for the run to complete. + *

+ * Remember to start the thread first + */ + public synchronized void waitForCompletion() + { + while (!completed) + { + try + { + // release our lock and wait a bit + this.wait(1000L); // 200 ms + } + catch (InterruptedException e) + { + } + } + } + + /** + * @param msg the message to add to the buffer + */ + public void addToBuffer(String msg) + { + buffer.append(msg); + } + + public boolean isComplete() + { + return completed; + } + + /** + * @return Returns the current state of the buffer + */ + public String getBuffer() + { + return buffer.toString(); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/exec/RuntimeExecBootstrapBean.java b/core/src/main/java/org/alfresco/util/exec/RuntimeExecBootstrapBean.java new file mode 100644 index 0000000000..b1293f16fd --- /dev/null +++ b/core/src/main/java/org/alfresco/util/exec/RuntimeExecBootstrapBean.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.alfresco.util.bean.BooleanBean; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +/** + * Application bootstrap bean that is able to execute one or more + * native executable statements upon startup and shutdown. + * + * @author Derek Hulley + */ +public class RuntimeExecBootstrapBean extends AbstractLifecycleBean +{ + private static Log logger = LogFactory.getLog(RuntimeExecBootstrapBean.class); + + private List startupCommands; + private boolean failOnError; + private boolean killProcessesOnShutdown; + private boolean enabled; + + /** Keep track of the processes so that we can kill them on shutdown */ + private List executionResults; + + private Thread shutdownThread; + + /** + * Initializes the bean + *

    + *
  • failOnError = true
  • + *
  • killProcessesOnShutdown = true
  • + *
  • enabled = true
  • + *
+ */ + public RuntimeExecBootstrapBean() + { + this.startupCommands = Collections.emptyList(); + this.executionResults = new ArrayList(1); + failOnError = true; + killProcessesOnShutdown = true; + enabled = true; + } + + /** + * Set the commands to execute, in sequence, when the application context + * is initialized. + * + * @param startupCommands list of commands + */ + public void setStartupCommands(List startupCommands) + { + this.startupCommands = startupCommands; + } + + /** + * Set whether a process failure generates an error or not. Deviation from the default is + * useful if use as part of a process where the command or the codes generated by the + * execution may be ignored or avoided by the system. + * + * @param failOnError true (default) to issue an error message and throw an + * exception if the process fails to execute or generates an error + * return value. + * + * @since 2.1 + */ + public void setFailOnError(boolean failOnError) + { + this.failOnError = failOnError; + } + + /** + * Set whether or not to force a shutdown of successfully started processes. As most + * bootstrap processes are kicked off in order to provide the server with some or other + * service, this is true by default. + * + * @param killProcessesOnShutdown + * true to force any successfully executed commands' processes to + * be forcibly killed when the server shuts down. + * + * @since 2.1.0 + */ + public void setKillProcessesOnShutdown(boolean killProcessesOnShutdown) + { + this.killProcessesOnShutdown = killProcessesOnShutdown; + } + + /** + * Set whether or not the process should be disabled at ApplicationContext bootstrap. + * If a RuntimeExecBootstrapBean is disabled, then the command will not be executed. + * This property is not required and is false by default. + *

+ * This method has been deprecated in favour of a clearer name introduced in 3.3. + * See {@link #setEnabled}. + * + * @param disabledAtStartUp any String which equalsIgnoreCase("true") + * to prevent the command from being executed. + * @since 3.2.1 + * @deprecated Use {@link #setEnabled} instead, remembering that the boolean property should be inverted. + */ + public void setDisabledAtStartUp(String disabledAtStartUp) + { + boolean disabled = Boolean.parseBoolean(disabledAtStartUp); + this.setEnabled(Boolean.toString(!disabled)); + } + + /** + * Set whether or not the process should be enabled at ApplicationContext bootstrap. + * If a RuntimeExecBootstrapBean is not enabled, then the command will not be executed. + * This property is not required and is true by default. + * + * @param enabled any String which does not equalsIgnoreCase("true") + * will prevent the command from being executed. + * + * @since 3.3 + */ + public void setEnabled(String enabled) + { + // A String parameter rather than a boolean parameter is used here in order to allow + // the injection of properties ${foo.bar}. In this way undefined properties (which will + // be injected as "${foo.bar}") will mean the parameter is equivalent to false. + this.enabled = Boolean.parseBoolean(enabled); + } + + public void setEnabledFromBean(BooleanBean enabled) + { + this.enabled = enabled.isTrue(); + } + + @Override + protected synchronized void onBootstrap(ApplicationEvent event) + { + // If the command is disabled then do nothing. + if (this.enabled == false) + { + if (logger.isDebugEnabled()) + { + logger.debug("Bootstrap execution of " + startupCommands.size() + " was not enabled"); + } + return; + } + // execute + for (RuntimeExec command : startupCommands) + { + ExecutionResult result = command.execute(); + // check for failure + if (!result.getSuccess()) + { + String msg = "Bootstrap command failed: \n" + result; + if (failOnError) + { + throw new AlfrescoRuntimeException(msg); + } + else + { + logger.error(msg); + } + } + else + { + // It executed, so keep track of it + executionResults.add(result); + } + } + if (killProcessesOnShutdown) + { + // Force a shutdown on VM termination as we can't rely on the Spring context termination + this.shutdownThread = new KillProcessShutdownThread(); + Runtime.getRuntime().addShutdownHook(this.shutdownThread); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bootstrap execution of " + startupCommands.size() + " commands was successful"); + } + } + + /** + * A thread that serves to kill the successfully created process, if required + * + * @since 2.1 + * @author Derek Hulley + */ + private class KillProcessShutdownThread extends Thread + { + public KillProcessShutdownThread() + { + super(RuntimeExecBootstrapBean.class.getName()); + } + @Override + public void run() + { + doShutdown(); + } + } + + /** + * Handle the shutdown of a subsystem but not the entire VM + */ + @Override + protected synchronized void onShutdown(ApplicationEvent event) + { + if (this.enabled == false) + { + return; + } + + try + { + // We managed to stop the process ourselves (e.g. on subsystem shutdown). Remove the shutdown hook + Runtime.getRuntime().removeShutdownHook(this.shutdownThread); + doShutdown(); + } + catch (IllegalStateException e) + { + // The system is shutting down - we'll have to let the shutdown hook run + } + } + + private void doShutdown() + { + if (!killProcessesOnShutdown) + { + // Do not force a kill + return; + } + for (ExecutionResult executionResult : executionResults) + { + executionResult.killProcess(); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/exec/RuntimeExecShutdownBean.java b/core/src/main/java/org/alfresco/util/exec/RuntimeExecShutdownBean.java new file mode 100644 index 0000000000..842d217fa2 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/exec/RuntimeExecShutdownBean.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import java.util.Collections; +import java.util.List; + +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +/** + * This bean executes a list of shutdown commands when either the VM shuts down + * or the application context closes. In both cases, the commands are only + * executed if the application context was started. + * + * @author Derek Hulley + */ +public class RuntimeExecShutdownBean extends AbstractLifecycleBean +{ + private static Log logger = LogFactory.getLog(RuntimeExecShutdownBean.class); + + /** the commands to execute on context closure or VM shutdown */ + private List shutdownCommands; + /** the registered shutdown hook */ + private Thread shutdownHook; + /** ensures that commands don't get executed twice */ + private boolean executed; + + /** + * Initializes the bean with empty defaults, i.e. it will do nothing + */ + public RuntimeExecShutdownBean() + { + this.shutdownCommands = Collections.emptyList(); + this.executed = false; + } + + /** + * Set the commands to execute, in sequence, when the application context + * is initialized. + * + * @param startupCommands list of commands + */ + public void setShutdownCommands(List startupCommands) + { + this.shutdownCommands = startupCommands; + } + + private synchronized void execute() + { + // have we already done this? + if (executed) + { + return; + } + executed = true; + for (RuntimeExec command : shutdownCommands) + { + ExecutionResult result = command.execute(); + // check for failure + if (!result.getSuccess()) + { + logger.error("Shutdown command execution failed. Continuing with other commands.: \n" + result); + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Executed shutdown commands"); + } + } + + /** + * The thread that will call the shutdown commands. + * + * @author Derek Hulley + */ + private class ShutdownThread extends Thread + { + private ShutdownThread() + { + super(RuntimeExecShutdownBean.class.getName()); + this.setDaemon(true); + } + + @Override + public void run() + { + execute(); + } + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // register shutdown hook + shutdownHook = new ShutdownThread(); + Runtime.getRuntime().addShutdownHook(shutdownHook); + + if (logger.isDebugEnabled()) + { + logger.debug("Registered shutdown hook"); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // remove shutdown hook and execute + if (shutdownHook != null) + { + // execute + execute(); + // remove hook + try + { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } + catch (IllegalStateException e) + { + // VM is already shutting down + } + shutdownHook = null; + + if (logger.isDebugEnabled()) + { + logger.debug("Deregistered shutdown hook"); + } + } + } + +} + + + + + + + + + + + + + + diff --git a/core/src/main/java/org/alfresco/util/log/NDC.java b/core/src/main/java/org/alfresco/util/log/NDC.java new file mode 100644 index 0000000000..302f5a46ce --- /dev/null +++ b/core/src/main/java/org/alfresco/util/log/NDC.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.log; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * A stand in for the org.apache.log4j.NDC class that avoids introducing runtime dependencies against the otherwise + * optional log4j. + * + * @author dward + */ +public class NDC +{ + private static Log logger = LogFactory.getLog(NDC.class); + + /** Log4J delegate for NDC */ + private static NDCDelegate ndcDelegate; + + static + { + if (logger.isDebugEnabled()) + { + try + { + ndcDelegate = (NDCDelegate) Class.forName("org.alfresco.util.log.log4j.Log4JNDC").newInstance(); + } + catch (Throwable e) + { + // We just ignore it + } + } + } + + /** + * Push new diagnostic context information for the current thread. + * + * @param message + * The new diagnostic context information. + */ + public static void push(String message) + { + if (ndcDelegate != null) + { + ndcDelegate.push(message); + } + } + + /** + * Remove the diagnostic context for this thread. + */ + static public void remove() + { + if (ndcDelegate != null) + { + ndcDelegate.remove(); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/log/NDCDelegate.java b/core/src/main/java/org/alfresco/util/log/NDCDelegate.java new file mode 100644 index 0000000000..1d9bbebcb2 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/log/NDCDelegate.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.log; + +/** + * @author dward + * + */ +public interface NDCDelegate +{ + /** + * Push new diagnostic context information for the current thread. + * + * @param message + * The new diagnostic context information. + */ + public void push(String message); + + /** + * Remove the diagnostic context for this thread. + */ + public void remove(); +} diff --git a/core/src/main/java/org/alfresco/util/log/log4j/Log4JNDC.java b/core/src/main/java/org/alfresco/util/log/log4j/Log4JNDC.java new file mode 100644 index 0000000000..49f599f054 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/log/log4j/Log4JNDC.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.log.log4j; + +import org.alfresco.util.log.NDCDelegate; +import org.apache.log4j.NDC; + +/** + * A stand in for the org.apache.log4j.NDC class that avoids introducing runtime dependencies against the otherwise + * optional log4j. + * + * @author dward + */ +public class Log4JNDC implements NDCDelegate +{ + // Force resolution of the log4j NDC class by the classloader (thus forcing an error if unavailable) + @SuppressWarnings("unused") + private static final Class NDC_REF = NDC.class; + + /** + * Push new diagnostic context information for the current thread. + * + * @param message + * The new diagnostic context information. + */ + public void push(String message) + { + NDC.push(message); + } + + /** + * Remove the diagnostic context for this thread. + */ + public void remove() + { + NDC.remove(); + } +} diff --git a/core/src/main/java/org/alfresco/util/random/NormalDistributionHelper.java b/core/src/main/java/org/alfresco/util/random/NormalDistributionHelper.java new file mode 100644 index 0000000000..a8c0f71d50 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/random/NormalDistributionHelper.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.random; + +import org.apache.commons.math3.distribution.NormalDistribution; + +/** + * Utility functions guided by the + * Normal Distribution. + * + * @author Derek Hulley + * @since 5.1 + */ +public class NormalDistributionHelper +{ + private final NormalDistribution normalDistribution; + + /** + * Use a simple normal distribution to generate random numbers + */ + public NormalDistributionHelper() + { + this.normalDistribution = new NormalDistribution(); + } + + /** + * Get a random long where a standard deviation of 1.0 corresponds to the + * min and max values provided. The sampling is repeated until a value is + * found within the range given. + */ + public long getValue(long min, long max) + { + if (min > max) + { + throw new IllegalArgumentException("Min must less than or equal to max."); + } + + double sample = -2.0; + // Keep sampling until we get something within bounds of the standard deviation + while (sample < -1.0 || sample > 1.0) + { + sample = normalDistribution.sample(); + } + long halfRange = (max - min)/2L; + long mean = min + halfRange; + long ret = mean + (long) (halfRange * sample); + // Done + return ret; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/util/registry/NamedObjectRegistry.java b/core/src/main/java/org/alfresco/util/registry/NamedObjectRegistry.java new file mode 100644 index 0000000000..19e92cc1d9 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/registry/NamedObjectRegistry.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.registry; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.ParameterCheck; + +/** + * An generic registry of objects held by name. This is effectively a strongly-typed, + * synchronized map. + * + * @author Derek Hulley + * @since 3.2 + */ +@AlfrescoPublicApi +public class NamedObjectRegistry +{ + private static final Log logger = LogFactory.getLog(NamedObjectRegistry.class); + + private final ReentrantReadWriteLock.ReadLock readLock; + private final ReentrantReadWriteLock.WriteLock writeLock; + + private Class storageType; + private Pattern namePattern; + private final Map objects; + + /** + * Default constructor. The {@link #setStorageType(Class)} method must be called. + */ + public NamedObjectRegistry() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + this.namePattern = null; // Deliberately null + this.storageType = null; // Deliberately null + this.objects = new HashMap(13); + } + + /** + * Constructor that takes care of {@link #setStorageType(Class)}. + * + * @see #setStorageType(Class) + */ + public NamedObjectRegistry(Class type) + { + this(); + setStorageType(type); + } + + /** + * Set the type of class that the registry holds. Any attempt to register a + * an instance of another type will be rejected. + * + * @param clazz the type to store + */ + public void setStorageType(Class clazz) + { + writeLock.lock(); + try + { + this.storageType = clazz; + } + finally + { + writeLock.unlock(); + } + } + + /** + * Optionally set a pattern to which all object names must conform + * @param namePattern a regular expression + */ + public void setNamePattern(String namePattern) + { + writeLock.lock(); + try + { + this.namePattern = Pattern.compile(namePattern); + } + catch (PatternSyntaxException e) + { + throw new AlfrescoRuntimeException( + "Regular expression compilation failed for property 'namePrefix': " + e.getMessage(), + e); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Register a named object instance. + * + * @param name the name of the object + * @param object the instance to register, which correspond to the type + */ + public void register(String name, T object) + { + ParameterCheck.mandatoryString("name", name); + ParameterCheck.mandatory("object", object); + + if (!storageType.isAssignableFrom(object.getClass())) + { + throw new IllegalArgumentException( + "This NameObjectRegistry only accepts objects of type " + storageType); + } + writeLock.lock(); + try + { + if (storageType == null) + { + throw new IllegalStateException( + "The registry has not been configured (setStorageType not yet called yet)"); + } + if (namePattern != null) + { + if (!namePattern.matcher(name).matches()) + { + throw new IllegalArgumentException( + "Object name '" + name + "' does not match required pattern: " + namePattern); + } + } + T prevObject = objects.put(name, object); + if (prevObject != null && prevObject != object) + { + logger.warn( + "Overwriting name object in registry: \n" + + " Previous: " + prevObject + "\n" + + " New: " + object); + } + } + finally + { + writeLock.unlock(); + } + } + + /** + * Get a named object if it has been registered + * + * @param name the name of the object to retrieve + * @return Returns the instance of the object, which will necessarily + * be of the correct type, or null + */ + public T getNamedObject(String name) + { + readLock.lock(); + try + { + // Get it + return objects.get(name); + } + finally + { + readLock.unlock(); + } + } + + /** + * @return Returns a copy of the map of instances + */ + public Map getAllNamedObjects() + { + readLock.lock(); + try + { + // Get it + return new HashMap(objects); + } + finally + { + readLock.unlock(); + } + } + + public void reset() + { + writeLock.lock(); + try + { + if (storageType == null) + objects.clear(); + } + finally + { + writeLock.unlock(); + } + } +} diff --git a/core/src/main/java/org/alfresco/util/shard/ExplicitShardingPolicy.java b/core/src/main/java/org/alfresco/util/shard/ExplicitShardingPolicy.java new file mode 100644 index 0000000000..f5c421fef2 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/shard/ExplicitShardingPolicy.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.shard; + +import java.util.LinkedList; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Common ACL based index sharding behaviour for SOLR and the repository + * + * @author Andy + */ +public class ExplicitShardingPolicy +{ + private int numShards; + + private int replicationFactor; + + private int numNodes; + + public ExplicitShardingPolicy(int numShards, int replicationFactor, int numNodes) + { + this.numShards = numShards; + this.replicationFactor = replicationFactor; + this.numNodes = numNodes; + } + + public boolean configurationIsValid() + { + if ((numShards * replicationFactor) % numNodes != 0) + { + return false; + } + + int shardsPerNode = numShards * replicationFactor / numNodes; + if ((shardsPerNode > numShards) || (shardsPerNode < 1)) + { + return false; + } + + return true; + } + + public List getShardIdsForNode(int nodeInstance) + { + LinkedList shardIds = new LinkedList(); + int test = 0; + for (int replica = 0; replica < replicationFactor; replica++) + { + for (int shard = replica; shard < numShards + replica; shard++) + { + if (test % numNodes == nodeInstance - 1) + { + // This algorithm fails for some sets of parameters. (See SEARCH-1785) + if (shardIds.contains(shard % numShards)) + { + throw new AlfrescoRuntimeException("Sharding configuration not supported - unable to create shard list for node " + nodeInstance + + " (shards:" + numShards + ", replication:" + replicationFactor + ", nodes:" + numNodes + ")." + + " Please set up the shards manually or use a different sharding configuration."); + } + shardIds.add(shard % numShards); + } + test++; + } + + } + return shardIds; + } + + public List getNodeInstancesForShardId(int shardId) + { + LinkedList nodeInstances = new LinkedList(); + for (int nodeInstance = 1; nodeInstance <= numNodes; nodeInstance++) + { + int test = 0; + for (int replica = 0; replica < replicationFactor; replica++) + { + for (int shard = replica; shard < numShards + replica; shard++) + { + if (test % numNodes == nodeInstance - 1) + { + if(shard % numShards == shardId) + { + nodeInstances.add(nodeInstance); + } + } + test++; + } + + } + } + return nodeInstances; + } + +} diff --git a/core/src/main/java/org/alfresco/util/transaction/ConnectionPoolException.java b/core/src/main/java/org/alfresco/util/transaction/ConnectionPoolException.java new file mode 100644 index 0000000000..c2886846c8 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/transaction/ConnectionPoolException.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.transaction; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Exception wraps {@link java.util.NoSuchElementException} from {@link org.apache.commons.dbcp.BasicDataSource} + * + * @author alex.mukha + * @since 4.1.9 + */ +public class ConnectionPoolException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 1L; + + public ConnectionPoolException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } + + public ConnectionPoolException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + public ConnectionPoolException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + public ConnectionPoolException(String msgId) + { + super(msgId); + } +} diff --git a/core/src/main/java/org/alfresco/util/transaction/SpringAwareUserTransaction.java b/core/src/main/java/org/alfresco/util/transaction/SpringAwareUserTransaction.java new file mode 100644 index 0000000000..5199fcfed5 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/transaction/SpringAwareUserTransaction.java @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.transaction; + +import java.lang.reflect.Method; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import org.alfresco.error.StackTraceUtil; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * A UserTransaction that will allow the thread using it to participate + * in transactions that are normally only begun and committed by the SpringFramework + * transaction aware components. + *

+ * Client code can use this class directly, but should be very careful to handle the exception + * conditions with the appropriate finally blocks and rollback code. + * It is recommended that clients use this class indirectly via an instance of the + * {@link org.alfresco.repo.transaction.RetryingTransactionHelper}. + *

+ * This class is thread-safe in that it will detect multithreaded access and throw + * exceptions. Therefore do not use on multiple threads. Instances should be + * used only for the duration of the required user transaction and then discarded. + * Any attempt to reuse an instance will result in failure. + *

+ * Nested user transaction are allowed. + *

+ * Logging:
+ * To dump exceptions during commits, turn debugging on for this class.
+ * To log leaked transactions i.e. a begin() is not matched by a commit() or rollback(), + * add .trace to the usual classname-based debug category and set to WARN log + * level. This will log the first detection of a leaked transaction and automatically enable + * transaction call stack logging for subsequent leaked transactions. To enforce + * call stack logging from the start set the .trace log level to DEBUG. Call stack + * logging will hamper performance but is useful when it appears that something is eating + * connections or holding onto resources - usually a sign that client code hasn't handled all + * possible exception conditions. + * + * @see org.springframework.transaction.PlatformTransactionManager + * @see org.springframework.transaction.support.DefaultTransactionDefinition + * + * @author Derek Hulley + */ +public class SpringAwareUserTransaction + extends TransactionAspectSupport + implements UserTransaction, TransactionAttributeSource, TransactionAttribute +{ + /* + * There is some extra work in here to perform safety checks against the thread ID. + * This is because this class doesn't operate in an environment that guarantees that the + * thread coming into the begin() method is the same as the thread forcing commit() or + * rollback(). + */ + + private static final long serialVersionUID = 3762538897183224373L; + + + private static final String NAME = "UserTransaction"; + + private static final Log logger = LogFactory.getLog(SpringAwareUserTransaction.class); + + + /* + * Leaked Transaction Logging + */ + private static final Log traceLogger = LogFactory.getLog(SpringAwareUserTransaction.class.getName() + ".trace"); + private static volatile boolean isCallStackTraced = false; + + static + { + if (traceLogger.isDebugEnabled()) + { + isCallStackTraced = true; + traceLogger.warn("Logging of transaction call stack is enforced and will affect performance"); + } + } + + + static boolean isCallStackTraced() + { + return isCallStackTraced; + } + + /** stores whether begin() & commit()/rollback() methods calls are balanced */ + private boolean isBeginMatched = true; + /** stores the begin() call stack when auto tracing */ + private StackTraceElement[] beginCallStack; + + + private boolean readOnly; + private int isolationLevel; + private int propagationBehaviour; + private int timeout; + + /** Stores the user transaction current status as affected by explicit operations */ + private int internalStatus = Status.STATUS_NO_TRANSACTION; + /** the transaction information used to check for mismatched begin/end */ + private TransactionInfo internalTxnInfo; + /** keep the thread that the transaction was started on to perform thread safety checks */ + private long threadId = Long.MIN_VALUE; + /** make sure that we clean up the thread transaction stack properly */ + private boolean finalized = false; + + /** + * Creates a user transaction that defaults to {@link TransactionDefinition#PROPAGATION_REQUIRED}. + * + * @param transactionManager the transaction manager to use + * @param readOnly true to force a read-only transaction + * @param isolationLevel one of the + * {@link TransactionDefinition#ISOLATION_DEFAULT TransactionDefinition.ISOLATION_XXX} + * constants + * @param propagationBehaviour one of the + * {@link TransactionDefinition#PROPAGATION_MANDATORY TransactionDefinition.PROPAGATION__XXX} + * constants + * @param timeout the transaction timeout in seconds. + * + * @see TransactionDefinition#getTimeout() + */ + public SpringAwareUserTransaction( + PlatformTransactionManager transactionManager, + boolean readOnly, + int isolationLevel, + int propagationBehaviour, + int timeout) + { + super(); + setTransactionManager(transactionManager); + setTransactionAttributeSource(this); + this.readOnly = readOnly; + this.isolationLevel = isolationLevel; + this.propagationBehaviour = propagationBehaviour; + this.timeout = timeout; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(256); + sb.append("UserTransaction") + .append("[object=").append(super.toString()) + .append(", status=").append(internalStatus) + .append("]"); + return sb.toString(); + } + + /** + * This class carries all the information required to fullfil requests about the transaction + * attributes. It acts as a source of the transaction attributes. + * + * @return Return this instance + */ + public TransactionAttribute getTransactionAttribute(Method method, Class targetClass) + { + return this; + } + + /** + * Return a qualifier value associated with this transaction attribute. This is not used as the transaction manager + * has been selected for us. + * + * @return null always + */ + public String getQualifier() + { + return null; + } + + /** + * The {@link UserTransaction } must rollback regardless of the error. The + * {@link #rollback() rollback} behaviour is implemented by simulating a caught + * exception. As this method will always return true, the rollback + * behaviour will be to rollback the transaction or mark it for rollback. + * + * @return Returns true always + */ + public boolean rollbackOn(Throwable ex) + { + return true; + } + + public String getName() + { + return Thread.currentThread().getName() + "-" + GUID.generate(); + } + + public boolean isReadOnly() + { + return readOnly; + } + + public int getIsolationLevel() + { + return isolationLevel; + } + + public int getPropagationBehavior() + { + return propagationBehaviour; + } + + public int getTimeout() + { + return timeout; + } + + /** + * Implementation required for {@link UserTransaction}. + */ + public void setTransactionTimeout(int timeout) throws SystemException + { + if (internalStatus != Status.STATUS_NO_TRANSACTION) + { + throw new RuntimeException("Can only set the timeout before begin"); + } + this.timeout = timeout; + } + + /** + * Gets the current transaction info, or null if none exists. + *

+ * A check is done to ensure that the transaction info on the stack is exactly + * the same instance used when this transaction was started. + * The internal status is also checked against the transaction info. + * These checks ensure that the transaction demarcation is done correctly and that + * thread safety is adhered to. + * + * @return Returns the current transaction + */ + private TransactionInfo getTransactionInfo() + { + // a few quick self-checks + if (threadId < 0 && internalStatus != Status.STATUS_NO_TRANSACTION) + { + throw new RuntimeException("Transaction has been started but there is no thread ID"); + } + else if (threadId >= 0 && internalStatus == Status.STATUS_NO_TRANSACTION) + { + throw new RuntimeException("Transaction has not been started but a thread ID has been recorded"); + } + + TransactionInfo txnInfo = null; + try + { + txnInfo = TransactionAspectSupport.currentTransactionInfo(); + // we are in a transaction + } + catch (NoTransactionException e) + { + // No transaction. It is possible that the transaction threw an exception during commit. + } + // perform checks for active transactions + if (internalStatus == Status.STATUS_ACTIVE) + { + if (Thread.currentThread().getId() != threadId) + { + // the internally stored transaction info (retrieved in begin()) should match the info + // on the thread + throw new RuntimeException("UserTransaction may not be accessed by multiple threads"); + } + else if (txnInfo == null) + { + // internally we recorded a transaction starting, but there is nothing on the thread + throw new RuntimeException("Transaction boundaries have been made to overlap in the stack"); + } + else if (txnInfo != internalTxnInfo) + { + // the transaction info on the stack isn't the one we started with + throw new RuntimeException("UserTransaction begin/commit mismatch"); + } + } + return txnInfo; + } + + /** + * This status is a combination of the internal status, as recorded during explicit operations, + * and the status provided by the Spring support. + * + * @see Status + */ + public synchronized int getStatus() throws SystemException + { + TransactionInfo txnInfo = getTransactionInfo(); + + // if the txn info is null, then we are outside a transaction + if (txnInfo == null) + { + return internalStatus; // this is checked in getTransactionInfo + } + + // normally the internal status is correct, but we only need to double check + // for the case where the transaction was marked for rollback, or rolledback + // in a deeper transaction + TransactionStatus txnStatus = txnInfo.getTransactionStatus(); + if (internalStatus == Status.STATUS_ROLLEDBACK) + { + // explicitly rolled back at some point + return internalStatus; + } + else if (txnStatus.isRollbackOnly()) + { + // marked for rollback at some point in the stack + return Status.STATUS_MARKED_ROLLBACK; + } + else + { + // just rely on the internal status + return internalStatus; + } + } + + public synchronized void setRollbackOnly() throws IllegalStateException, SystemException + { + // just a check + TransactionInfo txnInfo = getTransactionInfo(); + + int status = getStatus(); + // check the status + if (status == Status.STATUS_MARKED_ROLLBACK) + { + // this is acceptable + } + else if (status == Status.STATUS_NO_TRANSACTION) + { + throw new IllegalStateException("The transaction has not been started yet"); + } + else if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) + { + throw new IllegalStateException("The transaction has already been rolled back"); + } + else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) + { + throw new IllegalStateException("The transaction has already been committed"); + } + else if (status != Status.STATUS_ACTIVE) + { + throw new IllegalStateException("The transaction is not active: " + status); + } + + // mark for rollback + txnInfo.getTransactionStatus().setRollbackOnly(); + // make sure that we record the fact that we have been marked for rollback + internalStatus = Status.STATUS_MARKED_ROLLBACK; + // done + if (logger.isDebugEnabled()) + { + logger.debug("Set transaction status to rollback only: " + this); + } + } + + /** + * @throws NotSupportedException if an attempt is made to reuse this instance + */ + public synchronized void begin() throws NotSupportedException, SystemException + { + // make sure that the status and info align - the result may or may not be null + @SuppressWarnings("unused") + TransactionInfo txnInfo = getTransactionInfo(); + if (internalStatus != Status.STATUS_NO_TRANSACTION) + { + throw new NotSupportedException("The UserTransaction may not be reused"); + } + + // check + + if( (propagationBehaviour != TransactionDefinition.PROPAGATION_REQUIRES_NEW)) + { + if(!readOnly && + TransactionSynchronizationManager.isSynchronizationActive() && + TransactionSynchronizationManager.isCurrentTransactionReadOnly() + ) + { + throw new IllegalStateException("Nested writable transaction in a read only transaction"); + } + } + + // begin a transaction + try + { + internalTxnInfo = createTransactionIfNecessary( + getTransactionManager(), getTransactionAttribute(null, null), getName()); + } + catch (CannotCreateTransactionException e) + { + throw new ConnectionPoolException("The DB connection pool is depleted.", e); + } + + internalStatus = Status.STATUS_ACTIVE; + threadId = Thread.currentThread().getId(); + + // Record that transaction details now that begin was successful + isBeginMatched = false; + if (isCallStackTraced) + { + // get the stack trace + Exception e = new Exception(); + e.fillInStackTrace(); + beginCallStack = e.getStackTrace(); + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Began user transaction: " + this); + } + } + + /** + * @throws IllegalStateException if a transaction was not started + */ + public synchronized void commit() + throws RollbackException, HeuristicMixedException, HeuristicRollbackException, + SecurityException, IllegalStateException, SystemException + { + // perform checks + TransactionInfo txnInfo = getTransactionInfo(); + + int status = getStatus(); + // check the status + if (status == Status.STATUS_NO_TRANSACTION) + { + throw new IllegalStateException("The transaction has not yet begun"); + } + else if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) + { + throw new RollbackException("The transaction has already been rolled back"); + } + else if (status == Status.STATUS_MARKED_ROLLBACK) + { + throw new RollbackException("The transaction has already been marked for rollback"); + } + else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) + { + throw new IllegalStateException("The transaction has already been committed"); + } + else if (status != Status.STATUS_ACTIVE || txnInfo == null) + { + throw new IllegalStateException("No user transaction is active"); + } + + if (!finalized) + { + try + { + // the status seems correct - we can try a commit + commitTransactionAfterReturning(txnInfo); + } + catch (Throwable e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Transaction didn't commit", e); + } + // commit failed + internalStatus = Status.STATUS_ROLLEDBACK; + RollbackException re = new RollbackException("Transaction didn't commit: " + e.getMessage()); + // Stick the originating reason for failure into the exception. + re.initCause(e); + throw re; + } + finally + { + // make sure that we clean up the stack + cleanupTransactionInfo(txnInfo); + finalized = true; + // clean up leaked transaction logging + isBeginMatched = true; + beginCallStack = null; + } + } + + // regardless of whether the transaction was finally committed or not, the status + // as far as UserTransaction is concerned should be 'committed' + + // keep track that this UserTransaction was explicitly committed + internalStatus = Status.STATUS_COMMITTED; + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Committed user transaction: " + this); + } + } + + public synchronized void rollback() + throws IllegalStateException, SecurityException, SystemException + { + // perform checks + TransactionInfo txnInfo = getTransactionInfo(); + + int status = getStatus(); + // check the status + if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) + { + throw new IllegalStateException("The transaction has already been rolled back"); + } + else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) + { + throw new IllegalStateException("The transaction has already been committed"); + } + else if (txnInfo == null) + { + throw new IllegalStateException("No user transaction is active"); + } + + if (!finalized) + { + try + { + // force a rollback by generating an exception that will trigger a rollback + completeTransactionAfterThrowing(txnInfo, new Exception()); + } + finally + { + // make sure that we clean up the stack + cleanupTransactionInfo(txnInfo); + finalized = true; + // clean up leaked transaction logging + isBeginMatched = true; + beginCallStack = null; + } + } + + // the internal status notes that we were specifically rolled back + internalStatus = Status.STATUS_ROLLEDBACK; + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Rolled back user transaction: " + this); + } + } + + @Override + protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) + { + if (logger.isDebugEnabled()) + { + logger.debug("Exception attempting to pass transaction boundaries.", ex); + } + super.completeTransactionAfterThrowing(txInfo, ex); + } + + @Override + protected void finalize() throws Throwable + { + if (!isBeginMatched) + { + if (isCallStackTraced) + { + if (beginCallStack == null) + { + traceLogger.error("UserTransaction being garbage collected without a commit() or rollback(). " + + "NOTE: Prior to transaction call stack logging."); + } + else + { + StringBuilder sb = new StringBuilder(1024); + StackTraceUtil.buildStackTrace( + "UserTransaction being garbage collected without a commit() or rollback().", + beginCallStack, + sb, + -1); + traceLogger.error(sb); + } + } + else + { + traceLogger.error("Detected first UserTransaction which is being garbage collected without a commit() or rollback()"); + traceLogger.error("Logging of transaction call stack is now enabled and will affect performance"); + isCallStackTraced = true; + } + } + } +} diff --git a/core/src/main/java/org/alfresco/util/transaction/TransactionListener.java b/core/src/main/java/org/alfresco/util/transaction/TransactionListener.java new file mode 100644 index 0000000000..e97222ad5a --- /dev/null +++ b/core/src/main/java/org/alfresco/util/transaction/TransactionListener.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.transaction; + +/** + * Listener for Alfresco-specific transaction callbacks. + * + * @see org.alfresco.repo.transaction.AlfrescoTransactionSupport + * + * @author Derek Hulley + */ +public interface TransactionListener +{ + + /** + * Called before a transaction is committed. + *

+ * All transaction resources are still available. + * + * @param readOnly true if the transaction is read-only + */ + void beforeCommit(boolean readOnly); + + /** + * Invoked before transaction commit/rollback. Will be called after + * {@link #beforeCommit(boolean) } even if {@link #beforeCommit(boolean)} + * failed. + *

+ * All transaction resources are still available. + */ + void beforeCompletion(); + + /** + * Invoked after transaction commit. + *

+ * Any exceptions generated here will only be logged and will have no effect + * on the state of the transaction. + *

+ * Although all transaction resources are still available, this method should + * be used only for cleaning up resources after a commit has occured. + */ + void afterCommit(); + + /** + * Invoked after transaction rollback. + *

+ * Any exceptions generated here will only be logged and will have no effect + * on the state of the transaction. + *

+ * Although all transaction resources are still available, this method should + * be used only for cleaning up resources after a rollback has occured. + */ + void afterRollback(); +} diff --git a/core/src/main/java/org/alfresco/util/transaction/TransactionListenerAdapter.java b/core/src/main/java/org/alfresco/util/transaction/TransactionListenerAdapter.java new file mode 100644 index 0000000000..efd42e3b17 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/transaction/TransactionListenerAdapter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.transaction; + +/** + * NO-OP listener. + * + * @author Derek Hulley + * @since 5.0 + */ +public abstract class TransactionListenerAdapter implements TransactionListener +{ + /** + * {@inheritDoc} + */ + @Override + public void beforeCommit(boolean readOnly) + { + } + + /** + * {@inheritDoc} + */ + @Override + public void beforeCompletion() + { + } + + /** + * {@inheritDoc} + */ + @Override + public void afterCommit() + { + } + + /** + * {@inheritDoc} + */ + @Override + public void afterRollback() + { + } +} diff --git a/core/src/main/java/org/alfresco/util/transaction/TransactionSupportUtil.java b/core/src/main/java/org/alfresco/util/transaction/TransactionSupportUtil.java new file mode 100644 index 0000000000..05f7bac5a1 --- /dev/null +++ b/core/src/main/java/org/alfresco/util/transaction/TransactionSupportUtil.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2014-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.transaction; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.concurrent.ConcurrentSkipListSet; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.ParameterCheck; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Helper class to manage transaction synchronization. This provides helpers to + * ensure that the necessary TransactionSynchronization instances + * are registered on behalf of the application code. + * + * @author mrogers + */ +public abstract class TransactionSupportUtil +{ + private static Log logger = LogFactory.getLog(TransactionSupportUtil.class); + + private static final int SESSION_SYNCHRONIZATION_ORDER = 800; + /** resource key to store the transaction synchronizer instance */ + private static final String RESOURCE_KEY_TXN_SYNCH = "AlfrescoTransactionSupport.txnSynch"; + /** resource key to store the transaction id, it needs to live even if the synchronization was cleared */ + private static final String RESOURCE_KEY_TXN_ID = "AlfrescoTransactionSupport.txnId"; + /** + *

+ * As in Spring 5 the synchronisations are cleared after the transaction is committed or rolled back, + * it is required to manage the txn resources in a separate thread local. + * This is required to be able to use resources by afterCommit listeners. + *

+ *

+ * If the transaction is suspended the resources are saved and restored afterwards. + * See {@link TransactionSynchronizationImpl#suspend()} and {@link TransactionSynchronizationImpl#resume()} + *

+ */ + private static final ThreadLocal txnResources = + ThreadLocal.withInitial(() -> new ResourcesHolder(new HashMap<>(3))); + + /** + * @return Returns the system time when the transaction started, or -1 if there is no current transaction. + */ + public static long getTransactionStartTime() + { + /* + * This method can be called outside of a transaction, so we can go direct to the synchronizations. + */ + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + // need to lazily register synchronizations + return TransactionSupportUtil.getSynchronization().getTransactionStartTime(); + } + else + { + return -1; // not in a transaction + } + } + + /** + * Get a unique identifier associated with each transaction of each thread. Null is returned if + * no transaction is currently active. + * + * @return Returns the transaction ID, or null if no transaction is present + */ + public static String getTransactionId() + { + /* + * Go direct to the synchronizations as we don't want to register a resource if one doesn't exist. + * This method is heavily used, so the simple Map lookup on the ThreadLocal is the fastest. + */ + return getResource(RESOURCE_KEY_TXN_ID); + } + + public static boolean isActualTransactionActive() + { + return TransactionSynchronizationManager.isActualTransactionActive(); + } + + /** + * Gets a resource associated with the current transaction + * + * @param key the thread resource map key + * @return Returns a thread resource of null if not present + */ + @SuppressWarnings("unchecked") + public static R getResource(Object key) + { + // The resources might be requested outside of the active txn (post completion) + // The resource requested might be the txn synchronization itself or txn id, + // which might be not created and registered yet + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + TransactionSupportUtil.getSynchronization(); + } + Object resource = txnResources.get().resources.get(key); + if (logger.isTraceEnabled()) + { + logger.trace("Fetched resource in " + TransactionSynchronizationManager.getCurrentTransactionName() + + ": \n" + + " key: " + key + "\n" + + " resource: " + resource); + } + return (R) resource; + } + + /** + * Registers new transaction synchronization instance in {@link TransactionSynchronizationManager} and + * creates necessary resources, see {@link #txnResources} + * + * @return Returns new synchronization implementation + */ + private static TransactionSynchronizationImpl registerSynchronization() + { + if (!TransactionSynchronizationManager.isSynchronizationActive()) + { + Thread currentThread = Thread.currentThread(); + throw new AlfrescoRuntimeException("Transaction must be active and synchronization is required: " + currentThread); + } + + // a unique ID for the transaction is required + String txnId = GUID.generate(); + TransactionSynchronizationImpl txnSynch = new TransactionSynchronizationImpl(txnId); + TransactionSynchronizationManager.registerSynchronization(txnSynch); + // save the synchronization to ensure we don't duplicate it + // it might be required to create a nested resource holder + ResourcesHolder resourcesHolder = txnResources.get(); + if (!resourcesHolder.resources.isEmpty()) + { + ResourcesHolder newResourcesHolder = new ResourcesHolder(resourcesHolder, new HashMap<>(3)); + txnResources.set(newResourcesHolder); + } + Map data = txnResources.get().resources; + data.put(RESOURCE_KEY_TXN_SYNCH, txnSynch); + data.put(RESOURCE_KEY_TXN_ID, txnId); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bound txn synch: " + txnSynch + " with txn name: " + + TransactionSynchronizationManager.getCurrentTransactionName()); + } + return txnSynch; + } + + /** + * Gets the current transaction synchronization instance if the transaction was not completed + * + * @return Returns the current or new synchronization implementation + */ + private static TransactionSynchronizationImpl getSynchronization() + { + Map data = txnResources.get().resources; + if (data.get(RESOURCE_KEY_TXN_SYNCH) != null) + { + return (TransactionSynchronizationImpl) data.get(RESOURCE_KEY_TXN_SYNCH); + } + else + { + return TransactionSupportUtil.registerSynchronization(); + } + } + + /** + * Saves resources and creates a new empty resource holder + */ + private static void suspendSynchronization() + { + ResourcesHolder currentResourcesHolder = txnResources.get(); + ResourcesHolder newResourcesHolder = new ResourcesHolder(currentResourcesHolder, new HashMap<>(3)); + txnResources.set(newResourcesHolder); + } + + /** + * Cleans up the resource holder if it is empty. This is required when transaction was suspended + * but none of application transactions were started before it was resumed. + */ + private static void resumeSynchronization() + { + ResourcesHolder currentResourcesHolder = txnResources.get(); + ResourcesHolder previousResourcesHolder = currentResourcesHolder.previousResourceHolder; + if (currentResourcesHolder.resources.isEmpty() && + previousResourcesHolder != null) + { + txnResources.set(previousResourcesHolder); + } + } + + /** + * Cleans all thread local transaction resources. Restores parent transaction resources if necessary + */ + private static void clearResources() + { + ResourcesHolder currentResourcesHolder = txnResources.get(); + Map txnData = currentResourcesHolder.resources; + txnData.clear(); + if (logger.isDebugEnabled()) + { + logger.debug("Clear txn resources for " + Thread.currentThread().getName()); + } + ResourcesHolder previousResourcesHolder = currentResourcesHolder.previousResourceHolder; + if (previousResourcesHolder != null) + { + txnResources.set(previousResourcesHolder); + } + } + + /** + * Binds a resource to the current transaction + *

+ * All necessary synchronization instances will be registered automatically, if required. + * + * @param key Object + * @param resource Object + */ + public static void bindResource(Object key, Object resource) + { + // The resources should be still available outside of active txn (post completion) + // If the txn is active the synchronization must be created and registered if it doesn't exist yet + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + TransactionSupportUtil.getSynchronization(); + } + txnResources.get().resources.put(key, resource); + // done + if (logger.isTraceEnabled()) + { + logger.trace("Bound resource to " + TransactionSynchronizationManager.getCurrentTransactionName() + ": \n" + + " key: " + key + "\n" + + " resource: " + resource); + } + } + + /** + * Unbinds a resource from the current transaction, which must be active. + * @param key Object + */ + public static void unbindResource(Object key) + { + txnResources.get().resources.remove(key); + if (logger.isTraceEnabled()) + { + logger.trace("Unbound resource from " + TransactionSynchronizationManager.getCurrentTransactionName() + + ": \n" + + " key: " + key); + } + } + + /** + * Bind listener to the specified priority. Duplicate bindings + * + * The priority specifies the position for the listener during commit. + * For example flushing of caches needs to happen very late. + * @param listener the listener to bind. + * @param priority 0 = Normal priority + * @return true if the new listener was bound. False if the listener was already bound. + */ + public static boolean bindListener(TransactionListener listener, int priority) + { + if (logger.isDebugEnabled()) + { + logger.debug("Bind Listener listener: " + listener + ", priority: " + priority); + } + TransactionSynchronizationImpl synch = TransactionSupportUtil.getSynchronization(); + return synch.addListener(listener, priority); + } + + /** + * @return Returns all the listeners in a list disconnected from the original set + */ + public static Set getListeners() + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = TransactionSupportUtil.getSynchronization(); + + return txnSynch.getListenersIterable(); + + } + + /** + * Resource holder to link all necessary resources for the current transaction. + * Also holds the resources for outer transactions. + * This is used by {@link TransactionSynchronizationImpl#suspend()} + * and {@link TransactionSynchronizationImpl#resume()} + * + */ + private static class ResourcesHolder + { + @Nullable + private ResourcesHolder previousResourceHolder; + @NonNull + private Map resources; + + ResourcesHolder(ResourcesHolder previousResourceHolder, Map resources) + { + this.previousResourceHolder = previousResourceHolder; + this.resources = resources; + } + + ResourcesHolder(Map resources) + { + this(null, resources); + } + } + + /** + * Handler of txn synchronization callbacks specific to internal application requirements. + *

+ * This class is not thread safe. It is expected to be used only for purposes of controlling listeners + * for a single thread per instance. + */ + private static class TransactionSynchronizationImpl extends TransactionSynchronizationAdapter + { + private long txnStartTime; + private final String txnId; + + /** + * priority to listeners + */ + private final Map>priorityLookup = new HashMap>(); + + /** + * Sets up the resource map + * + * @param txnId String + */ + public TransactionSynchronizationImpl(String txnId) + { + this.txnStartTime = System.currentTimeMillis(); + this.txnId = txnId; + priorityLookup.put(0, new LinkedHashSet(5)); + } + + public long getTransactionStartTime() + { + return txnStartTime; + } + + /** + * Add a trasaction listener + * + * @return true if the listener was added, false if it already existed. + */ + public boolean addListener(TransactionListener listener, int priority) + { + ParameterCheck.mandatory("listener", listener); + + if (this.priorityLookup.containsKey(priority)) + { + Set listeners = priorityLookup.get(priority); + return listeners.add(listener); + } + else + { + Set listeners = new LinkedHashSet(5); + priorityLookup.put(priority, listeners); + return listeners.add(listener); + } + } + + /** + * Return the level zero (normal) listeners + * + * @return Returns the level zero (normal) listeners in a list disconnected from the original set + */ + private List getLevelZeroListenersIterable() + { + Setlisteners = priorityLookup.get(0); + return new ArrayList(listeners); + } + + /** + * @return all the listeners regardless of priority + */ + private Set getListenersIterable() + { + Set ret = new LinkedHashSet(); + Set>> entries = priorityLookup.entrySet(); + + for(Entry> entry : entries) + { + ret.addAll((Set)entry.getValue()); + } + + return ret; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(50); + sb.append("TransactionSychronizationImpl") + .append("[ txnId=").append(txnId) + .append("]"); + return sb.toString(); + } + + @Override + public int getOrder() + { + return TransactionSupportUtil.SESSION_SYNCHRONIZATION_ORDER; + } + + @Override + public void suspend() + { + if (logger.isDebugEnabled()) + { + logger.debug("Suspending transaction: " + this); + } + TransactionSupportUtil.suspendSynchronization(); + } + + @Override + public void resume() + { + if (logger.isDebugEnabled()) + { + logger.debug("Resuming transaction: " + this); + } + TransactionSupportUtil.resumeSynchronization(); + } + + /** + * Pre-commit cleanup. + *

+ * Ensures that the session transaction listeners are property executed. + * + * The Lucene indexes are then prepared. + */ + @Override + public void beforeCommit(boolean readOnly) + { + if (logger.isDebugEnabled()) + { + logger.debug("Before commit " + (readOnly ? "read-only" : "" ) + this); + } + // get the txn ID + TransactionSynchronizationImpl synch = getResource(RESOURCE_KEY_TXN_SYNCH); + if (synch == null) + { + throw new AlfrescoRuntimeException("No synchronization bound to thread"); + } + + logger.trace("Before Prepare - level 0"); + + // Run the priority 0 (normal) listeners + // These are still considered part of the transaction so are executed here + doBeforeCommit(readOnly); + + // Now run the != 0 listeners beforeCommit + Set priorities = priorityLookup.keySet(); + + SortedSet sortedPriorities = new ConcurrentSkipListSet(FORWARD_INTEGER_ORDER); + sortedPriorities.addAll(priorities); + sortedPriorities.remove(0); // already done level 0 above + + if(logger.isDebugEnabled()) + { + logger.debug("Before Prepare priorities:" + sortedPriorities); + } + for(Integer priority : sortedPriorities) + { + Set listeners = priorityLookup.get(priority); + + for(TransactionListener listener : listeners) + { + listener.beforeCommit(readOnly); + } + } + if(logger.isDebugEnabled()) + { + logger.debug("Prepared"); + } + } + + /** + * Execute the beforeCommit event handlers for the registered listeners + * + * @param readOnly is read only + */ + private void doBeforeCommit(boolean readOnly) + { + doBeforeCommit(new HashSet(), readOnly); + } + + /** + * Executes the beforeCommit event handlers for the outstanding listeners. + * This process is iterative as the process of calling listeners may lead to more listeners + * being added. The new listeners will be processed until there no listeners remaining. + * + * @param visitedListeners a set containing the already visited listeners + * @param readOnly is read only + */ + private void doBeforeCommit(Set visitedListeners, boolean readOnly) + { + List pendingListeners = getLevelZeroListenersIterable(); + pendingListeners.removeAll(visitedListeners); + + if (pendingListeners.size() != 0) + { + for (TransactionListener listener : pendingListeners) + { + listener.beforeCommit(readOnly); + visitedListeners.add(listener); + } + + doBeforeCommit(visitedListeners, readOnly); + } + } + + @Override + public void beforeCompletion() + { + if (logger.isDebugEnabled()) + { + logger.debug("Before completion: " + this); + } + // notify listeners + for (TransactionListener listener : getLevelZeroListenersIterable()) + { + listener.beforeCompletion(); + } + } + + @Override + public void afterCompletion(int status) + { + // As in Spring 5 the synchronisations are cleared after the transaction is committed or rolled back. + // It is required to remove synchronization, as it is enforces binding of + // new txn synchronization if one will be started in afterCommit/afterRollback listeners. + TransactionSupportUtil.unbindResource(RESOURCE_KEY_TXN_SYNCH); + + String statusStr = "unknown"; + switch (status) + { + case TransactionSynchronization.STATUS_COMMITTED: + statusStr = "committed"; + break; + case TransactionSynchronization.STATUS_ROLLED_BACK: + statusStr = "rolled-back"; + break; + default: + } + if (logger.isDebugEnabled()) + { + logger.debug("After completion (" + statusStr + "): " + this); + } + + Set priorities = priorityLookup.keySet(); + + SortedSet sortedPriorities = new ConcurrentSkipListSet(REVERSE_INTEGER_ORDER); + sortedPriorities.addAll(priorities); + + // Need to run these in reverse order cache,lucene,listeners + for(Integer priority : sortedPriorities) + { + Set listeners = new HashSet(priorityLookup.get(priority)); + + for(TransactionListener listener : listeners) + { + try + { + if (status == TransactionSynchronization.STATUS_COMMITTED) + { + listener.afterCommit(); + } + else + { + listener.afterRollback(); + } + } + catch (RuntimeException e) + { + logger.error("After completion (" + statusStr + ") exception", e); + } + } + } + if(logger.isDebugEnabled()) + { + logger.debug("After Completion: DONE"); + } + + TransactionSupportUtil.clearResources(); + } + } + + static private Comparator FORWARD_INTEGER_ORDER = new Comparator() + { + @Override + public int compare(Integer arg0, Integer arg1) + { + return arg0.intValue() - arg1.intValue(); + } + } ; + + static private Comparator REVERSE_INTEGER_ORDER = new Comparator() + { + @Override + public int compare(Integer arg0, Integer arg1) + { + return arg1.intValue() - arg0.intValue(); + } + } ; + +} diff --git a/core/src/main/java/org/alfresco/web/scripts/servlet/StaticAssetCacheFilter.java b/core/src/main/java/org/alfresco/web/scripts/servlet/StaticAssetCacheFilter.java new file mode 100644 index 0000000000..0a77cdfcea --- /dev/null +++ b/core/src/main/java/org/alfresco/web/scripts/servlet/StaticAssetCacheFilter.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.web.scripts.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Simple servlet filter to add a 'Cache-Control' HTTP header to a response. + * The Cache-Control header is set to a max-age value by a configurable setting + * in the 'expires' init parameters - values are in days. + * + * WebScripts or other servlets that happen to match the response type + * configured for the filter (e.g. "*.js") should override cache settings + * as required. + * + * @author Kevin Roast + */ +public class StaticAssetCacheFilter implements Filter +{ + private static final long DAY_S = 60L*60L*24L; // 1 day in seconds + private static final long DEFAULT_30DAYS = 30L; // default of 30 days if not configured + + private long expire = DAY_S * DEFAULT_30DAYS; // initially set to default value of 30 days + + + /* (non-Javadoc) + * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) + */ + public void init(FilterConfig config) throws ServletException + { + String expireParam = config.getInitParameter("expires"); + if (expireParam != null) + { + this.expire = Long.parseLong(expireParam) * DAY_S; + } + } + + /* (non-Javadoc) + * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, + ServletException + { + ((HttpServletResponse)res).setHeader("Cache-Control", "must-revalidate, max-age=" + Long.toString(this.expire)); + chain.doFilter(req, res); + } + + /* (non-Javadoc) + * @see javax.servlet.Filter#destroy() + */ + public void destroy() + { + this.expire = DAY_S * DEFAULT_30DAYS; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/alfresco/web/scripts/servlet/X509ServletFilterBase.java b/core/src/main/java/org/alfresco/web/scripts/servlet/X509ServletFilterBase.java new file mode 100644 index 0000000000..d30f688cf6 --- /dev/null +++ b/core/src/main/java/org/alfresco/web/scripts/servlet/X509ServletFilterBase.java @@ -0,0 +1,346 @@ +/* +* Copyright (C) 2005-2013 Alfresco Software Limited. +* +* This file is part of Alfresco +* +* Alfresco is free software: you can redistribute it and/or modify +* it under the terms of the GNU Lesser General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Alfresco is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with Alfresco. If not, see . +*/ + +package org.alfresco.web.scripts.servlet; + +import javax.management.*; +import javax.security.auth.x500.X500Principal; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * The X509ServletFilterBase enforces X509 Authentication. + * + * Optional Init Param: + * cert-contains : Ensure that the principal subject of the cert contains a specific string. + * + * The X509ServletFilter will also ensure that the cert is present in the request, which will only happen if there + * is a successful SSL handshake which includes client authentication. This handshake is handled by the Application Server. + * A SSL handshake that does not include client Authentication will receive a 403 error response. + * + * The checkInforce method must be implemented to determine if the X509 Authentication is turned on. This allows + * applications to turn on/off X509 Authentication based on parameters outside of the web.xml. + * + * */ + +public abstract class X509ServletFilterBase implements Filter +{ + + protected boolean enforce; + private String httpsPort; + private String certContains; + + /** + * The regular expression that will match the CR/LF. + */ + private static final Pattern PATTERN_CRLF = Pattern.compile("(\\r|\\n)"); + private static Log logger = LogFactory.getLog(X509ServletFilterBase.class); + + public void init(FilterConfig config) throws ServletException + { + try + { + /* + * Find out if we are enforcing. + */ + + if(logger.isDebugEnabled()) + { + logger.debug("Initializing X509ServletFilter"); + } + + this.handleClientAuth(); + + this.enforce = checkEnforce(config.getServletContext()); + + if(logger.isDebugEnabled()) + { + logger.debug("Enforcing X509 Authentication:"+this.enforce); + } + + if (this.enforce) + { + /* + * We are enforcing so get the cert-contains string. + */ + + this.certContains = config.getInitParameter("cert-contains"); + + if(logger.isDebugEnabled()) + { + if(certContains == null) + { + logger.debug("Not enforcing cert-contains"); + } + else + { + logger.debug("Enforcing cert-contains:" + this.certContains); + } + } + } + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + public void setHttpsPort(int port) + { + this.httpsPort = Integer.toString(port); + } + + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) throws IOException, + ServletException + { + + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + /* + * Test if we are enforcing X509. + */ + if(this.enforce) + { + if(logger.isDebugEnabled()) + { + logger.debug("Enforcing X509 request"); + } + + X509Certificate[] certs = (X509Certificate[])httpRequest.getAttribute("javax.servlet.request.X509Certificate"); + if(validCert(certs)) + { + + if(logger.isDebugEnabled()) + { + logger.debug("Cert is valid"); + } + + /* + * The cert is valid so forward the request. + */ + + chain.doFilter(request,response); + } + else + { + if(logger.isDebugEnabled()) + { + logger.debug("Cert is invalid"); + } + + if(!httpRequest.isSecure()) + { + if(this.httpsPort != null) + { + String redirectUrl = httpRequest.getRequestURL().toString(); + int port = httpRequest.getLocalPort(); + String httpPort = Integer.toString(port); + redirectUrl = redirectUrl.replace(httpPort, httpsPort); + redirectUrl = redirectUrl.replace("http", "https"); + String query = httpRequest.getQueryString(); + if(query != null) + { + redirectUrl = redirectUrl+"?"+query; + } + + if(logger.isDebugEnabled()) + { + logger.debug("Redirecting to:"+redirectUrl); + } + + // MNT-18461: Prevent redirects containing CRLF + redirectUrl = sanitize(redirectUrl); + httpResponse.sendRedirect(redirectUrl); + return; + } + } + /* + * Invalid cert so send 403. + */ + httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "X509 Authentication failure"); + } + } + else + { + /* + * We are not enforcing X509 so forward the request + */ + chain.doFilter(request,response); + } + } + + + /** + * + * @param servletContext + * @return true if enforcing X509 false if not enforcing X509 + * @throws IOException + * + * The checkInforce method is called during the initialization of the Filter. Implement this method to decide if + * X509 security is being enforced. + * + **/ + + protected abstract boolean checkEnforce(ServletContext servletContext) throws IOException; + + private boolean validCert(X509Certificate[] certs) + { + /* + * If the cert is null then the it's not valid. + */ + + if(certs == null) + { + return false; + } + + /* + * Get the first certificate in the chain. The first certificate is the client certificate. + */ + + X509Certificate cert = certs[0]; + try + { + /* + * check the certificate has not expired. + */ + if(logger.isDebugEnabled()) + { + logger.debug("Checking cert is valid"); + } + cert.checkValidity(); + } + catch (Exception e) + { + logger.error("Cert is invalid", e); + return false; + } + + X500Principal x500Principal = cert.getSubjectX500Principal(); + String name = x500Principal.getName(); + + /* + * Cert contains is an optional check + */ + + if(this.certContains == null) + { + return true; + } + + /* + * Check that the cert contains the specified value. + */ + + if(name.contains(this.certContains)) + { + if(logger.isDebugEnabled()) + { + logger.debug("Cert: "+ name + " contains: "+ this.certContains); + } + + return true; + } + else + { + logger.error("Cert: " + name + " does not contain: " + this.certContains); + return false; + } + } + + + private String sanitize(String redirectUrl) + { + if (redirectUrl != null) + { + return PATTERN_CRLF.matcher(redirectUrl).replaceAll(""); + } + + return null; + } + + public void destroy() + { + } + + private void handleClientAuth() + { + try + { + MBeanServer mBeanServer = MBeanServerFactory.findMBeanServer(null).get(0); + + //Are we Tomcat + ObjectName catalina = new ObjectName("Catalina", "type", "Engine"); + Set objectNames = mBeanServer.queryNames(catalina, null); + if(objectNames == null || objectNames.size() == 0) + { + //We do not appear to be Tomcat + return; + } + + //We are Tomcat so look for the clientAuth + QueryExp query = Query.or(Query.eq(Query.attr("clientAuth"), Query.value("want")), + Query.eq(Query.attr("clientAuth"), Query.value(true))); + + objectNames = mBeanServer.queryNames(null, query); + + if (objectNames != null && objectNames.size() == 0) + { + logger.warn("clientAuth does not appear to be set for Tomcat. clientAuth must be set to 'want' for X509 Authentication"); + logger.warn("Attempting to set clientAuth=want through JMX..."); + + query = Query.eq(Query.attr("secure"), Query.value(true)); + + Set objectNames1 = mBeanServer.queryNames(null, query); + + if(objectNames1 != null) + { + for(ObjectName objectName : objectNames1) + { + if(objectName.toString().contains("ProtocolHandler")) + { + logger.warn("Setting clientAuth=want on MBean:" + objectName.toString()); + mBeanServer.setAttribute(objectName, new Attribute("clientAuth", "want")); + return; + } + } + } + + logger.warn("Unable to set clientAuth=want through JMX."); + } + } + catch(Throwable t) + { + logger.warn("An error occurred while checking for clientAuth. Turn on debug logging to see the stacktrace."); + logger.debug("Error while handling clientAuth",t); + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/alfresco/config/SystemPropertiesSetterBeanTest.java b/core/src/test/java/org/alfresco/config/SystemPropertiesSetterBeanTest.java new file mode 100644 index 0000000000..4d91bc75f0 --- /dev/null +++ b/core/src/test/java/org/alfresco/config/SystemPropertiesSetterBeanTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.config; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * @see SystemPropertiesSetterBean + * + * @author Derek Hulley + */ +public class SystemPropertiesSetterBeanTest extends TestCase +{ + private static final String KEY_A = "SystemPropertiesSetterBeanTest.A"; + private static final String KEY_B = "SystemPropertiesSetterBeanTest.B"; + private static final String KEY_C = "SystemPropertiesSetterBeanTest.C"; + private static final String KEY_EXISTING = "SystemPropertiesSetterBeanTest.EXISTING "; + private static final String KEY_PLACEHOLDER = "SystemPropertiesSetterBeanTest.PLACEHOLDER"; + private static final String KEY_EMPTY_STRING = "SystemPropertiesSetterBeanTest.EMPTY_STRING"; + private static final String KEY_NULL = "SystemPropertiesSetterBeanTest.NULL"; + private static final String VALUE_A = "A"; + private static final String VALUE_B = "B"; + private static final String VALUE_C = "C"; + private static final String VALUE_EXISTING = "EXISTING"; + private static final String VALUE_PLACEHOLDER = "${OOPS}"; + private static final String VALUE_EMPTY_STRING = ""; + private static final String VALUE_NULL = null; + + SystemPropertiesSetterBean setter; + private Map propertyMap; + + public void setUp() throws Exception + { + System.setProperty(KEY_EXISTING, VALUE_EXISTING); + + propertyMap = new HashMap(7); + propertyMap.put(KEY_A, VALUE_A); + propertyMap.put(KEY_B, VALUE_B); + propertyMap.put(KEY_C, VALUE_C); + propertyMap.put(KEY_EXISTING, "SHOULD NOT HAVE OVERRIDDEN EXISTING PROPERTY"); + propertyMap.put(KEY_PLACEHOLDER, VALUE_PLACEHOLDER); + propertyMap.put(KEY_EMPTY_STRING, VALUE_EMPTY_STRING); + propertyMap.put(KEY_NULL, VALUE_NULL); + + setter = new SystemPropertiesSetterBean(); + setter.setPropertyMap(propertyMap); + } + + public void testSetUp() + { + assertEquals(VALUE_EXISTING, System.getProperty(KEY_EXISTING)); + assertNull(System.getProperty(KEY_A)); + assertNull(System.getProperty(KEY_B)); + assertNull(System.getProperty(KEY_C)); + } + + public void testSettingOfSystemProperties() + { + setter.init(); + // Check + assertEquals(VALUE_A, System.getProperty(KEY_A)); + assertEquals(VALUE_B, System.getProperty(KEY_B)); + assertEquals(VALUE_C, System.getProperty(KEY_C)); + assertEquals(VALUE_EXISTING, System.getProperty(KEY_EXISTING)); + assertNull("Property placeholder not detected", System.getProperty(KEY_PLACEHOLDER)); + assertNull("Empty string not detected", System.getProperty(KEY_EMPTY_STRING)); + assertNull("Null string not detected", System.getProperty(KEY_NULL)); + } +} diff --git a/core/src/test/java/org/alfresco/encryption/EncryptingOutputStreamTest.java b/core/src/test/java/org/alfresco/encryption/EncryptingOutputStreamTest.java new file mode 100644 index 0000000000..9a22b69a75 --- /dev/null +++ b/core/src/test/java/org/alfresco/encryption/EncryptingOutputStreamTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved. + * + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ +package org.alfresco.encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.util.Arrays; + +import junit.framework.TestCase; + +/** + * Tests that the EncryptingOutputStream and EncryptingInputStream classes work correctly. + */ +public class EncryptingOutputStreamTest extends TestCase +{ + + /** + * Tests encryption / decryption by attempting to encrypt and decrypt the bytes forming this class definition and + * comparing it with the unencrypted bytes. + * + * @throws Exception + * an exception + */ + public void testEncrypt() throws Exception + { + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + final SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + final byte[] seed = getClass().getName().getBytes("UTF-8"); + random.setSeed(seed); + keyGen.initialize(1024, random); + final KeyPair pair = keyGen.generateKeyPair(); + + final ByteArrayOutputStream buff = new ByteArrayOutputStream(2048); + final OutputStream encrypting = new EncryptingOutputStream(buff, pair.getPublic(), random); + final ByteArrayOutputStream plainText1 = new ByteArrayOutputStream(2048); + + final InputStream in = getClass().getResourceAsStream(getClass().getSimpleName() + ".class"); + final byte[] inbuff = new byte[17]; + int bytesRead; + while ((bytesRead = in.read(inbuff)) != -1) + { + encrypting.write(inbuff, 0, bytesRead); + plainText1.write(inbuff, 0, bytesRead); + } + in.close(); + encrypting.close(); + plainText1.close(); + final InputStream decrypting = new DecryptingInputStream(new ByteArrayInputStream(buff.toByteArray()), pair + .getPrivate()); + final ByteArrayOutputStream plainText2 = new ByteArrayOutputStream(2048); + while ((bytesRead = decrypting.read(inbuff)) != -1) + { + plainText2.write(inbuff, 0, bytesRead); + } + + assertTrue(Arrays.equals(plainText1.toByteArray(), plainText2.toByteArray())); + + } +} diff --git a/core/src/test/java/org/alfresco/error/AlfrescoRuntimeExceptionTest.java b/core/src/test/java/org/alfresco/error/AlfrescoRuntimeExceptionTest.java new file mode 100644 index 0000000000..76a228a3c0 --- /dev/null +++ b/core/src/test/java/org/alfresco/error/AlfrescoRuntimeExceptionTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.error; + +import java.net.URL; +import java.util.Locale; +import java.util.ResourceBundle; + +import junit.framework.TestCase; + +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Alfresco runtime exception test + * + * @author Roy Wetherall + */ +public class AlfrescoRuntimeExceptionTest extends TestCase +{ + private static final String BASE_RESOURCE_NAME = "org.alfresco.i18n.testMessages"; + private static final String PARAM_VALUE = "television"; + private static final String MSG_PARAMS = "msg_params"; + private static final String MSG_ERROR = "msg_error"; + private static final String VALUE_ERROR = "This is an error message. \n This is on a new line."; + private static final String VALUE_FR_ERROR = "C'est un message d'erreur. \n C'est sur une nouvelle ligne."; + private static final String VALUE_PARAMS = "What no " + PARAM_VALUE + "?"; + private static final String VALUE_FR_PARAMS = "Que non " + PARAM_VALUE + "?"; + private static final String NON_I18NED_MSG = "This is a non i18ned error message."; + private static final String NON_EXISTING_MSG = "non.existing.msgId"; + + @Override + protected void setUp() throws Exception + { + // Re-set the current locale to be the default + Locale.setDefault(Locale.ENGLISH); + I18NUtil.setLocale(Locale.getDefault()); + } + + public void testI18NBehaviour() + { + // Ensure that the bundle is present on the classpath + String baseResourceAsProperty = BASE_RESOURCE_NAME.replace('.', '/') + ".properties"; + URL baseResourceURL = AlfrescoRuntimeExceptionTest.class.getClassLoader().getResource(baseResourceAsProperty); + assertNotNull(baseResourceURL); + + baseResourceAsProperty = BASE_RESOURCE_NAME.replace('.', '/') + "_fr_FR" + ".properties"; + baseResourceURL = AlfrescoRuntimeExceptionTest.class.getClassLoader().getResource(baseResourceAsProperty); + assertNotNull(baseResourceURL); + + // Ensure we can load it as a resource bundle + ResourceBundle properties = ResourceBundle.getBundle(BASE_RESOURCE_NAME); + assertNotNull(properties); + properties = ResourceBundle.getBundle(BASE_RESOURCE_NAME, new Locale("fr", "FR")); + assertNotNull(properties); + + + // From here on in, we use Spring + + // Register the bundle + I18NUtil.registerResourceBundle(BASE_RESOURCE_NAME); + + AlfrescoRuntimeException exception1 = new AlfrescoRuntimeException(MSG_PARAMS, new Object[]{PARAM_VALUE}); + assertTrue(exception1.getMessage().contains(VALUE_PARAMS)); + AlfrescoRuntimeException exception3 = new AlfrescoRuntimeException(MSG_ERROR); + assertTrue(exception3.getMessage().contains(VALUE_ERROR)); + + // Change the locale and re-test + I18NUtil.setLocale(new Locale("fr", "FR")); + + AlfrescoRuntimeException exception2 = new AlfrescoRuntimeException(MSG_PARAMS, new Object[]{PARAM_VALUE}); + assertTrue(exception2.getMessage().contains(VALUE_FR_PARAMS)); + AlfrescoRuntimeException exception4 = new AlfrescoRuntimeException(MSG_ERROR); + assertTrue(exception4.getMessage().contains(VALUE_FR_ERROR)); + + AlfrescoRuntimeException exception5 = new AlfrescoRuntimeException(NON_I18NED_MSG); + assertTrue(exception5.getMessage().contains(NON_I18NED_MSG)); + + // MNT-13028 + String param1 = PARAM_VALUE + "_1"; + String param2 = PARAM_VALUE + "_2"; + String param3 = PARAM_VALUE + "_3"; + AlfrescoRuntimeException exception6 = new AlfrescoRuntimeException(NON_EXISTING_MSG, new Object[]{param1, param2, param3}); + String message6 = exception6.getMessage(); + assertTrue(message6.contains(NON_EXISTING_MSG)); + assertTrue(message6.contains(param1)); + assertTrue(message6.contains(param2)); + assertTrue(message6.contains(param3)); + } + + public void testMakeRuntimeException() + { + Throwable e = new RuntimeException("sfsfs"); + RuntimeException ee = AlfrescoRuntimeException.makeRuntimeException(e, "Test"); + assertTrue("Exception should not have been changed", ee == e); + + e = new Exception(); + ee = AlfrescoRuntimeException.makeRuntimeException(e, "Test"); + assertTrue("Expected an AlfrescoRuntimeException instance", ee instanceof AlfrescoRuntimeException); + } +} diff --git a/core/src/test/java/org/alfresco/query/CannedQueryTest.java b/core/src/test/java/org/alfresco/query/CannedQueryTest.java new file mode 100644 index 0000000000..73c2b96a56 --- /dev/null +++ b/core/src/test/java/org/alfresco/query/CannedQueryTest.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.TestCase; + +import org.alfresco.query.CannedQuerySortDetails.SortOrder; +import org.alfresco.util.Pair; +import org.alfresco.util.registry.NamedObjectRegistry; + +/** + * Tests the {@link CannedQuery name query} infrastructure. + * + * @author Derek Hulley + * @since 4.0 + */ +public class CannedQueryTest extends TestCase +{ + private static final String QUERY_TEST_ONE = "test.query.one"; + private static final String QUERY_TEST_TWO = "test.query.two"; + + private static final List RESULTS_ONE; + private static final List RESULTS_TWO; + private static final Set ANTI_RESULTS; + + static + { + RESULTS_ONE = new ArrayList(10); + for (int i = 0; i < 10; i++) + { + RESULTS_ONE.add("ONE_" + i); + } + RESULTS_TWO = new ArrayList(10); + for (int i = 0; i < 10; i++) + { + RESULTS_TWO.add(new Long(i)); + } + ANTI_RESULTS = new HashSet(); + ANTI_RESULTS.add("ONE_5"); + ANTI_RESULTS.add(new Long(5)); + } + + @SuppressWarnings("rawtypes") + private NamedObjectRegistry namedQueryFactoryRegistry; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void setUp() throws Exception + { + // Create the registry + namedQueryFactoryRegistry = new NamedObjectRegistry(); + namedQueryFactoryRegistry.setStorageType(CannedQueryFactory.class); + namedQueryFactoryRegistry.setNamePattern("test\\.query\\..*"); + // Registry the query factories + // ONE + TestCannedQueryFactory namedQueryFactoryOne = new TestCannedQueryFactory(RESULTS_ONE); + namedQueryFactoryOne.setBeanName(QUERY_TEST_ONE); + namedQueryFactoryOne.setRegistry(namedQueryFactoryRegistry); + namedQueryFactoryOne.afterPropertiesSet(); + // TWO + TestCannedQueryFactory namedQueryFactoryTwo = new TestCannedQueryFactory(RESULTS_TWO); + namedQueryFactoryTwo.setBeanName(QUERY_TEST_TWO); + namedQueryFactoryTwo.setRegistry(namedQueryFactoryRegistry); + namedQueryFactoryTwo.afterPropertiesSet(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testRegistry() throws Exception + { + CannedQueryFactory one = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + assertNotNull("No factory for " + QUERY_TEST_ONE, one); + CannedQueryFactory two = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_TWO); + assertNotNull("No factory for " + QUERY_TEST_TWO, two); + // Kick out registrations with incorrect naming convention + try + { + TestCannedQueryFactory namedQueryFactoryBogus = new TestCannedQueryFactory(RESULTS_TWO); + namedQueryFactoryBogus.setBeanName("test_query_blah"); + namedQueryFactoryBogus.setRegistry(namedQueryFactoryRegistry); + namedQueryFactoryBogus.afterPropertiesSet(); + fail("Should have kicked out incorrectly-named registered queries"); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + @SuppressWarnings("unchecked") + public void testQueryAllResults() throws Exception + { + // An instance of the CannedQueryFactory could be injected or constructed as well + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryParameters params = new CannedQueryParameters(null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Attempt to reuse the query + try + { + qOne.execute(); + fail("Second execution of same instance must not be allowed."); + } + catch (IllegalStateException e) + { + // Expected + } + // Get the number of results when not requested + try + { + qrOne.getTotalResultCount(); + fail("Expected failure when requesting total count without explicit request."); + } + catch (IllegalStateException e) + { + // Expected + } + // Get the paged result count + int pagedResultCount = qrOne.getPagedResultCount(); + assertEquals("Incorrect number of results", 9, pagedResultCount); + assertEquals("No sorting was specified in the parameters", "ONE_0", qrOne.getPages().get(0).get(0)); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryMaxResults() throws Exception + { + // An instance of the CannedQueryFactory could be injected or constructed as well + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQuery qOne = qfOne.getCannedQuery(null, 0, 9, null); + CannedQueryResults qrOne = qOne.execute(); + + // Get the paged result count + int pagedResultCount = qrOne.getPagedResultCount(); + assertEquals("Incorrect number of results", 9, pagedResultCount); + assertEquals("Incorrect number of pages", 1, qrOne.getPageCount()); + List> pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 1, pages.size()); + assertEquals("No sorting was specified in the parameters", "ONE_0", qrOne.getPages().get(0).get(0)); + assertEquals("No sorting was specified in the parameters", "ONE_9", qrOne.getPages().get(0).get(8)); + assertFalse("Should have more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryPagedResults() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryPageDetails qPageDetails = new CannedQueryPageDetails(0, 5, 1, 2); + CannedQueryParameters params = new CannedQueryParameters(null, qPageDetails, null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 9, qrOne.getPagedResultCount()); + assertEquals("No sorting was specified in the parameters", "ONE_0", qrOne.getPages().get(0).get(0)); + assertEquals("No sorting was specified in the parameters", "ONE_9", qrOne.getPages().get(1).get(3)); + List> pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 2, pages.size()); + assertEquals("Incorrect results on page", 5, pages.get(0).size()); + assertEquals("Incorrect results on page", 4, pages.get(1).size()); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + + // Skip some results and use different page sizes + qPageDetails = new CannedQueryPageDetails(2, 3, 1, 3); + params = new CannedQueryParameters(null, qPageDetails, null); + qOne = qfOne.getCannedQuery(params); + qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 7, qrOne.getPagedResultCount()); + assertEquals("Incorrect number of pages", 3, qrOne.getPageCount()); + pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 3, pages.size()); + assertEquals("Incorrect results on page", 3, pages.get(0).size()); + assertEquals("Incorrect results on page", 3, pages.get(1).size()); + assertEquals("Incorrect results on page", 1, pages.get(2).size()); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + + // Skip some results and return less pages + qPageDetails = new CannedQueryPageDetails(2, 3, 1, 2); + params = new CannedQueryParameters(null, qPageDetails, null); + qOne = qfOne.getCannedQuery(params); + qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 6, qrOne.getPagedResultCount()); + assertEquals("Incorrect number of pages", 2, qrOne.getPageCount()); + pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 2, pages.size()); + assertEquals("Incorrect results on page", 3, pages.get(0).size()); + assertEquals("Incorrect results on page", 3, pages.get(1).size()); + assertTrue("Should have more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQuerySortedResults() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQuerySortDetails qSortDetails = new CannedQuerySortDetails( + new Pair("blah", SortOrder.DESCENDING)); + CannedQueryParameters params = new CannedQueryParameters(null, null, qSortDetails); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 9, qrOne.getPagedResultCount()); + assertEquals("Expected inverse sorting", "ONE_9", qrOne.getPages().get(0).get(0)); + assertEquals("Expected inverse sorting", "ONE_0", qrOne.getPages().get(0).get(8)); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryPermissionCheckedResults() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryParameters params = new CannedQueryParameters(null, null, null, 0, null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 9, qrOne.getPagedResultCount()); + assertEquals("Incorrect result order", "ONE_0", qrOne.getPages().get(0).get(0)); + assertEquals("Incorrect result order", "ONE_1", qrOne.getPages().get(0).get(1)); + assertEquals("Incorrect result order", "ONE_2", qrOne.getPages().get(0).get(2)); + assertEquals("Incorrect result order", "ONE_3", qrOne.getPages().get(0).get(3)); + assertEquals("Incorrect result order", "ONE_4", qrOne.getPages().get(0).get(4)); // << missing 5! + assertEquals("Incorrect result order", "ONE_6", qrOne.getPages().get(0).get(5)); + assertEquals("Incorrect result order", "ONE_7", qrOne.getPages().get(0).get(6)); + assertEquals("Incorrect result order", "ONE_8", qrOne.getPages().get(0).get(7)); + assertEquals("Incorrect result order", "ONE_9", qrOne.getPages().get(0).get(8)); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryPermissionCheckedPagedTotalCount() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryPageDetails qPageDetails = new CannedQueryPageDetails(5, 1, 1, 1); + CannedQuerySortDetails qSortDetails = new CannedQuerySortDetails( + new Pair("blah", SortOrder.DESCENDING)); + CannedQueryParameters params = new CannedQueryParameters(null, qPageDetails, qSortDetails, 1000, null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of total results", + new Pair(9,9), qrOne.getTotalResultCount()); // Pre-paging + assertEquals("Incorrect number of paged results", 1, qrOne.getPagedResultCount()); // Skipped 5 + assertEquals("Incorrect result order", "ONE_3", qrOne.getPages().get(0).get(0)); // Order reversed + assertTrue("Should have more pages/items", qrOne.hasMoreItems()); + } + + /** + * Test factory to generate "queries" that just return a list of Strings. + * + * @param the type of the results + * + * @author Derek Hulley + * @since 4.0 + */ + private static class TestCannedQueryFactory extends AbstractCannedQueryFactory + { + private final List results; + private TestCannedQueryFactory(List results) + { + this.results = results; + } + + @Override + public CannedQuery getCannedQuery(CannedQueryParameters parameters) + { + String queryExecutionId = super.getQueryExecutionId(parameters); + return new TestCannedQuery(parameters, queryExecutionId, results, ANTI_RESULTS); + } + } + + /** + * Test query that just returns values passed in + * + * @param the type of the results + * + * @author Derek Hulley + * @since 4.0 + */ + private static class TestCannedQuery extends AbstractCannedQuery + { + private final List results; + private final Set antiResults; + private TestCannedQuery( + CannedQueryParameters params, + String queryExecutionId, List results, Set antiResults) + { + super(params); + this.results = results; + this.antiResults = antiResults; + } + + @Override + protected List queryAndFilter(CannedQueryParameters parameters) + { + return results; + } + + @Override + protected boolean isApplyPostQuerySorting() + { + return true; + } + + @Override + protected List applyPostQuerySorting(List results, CannedQuerySortDetails sortDetails) + { + if (sortDetails.getSortPairs().size() == 0) + { + // Nothing to sort on + return results; + } + List ret = new ArrayList(results); + Collections.reverse(ret); + return ret; + } + + @Override + protected boolean isApplyPostQueryPermissions() + { + return true; + } + + @Override + protected List applyPostQueryPermissions(List results, int requestedCount) + { + boolean cutoffAllowed = (getParameters().getTotalResultCountMax() == 0); + + final List ret = new ArrayList(results.size()); + for (T t : results) + { + if (!antiResults.contains(t)) + { + ret.add(t); + } + // Cut off if we have enough results + if (cutoffAllowed && ret.size() == requestedCount) + { + break; + } + } + + return ret; + } + } +} diff --git a/core/src/test/java/org/alfresco/util/BaseTest.java b/core/src/test/java/org/alfresco/util/BaseTest.java new file mode 100644 index 0000000000..6caebc4856 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/BaseTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.extensions.config.ConfigSource; +import org.springframework.extensions.config.source.ClassPathConfigSource; +import org.springframework.extensions.config.source.FileConfigSource; +import org.springframework.extensions.config.xml.XMLConfigService; + +/** + * Base class for all JUnit tests + * + * @author gavinc, Neil McErlean + */ +public abstract class BaseTest extends TestCase +{ + protected String resourcesDir; + + protected boolean loadFromClasspath; + + public BaseTest() + { + // GC: Added this to allow flexible test resources folder configuration + // Try to get resources dir from a system property otherwise uses the default hardcoded + // backward compatible + String resourcesDir = null; + // This allows subclasses to override the getResourcesDir method (e.g. by passing classpath: ) + if(getResourcesDir()==null) + { + resourcesDir = System.getProperty("alfresco.test.resources.dir"); + if(resourcesDir == null || resourcesDir.equals("")) + { + // defaults to source/test-resources + resourcesDir = System.getProperty("user.dir") + File.separator + "source" + File.separator + "test-resources"; + } + } + else + { + resourcesDir = getResourcesDir(); + } + loadFromClasspath = resourcesDir.startsWith("classpath:"); + // Returns the resources dir with trailing separator or just the classpath: string in case that was specified + this.resourcesDir = resourcesDir + ((loadFromClasspath) ? "" : File.separator); + } + + /** + * Override this method to pass a custom resource dir. + * Valid values are a file system path or the string "classpath:" + * @return + */ + public String getResourcesDir() + { + return this.resourcesDir; + } + + /** + * Checks for validity of the resource location. + * + * In case of file resource, provide the full file path + * In case of classpath resources, please pass the full resource URI, prepended with the classpath: string + * @param fullFileName + */ + protected void assertFileIsValid(String fullFileName) + { + if(loadFromClasspath) + { + // if we load from classpath, we need to remove the "classpath:" trailing string + String resourceName = fullFileName.substring(fullFileName.indexOf(":") + 1); + assertNotNull(resourceName); + URL configResourceUrl = getClass().getClassLoader().getResource(resourceName); + assertNotNull(configResourceUrl); + } + else + { + File f = new File(fullFileName); + assertTrue("Required file missing: " + fullFileName, f.exists()); + assertTrue("Required file not readable: " + fullFileName, f.canRead()); + } + } + + /** + * Loads a config file from filesystem or classpath + * + * In case of file resource, just provide the file name relative to resourceDir + * In case of classpath resources, just provide the resource URI, without with the prepending classpath: string + * @param xmlConfigFile + * @return + */ + protected XMLConfigService initXMLConfigService(String xmlConfigFile) + { + XMLConfigService svc = null; + if(loadFromClasspath) + { + svc = new XMLConfigService(new ClassPathConfigSource(xmlConfigFile)); + } + else + { + String fullFileName = getResourcesDir() + xmlConfigFile; + assertFileIsValid(fullFileName); + svc = new XMLConfigService(new FileConfigSource(fullFileName)); + } + svc.initConfig(); + return svc; + } + + protected XMLConfigService initXMLConfigService(String xmlConfigFile, String overridingXmlConfigFile) + { + List files = new ArrayList(2); + files.add(xmlConfigFile); + files.add(overridingXmlConfigFile); + return initXMLConfigService(files); + } + + protected XMLConfigService initXMLConfigService(List xmlConfigFilenames) + { + List configFiles = new ArrayList(); + for (String filename : xmlConfigFilenames) + { + // if we load from classpath then no need to prepend the resources dir (which will be .equals("classpath:") + String path = ((loadFromClasspath) ? "" : getResourcesDir()) + filename; + assertFileIsValid(path); + configFiles.add(path); + } + ConfigSource configSource = (loadFromClasspath) ? new ClassPathConfigSource(configFiles) : new FileConfigSource(configFiles); + XMLConfigService svc = new XMLConfigService(configSource); + svc.initConfig(); + return svc; + } +} diff --git a/core/src/test/java/org/alfresco/util/BridgeTableTest.java b/core/src/test/java/org/alfresco/util/BridgeTableTest.java new file mode 100644 index 0000000000..2ea97a5bb0 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/BridgeTableTest.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.HashSet; +import java.util.Set; + +import junit.framework.TestCase; + +import org.junit.Test; + +/** + * @author Andy + * + */ +public class BridgeTableTest extends TestCase +{ + + @Test + public void testBasic() + { + BridgeTable bridgeTable = new BridgeTable(); + bridgeTable.addLink("A", "B"); + bridgeTable.addLink("C", "D"); + bridgeTable.addLink("E", "F"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(0, bridgeTable.getAncestors("C").size()); + assertEquals(1, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(1, bridgeTable.getDescendants("A").size()); + assertEquals(0, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.addLink("B", "C"); + + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(2, bridgeTable.getAncestors("C").size()); + assertEquals(3, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(3, bridgeTable.getDescendants("A").size()); + assertEquals(2, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.addLink("D", "E"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertTrue(bridgeTable.getAncestors("B", 1).contains("A")); + assertEquals(2, bridgeTable.getAncestors("C").size()); + assertTrue(bridgeTable.getAncestors("C", 1).contains("B")); + assertTrue(bridgeTable.getAncestors("C", 2).contains("A")); + assertEquals(3, bridgeTable.getAncestors("D").size()); + assertTrue(bridgeTable.getAncestors("D", 1).contains("C")); + assertTrue(bridgeTable.getAncestors("D", 2).contains("B")); + assertTrue(bridgeTable.getAncestors("D", 3).contains("A")); + assertEquals(4, bridgeTable.getAncestors("E").size()); + assertTrue(bridgeTable.getAncestors("E", 1).contains("D")); + assertTrue(bridgeTable.getAncestors("E", 2).contains("C")); + assertTrue(bridgeTable.getAncestors("E", 3).contains("B")); + assertTrue(bridgeTable.getAncestors("E", 4).contains("A")); + assertEquals(5, bridgeTable.getAncestors("F").size()); + assertTrue(bridgeTable.getAncestors("F", 1).contains("E")); + assertTrue(bridgeTable.getAncestors("F", 2).contains("D")); + assertTrue(bridgeTable.getAncestors("F", 3).contains("C")); + assertTrue(bridgeTable.getAncestors("F", 4).contains("B")); + assertTrue(bridgeTable.getAncestors("F", 5).contains("A")); + + assertEquals(5, bridgeTable.getDescendants("A").size()); + assertTrue(bridgeTable.getDescendants("A", 1).contains("B")); + assertTrue(bridgeTable.getDescendants("A", 2).contains("C")); + assertTrue(bridgeTable.getDescendants("A", 3).contains("D")); + assertTrue(bridgeTable.getDescendants("A", 4).contains("E")); + assertTrue(bridgeTable.getDescendants("A", 5).contains("F")); + assertEquals(4, bridgeTable.getDescendants("B").size()); + assertTrue(bridgeTable.getDescendants("B", 1).contains("C")); + assertTrue(bridgeTable.getDescendants("B", 2).contains("D")); + assertTrue(bridgeTable.getDescendants("B", 3).contains("E")); + assertTrue(bridgeTable.getDescendants("B", 4).contains("F")); + assertEquals(3, bridgeTable.getDescendants("C").size()); + assertTrue(bridgeTable.getDescendants("C", 1).contains("D")); + assertTrue(bridgeTable.getDescendants("C", 2).contains("E")); + assertTrue(bridgeTable.getDescendants("C", 3).contains("F")); + assertEquals(2, bridgeTable.getDescendants("D").size()); + assertTrue(bridgeTable.getDescendants("D", 1).contains("E")); + assertTrue(bridgeTable.getDescendants("D", 2).contains("F")); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertTrue(bridgeTable.getDescendants("E", 1).contains("F")); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.removeLink("D", "E"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(2, bridgeTable.getAncestors("C").size()); + assertEquals(3, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(3, bridgeTable.getDescendants("A").size()); + assertEquals(2, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.removeLink("B", "C"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(0, bridgeTable.getAncestors("C").size()); + assertEquals(1, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(1, bridgeTable.getDescendants("A").size()); + assertEquals(0, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + } + +// @Test +// public void test_1M() +// { +// // 1M = 21 +// for (int i = 0; i < 20; i++) +// { +// BridgeTable bridgeTable = new BridgeTable(); +// long start = System.nanoTime(); +// bridgeTable.addLinks(getTreeLinks(i)); +// long end = System.nanoTime(); +// double elapsed = ((end - start) / 1e9); +// System.out.println("" + bridgeTable.size() + " in " + elapsed); +// assertTrue(elapsed < 60); +// } +// } + + @Test + public void test_16k() + { + // 1M = 21 + for (int i = 0; i < 15; i++) + { + BridgeTable bridgeTable = new BridgeTable(); + long start = System.nanoTime(); + bridgeTable.addLinks(getTreeLinks(i)); + long end = System.nanoTime(); + double elapsed = ((end - start) / 1e9); + System.out.println("" + bridgeTable.size() + " in " + elapsed); + assertTrue(elapsed < 60); + } + } + +// @Test +// public void test_1000x1000() +// { +// BridgeTable bridgeTable = new BridgeTable(); +// HashSet> links = new HashSet>(); +// for (int i = 0; i < 10; i++) +// { +// for (int j = 0; j < 100; j++) +// { +// links.addAll(getTreeLinks(10)); +// } +// +// +// long start = System.nanoTime(); +// bridgeTable.addLinks(links); +// long end = System.nanoTime(); +// System.out.println("Trees " + bridgeTable.size() + " in " + ((end - start) / 1e9)); +// +// start = System.nanoTime(); +// for (String key : bridgeTable.keySet()) +// { +// bridgeTable.getAncestors(key); +// } +// end = System.nanoTime(); +// System.out.println("By key " + bridgeTable.size() + " in " + ((end - start) / 1e9)); +// } +// } + + + @Test + public void test_100x100() + { + BridgeTable bridgeTable = new BridgeTable(); + HashSet> links = new HashSet>(); + for (int i = 0; i < 10; i++) + { + for (int j = 0; j < 10; j++) + { + links.addAll(getTreeLinks(7)); + } + + + long start = System.nanoTime(); + bridgeTable.addLinks(links); + long end = System.nanoTime(); + System.out.println("Trees " + bridgeTable.size() + " in " + ((end - start) / 1e9)); + + start = System.nanoTime(); + for (String key : bridgeTable.keySet()) + { + bridgeTable.getAncestors(key); + } + end = System.nanoTime(); + System.out.println("By key " + bridgeTable.size() + " in " + ((end - start) / 1e9)); + } + } + + @Test + public void testSecondary() + { + BridgeTable bridgeTable = new BridgeTable(); + + bridgeTable.addLink("A", "B"); + bridgeTable.addLink("A", "C"); + bridgeTable.addLink("B", "D"); + bridgeTable.addLink("B", "E"); + bridgeTable.addLink("C", "F"); + bridgeTable.addLink("C", "G"); + + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(4, bridgeTable.getDescendants("A", 2).size()); + assertEquals(6, bridgeTable.getDescendants("A", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + assertEquals(0, bridgeTable.getDescendants("B", 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1, 2).size()); + + bridgeTable.addLink("N", "O"); + bridgeTable.addLink("N", "P"); + bridgeTable.addLink("O", "Q"); + bridgeTable.addLink("O", "R"); + bridgeTable.addLink("P", "S"); + bridgeTable.addLink("P", "T"); + + assertEquals(2, bridgeTable.getDescendants("N", 1).size()); + assertEquals(4, bridgeTable.getDescendants("N", 2).size()); + assertEquals(6, bridgeTable.getDescendants("N", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1).size()); + assertEquals(0, bridgeTable.getDescendants("O", 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1, 2).size()); + + bridgeTable.addLink("A", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.addLink("A", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.addLink("B", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.addLink("B", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("A", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("A", "N"); + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("B", "N"); + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("B", "N"); + + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(4, bridgeTable.getDescendants("A", 2).size()); + assertEquals(6, bridgeTable.getDescendants("A", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + assertEquals(0, bridgeTable.getDescendants("B", 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1, 2).size()); + + + assertEquals(2, bridgeTable.getDescendants("N", 1).size()); + assertEquals(4, bridgeTable.getDescendants("N", 2).size()); + assertEquals(6, bridgeTable.getDescendants("N", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1).size()); + assertEquals(0, bridgeTable.getDescendants("O", 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1, 2).size()); + + } + + private Set> getTreeLinks(int depth) + { + int count = 0; + String base = "" + System.nanoTime(); + + HashSet currentRow = new HashSet(); + HashSet lastRow = new HashSet(); + + HashSet> links = new HashSet>(); + + for (int i = 0; i < depth; i++) + { + if (lastRow.size() == 0) + { + currentRow.add("GROUP_" + base + "_" + count); + count++; + } + else + { + for (String group : lastRow) + { + String newGroup = "GROUP_" + base + "_" + count; + currentRow.add(newGroup); + count++; + links.add(new Pair(group, newGroup)); + + newGroup = "GROUP_" + base + "_" + count; + currentRow.add(newGroup); + count++; + links.add(new Pair(group, newGroup)); + } + } + lastRow = currentRow; + currentRow = new HashSet(); + } + + return links; + + } + +} diff --git a/core/src/test/java/org/alfresco/util/CachingDateFormatTest.java b/core/src/test/java/org/alfresco/util/CachingDateFormatTest.java new file mode 100644 index 0000000000..dfd3096181 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/CachingDateFormatTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2005-2018 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public class CachingDateFormatTest +{ + private final LocalDateTime REFERENCE_DATE_TIME = LocalDateTime.of(2018, 4, 1, 10, 0); //2018-04-01 at 10:00am + private final Locale defaultLocale = Locale.getDefault(); + + @Before + public void setUp() + { + CachingDateFormat.S_LOCAL_SOLR_DATETIME.remove(); + } + + @Test + public void solrDatetimeFormat_shouldFormatTheMinDate() + { + Date shanghaiDate = testDate("Asia/Shanghai"); + SimpleDateFormat solrDatetimeFormat = CachingDateFormat.getSolrDatetimeFormatWithoutMsecs(); + + String formattedDate = solrDatetimeFormat.format(shanghaiDate); + + assertThat(formattedDate,is("2018-04-01T02:00:00Z")); + } + + @Test + public void solrDatetimeFormat_allLocales_shouldReturnISO8601DateString() + { + for(Locale currentLocale:Locale.getAvailableLocales()) + { + CachingDateFormat.S_LOCAL_SOLR_DATETIME.remove(); + Locale.setDefault(currentLocale); + + Date utcDate = testDate("UTC"); + SimpleDateFormat solrDatetimeFormat = CachingDateFormat.getSolrDatetimeFormat(); + + String formattedDate = solrDatetimeFormat.format(utcDate); + + assertThat(formattedDate, is("2018-04-01T10:00:00.000Z")); + } + } + + @Test + public void onlyDateFormatReturnsOnlyTheDatePart() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getDateOnlyFormat(); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals("2018-04-01", formatter.format(utcDate)); + } + + @Test + public void onlyTimeFormatShouldReturnOnlyTheTimePart() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getTimeOnlyFormat(); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals("10:00:00", formatter.format(utcDate)); + } + + @Test + public void dateTimeFormatShouldReturnDateAndTime() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getDateFormat(); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + + assertEquals("2018-04-01T10:00:00", formatter.format(utcDate)); + } + + @Test + public void utcWithoutMsecsDatetimeFormat_shouldReturnStringsWithoutMsecs() + { + Date utcDate = testDate("UTC"); + + SimpleDateFormat formatter = CachingDateFormat.getSolrDatetimeFormatWithoutMsecs(); + + assertEquals("2018-04-01T10:00:00Z", formatter.format(utcDate)); + } + + @After + public void tearDown() + { + Locale.setDefault(defaultLocale); + } + + /** + * Creates a test date using the given timezone id. + * + * @param zoneId the timezone id. + * @return a test date using the given timezone id. + */ + private Date testDate(String zoneId) + { + Instant utcInstant = REFERENCE_DATE_TIME.atZone(ZoneId.of(zoneId)).toInstant(); + return Date.from(utcInstant); + } +} diff --git a/core/src/test/java/org/alfresco/util/DynamicallySizedThreadPoolExecutorTest.java b/core/src/test/java/org/alfresco/util/DynamicallySizedThreadPoolExecutorTest.java new file mode 100644 index 0000000000..c5ace65cdf --- /dev/null +++ b/core/src/test/java/org/alfresco/util/DynamicallySizedThreadPoolExecutorTest.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.Map.Entry; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import junit.framework.TestCase; + +/** + * Tests for our instance of {@link java.util.concurrent.ThreadPoolExecutor} + * + * @author Nick Burch + */ +public class DynamicallySizedThreadPoolExecutorTest extends TestCase +{ + + private static Log logger = LogFactory.getLog(DynamicallySizedThreadPoolExecutorTest.class); + private static final int DEFAULT_KEEP_ALIVE_TIME = 90; + + @Override + protected void setUp() throws Exception + { + SleepUntilAllWake.reset(); + } + + public void testUpToCore() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(5,10, DEFAULT_KEEP_ALIVE_TIME); + + assertEquals(0, exec.getPoolSize()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(4, exec.getPoolSize()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertEquals(5, exec.getPoolSize()); + } + + public void testPastCoreButNotHugeQueue() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(5,10, DEFAULT_KEEP_ALIVE_TIME); + + assertEquals(0, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + // Need to hit max pool size before it adds more + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + assertEquals(5, exec.getQueue().size()); + + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + assertEquals(7, exec.getQueue().size()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertEquals(5, exec.getPoolSize()); + } + + public void testToExpandQueue() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(2,4,1); + + assertEquals(0, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // Next should add one + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(3, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // And again + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(4, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // But no more will be added, as we're at max + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(4, exec.getPoolSize()); + assertEquals(6, exec.getQueue().size()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + + // All threads still running, as 1 second timeout + assertEquals(4, exec.getPoolSize()); + } + + public void offTestToExpandThenContract() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(2,4,1); + exec.setKeepAliveTime(30, TimeUnit.MILLISECONDS); + + assertEquals(0, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // Next should add one + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(3, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // And again + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(4, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // But no more will be added, as we're at max + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(4, exec.getPoolSize()); + assertEquals(6, exec.getQueue().size()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + + // Wait longer than the timeout without any work, which should + // let all the extra threads go away + // (Depending on how closely your JVM follows the specification, + // we may fall back to the core size which is correct, or we + // may go to zero which is wrong, but hey, it's the JVM...) + logger.debug("Core pool size is " + exec.getCorePoolSize()); + logger.debug("Current pool size is " + exec.getPoolSize()); + logger.debug("Queue size is " + exec.getQueue().size()); + assertTrue( + "Pool size should be 0-2 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() >= 0 + ); + assertTrue( + "Pool size should be 0-2 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() <= 2 + ); + + SleepUntilAllWake.reset(); + + // Add 2 new jobs, will stay/ go to at 2 threads + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + + // Let the idle threads grab them, then check + Thread.sleep(20); + assertEquals(2, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + // 3 more, still at 2 threads + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // And again wait for it all + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertEquals(2, exec.getPoolSize()); + + + // Now decrease the overall pool size + // Will rise and fall to there now + exec.setCorePoolSize(1); + + // Run a quick job, to ensure that the + // "can I kill one yet" logic is applied + SleepUntilAllWake.reset(); + exec.execute(new SleepUntilAllWake()); + SleepUntilAllWake.wakeAll(); + + Thread.sleep(100); + assertEquals(1, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + SleepUntilAllWake.reset(); + + + // Push enough on to go up to 4 active threads + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + + Thread.sleep(20); // Let the new threads spin up + assertEquals(4, exec.getPoolSize()); + assertEquals(6, exec.getQueue().size()); + + // Wait for them all to finish, should drop back to 1 now + // (Or zero, if your JVM can't read the specification...) + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertTrue( + "Pool size should be 0 or 1 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() >= 0 + ); + assertTrue( + "Pool size should be 0 or 1 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() <= 1 + ); + } + + private DynamicallySizedThreadPoolExecutor createInstance(int corePoolSize, int maximumPoolSize, int keepAliveTime) + { + // We need a thread factory + TraceableThreadFactory threadFactory = new TraceableThreadFactory(); + threadFactory.setThreadDaemon(true); + threadFactory.setThreadPriority(Thread.NORM_PRIORITY); + + BlockingQueue workQueue = new LinkedBlockingQueue(); + + return new DynamicallySizedThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + TimeUnit.SECONDS, + workQueue, + threadFactory, + new ThreadPoolExecutor.CallerRunsPolicy()); + } + + public static class SleepUntilAllWake implements Runnable + { + private static ConcurrentMap sleeping = new ConcurrentHashMap(); + private static boolean allAwake = false; + + @Override + public void run() + { + if(allAwake) return; + + // Track us, and wait for the bang + logger.debug("Adding thread: " + Thread.currentThread().getName()); + sleeping.put(Thread.currentThread().getName(), Thread.currentThread()); + try + { + Thread.sleep(30*1000); + System.err.println("Warning - Thread finished sleeping without wake!"); + } + catch(InterruptedException e) + { + logger.debug("Interrupted thread: " + Thread.currentThread().getName()); + } + } + + public static void wakeAll() + { + allAwake = true; + for(Entry t : sleeping.entrySet()) + { + logger.debug("Interrupting thread: " + t.getKey()); + t.getValue().interrupt(); + } + } + public static void reset() + { + logger.debug("Resetting."); + allAwake = false; + sleeping.clear(); + } + } +} diff --git a/core/src/test/java/org/alfresco/util/EqualsHelperTest.java b/core/src/test/java/org/alfresco/util/EqualsHelperTest.java new file mode 100644 index 0000000000..c40d8afa34 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/EqualsHelperTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.EqualsHelper.MapValueComparison; + +import junit.framework.TestCase; + +/** + * @see EqualsHelper + * + * @author Derek Hulley + * @since 3.1SP2 + */ +public class EqualsHelperTest extends TestCase +{ + private File fileOne; + private File fileTwo; + + @Override + public void setUp() throws Exception + { + fileOne = TempFileProvider.createTempFile(getName(), "-one.txt"); + fileTwo = TempFileProvider.createTempFile(getName(), "-two.txt"); + + OutputStream osOne = new FileOutputStream(fileOne); + osOne.write("1234567890 - ONE".getBytes("UTF-8")); + osOne.close(); + + OutputStream osTwo = new FileOutputStream(fileTwo); + osTwo.write("1234567890 - TWO".getBytes("UTF-8")); + osTwo.close(); + } + + public void testStreamsNotEqual() throws Exception + { + InputStream isLeft = new FileInputStream(fileOne); + InputStream isRight = new FileInputStream(fileTwo); + boolean equal = EqualsHelper.binaryStreamEquals(isLeft, isRight); + assertFalse("Should not be the same", equal); + } + + public void testStreamsEqual() throws Exception + { + InputStream isLeft = new FileInputStream(fileOne); + InputStream isRight = new FileInputStream(fileOne); + boolean equal = EqualsHelper.binaryStreamEquals(isLeft, isRight); + assertTrue("Should be the same", equal); + } + + public void testMapComparison() throws Exception + { + Map left = new HashMap(); + Map right = new HashMap(); + // EQUAL + left.put(0, "A"); + right.put(0, "A"); + // NOT_EQUAL + left.put(1, "A"); + right.put(1, "B"); + // EQUAL + left.put(2, null); + right.put(2, null); + // NOT_EQUAL + left.put(3, null); + right.put(3, "B"); + // NOT_EQUAL + left.put(4, "A"); + right.put(4, null); + // RIGHT_ONLY + right.put(5, "B"); + // LEFT_ONLY + left.put(6, "A"); + Map diff = EqualsHelper.getMapComparison(left, right); + assertEquals("'EQUAL' check failed", MapValueComparison.EQUAL, diff.get(0)); + assertEquals("'NOT_EQUAL' check failed", MapValueComparison.NOT_EQUAL, diff.get(1)); + assertEquals("'EQUAL' check failed", MapValueComparison.EQUAL, diff.get(2)); + assertEquals("'NOT_EQUAL' check failed", MapValueComparison.NOT_EQUAL, diff.get(3)); + assertEquals("'NOT_EQUAL' check failed", MapValueComparison.NOT_EQUAL, diff.get(4)); + assertEquals("'RIGHT_ONLY' check failed", MapValueComparison.RIGHT_ONLY, diff.get(5)); + assertEquals("'LEFT_ONLY' check failed", MapValueComparison.LEFT_ONLY, diff.get(6)); + } +} diff --git a/core/src/test/java/org/alfresco/util/GuidTest.java b/core/src/test/java/org/alfresco/util/GuidTest.java new file mode 100644 index 0000000000..a99f96b00d --- /dev/null +++ b/core/src/test/java/org/alfresco/util/GuidTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.util; + +import java.lang.Thread.State; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.TestCase; + +import org.junit.Assert; + +/** + * Test class for GUID generation + * + * @author Andreea Dragoi + * + */ +public class GuidTest extends TestCase +{ + class GuidRunner implements Runnable + { + @Override + public void run() + { + GUID.generate(); + } + } + + /** + * Tests the improvement added by using a SecureRandom pool when generating GUID's + */ + public void testGuid() + { + // warm-up (to pre-init the secureRandomArray) + GUID.generate(); + + List threads = new ArrayList<>(); + int n = 30; + + for (int i = 0; i < n; i++) + { + Thread thread = new Thread(new GuidRunner()); + threads.add(thread); + thread.start(); + } + + Set blocked = new HashSet(); + Set terminated = new HashSet(); + + int maxItemsBlocked = 0; + + while (terminated.size() != n) + { + for (Thread current : threads) + { + State state = current.getState(); + String name = current.getName(); + + if (state == State.BLOCKED) + { + if (!blocked.contains(name)) + { + blocked.add(name); + maxItemsBlocked = blocked.size() > maxItemsBlocked ? blocked.size() : maxItemsBlocked; + } + } + else // not BLOCKED, eg. RUNNABLE, TERMINATED, ... + { + blocked.remove(name); + if (state == State.TERMINATED && !terminated.contains(name)) + { + terminated.add(name); + } + } + } + } + + //worst case scenario : max number of threads blocked at a moment = number of threads - 2 ( usually ~5 for 30 threads) + //the implementation without RandomSecure pool reaches constantly (number of threads - 1) max blocked threads + Assert.assertTrue("Exceeded number of blocked threads : " + maxItemsBlocked, maxItemsBlocked < n-2); + } + +} + diff --git a/core/src/test/java/org/alfresco/util/ISO8601DateFormatTest.java b/core/src/test/java/org/alfresco/util/ISO8601DateFormatTest.java new file mode 100644 index 0000000000..0a59373ea3 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/ISO8601DateFormatTest.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2005-2016 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import junit.framework.TestCase; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class ISO8601DateFormatTest extends TestCase +{ + public void testConversion() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "2005-09-16T17:01:03.456+01:00"; + String test2 = "1801-09-16T17:01:03.456+01:00"; + // convert to a date + Date date = ISO8601DateFormat.parse(test); + Date date2 = ISO8601DateFormat.parse(test2); + // get the string form + String strDate = ISO8601DateFormat.format(date); + String strDate2 = ISO8601DateFormat.format(date2); + // convert back to a date from the converted string + Date dateAfter = ISO8601DateFormat.parse(strDate); + Date dateAfter2 = ISO8601DateFormat.parse(strDate2); + // make sure the date objects match, test this instead of the + // string as the string form will be different in different + // locales + assertEquals(date, dateAfter); + assertEquals(date2, dateAfter2); + } + + public void test19000101() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "1900-01-01"; + Date date = ISO8601DateFormat.parse(test); + String strDate = ISO8601DateFormat.format(date); + Date dateAfter = ISO8601DateFormat.parse(strDate); + assertEquals(date, dateAfter); + } + + public void test18991231() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "1899-12-31"; + Date date = ISO8601DateFormat.parse(test); + String strDate = ISO8601DateFormat.format(date); + Date dateAfter = ISO8601DateFormat.parse(strDate); + assertEquals(date, dateAfter); + } + + public void test18800207() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "1880-02-07"; + Date date = ISO8601DateFormat.parse(test); + String strDate = ISO8601DateFormat.format(date); + Date dateAfter = ISO8601DateFormat.parse(strDate); + assertEquals(date, dateAfter); + } + + public void test15000207() // MNT-19261 JULIAN CALENDAR + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "1500-02-07"; + Date date = ISO8601DateFormat.parse(test); + String strDate = ISO8601DateFormat.format(date); // 1500-02-07T00:00:00.000Z + Date dateAfter = ISO8601DateFormat.parse(strDate); + // Before MNT-19261 Expected Wed Jan 29 03:06:28 GMT 1500 Actual Mon Jan 20 03:06:28 GMT 1500 + // After MNT-19261 Expected and Actual: Fri Feb 07 00:00:00 GMT 1500 + assertEquals(date, dateAfter); + } + + public void test10661014() // Battle of Hastings + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "10661014"; + Date date = ISO8601DateFormat.parse(test); + String strDate = ISO8601DateFormat.format(date); + Date dateAfter = ISO8601DateFormat.parse(strDate); + assertEquals(date, dateAfter); + } + + public void testFormat() + { + TimeZone.setDefault(TimeZone.getTimeZone("PST")); // Any timezone other than UTC + + String strDate1 = "2005-09-16T17:01:03.456Z"; + String strDate2 = "1801-09-16T17:01:03.456Z"; + // convert to a date + Date date1 = ISO8601DateFormat.parse(strDate1); + Date date2 = ISO8601DateFormat.parse(strDate2); + // get the string form + String strDate1Formatted = ISO8601DateFormat.format(date1); + String strDate2Formatted = ISO8601DateFormat.format(date2); + + // Format needs to be in the UTC timezone with zero offset + assertEquals(strDate1, strDate1Formatted); + assertEquals(strDate2, strDate2Formatted); + } + + public void testGetCalendarMethod() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + Calendar calendarGMT = ISO8601DateFormat.getCalendar(); + + TimeZone.setDefault(TimeZone.getTimeZone("BST")); + Calendar calendarBST = ISO8601DateFormat.getCalendar(); + + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + Calendar calendarGMT1 = ISO8601DateFormat.getCalendar(); + + assertNotSame(calendarGMT, calendarBST); + assertSame(calendarGMT, calendarGMT1); + } + + public void testDateParser() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "2005-09-16T17:01:03.456+01:00"; + String test2 = "1801-09-16T17:01:03.456+01:00"; + + String isoFormattedDate = "2005-09-16T16:01:03.456Z"; + String isoFormattedDate2 = "1801-09-16T16:01:03.456Z"; + + Date testDate = getDateValue(2005, 9, 16, 17, 1, 3, 456, 60); + Date testDate2 = getDateValue(1801, 9, 16, 17, 1, 3, 456, 60); + + // convert to a date + Date date = ISO8601DateFormat.parse(test); + Date date2 = ISO8601DateFormat.parse(test2); + // check converted to date value + assertEquals(testDate, date); + assertEquals(testDate2, date2); + + // get the string form + String strDate = ISO8601DateFormat.format(date); + String strDate2 = ISO8601DateFormat.format(date2); + // check the date converted to sting + assertEquals(isoFormattedDate, strDate); + assertEquals(isoFormattedDate2, strDate2); + } + + private Date getDateValue(int year, int month, int day, int hours, int minutes, int sec, int msec, int offsetInMinutes) + { + // minute in millis + int millisInMinute = 1000 * 60; + + GregorianCalendar gc = new GregorianCalendar(); + + // set correct offset + String[] tzArray = TimeZone.getAvailableIDs(millisInMinute * offsetInMinutes); + if (tzArray.length > 0) + { + gc.setTimeZone(TimeZone.getTimeZone(tzArray[0])); + } + + // set date + gc.set(GregorianCalendar.YEAR, year); + gc.set(GregorianCalendar.MONTH, month - 1); + gc.set(GregorianCalendar.DAY_OF_MONTH, day); + gc.set(GregorianCalendar.HOUR_OF_DAY, hours); + gc.set(GregorianCalendar.MINUTE, minutes); + gc.set(GregorianCalendar.SECOND, sec); + gc.set(GregorianCalendar.MILLISECOND, msec); + + return gc.getTime(); + } + + public void testMiliseconds() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + // ALF-3803 bug fix, milliseconds are optional + String testA = "2005-09-16T17:01:03.456Z"; + String testB = "2005-09-16T17:01:03Z"; + String testBms = "2005-09-16T17:01:03.000Z"; + String testC = "1801-09-16T17:01:03Z"; + String testCms = "1801-09-16T17:01:03.000Z"; + + Date dateA = ISO8601DateFormat.parse(testA); + Date dateB = ISO8601DateFormat.parse(testB); + Date dateC = ISO8601DateFormat.parse(testC); + + assertEquals(testA, ISO8601DateFormat.format(dateA)); + + assertEquals(testBms, ISO8601DateFormat.format(dateB)); + + assertEquals(testCms, ISO8601DateFormat.format(dateC)); + + // The official ISO 8601.2004 spec doesn't say much helpful about milliseconds + // The W3C version says it's up to different + // implementations to put bounds on them + // We can silently ignore anything beyond 3 digits, see ALF-14687 + String testCms3 = "2005-09-16T17:01:03.123+01:00"; + String testCms4 = "2005-09-16T17:01:03.1234+01:00"; + String testCms5 = "2005-09-16T17:01:03.12345+01:00"; + String testCms6 = "2005-09-16T17:01:03.123456+01:00"; + String testCms7 = "2005-09-16T17:01:03.1234567+01:00"; + + Date testCDate = ISO8601DateFormat.parse(testCms3); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms4)); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms5)); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms6)); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms7)); + } + + public void testTimezones() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + Date date = null; + + TimeZone tz = TimeZone.getTimeZone("Australia/Sydney"); + String testSydney = "2011-02-04T16:13:14"; + String testUTC = "2011-02-04T05:13:14.000Z"; + + //Sydney + date = ISO8601DateFormat.parse(testSydney, tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + + // Check with ms too + date = ISO8601DateFormat.parse(testSydney + ".000", tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + + //Sydney with an offset and timezone + date = ISO8601DateFormat.parse(testSydney+"+11:00", tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + + // Check with ms too + date = ISO8601DateFormat.parse(testSydney + ".000"+"+11:00", tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + } + + public void testToZulu(){ + String base = "2011-02-04T16:13:14.000"; + String zulu = base + "Z"; + String utc0 = base + "+00:00"; + String utc1 = "2011-02-04T17:13:14" + "+01:00"; + String utcMinus1 = "2011-02-04T15:13:14" + "-01:00"; + + assertEquals(zulu, ISO8601DateFormat.formatToZulu(zulu)); + assertEquals(zulu, ISO8601DateFormat.formatToZulu(utc1)); + assertEquals(zulu, ISO8601DateFormat.formatToZulu(utc0)); + assertEquals(zulu, ISO8601DateFormat.formatToZulu(utcMinus1)); + } + + public void testDayOnly() + { + Date date = null; + + // Test simple parsing + TimeZone tz = TimeZone.getTimeZone("Europe/London"); + date = ISO8601DateFormat.parseDayOnly("2012-05-21", tz); + + Calendar cal = Calendar.getInstance(tz); + cal.setTime(date); + + // Check date and time component + assertEquals(2012, cal.get(Calendar.YEAR)); + assertEquals(4, cal.get(Calendar.MONTH)); + assertEquals(21, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, cal.get(Calendar.HOUR)); + assertEquals(0, cal.get(Calendar.MINUTE)); + assertEquals(0, cal.get(Calendar.SECOND)); + assertEquals(0, cal.get(Calendar.MILLISECOND)); + + // Check time is ignored on full ISO8601-string + date = ISO8601DateFormat.parseDayOnly("2012-05-21T12:13:14Z", tz); + cal = Calendar.getInstance(tz); + cal.setTime(date); + + assertEquals(2012, cal.get(Calendar.YEAR)); + assertEquals(4, cal.get(Calendar.MONTH)); + assertEquals(21, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, cal.get(Calendar.HOUR)); + assertEquals(0, cal.get(Calendar.MINUTE)); + assertEquals(0, cal.get(Calendar.SECOND)); + assertEquals(0, cal.get(Calendar.MILLISECOND)); + + // Check year signs + date = ISO8601DateFormat.parseDayOnly("+2012-05-21", tz); + cal = Calendar.getInstance(tz); + cal.setTime(date); + assertEquals(GregorianCalendar.AD, cal.get(Calendar.ERA)); + + date = ISO8601DateFormat.parseDayOnly("-2012-05-21", tz); + cal = Calendar.getInstance(tz); + cal.setTime(date); + assertEquals(GregorianCalendar.BC, cal.get(Calendar.ERA)); + + // Check illegal format + try + { + ISO8601DateFormat.parseDayOnly("2011-02-0", tz); + fail("Exception expected on illegal format"); + } + catch(AlfrescoRuntimeException e) {} + try + { + ISO8601DateFormat.parseDayOnly("201a-02-02", tz); + fail("Exception expected on illegal format"); + } + catch(AlfrescoRuntimeException e) {} + } + + public void testDSTParser() + { + TimeZone tz = TimeZone.getTimeZone("America/Sao_Paulo"); + TimeZone.setDefault(tz); + // MNT-15454: This date is invalid as the 00:00 hour became 01:00 because of daylight saving time. + String test1 = "2014-10-19T"; + String test2 = "2014-10-19T00:01:01.000"; + + String isoFormattedDate = "2014-10-19T03:00:00.000Z"; + + // Sun Oct 19 01:00:00 BRST 2014 + Date testDate = getDateValue(2014, 10, 19, 0, 0, 0, 0, - 3*60); + // convert to a date + Date date = ISO8601DateFormat.parse(test1, tz); + // Check converted to date value + assertEquals(testDate, date); + + // Convert to a date + date = ISO8601DateFormat.parse(test2, tz); + // Check converted to date value + assertEquals(testDate, date); + + // Get the string form + String strDate = ISO8601DateFormat.format(date); + // Check the date converted to sting + assertEquals(isoFormattedDate, strDate); + } +} diff --git a/core/src/test/java/org/alfresco/util/LogAdapterTest.java b/core/src/test/java/org/alfresco/util/LogAdapterTest.java new file mode 100644 index 0000000000..6d707e77af --- /dev/null +++ b/core/src/test/java/org/alfresco/util/LogAdapterTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2005-2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.commons.logging.Log; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for LogAdapter. + * + * @author Alan Davis + */ +public class LogAdapterTest +{ + @Mock + Log log; + + LogAdapter adapter; + + Throwable throwable; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.initMocks(this); + + adapter = new LogAdapter(log) { }; + throwable = new Exception(); + } + + @Test + public void traceTest() + { + adapter.trace(""); + adapter.trace("", throwable); + verify(log).trace("", null); + verify(log).trace("", throwable); + + when(log.isTraceEnabled()).thenReturn(true); + assertTrue("", adapter.isTraceEnabled()); + + when(log.isTraceEnabled()).thenReturn(false); + assertFalse("", adapter.isTraceEnabled()); + } + + @Test + public void debugTest() + { + adapter.debug(""); + adapter.debug("", throwable); + verify(log).debug("", null); + verify(log).debug("", throwable); + + when(log.isDebugEnabled()).thenReturn(true); + assertTrue("", adapter.isDebugEnabled()); + + when(log.isDebugEnabled()).thenReturn(false); + assertFalse("", adapter.isDebugEnabled()); + } + + @Test + public void infoTest() + { + adapter.info(""); + adapter.info("", throwable); + verify(log).info("", null); + verify(log).info("", throwable); + + when(log.isInfoEnabled()).thenReturn(true); + assertTrue("", adapter.isInfoEnabled()); + + when(log.isInfoEnabled()).thenReturn(false); + assertFalse("", adapter.isInfoEnabled()); + } + + @Test + public void warnTest() + { + adapter.warn(""); + adapter.warn("", throwable); + verify(log).warn("", null); + verify(log).warn("", throwable); + + when(log.isWarnEnabled()).thenReturn(true); + assertTrue("", adapter.isWarnEnabled()); + + when(log.isWarnEnabled()).thenReturn(false); + assertFalse("", adapter.isWarnEnabled()); + } + + @Test + public void errorTest() + { + adapter.error(""); + adapter.error("", throwable); + verify(log).error("", null); + verify(log).error("", throwable); + + when(log.isErrorEnabled()).thenReturn(true); + assertTrue("", adapter.isErrorEnabled()); + + when(log.isErrorEnabled()).thenReturn(false); + assertFalse("", adapter.isErrorEnabled()); + } + + @Test + public void fatalTest() + { + adapter.fatal(""); + adapter.fatal("", throwable); + verify(log).fatal("", null); + verify(log).fatal("", throwable); + + when(log.isFatalEnabled()).thenReturn(true); + assertTrue("", adapter.isFatalEnabled()); + + when(log.isFatalEnabled()).thenReturn(false); + assertFalse("", adapter.isFatalEnabled()); + } + + @Test + public void nullTest() + { + adapter = new LogAdapter(null) { }; + + adapter.trace(""); + adapter.trace("", throwable); + adapter.debug(""); + adapter.debug("", throwable); + adapter.info(""); + adapter.info("", throwable); + adapter.warn(""); + adapter.warn("", throwable); + adapter.error(""); + adapter.error("", throwable); + adapter.fatal(""); + adapter.fatal("", throwable); + verify(log, times(0)).trace("", null); + verify(log, times(0)).trace("", throwable); + verify(log, times(0)).debug("", null); + verify(log, times(0)).debug("", throwable); + verify(log, times(0)).info("", null); + verify(log, times(0)).info("", throwable); + verify(log, times(0)).warn("", null); + verify(log, times(0)).warn("", throwable); + verify(log, times(0)).error("", null); + verify(log, times(0)).error("", throwable); + verify(log, times(0)).fatal("", null); + verify(log, times(0)).fatal("", throwable); + + when(log.isTraceEnabled()).thenReturn(true); + assertFalse("", adapter.isTraceEnabled()); + + when(log.isTraceEnabled()).thenReturn(false); + assertFalse("", adapter.isTraceEnabled()); + + when(log.isDebugEnabled()).thenReturn(true); + assertFalse("", adapter.isDebugEnabled()); + + when(log.isDebugEnabled()).thenReturn(false); + assertFalse("", adapter.isDebugEnabled()); + + when(log.isInfoEnabled()).thenReturn(true); + assertFalse("", adapter.isInfoEnabled()); + + when(log.isInfoEnabled()).thenReturn(false); + assertFalse("", adapter.isInfoEnabled()); + + when(log.isWarnEnabled()).thenReturn(true); + assertFalse("", adapter.isWarnEnabled()); + + when(log.isWarnEnabled()).thenReturn(false); + assertFalse("", adapter.isWarnEnabled()); + + when(log.isErrorEnabled()).thenReturn(true); + assertFalse("", adapter.isErrorEnabled()); + + when(log.isErrorEnabled()).thenReturn(false); + assertFalse("", adapter.isErrorEnabled()); + + when(log.isFatalEnabled()).thenReturn(true); + assertFalse("", adapter.isFatalEnabled()); + + when(log.isFatalEnabled()).thenReturn(false); + assertFalse("", adapter.isFatalEnabled()); + } +} diff --git a/core/src/test/java/org/alfresco/util/LogTeeTest.java b/core/src/test/java/org/alfresco/util/LogTeeTest.java new file mode 100644 index 0000000000..660fc1e812 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/LogTeeTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2013 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.commons.logging.Log; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for LogTee. + * + * @author Alan Davis + */ +public class LogTeeTest +{ + @Mock + Log log1; + + @Mock + Log log2; + + LogTee tee; + + Throwable throwable; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.initMocks(this); + + tee = new LogTee(log1, log2) { }; + throwable = new Exception(); + } + + @Test + public void traceTest() + { + tee.trace(""); + tee.trace("", throwable); + verify(log1).trace("", null); + verify(log1).trace("", throwable); + verify(log2).trace("", null); + verify(log2).trace("", throwable); + + when(log1.isTraceEnabled()).thenReturn(true); + assertTrue("", tee.isTraceEnabled()); + + when(log2.isTraceEnabled()).thenReturn(true); + assertTrue("", tee.isTraceEnabled()); + + when(log1.isTraceEnabled()).thenReturn(false); + assertTrue("", tee.isTraceEnabled()); + + when(log2.isTraceEnabled()).thenReturn(false); + assertFalse("", tee.isTraceEnabled()); + + when(log2.isTraceEnabled()).thenReturn(true); + assertTrue("", tee.isTraceEnabled()); + } + + @Test + public void debugTest() + { + tee.debug(""); + tee.debug("", throwable); + verify(log1).debug("", null); + verify(log1).debug("", throwable); + verify(log2).debug("", null); + verify(log2).debug("", throwable); + + + when(log1.isDebugEnabled()).thenReturn(true); + assertTrue("", tee.isDebugEnabled()); + + when(log2.isDebugEnabled()).thenReturn(true); + assertTrue("", tee.isDebugEnabled()); + + when(log1.isDebugEnabled()).thenReturn(false); + assertTrue("", tee.isDebugEnabled()); + + when(log2.isDebugEnabled()).thenReturn(false); + assertFalse("", tee.isDebugEnabled()); + + when(log2.isDebugEnabled()).thenReturn(true); + assertTrue("", tee.isDebugEnabled()); + } + + @Test + public void infoTest() + { + tee.info(""); + tee.info("", throwable); + verify(log1).info("", null); + verify(log1).info("", throwable); + verify(log2).info("", null); + verify(log2).info("", throwable); + + + when(log1.isInfoEnabled()).thenReturn(true); + assertTrue("", tee.isInfoEnabled()); + + when(log2.isInfoEnabled()).thenReturn(true); + assertTrue("", tee.isInfoEnabled()); + + when(log1.isInfoEnabled()).thenReturn(false); + assertTrue("", tee.isInfoEnabled()); + + when(log2.isInfoEnabled()).thenReturn(false); + assertFalse("", tee.isInfoEnabled()); + + when(log2.isInfoEnabled()).thenReturn(true); + assertTrue("", tee.isInfoEnabled()); + } + + @Test + public void warnTest() + { + tee.warn(""); + tee.warn("", throwable); + verify(log1).warn("", null); + verify(log1).warn("", throwable); + verify(log2).warn("", null); + verify(log2).warn("", throwable); + + + when(log1.isWarnEnabled()).thenReturn(true); + assertTrue("", tee.isWarnEnabled()); + + when(log2.isWarnEnabled()).thenReturn(true); + assertTrue("", tee.isWarnEnabled()); + + when(log1.isWarnEnabled()).thenReturn(false); + assertTrue("", tee.isWarnEnabled()); + + when(log2.isWarnEnabled()).thenReturn(false); + assertFalse("", tee.isWarnEnabled()); + + when(log2.isWarnEnabled()).thenReturn(true); + assertTrue("", tee.isWarnEnabled()); + } + + @Test + public void errorTest() + { + tee.error(""); + tee.error("", throwable); + verify(log1).error("", null); + verify(log1).error("", throwable); + verify(log2).error("", null); + verify(log2).error("", throwable); + + + when(log1.isErrorEnabled()).thenReturn(true); + assertTrue("", tee.isErrorEnabled()); + + when(log2.isErrorEnabled()).thenReturn(true); + assertTrue("", tee.isErrorEnabled()); + + when(log1.isErrorEnabled()).thenReturn(false); + assertTrue("", tee.isErrorEnabled()); + + when(log2.isErrorEnabled()).thenReturn(false); + assertFalse("", tee.isErrorEnabled()); + + when(log2.isErrorEnabled()).thenReturn(true); + assertTrue("", tee.isErrorEnabled()); + } + + @Test + public void fatalTest() + { + tee.fatal(""); + tee.fatal("", throwable); + verify(log1).fatal("", null); + verify(log1).fatal("", throwable); + verify(log2).fatal("", null); + verify(log2).fatal("", throwable); + + + when(log1.isFatalEnabled()).thenReturn(true); + assertTrue("", tee.isFatalEnabled()); + + when(log2.isFatalEnabled()).thenReturn(true); + assertTrue("", tee.isFatalEnabled()); + + when(log1.isFatalEnabled()).thenReturn(false); + assertTrue("", tee.isFatalEnabled()); + + when(log2.isFatalEnabled()).thenReturn(false); + assertFalse("", tee.isFatalEnabled()); + + when(log2.isFatalEnabled()).thenReturn(true); + assertTrue("", tee.isFatalEnabled()); + } +} diff --git a/core/src/test/java/org/alfresco/util/PathMapperTest.java b/core/src/test/java/org/alfresco/util/PathMapperTest.java new file mode 100644 index 0000000000..f9455e1edd --- /dev/null +++ b/core/src/test/java/org/alfresco/util/PathMapperTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * @see PathMapper + * + * @author Derek Hulley + * @since 3.2 + */ +public class PathMapperTest extends TestCase +{ + private PathMapper mapper; + + @Override + protected void setUp() throws Exception + { + mapper = new PathMapper(); + mapper.addPathMap("/a/b/c", "/1/2/3"); + mapper.addPathMap("/a/b/c", "/one/two/three"); + mapper.addPathMap("/a/c/c", "/1/3/3"); + mapper.addPathMap("/a/c/c", "/one/three/three"); + mapper.addPathMap("/A/B/C", "/1/2/3"); + mapper.addPathMap("/A/B/C", "/ONE/TWO/THREE"); + mapper.addPathMap("/A/C/C", "/1/3/3"); + mapper.addPathMap("/A/C/C", "/ONE/THREE/THREE"); + } + + public void testConvertValueMap() + { + Map inputMap = new HashMap(5); + inputMap.put("/a/a/a/111", 111); + inputMap.put("/a/b/c/123", 123); + inputMap.put("/a/b/b/122", 122); + inputMap.put("/a/c/c/133", 133); + inputMap.put("/A/A/A/111", 111); + inputMap.put("/A/B/C/123", 123); + inputMap.put("/A/B/B/122", 122); + inputMap.put("/A/C/C/133", 133); + + Map expectedOutputMap = new HashMap(5); + expectedOutputMap.put("/1/2/3/123", 123); + expectedOutputMap.put("/one/two/three/123", 123); + expectedOutputMap.put("/1/3/3/133", 133); + expectedOutputMap.put("/one/three/three/133", 133); + expectedOutputMap.put("/1/2/3/123", 123); + expectedOutputMap.put("/ONE/TWO/THREE/123", 123); + expectedOutputMap.put("/1/3/3/133", 133); + expectedOutputMap.put("/ONE/THREE/THREE/133", 133); + + Map outputMap = mapper.convertMap(inputMap); + + String diff = EqualsHelper.getMapDifferenceReport(outputMap, expectedOutputMap); + if (diff != null) + { + fail(diff); + } + } + + public void testPathMatchesExact() + { + Set mappedPaths = mapper.getMappedPaths("/a/b/c"); + assertEquals("Exact matches expected", 2, mappedPaths.size()); + mappedPaths = mapper.getMappedPaths("/a"); + assertEquals("Exact match NOT expected", 0, mappedPaths.size()); + } + + public void testPathMatchesPartial() + { + Set mappedPaths = mapper.getMappedPathsWithPartialMatch("/a"); + assertEquals("Partial matches expected", 4, mappedPaths.size()); + } +} diff --git a/core/src/test/java/org/alfresco/util/TempFileProviderTest.java b/core/src/test/java/org/alfresco/util/TempFileProviderTest.java new file mode 100644 index 0000000000..cd5b1c8358 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/TempFileProviderTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import java.io.File; + +import junit.framework.TestCase; + +/** + * Unit test for TempFileProvider + * + * @see org.alfresco.util.TempFileProvider + * + * @author Derek Hulley + */ +public class TempFileProviderTest extends TestCase +{ + /** + * test of getTempDir + * + * @throws Exception + */ + public void testTempDir() throws Exception + { + File tempDir = TempFileProvider.getTempDir(); + assertTrue("Not a directory", tempDir.isDirectory()); + File tempDirParent = tempDir.getParentFile(); + + // create a temp file + File tempFile = File.createTempFile("AAAA", ".tmp"); + File tempFileParent = tempFile.getParentFile(); + + // they should be equal + assertEquals("Our temp dir not subdirectory system temp directory", + tempFileParent, tempDirParent); + } + + /** + * test create a temporary file + * + * create another file with the same prefix and suffix. + * @throws Exception + */ + public void testTempFile() throws Exception + { + File tempFile = TempFileProvider.createTempFile("AAAA", ".tmp"); + File tempFileParent = tempFile.getParentFile(); + File tempDir = TempFileProvider.getTempDir(); + assertEquals("Temp file not located in our temp directory", + tempDir, tempFileParent); + + /** + * Create another temp file and then delete it. + */ + File tempFile2 = TempFileProvider.createTempFile("AAAA", ".tmp"); + tempFile2.delete(); + } + + /** + * test create a temporary file with a directory + * + * create another file with the same prefix and suffix. + * @throws Exception + */ + public void testTempFileWithDir() throws Exception + { + File tempDir = TempFileProvider.getTempDir(); + File tempFile = TempFileProvider.createTempFile("AAAA", ".tmp", tempDir); + File tempFileParent = tempFile.getParentFile(); + assertEquals("Temp file not located in our temp directory", + tempDir, tempFileParent); + + /** + * Create another temp file and then delete it. + */ + File tempFile2 = TempFileProvider.createTempFile("AAAA", ".tmp", tempDir); + tempFile2.delete(); + } +} diff --git a/core/src/test/java/org/alfresco/util/VersionNumberTest.java b/core/src/test/java/org/alfresco/util/VersionNumberTest.java new file mode 100644 index 0000000000..3a1bd79b60 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/VersionNumberTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util; + +import junit.framework.TestCase; + +/** + * Test for extension version class. + * + * @author Roy Wetherall + */ +public class VersionNumberTest extends TestCase +{ + public void testCreate() + { + VersionNumber version1 = new VersionNumber("1"); + int[] parts1 = version1.getParts(); + assertNotNull(parts1); + assertEquals(1, parts1.length); + assertEquals(1, parts1[0]); + + VersionNumber version2 = new VersionNumber("1.2"); + int[] parts2 = version2.getParts(); + assertNotNull(parts2); + assertEquals(2, parts2.length); + assertEquals(1, parts2[0]); + assertEquals(2, parts2[1]); + + VersionNumber version3 = new VersionNumber("1.2.3"); + int[] parts3 = version3.getParts(); + assertNotNull(parts3); + assertEquals(3, parts3.length); + assertEquals(1, parts3[0]); + assertEquals(2, parts3[1]); + assertEquals(3, parts3[2]); + + try + { + new VersionNumber("xxx"); + fail("Should not have created an invalid version"); + } catch (Exception exception) + { + // OK + } + try + { + new VersionNumber("1-1-2"); + fail("Should not have created an invalid version"); + } catch (Exception exception) + { + // OK + } + try + { + new VersionNumber("1.2.3a"); + fail("Should not have created an invalid version"); + } catch (Exception exception) + { + // OK + } + } + + public void testEquals() + { + VersionNumber version0 = new VersionNumber("1"); + VersionNumber version1 = new VersionNumber("1.2"); + VersionNumber version2 = new VersionNumber("1.2"); + VersionNumber version3 = new VersionNumber("1.2.3"); + VersionNumber version4 = new VersionNumber("1.2.3"); + VersionNumber version5 = new VersionNumber("1.3.3"); + VersionNumber version6 = new VersionNumber("1.0"); + + assertFalse(version0.equals(version1)); + assertTrue(version1.equals(version2)); + assertFalse(version2.equals(version3)); + assertTrue(version3.equals(version4)); + assertFalse(version4.equals(version5)); + assertTrue(version0.equals(version6)); + } + + public void testCompare() + { + VersionNumber version0 = new VersionNumber("1"); + VersionNumber version1 = new VersionNumber("1.2"); + VersionNumber version2 = new VersionNumber("1.2"); + VersionNumber version3 = new VersionNumber("1.2.3"); + VersionNumber version4 = new VersionNumber("1.11"); + VersionNumber version5 = new VersionNumber("1.3.3"); + VersionNumber version6 = new VersionNumber("2.0"); + VersionNumber version7 = new VersionNumber("2.0.1"); + VersionNumber version8 = new VersionNumber("10.0"); + VersionNumber version9 = new VersionNumber("10.3"); + VersionNumber version10 = new VersionNumber("11.1"); + + assertEquals(-1, version0.compareTo(version1)); + assertEquals(1, version1.compareTo(version0)); + assertEquals(0, version1.compareTo(version2)); + assertEquals(-1, version2.compareTo(version3)); + assertEquals(-1, version2.compareTo(version4)); + assertEquals(-1, version3.compareTo(version5)); + assertEquals(1, version6.compareTo(version5)); + assertEquals(-1, version6.compareTo(version7)); + assertEquals(-1, version1.compareTo(version8)); + assertEquals(-1, version8.compareTo(version9)); + assertEquals(-1, version9.compareTo(version10)); + } +} diff --git a/core/src/test/java/org/alfresco/util/collections/CollectionUtilsTest.java b/core/src/test/java/org/alfresco/util/collections/CollectionUtilsTest.java new file mode 100644 index 0000000000..18ac6af79a --- /dev/null +++ b/core/src/test/java/org/alfresco/util/collections/CollectionUtilsTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.collections; + +import static java.util.Arrays.asList; + +import static org.alfresco.util.collections.CollectionUtils.asSet; +import static org.alfresco.util.collections.CollectionUtils.nullSafeMerge; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for {@link CollectionUtils}. + * + * @author Neil Mc Erlean + */ +public class CollectionUtilsTest +{ + private static Set stooges; + + private static Map primes; + private static Map squares; + private static Map nullMap; + private static Map nerdsBirthdays; + + @Before public void initData() + { + stooges = new HashSet<>(); + stooges.add("Larry"); + stooges.add("Curly"); + stooges.add("Moe"); + + primes = new HashMap<>(); + primes.put("two", 2); + primes.put("three", 3); + primes.put("five", 5); + + squares = new HashMap<>(); + squares.put("one", 1); + squares.put("two", 4); + squares.put("three", 9); + + nerdsBirthdays = new HashMap<>(); + nerdsBirthdays.put("Alan Turing", 1912); + nerdsBirthdays.put("Charles Babbage", 1791); + nerdsBirthdays.put("Matthew Smith", 1966); + nerdsBirthdays.put("Paul Dirac", 1902); + nerdsBirthdays.put("Robert Boyle", 1627); + nerdsBirthdays.put("Robert Hooke", 1635); + nerdsBirthdays.put("J. Robert Oppenheimer", 1904); + } + + @Test public void varArgsAsSet() { + assertEquals(stooges, asSet("Larry", "Curly", "Moe")); + + assertEquals(stooges, CollectionUtils.asSet(String.class, "Larry", "Curly", "Moe")); + } + + @Test public void nullSafeMergeMaps() + { + assertNull(nullSafeMerge(nullMap, nullMap, true)); + + assertEquals(Collections.emptyMap(), nullSafeMerge(nullMap, nullMap)); + assertEquals(primes, nullSafeMerge(nullMap, primes)); + assertEquals(primes, nullSafeMerge(primes, nullMap)); + + Map primesAndSquares = new HashMap<>(); + primesAndSquares.putAll(primes); + primesAndSquares.putAll(squares); + + assertEquals(primesAndSquares, nullSafeMerge(primes, squares)); + } + + @Test public void collectionFiltering() throws Exception + { + Function johnFilter = new KeySubstringFilter("John"); + assertEquals(0, CollectionUtils.filterKeys(nerdsBirthdays, johnFilter).size()); + + Function robertFilter = new KeySubstringFilter("Robert"); + assertEquals(3, CollectionUtils.filterKeys(nerdsBirthdays, robertFilter).size()); + } + + private static final class KeySubstringFilter implements Function + { + private final String substring; + public KeySubstringFilter(String substring) { this.substring = substring; } + @Override public Boolean apply(String value) + { + return value.contains(substring); + } + } + + @Test public void sortMapsByEntry() throws Exception + { + final Map expectedSorting = getNerdsSortedByBirthDate(); + + Comparator> entryComparator = new Comparator>() + { + @Override public int compare(Entry e1, Entry e2) + { + return e1.getValue().intValue() - e2.getValue().intValue(); + } + }; + + final Map actualSorting = CollectionUtils.sortMapByValue(nerdsBirthdays, entryComparator); + + assertEquals(expectedSorting, actualSorting); + } + + @Test public void sortMapsByValue() throws Exception + { + final Map expectedSorting = getNerdsSortedByBirthDate(); + + Comparator valueComparator = new Comparator() + { + @Override public int compare(Integer i1, Integer i2) + { + return i1.intValue() - i2.intValue(); + } + }; + + Comparator> entryComparator = CollectionUtils.toEntryComparator(valueComparator); + + final Map actualSorting = CollectionUtils.sortMapByValue(nerdsBirthdays, entryComparator); + + assertEquals(expectedSorting, actualSorting); + } + + private Map getNerdsSortedByBirthDate() + { + final Map result = new LinkedHashMap<>(); // maintains insertion order + result.put("Robert Boyle", 1627); + result.put("Robert Hooke", 1635); + result.put("Charles Babbage", 1791); + result.put("Paul Dirac", 1902); + result.put("J. Robert Oppenheimer", 1904); + result.put("Alan Turing", 1912); + result.put("Matthew Smith", 1966); + return result; + } + + @Test public void moveItemInList() throws Exception + { + final List input = asList("a", "b", "c"); + + assertEquals(asList("a", "b", "c"), CollectionUtils.moveRight(0, "b", input)); + assertEquals(asList("a", "c", "b"), CollectionUtils.moveRight(1, "b", input)); + assertEquals(asList("a", "c", "b"), CollectionUtils.moveRight(5, "b", input)); + + assertEquals(asList("c", "a", "b"), CollectionUtils.moveRight(-2, "c", input)); + assertEquals(asList("c", "a", "b"), CollectionUtils.moveRight(-5, "c", input)); + + assertEquals(asList("a", "b", "c"), CollectionUtils.moveLeft(0, "b", input)); + assertEquals(asList("b", "a", "c"), CollectionUtils.moveLeft(1, "b", input)); + assertEquals(asList("b", "a", "c"), CollectionUtils.moveLeft(5, "b", input)); + + assertEquals(asList("b", "c", "a"), CollectionUtils.moveLeft(-2, "a", input)); + assertEquals(asList("b", "c", "a"), CollectionUtils.moveLeft(-5, "a", input)); + + try { CollectionUtils.moveRight(1, "x", input); } + catch (NoSuchElementException expected) { return; } + + fail("Expected exception was not thrown."); + } + + @Test public void flattenCollections() throws Exception + { + final List list1 = CollectionUtils.toListOfStrings(stooges); + Collections.sort(list1); + final List list2 = asList("Hello", "World"); + + assertEquals(asList("Curly", "Larry", "Moe", "Hello", "World"), + CollectionUtils.flatten(list1, list2)); + } +} diff --git a/core/src/test/java/org/alfresco/util/exec/ExecParameterTokenizerTest.java b/core/src/test/java/org/alfresco/util/exec/ExecParameterTokenizerTest.java new file mode 100644 index 0000000000..c2d660da46 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/exec/ExecParameterTokenizerTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +/** + * Unit test class for {@link ExecParameterTokenizer}. + * + * @author Neil Mc Erlean + * @since 3.4.2 + */ +public class ExecParameterTokenizerTest +{ + @Test public void tokenizeEmptyString() + { + final String str1 = ""; + final String str2 = " \t "; + + List expectedTokens = Arrays.asList(new String[0]); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test(expected=NullPointerException.class) public void tokenizeNullString() + { + final String str1 = null; + + List expectedTokens = Arrays.asList(new String[0]); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeSimpleParameterString() + { + final String str1 = "-font Helvetica -pointsize 50"; + final String str2 = " -font Helvetica -pointsize 50 "; + + List expectedTokens = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeParameterStringEntirelyQuoted() + { + final String str1 = "\"circle 100,100 150,150\""; + final String str2 = "'circle 100,100 150,150'"; + + List expectedTokens = Arrays.asList(new String[] {"circle 100,100 150,150"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test(expected=IllegalArgumentException.class) + public void tokenizeParameterStringWithUnclosedSingleQuote() + { + final String str = "-font Helvetica -pointsize 50 -draw 'circle"; + + ExecParameterTokenizer t = new ExecParameterTokenizer(str); + t.getAllTokens(); + } + + @Test(expected=IllegalArgumentException.class) + public void tokenizeParameterStringWithUnclosedDoubleQuote() + { + final String str = "-font Helvetica -pointsize 50 -draw \"circle"; + + ExecParameterTokenizer t = new ExecParameterTokenizer(str); + t.getAllTokens(); + } + + @Test(expected=IllegalArgumentException.class) + public void tokenizeParameterStringWithMalformedQuoteNesting() + { + final String str = " \"foo 'bar baz\" hello' "; + + ExecParameterTokenizer t = new ExecParameterTokenizer(str); + t.getAllTokens(); + } + + @Test public void tokenizeParameterStringWithQuotedParam() + { + final String str1 = "-font Helvetica -pointsize 50 -draw \"circle 100,100 150,150\""; + final String str2 = "-font Helvetica -pointsize 50 -draw 'circle 100,100 150,150'"; + + List expectedTokens = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50", + "-draw", "circle 100,100 150,150"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeParameterStringWithQuotedParam_MixedQuotes() + { + final String str1 = "'Hello world' middle \"Goodbye world\""; + final String str2 = "\"Hello world\" middle 'Goodbye world'"; + + List expectedTokens = Arrays.asList(new String[] {"Hello world", "middle", "Goodbye world"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeParameterStringWithQuotedParamContainingQuotes() + { + final String str1 = "-font Helvetica -pointsize 50 -draw \"gravity south fill black text 0,12 'CopyRight'\""; + final String str2 = "-font Helvetica -pointsize 50 -draw 'gravity south fill black text 0,12 \"CopyRight\"'"; + + List expectedTokens1 = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50", + "-draw", "gravity south fill black text 0,12 'CopyRight'"}); + + List expectedTokens2 = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50", + "-draw", "gravity south fill black text 0,12 \"CopyRight\""}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens1, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens2, t.getAllTokens()); + } +} diff --git a/core/src/test/java/org/alfresco/util/exec/RuntimeExecBeansTest.java b/core/src/test/java/org/alfresco/util/exec/RuntimeExecBeansTest.java new file mode 100644 index 0000000000..464d980ba3 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/exec/RuntimeExecBeansTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import java.io.File; +import java.util.Arrays; + +import junit.framework.TestCase; + +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @see org.alfresco.util.exec.RuntimeExecBootstrapBean + * + * @author Derek Hulley + */ +public class RuntimeExecBeansTest extends TestCase +{ + private static Log logger = LogFactory.getLog(RuntimeExecBeansTest.class); + + private static final String APP_CONTEXT_XML = + "classpath:org/alfresco/util/exec/RuntimeExecBeansTest-context.xml"; + private static final String DIR = "dir RuntimeExecBootstrapBeanTest"; + + private File dir; + + public void setUp() throws Exception + { + dir = new File(DIR); + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + } + + public void testBootstrapAndShutdown() throws Exception + { + // now bring up the bootstrap + ApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + + // the folder should be gone + assertFalse("Folder was not deleted by bootstrap", dir.exists()); + + // now create the folder again + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + + // announce that the context is closing + ctx.publishEvent(new ContextClosedEvent(ctx)); + + // the folder should be gone + assertFalse("Folder was not deleted by shutdown", dir.exists()); + } + + public void testSimpleSuccess() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec dirRootExec = (RuntimeExec) ctx.getBean("commandListRootDir"); + assertNotNull(dirRootExec); + // Execute it + dirRootExec.execute(); + } + finally + { + ctx.close(); + } + } + + public void testDeprecatedSetCommandMap() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec deprecatedExec = (RuntimeExec) ctx.getBean("commandCheckDeprecatedSetCommandMap"); + assertNotNull(deprecatedExec); + // Execute it + deprecatedExec.execute(); + } + finally + { + ctx.close(); + } + // The best we can do is look at the log manually + logger.warn("There should be a warning re. the use of deprecated 'setCommandMap'."); + } + + public void testSplitArguments() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec splitExec = (RuntimeExec) ctx.getBean("commandSplitArguments"); + assertNotNull(splitExec); + String[] splitCommand = splitExec.getCommand(); + assertTrue( + "Command arguments not split into 'dir', '.' and '..' :" + Arrays.deepToString(splitCommand), + Arrays.deepEquals(new String[] {"dir", ".", ".."}, splitCommand)); + } + finally + { + ctx.close(); + } + } + + public void testSplitArgumentsAsSingleValue() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec splitExec = (RuntimeExec) ctx.getBean("commandSplitArgumentsAsSingleValue"); + assertNotNull(splitExec); + String[] splitCommand = splitExec.getCommand(); + assertTrue( + "Command arguments not split into 'dir', '.' and '..' : " + Arrays.deepToString(splitCommand), + Arrays.deepEquals(new String[] {"dir", ".", ".."}, splitCommand)); + } + finally + { + ctx.close(); + } + } + + public void testFailureModeOfMissingCommand() + { + File dir = new File(DIR); + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec failureExec = (RuntimeExec) ctx.getBean("commandFailureGuaranteed"); + assertNotNull(failureExec); + // Execute it + ExecutionResult result = failureExec.execute(); + assertEquals("Expected first error code in list", 666, result.getExitValue()); + } + finally + { + ctx.close(); + } + } + +// /** +// * Checks that the encoding setting feeds through to the streams. +// */ +// public void testStreamReading() throws Exception +// { +// String manglingCharsetName = "UTF-16"; +// +// File dir = new File(DIR); +// dir.mkdir(); +// assertTrue("Directory not created", dir.exists()); +// +// ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); +// try +// { +// RuntimeExec dirRootExec = (RuntimeExec) ctx.getBean("commandListRootDir"); +// assertNotNull(dirRootExec); +// // Execute it +// ExecutionResult result = dirRootExec.execute(); +// +// // Get the error stream +// String defaultStdOut = result.getStdOut(); +// +// // Change the encoding +// dirRootExec.setCharset(manglingCharsetName); +// result = dirRootExec.execute(); +// String mangledStdOut = result.getStdOut(); +// // The two error strings must not be the same +// assertNotSame("Differently encoded strings should differ", defaultStdOut, mangledStdOut); +// +// // Now convert the Shift-JIS string and ensure it's the same as originally expected +// Charset defaultCharset = Charset.defaultCharset(); +// byte[] mangledBytes = mangledStdOut.getBytes(manglingCharsetName); +// String convertedStrOut = new String(mangledBytes, defaultCharset.name()); +// // Check, catering for any mangled characters +// assertTrue("Expected to be able to convert value back to default charset.", convertedStrOut.contains(defaultStdOut)); +// } +// finally +// { +// ctx.close(); +// } +// } +// + public void testExecOfNeverEndingProcess() + { + File dir = new File(DIR); + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec failureExec = (RuntimeExec) ctx.getBean("commandNeverEnding"); + assertNotNull(failureExec); + // Execute it + failureExec.execute(); + // The command is never-ending, so this should be out immediately + } + finally + { + ctx.close(); + } + } +} diff --git a/core/src/test/java/org/alfresco/util/exec/RuntimeExecTest.java b/core/src/test/java/org/alfresco/util/exec/RuntimeExecTest.java new file mode 100644 index 0000000000..9144a2fb53 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/exec/RuntimeExecTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.exec; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.util.exec.RuntimeExec + * + * @author Derek Hulley + */ +public class RuntimeExecTest extends TestCase +{ + public void testStreams() throws Exception + { + RuntimeExec exec = new RuntimeExec(); + + // This test will return different results on Windows and Linux! + // note that some Unix variants will error without a path + HashMap commandMap = new HashMap(5); + commandMap.put("*", new String[] {"find", "/", "-maxdepth", "1", "-name", "var"}); + commandMap.put("Windows.*", new String[] {"find", "/?"}); + exec.setCommandsAndArguments(commandMap); + // execute + ExecutionResult ret = exec.execute(); + + String out = ret.getStdOut(); + String err = ret.getStdErr(); + + assertEquals("Didn't expect error code", 0, ret.getExitValue()); + assertEquals("Didn't expect any error output", 0, err.length()); + assertTrue("No output found", out.length() > 0); + } + + public void testWildcard() throws Exception + { + RuntimeExec exec = new RuntimeExec(); + + // set the command + Map commandMap = new HashMap(3, 1.0f); + commandMap.put(".*", (new String[]{"TEST"})); + exec.setCommandsAndArguments(commandMap); + + String[] commandStr = exec.getCommand(); + assertTrue("Expected default match to work", Arrays.deepEquals(new String[] {"TEST"}, commandStr)); + } + + public void testWithProperties() throws Exception + { + RuntimeExec exec = new RuntimeExec(); + + // set the command + Map commandMap = new HashMap(3, 1.0f); + commandMap.put("Windows.*", new String[]{"dir", "${path}"}); + commandMap.put("Linux", new String[] {"ls", "${path}"}); + commandMap.put("Mac OS X", new String[]{"ls", "${path}"}); + commandMap.put("*", new String[]{"wibble", "${path}"}); + exec.setCommandsAndArguments(commandMap); + + // set the default properties + Map defaultProperties = new HashMap(1, 1.0f); + defaultProperties.put("path", "."); + exec.setDefaultProperties(defaultProperties); + + // check that the command lines generated are correct + String defaultCommand[] = exec.getCommand(); + String dynamicCommand[] = exec.getCommand(Collections.singletonMap("path", "./")); + // check + String os = System.getProperty("os.name"); + String[] defaultCommandCheck = null; + String[] dynamicCommandCheck = null; + if (os.matches("Windows.*")) + { + defaultCommandCheck = new String[]{"dir", "."}; + dynamicCommandCheck = new String[]{"dir", "./"}; + } + else if (os.equals("Linux") || os.equals("Mac OS X")) + { + defaultCommandCheck = new String[]{"ls", "."}; + dynamicCommandCheck = new String[]{"ls", "./"}; + } + else + { + defaultCommandCheck = new String[]{"wibble", "."}; + dynamicCommandCheck = new String[]{"wibble", "./"}; + } + assertTrue("Default command for OS " + os + " is incorrect", Arrays.deepEquals(defaultCommandCheck, defaultCommand)); + assertTrue("Dynamic command for OS " + os + " is incorrect", Arrays.deepEquals(dynamicCommandCheck, dynamicCommand)); + } + + public void testNoTimeout() throws Exception + { + long timeout = -1; + int runFor = 10000; + + longishRunningProcess(runFor, timeout); + } + + public void testTimeout() throws Exception + { + long timeout = 5000; + int runFor = 10000; + + longishRunningProcess(runFor, timeout); + } + + private void longishRunningProcess(int runFor, long timeout) + { + long marginOfError = 3000; + boolean shouldComplete = timeout <= 0; + + assertTrue("The timeout when set must be more than "+marginOfError+"ms", shouldComplete || timeout >= marginOfError); + assertTrue("The timeout when set plus "+marginOfError+"ms must less than the runFor value", shouldComplete || timeout+marginOfError <= runFor); + + long minTime = (shouldComplete ? runFor : timeout) - marginOfError; + long maxTime = (shouldComplete ? runFor : timeout) + marginOfError; + + RuntimeExec exec = new RuntimeExec(); + + // This test will return different results on Windows and Linux! + // note that some Unix variants will error without a path + HashMap commandMap = new HashMap(); + commandMap.put("*", new String[] {"sleep", ""+(runFor/1000)}); + commandMap.put("Windows.*", new String[] {"ping", "-n", ""+(runFor/1000+1), "127.0.0.1"}); // don't you just love Microsoft + + // execute + exec.setCommandsAndArguments(commandMap); + long time = System.currentTimeMillis(); + ExecutionResult ret = exec.execute(Collections.emptyMap(), timeout); + time = System.currentTimeMillis()-time; + + String out = ret.getStdOut(); + String err = ret.getStdErr(); + + assertTrue("Command was too fast "+time+"ms", time >= minTime); + assertTrue("Command was too slow "+time+"ms", time <= maxTime); + + if (shouldComplete) + assertEquals("Didn't expect error code", 0, ret.getExitValue()); + else + assertFalse("Didn't expect success code", 0 == ret.getExitValue()); + } +} diff --git a/core/src/test/java/org/alfresco/util/random/NormalDistributionHelperTest.java b/core/src/test/java/org/alfresco/util/random/NormalDistributionHelperTest.java new file mode 100644 index 0000000000..47d4c56a8d --- /dev/null +++ b/core/src/test/java/org/alfresco/util/random/NormalDistributionHelperTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.random; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @see NormalDistributionHelper + * + * @author Derek Hulley + * @since 5.1 + */ +public class NormalDistributionHelperTest +{ + private NormalDistributionHelper normalDistribution = new NormalDistributionHelper(); + + @Test + public void testGetValue_Fail() + { + try + { + normalDistribution.getValue(5L, -5L); + fail("Min-max relation not detected."); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + @Test + public void testGetValue_Precise() + { + assertEquals(10L, normalDistribution.getValue(10L, 10L)); + assertEquals(0L, normalDistribution.getValue(0L, 0L)); + assertEquals(-10L, normalDistribution.getValue(-10L, -10L)); + } + + @Test + public void testGetValue_Repeated() + { + for (int i = 0; i < 1000; i++) + { + long value = normalDistribution.getValue(-1*i, i); + assertTrue("Min not respected", value >= -1*i); + assertTrue("Max not respected", value <= i); + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/alfresco/util/shard/ExplicitShardingPolicyTest.java b/core/src/test/java/org/alfresco/util/shard/ExplicitShardingPolicyTest.java new file mode 100644 index 0000000000..ce188fb229 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/shard/ExplicitShardingPolicyTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.shard; + +import static org.junit.Assert.*; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.junit.Test; + +/** + * @author Andy + */ +public class ExplicitShardingPolicyTest +{ + + @Test + public void tenShards_noReplication_oneNodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 1, 1); + assertTrue(policy.configurationIsValid()); + List shardIds = policy.getShardIdsForNode(1); + assertEquals(10, shardIds.size()); + for (int i = 0; i < 10; i++) + { + assertTrue(shardIds.contains(i)); + } + assertEquals(0, policy.getShardIdsForNode(2).size()); + + for (int i = 0; i < 10; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(1, nodeInstances.size()); + } + } + + @Test + public void tenShards_noReplication_tenNodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 1, 10); + assertTrue(policy.configurationIsValid()); + + for (int i = 0; i < 10; i++) + { + List shardIds = policy.getShardIdsForNode(i + 1); + assertEquals(1, shardIds.size()); + assertTrue(shardIds.contains(i)); + } + assertEquals(0, policy.getShardIdsForNode(11).size()); + + for (int i = 0; i < 10; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(1, nodeInstances.size()); + + } + } + + @Test + public void tenShards_doubled_tenNodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 2, 10); + assertTrue(policy.configurationIsValid()); + + for (int i = 0; i < 10; i++) + { + List shardIds = policy.getShardIdsForNode(i + 1); + assertEquals(2, shardIds.size()); + assertTrue(shardIds.contains(i)); + assertTrue(shardIds.contains((i + 1) % 10)); + } + assertEquals(0, policy.getShardIdsForNode(11).size()); + + for (int i = 0; i < 10; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(2, nodeInstances.size()); + + } + } + + @Test + public void check_24_3() + { + buildAndTest(24, 3, 72); + buildAndTest(24, 3, 36); + buildAndTest(24, 3, 24); + buildAndTest(24, 3, 18); + buildAndTest(24, 3, 12); + buildAndTest(24, 3, 9); + buildAndTest(24, 3, 8); + buildAndTest(24, 3, 6); + buildAndTest(24, 3, 4); + buildAndTest(24, 3, 3); + } + + @Test + + public void failing() + { + buildAndTest(10, 2, 4); + } + + /** ExplicitShardingPolicy algorithm fails for 2 shards, 3 replicas, 3 nodes. (See SEARCH-1785) */ + @Test(expected = AlfrescoRuntimeException.class) + public void search1785_233() + { + buildAndTest(2, 3, 3); + } + + /** ExplicitShardingPolicy algorithm fails for 4 shards, 3 replicas, 6 nodes. (See SEARCH-1785) */ + @Test (expected = AlfrescoRuntimeException.class) + public void search1785_436() + { + buildAndTest(4, 3, 6); + } + + @Test + public void check_10_2() + { + buildAndTest(10, 2, 20); + buildAndTest(10, 2, 10); + buildAndTest(10, 2, 5); + buildAndTest(10, 2, 4); + buildAndTest(10, 2, 2); + } + + @Test + public void check_12_2() + { + buildAndTest(12, 2, 24); + buildAndTest(12, 2, 12); + buildAndTest(12, 2, 8); + buildAndTest(12, 2, 6); + buildAndTest(12, 2, 4); + buildAndTest(12, 2, 3); + buildAndTest(12, 2, 2); + } + + @Test + public void invalidConfiguration_nodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 2, 11); + assertFalse(policy.configurationIsValid()); + + policy = new ExplicitShardingPolicy(10, 0, 10); + assertFalse(policy.configurationIsValid()); + + policy = new ExplicitShardingPolicy(0, 2, 10); + assertFalse(policy.configurationIsValid()); + + policy = new ExplicitShardingPolicy(10, 11, 10); + assertFalse(policy.configurationIsValid()); + } + + private void buildAndTest(int numShards, int replicationFactor, int numNodes) + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(numShards, replicationFactor, numNodes); + assertTrue(policy.configurationIsValid()); + + int[] found = new int[numShards]; + for (int i = 0; i < numNodes; i++) + { + // Convert to a set to remove any duplicates. + Set shardIds = new HashSet<>(policy.getShardIdsForNode(i + 1)); + assertEquals(numShards * replicationFactor / numNodes, shardIds.size()); + for (Integer shardId : shardIds) + { + found[shardId]++; + } + } + check(found, replicationFactor); + assertEquals(0, policy.getShardIdsForNode(numNodes + 1).size()); + + + int[] nodes = new int[numNodes]; + for(int i = 0; i < numShards; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(replicationFactor, nodeInstances.size()); + for (Integer nodeInstance : nodeInstances) + { + nodes[nodeInstance-1]++; + } + } + check(nodes, numShards * replicationFactor / numNodes); + } + + /** + * @param found + * @param i + */ + private void check(int[] found, int count) + { + for (int i = 0; i < found.length; i++) + { + assertEquals(count, found[i]); + } + } +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/DBTests.java b/core/src/test/java/org/alfresco/util/testing/category/DBTests.java new file mode 100644 index 0000000000..3c462fb57e --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/DBTests.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that test specific DB flavor functionality + */ +public interface DBTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/DebugTests.java b/core/src/test/java/org/alfresco/util/testing/category/DebugTests.java new file mode 100644 index 0000000000..886cee4d64 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/DebugTests.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that are not meant to run every build. Only run by developers while working on a + * specific feature/class. They should not be added as part of test suites. + */ +public interface DebugTests extends NonBuildTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/LuceneTests.java b/core/src/test/java/org/alfresco/util/testing/category/LuceneTests.java new file mode 100644 index 0000000000..5b7a9d3702 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/LuceneTests.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that use Lucene search. + * + * All tests marked with this category will eventually have to be transformed to not use lucene (mock the results if + * needed) or they will be move to another level of the testing scale: system/TAS testing; + */ +public interface LuceneTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/NeverRunsTests.java b/core/src/test/java/org/alfresco/util/testing/category/NeverRunsTests.java new file mode 100644 index 0000000000..77162dcce7 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/NeverRunsTests.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that have been discovered not to be part of any test suites, but the cause of why they + * have not been added to a test suite is not clear. Investigation Jira tickets should also be created when marking + * these tests. + */ +public interface NeverRunsTests extends NonBuildTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/NonBuildTests.java b/core/src/test/java/org/alfresco/util/testing/category/NonBuildTests.java new file mode 100644 index 0000000000..7af1d804d1 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/NonBuildTests.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Super category marker for tests that are not meant to run every build. + */ +public interface NonBuildTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/PerformanceTests.java b/core/src/test/java/org/alfresco/util/testing/category/PerformanceTests.java new file mode 100644 index 0000000000..ce16aa8132 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/PerformanceTests.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that can't really be run on the build servers because they are meant to compare performance + * of some component on the same system. It usually requires manually running them locally for multiple times and + * comparing the results. + */ +public interface PerformanceTests extends NonBuildTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/RedundantTests.java b/core/src/test/java/org/alfresco/util/testing/category/RedundantTests.java new file mode 100644 index 0000000000..91ddbf930c --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/RedundantTests.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that are duplicate (test the same functionality as other tests), or tests that test + * deprecated functionality.

+ * + * Use the {@link NeverRunsTests} interface if the reason a test is not run is unknown. Also see {@link DebugTests}, + * {@link PerformanceTests} if the reason is known. + */ +public interface RedundantTests extends NonBuildTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/SanityTests.java b/core/src/test/java/org/alfresco/util/testing/category/SanityTests.java new file mode 100644 index 0000000000..181eb27eb0 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/SanityTests.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that should quickly validate (about 20% of) the functionality very fast. The reviewer + * should really question any tests added to this category. + */ +public interface SanityTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/testing/category/SlowTests.java b/core/src/test/java/org/alfresco/util/testing/category/SlowTests.java new file mode 100644 index 0000000000..2a5a7e6759 --- /dev/null +++ b/core/src/test/java/org/alfresco/util/testing/category/SlowTests.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2017 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.testing.category; + +/** + * Category marker for tests that mark slow tests. + */ +public interface SlowTests +{ +} diff --git a/core/src/test/java/org/alfresco/util/transaction/SpringAwareUserTransactionTest.java b/core/src/test/java/org/alfresco/util/transaction/SpringAwareUserTransactionTest.java new file mode 100644 index 0000000000..2b11281c5d --- /dev/null +++ b/core/src/test/java/org/alfresco/util/transaction/SpringAwareUserTransactionTest.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2005-2014 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.util.transaction; + +import java.util.NoSuchElementException; +import java.util.Objects; + +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +/** + * @see org.alfresco.util.transaction.SpringAwareUserTransaction + * + * @author Derek Hulley + */ +public class SpringAwareUserTransactionTest extends TestCase +{ + private DummyTransactionManager transactionManager; + private FailingTransactionManager failingTransactionManager; + private UserTransaction txn; + + public SpringAwareUserTransactionTest() + { + super(); + } + + @Override + protected void setUp() throws Exception + { + transactionManager = new DummyTransactionManager(); + failingTransactionManager = new FailingTransactionManager(); + txn = getTxn(); + } + + private UserTransaction getTxn() + { + return new SpringAwareUserTransaction( + transactionManager, + false, + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRED, + TransactionDefinition.TIMEOUT_DEFAULT); + } + + public void testSetUp() throws Exception + { + assertNotNull(transactionManager); + assertNotNull(txn); + } + + private void checkNoStatusOnThread() + { + try + { + TransactionAspectSupport.currentTransactionStatus(); + fail("Spring transaction info is present outside of transaction boundaries"); + } + catch (NoTransactionException e) + { + // expected + } + } + + public void testNoTxnStatus() throws Exception + { + checkNoStatusOnThread(); + assertEquals("Transaction status is not correct", + Status.STATUS_NO_TRANSACTION, + txn.getStatus()); + assertEquals("Transaction manager not set up correctly", + txn.getStatus(), + transactionManager.getStatus()); + } + + public void testSimpleTxnWithCommit() throws Throwable + { + testNoTxnStatus(); + try + { + txn.begin(); + assertEquals("Transaction status is not correct", + Status.STATUS_ACTIVE, + txn.getStatus()); + assertEquals("Transaction manager not called correctly", + txn.getStatus(), + transactionManager.getStatus()); + + txn.commit(); + assertEquals("Transaction status is not correct", + Status.STATUS_COMMITTED, + txn.getStatus()); + assertEquals("Transaction manager not called correctly", + txn.getStatus(), + transactionManager.getStatus()); + } + catch (Throwable e) + { + // unexpected exception - attempt a cleanup + try + { + txn.rollback(); + } + catch (Throwable ee) + { + e.printStackTrace(); + } + throw e; + } + checkNoStatusOnThread(); + } + + public void testSimpleTxnWithRollback() throws Exception + { + testNoTxnStatus(); + try + { + txn.begin(); + + throw new Exception("Blah"); + } + catch (Throwable e) + { + txn.rollback(); + } + assertEquals("Transaction status is not correct", + Status.STATUS_ROLLEDBACK, + txn.getStatus()); + assertEquals("Transaction manager not called correctly", + txn.getStatus(), + transactionManager.getStatus()); + checkNoStatusOnThread(); + } + + public void testNoBeginCommit() throws Exception + { + testNoTxnStatus(); + try + { + txn.commit(); + fail("Failed to detected no begin"); + } + catch (IllegalStateException e) + { + // expected + } + checkNoStatusOnThread(); + } + + public void testPostRollbackCommitDetection() throws Exception + { + testNoTxnStatus(); + + txn.begin(); + txn.rollback(); + try + { + txn.commit(); + fail("Failed to detect rolled back txn"); + } + catch (RollbackException e) + { + // expected + } + checkNoStatusOnThread(); + } + + public void testPostSetRollbackOnlyCommitDetection() throws Exception + { + testNoTxnStatus(); + + txn.begin(); + txn.setRollbackOnly(); + try + { + txn.commit(); + fail("Failed to detect set rollback"); + } + catch (RollbackException e) + { + // expected + txn.rollback(); + } + checkNoStatusOnThread(); + } + + public void testMismatchedBeginCommit() throws Exception + { + UserTransaction txn1 = getTxn(); + UserTransaction txn2 = getTxn(); + + testNoTxnStatus(); + + txn1.begin(); + txn2.begin(); + + txn2.commit(); + txn1.commit(); + + checkNoStatusOnThread(); + + txn1 = getTxn(); + txn2 = getTxn(); + + txn1.begin(); + txn2.begin(); + + try + { + txn1.commit(); + fail("Failure to detect mismatched transaction begin/commit"); + } + catch (RuntimeException e) + { + // expected + } + txn2.commit(); + txn1.commit(); + + checkNoStatusOnThread(); + } + + /** + * Test for leaked transactions (no guarantee it will succeed due to reliance + * on garbage collector), so disabled by default. + * + * Also, if it succeeds, transaction call stack tracing will be enabled + * potentially hitting the performance of all subsequent tests. + * + * @throws Exception + */ + public void xtestLeakedTransactionLogging() throws Exception + { + assertFalse(SpringAwareUserTransaction.isCallStackTraced()); + + TrxThread t1 = new TrxThread(); + t1.start(); + System.gc(); + Thread.sleep(1000); + + TrxThread t2 = new TrxThread(); + t2.start(); + System.gc(); + Thread.sleep(1000); + + assertTrue(SpringAwareUserTransaction.isCallStackTraced()); + + TrxThread t3 = new TrxThread(); + t3.start(); + System.gc(); + Thread.sleep(3000); + System.gc(); + Thread.sleep(3000); + } + + private class TrxThread extends Thread + { + public void run() + { + try + { + getTrx(); + } + catch (Exception e) {} + } + + public void getTrx() throws Exception + { + UserTransaction txn = getTxn(); + txn.begin(); + txn = null; + } + } + + public void testConnectionPoolException() throws Exception + { + testNoTxnStatus(); + txn = getFailingTxn(); + try + { + txn.begin(); + fail("ConnectionPoolException should be thrown."); + } + catch (ConnectionPoolException cpe) + { + // Expected fail + } + } + + private UserTransaction getFailingTxn() + { + return new SpringAwareUserTransaction( + failingTransactionManager, + false, + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRED, + TransactionDefinition.TIMEOUT_DEFAULT); + } + + public void testTransactionListenerOrder() throws Throwable + { + testNoTxnStatus(); + try + { + txn.begin(); + StringBuffer buffer = new StringBuffer(); + TransactionSupportUtil.bindListener(new TestTransactionListener("5x", buffer), 5); + TransactionSupportUtil.bindListener(new TestTransactionListener("0a", buffer), 0); + TransactionSupportUtil.bindListener(new TestTransactionListener("0e", buffer), 0); + TransactionSupportUtil.bindListener(new TestTransactionListener("0d", buffer), 0); + TransactionSupportUtil.bindListener(new TestTransactionListener("0b", buffer), 0); + TransactionSupportUtil.bindListener(new TestTransactionListener("0c", buffer), 0); + TransactionSupportUtil.bindListener(new TestTransactionListener("4x", buffer), 4); + TransactionSupportUtil.bindListener(new TestTransactionListener("1x", buffer), -1); + TransactionSupportUtil.bindListener(new TestTransactionListener("3a", buffer), 3); + TransactionSupportUtil.bindListener(new TestTransactionListener("3e", buffer), 3); + TransactionSupportUtil.bindListener(new TestTransactionListener("3d", buffer), 3); + TransactionSupportUtil.bindListener(new TestTransactionListener("3b", buffer), 3); + TransactionSupportUtil.bindListener(new TestTransactionListener("3c", buffer), 3); + TransactionSupportUtil.bindListener(new TestTransactionListener("2x", buffer), -2); + txn.commit(); + assertEquals("0a0e0d0b0c2x1x3a3e3d3b3c4x5x", buffer.toString()); + } + catch (Exception e) + { + try + { + txn.rollback(); + } + catch (Exception ee) + { + e.addSuppressed(ee); + } + throw e; + } + checkNoStatusOnThread(); + } + + private static class TestTransactionListener extends TransactionListenerAdapter + { + private final String name; + private final StringBuffer buffer; + + public TestTransactionListener(String name, StringBuffer buffer) + { + Objects.requireNonNull(name); + Objects.requireNonNull(buffer); + this.name = name; + this.buffer = buffer; + } + + @Override + public void beforeCommit(boolean readOnly) + { + buffer.append(name); + } + + public String getName() + { + return name; + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof TestTransactionListener) + { + return name.equals(((TestTransactionListener) obj).getName()); + } + return false; + } + + @Override + public int hashCode() + { + return name.hashCode(); + } + } + + /** + * Used to check that the transaction manager is being called correctly + * + * @author Derek Hulley + */ + @SuppressWarnings("serial") + private static class DummyTransactionManager extends AbstractPlatformTransactionManager + { + private int status = Status.STATUS_NO_TRANSACTION; + private Object txn = new Object(); + + /** + * @return Returns one of the {@link Status Status.STATUS_XXX} constants + */ + public int getStatus() + { + return status; + } + + protected void doBegin(Object arg0, TransactionDefinition arg1) + { + status = Status.STATUS_ACTIVE; + } + + protected void doCommit(DefaultTransactionStatus arg0) + { + status = Status.STATUS_COMMITTED; + } + + protected Object doGetTransaction() + { + return txn; + } + + protected void doRollback(DefaultTransactionStatus arg0) + { + status = Status.STATUS_ROLLEDBACK; + } + } + + /** + * Throws {@link NoSuchElementException} on begin() + * + * @author alex.mukha + */ + private static class FailingTransactionManager extends AbstractPlatformTransactionManager + { + private static final long serialVersionUID = 1L; + private int status = Status.STATUS_NO_TRANSACTION; + private Object txn = new Object(); + + /** + * @return Returns one of the {@link Status Status.STATUS_XXX} constants + */ + @SuppressWarnings("unused") + public int getStatus() + { + return status; + } + + protected void doBegin(Object arg0, TransactionDefinition arg1) + { + throw new CannotCreateTransactionException("Test exception."); + } + + protected void doCommit(DefaultTransactionStatus arg0) + { + status = Status.STATUS_COMMITTED; + } + + protected Object doGetTransaction() + { + return txn; + } + + protected void doRollback(DefaultTransactionStatus arg0) + { + status = Status.STATUS_ROLLEDBACK; + } + } +} diff --git a/core/src/test/resources/config-areas.xml b/core/src/test/resources/config-areas.xml new file mode 100644 index 0000000000..594902261c --- /dev/null +++ b/core/src/test/resources/config-areas.xml @@ -0,0 +1,13 @@ + + + + + value + + + + + A value + + + \ No newline at end of file diff --git a/core/src/test/resources/config-multi.xml b/core/src/test/resources/config-multi.xml new file mode 100644 index 0000000000..36b3116dde --- /dev/null +++ b/core/src/test/resources/config-multi.xml @@ -0,0 +1,30 @@ + + + + + + + + + + Another global value + true + + child two value + + + + + Another value + + + + the overridden first value + second value + + child two value + child three value + + + + \ No newline at end of file diff --git a/core/src/test/resources/config-props.properties b/core/src/test/resources/config-props.properties new file mode 100644 index 0000000000..fd5e95097e --- /dev/null +++ b/core/src/test/resources/config-props.properties @@ -0,0 +1,5 @@ +globalValue=globalValue +childOneValue=childOneValue +theValue=theValue +theAttr=attrValue +true=true diff --git a/core/src/test/resources/config-props.xml b/core/src/test/resources/config-props.xml new file mode 100644 index 0000000000..d975b5ce0d --- /dev/null +++ b/core/src/test/resources/config-props.xml @@ -0,0 +1,19 @@ + + + + The global value + ${globalValue} + ${missingGlobalValue} + false + + + + The value + ${theValue} + ${missingTheValue} + true + ${true} + ${missingTrue} + + + \ No newline at end of file diff --git a/core/src/test/resources/config-replace.xml b/core/src/test/resources/config-replace.xml new file mode 100644 index 0000000000..c95276c335 --- /dev/null +++ b/core/src/test/resources/config-replace.xml @@ -0,0 +1,19 @@ + + + + The replaced global value + + child custom value + + + + + the replaced first value + new fourth value + + child two value + child three value + + + + \ No newline at end of file diff --git a/core/src/test/resources/config.xml b/core/src/test/resources/config.xml new file mode 100644 index 0000000000..116b828479 --- /dev/null +++ b/core/src/test/resources/config.xml @@ -0,0 +1,57 @@ + + + + The global value + false + + child one value + + + child one value + child two value + child three value + + + + + The value + true + + + + first value + second value + third value + + child one value + + + + + first value + second value + third value + + child one value + + + + + + child one value + child two value + + + grand child one value + grand child two value + + + child four value + + + + + A value + + + \ No newline at end of file diff --git a/core/src/test/resources/org/alfresco/i18n/testMessages.properties b/core/src/test/resources/org/alfresco/i18n/testMessages.properties new file mode 100644 index 0000000000..6d54f72162 --- /dev/null +++ b/core/src/test/resources/org/alfresco/i18n/testMessages.properties @@ -0,0 +1,4 @@ +msg_yes=Yes +msg_no=No +msg_params=What no {0}? +msg_error=This is an error message. \n This is on a new line. \ No newline at end of file diff --git a/core/src/test/resources/org/alfresco/i18n/testMessages_fr_FR.properties b/core/src/test/resources/org/alfresco/i18n/testMessages_fr_FR.properties new file mode 100644 index 0000000000..7db02235fb --- /dev/null +++ b/core/src/test/resources/org/alfresco/i18n/testMessages_fr_FR.properties @@ -0,0 +1,4 @@ +msg_yes=Oui +msg_no=Non +msg_params=Que non {0}? +msg_error=C'est un message d'erreur. \n C'est sur une nouvelle ligne. \ No newline at end of file diff --git a/core/src/test/resources/org/alfresco/util/exec/RuntimeExecBeansTest-context.xml b/core/src/test/resources/org/alfresco/util/exec/RuntimeExecBeansTest-context.xml new file mode 100644 index 0000000000..e68189f6b1 --- /dev/null +++ b/core/src/test/resources/org/alfresco/util/exec/RuntimeExecBeansTest-context.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + dir + + + + + ls + + + + + + + + value1 + + + null + + + ${env.prop3.unsubstituted} + + + + + . + + + true + + + 1, 2 + + + + + + + + + dir + SPLIT:${paths} + + + + + + + . .. + + + + 1, 2 + + + + + + + + + SPLIT: dir . .. + + + + + + 1, 2 + + + + + + + + + dir c: + + + ls / + + + + + true + + + 1 + + + + + + + + + cmd + /C + rmdir + ${dir} + + + + + rm + -rf + ${dir} + + + + + rm + -rf + ${dir} + + + + + wibble + + + + + + + dir RuntimeExecBootstrapBeanTest + + + + 1, 2 + + + + + + + + + + wibble + + + + + + 666 + + + + + + + + + + + + + + + + + + + + + + + + + + cmd + + + + + ls + + + + + + false + + + 1 + + + + + + + + + + dir + ${dir} + + + + + ls + ${dir} + + + + + + true + + + 1 + + + + diff --git a/core/src/test/resources/resource-loader/java.util.AbstractCollection/file.txt b/core/src/test/resources/resource-loader/java.util.AbstractCollection/file.txt new file mode 100644 index 0000000000..284f2bfe50 --- /dev/null +++ b/core/src/test/resources/resource-loader/java.util.AbstractCollection/file.txt @@ -0,0 +1 @@ +java.util.AbstractCollection \ No newline at end of file diff --git a/core/src/test/resources/resource-loader/java.util.AbstractList/file.txt b/core/src/test/resources/resource-loader/java.util.AbstractList/file.txt new file mode 100644 index 0000000000..f6ba039304 --- /dev/null +++ b/core/src/test/resources/resource-loader/java.util.AbstractList/file.txt @@ -0,0 +1 @@ +java.util.AbstractList \ No newline at end of file diff --git a/core/src/test/resources/resource-loader/java.util.TreeSet/file.txt b/core/src/test/resources/resource-loader/java.util.TreeSet/file.txt new file mode 100644 index 0000000000..8817feae36 --- /dev/null +++ b/core/src/test/resources/resource-loader/java.util.TreeSet/file.txt @@ -0,0 +1 @@ +java.util.TreeSet \ No newline at end of file diff --git a/core/src/test/resources/test-config-forms-basic-override.xml b/core/src/test/resources/test-config-forms-basic-override.xml new file mode 100644 index 0000000000..604074e48f --- /dev/null +++ b/core/src/test/resources/test-config-forms-basic-override.xml @@ -0,0 +1,99 @@ + + + + + + + 999 + + + Goodbye + + This is new + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + 500px + bar + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + +
+
+
+ + + +
+ + + +
+
+
+ + + \ No newline at end of file diff --git a/core/src/test/resources/test-config-forms-basic.xml b/core/src/test/resources/test-config-forms-basic.xml new file mode 100644 index 0000000000..1127991ca9 --- /dev/null +++ b/core/src/test/resources/test-config-forms-basic.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + 50 + + + Hello + + + + + + 1 + Hello + For ever and ever. + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + 500px + + + + + + + + 10 + 500px + + + + + + + +
+ +
+
+
+
+ + + +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + + +
+
+
+ + + +
+ + + +
+
+
+ + + +
+ + + +
+
+
+ + + +
+ + + + + + + + +
+
+
+ + + +
+ + + + + + + + + +
+
+ + +
diff --git a/core/src/test/resources/test-config-forms-negative.xml b/core/src/test/resources/test-config-forms-negative.xml new file mode 100644 index 0000000000..35abe68e9f --- /dev/null +++ b/core/src/test/resources/test-config-forms-negative.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/core/src/test/resources/test-config-forms.xml b/core/src/test/resources/test-config-forms.xml new file mode 100644 index 0000000000..6010638f2f --- /dev/null +++ b/core/src/test/resources/test-config-forms.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + 50 + + + + + 1 + Hello + Greetings + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + bar + + + + + + + + + + + + un + deux + + + quatre + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+ + + + + + + + + + + + + + + + +
+
+
+
\ No newline at end of file