diff --git a/remote-api/.gitbugtraq b/remote-api/.gitbugtraq new file mode 100644 index 00000000000..bacffb702d6 --- /dev/null +++ b/remote-api/.gitbugtraq @@ -0,0 +1,4 @@ +# For SmartGit +[bugtraq "jira"] + url = https://issues.alfresco.com/jira/browse/%BUGID% + logRegex = ([A-Z]+-\\d+) diff --git a/remote-api/.gitignore b/remote-api/.gitignore new file mode 100644 index 00000000000..756a7325d22 --- /dev/null +++ b/remote-api/.gitignore @@ -0,0 +1,39 @@ +*.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* + +# Development +alf_data/ +/src/main/resources/alfresco-global.properties +/src/main/resources/alfresco/extension/custom-log4j.properties \ No newline at end of file diff --git a/remote-api/.travis.settings.xml b/remote-api/.travis.settings.xml new file mode 100644 index 00000000000..e8f11969355 --- /dev/null +++ b/remote-api/.travis.settings.xml @@ -0,0 +1,10 @@ + + + + + alfresco-public + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + diff --git a/remote-api/.travis.yml b/remote-api/.travis.yml new file mode 100644 index 00000000000..f2a014b7cd4 --- /dev/null +++ b/remote-api/.travis.yml @@ -0,0 +1,65 @@ +dist: xenial +sudo: required +language: java +jdk: + - openjdk11 +services: + - docker + +cache: + directories: + - $HOME/.m2 +# the cache can grow constantly +before_cache: + - rm -rf $HOME/.m2/repository/org/alfresco/alfresco-remote-api + - rm -rf $HOME/.m2/repository/org/alfresco/alfresco-repository + +branches: + only: + - master + - /support\/.*/ + - /feature\/.*/ + +stages: + - test + - release + +before_install: + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:11.7 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.15.8 + +install: travis_retry mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V + +jobs: + include: + - stage: test + name: "WhiteSource scan" + # only on support 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: "AppContext01TestSuite" + script: travis_wait 20 mvn test -B -Dtest=AppContext01TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco + - name: "AppContext02TestSuite" + script: travis_wait 20 mvn test -B -Dtest=AppContext02TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco + - name: "AppContext03TestSuite" + script: travis_wait 20 mvn test -B -Dtest=AppContext03TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco + - name: "AppContext04TestSuite" + script: travis_wait 20 mvn test -B -Dtest=AppContext04TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco + - name: "AppContextExtraTestSuite" + script: travis_wait 20 mvn test -B -Dtest=AppContextExtraTestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco + - 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 -DscmCommentPrefix="[maven-release-plugin][skip ci] " -Dusername="${GIT_USERNAME}" -Dpassword="${GIT_PASSWORD}" -DskipTests -Darguments=-DskipTests release:clean release:prepare release:perform diff --git a/remote-api/.whitesource b/remote-api/.whitesource new file mode 100644 index 00000000000..f0569521415 --- /dev/null +++ b/remote-api/.whitesource @@ -0,0 +1,8 @@ +{ + "generalSettings": { + "shouldScanRepo": true + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure" + } +} \ No newline at end of file diff --git a/remote-api/.wss-unified-agent.config b/remote-api/.wss-unified-agent.config new file mode 100644 index 00000000000..4dbb9c72bd2 --- /dev/null +++ b/remote-api/.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-remote-api +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/remote-api/CONTRIBUTING.md b/remote-api/CONTRIBUTING.md new file mode 100644 index 00000000000..8d059e27ca0 --- /dev/null +++ b/remote-api/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/remote-api/LICENSE b/remote-api/LICENSE new file mode 100644 index 00000000000..65c5ca88a67 --- /dev/null +++ b/remote-api/LICENSE @@ -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/remote-api/README.md b/remote-api/README.md new file mode 100644 index 00000000000..8065f12c9de --- /dev/null +++ b/remote-api/README.md @@ -0,0 +1,41 @@ +### Alfresco Remote API +[![Build Status](https://travis-ci.com/Alfresco/alfresco-remote-api.svg?branch=master)](https://travis-ci.com/Alfresco/alfresco-remote-api) + +Remote API 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: +* REST API framework +* WebScript implementations including [V1 REST APIs](https://community.alfresco.com/community/ecm/blog/2017/05/02/v1-rest-api-10-things-you-should-know) +* [OpenCMIS](https://chemistry.apache.org/java/opencmis.html) implementations + +### Building and testing +The project can be built by running Maven command: +~~~ +mvn clean install +~~~ +The tests are combined in test classes split by test type or Spring application context used in the test, see classes in _src/test/java/org/alfresco_. All of these classes as well as individual tests can be run by specifying the test class name and a set of DB connection properties, for example: +~~~ +mvn clean test -Dtest=SomeTest -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql:alfresco -Ddb.username=alfresco -Ddb.password=alfresco +~~~ + +### 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-remote-api + version + +~~~ +and Alfresco Maven repository: +~~~ + + alfresco-maven-repo + https://artifacts.alfresco.com/nexus/content/groups/public + +~~~ +The SNAPSHOT version of the artifact is **never** published. + +### Contributing guide +Please use [this guide](CONTRIBUTING.md) to make a contribution to the project. diff --git a/remote-api/l10n.properties b/remote-api/l10n.properties new file mode 100644 index 00000000000..5ee676302f9 --- /dev/null +++ b/remote-api/l10n.properties @@ -0,0 +1,8 @@ +# Branch specific configuration file for localisation scripts + + +MESSAGE_SEARCH_PATH="src/main/resources/alfresco/messages/admin-console*.properties src/main/resources/alfresco/messages/custommodel-restapi-messages*.properties src/main/resources/alfresco/messages/rest-framework-messages*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/admin-communitysummary.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-repoconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-tenantconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-workflowconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/support-tools/admin-nodebrowser.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/audit/entry*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links-delete.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links.put*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.put*.properties" + + +EXCLUDED_FILES="src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/audit/control.properties" + diff --git a/remote-api/pom.xml b/remote-api/pom.xml new file mode 100644 index 00000000000..4a02dbaae00 --- /dev/null +++ b/remote-api/pom.xml @@ -0,0 +1,618 @@ + + 4.0.0 + alfresco-remote-api + Alfresco Remote API + 8.213-SNAPSHOT + jar + + + org.alfresco + alfresco-super-pom + 12 + + + + scm:git:https://github.com/Alfresco/alfresco-remote-api.git + scm:git:https://github.com/Alfresco/alfresco-remote-api.git + https://github.com/Alfresco/alfresco-remote-api + HEAD + + + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/repositories/releases + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/repositories/snapshots + + + + + community + + 11 + + ${project.build.directory}/alf_data + convert + + 8.243 + 8.135 + 8.37 + + 1.1 + 2.11.1 + 2.10.1 + + 8.5 + 1.0.0 + 5.2.7.RELEASE + 42.2.14 + 3.3.7 + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + ${dependency.cxf.version} + + + org.apache.cxf + cxf-rt-frontend-jaxrs + ${dependency.cxf.version} + + + org.apache.cxf + cxf-rt-rs-client + ${dependency.cxf.version} + + + org.apache.cxf + cxf-rt-transports-http + ${dependency.cxf.version} + + + org.apache.cxf + cxf-rt-ws-policy + ${dependency.cxf.version} + + + + com.github.junrar + junrar + 4.0.0 + + + + org.jsoup + jsoup + 1.13.1 + + + + commons-httpclient + commons-httpclient + 3.1-HTTPCLIENT-1265 + + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${dependency.jackson.version} + + + commons-logging + commons-logging + 1.2 + + + org.apache.httpcomponents + httpclient + 4.5.12 + + + org.apache.httpcomponents + httpcore + 4.4.13 + + + + org.springframework + spring-jms + ${dependency.spring.version} + + + + + + + org.alfresco + alfresco-repository + ${dependency.alfresco-repository.version} + + + org.alfresco + alfresco-data-model + ${dependency.alfresco-data-model.version} + + + + com.sun.activation + javax.activation + + + + org.codehaus.woodstox + woodstox-core-asl + + + + javax.annotation + javax.annotation-api + + + + org.apache.geronimo.specs + geronimo-ws-metadata_2.0_spec + + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + + + + + com.fasterxml.jackson.core + jackson-core + ${dependency.jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${dependency.jackson-databind.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${dependency.jackson.version} + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + org.apache.commons + commons-csv + 1.8 + + + org.apache.santuario + xmlsec + 1.5.8 + + + + org.alfresco.surf + spring-webscripts + ${dependency.webscripts.version} + + + javax.xml + jaxrpc-api + 1.1 + + + + + org.apache.chemistry.opencmis + chemistry-opencmis-test-tck + ${dependency.opencmis.version} + + + + com.sun.activation + javax.activation + + + + org.apache.geronimo.specs + geronimo-ws-metadata_2.0_spec + + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + + + + + + + + + + junit + junit + 4.13 + test + + + org.mockito + mockito-core + 3.4.2 + test + + + org.springframework + spring-test + ${dependency.spring.version} + test + + + org.alfresco + alfresco-repository + ${dependency.alfresco-repository.version} + tests + test + + + org.alfresco + alfresco-core + ${dependency.alfresco-core.version} + tests + test + + + + javax.transaction + jta + + + + javax.xml.bind + jaxb-api + + + + + org.alfresco.surf + spring-webscripts + ${dependency.webscripts.version} + tests + test + + + org.postgresql + postgresql + ${dependency.postgresql.version} + test + + + org.eclipse.jetty + jetty-server + 8.2.0.v20160908 + test + + + org.eclipse.jetty.orbit + javax.servlet + + + + + org.eclipse.jetty + jetty-security + 8.2.0.v20160908 + test + + + org.eclipse.jetty + jetty-webapp + 8.2.0.v20160908 + test + + + org.alfresco.cmis.client + alfresco-opencmis-extension + 2.0 + test + + + + org.apache.geronimo.specs + geronimo-ws-metadata_2.0_spec + + + + javax.annotation + javax.annotation-api + + + + + + + + win-alfresco-pdf-renderer-test + + + windows + + + + + org.alfresco + alfresco-pdf-renderer + ${dependency.alfresco-pdf-renderer.version} + win64 + tgz + + + + + + false + maven-antrun-plugin + + + extract-alfresco-pdf-renderer-test + generate-test-resources + + run + + + ${skipTests} + + + + + + + + + + + + + ${project.build.directory}/test-binaries/alfresco-pdf-renderer/alfresco-pdf-renderer + + + + linux-alfresco-pdf-renderer-test + + + linux + + + + + org.alfresco + alfresco-pdf-renderer + ${dependency.alfresco-pdf-renderer.version} + linux + tgz + + + + + + false + maven-antrun-plugin + + + extract-alfresco-pdf-renderer-test + generate-test-resources + + run + + + ${skipTests} + + + + + + + + + + + + + + + + ${project.build.directory}/test-binaries/alfresco-pdf-renderer/alfresco-pdf-renderer + + + + osx-alfresco-pdf-renderer-test + + + mac + + + + + org.alfresco + alfresco-pdf-renderer + ${dependency.alfresco-pdf-renderer.version} + osx + tgz + + + + + + false + maven-antrun-plugin + + + extract-alfresco-pdf-renderer-test + generate-test-resources + + run + + + ${skipTests} + + + + + + + + + + + + + + + + ${project.build.directory}/test-binaries/alfresco-pdf-renderer/alfresco-pdf-renderer + + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + + + + + maven-jar-plugin + + + create-test-jar + + test-jar + + + + + + + maven-surefire-plugin + + + default-test + + + ${project.build.directory} + + ${alfresco-pdf-renderer.exe} + + + ${db.url} + ${db.driver} + ${db.name} + ${db.username} + ${db.password} + ${dir.root} + ${img.exe} + + 30 + 2000 + + true + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + + add-source + + + + src/main/generated + + + + + + + + + maven-antrun-plugin + + + duplicate-english-messages + generate-resources + + run + + + + + + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + + false + Alfresco Software Limited + true + true + classpath://alfresco + ${licenseName} + + src + + + **/*.java + **/*.jsp + + + + + check-licenses + compile + + check-file-header + + + + + + org.alfresco + alfresco-license-headers + 1.0 + + + + + + + diff --git a/remote-api/src/main/antlr3/org/alfresco/rest/antlr/WhereClause.g b/remote-api/src/main/antlr3/org/alfresco/rest/antlr/WhereClause.g new file mode 100644 index 00000000000..6e2f8bf4722 --- /dev/null +++ b/remote-api/src/main/antlr3/org/alfresco/rest/antlr/WhereClause.g @@ -0,0 +1,154 @@ +grammar WhereClause; + +options +{ + // antlr will generate java lexer and parser + language = Java; + // generated parser should create abstract syntax tree + output = AST; +} + +//package, we have to add package declaration on top of it +@lexer::header { +package org.alfresco.rest.antlr; +import java.util.Map; +import java.util.HashMap; +import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException; +import org.alfresco.rest.framework.resource.parameters.where.WhereCompiler; +} + +@lexer::members { + + @Override + public void recover(RecognitionException e) + { + throw new InvalidQueryException(WhereCompiler.resolveMessage(e)); + } +} +//package, we have to add package declaration on top of it +@parser::header { +package org.alfresco.rest.antlr; +import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException; +} + +@parser::members { + + // These methods are here to force the parser to error instead of suppressing problems. +// @Override +// public void reportError(RecognitionException e) { +// System.out.println("CUSTOM ERROR...\n" + e); +// throw new InvalidQueryException(e.getMessage()); +// } + + @Override + protected Object recoverFromMismatchedToken(IntStream input, int ttype, BitSet follow) throws RecognitionException + { + throw new MismatchedTokenException(ttype, input); + } + + @Override + public Object recoverFromMismatchedSet(IntStream input, RecognitionException e, BitSet follow) throws RecognitionException + { + throw e; + } +// +// @Override +// public String getErrorMessage(RecognitionException e, String[] tokenNames) +// { +// System.out.println("THROW ME...\n" + e); +// throw new InvalidQueryException(e.getMessage()); +// } +// End of methods here to force the parser to error instead of supressing problems. +} + +@rulecatch +{ +catch(RecognitionException e) +{ + throw e; +} +} +// ***************** lexer rules: +NEGATION: ('not'|'NOT')WS; +EXISTS: 'exists'|'EXISTS'; +IN: WS('in'|'IN'); +MATCHES: WS('matches'|'MATCHES'); +BETWEEN: WS('between'|'BETWEEN'); +OR: WS('or'|'OR')WS; +AND: WS('and'|'AND')WS; +EQUALS: WS?'='WS?; +LESSTHAN: WS?'<'WS?; +GREATERTHAN: WS?'>'WS?; +LESSTHANOREQUALS: WS?'<='WS?; +GREATERTHANOREQUALS: WS?'>='WS?; +LEFTPAREN: '('; +RIGHTPAREN: ')'; +COMMA: ','; +COLON: ':'; +SINGLEQUOTE: '\''; +PROPERTYVALUE: (SINGLEQUOTE (~SINGLEQUOTE|'\\'SINGLEQUOTE)* SINGLEQUOTE) |IDENTIFIERDIGIT+; +PROPERTYNAME: '/'? IDENTIFIER ('/'IDENTIFIER)*; +fragment IDENTIFIERLETTERORDIGIT: (IDENTIFIERLETTER | IDENTIFIERDIGIT); +fragment IDENTIFIER : (IDENTIFIERLETTER (IDENTIFIERLETTERORDIGIT* | (IDENTIFIERLETTERORDIGIT* COLON IDENTIFIERLETTERORDIGIT*))); +WS : ( ' ' | '\t' | '\r' | '\n' )+ { $channel = HIDDEN; }; +fragment IDENTIFIERLETTER // any Unicode character that is a Java letter (see below) + : '\u0041'..'\u005a' // A-Z + | '\u005f' // _ + | '\u0061'..'\u007a' // a-z + | '\u00c0'..'\u00d6' // À-Ö + | '\u00d8'..'\u00f6' // Ø-ö + | '\u00f8'..'\u00ff' // ø-ÿ + | '\u0100'..'\u1fff' + | '\u3040'..'\u318f' + | '\u3300'..'\u337f' + | '\u3400'..'\u3d2d' + | '\u4e00'..'\u9fff' + | '\uf900'..'\ufaff' + ; +fragment IDENTIFIERDIGIT + : '\u0030'..'\u0039' // 0-9 + | '\u0660'..'\u0669' // Arabic 0-9 + | '\u06f0'..'\u06f9' // Arabic-Indic 0-9 + | '\u0966'..'\u096f' // Devanagari 0-9 + | '\u09e6'..'\u09ef' // Bengali 0-9 + | '\u0a66'..'\u0a6f' // Gurmukhi 0-9 + | '\u0ae6'..'\u0aef' // Gujarati 0-9 + | '\u0b66'..'\u0b6f' // Oriya 0-9 + | '\u0be7'..'\u0bef' // Tamil 0-9 + | '\u0c66'..'\u0c6f' // Telugu 0-9 + | '\u0ce6'..'\u0cef' // Kannada 0-9 + | '\u0d66'..'\u0d6f' // Malayalam 0-9 + | '\u0e50'..'\u0e59' // Thai 0-9 + | '\u0ed0'..'\u0ed9' // Lao 0-9 + | '\u1040'..'\u1049' // Myanmar 0-9 + ; + +// ***************** parser rules: +whereclause : WS? LEFTPAREN! WS? predicate RIGHTPAREN! WS?; +predicate : simplepredicate + | simplepredicate (AND simplepredicate)+ -> ^(AND simplepredicate+) + | simplepredicate (OR simplepredicate)+ -> ^(OR simplepredicate+); +simplepredicate : allowedpredicates -> allowedpredicates + | NEGATION allowedpredicates -> ^(NEGATION allowedpredicates); +allowedpredicates : comparisonpredicate | existspredicate | betweenpredicate | inpredicate | matchespredicate; +comparisonpredicate: PROPERTYNAME comparisonoperator value -> ^(comparisonoperator PROPERTYNAME value); +comparisonoperator: EQUALS|LESSTHAN|GREATERTHAN|LESSTHANOREQUALS|GREATERTHANOREQUALS; +existspredicate: EXISTS LEFTPAREN WS? PROPERTYNAME RIGHTPAREN -> ^(EXISTS PROPERTYNAME); +betweenpredicate: PROPERTYNAME BETWEEN LEFTPAREN WS? propertyvaluepair RIGHTPAREN -> ^(BETWEEN PROPERTYNAME propertyvaluepair); +inpredicate: PROPERTYNAME IN LEFTPAREN WS? propertyvaluelist RIGHTPAREN -> ^(IN PROPERTYNAME propertyvaluelist); +matchespredicate: PROPERTYNAME MATCHES LEFTPAREN WS? value RIGHTPAREN -> ^(MATCHES PROPERTYNAME value); +propertyvaluepair: value COMMA value -> value+; +propertyvaluelist: value (COMMA value)* -> value+; +value: a=PROPERTYVALUE -> ^(PROPERTYVALUE[$a] ) + | b=PROPERTYNAME -> ^(PROPERTYVALUE[$b] ); //rewrite this a propertyvalue +selectClause: PROPERTYNAME (COMMA PROPERTYNAME)* -> PROPERTYNAME+; +// **** +// SOME NOTES +// () - Parentheses. Used to group several elements, so they are treated as one single token +// ? - Any token followed by ? occurs 0 or 1 times +// * - Any token followed by * can occur 0 or more times +// + - Any token followed by + can occur 1 or more times +// . - Any character/token can occur one time +// ~ - Any character/token following the ~ may not occur at the current place +// .. - Between two characters .. spans a range which accepts every character between both boundaries inclusive +// **** diff --git a/remote-api/src/main/apptest/apptest.readme.txt b/remote-api/src/main/apptest/apptest.readme.txt new file mode 100644 index 00000000000..be519b842cd --- /dev/null +++ b/remote-api/src/main/apptest/apptest.readme.txt @@ -0,0 +1,33 @@ +AppClientTest is a tool for testing an AtomPub Service. + +http://code.google.com/p/feedvalidator/wiki/AppClientTest + +1) To install and test installation... + +$ svn co http://feedvalidator.googlecode.com/svn/trunk/apptestsuite/client/validator/ validator +$ python validator/appclienttest.py --output=results.html "http://bitworking.org/projects/apptestsite/app.cgi/service/;service_document" +$ firefox results.html + +The above has been encapsulated in... + +/projects/remote-api/source/test/apptest/install.sh + +2) To execute appclienttest against Alfresco + +/projects/remote-api/source/test/apptest/test.sh + + +3) Note: appclienttest.py arguments... + + -h, --help show this help message and exit + --credentials=FILE FILE that contains a name and password on separate lines + with an optional third line with the authentication type + of 'ClientLogin '. + --output=FILE FILE to store test results + --verbose Print extra information while running. + --quiet Do not print anything while running. + --debug Print low level HTTP information while running. + --html Output is formatted in HTML + --record=DIR Record all the responses to be used later in playback + mode. + --playback=DIR Playback responses stored from a previous run. diff --git a/remote-api/src/main/apptest/credentials.txt b/remote-api/src/main/apptest/credentials.txt new file mode 100644 index 00000000000..295182ad9ce --- /dev/null +++ b/remote-api/src/main/apptest/credentials.txt @@ -0,0 +1,2 @@ +admin +admin \ No newline at end of file diff --git a/remote-api/src/main/apptest/install.sh b/remote-api/src/main/apptest/install.sh new file mode 100644 index 00000000000..acf50596312 --- /dev/null +++ b/remote-api/src/main/apptest/install.sh @@ -0,0 +1,5 @@ +# ! /bin/sh + +svn co http://feedvalidator.googlecode.com/svn/trunk/apptestsuite/client/validator/ validator +python validator/appclienttest.py --html --verbose --output=results.html "http://bitworking.org/projects/apptestsite/app.cgi/service/;service_document" +open results.html diff --git a/remote-api/src/main/apptest/results.html b/remote-api/src/main/apptest/results.html new file mode 100644 index 00000000000..820d37fc152 --- /dev/null +++ b/remote-api/src/main/apptest/results.html @@ -0,0 +1,2963 @@ + + + + + + + + + + AppClientTest - Results + + +

Test Report

+
+
Date
+
Thu May 7 14:22:03 2009
+
+
+

Legend

+
+
Informational
+
Information on what was being tested.
+
Warning
+
Warnings indicate behavior that, while legal, may cause
+ either performance or interoperability problems in the field.
+
Error
+
Errors are violations of either the Atom, AtomPub
or HTTP specifications.
+
Log
+
Detailed information on the transaction to help you
debug your service.
+
Success
+
A specific sub-test has been passed successfully.
+ +
+

Entry Collection

Find the first entry collection listed in an Introspection document and run the Entry collection tests against it.

+
    +
  1. Request
    
    +GET http://localhost:8080/alfresco/service/api/repository
    +
    +
    +
    +
    +
  2. +
  3. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/repository
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:47 GMT
    +content-type: application/atom+xml;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><service xmlns="http://www.w3.org/2007/app" xmlns:alf="http://www.alfresco.org" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +  <workspace cmis:repositoryRelationship="self">
    +    <atom:title>Main Repository</atom:title>
    +
    +    <cmis:repositoryInfo> 
    +      <cmis:repositoryId>418bc6ad-eb45-4259-ad52-086966fe8775</cmis:repositoryId>
    +      <cmis:repositoryName>Main Repository</cmis:repositoryName>
    +      <cmis:repositoryRelationship>self</cmis:repositoryRelationship>
    +      <cmis:repositoryDescription/>   
    +      <cmis:vendorName>Alfresco</cmis:vendorName> 
    +      <cmis:productName>Alfresco Repository (Labs)</cmis:productName>
    +      <cmis:productVersion>3.2.0 (_Preview2_dev @build-number@)</cmis:productVersion>
    +      <cmis:rootFolderId>http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home</cmis:rootFolderId> 
    +      <cmis:capabilities>
    +        <cmis:capabilityMultifiling>true</cmis:capabilityMultifiling>
    +        <cmis:capabilityUnfiling>false</cmis:capabilityUnfiling>
    +        <cmis:capabilityVersionSpecificFiling>false</cmis:capabilityVersionSpecificFiling>        
    +        <cmis:capabilityPWCUpdateable>true</cmis:capabilityPWCUpdateable>
    +        <cmis:capabilityPWCSearchable>true</cmis:capabilityPWCSearchable>
    +        <cmis:capabilityAllVersionsSearchable>false</cmis:capabilityAllVersionsSearchable>
    +        <cmis:capabilityQuery>bothcombined</cmis:capabilityQuery>
    +        <cmis:capabilityJoin>none</cmis:capabilityJoin>
    +        <cmis:capabilityChanges>none</cmis:capabilityChanges>
    +        <cmis:changesIncomplete>false</cmis:changesIncomplete>
    +        <cmis:capabilityACL>none</cmis:capabilityACL>
    +      </cmis:capabilities> 
    +      <cmis:cmisVersionSupported>0.61</cmis:cmisVersionSupported>
    +      <cmis:repositorySpecificInformation/>
    +    </cmis:repositoryInfo>
    +
    +    <collection cmis:collectionType="rootchildren" href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children"> 
    +      <atom:title>root collection</atom:title> 
    +    </collection> 
    +    <collection cmis:collectionType="rootdescendants" href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/descendants"> 
    +      <atom:title>root collection</atom:title> 
    +    </collection> 
    +    <collection cmis:collectionType="checkedout" href="http://localhost:8080/alfresco/service/api/checkedout"> 
    +      <atom:title>checkedout collection</atom:title> 
    +      <atom:accept>application/atom+xml;type=entry</atom:accept>
    +    </collection> 
    +    <collection cmis:collectionType="unfiled" href="http://localhost:8080/alfresco/service/api/unfiled"> 
    +      <atom:title>unfiled collection</atom:title> 
    +      <atom:accept>application/atom+xml;type=entry</atom:accept>
    +    </collection>
    +    <collection cmis:collectionType="typeschildren" href="http://localhost:8080/alfresco/service/api/types"> 
    +      <atom:title>type collection</atom:title> 
    +    </collection>
    +    <collection cmis:collectionType="typesdescendants" href="http://localhost:8080/alfresco/service/api/types"> 
    +      <atom:title>type collection</atom:title> 
    +    </collection> 
    +    <collection cmis:collectionType="query" href="http://localhost:8080/alfresco/service/api/query"> 
    +      <atom:title>query collection</atom:title> 
    +      <atom:accept>application/cmisquery+xml</atom:accept>
    +    </collection>
    +     
    +  </workspace> 
    +</service>
  4. +
  5. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  6. +
  7. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  8. +
  9. [RFC4287] line 40, column 6: Undefined app:collection element: atom:accept (3 occurrences)
  10. +
  11. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org
  12. +
  13. line 4, column 4: atom_title should contain a xml:lang element (8 occurrences)
  14. +
+

Basic Entry Manipulation

Add and remove three entries to the collection

+
    +
  1. Service Document: http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
  2. +
  3. Request
    
    +GET http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +
    +
    +
    +
    +
  4. +
  5. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:50 GMT
    +content-type: application/atom+xml;type=feed;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
    +<author><name>System</name></author>
    +<generator version="3.2.0 (_Preview2_dev @build-number@)">Alfresco (Labs)</generator>
    +<icon>http://localhost:8080/alfresco/images/logo/AlfrescoLogo16.ico</icon>
    +<id>urn:uuid:38381008-9909-4210-958d-53476656d919-children</id>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/38381008-9909-4210-958d-53476656d919" rel="source"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="first" type="application/atom+xml;type=feed"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="last" type="application/atom+xml;type=feed"/>
    +<title>Company Home Children</title>
    +<updated>2009-05-07T09:53:58.833+01:00</updated>
    +<entry>
    +<author><name>System</name></author>
    +<content>8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</content>
    +<id>urn:uuid:8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/F/st_sites" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:14.297+01:00</published>
    +<summary>Site Collaboration Spaces</summary>
    +<title>Sites</title>
    +<updated>2009-05-07T09:54:14.529+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Sites</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:14.529+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:14.297+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>F/st_sites</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:14.529+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>07c77166-1478-41f6-8516-9d246195af7a</content>
    +<id>urn:uuid:07c77166-1478-41f6-8516-9d246195af7a</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:58.839+01:00</published>
    +<summary>User managed definitions</summary>
    +<title>Data Dictionary</title>
    +<updated>2009-05-07T09:53:58.901+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Data Dictionary</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:58.901+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:58.839+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/07c77166-1478-41f6-8516-9d246195af7a</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:58.901+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>7d56fcd8-6efd-4593-b3db-d3d1b0459d43</content>
    +<id>urn:uuid:7d56fcd8-6efd-4593-b3db-d3d1b0459d43</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.235+01:00</published>
    +<summary>The guest root space</summary>
    +<title>Guest Home</title>
    +<updated>2009-05-07T09:53:59.329+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Guest Home</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.329+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.235+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.329+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>43009267-2ea4-4da1-868f-e3110de3d89b</content>
    +<id>urn:uuid:43009267-2ea4-4da1-868f-e3110de3d89b</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.331+01:00</published>
    +<summary>User Homes</summary>
    +<title>User Homes</title>
    +<updated>2009-05-07T09:53:59.350+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>User Homes</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.350+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.331+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.350+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content>cb6dc8da-1282-4f96-aa63-e97865359ec5</content>
    +<id>urn:uuid:cb6dc8da-1282-4f96-aa63-e97865359ec5</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:36.952+01:00</published>
    +<summary>CMIS Tests (summary)</summary>
    +<title>CMIS Tests</title>
    +<updated>2009-05-07T09:54:37.007+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>CMIS Tests</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:37.007+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:36.952+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:37.007+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>41aad918-1d38-4711-8904-f75c72dca106</content>
    +<id>urn:uuid:41aad918-1d38-4711-8904-f75c72dca106</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:18:29.101+01:00</published>
    +<summary>Web Content Management Spaces</summary>
    +<title>Web Projects</title>
    +<updated>2009-05-07T14:18:31.477+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Web Projects</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:18:31.477+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:18:29.101+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/41aad918-1d38-4711-8904-f75c72dca106</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:18:31.477+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<cmis:hasMoreItems>false</cmis:hasMoreItems>
    +<opensearch:totalResults>6</opensearch:totalResults>
    +<opensearch:startIndex>0</opensearch:startIndex>
    +<opensearch:itemsPerPage>0</opensearch:itemsPerPage>
    +</feed>
  6. +
  7. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  8. +
  9. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  10. +
  11. [RFC4287] line 6, column 58: id is not a valid UUID
  12. +
  13. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 134: Unregistered link relationship +line 19, column 156: Unregistered link relationship (42 occurrences)
  14. +
  15. line 11, column 0: title should contain a xml:lang element (7 occurrences) +line 15, column 0: content should contain a xml:lang element (6 occurrences) +line 27, column 0: summary should contain a xml:lang element (6 occurrences)
  16. +
  17. Create new entry #1
  18. +
  19. Request
    
    +POST http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +content-type: application/atom+xml
    +slug: dcghgffkjh
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom">
    +<title>Iñtërnâtiônàlizætiøn - 1</title>
    +<id>tag:bitworking.org,2008-02-26:1241702511.039212</id>
    +<updated>2005-07-10T12:29:29Z</updated>
    +<author>
    +<name>Joe Gregorio</name>
    +</author>
    +<content type="xhtml">
    +<div xmlns="http://www.w3.org/1999/xhtml">
    +<p><i>A test of utf-8</i></p>
    +</div>
    +</content>
    +</entry>
  20. +
  21. Response
    
    +
    +status: 201
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:53 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" type="text/xhtml"/><id>urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:52.635+01:00</published>
    +<summary>dcghgffkjh</summary>
    +<title>dcghgffkjh</title>
    +<updated>2009-05-07T14:21:52.869+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:52.635+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:52.869+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +<cmis:allowableActions>
    +<cmis:canDelete>true</cmis:canDelete>
    +<cmis:canUpdateProperties>true</cmis:canUpdateProperties>
    +<cmis:canGetProperties>true</cmis:canGetProperties>
    +<cmis:canGetRelationships>true</cmis:canGetRelationships>
    +<cmis:canGetParents>true</cmis:canGetParents>
    +<cmis:canMove>true</cmis:canMove>
    +<cmis:canDeleteVersion>true</cmis:canDeleteVersion>
    +<cmis:canDeleteContent>true</cmis:canDeleteContent>
    +<cmis:canCheckout>true</cmis:canCheckout>
    +<cmis:canCancelCheckout>false</cmis:canCancelCheckout>
    +<cmis:canCheckin>false</cmis:canCheckin>
    +<cmis:canSetContent>true</cmis:canSetContent>
    +<cmis:canGetAllVersions>true</cmis:canGetAllVersions>
    +<cmis:canAddToFolder>true</cmis:canAddToFolder>
    +<cmis:canRemoveFromFolder>true</cmis:canRemoveFromFolder>
    +<cmis:canViewContent>true</cmis:canViewContent>
    +<cmis:canAddPolicy>false</cmis:canAddPolicy>
    +<cmis:canGetAppliedPolicies>false</cmis:canGetAppliedPolicies>
    +<cmis:canRemovePolicy>false</cmis:canRemovePolicy>
    +<cmis:canCreateRelationship>true</cmis:canCreateRelationship>
    +</cmis:allowableActions>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:52.869+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  22. +
  23. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  24. +
  25. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  26. +
  27. [RFC5023] Section 9.2 Content-Location: not returned in response headers.
  28. +
  29. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719
    +
    +
    +
    +
    +
  30. +
  31. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:53 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" type="text/xhtml"/><id>urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:52.635+01:00</published>
    +<summary>dcghgffkjh</summary>
    +<title>dcghgffkjh</title>
    +<updated>2009-05-07T14:21:52.869+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:52.635+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:52.869+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:52.869+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  32. +
  33. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  34. +
  35. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  36. +
  37. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  38. +
  39. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  40. +
  41. Create new entry #2
  42. +
  43. Request
    
    +POST http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +content-type: application/atom+xml
    +slug: cajgclekci
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom">
    +<title>Iñtërnâtiônàlizætiøn - 2</title>
    +<id>tag:bitworking.org,2008-02-26:1241702515.965076</id>
    +<updated>2005-07-10T12:29:29Z</updated>
    +<author>
    +<name>Joe Gregorio</name>
    +</author>
    +<content type="xhtml">
    +<div xmlns="http://www.w3.org/1999/xhtml">
    +<p><i>A test of utf-8</i></p>
    +</div>
    +</content>
    +</entry>
  44. +
  45. Response
    
    +
    +status: 201
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:56 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>cajgclekci</title>
    +<updated>2009-05-07T14:21:56.047+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>cajgclekci</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>cajgclekci</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:56.047+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +<cmis:allowableActions>
    +<cmis:canDelete>true</cmis:canDelete>
    +<cmis:canUpdateProperties>true</cmis:canUpdateProperties>
    +<cmis:canGetProperties>true</cmis:canGetProperties>
    +<cmis:canGetRelationships>true</cmis:canGetRelationships>
    +<cmis:canGetParents>true</cmis:canGetParents>
    +<cmis:canMove>true</cmis:canMove>
    +<cmis:canDeleteVersion>true</cmis:canDeleteVersion>
    +<cmis:canDeleteContent>true</cmis:canDeleteContent>
    +<cmis:canCheckout>true</cmis:canCheckout>
    +<cmis:canCancelCheckout>false</cmis:canCancelCheckout>
    +<cmis:canCheckin>false</cmis:canCheckin>
    +<cmis:canSetContent>true</cmis:canSetContent>
    +<cmis:canGetAllVersions>true</cmis:canGetAllVersions>
    +<cmis:canAddToFolder>true</cmis:canAddToFolder>
    +<cmis:canRemoveFromFolder>true</cmis:canRemoveFromFolder>
    +<cmis:canViewContent>true</cmis:canViewContent>
    +<cmis:canAddPolicy>false</cmis:canAddPolicy>
    +<cmis:canGetAppliedPolicies>false</cmis:canGetAppliedPolicies>
    +<cmis:canRemovePolicy>false</cmis:canRemovePolicy>
    +<cmis:canCreateRelationship>true</cmis:canCreateRelationship>
    +</cmis:allowableActions>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:56.047+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  46. +
  47. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  48. +
  49. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  50. +
  51. [RFC5023] Section 9.2 Content-Location: not returned in response headers.
  52. +
  53. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +
    +
    +
    +
    +
  54. +
  55. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:56 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>cajgclekci</title>
    +<updated>2009-05-07T14:21:56.047+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>cajgclekci</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>cajgclekci</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:56.047+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:56.047+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  56. +
  57. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  58. +
  59. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  60. +
  61. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  62. +
  63. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  64. +
  65. Create new entry #3
  66. +
  67. Request
    
    +POST http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +content-type: application/atom+xml
    +slug: kabiabakfg
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom">
    +<title>Iñtërnâtiônàlizætiøn - 3</title>
    +<id>tag:bitworking.org,2008-02-26:1241702517.5035069</id>
    +<updated>2005-07-10T12:29:29Z</updated>
    +<author>
    +<name>Joe Gregorio</name>
    +</author>
    +<content type="xhtml">
    +<div xmlns="http://www.w3.org/1999/xhtml">
    +<p><i>A test of utf-8</i></p>
    +</div>
    +</content>
    +</entry>
  68. +
  69. Response
    
    +
    +status: 201
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:59 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" type="text/xhtml"/><id>urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:57.724+01:00</published>
    +<summary>kabiabakfg</summary>
    +<title>kabiabakfg</title>
    +<updated>2009-05-07T14:21:59.210+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:57.724+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:59.210+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +<cmis:allowableActions>
    +<cmis:canDelete>true</cmis:canDelete>
    +<cmis:canUpdateProperties>true</cmis:canUpdateProperties>
    +<cmis:canGetProperties>true</cmis:canGetProperties>
    +<cmis:canGetRelationships>true</cmis:canGetRelationships>
    +<cmis:canGetParents>true</cmis:canGetParents>
    +<cmis:canMove>true</cmis:canMove>
    +<cmis:canDeleteVersion>true</cmis:canDeleteVersion>
    +<cmis:canDeleteContent>true</cmis:canDeleteContent>
    +<cmis:canCheckout>true</cmis:canCheckout>
    +<cmis:canCancelCheckout>false</cmis:canCancelCheckout>
    +<cmis:canCheckin>false</cmis:canCheckin>
    +<cmis:canSetContent>true</cmis:canSetContent>
    +<cmis:canGetAllVersions>true</cmis:canGetAllVersions>
    +<cmis:canAddToFolder>true</cmis:canAddToFolder>
    +<cmis:canRemoveFromFolder>true</cmis:canRemoveFromFolder>
    +<cmis:canViewContent>true</cmis:canViewContent>
    +<cmis:canAddPolicy>false</cmis:canAddPolicy>
    +<cmis:canGetAppliedPolicies>false</cmis:canGetAppliedPolicies>
    +<cmis:canRemovePolicy>false</cmis:canRemovePolicy>
    +<cmis:canCreateRelationship>true</cmis:canCreateRelationship>
    +</cmis:allowableActions>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:59.210+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  70. +
  71. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  72. +
  73. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  74. +
  75. [RFC5023] Section 9.2 Content-Location: not returned in response headers.
  76. +
  77. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4
    +
    +
    +
    +
    +
  78. +
  79. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:21:59 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" type="text/xhtml"/><id>urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:57.724+01:00</published>
    +<summary>kabiabakfg</summary>
    +<title>kabiabakfg</title>
    +<updated>2009-05-07T14:21:59.210+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:57.724+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:59.210+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:59.210+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  80. +
  81. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  82. +
  83. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  84. +
  85. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  86. +
  87. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  88. +
  89. Request
    
    +GET http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +
    +
    +
    +
    +
  90. +
  91. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:00 GMT
    +content-type: application/atom+xml;type=feed;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
    +<author><name>System</name></author>
    +<generator version="3.2.0 (_Preview2_dev @build-number@)">Alfresco (Labs)</generator>
    +<icon>http://localhost:8080/alfresco/images/logo/AlfrescoLogo16.ico</icon>
    +<id>urn:uuid:38381008-9909-4210-958d-53476656d919-children</id>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/38381008-9909-4210-958d-53476656d919" rel="source"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="first" type="application/atom+xml;type=feed"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="last" type="application/atom+xml;type=feed"/>
    +<title>Company Home Children</title>
    +<updated>2009-05-07T09:53:58.833+01:00</updated>
    +<entry>
    +<author><name>System</name></author>
    +<content>8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</content>
    +<id>urn:uuid:8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/F/st_sites" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:14.297+01:00</published>
    +<summary>Site Collaboration Spaces</summary>
    +<title>Sites</title>
    +<updated>2009-05-07T09:54:14.529+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Sites</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:14.529+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:14.297+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>F/st_sites</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:14.529+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>07c77166-1478-41f6-8516-9d246195af7a</content>
    +<id>urn:uuid:07c77166-1478-41f6-8516-9d246195af7a</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:58.839+01:00</published>
    +<summary>User managed definitions</summary>
    +<title>Data Dictionary</title>
    +<updated>2009-05-07T09:53:58.901+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Data Dictionary</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:58.901+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:58.839+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/07c77166-1478-41f6-8516-9d246195af7a</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:58.901+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>7d56fcd8-6efd-4593-b3db-d3d1b0459d43</content>
    +<id>urn:uuid:7d56fcd8-6efd-4593-b3db-d3d1b0459d43</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.235+01:00</published>
    +<summary>The guest root space</summary>
    +<title>Guest Home</title>
    +<updated>2009-05-07T09:53:59.329+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Guest Home</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.329+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.235+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.329+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>43009267-2ea4-4da1-868f-e3110de3d89b</content>
    +<id>urn:uuid:43009267-2ea4-4da1-868f-e3110de3d89b</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.331+01:00</published>
    +<summary>User Homes</summary>
    +<title>User Homes</title>
    +<updated>2009-05-07T09:53:59.350+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>User Homes</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.350+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.331+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.350+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content>cb6dc8da-1282-4f96-aa63-e97865359ec5</content>
    +<id>urn:uuid:cb6dc8da-1282-4f96-aa63-e97865359ec5</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:36.952+01:00</published>
    +<summary>CMIS Tests (summary)</summary>
    +<title>CMIS Tests</title>
    +<updated>2009-05-07T09:54:37.007+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>CMIS Tests</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:37.007+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:36.952+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:37.007+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>41aad918-1d38-4711-8904-f75c72dca106</content>
    +<id>urn:uuid:41aad918-1d38-4711-8904-f75c72dca106</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:18:29.101+01:00</published>
    +<summary>Web Content Management Spaces</summary>
    +<title>Web Projects</title>
    +<updated>2009-05-07T14:18:31.477+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Web Projects</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:18:31.477+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:18:29.101+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/41aad918-1d38-4711-8904-f75c72dca106</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:18:31.477+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" type="text/xhtml"/><id>urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:52.635+01:00</published>
    +<summary>dcghgffkjh</summary>
    +<title>dcghgffkjh</title>
    +<updated>2009-05-07T14:21:52.869+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:52.635+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:52.869+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:52.869+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>cajgclekci</title>
    +<updated>2009-05-07T14:21:56.047+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>cajgclekci</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>cajgclekci</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:56.047+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:56.047+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" type="text/xhtml"/><id>urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:57.724+01:00</published>
    +<summary>kabiabakfg</summary>
    +<title>kabiabakfg</title>
    +<updated>2009-05-07T14:21:59.210+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:57.724+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:59.210+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:59.210+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<cmis:hasMoreItems>false</cmis:hasMoreItems>
    +<opensearch:totalResults>9</opensearch:totalResults>
    +<opensearch:startIndex>0</opensearch:startIndex>
    +<opensearch:itemsPerPage>0</opensearch:itemsPerPage>
    +</feed>
  92. +
  93. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  94. +
  95. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  96. +
  97. [RFC4287] line 6, column 58: id is not a valid UUID
  98. +
  99. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 134: Unregistered link relationship +line 19, column 156: Unregistered link relationship (63 occurrences)
  100. +
  101. line 11, column 0: title should contain a xml:lang element (10 occurrences) +line 15, column 0: content should contain a xml:lang element (9 occurrences) +line 27, column 0: summary should contain a xml:lang element (9 occurrences)
  102. +
  103. Check order of entries in the collection document
  104. +
  105. [RFC5023] Section 10 Failed to preserve order of entries, was expecting urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4, but found urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719
  106. +
  107. [RFC5023] Section 9.7 Slug was ignored
  108. +
  109. Member contained an 'edit' link
  110. +
  111. Update entry #2 and write back to the collection
  112. +
  113. Request
    
    +PUT http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +
    +
    +
    +
    +
  114. +
  115. Response
    
    +
    +status: 200
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:00 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>Internationalization - 2</title>
    +<updated>2009-05-07T14:22:00.918+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:22:00.918+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +<cmis:allowableActions>
    +<cmis:canDelete>true</cmis:canDelete>
    +<cmis:canUpdateProperties>true</cmis:canUpdateProperties>
    +<cmis:canGetProperties>true</cmis:canGetProperties>
    +<cmis:canGetRelationships>true</cmis:canGetRelationships>
    +<cmis:canGetParents>true</cmis:canGetParents>
    +<cmis:canMove>true</cmis:canMove>
    +<cmis:canDeleteVersion>true</cmis:canDeleteVersion>
    +<cmis:canDeleteContent>true</cmis:canDeleteContent>
    +<cmis:canCheckout>true</cmis:canCheckout>
    +<cmis:canCancelCheckout>false</cmis:canCancelCheckout>
    +<cmis:canCheckin>false</cmis:canCheckin>
    +<cmis:canSetContent>true</cmis:canSetContent>
    +<cmis:canGetAllVersions>true</cmis:canGetAllVersions>
    +<cmis:canAddToFolder>true</cmis:canAddToFolder>
    +<cmis:canRemoveFromFolder>true</cmis:canRemoveFromFolder>
    +<cmis:canViewContent>true</cmis:canViewContent>
    +<cmis:canAddPolicy>false</cmis:canAddPolicy>
    +<cmis:canGetAppliedPolicies>false</cmis:canGetAppliedPolicies>
    +<cmis:canRemovePolicy>false</cmis:canRemovePolicy>
    +<cmis:canCreateRelationship>true</cmis:canCreateRelationship>
    +</cmis:allowableActions>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:22:00.918+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  116. +
  117. Updated Entry #2
  118. +
  119. Check order of entries in the collection document
  120. +
  121. Request
    
    +GET http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +
    +
    +
    +
    +
  122. +
  123. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:00 GMT
    +content-type: application/atom+xml;type=feed;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
    +<author><name>System</name></author>
    +<generator version="3.2.0 (_Preview2_dev @build-number@)">Alfresco (Labs)</generator>
    +<icon>http://localhost:8080/alfresco/images/logo/AlfrescoLogo16.ico</icon>
    +<id>urn:uuid:38381008-9909-4210-958d-53476656d919-children</id>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/38381008-9909-4210-958d-53476656d919" rel="source"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="first" type="application/atom+xml;type=feed"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="last" type="application/atom+xml;type=feed"/>
    +<title>Company Home Children</title>
    +<updated>2009-05-07T09:53:58.833+01:00</updated>
    +<entry>
    +<author><name>System</name></author>
    +<content>8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</content>
    +<id>urn:uuid:8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/F/st_sites" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:14.297+01:00</published>
    +<summary>Site Collaboration Spaces</summary>
    +<title>Sites</title>
    +<updated>2009-05-07T09:54:14.529+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Sites</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:14.529+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:14.297+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>F/st_sites</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:14.529+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>07c77166-1478-41f6-8516-9d246195af7a</content>
    +<id>urn:uuid:07c77166-1478-41f6-8516-9d246195af7a</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:58.839+01:00</published>
    +<summary>User managed definitions</summary>
    +<title>Data Dictionary</title>
    +<updated>2009-05-07T09:53:58.901+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Data Dictionary</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:58.901+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:58.839+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/07c77166-1478-41f6-8516-9d246195af7a</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:58.901+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>7d56fcd8-6efd-4593-b3db-d3d1b0459d43</content>
    +<id>urn:uuid:7d56fcd8-6efd-4593-b3db-d3d1b0459d43</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.235+01:00</published>
    +<summary>The guest root space</summary>
    +<title>Guest Home</title>
    +<updated>2009-05-07T09:53:59.329+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Guest Home</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.329+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.235+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.329+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>43009267-2ea4-4da1-868f-e3110de3d89b</content>
    +<id>urn:uuid:43009267-2ea4-4da1-868f-e3110de3d89b</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.331+01:00</published>
    +<summary>User Homes</summary>
    +<title>User Homes</title>
    +<updated>2009-05-07T09:53:59.350+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>User Homes</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.350+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.331+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.350+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content>cb6dc8da-1282-4f96-aa63-e97865359ec5</content>
    +<id>urn:uuid:cb6dc8da-1282-4f96-aa63-e97865359ec5</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:36.952+01:00</published>
    +<summary>CMIS Tests (summary)</summary>
    +<title>CMIS Tests</title>
    +<updated>2009-05-07T09:54:37.007+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>CMIS Tests</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:37.007+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:36.952+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:37.007+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>41aad918-1d38-4711-8904-f75c72dca106</content>
    +<id>urn:uuid:41aad918-1d38-4711-8904-f75c72dca106</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:18:29.101+01:00</published>
    +<summary>Web Content Management Spaces</summary>
    +<title>Web Projects</title>
    +<updated>2009-05-07T14:18:31.477+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Web Projects</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:18:31.477+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:18:29.101+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/41aad918-1d38-4711-8904-f75c72dca106</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:18:31.477+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" type="text/xhtml"/><id>urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:52.635+01:00</published>
    +<summary>dcghgffkjh</summary>
    +<title>dcghgffkjh</title>
    +<updated>2009-05-07T14:21:52.869+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:52.635+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:52.869+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:52.869+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" type="text/xhtml"/><id>urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:57.724+01:00</published>
    +<summary>kabiabakfg</summary>
    +<title>kabiabakfg</title>
    +<updated>2009-05-07T14:21:59.210+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:57.724+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:59.210+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:59.210+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>Internationalization - 2</title>
    +<updated>2009-05-07T14:22:00.918+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:22:00.918+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:22:00.918+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<cmis:hasMoreItems>false</cmis:hasMoreItems>
    +<opensearch:totalResults>9</opensearch:totalResults>
    +<opensearch:startIndex>0</opensearch:startIndex>
    +<opensearch:itemsPerPage>0</opensearch:itemsPerPage>
    +</feed>
  124. +
  125. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  126. +
  127. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  128. +
  129. [RFC4287] line 6, column 58: id is not a valid UUID
  130. +
  131. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 134: Unregistered link relationship +line 19, column 156: Unregistered link relationship (63 occurrences)
  132. +
  133. line 11, column 0: title should contain a xml:lang element (10 occurrences) +line 15, column 0: content should contain a xml:lang element (9 occurrences) +line 27, column 0: summary should contain a xml:lang element (9 occurrences)
  134. +
  135. [RFC5023] Section 10 Failed to preserve order of entries, was expecting urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d, but found urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719
  136. +
  137. Request
    
    +GET http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +
    +
    +
    +
    +
  138. +
  139. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=feed;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
    +<author><name>System</name></author>
    +<generator version="3.2.0 (_Preview2_dev @build-number@)">Alfresco (Labs)</generator>
    +<icon>http://localhost:8080/alfresco/images/logo/AlfrescoLogo16.ico</icon>
    +<id>urn:uuid:38381008-9909-4210-958d-53476656d919-children</id>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/38381008-9909-4210-958d-53476656d919" rel="source"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="first" type="application/atom+xml;type=feed"/>
    +<link href="http://localhost:8080/alfresco/service/api/path/workspace/SpacesStore/Company%20Home/children?pageNo=1&amp;pageSize=0&amp;guest=&amp;format=atomfeed" rel="last" type="application/atom+xml;type=feed"/>
    +<title>Company Home Children</title>
    +<updated>2009-05-07T09:53:58.833+01:00</updated>
    +<entry>
    +<author><name>System</name></author>
    +<content>8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</content>
    +<id>urn:uuid:8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/F/st_sites" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:14.297+01:00</published>
    +<summary>Site Collaboration Spaces</summary>
    +<title>Sites</title>
    +<updated>2009-05-07T09:54:14.529+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Sites</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:14.529+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:14.297+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>F/st_sites</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:14.529+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>07c77166-1478-41f6-8516-9d246195af7a</content>
    +<id>urn:uuid:07c77166-1478-41f6-8516-9d246195af7a</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:58.839+01:00</published>
    +<summary>User managed definitions</summary>
    +<title>Data Dictionary</title>
    +<updated>2009-05-07T09:53:58.901+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Data Dictionary</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:58.901+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:58.839+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/07c77166-1478-41f6-8516-9d246195af7a</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:58.901+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>7d56fcd8-6efd-4593-b3db-d3d1b0459d43</content>
    +<id>urn:uuid:7d56fcd8-6efd-4593-b3db-d3d1b0459d43</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.235+01:00</published>
    +<summary>The guest root space</summary>
    +<title>Guest Home</title>
    +<updated>2009-05-07T09:53:59.329+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Guest Home</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.329+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.235+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.329+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>43009267-2ea4-4da1-868f-e3110de3d89b</content>
    +<id>urn:uuid:43009267-2ea4-4da1-868f-e3110de3d89b</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.331+01:00</published>
    +<summary>User Homes</summary>
    +<title>User Homes</title>
    +<updated>2009-05-07T09:53:59.350+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>User Homes</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.350+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.331+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.350+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content>cb6dc8da-1282-4f96-aa63-e97865359ec5</content>
    +<id>urn:uuid:cb6dc8da-1282-4f96-aa63-e97865359ec5</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:36.952+01:00</published>
    +<summary>CMIS Tests (summary)</summary>
    +<title>CMIS Tests</title>
    +<updated>2009-05-07T09:54:37.007+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>CMIS Tests</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:37.007+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:36.952+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:37.007+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>System</name></author>
    +<content>41aad918-1d38-4711-8904-f75c72dca106</content>
    +<id>urn:uuid:41aad918-1d38-4711-8904-f75c72dca106</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:18:29.101+01:00</published>
    +<summary>Web Content Management Spaces</summary>
    +<title>Web Projects</title>
    +<updated>2009-05-07T14:18:31.477+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Web Projects</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:18:31.477+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:18:29.101+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/41aad918-1d38-4711-8904-f75c72dca106</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:18:31.477+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" type="text/xhtml"/><id>urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:52.635+01:00</published>
    +<summary>dcghgffkjh</summary>
    +<title>dcghgffkjh</title>
    +<updated>2009-05-07T14:21:52.869+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:52.635+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:52.869+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:52.869+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" type="text/xhtml"/><id>urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:57.724+01:00</published>
    +<summary>kabiabakfg</summary>
    +<title>kabiabakfg</title>
    +<updated>2009-05-07T14:21:59.210+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:57.724+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:59.210+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:59.210+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<entry>
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>Internationalization - 2</title>
    +<updated>2009-05-07T14:22:00.918+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:22:00.918+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:22:00.918+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
    +<cmis:hasMoreItems>false</cmis:hasMoreItems>
    +<opensearch:totalResults>9</opensearch:totalResults>
    +<opensearch:startIndex>0</opensearch:startIndex>
    +<opensearch:itemsPerPage>0</opensearch:itemsPerPage>
    +</feed>
  140. +
  141. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  142. +
  143. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  144. +
  145. [RFC4287] line 6, column 58: id is not a valid UUID
  146. +
  147. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 134: Unregistered link relationship +line 19, column 156: Unregistered link relationship (63 occurrences)
  148. +
  149. line 11, column 0: title should contain a xml:lang element (10 occurrences) +line 15, column 0: content should contain a xml:lang element (9 occurrences) +line 27, column 0: summary should contain a xml:lang element (9 occurrences)
  150. +
  151. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb
    +
    +
    +
    +
    +
  152. +
  153. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>System</name></author>
    +<content>8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</content>
    +<id>urn:uuid:8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/F/st_sites" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:14.297+01:00</published>
    +<summary>Site Collaboration Spaces</summary>
    +<title>Sites</title>
    +<updated>2009-05-07T09:54:14.529+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Sites</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:14.529+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:14.297+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>F/st_sites</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/8ed1cc95-85b5-4c78-ad5f-7a1ddd642aeb</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:14.529+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
  154. +
  155. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  156. +
  157. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  158. +
  159. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 156: Unregistered link relationship (7 occurrences)
  160. +
  161. line 4, column 0: content should contain a xml:lang element +line 16, column 0: summary should contain a xml:lang element +line 17, column 0: title should contain a xml:lang element
  162. +
  163. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a
    +
    +
    +
    +
    +
  164. +
  165. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>System</name></author>
    +<content>07c77166-1478-41f6-8516-9d246195af7a</content>
    +<id>urn:uuid:07c77166-1478-41f6-8516-9d246195af7a</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/07c77166-1478-41f6-8516-9d246195af7a/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:58.839+01:00</published>
    +<summary>User managed definitions</summary>
    +<title>Data Dictionary</title>
    +<updated>2009-05-07T09:53:58.901+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Data Dictionary</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:58.901+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:58.839+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/07c77166-1478-41f6-8516-9d246195af7a</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:58.901+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
  166. +
  167. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  168. +
  169. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  170. +
  171. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 156: Unregistered link relationship (7 occurrences)
  172. +
  173. line 4, column 0: content should contain a xml:lang element +line 16, column 0: summary should contain a xml:lang element +line 17, column 0: title should contain a xml:lang element
  174. +
  175. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43
    +
    +
    +
    +
    +
  176. +
  177. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>System</name></author>
    +<content>7d56fcd8-6efd-4593-b3db-d3d1b0459d43</content>
    +<id>urn:uuid:7d56fcd8-6efd-4593-b3db-d3d1b0459d43</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.235+01:00</published>
    +<summary>The guest root space</summary>
    +<title>Guest Home</title>
    +<updated>2009-05-07T09:53:59.329+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Guest Home</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.329+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.235+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/7d56fcd8-6efd-4593-b3db-d3d1b0459d43</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.329+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
  178. +
  179. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  180. +
  181. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  182. +
  183. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 156: Unregistered link relationship (7 occurrences)
  184. +
  185. line 4, column 0: content should contain a xml:lang element +line 16, column 0: summary should contain a xml:lang element +line 17, column 0: title should contain a xml:lang element
  186. +
  187. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b
    +
    +
    +
    +
    +
  188. +
  189. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>System</name></author>
    +<content>43009267-2ea4-4da1-868f-e3110de3d89b</content>
    +<id>urn:uuid:43009267-2ea4-4da1-868f-e3110de3d89b</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:53:59.331+01:00</published>
    +<summary>User Homes</summary>
    +<title>User Homes</title>
    +<updated>2009-05-07T09:53:59.350+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>User Homes</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:53:59.350+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:53:59.331+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/43009267-2ea4-4da1-868f-e3110de3d89b</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:53:59.350+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
  190. +
  191. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  192. +
  193. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  194. +
  195. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 156: Unregistered link relationship (7 occurrences)
  196. +
  197. line 4, column 0: content should contain a xml:lang element +line 16, column 0: summary should contain a xml:lang element +line 17, column 0: title should contain a xml:lang element
  198. +
  199. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5
    +
    +
    +
    +
    +
  200. +
  201. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content>cb6dc8da-1282-4f96-aa63-e97865359ec5</content>
    +<id>urn:uuid:cb6dc8da-1282-4f96-aa63-e97865359ec5</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T09:54:36.952+01:00</published>
    +<summary>CMIS Tests (summary)</summary>
    +<title>CMIS Tests</title>
    +<updated>2009-05-07T09:54:37.007+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>CMIS Tests</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T09:54:37.007+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T09:54:36.952+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/cb6dc8da-1282-4f96-aa63-e97865359ec5</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T09:54:37.007+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
  202. +
  203. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  204. +
  205. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  206. +
  207. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 156: Unregistered link relationship (7 occurrences)
  208. +
  209. line 4, column 0: content should contain a xml:lang element +line 16, column 0: summary should contain a xml:lang element +line 17, column 0: title should contain a xml:lang element
  210. +
  211. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106
    +
    +
    +
    +
    +
  212. +
  213. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>System</name></author>
    +<content>41aad918-1d38-4711-8904-f75c72dca106</content>
    +<id>urn:uuid:41aad918-1d38-4711-8904-f75c72dca106</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/parent" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/children" rel="children"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/41aad918-1d38-4711-8904-f75c72dca106/descendants" rel="descendants"/>
    +<link href="http://localhost:8080/alfresco/service/api/type/folder" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:18:29.101+01:00</published>
    +<summary>Web Content Management Spaces</summary>
    +<title>Web Projects</title>
    +<updated>2009-05-07T14:18:31.477+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>folder</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Web Projects</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ParentId"><cmis:value>workspace://SpacesStore/38381008-9909-4210-958d-53476656d919</cmis:value></cmis:propertyId>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:18:31.477+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyId cmis:name="AllowedChildObjectTypeIds"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:18:29.101+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>System</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>folder</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/41aad918-1d38-4711-8904-f75c72dca106</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:18:31.477+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/icons/space-icon-default-16.gif</alf:icon>
    +</entry>
  214. +
  215. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  216. +
  217. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  218. +
  219. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 8, column 156: Unregistered link relationship (7 occurrences)
  220. +
  221. line 4, column 0: content should contain a xml:lang element +line 16, column 0: summary should contain a xml:lang element +line 17, column 0: title should contain a xml:lang element
  222. +
  223. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719
    +
    +
    +
    +
    +
  224. +
  225. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:01 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" type="text/xhtml"/><id>urn:uuid:9f28d574-f3ef-43b1-b346-be7aaf79d719</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:52.635+01:00</published>
    +<summary>dcghgffkjh</summary>
    +<title>dcghgffkjh</title>
    +<updated>2009-05-07T14:21:52.869+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:52.635+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>dcghgffkjh</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:52.869+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:52.869+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  226. +
  227. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  228. +
  229. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  230. +
  231. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  232. +
  233. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  234. +
  235. Remove entry
  236. +
  237. Request
    
    +DELETE http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9f28d574-f3ef-43b1-b346-be7aaf79d719
    +
    +
    +
    +
    +
  238. +
  239. Response
    
    +
    +date: Thu, 07 May 2009 13:22:03 GMT
    +status: 204
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +
    +
    +
    +
  240. +
  241. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4
    +
    +
    +
    +
    +
  242. +
  243. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:03 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" type="text/xhtml"/><id>urn:uuid:a56de7d2-ddea-431c-bd2d-5664126ad5b4</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:57.724+01:00</published>
    +<summary>kabiabakfg</summary>
    +<title>kabiabakfg</title>
    +<updated>2009-05-07T14:21:59.210+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:57.724+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>kabiabakfg</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:21:59.210+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:21:59.210+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  244. +
  245. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  246. +
  247. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  248. +
  249. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  250. +
  251. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  252. +
  253. Remove entry
  254. +
  255. Request
    
    +DELETE http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/a56de7d2-ddea-431c-bd2d-5664126ad5b4
    +
    +
    +
    +
    +
  256. +
  257. Response
    
    +
    +date: Thu, 07 May 2009 13:22:03 GMT
    +status: 204
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +
    +
    +
    +
  258. +
  259. Request
    
    +GET http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +
    +
    +
    +
    +
  260. +
  261. Response
    
    +
    +status: 200
    +content-location: http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +transfer-encoding: chunked
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +date: Thu, 07 May 2009 13:22:03 GMT
    +content-type: application/atom+xml;type=entry;charset=UTF-8
    +
    +
    +
    +
    <?xml version="1.0" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:alf="http://www.alfresco.org" xmlns:app="http://www.w3.org/2007/app" xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200901">
    +<author><name>admin</name></author>
    +<content src="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" type="text/xhtml"/><id>urn:uuid:9ef7edfc-8298-4643-a9b1-8bfdeb32904d</id>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="self"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="enclosure" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d" rel="edit"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="edit-media" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/permissions" rel="allowableactions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/associations" rel="relationships"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/parents" rel="parents"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/versions" rel="allversions"/>
    +<link href="http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content" rel="stream" type="text/xhtml"/><link href="http://localhost:8080/alfresco/service/api/type/document" rel="type"/>
    +<link href="http://localhost:8080/alfresco/service/api/repository" rel="repository"/>
    +<published>2009-05-07T14:21:56.005+01:00</published>
    +<summary>cajgclekci</summary>
    +<title>Internationalization - 2</title>
    +<updated>2009-05-07T14:22:00.918+01:00</updated>
    +<cmis:object>
    +<cmis:properties>
    +<cmis:propertyString cmis:name="BaseType"><cmis:value>document</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="ContentStreamFilename"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyString cmis:name="CheckinComment"/>
    +<cmis:propertyInteger cmis:name="ContentStreamLength"><cmis:value>68</cmis:value></cmis:propertyInteger>
    +<cmis:propertyString cmis:name="VersionLabel"/>
    +<cmis:propertyString cmis:name="ContentStreamAllowed"><cmis:value>ALLOWED</cmis:value></cmis:propertyString>
    +<cmis:propertyUri cmis:name="ContentStreamUri"><cmis:value>http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d/content</cmis:value></cmis:propertyUri>
    +<cmis:propertyBoolean cmis:name="IsLatestVersion"><cmis:value>true</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="ContentStreamMimeType"><cmis:value>text/xhtml</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsVersionSeriesCheckedOut"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyBoolean cmis:name="IsMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="VersionSeriesCheckedOutBy"/>
    +<cmis:propertyDateTime cmis:name="CreationDate"><cmis:value>2009-05-07T14:21:56.005+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyString cmis:name="ChangeToken"/>
    +<cmis:propertyString cmis:name="LastModifiedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +<cmis:propertyBoolean cmis:name="IsLatestMajorVersion"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyString cmis:name="Name"><cmis:value>Internationalization - 2</cmis:value></cmis:propertyString>
    +<cmis:propertyBoolean cmis:name="IsImmutable"><cmis:value>false</cmis:value></cmis:propertyBoolean>
    +<cmis:propertyDateTime cmis:name="LastModificationDate"><cmis:value>2009-05-07T14:22:00.918+01:00</cmis:value></cmis:propertyDateTime>
    +<cmis:propertyUri cmis:name="Uri"/>
    +<cmis:propertyString cmis:name="CreatedBy"><cmis:value>admin</cmis:value></cmis:propertyString>
    +<cmis:propertyId cmis:name="VersionSeriesCheckedOutId"/>
    +<cmis:propertyId cmis:name="ObjectTypeId"><cmis:value>document</cmis:value></cmis:propertyId>
    +<cmis:propertyId cmis:name="ObjectId"><cmis:value>workspace://SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d</cmis:value></cmis:propertyId>
    +</cmis:properties>
    +</cmis:object>
    +<cmis:terminator/>
    +<app:edited>2009-05-07T14:22:00.918+01:00</app:edited>
    +<alf:icon>http://localhost:8080/alfresco/images/filetypes/_default.gif</alf:icon>
    +</entry>
  262. +
  263. [RFC2616] Section 13.3.4 No ETag: header was sent with the response.
  264. +
  265. [RFC2616] Section 13.3.4 No Last-Modified: header was sent with the response.
  266. +
  267. [RFC4287] line 2, column 0: Use of unknown namespace: http://docs.oasis-open.org/ns/cmis/core/200901 +line 2, column 0: Use of unknown namespace: http://www.alfresco.org +line 7, column 320: Unregistered link relationship (7 occurrences)
  268. +
  269. line 4, column 0: content should contain a xml:lang element +line 14, column 0: summary should contain a xml:lang element +line 15, column 0: title should contain a xml:lang element
  270. +
  271. Remove entry
  272. +
  273. Request
    
    +DELETE http://localhost:8080/alfresco/service/api/node/workspace/SpacesStore/9ef7edfc-8298-4643-a9b1-8bfdeb32904d
    +
    +
    +
    +
    +
  274. +
  275. Response
    
    +
    +date: Thu, 07 May 2009 13:22:03 GMT
    +status: 204
    +server: Apache-Coyote/1.1
    +pragma: no-cache
    +cache-control: no-cache
    +
    +
    +
    +
  276. +
  277. Removed all entries that we're previously added.
  278. +
  279. Didn't find any Media Collections that would accept GIF images
  280. +
+ diff --git a/remote-api/src/main/apptest/test.sh b/remote-api/src/main/apptest/test.sh new file mode 100644 index 00000000000..bb9e5fcd91f --- /dev/null +++ b/remote-api/src/main/apptest/test.sh @@ -0,0 +1,4 @@ +# ! /bin/sh + +python validator/appclienttest.py --html --credentials=credentials.txt --verbose --output=results.html "http://localhost:8080/alfresco/service/api/repository" +open results.html diff --git a/remote-api/src/main/generated/WhereClause.tokens b/remote-api/src/main/generated/WhereClause.tokens new file mode 100644 index 00000000000..c3763e00406 --- /dev/null +++ b/remote-api/src/main/generated/WhereClause.tokens @@ -0,0 +1,24 @@ +AND=4 +BETWEEN=5 +COLON=6 +COMMA=7 +EQUALS=8 +EXISTS=9 +GREATERTHAN=10 +GREATERTHANOREQUALS=11 +IDENTIFIER=12 +IDENTIFIERDIGIT=13 +IDENTIFIERLETTER=14 +IDENTIFIERLETTERORDIGIT=15 +IN=16 +LEFTPAREN=17 +LESSTHAN=18 +LESSTHANOREQUALS=19 +MATCHES=20 +NEGATION=21 +OR=22 +PROPERTYNAME=23 +PROPERTYVALUE=24 +RIGHTPAREN=25 +SINGLEQUOTE=26 +WS=27 diff --git a/remote-api/src/main/generated/org/alfresco/rest/antlr/WhereClauseLexer.java b/remote-api/src/main/generated/org/alfresco/rest/antlr/WhereClauseLexer.java new file mode 100644 index 00000000000..7dc51bddf6e --- /dev/null +++ b/remote-api/src/main/generated/org/alfresco/rest/antlr/WhereClauseLexer.java @@ -0,0 +1,1840 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +// $ANTLR 3.4 org/alfresco/rest/antlr/WhereClause.g 2013-05-24 09:01:14 + +package org.alfresco.rest.antlr; +import java.util.Map; +import java.util.HashMap; +import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException; +import org.alfresco.rest.framework.resource.parameters.where.WhereCompiler; + + +import org.antlr.runtime.*; +import java.util.Stack; +import java.util.List; +import java.util.ArrayList; + +@SuppressWarnings({"all", "warnings", "unchecked"}) +public class WhereClauseLexer extends Lexer { + public static final int EOF=-1; + public static final int AND=4; + public static final int BETWEEN=5; + public static final int COLON=6; + public static final int COMMA=7; + public static final int EQUALS=8; + public static final int EXISTS=9; + public static final int GREATERTHAN=10; + public static final int GREATERTHANOREQUALS=11; + public static final int IDENTIFIER=12; + public static final int IDENTIFIERDIGIT=13; + public static final int IDENTIFIERLETTER=14; + public static final int IDENTIFIERLETTERORDIGIT=15; + public static final int IN=16; + public static final int LEFTPAREN=17; + public static final int LESSTHAN=18; + public static final int LESSTHANOREQUALS=19; + public static final int MATCHES=20; + public static final int NEGATION=21; + public static final int OR=22; + public static final int PROPERTYNAME=23; + public static final int PROPERTYVALUE=24; + public static final int RIGHTPAREN=25; + public static final int SINGLEQUOTE=26; + public static final int WS=27; + + + @Override + public void recover(RecognitionException e) + { + throw new InvalidQueryException(WhereCompiler.resolveMessage(e)); + } + + + // delegates + // delegators + public Lexer[] getDelegates() { + return new Lexer[] {}; + } + + public WhereClauseLexer() {} + public WhereClauseLexer(CharStream input) { + this(input, new RecognizerSharedState()); + } + public WhereClauseLexer(CharStream input, RecognizerSharedState state) { + super(input,state); + } + public String getGrammarFileName() { return "org/alfresco/rest/antlr/WhereClause.g"; } + + // $ANTLR start "NEGATION" + public final void mNEGATION() throws RecognitionException { + try { + int _type = NEGATION; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:72:9: ( ( 'not' | 'NOT' ) WS ) + // org/alfresco/rest/antlr/WhereClause.g:72:11: ( 'not' | 'NOT' ) WS + { + // org/alfresco/rest/antlr/WhereClause.g:72:11: ( 'not' | 'NOT' ) + int alt1=2; + int LA1_0 = input.LA(1); + + if ( (LA1_0=='n') ) { + alt1=1; + } + else if ( (LA1_0=='N') ) { + alt1=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 1, 0, input); + + throw nvae; + + } + switch (alt1) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:72:12: 'not' + { + match("not"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:72:18: 'NOT' + { + match("NOT"); + + + + } + break; + + } + + + mWS(); + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "NEGATION" + + // $ANTLR start "EXISTS" + public final void mEXISTS() throws RecognitionException { + try { + int _type = EXISTS; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:73:7: ( 'exists' | 'EXISTS' ) + int alt2=2; + int LA2_0 = input.LA(1); + + if ( (LA2_0=='e') ) { + alt2=1; + } + else if ( (LA2_0=='E') ) { + alt2=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 2, 0, input); + + throw nvae; + + } + switch (alt2) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:73:9: 'exists' + { + match("exists"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:73:18: 'EXISTS' + { + match("EXISTS"); + + + + } + break; + + } + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "EXISTS" + + // $ANTLR start "IN" + public final void mIN() throws RecognitionException { + try { + int _type = IN; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:74:3: ( WS ( 'in' | 'IN' ) ) + // org/alfresco/rest/antlr/WhereClause.g:74:5: WS ( 'in' | 'IN' ) + { + mWS(); + + + // org/alfresco/rest/antlr/WhereClause.g:74:7: ( 'in' | 'IN' ) + int alt3=2; + int LA3_0 = input.LA(1); + + if ( (LA3_0=='i') ) { + alt3=1; + } + else if ( (LA3_0=='I') ) { + alt3=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 3, 0, input); + + throw nvae; + + } + switch (alt3) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:74:8: 'in' + { + match("in"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:74:13: 'IN' + { + match("IN"); + + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "IN" + + // $ANTLR start "MATCHES" + public final void mMATCHES() throws RecognitionException { + try { + int _type = MATCHES; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:75:8: ( WS ( 'matches' | 'MATCHES' ) ) + // org/alfresco/rest/antlr/WhereClause.g:75:10: WS ( 'matches' | 'MATCHES' ) + { + mWS(); + + + // org/alfresco/rest/antlr/WhereClause.g:75:12: ( 'matches' | 'MATCHES' ) + int alt4=2; + int LA4_0 = input.LA(1); + + if ( (LA4_0=='m') ) { + alt4=1; + } + else if ( (LA4_0=='M') ) { + alt4=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 4, 0, input); + + throw nvae; + + } + switch (alt4) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:75:13: 'matches' + { + match("matches"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:75:23: 'MATCHES' + { + match("MATCHES"); + + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "MATCHES" + + // $ANTLR start "BETWEEN" + public final void mBETWEEN() throws RecognitionException { + try { + int _type = BETWEEN; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:76:8: ( WS ( 'between' | 'BETWEEN' ) ) + // org/alfresco/rest/antlr/WhereClause.g:76:10: WS ( 'between' | 'BETWEEN' ) + { + mWS(); + + + // org/alfresco/rest/antlr/WhereClause.g:76:12: ( 'between' | 'BETWEEN' ) + int alt5=2; + int LA5_0 = input.LA(1); + + if ( (LA5_0=='b') ) { + alt5=1; + } + else if ( (LA5_0=='B') ) { + alt5=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 5, 0, input); + + throw nvae; + + } + switch (alt5) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:76:13: 'between' + { + match("between"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:76:23: 'BETWEEN' + { + match("BETWEEN"); + + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "BETWEEN" + + // $ANTLR start "OR" + public final void mOR() throws RecognitionException { + try { + int _type = OR; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:77:3: ( WS ( 'or' | 'OR' ) WS ) + // org/alfresco/rest/antlr/WhereClause.g:77:5: WS ( 'or' | 'OR' ) WS + { + mWS(); + + + // org/alfresco/rest/antlr/WhereClause.g:77:7: ( 'or' | 'OR' ) + int alt6=2; + int LA6_0 = input.LA(1); + + if ( (LA6_0=='o') ) { + alt6=1; + } + else if ( (LA6_0=='O') ) { + alt6=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 6, 0, input); + + throw nvae; + + } + switch (alt6) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:77:8: 'or' + { + match("or"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:77:13: 'OR' + { + match("OR"); + + + + } + break; + + } + + + mWS(); + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "OR" + + // $ANTLR start "AND" + public final void mAND() throws RecognitionException { + try { + int _type = AND; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:78:4: ( WS ( 'and' | 'AND' ) WS ) + // org/alfresco/rest/antlr/WhereClause.g:78:6: WS ( 'and' | 'AND' ) WS + { + mWS(); + + + // org/alfresco/rest/antlr/WhereClause.g:78:8: ( 'and' | 'AND' ) + int alt7=2; + int LA7_0 = input.LA(1); + + if ( (LA7_0=='a') ) { + alt7=1; + } + else if ( (LA7_0=='A') ) { + alt7=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 7, 0, input); + + throw nvae; + + } + switch (alt7) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:78:9: 'and' + { + match("and"); + + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:78:15: 'AND' + { + match("AND"); + + + + } + break; + + } + + + mWS(); + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "AND" + + // $ANTLR start "EQUALS" + public final void mEQUALS() throws RecognitionException { + try { + int _type = EQUALS; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:79:7: ( ( WS )? '=' ( WS )? ) + // org/alfresco/rest/antlr/WhereClause.g:79:9: ( WS )? '=' ( WS )? + { + // org/alfresco/rest/antlr/WhereClause.g:79:9: ( WS )? + int alt8=2; + int LA8_0 = input.LA(1); + + if ( ((LA8_0 >= '\t' && LA8_0 <= '\n')||LA8_0=='\r'||LA8_0==' ') ) { + alt8=1; + } + switch (alt8) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:79:9: WS + { + mWS(); + + + } + break; + + } + + + match('='); + + // org/alfresco/rest/antlr/WhereClause.g:79:15: ( WS )? + int alt9=2; + int LA9_0 = input.LA(1); + + if ( ((LA9_0 >= '\t' && LA9_0 <= '\n')||LA9_0=='\r'||LA9_0==' ') ) { + alt9=1; + } + switch (alt9) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:79:15: WS + { + mWS(); + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "EQUALS" + + // $ANTLR start "LESSTHAN" + public final void mLESSTHAN() throws RecognitionException { + try { + int _type = LESSTHAN; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:80:9: ( ( WS )? '<' ( WS )? ) + // org/alfresco/rest/antlr/WhereClause.g:80:11: ( WS )? '<' ( WS )? + { + // org/alfresco/rest/antlr/WhereClause.g:80:11: ( WS )? + int alt10=2; + int LA10_0 = input.LA(1); + + if ( ((LA10_0 >= '\t' && LA10_0 <= '\n')||LA10_0=='\r'||LA10_0==' ') ) { + alt10=1; + } + switch (alt10) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:80:11: WS + { + mWS(); + + + } + break; + + } + + + match('<'); + + // org/alfresco/rest/antlr/WhereClause.g:80:17: ( WS )? + int alt11=2; + int LA11_0 = input.LA(1); + + if ( ((LA11_0 >= '\t' && LA11_0 <= '\n')||LA11_0=='\r'||LA11_0==' ') ) { + alt11=1; + } + switch (alt11) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:80:17: WS + { + mWS(); + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "LESSTHAN" + + // $ANTLR start "GREATERTHAN" + public final void mGREATERTHAN() throws RecognitionException { + try { + int _type = GREATERTHAN; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:81:12: ( ( WS )? '>' ( WS )? ) + // org/alfresco/rest/antlr/WhereClause.g:81:14: ( WS )? '>' ( WS )? + { + // org/alfresco/rest/antlr/WhereClause.g:81:14: ( WS )? + int alt12=2; + int LA12_0 = input.LA(1); + + if ( ((LA12_0 >= '\t' && LA12_0 <= '\n')||LA12_0=='\r'||LA12_0==' ') ) { + alt12=1; + } + switch (alt12) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:81:14: WS + { + mWS(); + + + } + break; + + } + + + match('>'); + + // org/alfresco/rest/antlr/WhereClause.g:81:20: ( WS )? + int alt13=2; + int LA13_0 = input.LA(1); + + if ( ((LA13_0 >= '\t' && LA13_0 <= '\n')||LA13_0=='\r'||LA13_0==' ') ) { + alt13=1; + } + switch (alt13) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:81:20: WS + { + mWS(); + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "GREATERTHAN" + + // $ANTLR start "LESSTHANOREQUALS" + public final void mLESSTHANOREQUALS() throws RecognitionException { + try { + int _type = LESSTHANOREQUALS; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:82:17: ( ( WS )? '<=' ( WS )? ) + // org/alfresco/rest/antlr/WhereClause.g:82:19: ( WS )? '<=' ( WS )? + { + // org/alfresco/rest/antlr/WhereClause.g:82:19: ( WS )? + int alt14=2; + int LA14_0 = input.LA(1); + + if ( ((LA14_0 >= '\t' && LA14_0 <= '\n')||LA14_0=='\r'||LA14_0==' ') ) { + alt14=1; + } + switch (alt14) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:82:19: WS + { + mWS(); + + + } + break; + + } + + + match("<="); + + + + // org/alfresco/rest/antlr/WhereClause.g:82:26: ( WS )? + int alt15=2; + int LA15_0 = input.LA(1); + + if ( ((LA15_0 >= '\t' && LA15_0 <= '\n')||LA15_0=='\r'||LA15_0==' ') ) { + alt15=1; + } + switch (alt15) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:82:26: WS + { + mWS(); + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "LESSTHANOREQUALS" + + // $ANTLR start "GREATERTHANOREQUALS" + public final void mGREATERTHANOREQUALS() throws RecognitionException { + try { + int _type = GREATERTHANOREQUALS; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:83:20: ( ( WS )? '>=' ( WS )? ) + // org/alfresco/rest/antlr/WhereClause.g:83:22: ( WS )? '>=' ( WS )? + { + // org/alfresco/rest/antlr/WhereClause.g:83:22: ( WS )? + int alt16=2; + int LA16_0 = input.LA(1); + + if ( ((LA16_0 >= '\t' && LA16_0 <= '\n')||LA16_0=='\r'||LA16_0==' ') ) { + alt16=1; + } + switch (alt16) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:83:22: WS + { + mWS(); + + + } + break; + + } + + + match(">="); + + + + // org/alfresco/rest/antlr/WhereClause.g:83:29: ( WS )? + int alt17=2; + int LA17_0 = input.LA(1); + + if ( ((LA17_0 >= '\t' && LA17_0 <= '\n')||LA17_0=='\r'||LA17_0==' ') ) { + alt17=1; + } + switch (alt17) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:83:29: WS + { + mWS(); + + + } + break; + + } + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "GREATERTHANOREQUALS" + + // $ANTLR start "LEFTPAREN" + public final void mLEFTPAREN() throws RecognitionException { + try { + int _type = LEFTPAREN; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:84:10: ( '(' ) + // org/alfresco/rest/antlr/WhereClause.g:84:12: '(' + { + match('('); + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "LEFTPAREN" + + // $ANTLR start "RIGHTPAREN" + public final void mRIGHTPAREN() throws RecognitionException { + try { + int _type = RIGHTPAREN; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:85:11: ( ')' ) + // org/alfresco/rest/antlr/WhereClause.g:85:13: ')' + { + match(')'); + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "RIGHTPAREN" + + // $ANTLR start "COMMA" + public final void mCOMMA() throws RecognitionException { + try { + int _type = COMMA; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:86:6: ( ',' ) + // org/alfresco/rest/antlr/WhereClause.g:86:8: ',' + { + match(','); + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "COMMA" + + // $ANTLR start "COLON" + public final void mCOLON() throws RecognitionException { + try { + int _type = COLON; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:87:6: ( ':' ) + // org/alfresco/rest/antlr/WhereClause.g:87:8: ':' + { + match(':'); + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "COLON" + + // $ANTLR start "SINGLEQUOTE" + public final void mSINGLEQUOTE() throws RecognitionException { + try { + int _type = SINGLEQUOTE; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:88:12: ( '\\'' ) + // org/alfresco/rest/antlr/WhereClause.g:88:14: '\\'' + { + match('\''); + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "SINGLEQUOTE" + + // $ANTLR start "PROPERTYVALUE" + public final void mPROPERTYVALUE() throws RecognitionException { + try { + int _type = PROPERTYVALUE; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:89:14: ( ( SINGLEQUOTE (~ SINGLEQUOTE | '\\\\' SINGLEQUOTE )* SINGLEQUOTE ) | ( IDENTIFIERDIGIT )+ ) + int alt20=2; + int LA20_0 = input.LA(1); + + if ( (LA20_0=='\'') ) { + alt20=1; + } + else if ( ((LA20_0 >= '0' && LA20_0 <= '9')||(LA20_0 >= '\u0660' && LA20_0 <= '\u0669')||(LA20_0 >= '\u06F0' && LA20_0 <= '\u06F9')||(LA20_0 >= '\u0966' && LA20_0 <= '\u096F')||(LA20_0 >= '\u09E6' && LA20_0 <= '\u09EF')||(LA20_0 >= '\u0A66' && LA20_0 <= '\u0A6F')||(LA20_0 >= '\u0AE6' && LA20_0 <= '\u0AEF')||(LA20_0 >= '\u0B66' && LA20_0 <= '\u0B6F')||(LA20_0 >= '\u0BE7' && LA20_0 <= '\u0BEF')||(LA20_0 >= '\u0C66' && LA20_0 <= '\u0C6F')||(LA20_0 >= '\u0CE6' && LA20_0 <= '\u0CEF')||(LA20_0 >= '\u0D66' && LA20_0 <= '\u0D6F')||(LA20_0 >= '\u0E50' && LA20_0 <= '\u0E59')||(LA20_0 >= '\u0ED0' && LA20_0 <= '\u0ED9')||(LA20_0 >= '\u1040' && LA20_0 <= '\u1049')) ) { + alt20=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 20, 0, input); + + throw nvae; + + } + switch (alt20) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:89:16: ( SINGLEQUOTE (~ SINGLEQUOTE | '\\\\' SINGLEQUOTE )* SINGLEQUOTE ) + { + // org/alfresco/rest/antlr/WhereClause.g:89:16: ( SINGLEQUOTE (~ SINGLEQUOTE | '\\\\' SINGLEQUOTE )* SINGLEQUOTE ) + // org/alfresco/rest/antlr/WhereClause.g:89:17: SINGLEQUOTE (~ SINGLEQUOTE | '\\\\' SINGLEQUOTE )* SINGLEQUOTE + { + mSINGLEQUOTE(); + + + // org/alfresco/rest/antlr/WhereClause.g:89:29: (~ SINGLEQUOTE | '\\\\' SINGLEQUOTE )* + loop18: + do { + int alt18=3; + int LA18_0 = input.LA(1); + + if ( (LA18_0=='\\') ) { + int LA18_2 = input.LA(2); + + if ( (LA18_2=='\'') ) { + int LA18_4 = input.LA(3); + + if ( ((LA18_4 >= '\u0000' && LA18_4 <= '\uFFFF')) ) { + alt18=2; + } + + else { + alt18=1; + } + + + } + else if ( ((LA18_2 >= '\u0000' && LA18_2 <= '&')||(LA18_2 >= '(' && LA18_2 <= '\uFFFF')) ) { + alt18=1; + } + + + } + else if ( ((LA18_0 >= '\u0000' && LA18_0 <= '&')||(LA18_0 >= '(' && LA18_0 <= '[')||(LA18_0 >= ']' && LA18_0 <= '\uFFFF')) ) { + alt18=1; + } + + + switch (alt18) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:89:30: ~ SINGLEQUOTE + { + if ( (input.LA(1) >= '\u0000' && input.LA(1) <= '\u0019')||(input.LA(1) >= '\u001B' && input.LA(1) <= '\uFFFF') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:89:43: '\\\\' SINGLEQUOTE + { + match('\\'); + + mSINGLEQUOTE(); + + + } + break; + + default : + break loop18; + } + } while (true); + + + mSINGLEQUOTE(); + + + } + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:89:75: ( IDENTIFIERDIGIT )+ + { + // org/alfresco/rest/antlr/WhereClause.g:89:75: ( IDENTIFIERDIGIT )+ + int cnt19=0; + loop19: + do { + int alt19=2; + int LA19_0 = input.LA(1); + + if ( ((LA19_0 >= '0' && LA19_0 <= '9')||(LA19_0 >= '\u0660' && LA19_0 <= '\u0669')||(LA19_0 >= '\u06F0' && LA19_0 <= '\u06F9')||(LA19_0 >= '\u0966' && LA19_0 <= '\u096F')||(LA19_0 >= '\u09E6' && LA19_0 <= '\u09EF')||(LA19_0 >= '\u0A66' && LA19_0 <= '\u0A6F')||(LA19_0 >= '\u0AE6' && LA19_0 <= '\u0AEF')||(LA19_0 >= '\u0B66' && LA19_0 <= '\u0B6F')||(LA19_0 >= '\u0BE7' && LA19_0 <= '\u0BEF')||(LA19_0 >= '\u0C66' && LA19_0 <= '\u0C6F')||(LA19_0 >= '\u0CE6' && LA19_0 <= '\u0CEF')||(LA19_0 >= '\u0D66' && LA19_0 <= '\u0D6F')||(LA19_0 >= '\u0E50' && LA19_0 <= '\u0E59')||(LA19_0 >= '\u0ED0' && LA19_0 <= '\u0ED9')||(LA19_0 >= '\u1040' && LA19_0 <= '\u1049')) ) { + alt19=1; + } + + + switch (alt19) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '0' && input.LA(1) <= '9')||(input.LA(1) >= '\u0660' && input.LA(1) <= '\u0669')||(input.LA(1) >= '\u06F0' && input.LA(1) <= '\u06F9')||(input.LA(1) >= '\u0966' && input.LA(1) <= '\u096F')||(input.LA(1) >= '\u09E6' && input.LA(1) <= '\u09EF')||(input.LA(1) >= '\u0A66' && input.LA(1) <= '\u0A6F')||(input.LA(1) >= '\u0AE6' && input.LA(1) <= '\u0AEF')||(input.LA(1) >= '\u0B66' && input.LA(1) <= '\u0B6F')||(input.LA(1) >= '\u0BE7' && input.LA(1) <= '\u0BEF')||(input.LA(1) >= '\u0C66' && input.LA(1) <= '\u0C6F')||(input.LA(1) >= '\u0CE6' && input.LA(1) <= '\u0CEF')||(input.LA(1) >= '\u0D66' && input.LA(1) <= '\u0D6F')||(input.LA(1) >= '\u0E50' && input.LA(1) <= '\u0E59')||(input.LA(1) >= '\u0ED0' && input.LA(1) <= '\u0ED9')||(input.LA(1) >= '\u1040' && input.LA(1) <= '\u1049') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + break; + + default : + if ( cnt19 >= 1 ) break loop19; + EarlyExitException eee = + new EarlyExitException(19, input); + throw eee; + } + cnt19++; + } while (true); + + + } + break; + + } + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "PROPERTYVALUE" + + // $ANTLR start "PROPERTYNAME" + public final void mPROPERTYNAME() throws RecognitionException { + try { + int _type = PROPERTYNAME; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:90:13: ( ( '/' )? IDENTIFIER ( '/' IDENTIFIER )* ) + // org/alfresco/rest/antlr/WhereClause.g:90:15: ( '/' )? IDENTIFIER ( '/' IDENTIFIER )* + { + // org/alfresco/rest/antlr/WhereClause.g:90:15: ( '/' )? + int alt21=2; + int LA21_0 = input.LA(1); + + if ( (LA21_0=='/') ) { + alt21=1; + } + switch (alt21) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:90:15: '/' + { + match('/'); + + } + break; + + } + + + mIDENTIFIER(); + + + // org/alfresco/rest/antlr/WhereClause.g:90:31: ( '/' IDENTIFIER )* + loop22: + do { + int alt22=2; + int LA22_0 = input.LA(1); + + if ( (LA22_0=='/') ) { + alt22=1; + } + + + switch (alt22) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:90:32: '/' IDENTIFIER + { + match('/'); + + mIDENTIFIER(); + + + } + break; + + default : + break loop22; + } + } while (true); + + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "PROPERTYNAME" + + // $ANTLR start "IDENTIFIERLETTERORDIGIT" + public final void mIDENTIFIERLETTERORDIGIT() throws RecognitionException { + try { + // org/alfresco/rest/antlr/WhereClause.g:91:33: ( ( IDENTIFIERLETTER | IDENTIFIERDIGIT ) ) + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '0' && input.LA(1) <= '9')||(input.LA(1) >= 'A' && input.LA(1) <= 'Z')||input.LA(1)=='_'||(input.LA(1) >= 'a' && input.LA(1) <= 'z')||(input.LA(1) >= '\u00C0' && input.LA(1) <= '\u00D6')||(input.LA(1) >= '\u00D8' && input.LA(1) <= '\u00F6')||(input.LA(1) >= '\u00F8' && input.LA(1) <= '\u1FFF')||(input.LA(1) >= '\u3040' && input.LA(1) <= '\u318F')||(input.LA(1) >= '\u3300' && input.LA(1) <= '\u337F')||(input.LA(1) >= '\u3400' && input.LA(1) <= '\u3D2D')||(input.LA(1) >= '\u4E00' && input.LA(1) <= '\u9FFF')||(input.LA(1) >= '\uF900' && input.LA(1) <= '\uFAFF') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + + + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "IDENTIFIERLETTERORDIGIT" + + // $ANTLR start "IDENTIFIER" + public final void mIDENTIFIER() throws RecognitionException { + try { + // org/alfresco/rest/antlr/WhereClause.g:92:21: ( ( IDENTIFIERLETTER ( ( IDENTIFIERLETTERORDIGIT )* | ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) ) ) ) + // org/alfresco/rest/antlr/WhereClause.g:92:23: ( IDENTIFIERLETTER ( ( IDENTIFIERLETTERORDIGIT )* | ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) ) ) + { + // org/alfresco/rest/antlr/WhereClause.g:92:23: ( IDENTIFIERLETTER ( ( IDENTIFIERLETTERORDIGIT )* | ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) ) ) + // org/alfresco/rest/antlr/WhereClause.g:92:24: IDENTIFIERLETTER ( ( IDENTIFIERLETTERORDIGIT )* | ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) ) + { + mIDENTIFIERLETTER(); + + + // org/alfresco/rest/antlr/WhereClause.g:92:41: ( ( IDENTIFIERLETTERORDIGIT )* | ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) ) + int alt26=2; + alt26 = dfa26.predict(input); + switch (alt26) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:92:42: ( IDENTIFIERLETTERORDIGIT )* + { + // org/alfresco/rest/antlr/WhereClause.g:92:42: ( IDENTIFIERLETTERORDIGIT )* + loop23: + do { + int alt23=2; + int LA23_0 = input.LA(1); + + if ( ((LA23_0 >= '0' && LA23_0 <= '9')||(LA23_0 >= 'A' && LA23_0 <= 'Z')||LA23_0=='_'||(LA23_0 >= 'a' && LA23_0 <= 'z')||(LA23_0 >= '\u00C0' && LA23_0 <= '\u00D6')||(LA23_0 >= '\u00D8' && LA23_0 <= '\u00F6')||(LA23_0 >= '\u00F8' && LA23_0 <= '\u1FFF')||(LA23_0 >= '\u3040' && LA23_0 <= '\u318F')||(LA23_0 >= '\u3300' && LA23_0 <= '\u337F')||(LA23_0 >= '\u3400' && LA23_0 <= '\u3D2D')||(LA23_0 >= '\u4E00' && LA23_0 <= '\u9FFF')||(LA23_0 >= '\uF900' && LA23_0 <= '\uFAFF')) ) { + alt23=1; + } + + + switch (alt23) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '0' && input.LA(1) <= '9')||(input.LA(1) >= 'A' && input.LA(1) <= 'Z')||input.LA(1)=='_'||(input.LA(1) >= 'a' && input.LA(1) <= 'z')||(input.LA(1) >= '\u00C0' && input.LA(1) <= '\u00D6')||(input.LA(1) >= '\u00D8' && input.LA(1) <= '\u00F6')||(input.LA(1) >= '\u00F8' && input.LA(1) <= '\u1FFF')||(input.LA(1) >= '\u3040' && input.LA(1) <= '\u318F')||(input.LA(1) >= '\u3300' && input.LA(1) <= '\u337F')||(input.LA(1) >= '\u3400' && input.LA(1) <= '\u3D2D')||(input.LA(1) >= '\u4E00' && input.LA(1) <= '\u9FFF')||(input.LA(1) >= '\uF900' && input.LA(1) <= '\uFAFF') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + break; + + default : + break loop23; + } + } while (true); + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:92:69: ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) + { + // org/alfresco/rest/antlr/WhereClause.g:92:69: ( ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* ) + // org/alfresco/rest/antlr/WhereClause.g:92:70: ( IDENTIFIERLETTERORDIGIT )* COLON ( IDENTIFIERLETTERORDIGIT )* + { + // org/alfresco/rest/antlr/WhereClause.g:92:70: ( IDENTIFIERLETTERORDIGIT )* + loop24: + do { + int alt24=2; + int LA24_0 = input.LA(1); + + if ( ((LA24_0 >= '0' && LA24_0 <= '9')||(LA24_0 >= 'A' && LA24_0 <= 'Z')||LA24_0=='_'||(LA24_0 >= 'a' && LA24_0 <= 'z')||(LA24_0 >= '\u00C0' && LA24_0 <= '\u00D6')||(LA24_0 >= '\u00D8' && LA24_0 <= '\u00F6')||(LA24_0 >= '\u00F8' && LA24_0 <= '\u1FFF')||(LA24_0 >= '\u3040' && LA24_0 <= '\u318F')||(LA24_0 >= '\u3300' && LA24_0 <= '\u337F')||(LA24_0 >= '\u3400' && LA24_0 <= '\u3D2D')||(LA24_0 >= '\u4E00' && LA24_0 <= '\u9FFF')||(LA24_0 >= '\uF900' && LA24_0 <= '\uFAFF')) ) { + alt24=1; + } + + + switch (alt24) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '0' && input.LA(1) <= '9')||(input.LA(1) >= 'A' && input.LA(1) <= 'Z')||input.LA(1)=='_'||(input.LA(1) >= 'a' && input.LA(1) <= 'z')||(input.LA(1) >= '\u00C0' && input.LA(1) <= '\u00D6')||(input.LA(1) >= '\u00D8' && input.LA(1) <= '\u00F6')||(input.LA(1) >= '\u00F8' && input.LA(1) <= '\u1FFF')||(input.LA(1) >= '\u3040' && input.LA(1) <= '\u318F')||(input.LA(1) >= '\u3300' && input.LA(1) <= '\u337F')||(input.LA(1) >= '\u3400' && input.LA(1) <= '\u3D2D')||(input.LA(1) >= '\u4E00' && input.LA(1) <= '\u9FFF')||(input.LA(1) >= '\uF900' && input.LA(1) <= '\uFAFF') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + break; + + default : + break loop24; + } + } while (true); + + + mCOLON(); + + + // org/alfresco/rest/antlr/WhereClause.g:92:101: ( IDENTIFIERLETTERORDIGIT )* + loop25: + do { + int alt25=2; + int LA25_0 = input.LA(1); + + if ( ((LA25_0 >= '0' && LA25_0 <= '9')||(LA25_0 >= 'A' && LA25_0 <= 'Z')||LA25_0=='_'||(LA25_0 >= 'a' && LA25_0 <= 'z')||(LA25_0 >= '\u00C0' && LA25_0 <= '\u00D6')||(LA25_0 >= '\u00D8' && LA25_0 <= '\u00F6')||(LA25_0 >= '\u00F8' && LA25_0 <= '\u1FFF')||(LA25_0 >= '\u3040' && LA25_0 <= '\u318F')||(LA25_0 >= '\u3300' && LA25_0 <= '\u337F')||(LA25_0 >= '\u3400' && LA25_0 <= '\u3D2D')||(LA25_0 >= '\u4E00' && LA25_0 <= '\u9FFF')||(LA25_0 >= '\uF900' && LA25_0 <= '\uFAFF')) ) { + alt25=1; + } + + + switch (alt25) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '0' && input.LA(1) <= '9')||(input.LA(1) >= 'A' && input.LA(1) <= 'Z')||input.LA(1)=='_'||(input.LA(1) >= 'a' && input.LA(1) <= 'z')||(input.LA(1) >= '\u00C0' && input.LA(1) <= '\u00D6')||(input.LA(1) >= '\u00D8' && input.LA(1) <= '\u00F6')||(input.LA(1) >= '\u00F8' && input.LA(1) <= '\u1FFF')||(input.LA(1) >= '\u3040' && input.LA(1) <= '\u318F')||(input.LA(1) >= '\u3300' && input.LA(1) <= '\u337F')||(input.LA(1) >= '\u3400' && input.LA(1) <= '\u3D2D')||(input.LA(1) >= '\u4E00' && input.LA(1) <= '\u9FFF')||(input.LA(1) >= '\uF900' && input.LA(1) <= '\uFAFF') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + break; + + default : + break loop25; + } + } while (true); + + + } + + + } + break; + + } + + + } + + + } + + + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "IDENTIFIER" + + // $ANTLR start "WS" + public final void mWS() throws RecognitionException { + try { + int _type = WS; + int _channel = DEFAULT_TOKEN_CHANNEL; + // org/alfresco/rest/antlr/WhereClause.g:94:4: ( ( ' ' | '\\t' | '\\r' | '\\n' )+ ) + // org/alfresco/rest/antlr/WhereClause.g:94:6: ( ' ' | '\\t' | '\\r' | '\\n' )+ + { + // org/alfresco/rest/antlr/WhereClause.g:94:6: ( ' ' | '\\t' | '\\r' | '\\n' )+ + int cnt27=0; + loop27: + do { + int alt27=2; + int LA27_0 = input.LA(1); + + if ( ((LA27_0 >= '\t' && LA27_0 <= '\n')||LA27_0=='\r'||LA27_0==' ') ) { + alt27=1; + } + + + switch (alt27) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '\t' && input.LA(1) <= '\n')||input.LA(1)=='\r'||input.LA(1)==' ' ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + break; + + default : + if ( cnt27 >= 1 ) break loop27; + EarlyExitException eee = + new EarlyExitException(27, input); + throw eee; + } + cnt27++; + } while (true); + + + _channel = HIDDEN; + + } + + state.type = _type; + state.channel = _channel; + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "WS" + + // $ANTLR start "IDENTIFIERLETTER" + public final void mIDENTIFIERLETTER() throws RecognitionException { + try { + // org/alfresco/rest/antlr/WhereClause.g:96:5: ( '\\u0041' .. '\\u005a' | '\\u005f' | '\\u0061' .. '\\u007a' | '\\u00c0' .. '\\u00d6' | '\\u00d8' .. '\\u00f6' | '\\u00f8' .. '\\u00ff' | '\\u0100' .. '\\u1fff' | '\\u3040' .. '\\u318f' | '\\u3300' .. '\\u337f' | '\\u3400' .. '\\u3d2d' | '\\u4e00' .. '\\u9fff' | '\\uf900' .. '\\ufaff' ) + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= 'A' && input.LA(1) <= 'Z')||input.LA(1)=='_'||(input.LA(1) >= 'a' && input.LA(1) <= 'z')||(input.LA(1) >= '\u00C0' && input.LA(1) <= '\u00D6')||(input.LA(1) >= '\u00D8' && input.LA(1) <= '\u00F6')||(input.LA(1) >= '\u00F8' && input.LA(1) <= '\u1FFF')||(input.LA(1) >= '\u3040' && input.LA(1) <= '\u318F')||(input.LA(1) >= '\u3300' && input.LA(1) <= '\u337F')||(input.LA(1) >= '\u3400' && input.LA(1) <= '\u3D2D')||(input.LA(1) >= '\u4E00' && input.LA(1) <= '\u9FFF')||(input.LA(1) >= '\uF900' && input.LA(1) <= '\uFAFF') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + + + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "IDENTIFIERLETTER" + + // $ANTLR start "IDENTIFIERDIGIT" + public final void mIDENTIFIERDIGIT() throws RecognitionException { + try { + // org/alfresco/rest/antlr/WhereClause.g:110:5: ( '\\u0030' .. '\\u0039' | '\\u0660' .. '\\u0669' | '\\u06f0' .. '\\u06f9' | '\\u0966' .. '\\u096f' | '\\u09e6' .. '\\u09ef' | '\\u0a66' .. '\\u0a6f' | '\\u0ae6' .. '\\u0aef' | '\\u0b66' .. '\\u0b6f' | '\\u0be7' .. '\\u0bef' | '\\u0c66' .. '\\u0c6f' | '\\u0ce6' .. '\\u0cef' | '\\u0d66' .. '\\u0d6f' | '\\u0e50' .. '\\u0e59' | '\\u0ed0' .. '\\u0ed9' | '\\u1040' .. '\\u1049' ) + // org/alfresco/rest/antlr/WhereClause.g: + { + if ( (input.LA(1) >= '0' && input.LA(1) <= '9')||(input.LA(1) >= '\u0660' && input.LA(1) <= '\u0669')||(input.LA(1) >= '\u06F0' && input.LA(1) <= '\u06F9')||(input.LA(1) >= '\u0966' && input.LA(1) <= '\u096F')||(input.LA(1) >= '\u09E6' && input.LA(1) <= '\u09EF')||(input.LA(1) >= '\u0A66' && input.LA(1) <= '\u0A6F')||(input.LA(1) >= '\u0AE6' && input.LA(1) <= '\u0AEF')||(input.LA(1) >= '\u0B66' && input.LA(1) <= '\u0B6F')||(input.LA(1) >= '\u0BE7' && input.LA(1) <= '\u0BEF')||(input.LA(1) >= '\u0C66' && input.LA(1) <= '\u0C6F')||(input.LA(1) >= '\u0CE6' && input.LA(1) <= '\u0CEF')||(input.LA(1) >= '\u0D66' && input.LA(1) <= '\u0D6F')||(input.LA(1) >= '\u0E50' && input.LA(1) <= '\u0E59')||(input.LA(1) >= '\u0ED0' && input.LA(1) <= '\u0ED9')||(input.LA(1) >= '\u1040' && input.LA(1) <= '\u1049') ) { + input.consume(); + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + recover(mse); + throw mse; + } + + + } + + + } + finally { + // do for sure before leaving + } + } + // $ANTLR end "IDENTIFIERDIGIT" + + public void mTokens() throws RecognitionException { + // org/alfresco/rest/antlr/WhereClause.g:1:8: ( NEGATION | EXISTS | IN | MATCHES | BETWEEN | OR | AND | EQUALS | LESSTHAN | GREATERTHAN | LESSTHANOREQUALS | GREATERTHANOREQUALS | LEFTPAREN | RIGHTPAREN | COMMA | COLON | SINGLEQUOTE | PROPERTYVALUE | PROPERTYNAME | WS ) + int alt28=20; + alt28 = dfa28.predict(input); + switch (alt28) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:1:10: NEGATION + { + mNEGATION(); + + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:1:19: EXISTS + { + mEXISTS(); + + + } + break; + case 3 : + // org/alfresco/rest/antlr/WhereClause.g:1:26: IN + { + mIN(); + + + } + break; + case 4 : + // org/alfresco/rest/antlr/WhereClause.g:1:29: MATCHES + { + mMATCHES(); + + + } + break; + case 5 : + // org/alfresco/rest/antlr/WhereClause.g:1:37: BETWEEN + { + mBETWEEN(); + + + } + break; + case 6 : + // org/alfresco/rest/antlr/WhereClause.g:1:45: OR + { + mOR(); + + + } + break; + case 7 : + // org/alfresco/rest/antlr/WhereClause.g:1:48: AND + { + mAND(); + + + } + break; + case 8 : + // org/alfresco/rest/antlr/WhereClause.g:1:52: EQUALS + { + mEQUALS(); + + + } + break; + case 9 : + // org/alfresco/rest/antlr/WhereClause.g:1:59: LESSTHAN + { + mLESSTHAN(); + + + } + break; + case 10 : + // org/alfresco/rest/antlr/WhereClause.g:1:68: GREATERTHAN + { + mGREATERTHAN(); + + + } + break; + case 11 : + // org/alfresco/rest/antlr/WhereClause.g:1:80: LESSTHANOREQUALS + { + mLESSTHANOREQUALS(); + + + } + break; + case 12 : + // org/alfresco/rest/antlr/WhereClause.g:1:97: GREATERTHANOREQUALS + { + mGREATERTHANOREQUALS(); + + + } + break; + case 13 : + // org/alfresco/rest/antlr/WhereClause.g:1:117: LEFTPAREN + { + mLEFTPAREN(); + + + } + break; + case 14 : + // org/alfresco/rest/antlr/WhereClause.g:1:127: RIGHTPAREN + { + mRIGHTPAREN(); + + + } + break; + case 15 : + // org/alfresco/rest/antlr/WhereClause.g:1:138: COMMA + { + mCOMMA(); + + + } + break; + case 16 : + // org/alfresco/rest/antlr/WhereClause.g:1:144: COLON + { + mCOLON(); + + + } + break; + case 17 : + // org/alfresco/rest/antlr/WhereClause.g:1:150: SINGLEQUOTE + { + mSINGLEQUOTE(); + + + } + break; + case 18 : + // org/alfresco/rest/antlr/WhereClause.g:1:162: PROPERTYVALUE + { + mPROPERTYVALUE(); + + + } + break; + case 19 : + // org/alfresco/rest/antlr/WhereClause.g:1:176: PROPERTYNAME + { + mPROPERTYNAME(); + + + } + break; + case 20 : + // org/alfresco/rest/antlr/WhereClause.g:1:189: WS + { + mWS(); + + + } + break; + + } + + } + + + protected DFA26 dfa26 = new DFA26(this); + protected DFA28 dfa28 = new DFA28(this); + static final String DFA26_eotS = + "\2\2\2\uffff"; + static final String DFA26_eofS = + "\4\uffff"; + static final String DFA26_minS = + "\2\60\2\uffff"; + static final String DFA26_maxS = + "\2\ufaff\2\uffff"; + static final String DFA26_acceptS = + "\2\uffff\1\1\1\2"; + static final String DFA26_specialS = + "\4\uffff}>"; + static final String[] DFA26_transitionS = { + "\12\1\1\3\6\uffff\32\1\4\uffff\1\1\1\uffff\32\1\105\uffff\27"+ + "\1\1\uffff\37\1\1\uffff\u1f08\1\u1040\uffff\u0150\1\u0170\uffff"+ + "\u0080\1\u0080\uffff\u092e\1\u10d2\uffff\u5200\1\u5900\uffff"+ + "\u0200\1", + "\12\1\1\3\6\uffff\32\1\4\uffff\1\1\1\uffff\32\1\105\uffff\27"+ + "\1\1\uffff\37\1\1\uffff\u1f08\1\u1040\uffff\u0150\1\u0170\uffff"+ + "\u0080\1\u0080\uffff\u092e\1\u10d2\uffff\u5200\1\u5900\uffff"+ + "\u0200\1", + "", + "" + }; + + static final short[] DFA26_eot = DFA.unpackEncodedString(DFA26_eotS); + static final short[] DFA26_eof = DFA.unpackEncodedString(DFA26_eofS); + static final char[] DFA26_min = DFA.unpackEncodedStringToUnsignedChars(DFA26_minS); + static final char[] DFA26_max = DFA.unpackEncodedStringToUnsignedChars(DFA26_maxS); + static final short[] DFA26_accept = DFA.unpackEncodedString(DFA26_acceptS); + static final short[] DFA26_special = DFA.unpackEncodedString(DFA26_specialS); + static final short[][] DFA26_transition; + + static { + int numStates = DFA26_transitionS.length; + DFA26_transition = new short[numStates][]; + for (int i=0; i"; + static final String[] DFA28_transitionS = { + "\2\5\2\uffff\1\5\22\uffff\1\5\6\uffff\1\15\1\11\1\12\2\uffff"+ + "\1\13\2\uffff\1\17\12\20\1\14\1\uffff\1\7\1\6\1\10\2\uffff\4"+ + "\17\1\4\10\17\1\2\14\17\4\uffff\1\17\1\uffff\4\17\1\3\10\17"+ + "\1\1\14\17\105\uffff\27\17\1\uffff\37\17\1\uffff\u0568\17\12"+ + "\16\u0086\17\12\16\u026c\17\12\16\166\17\12\16\166\17\12\16"+ + "\166\17\12\16\166\17\12\16\167\17\11\16\166\17\12\16\166\17"+ + "\12\16\166\17\12\16\u00e0\17\12\16\166\17\12\16\u0166\17\12"+ + "\16\u0fb6\17\u1040\uffff\u0150\17\u0170\uffff\u0080\17\u0080"+ + "\uffff\u092e\17\u10d2\uffff\u5200\17\u5900\uffff\u0200\17", + "\1\21", + "\1\22", + "\1\23", + "\1\24", + "\2\5\2\uffff\1\5\22\uffff\1\5\33\uffff\1\7\1\6\1\10\2\uffff"+ + "\1\32\1\30\6\uffff\1\26\3\uffff\1\27\1\uffff\1\31\21\uffff\1"+ + "\32\1\30\6\uffff\1\26\3\uffff\1\27\1\uffff\1\31", + "", + "\1\33", + "\1\35", + "", + "", + "", + "", + "\0\20", + "\1\17\12\40\1\17\6\uffff\32\17\4\uffff\1\17\1\uffff\32\17\105"+ + "\uffff\27\17\1\uffff\37\17\1\uffff\u0568\17\12\40\u0086\17\12"+ + "\40\u026c\17\12\40\166\17\12\40\166\17\12\40\166\17\12\40\166"+ + "\17\12\40\167\17\11\40\166\17\12\40\166\17\12\40\166\17\12\40"+ + "\u00e0\17\12\40\166\17\12\40\u0166\17\12\40\u0fb6\17\u1040\uffff"+ + "\u0150\17\u0170\uffff\u0080\17\u0080\uffff\u092e\17\u10d2\uffff"+ + "\u5200\17\u5900\uffff\u0200\17", + "", + "", + "\1\41", + "\1\42", + "\1\43", + "\1\44", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "\1\17\12\40\1\17\6\uffff\32\17\4\uffff\1\17\1\uffff\32\17\105"+ + "\uffff\27\17\1\uffff\37\17\1\uffff\u0568\17\12\40\u0086\17\12"+ + "\40\u026c\17\12\40\166\17\12\40\166\17\12\40\166\17\12\40\166"+ + "\17\12\40\167\17\11\40\166\17\12\40\166\17\12\40\166\17\12\40"+ + "\u00e0\17\12\40\166\17\12\40\u0166\17\12\40\u0fb6\17\u1040\uffff"+ + "\u0150\17\u0170\uffff\u0080\17\u0080\uffff\u092e\17\u10d2\uffff"+ + "\u5200\17\u5900\uffff\u0200\17", + "\2\45\2\uffff\1\45\22\uffff\1\45", + "\2\45\2\uffff\1\45\22\uffff\1\45", + "\1\46", + "\1\47", + "", + "\1\50", + "\1\51", + "\1\52", + "\1\53", + "\14\17\6\uffff\32\17\4\uffff\1\17\1\uffff\32\17\105\uffff\27"+ + "\17\1\uffff\37\17\1\uffff\u1f08\17\u1040\uffff\u0150\17\u0170"+ + "\uffff\u0080\17\u0080\uffff\u092e\17\u10d2\uffff\u5200\17\u5900"+ + "\uffff\u0200\17", + "\14\17\6\uffff\32\17\4\uffff\1\17\1\uffff\32\17\105\uffff\27"+ + "\17\1\uffff\37\17\1\uffff\u1f08\17\u1040\uffff\u0150\17\u0170"+ + "\uffff\u0080\17\u0080\uffff\u092e\17\u10d2\uffff\u5200\17\u5900"+ + "\uffff\u0200\17", + "" + }; + + static final short[] DFA28_eot = DFA.unpackEncodedString(DFA28_eotS); + static final short[] DFA28_eof = DFA.unpackEncodedString(DFA28_eofS); + static final char[] DFA28_min = DFA.unpackEncodedStringToUnsignedChars(DFA28_minS); + static final char[] DFA28_max = DFA.unpackEncodedStringToUnsignedChars(DFA28_maxS); + static final short[] DFA28_accept = DFA.unpackEncodedString(DFA28_acceptS); + static final short[] DFA28_special = DFA.unpackEncodedString(DFA28_specialS); + static final short[][] DFA28_transition; + + static { + int numStates = DFA28_transitionS.length; + DFA28_transition = new short[numStates][]; + for (int i=0; i= '\u0000' && LA28_13 <= '\uFFFF')) ) {s = 16;} + + else s = 31; + + if ( s>=0 ) return s; + break; + } + NoViableAltException nvae = + new NoViableAltException(getDescription(), 28, _s, input); + error(nvae); + throw nvae; + } + + } + + +} \ No newline at end of file diff --git a/remote-api/src/main/generated/org/alfresco/rest/antlr/WhereClauseParser.java b/remote-api/src/main/generated/org/alfresco/rest/antlr/WhereClauseParser.java new file mode 100644 index 00000000000..36ec9f85bad --- /dev/null +++ b/remote-api/src/main/generated/org/alfresco/rest/antlr/WhereClauseParser.java @@ -0,0 +1,2216 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +// $ANTLR 3.4 org/alfresco/rest/antlr/WhereClause.g 2013-05-24 09:01:14 + +package org.alfresco.rest.antlr; +import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException; + + +import org.antlr.runtime.*; +import java.util.Stack; +import java.util.List; +import java.util.ArrayList; + +import org.antlr.runtime.tree.*; + + +@SuppressWarnings({"all", "warnings", "unchecked"}) +public class WhereClauseParser extends Parser { + public static final String[] tokenNames = new String[] { + "", "", "", "", "AND", "BETWEEN", "COLON", "COMMA", "EQUALS", "EXISTS", "GREATERTHAN", "GREATERTHANOREQUALS", "IDENTIFIER", "IDENTIFIERDIGIT", "IDENTIFIERLETTER", "IDENTIFIERLETTERORDIGIT", "IN", "LEFTPAREN", "LESSTHAN", "LESSTHANOREQUALS", "MATCHES", "NEGATION", "OR", "PROPERTYNAME", "PROPERTYVALUE", "RIGHTPAREN", "SINGLEQUOTE", "WS" + }; + + public static final int EOF=-1; + public static final int AND=4; + public static final int BETWEEN=5; + public static final int COLON=6; + public static final int COMMA=7; + public static final int EQUALS=8; + public static final int EXISTS=9; + public static final int GREATERTHAN=10; + public static final int GREATERTHANOREQUALS=11; + public static final int IDENTIFIER=12; + public static final int IDENTIFIERDIGIT=13; + public static final int IDENTIFIERLETTER=14; + public static final int IDENTIFIERLETTERORDIGIT=15; + public static final int IN=16; + public static final int LEFTPAREN=17; + public static final int LESSTHAN=18; + public static final int LESSTHANOREQUALS=19; + public static final int MATCHES=20; + public static final int NEGATION=21; + public static final int OR=22; + public static final int PROPERTYNAME=23; + public static final int PROPERTYVALUE=24; + public static final int RIGHTPAREN=25; + public static final int SINGLEQUOTE=26; + public static final int WS=27; + + // delegates + public Parser[] getDelegates() { + return new Parser[] {}; + } + + // delegators + + + public WhereClauseParser(TokenStream input) { + this(input, new RecognizerSharedState()); + } + public WhereClauseParser(TokenStream input, RecognizerSharedState state) { + super(input, state); + } + +protected TreeAdaptor adaptor = new CommonTreeAdaptor(); + +public void setTreeAdaptor(TreeAdaptor adaptor) { + this.adaptor = adaptor; +} +public TreeAdaptor getTreeAdaptor() { + return adaptor; +} + public String[] getTokenNames() { return WhereClauseParser.tokenNames; } + public String getGrammarFileName() { return "org/alfresco/rest/antlr/WhereClause.g"; } + + + + // These methods are here to force the parser to error instead of suppressing problems. + // @Override + // public void reportError(RecognitionException e) { + // System.out.println("CUSTOM ERROR...\n" + e); + // throw new InvalidQueryException(e.getMessage()); + // } + + @Override + protected Object recoverFromMismatchedToken(IntStream input, int ttype, BitSet follow) throws RecognitionException + { + throw new MismatchedTokenException(ttype, input); + } + + @Override + public Object recoverFromMismatchedSet(IntStream input, RecognitionException e, BitSet follow) throws RecognitionException + { + throw e; + } + // + // @Override + // public String getErrorMessage(RecognitionException e, String[] tokenNames) + // { + // System.out.println("THROW ME...\n" + e); + // throw new InvalidQueryException(e.getMessage()); + // } + // End of methods here to force the parser to error instead of supressing problems. + + + public static class whereclause_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "whereclause" + // org/alfresco/rest/antlr/WhereClause.g:128:1: whereclause : ( WS )? LEFTPAREN ! ( WS )? predicate RIGHTPAREN ! ( WS )? ; + public final WhereClauseParser.whereclause_return whereclause() throws RecognitionException { + WhereClauseParser.whereclause_return retval = new WhereClauseParser.whereclause_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token WS1=null; + Token LEFTPAREN2=null; + Token WS3=null; + Token RIGHTPAREN5=null; + Token WS6=null; + WhereClauseParser.predicate_return predicate4 =null; + + + Object WS1_tree=null; + Object LEFTPAREN2_tree=null; + Object WS3_tree=null; + Object RIGHTPAREN5_tree=null; + Object WS6_tree=null; + + try { + // org/alfresco/rest/antlr/WhereClause.g:128:13: ( ( WS )? LEFTPAREN ! ( WS )? predicate RIGHTPAREN ! ( WS )? ) + // org/alfresco/rest/antlr/WhereClause.g:128:15: ( WS )? LEFTPAREN ! ( WS )? predicate RIGHTPAREN ! ( WS )? + { + root_0 = (Object)adaptor.nil(); + + + // org/alfresco/rest/antlr/WhereClause.g:128:15: ( WS )? + int alt1=2; + int LA1_0 = input.LA(1); + + if ( (LA1_0==WS) ) { + alt1=1; + } + switch (alt1) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:128:15: WS + { + WS1=(Token)match(input,WS,FOLLOW_WS_in_whereclause779); + WS1_tree = + (Object)adaptor.create(WS1) + ; + adaptor.addChild(root_0, WS1_tree); + + + } + break; + + } + + + LEFTPAREN2=(Token)match(input,LEFTPAREN,FOLLOW_LEFTPAREN_in_whereclause782); + + // org/alfresco/rest/antlr/WhereClause.g:128:30: ( WS )? + int alt2=2; + int LA2_0 = input.LA(1); + + if ( (LA2_0==WS) ) { + alt2=1; + } + switch (alt2) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:128:30: WS + { + WS3=(Token)match(input,WS,FOLLOW_WS_in_whereclause785); + WS3_tree = + (Object)adaptor.create(WS3) + ; + adaptor.addChild(root_0, WS3_tree); + + + } + break; + + } + + + pushFollow(FOLLOW_predicate_in_whereclause788); + predicate4=predicate(); + + state._fsp--; + + adaptor.addChild(root_0, predicate4.getTree()); + + RIGHTPAREN5=(Token)match(input,RIGHTPAREN,FOLLOW_RIGHTPAREN_in_whereclause790); + + // org/alfresco/rest/antlr/WhereClause.g:128:56: ( WS )? + int alt3=2; + int LA3_0 = input.LA(1); + + if ( (LA3_0==WS) ) { + alt3=1; + } + switch (alt3) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:128:56: WS + { + WS6=(Token)match(input,WS,FOLLOW_WS_in_whereclause793); + WS6_tree = + (Object)adaptor.create(WS6) + ; + adaptor.addChild(root_0, WS6_tree); + + + } + break; + + } + + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "whereclause" + + + public static class predicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "predicate" + // org/alfresco/rest/antlr/WhereClause.g:129:1: predicate : ( simplepredicate | simplepredicate ( AND simplepredicate )+ -> ^( AND ( simplepredicate )+ ) | simplepredicate ( OR simplepredicate )+ -> ^( OR ( simplepredicate )+ ) ); + public final WhereClauseParser.predicate_return predicate() throws RecognitionException { + WhereClauseParser.predicate_return retval = new WhereClauseParser.predicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token AND9=null; + Token OR12=null; + WhereClauseParser.simplepredicate_return simplepredicate7 =null; + + WhereClauseParser.simplepredicate_return simplepredicate8 =null; + + WhereClauseParser.simplepredicate_return simplepredicate10 =null; + + WhereClauseParser.simplepredicate_return simplepredicate11 =null; + + WhereClauseParser.simplepredicate_return simplepredicate13 =null; + + + Object AND9_tree=null; + Object OR12_tree=null; + RewriteRuleTokenStream stream_OR=new RewriteRuleTokenStream(adaptor,"token OR"); + RewriteRuleTokenStream stream_AND=new RewriteRuleTokenStream(adaptor,"token AND"); + RewriteRuleSubtreeStream stream_simplepredicate=new RewriteRuleSubtreeStream(adaptor,"rule simplepredicate"); + try { + // org/alfresco/rest/antlr/WhereClause.g:129:11: ( simplepredicate | simplepredicate ( AND simplepredicate )+ -> ^( AND ( simplepredicate )+ ) | simplepredicate ( OR simplepredicate )+ -> ^( OR ( simplepredicate )+ ) ) + int alt6=3; + alt6 = dfa6.predict(input); + switch (alt6) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:129:13: simplepredicate + { + root_0 = (Object)adaptor.nil(); + + + pushFollow(FOLLOW_simplepredicate_in_predicate801); + simplepredicate7=simplepredicate(); + + state._fsp--; + + adaptor.addChild(root_0, simplepredicate7.getTree()); + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:130:13: simplepredicate ( AND simplepredicate )+ + { + pushFollow(FOLLOW_simplepredicate_in_predicate815); + simplepredicate8=simplepredicate(); + + state._fsp--; + + stream_simplepredicate.add(simplepredicate8.getTree()); + + // org/alfresco/rest/antlr/WhereClause.g:130:29: ( AND simplepredicate )+ + int cnt4=0; + loop4: + do { + int alt4=2; + int LA4_0 = input.LA(1); + + if ( (LA4_0==AND) ) { + alt4=1; + } + + + switch (alt4) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:130:30: AND simplepredicate + { + AND9=(Token)match(input,AND,FOLLOW_AND_in_predicate818); + stream_AND.add(AND9); + + + pushFollow(FOLLOW_simplepredicate_in_predicate820); + simplepredicate10=simplepredicate(); + + state._fsp--; + + stream_simplepredicate.add(simplepredicate10.getTree()); + + } + break; + + default : + if ( cnt4 >= 1 ) break loop4; + EarlyExitException eee = + new EarlyExitException(4, input); + throw eee; + } + cnt4++; + } while (true); + + + // AST REWRITE + // elements: AND, simplepredicate + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 130:52: -> ^( AND ( simplepredicate )+ ) + { + // org/alfresco/rest/antlr/WhereClause.g:130:55: ^( AND ( simplepredicate )+ ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_AND.nextNode() + , root_1); + + if ( !(stream_simplepredicate.hasNext()) ) { + throw new RewriteEarlyExitException(); + } + while ( stream_simplepredicate.hasNext() ) { + adaptor.addChild(root_1, stream_simplepredicate.nextTree()); + + } + stream_simplepredicate.reset(); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + break; + case 3 : + // org/alfresco/rest/antlr/WhereClause.g:131:13: simplepredicate ( OR simplepredicate )+ + { + pushFollow(FOLLOW_simplepredicate_in_predicate845); + simplepredicate11=simplepredicate(); + + state._fsp--; + + stream_simplepredicate.add(simplepredicate11.getTree()); + + // org/alfresco/rest/antlr/WhereClause.g:131:29: ( OR simplepredicate )+ + int cnt5=0; + loop5: + do { + int alt5=2; + int LA5_0 = input.LA(1); + + if ( (LA5_0==OR) ) { + alt5=1; + } + + + switch (alt5) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:131:30: OR simplepredicate + { + OR12=(Token)match(input,OR,FOLLOW_OR_in_predicate848); + stream_OR.add(OR12); + + + pushFollow(FOLLOW_simplepredicate_in_predicate850); + simplepredicate13=simplepredicate(); + + state._fsp--; + + stream_simplepredicate.add(simplepredicate13.getTree()); + + } + break; + + default : + if ( cnt5 >= 1 ) break loop5; + EarlyExitException eee = + new EarlyExitException(5, input); + throw eee; + } + cnt5++; + } while (true); + + + // AST REWRITE + // elements: simplepredicate, OR + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 131:51: -> ^( OR ( simplepredicate )+ ) + { + // org/alfresco/rest/antlr/WhereClause.g:131:54: ^( OR ( simplepredicate )+ ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_OR.nextNode() + , root_1); + + if ( !(stream_simplepredicate.hasNext()) ) { + throw new RewriteEarlyExitException(); + } + while ( stream_simplepredicate.hasNext() ) { + adaptor.addChild(root_1, stream_simplepredicate.nextTree()); + + } + stream_simplepredicate.reset(); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + break; + + } + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "predicate" + + + public static class simplepredicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "simplepredicate" + // org/alfresco/rest/antlr/WhereClause.g:132:1: simplepredicate : ( allowedpredicates -> allowedpredicates | NEGATION allowedpredicates -> ^( NEGATION allowedpredicates ) ); + public final WhereClauseParser.simplepredicate_return simplepredicate() throws RecognitionException { + WhereClauseParser.simplepredicate_return retval = new WhereClauseParser.simplepredicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token NEGATION15=null; + WhereClauseParser.allowedpredicates_return allowedpredicates14 =null; + + WhereClauseParser.allowedpredicates_return allowedpredicates16 =null; + + + Object NEGATION15_tree=null; + RewriteRuleTokenStream stream_NEGATION=new RewriteRuleTokenStream(adaptor,"token NEGATION"); + RewriteRuleSubtreeStream stream_allowedpredicates=new RewriteRuleSubtreeStream(adaptor,"rule allowedpredicates"); + try { + // org/alfresco/rest/antlr/WhereClause.g:132:17: ( allowedpredicates -> allowedpredicates | NEGATION allowedpredicates -> ^( NEGATION allowedpredicates ) ) + int alt7=2; + int LA7_0 = input.LA(1); + + if ( (LA7_0==EXISTS||LA7_0==PROPERTYNAME) ) { + alt7=1; + } + else if ( (LA7_0==NEGATION) ) { + alt7=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 7, 0, input); + + throw nvae; + + } + switch (alt7) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:132:19: allowedpredicates + { + pushFollow(FOLLOW_allowedpredicates_in_simplepredicate868); + allowedpredicates14=allowedpredicates(); + + state._fsp--; + + stream_allowedpredicates.add(allowedpredicates14.getTree()); + + // AST REWRITE + // elements: allowedpredicates + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 132:37: -> allowedpredicates + { + adaptor.addChild(root_0, stream_allowedpredicates.nextTree()); + + } + + + retval.tree = root_0; + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:133:19: NEGATION allowedpredicates + { + NEGATION15=(Token)match(input,NEGATION,FOLLOW_NEGATION_in_simplepredicate892); + stream_NEGATION.add(NEGATION15); + + + pushFollow(FOLLOW_allowedpredicates_in_simplepredicate894); + allowedpredicates16=allowedpredicates(); + + state._fsp--; + + stream_allowedpredicates.add(allowedpredicates16.getTree()); + + // AST REWRITE + // elements: NEGATION, allowedpredicates + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 133:46: -> ^( NEGATION allowedpredicates ) + { + // org/alfresco/rest/antlr/WhereClause.g:133:49: ^( NEGATION allowedpredicates ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_NEGATION.nextNode() + , root_1); + + adaptor.addChild(root_1, stream_allowedpredicates.nextTree()); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + break; + + } + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "simplepredicate" + + + public static class allowedpredicates_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "allowedpredicates" + // org/alfresco/rest/antlr/WhereClause.g:134:1: allowedpredicates : ( comparisonpredicate | existspredicate | betweenpredicate | inpredicate | matchespredicate ); + public final WhereClauseParser.allowedpredicates_return allowedpredicates() throws RecognitionException { + WhereClauseParser.allowedpredicates_return retval = new WhereClauseParser.allowedpredicates_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + WhereClauseParser.comparisonpredicate_return comparisonpredicate17 =null; + + WhereClauseParser.existspredicate_return existspredicate18 =null; + + WhereClauseParser.betweenpredicate_return betweenpredicate19 =null; + + WhereClauseParser.inpredicate_return inpredicate20 =null; + + WhereClauseParser.matchespredicate_return matchespredicate21 =null; + + + + try { + // org/alfresco/rest/antlr/WhereClause.g:134:19: ( comparisonpredicate | existspredicate | betweenpredicate | inpredicate | matchespredicate ) + int alt8=5; + int LA8_0 = input.LA(1); + + if ( (LA8_0==PROPERTYNAME) ) { + switch ( input.LA(2) ) { + case BETWEEN: + { + alt8=3; + } + break; + case IN: + { + alt8=4; + } + break; + case MATCHES: + { + alt8=5; + } + break; + case EQUALS: + case GREATERTHAN: + case GREATERTHANOREQUALS: + case LESSTHAN: + case LESSTHANOREQUALS: + { + alt8=1; + } + break; + default: + NoViableAltException nvae = + new NoViableAltException("", 8, 1, input); + + throw nvae; + + } + + } + else if ( (LA8_0==EXISTS) ) { + alt8=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 8, 0, input); + + throw nvae; + + } + switch (alt8) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:134:21: comparisonpredicate + { + root_0 = (Object)adaptor.nil(); + + + pushFollow(FOLLOW_comparisonpredicate_in_allowedpredicates909); + comparisonpredicate17=comparisonpredicate(); + + state._fsp--; + + adaptor.addChild(root_0, comparisonpredicate17.getTree()); + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:134:43: existspredicate + { + root_0 = (Object)adaptor.nil(); + + + pushFollow(FOLLOW_existspredicate_in_allowedpredicates913); + existspredicate18=existspredicate(); + + state._fsp--; + + adaptor.addChild(root_0, existspredicate18.getTree()); + + } + break; + case 3 : + // org/alfresco/rest/antlr/WhereClause.g:134:61: betweenpredicate + { + root_0 = (Object)adaptor.nil(); + + + pushFollow(FOLLOW_betweenpredicate_in_allowedpredicates917); + betweenpredicate19=betweenpredicate(); + + state._fsp--; + + adaptor.addChild(root_0, betweenpredicate19.getTree()); + + } + break; + case 4 : + // org/alfresco/rest/antlr/WhereClause.g:134:80: inpredicate + { + root_0 = (Object)adaptor.nil(); + + + pushFollow(FOLLOW_inpredicate_in_allowedpredicates921); + inpredicate20=inpredicate(); + + state._fsp--; + + adaptor.addChild(root_0, inpredicate20.getTree()); + + } + break; + case 5 : + // org/alfresco/rest/antlr/WhereClause.g:134:94: matchespredicate + { + root_0 = (Object)adaptor.nil(); + + + pushFollow(FOLLOW_matchespredicate_in_allowedpredicates925); + matchespredicate21=matchespredicate(); + + state._fsp--; + + adaptor.addChild(root_0, matchespredicate21.getTree()); + + } + break; + + } + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "allowedpredicates" + + + public static class comparisonpredicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "comparisonpredicate" + // org/alfresco/rest/antlr/WhereClause.g:135:1: comparisonpredicate : PROPERTYNAME comparisonoperator value -> ^( comparisonoperator PROPERTYNAME value ) ; + public final WhereClauseParser.comparisonpredicate_return comparisonpredicate() throws RecognitionException { + WhereClauseParser.comparisonpredicate_return retval = new WhereClauseParser.comparisonpredicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token PROPERTYNAME22=null; + WhereClauseParser.comparisonoperator_return comparisonoperator23 =null; + + WhereClauseParser.value_return value24 =null; + + + Object PROPERTYNAME22_tree=null; + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + RewriteRuleSubtreeStream stream_comparisonoperator=new RewriteRuleSubtreeStream(adaptor,"rule comparisonoperator"); + RewriteRuleSubtreeStream stream_value=new RewriteRuleSubtreeStream(adaptor,"rule value"); + try { + // org/alfresco/rest/antlr/WhereClause.g:135:20: ( PROPERTYNAME comparisonoperator value -> ^( comparisonoperator PROPERTYNAME value ) ) + // org/alfresco/rest/antlr/WhereClause.g:135:22: PROPERTYNAME comparisonoperator value + { + PROPERTYNAME22=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_comparisonpredicate931); + stream_PROPERTYNAME.add(PROPERTYNAME22); + + + pushFollow(FOLLOW_comparisonoperator_in_comparisonpredicate933); + comparisonoperator23=comparisonoperator(); + + state._fsp--; + + stream_comparisonoperator.add(comparisonoperator23.getTree()); + + pushFollow(FOLLOW_value_in_comparisonpredicate935); + value24=value(); + + state._fsp--; + + stream_value.add(value24.getTree()); + + // AST REWRITE + // elements: value, comparisonoperator, PROPERTYNAME + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 135:60: -> ^( comparisonoperator PROPERTYNAME value ) + { + // org/alfresco/rest/antlr/WhereClause.g:135:63: ^( comparisonoperator PROPERTYNAME value ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot(stream_comparisonoperator.nextNode(), root_1); + + adaptor.addChild(root_1, + stream_PROPERTYNAME.nextNode() + ); + + adaptor.addChild(root_1, stream_value.nextTree()); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "comparisonpredicate" + + + public static class comparisonoperator_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "comparisonoperator" + // org/alfresco/rest/antlr/WhereClause.g:136:1: comparisonoperator : ( EQUALS | LESSTHAN | GREATERTHAN | LESSTHANOREQUALS | GREATERTHANOREQUALS ); + public final WhereClauseParser.comparisonoperator_return comparisonoperator() throws RecognitionException { + WhereClauseParser.comparisonoperator_return retval = new WhereClauseParser.comparisonoperator_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token set25=null; + + Object set25_tree=null; + + try { + // org/alfresco/rest/antlr/WhereClause.g:136:19: ( EQUALS | LESSTHAN | GREATERTHAN | LESSTHANOREQUALS | GREATERTHANOREQUALS ) + // org/alfresco/rest/antlr/WhereClause.g: + { + root_0 = (Object)adaptor.nil(); + + + set25=(Token)input.LT(1); + + if ( input.LA(1)==EQUALS||(input.LA(1) >= GREATERTHAN && input.LA(1) <= GREATERTHANOREQUALS)||(input.LA(1) >= LESSTHAN && input.LA(1) <= LESSTHANOREQUALS) ) { + input.consume(); + adaptor.addChild(root_0, + (Object)adaptor.create(set25) + ); + state.errorRecovery=false; + } + else { + MismatchedSetException mse = new MismatchedSetException(null,input); + throw mse; + } + + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "comparisonoperator" + + + public static class existspredicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "existspredicate" + // org/alfresco/rest/antlr/WhereClause.g:137:1: existspredicate : EXISTS LEFTPAREN ( WS )? PROPERTYNAME RIGHTPAREN -> ^( EXISTS PROPERTYNAME ) ; + public final WhereClauseParser.existspredicate_return existspredicate() throws RecognitionException { + WhereClauseParser.existspredicate_return retval = new WhereClauseParser.existspredicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token EXISTS26=null; + Token LEFTPAREN27=null; + Token WS28=null; + Token PROPERTYNAME29=null; + Token RIGHTPAREN30=null; + + Object EXISTS26_tree=null; + Object LEFTPAREN27_tree=null; + Object WS28_tree=null; + Object PROPERTYNAME29_tree=null; + Object RIGHTPAREN30_tree=null; + RewriteRuleTokenStream stream_LEFTPAREN=new RewriteRuleTokenStream(adaptor,"token LEFTPAREN"); + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + RewriteRuleTokenStream stream_EXISTS=new RewriteRuleTokenStream(adaptor,"token EXISTS"); + RewriteRuleTokenStream stream_RIGHTPAREN=new RewriteRuleTokenStream(adaptor,"token RIGHTPAREN"); + RewriteRuleTokenStream stream_WS=new RewriteRuleTokenStream(adaptor,"token WS"); + + try { + // org/alfresco/rest/antlr/WhereClause.g:137:16: ( EXISTS LEFTPAREN ( WS )? PROPERTYNAME RIGHTPAREN -> ^( EXISTS PROPERTYNAME ) ) + // org/alfresco/rest/antlr/WhereClause.g:137:18: EXISTS LEFTPAREN ( WS )? PROPERTYNAME RIGHTPAREN + { + EXISTS26=(Token)match(input,EXISTS,FOLLOW_EXISTS_in_existspredicate965); + stream_EXISTS.add(EXISTS26); + + + LEFTPAREN27=(Token)match(input,LEFTPAREN,FOLLOW_LEFTPAREN_in_existspredicate967); + stream_LEFTPAREN.add(LEFTPAREN27); + + + // org/alfresco/rest/antlr/WhereClause.g:137:35: ( WS )? + int alt9=2; + int LA9_0 = input.LA(1); + + if ( (LA9_0==WS) ) { + alt9=1; + } + switch (alt9) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:137:35: WS + { + WS28=(Token)match(input,WS,FOLLOW_WS_in_existspredicate969); + stream_WS.add(WS28); + + + } + break; + + } + + + PROPERTYNAME29=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_existspredicate972); + stream_PROPERTYNAME.add(PROPERTYNAME29); + + + RIGHTPAREN30=(Token)match(input,RIGHTPAREN,FOLLOW_RIGHTPAREN_in_existspredicate974); + stream_RIGHTPAREN.add(RIGHTPAREN30); + + + // AST REWRITE + // elements: EXISTS, PROPERTYNAME + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 137:63: -> ^( EXISTS PROPERTYNAME ) + { + // org/alfresco/rest/antlr/WhereClause.g:137:66: ^( EXISTS PROPERTYNAME ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_EXISTS.nextNode() + , root_1); + + adaptor.addChild(root_1, + stream_PROPERTYNAME.nextNode() + ); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "existspredicate" + + + public static class betweenpredicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "betweenpredicate" + // org/alfresco/rest/antlr/WhereClause.g:138:1: betweenpredicate : PROPERTYNAME BETWEEN LEFTPAREN ( WS )? propertyvaluepair RIGHTPAREN -> ^( BETWEEN PROPERTYNAME propertyvaluepair ) ; + public final WhereClauseParser.betweenpredicate_return betweenpredicate() throws RecognitionException { + WhereClauseParser.betweenpredicate_return retval = new WhereClauseParser.betweenpredicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token PROPERTYNAME31=null; + Token BETWEEN32=null; + Token LEFTPAREN33=null; + Token WS34=null; + Token RIGHTPAREN36=null; + WhereClauseParser.propertyvaluepair_return propertyvaluepair35 =null; + + + Object PROPERTYNAME31_tree=null; + Object BETWEEN32_tree=null; + Object LEFTPAREN33_tree=null; + Object WS34_tree=null; + Object RIGHTPAREN36_tree=null; + RewriteRuleTokenStream stream_LEFTPAREN=new RewriteRuleTokenStream(adaptor,"token LEFTPAREN"); + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + RewriteRuleTokenStream stream_RIGHTPAREN=new RewriteRuleTokenStream(adaptor,"token RIGHTPAREN"); + RewriteRuleTokenStream stream_BETWEEN=new RewriteRuleTokenStream(adaptor,"token BETWEEN"); + RewriteRuleTokenStream stream_WS=new RewriteRuleTokenStream(adaptor,"token WS"); + RewriteRuleSubtreeStream stream_propertyvaluepair=new RewriteRuleSubtreeStream(adaptor,"rule propertyvaluepair"); + try { + // org/alfresco/rest/antlr/WhereClause.g:138:17: ( PROPERTYNAME BETWEEN LEFTPAREN ( WS )? propertyvaluepair RIGHTPAREN -> ^( BETWEEN PROPERTYNAME propertyvaluepair ) ) + // org/alfresco/rest/antlr/WhereClause.g:138:19: PROPERTYNAME BETWEEN LEFTPAREN ( WS )? propertyvaluepair RIGHTPAREN + { + PROPERTYNAME31=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_betweenpredicate988); + stream_PROPERTYNAME.add(PROPERTYNAME31); + + + BETWEEN32=(Token)match(input,BETWEEN,FOLLOW_BETWEEN_in_betweenpredicate990); + stream_BETWEEN.add(BETWEEN32); + + + LEFTPAREN33=(Token)match(input,LEFTPAREN,FOLLOW_LEFTPAREN_in_betweenpredicate992); + stream_LEFTPAREN.add(LEFTPAREN33); + + + // org/alfresco/rest/antlr/WhereClause.g:138:50: ( WS )? + int alt10=2; + int LA10_0 = input.LA(1); + + if ( (LA10_0==WS) ) { + alt10=1; + } + switch (alt10) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:138:50: WS + { + WS34=(Token)match(input,WS,FOLLOW_WS_in_betweenpredicate994); + stream_WS.add(WS34); + + + } + break; + + } + + + pushFollow(FOLLOW_propertyvaluepair_in_betweenpredicate997); + propertyvaluepair35=propertyvaluepair(); + + state._fsp--; + + stream_propertyvaluepair.add(propertyvaluepair35.getTree()); + + RIGHTPAREN36=(Token)match(input,RIGHTPAREN,FOLLOW_RIGHTPAREN_in_betweenpredicate999); + stream_RIGHTPAREN.add(RIGHTPAREN36); + + + // AST REWRITE + // elements: propertyvaluepair, BETWEEN, PROPERTYNAME + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 138:83: -> ^( BETWEEN PROPERTYNAME propertyvaluepair ) + { + // org/alfresco/rest/antlr/WhereClause.g:138:86: ^( BETWEEN PROPERTYNAME propertyvaluepair ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_BETWEEN.nextNode() + , root_1); + + adaptor.addChild(root_1, + stream_PROPERTYNAME.nextNode() + ); + + adaptor.addChild(root_1, stream_propertyvaluepair.nextTree()); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "betweenpredicate" + + + public static class inpredicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "inpredicate" + // org/alfresco/rest/antlr/WhereClause.g:139:1: inpredicate : PROPERTYNAME IN LEFTPAREN ( WS )? propertyvaluelist RIGHTPAREN -> ^( IN PROPERTYNAME propertyvaluelist ) ; + public final WhereClauseParser.inpredicate_return inpredicate() throws RecognitionException { + WhereClauseParser.inpredicate_return retval = new WhereClauseParser.inpredicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token PROPERTYNAME37=null; + Token IN38=null; + Token LEFTPAREN39=null; + Token WS40=null; + Token RIGHTPAREN42=null; + WhereClauseParser.propertyvaluelist_return propertyvaluelist41 =null; + + + Object PROPERTYNAME37_tree=null; + Object IN38_tree=null; + Object LEFTPAREN39_tree=null; + Object WS40_tree=null; + Object RIGHTPAREN42_tree=null; + RewriteRuleTokenStream stream_LEFTPAREN=new RewriteRuleTokenStream(adaptor,"token LEFTPAREN"); + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + RewriteRuleTokenStream stream_IN=new RewriteRuleTokenStream(adaptor,"token IN"); + RewriteRuleTokenStream stream_RIGHTPAREN=new RewriteRuleTokenStream(adaptor,"token RIGHTPAREN"); + RewriteRuleTokenStream stream_WS=new RewriteRuleTokenStream(adaptor,"token WS"); + RewriteRuleSubtreeStream stream_propertyvaluelist=new RewriteRuleSubtreeStream(adaptor,"rule propertyvaluelist"); + try { + // org/alfresco/rest/antlr/WhereClause.g:139:12: ( PROPERTYNAME IN LEFTPAREN ( WS )? propertyvaluelist RIGHTPAREN -> ^( IN PROPERTYNAME propertyvaluelist ) ) + // org/alfresco/rest/antlr/WhereClause.g:139:14: PROPERTYNAME IN LEFTPAREN ( WS )? propertyvaluelist RIGHTPAREN + { + PROPERTYNAME37=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_inpredicate1015); + stream_PROPERTYNAME.add(PROPERTYNAME37); + + + IN38=(Token)match(input,IN,FOLLOW_IN_in_inpredicate1017); + stream_IN.add(IN38); + + + LEFTPAREN39=(Token)match(input,LEFTPAREN,FOLLOW_LEFTPAREN_in_inpredicate1019); + stream_LEFTPAREN.add(LEFTPAREN39); + + + // org/alfresco/rest/antlr/WhereClause.g:139:40: ( WS )? + int alt11=2; + int LA11_0 = input.LA(1); + + if ( (LA11_0==WS) ) { + alt11=1; + } + switch (alt11) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:139:40: WS + { + WS40=(Token)match(input,WS,FOLLOW_WS_in_inpredicate1021); + stream_WS.add(WS40); + + + } + break; + + } + + + pushFollow(FOLLOW_propertyvaluelist_in_inpredicate1024); + propertyvaluelist41=propertyvaluelist(); + + state._fsp--; + + stream_propertyvaluelist.add(propertyvaluelist41.getTree()); + + RIGHTPAREN42=(Token)match(input,RIGHTPAREN,FOLLOW_RIGHTPAREN_in_inpredicate1026); + stream_RIGHTPAREN.add(RIGHTPAREN42); + + + // AST REWRITE + // elements: PROPERTYNAME, IN, propertyvaluelist + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 139:73: -> ^( IN PROPERTYNAME propertyvaluelist ) + { + // org/alfresco/rest/antlr/WhereClause.g:139:76: ^( IN PROPERTYNAME propertyvaluelist ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_IN.nextNode() + , root_1); + + adaptor.addChild(root_1, + stream_PROPERTYNAME.nextNode() + ); + + adaptor.addChild(root_1, stream_propertyvaluelist.nextTree()); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "inpredicate" + + + public static class matchespredicate_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "matchespredicate" + // org/alfresco/rest/antlr/WhereClause.g:140:1: matchespredicate : PROPERTYNAME MATCHES LEFTPAREN ( WS )? value RIGHTPAREN -> ^( MATCHES PROPERTYNAME value ) ; + public final WhereClauseParser.matchespredicate_return matchespredicate() throws RecognitionException { + WhereClauseParser.matchespredicate_return retval = new WhereClauseParser.matchespredicate_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token PROPERTYNAME43=null; + Token MATCHES44=null; + Token LEFTPAREN45=null; + Token WS46=null; + Token RIGHTPAREN48=null; + WhereClauseParser.value_return value47 =null; + + + Object PROPERTYNAME43_tree=null; + Object MATCHES44_tree=null; + Object LEFTPAREN45_tree=null; + Object WS46_tree=null; + Object RIGHTPAREN48_tree=null; + RewriteRuleTokenStream stream_LEFTPAREN=new RewriteRuleTokenStream(adaptor,"token LEFTPAREN"); + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + RewriteRuleTokenStream stream_RIGHTPAREN=new RewriteRuleTokenStream(adaptor,"token RIGHTPAREN"); + RewriteRuleTokenStream stream_MATCHES=new RewriteRuleTokenStream(adaptor,"token MATCHES"); + RewriteRuleTokenStream stream_WS=new RewriteRuleTokenStream(adaptor,"token WS"); + RewriteRuleSubtreeStream stream_value=new RewriteRuleSubtreeStream(adaptor,"rule value"); + try { + // org/alfresco/rest/antlr/WhereClause.g:140:17: ( PROPERTYNAME MATCHES LEFTPAREN ( WS )? value RIGHTPAREN -> ^( MATCHES PROPERTYNAME value ) ) + // org/alfresco/rest/antlr/WhereClause.g:140:19: PROPERTYNAME MATCHES LEFTPAREN ( WS )? value RIGHTPAREN + { + PROPERTYNAME43=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_matchespredicate1042); + stream_PROPERTYNAME.add(PROPERTYNAME43); + + + MATCHES44=(Token)match(input,MATCHES,FOLLOW_MATCHES_in_matchespredicate1044); + stream_MATCHES.add(MATCHES44); + + + LEFTPAREN45=(Token)match(input,LEFTPAREN,FOLLOW_LEFTPAREN_in_matchespredicate1046); + stream_LEFTPAREN.add(LEFTPAREN45); + + + // org/alfresco/rest/antlr/WhereClause.g:140:50: ( WS )? + int alt12=2; + int LA12_0 = input.LA(1); + + if ( (LA12_0==WS) ) { + alt12=1; + } + switch (alt12) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:140:50: WS + { + WS46=(Token)match(input,WS,FOLLOW_WS_in_matchespredicate1048); + stream_WS.add(WS46); + + + } + break; + + } + + + pushFollow(FOLLOW_value_in_matchespredicate1051); + value47=value(); + + state._fsp--; + + stream_value.add(value47.getTree()); + + RIGHTPAREN48=(Token)match(input,RIGHTPAREN,FOLLOW_RIGHTPAREN_in_matchespredicate1053); + stream_RIGHTPAREN.add(RIGHTPAREN48); + + + // AST REWRITE + // elements: PROPERTYNAME, value, MATCHES + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 140:71: -> ^( MATCHES PROPERTYNAME value ) + { + // org/alfresco/rest/antlr/WhereClause.g:140:74: ^( MATCHES PROPERTYNAME value ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + stream_MATCHES.nextNode() + , root_1); + + adaptor.addChild(root_1, + stream_PROPERTYNAME.nextNode() + ); + + adaptor.addChild(root_1, stream_value.nextTree()); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "matchespredicate" + + + public static class propertyvaluepair_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "propertyvaluepair" + // org/alfresco/rest/antlr/WhereClause.g:141:1: propertyvaluepair : value COMMA value -> ( value )+ ; + public final WhereClauseParser.propertyvaluepair_return propertyvaluepair() throws RecognitionException { + WhereClauseParser.propertyvaluepair_return retval = new WhereClauseParser.propertyvaluepair_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token COMMA50=null; + WhereClauseParser.value_return value49 =null; + + WhereClauseParser.value_return value51 =null; + + + Object COMMA50_tree=null; + RewriteRuleTokenStream stream_COMMA=new RewriteRuleTokenStream(adaptor,"token COMMA"); + RewriteRuleSubtreeStream stream_value=new RewriteRuleSubtreeStream(adaptor,"rule value"); + try { + // org/alfresco/rest/antlr/WhereClause.g:141:18: ( value COMMA value -> ( value )+ ) + // org/alfresco/rest/antlr/WhereClause.g:141:20: value COMMA value + { + pushFollow(FOLLOW_value_in_propertyvaluepair1069); + value49=value(); + + state._fsp--; + + stream_value.add(value49.getTree()); + + COMMA50=(Token)match(input,COMMA,FOLLOW_COMMA_in_propertyvaluepair1071); + stream_COMMA.add(COMMA50); + + + pushFollow(FOLLOW_value_in_propertyvaluepair1073); + value51=value(); + + state._fsp--; + + stream_value.add(value51.getTree()); + + // AST REWRITE + // elements: value + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 141:38: -> ( value )+ + { + if ( !(stream_value.hasNext()) ) { + throw new RewriteEarlyExitException(); + } + while ( stream_value.hasNext() ) { + adaptor.addChild(root_0, stream_value.nextTree()); + + } + stream_value.reset(); + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "propertyvaluepair" + + + public static class propertyvaluelist_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "propertyvaluelist" + // org/alfresco/rest/antlr/WhereClause.g:142:1: propertyvaluelist : value ( COMMA value )* -> ( value )+ ; + public final WhereClauseParser.propertyvaluelist_return propertyvaluelist() throws RecognitionException { + WhereClauseParser.propertyvaluelist_return retval = new WhereClauseParser.propertyvaluelist_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token COMMA53=null; + WhereClauseParser.value_return value52 =null; + + WhereClauseParser.value_return value54 =null; + + + Object COMMA53_tree=null; + RewriteRuleTokenStream stream_COMMA=new RewriteRuleTokenStream(adaptor,"token COMMA"); + RewriteRuleSubtreeStream stream_value=new RewriteRuleSubtreeStream(adaptor,"rule value"); + try { + // org/alfresco/rest/antlr/WhereClause.g:142:18: ( value ( COMMA value )* -> ( value )+ ) + // org/alfresco/rest/antlr/WhereClause.g:142:20: value ( COMMA value )* + { + pushFollow(FOLLOW_value_in_propertyvaluelist1084); + value52=value(); + + state._fsp--; + + stream_value.add(value52.getTree()); + + // org/alfresco/rest/antlr/WhereClause.g:142:26: ( COMMA value )* + loop13: + do { + int alt13=2; + int LA13_0 = input.LA(1); + + if ( (LA13_0==COMMA) ) { + alt13=1; + } + + + switch (alt13) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:142:27: COMMA value + { + COMMA53=(Token)match(input,COMMA,FOLLOW_COMMA_in_propertyvaluelist1087); + stream_COMMA.add(COMMA53); + + + pushFollow(FOLLOW_value_in_propertyvaluelist1089); + value54=value(); + + state._fsp--; + + stream_value.add(value54.getTree()); + + } + break; + + default : + break loop13; + } + } while (true); + + + // AST REWRITE + // elements: value + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 142:41: -> ( value )+ + { + if ( !(stream_value.hasNext()) ) { + throw new RewriteEarlyExitException(); + } + while ( stream_value.hasNext() ) { + adaptor.addChild(root_0, stream_value.nextTree()); + + } + stream_value.reset(); + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "propertyvaluelist" + + + public static class value_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "value" + // org/alfresco/rest/antlr/WhereClause.g:143:1: value : (a= PROPERTYVALUE -> ^( PROPERTYVALUE[$a] ) |b= PROPERTYNAME -> ^( PROPERTYVALUE[$b] ) ); + public final WhereClauseParser.value_return value() throws RecognitionException { + WhereClauseParser.value_return retval = new WhereClauseParser.value_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token a=null; + Token b=null; + + Object a_tree=null; + Object b_tree=null; + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + RewriteRuleTokenStream stream_PROPERTYVALUE=new RewriteRuleTokenStream(adaptor,"token PROPERTYVALUE"); + + try { + // org/alfresco/rest/antlr/WhereClause.g:143:6: (a= PROPERTYVALUE -> ^( PROPERTYVALUE[$a] ) |b= PROPERTYNAME -> ^( PROPERTYVALUE[$b] ) ) + int alt14=2; + int LA14_0 = input.LA(1); + + if ( (LA14_0==PROPERTYVALUE) ) { + alt14=1; + } + else if ( (LA14_0==PROPERTYNAME) ) { + alt14=2; + } + else { + NoViableAltException nvae = + new NoViableAltException("", 14, 0, input); + + throw nvae; + + } + switch (alt14) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:143:8: a= PROPERTYVALUE + { + a=(Token)match(input,PROPERTYVALUE,FOLLOW_PROPERTYVALUE_in_value1104); + stream_PROPERTYVALUE.add(a); + + + // AST REWRITE + // elements: PROPERTYVALUE + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 143:24: -> ^( PROPERTYVALUE[$a] ) + { + // org/alfresco/rest/antlr/WhereClause.g:143:27: ^( PROPERTYVALUE[$a] ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + (Object)adaptor.create(PROPERTYVALUE, a) + , root_1); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + break; + case 2 : + // org/alfresco/rest/antlr/WhereClause.g:144:9: b= PROPERTYNAME + { + b=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_value1124); + stream_PROPERTYNAME.add(b); + + + // AST REWRITE + // elements: + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 144:24: -> ^( PROPERTYVALUE[$b] ) + { + // org/alfresco/rest/antlr/WhereClause.g:144:27: ^( PROPERTYVALUE[$b] ) + { + Object root_1 = (Object)adaptor.nil(); + root_1 = (Object)adaptor.becomeRoot( + (Object)adaptor.create(PROPERTYVALUE, b) + , root_1); + + adaptor.addChild(root_0, root_1); + } + + } + + + retval.tree = root_0; + + } + break; + + } + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "value" + + + public static class selectClause_return extends ParserRuleReturnScope { + Object tree; + public Object getTree() { return tree; } + }; + + + // $ANTLR start "selectClause" + // org/alfresco/rest/antlr/WhereClause.g:145:1: selectClause : PROPERTYNAME ( COMMA PROPERTYNAME )* -> ( PROPERTYNAME )+ ; + public final WhereClauseParser.selectClause_return selectClause() throws RecognitionException { + WhereClauseParser.selectClause_return retval = new WhereClauseParser.selectClause_return(); + retval.start = input.LT(1); + + + Object root_0 = null; + + Token PROPERTYNAME55=null; + Token COMMA56=null; + Token PROPERTYNAME57=null; + + Object PROPERTYNAME55_tree=null; + Object COMMA56_tree=null; + Object PROPERTYNAME57_tree=null; + RewriteRuleTokenStream stream_COMMA=new RewriteRuleTokenStream(adaptor,"token COMMA"); + RewriteRuleTokenStream stream_PROPERTYNAME=new RewriteRuleTokenStream(adaptor,"token PROPERTYNAME"); + + try { + // org/alfresco/rest/antlr/WhereClause.g:145:13: ( PROPERTYNAME ( COMMA PROPERTYNAME )* -> ( PROPERTYNAME )+ ) + // org/alfresco/rest/antlr/WhereClause.g:145:16: PROPERTYNAME ( COMMA PROPERTYNAME )* + { + PROPERTYNAME55=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_selectClause1140); + stream_PROPERTYNAME.add(PROPERTYNAME55); + + + // org/alfresco/rest/antlr/WhereClause.g:145:29: ( COMMA PROPERTYNAME )* + loop15: + do { + int alt15=2; + int LA15_0 = input.LA(1); + + if ( (LA15_0==COMMA) ) { + alt15=1; + } + + + switch (alt15) { + case 1 : + // org/alfresco/rest/antlr/WhereClause.g:145:30: COMMA PROPERTYNAME + { + COMMA56=(Token)match(input,COMMA,FOLLOW_COMMA_in_selectClause1143); + stream_COMMA.add(COMMA56); + + + PROPERTYNAME57=(Token)match(input,PROPERTYNAME,FOLLOW_PROPERTYNAME_in_selectClause1145); + stream_PROPERTYNAME.add(PROPERTYNAME57); + + + } + break; + + default : + break loop15; + } + } while (true); + + + // AST REWRITE + // elements: PROPERTYNAME + // token labels: + // rule labels: retval + // token list labels: + // rule list labels: + // wildcard labels: + retval.tree = root_0; + RewriteRuleSubtreeStream stream_retval=new RewriteRuleSubtreeStream(adaptor,"rule retval",retval!=null?retval.tree:null); + + root_0 = (Object)adaptor.nil(); + // 145:51: -> ( PROPERTYNAME )+ + { + if ( !(stream_PROPERTYNAME.hasNext()) ) { + throw new RewriteEarlyExitException(); + } + while ( stream_PROPERTYNAME.hasNext() ) { + adaptor.addChild(root_0, + stream_PROPERTYNAME.nextNode() + ); + + } + stream_PROPERTYNAME.reset(); + + } + + + retval.tree = root_0; + + } + + retval.stop = input.LT(-1); + + + retval.tree = (Object)adaptor.rulePostProcessing(root_0); + adaptor.setTokenBoundaries(retval.tree, retval.start, retval.stop); + + } + + catch(RecognitionException e) + { + throw e; + } + + finally { + // do for sure before leaving + } + return retval; + } + // $ANTLR end "selectClause" + + // Delegated rules + + + protected DFA6 dfa6 = new DFA6(this); + static final String DFA6_eotS = + "\107\uffff"; + static final String DFA6_eofS = + "\107\uffff"; + static final String DFA6_minS = + "\1\11\1\5\1\21\1\11\3\21\2\27\1\5\1\21\3\27\2\4\1\27\1\31\3\21\3"+ + "\27\2\7\1\27\2\7\1\27\2\31\3\uffff\1\4\3\27\2\4\1\27\1\31\2\27\2"+ + "\4\1\27\2\7\1\27\2\7\1\27\2\31\1\4\2\31\2\7\2\27\3\4\2\31\2\7\1"+ + "\4"; + static final String DFA6_maxS = + "\1\27\1\24\1\21\1\27\3\21\1\30\1\33\1\24\1\21\3\33\2\31\1\27\1\31"+ + "\3\21\1\30\1\33\1\30\2\7\1\30\2\31\1\30\2\31\3\uffff\1\31\3\33\2"+ + "\31\1\27\1\31\2\30\2\31\1\30\2\7\1\30\2\31\1\30\7\31\2\30\10\31"; + static final String DFA6_acceptS = + "\40\uffff\1\1\1\2\1\3\44\uffff"; + static final String DFA6_specialS = + "\107\uffff}>"; + static final String[] DFA6_transitionS = { + "\1\2\13\uffff\1\3\1\uffff\1\1", + "\1\4\2\uffff\1\7\1\uffff\2\7\4\uffff\1\5\1\uffff\2\7\1\6", + "\1\10", + "\1\12\15\uffff\1\11", + "\1\13", + "\1\14", + "\1\15", + "\1\17\1\16", + "\1\21\3\uffff\1\20", + "\1\22\2\uffff\1\25\1\uffff\2\25\4\uffff\1\23\1\uffff\2\25\1"+ + "\24", + "\1\26", + "\1\31\1\30\2\uffff\1\27", + "\1\34\1\33\2\uffff\1\32", + "\1\37\1\36\2\uffff\1\35", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\21", + "\1\43", + "\1\44", + "\1\45", + "\1\46", + "\1\50\1\47", + "\1\52\3\uffff\1\51", + "\1\31\1\30", + "\1\53", + "\1\53", + "\1\34\1\33", + "\1\54\21\uffff\1\55", + "\1\54\21\uffff\1\55", + "\1\37\1\36", + "\1\56", + "\1\56", + "", + "", + "", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\61\1\60\2\uffff\1\57", + "\1\64\1\63\2\uffff\1\62", + "\1\67\1\66\2\uffff\1\65", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\52", + "\1\70", + "\1\72\1\71", + "\1\74\1\73", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\61\1\60", + "\1\75", + "\1\75", + "\1\64\1\63", + "\1\76\21\uffff\1\77", + "\1\76\21\uffff\1\77", + "\1\67\1\66", + "\1\100", + "\1\100", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\101", + "\1\101", + "\1\54\21\uffff\1\55", + "\1\54\21\uffff\1\55", + "\1\103\1\102", + "\1\105\1\104", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\41\21\uffff\1\42\2\uffff\1\40", + "\1\106", + "\1\106", + "\1\76\21\uffff\1\77", + "\1\76\21\uffff\1\77", + "\1\41\21\uffff\1\42\2\uffff\1\40" + }; + + static final short[] DFA6_eot = DFA.unpackEncodedString(DFA6_eotS); + static final short[] DFA6_eof = DFA.unpackEncodedString(DFA6_eofS); + static final char[] DFA6_min = DFA.unpackEncodedStringToUnsignedChars(DFA6_minS); + static final char[] DFA6_max = DFA.unpackEncodedStringToUnsignedChars(DFA6_maxS); + static final short[] DFA6_accept = DFA.unpackEncodedString(DFA6_acceptS); + static final short[] DFA6_special = DFA.unpackEncodedString(DFA6_specialS); + static final short[][] DFA6_transition; + + static { + int numStates = DFA6_transitionS.length; + DFA6_transition = new short[numStates][]; + for (int i=0; i ^( AND ( simplepredicate )+ ) | simplepredicate ( OR simplepredicate )+ -> ^( OR ( simplepredicate )+ ) );"; + } + } + + + public static final BitSet FOLLOW_WS_in_whereclause779 = new BitSet(new long[]{0x0000000000020000L}); + public static final BitSet FOLLOW_LEFTPAREN_in_whereclause782 = new BitSet(new long[]{0x0000000008A00200L}); + public static final BitSet FOLLOW_WS_in_whereclause785 = new BitSet(new long[]{0x0000000000A00200L}); + public static final BitSet FOLLOW_predicate_in_whereclause788 = new BitSet(new long[]{0x0000000002000000L}); + public static final BitSet FOLLOW_RIGHTPAREN_in_whereclause790 = new BitSet(new long[]{0x0000000008000002L}); + public static final BitSet FOLLOW_WS_in_whereclause793 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_simplepredicate_in_predicate801 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_simplepredicate_in_predicate815 = new BitSet(new long[]{0x0000000000000010L}); + public static final BitSet FOLLOW_AND_in_predicate818 = new BitSet(new long[]{0x0000000000A00200L}); + public static final BitSet FOLLOW_simplepredicate_in_predicate820 = new BitSet(new long[]{0x0000000000000012L}); + public static final BitSet FOLLOW_simplepredicate_in_predicate845 = new BitSet(new long[]{0x0000000000400000L}); + public static final BitSet FOLLOW_OR_in_predicate848 = new BitSet(new long[]{0x0000000000A00200L}); + public static final BitSet FOLLOW_simplepredicate_in_predicate850 = new BitSet(new long[]{0x0000000000400002L}); + public static final BitSet FOLLOW_allowedpredicates_in_simplepredicate868 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_NEGATION_in_simplepredicate892 = new BitSet(new long[]{0x0000000000800200L}); + public static final BitSet FOLLOW_allowedpredicates_in_simplepredicate894 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_comparisonpredicate_in_allowedpredicates909 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_existspredicate_in_allowedpredicates913 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_betweenpredicate_in_allowedpredicates917 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_inpredicate_in_allowedpredicates921 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_matchespredicate_in_allowedpredicates925 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_comparisonpredicate931 = new BitSet(new long[]{0x00000000000C0D00L}); + public static final BitSet FOLLOW_comparisonoperator_in_comparisonpredicate933 = new BitSet(new long[]{0x0000000001800000L}); + public static final BitSet FOLLOW_value_in_comparisonpredicate935 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_EXISTS_in_existspredicate965 = new BitSet(new long[]{0x0000000000020000L}); + public static final BitSet FOLLOW_LEFTPAREN_in_existspredicate967 = new BitSet(new long[]{0x0000000008800000L}); + public static final BitSet FOLLOW_WS_in_existspredicate969 = new BitSet(new long[]{0x0000000000800000L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_existspredicate972 = new BitSet(new long[]{0x0000000002000000L}); + public static final BitSet FOLLOW_RIGHTPAREN_in_existspredicate974 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_betweenpredicate988 = new BitSet(new long[]{0x0000000000000020L}); + public static final BitSet FOLLOW_BETWEEN_in_betweenpredicate990 = new BitSet(new long[]{0x0000000000020000L}); + public static final BitSet FOLLOW_LEFTPAREN_in_betweenpredicate992 = new BitSet(new long[]{0x0000000009800000L}); + public static final BitSet FOLLOW_WS_in_betweenpredicate994 = new BitSet(new long[]{0x0000000001800000L}); + public static final BitSet FOLLOW_propertyvaluepair_in_betweenpredicate997 = new BitSet(new long[]{0x0000000002000000L}); + public static final BitSet FOLLOW_RIGHTPAREN_in_betweenpredicate999 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_inpredicate1015 = new BitSet(new long[]{0x0000000000010000L}); + public static final BitSet FOLLOW_IN_in_inpredicate1017 = new BitSet(new long[]{0x0000000000020000L}); + public static final BitSet FOLLOW_LEFTPAREN_in_inpredicate1019 = new BitSet(new long[]{0x0000000009800000L}); + public static final BitSet FOLLOW_WS_in_inpredicate1021 = new BitSet(new long[]{0x0000000001800000L}); + public static final BitSet FOLLOW_propertyvaluelist_in_inpredicate1024 = new BitSet(new long[]{0x0000000002000000L}); + public static final BitSet FOLLOW_RIGHTPAREN_in_inpredicate1026 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_matchespredicate1042 = new BitSet(new long[]{0x0000000000100000L}); + public static final BitSet FOLLOW_MATCHES_in_matchespredicate1044 = new BitSet(new long[]{0x0000000000020000L}); + public static final BitSet FOLLOW_LEFTPAREN_in_matchespredicate1046 = new BitSet(new long[]{0x0000000009800000L}); + public static final BitSet FOLLOW_WS_in_matchespredicate1048 = new BitSet(new long[]{0x0000000001800000L}); + public static final BitSet FOLLOW_value_in_matchespredicate1051 = new BitSet(new long[]{0x0000000002000000L}); + public static final BitSet FOLLOW_RIGHTPAREN_in_matchespredicate1053 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_value_in_propertyvaluepair1069 = new BitSet(new long[]{0x0000000000000080L}); + public static final BitSet FOLLOW_COMMA_in_propertyvaluepair1071 = new BitSet(new long[]{0x0000000001800000L}); + public static final BitSet FOLLOW_value_in_propertyvaluepair1073 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_value_in_propertyvaluelist1084 = new BitSet(new long[]{0x0000000000000082L}); + public static final BitSet FOLLOW_COMMA_in_propertyvaluelist1087 = new BitSet(new long[]{0x0000000001800000L}); + public static final BitSet FOLLOW_value_in_propertyvaluelist1089 = new BitSet(new long[]{0x0000000000000082L}); + public static final BitSet FOLLOW_PROPERTYVALUE_in_value1104 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_value1124 = new BitSet(new long[]{0x0000000000000002L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_selectClause1140 = new BitSet(new long[]{0x0000000000000082L}); + public static final BitSet FOLLOW_COMMA_in_selectClause1143 = new BitSet(new long[]{0x0000000000800000L}); + public static final BitSet FOLLOW_PROPERTYNAME_in_selectClause1145 = new BitSet(new long[]{0x0000000000000082L}); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/heartbeat/RenditionsDataCollector.java b/remote-api/src/main/java/org/alfresco/heartbeat/RenditionsDataCollector.java new file mode 100644 index 00000000000..d2ce91c411d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/heartbeat/RenditionsDataCollector.java @@ -0,0 +1,139 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.heartbeat; + +import org.alfresco.heartbeat.datasender.HBData; +import org.alfresco.heartbeat.jobs.HeartBeatJobScheduler; +import org.alfresco.repo.descriptor.DescriptorDAO; +import org.alfresco.repo.thumbnail.ThumbnailDefinition; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class collects rendition request counts for HeartBeat. A rendition (such as "doclib") is always to the same + * target mimetype, but there may be different source mimetypes. As a result that may be multiple sets of data with + * the same rendition. It is also likely there will be multiple renditions reported in the same batch of data. + *
    + *
  • Collector ID: acs.repository.renditions
  • + *
  • Data: + *
      + *
    • rendition: String - The name of the rendition.
    • + *
    • count: Integer - The number of times a rendition and sourceMimetype combination has been requested.
    • + *
    • sourceMimetype: String - The source mimetype for the rendition.
    • + *
    • targetMimetype: String - The target mimetype for the rendition.
    • + *
    + *
  • + *
+ * + * @author adavis + */ +public class RenditionsDataCollector extends HBBaseDataCollector implements InitializingBean +{ + private static final Log logger = LogFactory.getLog(RenditionsDataCollector.class); + + private DescriptorDAO currentRepoDescriptorDAO; + + // Map keyed on rendition id to a Map keyed on source mimetypes to a count of the number of times it has been requested. + private final Map> renditionRequests = new ConcurrentHashMap<>(); + + public RenditionsDataCollector(String collectorId, String collectorVersion, String cronExpression, + HeartBeatJobScheduler hbJobScheduler) + { + super(collectorId, collectorVersion, cronExpression, hbJobScheduler); + } + + public void setCurrentRepoDescriptorDAO(DescriptorDAO currentRepoDescriptorDAO) + { + this.currentRepoDescriptorDAO = currentRepoDescriptorDAO; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "currentRepoDescriptorDAO", currentRepoDescriptorDAO); + } + + public void recordRenditionRequest(ThumbnailDefinition rendition, String sourceMimetype) + { + // Increment the count of renditions. Atomically creates missing parts of the Map structures. + renditionRequests.computeIfAbsent(rendition, + k -> new ConcurrentHashMap<>()).computeIfAbsent(sourceMimetype, + k -> new AtomicInteger()).incrementAndGet(); + } + + @Override + public List collectData() + { + List collectedData = new LinkedList<>(); + + String systemId = this.currentRepoDescriptorDAO.getDescriptor().getId(); + String collectorId = this.getCollectorId(); + String collectorVersion = this.getCollectorVersion(); + Date timestamp = new Date(); + + // We don't mind if new renditions are added while we iterate, as we will pick them up next time. + for (ThumbnailDefinition rendition : renditionRequests.keySet()) + { + String renditionName = rendition.getName(); + String targetMimetype = rendition.getMimetype(); + for (Map.Entry entry: renditionRequests.remove(rendition).entrySet()) + { + String sourceMimetype = entry.getKey(); + AtomicInteger count = entry.getValue(); + + Map values = new HashMap<>(); + values.put("rendition", renditionName); + values.put("count", count.intValue()); + values.put("sourceMimetype", sourceMimetype); + values.put("targetMimetype", targetMimetype); + + // Decided it would be simpler to be able to combine results in Kibana from different nodes + // and days if the data was flattened (denormalized) out at this point. It is very likely + // that different nodes would have different sets of sourceMimetypes which would make summing + // the counts harder to do, if there was a single entry for each rendition with a nested + // structure for each sourceMimetype. + collectedData.add(new HBData(systemId, collectorId, collectorVersion, timestamp, values)); + + if (logger.isDebugEnabled()) + { + logger.debug(renditionName+" "+count+" "+sourceMimetype+" "+targetMimetype); + } + } + } + + return collectedData; + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/AbstractBaseUrlGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/AbstractBaseUrlGenerator.java new file mode 100644 index 00000000000..ae064a0122b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/AbstractBaseUrlGenerator.java @@ -0,0 +1,196 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.alfresco.repo.tenant.TenantUtil; + +/** + * Generates an OpenCMIS base url based on the request, repository id and binding. + * + * @author steveglover + * + */ +public abstract class AbstractBaseUrlGenerator implements BaseUrlGenerator +{ + private boolean overrideContext; + private String contextOverride; + private boolean overrideServletPath; + private String servletPathOverride; + private PathGenerator pathGenerator; + + public void setPathGenerator(PathGenerator pathGenerator) + { + this.pathGenerator = pathGenerator; + } + + public void setOverrideContext(boolean overrideContext) + { + this.overrideContext = overrideContext; + } + + private String fixup(String urlSegment) + { + StringBuilder sb = new StringBuilder(); + int beginIndex = 0; + int endIndex = urlSegment.length(); + if(urlSegment != null) + { + if(!urlSegment.equals("") && !urlSegment.startsWith("/")) + { + sb.append("/"); + } + if(urlSegment.endsWith("/")) + { + endIndex -= 1; + } + } + sb.append(urlSegment.substring(beginIndex, endIndex)); + return sb.toString(); + } + + public void setContextOverride(String contextOverride) + { + this.contextOverride = fixup(contextOverride); + } + + public void setOverrideServletPath(boolean overrideServletPath) + { + this.overrideServletPath = overrideServletPath; + } + + public void setServletPathOverride(String servletPathOverride) + { + this.servletPathOverride = fixup(servletPathOverride); + } + + protected abstract String getServerPath(HttpServletRequest request); + + public String getContextPath(HttpServletRequest httpReq) + { + if(overrideContext) + { + return contextOverride; + } + else + { + return httpReq.getContextPath(); + } + } + + public String getServletPath(HttpServletRequest req) + { + if(overrideServletPath) + { + return servletPathOverride; + } + else + { + return req.getServletPath(); + } + } + + @Override + public String getRequestURI(HttpServletRequest req, String repositoryId, String operation, String id) + { + StringBuilder url = new StringBuilder(); + + String contextPath = getContextPath(req); + if(contextPath != null && !contextPath.equals("")) + { + url.append(contextPath); + } + + String servletPath = getServletPath(req); + if(servletPath != null && !servletPath.equals("")) + { + url.append(servletPath); + url.append("/"); + } + + if(url.length() == 0 || url.charAt(0) != '/') + { + url.append("/"); + } + + if(repositoryId != null) + { + url.append(repositoryId == null ? TenantUtil.DEFAULT_TENANT : repositoryId); + url.append("/"); + } + + if(operation != null) + { + url.append(operation); + url.append("/"); + } + + if(id != null) + { + url.append(id); + } + + int length = url.length(); + if(length > 0 && url.charAt(length - 1) == '/') + { + url.deleteCharAt(length - 1); + } + + return url.toString(); + } + + @Override + public String getBaseUrl(HttpServletRequest req, String repositoryId, Binding binding) + { + StringBuilder url = new StringBuilder(); + String serverPath = getServerPath(req); + url.append(serverPath); + + String contextPath = getContextPath(req); + if(contextPath != null && !contextPath.equals("")) + { + url.append(contextPath); + } + + String servletPath = getServletPath(req); + if(servletPath != null && !servletPath.equals("")) + { + url.append(servletPath); + url.append("/"); + } + + if(url.length() > 0 && url.charAt(url.length() - 1) != '/') + { + url.append("/"); + } + + pathGenerator.generatePath(req, url, repositoryId, binding); + + return url.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/AtomPubCMISDispatcher.java b/remote-api/src/main/java/org/alfresco/opencmis/AtomPubCMISDispatcher.java new file mode 100644 index 00000000000..0ec6c3a6584 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/AtomPubCMISDispatcher.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServlet; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.apache.chemistry.opencmis.server.impl.atompub.CmisAtomPubServlet; + +/** + * Dispatches OpenCMIS requests to the OpenCMIS AtomPub servlet. + * + * @author steveglover + * + */ +public class AtomPubCMISDispatcher extends CMISServletDispatcher +{ + @Override + protected Binding getBinding() + { + return Binding.atom; + } + + protected HttpServlet getServlet() + { + HttpServlet servlet = new CmisAtomPubServlet(); + return servlet; + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/BaseUrlGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/BaseUrlGenerator.java new file mode 100644 index 00000000000..e679829d76d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/BaseUrlGenerator.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; + +/** + * Generates an OpenCMIS base url based on the request, repository id and binding. + * + * @author steveglover + * + */ +public interface BaseUrlGenerator +{ + String getContextPath(HttpServletRequest httpReq); + String getServletPath(HttpServletRequest req); + String getBaseUrl(HttpServletRequest req, String repositoryId, Binding binding); + String getRequestURI(HttpServletRequest req, String repositoryId, String operation, String id); +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/BrowserCMISDispatcher.java b/remote-api/src/main/java/org/alfresco/opencmis/BrowserCMISDispatcher.java new file mode 100644 index 00000000000..6092a1a9438 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/BrowserCMISDispatcher.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServlet; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.apache.chemistry.opencmis.server.impl.browser.CmisBrowserBindingServlet; + +/** + * Dispatches OpenCMIS requests to the OpenCMIS Browser Binding servlet. + * + * @author steveglover + * + */ +public class BrowserCMISDispatcher extends CMISServletDispatcher +{ + @Override + protected Binding getBinding() + { + return Binding.browser; + } + + protected HttpServlet getServlet() + { + HttpServlet servlet = new CmisBrowserBindingServlet(); + return servlet; + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcher.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcher.java new file mode 100644 index 00000000000..b5c93468ee2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcher.java @@ -0,0 +1,42 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.io.IOException; + +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Dispatches OpenCMIS requests to the appropriate handler. + * + * @author steveglover + * + */ +public interface CMISDispatcher +{ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException; +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcherRegistry.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcherRegistry.java new file mode 100644 index 00000000000..f89a2d0a9b1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcherRegistry.java @@ -0,0 +1,120 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import org.apache.chemistry.opencmis.commons.enums.BindingType; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * A registry of OpenCMIS bindings to dispatchers. + * + * @author steveglover + * + */ +public interface CMISDispatcherRegistry +{ + /* + * Supported CMIS bindings + */ + public static enum Binding + { + atom, browser; + + public BindingType getOpenCmisBinding() + { + BindingType bindingType = null; + + if(this == atom) + { + bindingType = BindingType.ATOMPUB; + } + else if(this == browser) + { + bindingType = BindingType.BROWSER; + } + + return bindingType; + } + }; + + public static class Endpoint + { + private Binding binding; + private String version; + + public Endpoint(Binding binding, String version) + { + super(); + this.binding = binding; + this.version = version; + } + + public Binding getBinding() + { + return binding; + } + + public String getVersion() + { + return version; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + + ((binding == null) ? 0 : binding.hashCode()); + result = prime * result + + ((version == null) ? 0 : version.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; + Endpoint other = (Endpoint) obj; + if (binding != other.binding) + return false; + if (version == null) { + if (other.version != null) + return false; + } else if (!version.equals(other.version)) + return false; + return true; + } + } + + public void registerDispatcher(Endpoint endpoint, CMISDispatcher dispatcher); + public CMISDispatcher getDispatcher(WebScriptRequest req); +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcherRegistryImpl.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcherRegistryImpl.java new file mode 100644 index 00000000000..468dddc8a14 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISDispatcherRegistryImpl.java @@ -0,0 +1,84 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * A registry of OpenCMIS bindings to dispatchers. + * + * @author steveglover + * + */ +public class CMISDispatcherRegistryImpl implements CMISDispatcherRegistry +{ + private Map registry = new HashMap(); + + @Override + public void registerDispatcher(Endpoint endpoint, CMISDispatcher dispatcher) + { + registry.put(endpoint, dispatcher); + } + + @Override + public CMISDispatcher getDispatcher(WebScriptRequest req) + { + CMISDispatcher dispatcher = null; + + Match match = req.getServiceMatch(); + Map templateVars = match.getTemplateVars(); + String bindingStr = templateVars.get("binding"); + String apiVersion = templateVars.get("apiVersion"); + if(bindingStr != null && apiVersion != null) + { + Binding binding = null; + try + { + binding = Binding.valueOf(bindingStr); + } + catch(IllegalArgumentException e) + { + // nothing to do, binding remains null + } + + if(binding != null) + { + Endpoint endpoint = new Endpoint(binding, apiVersion); + dispatcher = registry.get(endpoint); + } + else + { + // TODO + } + } + + return dispatcher; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISHttpServletRequest.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISHttpServletRequest.java new file mode 100644 index 00000000000..9a7ea910dfd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISHttpServletRequest.java @@ -0,0 +1,643 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.web.scripts.TenantWebScriptServletRequest; +import org.alfresco.service.descriptor.Descriptor; +import org.apache.chemistry.opencmis.commons.impl.Constants; +import org.apache.chemistry.opencmis.server.shared.Dispatcher; +import org.apache.commons.collections.map.HashedMap; +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + +/** + * Wraps an OpenCMIS HttpServletRequest, mapping urls and adding servlet attributes specific to the Alfresco implementation of OpenCMIS. + */ +@SuppressWarnings("rawtypes") +public class CMISHttpServletRequest implements HttpServletRequest +{ + protected WebScriptRequest req; + protected HttpServletRequest httpReq; + protected String networkId; + protected String operation; + protected String id; // object id (or path for browser binding) + protected String serviceName; + protected BaseUrlGenerator baseUrlGenerator; + protected Binding binding; + protected Descriptor currentDescriptor; + + public CMISHttpServletRequest(WebScriptRequest req, String serviceName, BaseUrlGenerator baseUrlGenerator, Binding binding, Descriptor currentDescriptor, + TenantAdminService tenantAdminService) + { + this.req = req; + this.serviceName = serviceName; + this.baseUrlGenerator = baseUrlGenerator; + this.binding = binding; + + String pathInfo = req.getPathInfo(); + WebScriptRequest baseReq = getBaseRequest(req); + if(!pathInfo.startsWith("/cmis") && (baseReq instanceof TenantWebScriptServletRequest)) + { + TenantWebScriptServletRequest servletReq = (TenantWebScriptServletRequest)baseReq; + + String tenant = servletReq.getTenant(); + if(tenant.equalsIgnoreCase(TenantUtil.DEFAULT_TENANT)) + { + String user = AuthenticationUtil.getFullyAuthenticatedUser(); + String domain = tenantAdminService.getUserDomain(user); + if(domain == null || domain.equals(TenantService.DEFAULT_DOMAIN)) + { + this.networkId = tenant; + } + else + { + this.networkId = domain; + } + } + else + { + this.networkId = tenant; + } + } + + Match match = req.getServiceMatch(); + Map templateVars = match.getTemplateVars(); + + HttpServletRequest httpReq = WebScriptServletRuntime.getHttpServletRequest(req); + this.httpReq = httpReq; + this.operation = templateVars.get("operation"); + this.id = templateVars.get("id"); + + addAttributes(); + } + + /* + * Recursively unwrap req if it is a WrappingWebScriptRequest + */ + private WebScriptRequest getBaseRequest(WebScriptRequest req) + { + WebScriptRequest ret = req; + while(ret instanceof WrappingWebScriptRequest) + { + WrappingWebScriptRequest wrapping = (WrappingWebScriptRequest)req; + ret = wrapping.getNext(); + } + return ret; + } + + protected void addAttributes() + { + if(networkId != null) + { + httpReq.setAttribute(Constants.PARAM_REPOSITORY_ID, networkId); + } + httpReq.setAttribute("serviceName", serviceName); + } + + @Override + public Object getAttribute(String arg0) + { + if(arg0.equals(Dispatcher.BASE_URL_ATTRIBUTE)) + { + return baseUrlGenerator.getBaseUrl(this, networkId, binding); + } + else + { + return httpReq.getAttribute(arg0); + } + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration getAttributeNames() + { + Enumeration e = httpReq.getAttributeNames(); + List attrNames = new ArrayList(); + while(e.hasMoreElements()) + { + attrNames.add(e.nextElement()); + } + attrNames.add(Dispatcher.BASE_URL_ATTRIBUTE); + final Iterator it = attrNames.iterator(); + + return new Enumeration() + { + public boolean hasMoreElements() + { + return it.hasNext(); + } + + public Object nextElement() + { + return it.next(); + } + }; + } + + @Override + public String getCharacterEncoding() + { + return httpReq.getCharacterEncoding(); + } + + @Override + public int getContentLength() + { + return httpReq.getContentLength(); + } + + @Override + public String getContentType() + { + return httpReq.getContentType(); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + return httpReq.getInputStream(); + } + + @Override + public String getLocalAddr() + { + return httpReq.getLocalAddr(); + } + + @Override + public String getLocalName() + { + return httpReq.getLocalName(); + } + + @Override + public int getLocalPort() + { + return httpReq.getLocalPort(); + } + + @Override + public Locale getLocale() + { + return httpReq.getLocale(); + } + + @Override + public Enumeration getLocales() + { + return httpReq.getLocales(); + } + + @Override + public String getParameter(String arg0) + { + if(arg0.equals(Constants.PARAM_REPOSITORY_ID)) + { + return networkId; + } + return httpReq.getParameter(arg0); + } + + @SuppressWarnings("unchecked") + @Override + public Map getParameterMap() + { + Map map = httpReq.getParameterMap(); + Map ret = new HashedMap(map); + if(networkId != null) + { + ret.put(Constants.PARAM_REPOSITORY_ID, new String[] { networkId }); + } + return ret; + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration getParameterNames() + { + final Enumeration e = httpReq.getParameterNames(); + List l = new ArrayList(); + while(e.hasMoreElements()) + { + l.add(e.nextElement()); + } + if(networkId != null) + { + l.add(Constants.PARAM_REPOSITORY_ID); + } + final Iterator it = l.iterator(); + Enumeration ret = new Enumeration() + { + @Override + public boolean hasMoreElements() + { + return it.hasNext(); + } + + @Override + public Object nextElement() + { + return it.next(); + } + }; + + return ret; + } + + @Override + public String[] getParameterValues(String arg0) + { + return httpReq.getParameterValues(arg0); + } + + @Override + public String getProtocol() + { + return httpReq.getProtocol(); + } + + @Override + public BufferedReader getReader() throws IOException + { + return httpReq.getReader(); + } + + @SuppressWarnings("deprecation") + @Override + public String getRealPath(String arg0) + { + return httpReq.getRealPath(arg0); + } + + @Override + public String getRemoteAddr() + { + return httpReq.getRemoteAddr(); + } + + @Override + public String getRemoteHost() + { + return httpReq.getRemoteHost(); + } + + @Override + public int getRemotePort() + { + return httpReq.getRemotePort(); + } + + @Override + public RequestDispatcher getRequestDispatcher(String arg0) + { + return httpReq.getRequestDispatcher(arg0); + } + + @Override + public String getScheme() + { + return httpReq.getScheme(); + } + + @Override + public String getServerName() + { + return httpReq.getServerName(); + } + + @Override + public int getServerPort() + { + return httpReq.getServerPort(); + } + + @Override + public boolean isSecure() + { + return httpReq.isSecure(); + } + + @Override + public void removeAttribute(String arg0) + { + httpReq.removeAttribute(arg0); + } + + @Override + public void setAttribute(String arg0, Object arg1) + { + httpReq.setAttribute(arg0, arg1); + } + + @Override + public void setCharacterEncoding(String arg0) throws UnsupportedEncodingException + { + httpReq.setCharacterEncoding(arg0); + } + + @Override + public String getAuthType() + { + return httpReq.getAuthType(); + } + + @Override + public String getContextPath() + { + String contextPath = baseUrlGenerator.getContextPath(httpReq); + return contextPath; + } + + @Override + public Cookie[] getCookies() + { + return httpReq.getCookies(); + } + + @Override + public long getDateHeader(String arg0) + { + return httpReq.getDateHeader(arg0); + } + + @Override + public String getHeader(String arg0) + { + return httpReq.getHeader(arg0); + } + + @Override + public Enumeration getHeaderNames() + { + return httpReq.getHeaderNames(); + } + + @Override + public Enumeration getHeaders(String arg0) + { + return httpReq.getHeaders(arg0); + } + + @Override + public int getIntHeader(String arg0) + { + return httpReq.getIntHeader(arg0); + } + + @Override + public String getMethod() + { + return httpReq.getMethod(); + } + + @Override + public String getPathInfo() + { + StringBuilder sb = new StringBuilder("/"); + sb.append(networkId == null ? TenantUtil.DEFAULT_TENANT : networkId); + if(operation != null) + { + sb.append("/"); + sb.append(operation); + } + return sb.toString(); + } + + @Override + public String getPathTranslated() + { + return httpReq.getPathTranslated(); + } + + @Override + public String getQueryString() + { + StringBuilder queryString = new StringBuilder(); + String reqQueryString = httpReq.getQueryString(); + + if(networkId != null && networkId.length() > 0) + { + if (reqQueryString != null) + { + queryString.append(reqQueryString + "&"); + } + queryString.append("repositoryId=" + networkId); + if(operation == null || operation.isEmpty()) + { + queryString.append("&cmisselector="); + queryString.append(Constants.SELECTOR_REPOSITORY_INFO); + } + return queryString.toString(); + } + return reqQueryString; + } + + @Override + public String getRemoteUser() + { + return httpReq.getRemoteUser(); + } + + @Override + public String getRequestURI() + { + String requestURI = baseUrlGenerator.getRequestURI(httpReq, networkId, operation, id); + return requestURI; + } + + @Override + public StringBuffer getRequestURL() + { + return httpReq.getRequestURL(); + } + + @Override + public String getRequestedSessionId() + { + return httpReq.getRequestedSessionId(); + } + + @Override + public String getServletPath() + { + String servletPath = baseUrlGenerator.getServletPath(httpReq); + return servletPath; + } + + @Override + public HttpSession getSession() + { + return httpReq.getSession(); + } + + @Override + public HttpSession getSession(boolean arg0) + { + return httpReq.getSession(arg0); + } + + @Override + public Principal getUserPrincipal() + { + return httpReq.getUserPrincipal(); + } + + @Override + public boolean isRequestedSessionIdFromCookie() + { + return httpReq.isRequestedSessionIdFromCookie(); + } + + @Override + public boolean isRequestedSessionIdFromURL() + { + return httpReq.isRequestedSessionIdFromURL(); + } + + @Override + public boolean isRequestedSessionIdFromUrl() + { + return httpReq.isRequestedSessionIdFromURL(); + } + + @Override + public boolean isRequestedSessionIdValid() + { + return httpReq.isRequestedSessionIdValid(); + } + + @Override + public boolean isUserInRole(String arg0) + { + return httpReq.isUserInRole(arg0); + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException + { + return httpReq.authenticate(response); + } + + @Override + public void login(String username, String password) throws ServletException + { + httpReq.login(username, password); + } + + @Override + public void logout() throws ServletException + { + httpReq.logout(); + } + + @Override + public Collection getParts() throws IOException, ServletException + { + return httpReq.getParts(); + } + + @Override + public Part getPart(String name) throws IOException, ServletException + { + return httpReq.getPart(name); + } + + @Override + public ServletContext getServletContext() + { + return httpReq.getServletContext(); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException + { + return httpReq.startAsync(); + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException + { + return httpReq.startAsync(servletRequest, servletResponse); + } + + @Override + public boolean isAsyncStarted() + { + return httpReq.isAsyncStarted(); + } + + @Override + public boolean isAsyncSupported() + { + return httpReq.isAsyncSupported(); + } + + @Override + public AsyncContext getAsyncContext() + { + return httpReq.getAsyncContext(); + } + + @Override + public DispatcherType getDispatcherType() + { + return httpReq.getDispatcherType(); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISHttpServletResponse.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISHttpServletResponse.java new file mode 100644 index 00000000000..d458b73d6b0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISHttpServletResponse.java @@ -0,0 +1,302 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Set; + +/** + * Wraps an OpenCMIS HttpServletResponse for specific mapping to the Alfresco implementation of OpenCMIS. + * + * @author janv + */ +public class CMISHttpServletResponse implements HttpServletResponse +{ + protected HttpServletResponse httpResp; + + protected Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf + + private final static String HDR_CONTENT_DISPOSITION = "Content-Disposition"; + + private final static String ATTACHMENT = "attachment"; + private final static String INLINE = "inline"; + + public CMISHttpServletResponse(WebScriptResponse res, Set nonAttachContentTypes) + { + httpResp = WebScriptServletRuntime.getHttpServletResponse(res); + this.nonAttachContentTypes = nonAttachContentTypes; + } + + @Override + public void addCookie(Cookie cookie) + { + httpResp.addCookie(cookie); + } + + @Override + public boolean containsHeader(String name) + { + return httpResp.containsHeader(name); + } + + @Override + public String encodeURL(String url) + { + return httpResp.encodeURL(url); + } + + @Override + public String encodeRedirectURL(String url) + { + return httpResp.encodeRedirectURL(url); + } + + @Override + public String encodeUrl(String url) + { + return encodeUrl(url); + } + + @Override + public String encodeRedirectUrl(String url) + { + return httpResp.encodeRedirectUrl(url); + } + + @Override + public void sendError(int sc, String msg) throws IOException + { + httpResp.sendError(sc, msg); + } + + @Override + public void sendError(int sc) throws IOException + { + httpResp.sendError(sc); + } + + @Override + public void sendRedirect(String location) throws IOException + { + httpResp.sendRedirect(location); + } + + @Override + public void setDateHeader(String name, long date) + { + httpResp.setDateHeader(name, date); + } + + @Override + public void addDateHeader(String name, long date) + { + httpResp.addDateHeader(name, date); + } + + @Override + public void setHeader(String name, String value) + { + httpResp.setHeader(name, getStringHeaderValue(name, value, httpResp.getContentType())); + } + + @Override + public void addHeader(String name, String value) + { + httpResp.addHeader(name, getStringHeaderValue(name, value, httpResp.getContentType())); + } + + + + private String getStringHeaderValue(String name, String value, String contentType) + { + if (HDR_CONTENT_DISPOSITION.equals(name)) + { + if (! nonAttachContentTypes.contains(contentType)) + { + if (value.startsWith(INLINE)) + { + // force attachment + value = ATTACHMENT+value.substring(INLINE.length()); + } + else if (! value.startsWith(ATTACHMENT)) + { + throw new AlfrescoRuntimeException("Unexpected - header could not be set: "+name+" = "+value); + } + } + } + + return value; + } + + @Override + public void setIntHeader(String name, int value) + { + httpResp.setIntHeader(name, value); + } + + @Override + public void addIntHeader(String name, int value) + { + httpResp.addIntHeader(name, value); + } + + @Override + public void setStatus(int sc) + { + httpResp.setStatus(sc); + } + + @Override + public void setStatus(int sc, String sm) + { + httpResp.setStatus(sc, sm); + } + + @Override + public int getStatus() + { + return httpResp.getStatus(); + } + + @Override + public String getHeader(String name) + { + return httpResp.getHeader(name); + } + + @Override + public Collection getHeaders(String name) + { + return httpResp.getHeaders(name); + } + + @Override + public Collection getHeaderNames() + { + return httpResp.getHeaderNames(); + } + + @Override + public String getCharacterEncoding() + { + return httpResp.getCharacterEncoding(); + } + + @Override + public String getContentType() + { + return httpResp.getContentType(); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException + { + return httpResp.getOutputStream(); + } + + @Override + public PrintWriter getWriter() throws IOException + { + return httpResp.getWriter(); + } + + @Override + public void setCharacterEncoding(String charset) + { + httpResp.setCharacterEncoding(charset); + } + + @Override + public void setContentLength(int len) + { + httpResp.setContentLength(len); + } + + @Override + public void setContentType(String type) + { + httpResp.setContentType(type); + } + + @Override + public void setBufferSize(int size) + { + httpResp.setBufferSize(size); + } + + @Override + public int getBufferSize() + { + return httpResp.getBufferSize(); + } + + @Override + public void flushBuffer() throws IOException + { + httpResp.flushBuffer(); + } + + @Override + public void resetBuffer() + { + httpResp.resetBuffer(); + } + + @Override + public boolean isCommitted() + { + return httpResp.isCommitted(); + } + + @Override + public void reset() + { + httpResp.reset(); + } + + @Override + public void setLocale(Locale loc) + { + httpResp.setLocale(loc); + } + + @Override + public Locale getLocale() + { + return httpResp.getLocale(); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISServletDispatcher.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISServletDispatcher.java new file mode 100644 index 00000000000..d803a919512 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISServletDispatcher.java @@ -0,0 +1,601 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRegistration; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.alfresco.opencmis.CMISDispatcherRegistry.Endpoint; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.descriptor.DescriptorService; +import org.apache.chemistry.opencmis.commons.enums.CmisVersion; +import org.apache.chemistry.opencmis.commons.server.CmisServiceFactory; +import org.apache.chemistry.opencmis.server.impl.CmisRepositoryContextListener; +import org.apache.chemistry.opencmis.server.impl.atompub.CmisAtomPubServlet; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + +/** + * Dispatches OpenCMIS requests to a servlet e.g. the OpenCMIS AtomPub servlet. + * + * @author steveglover + * + */ +public abstract class CMISServletDispatcher implements CMISDispatcher +{ + private DescriptorService descriptorService; + private Descriptor currentDescriptor; + protected CmisServiceFactory cmisServiceFactory; + protected HttpServlet servlet; + protected CMISDispatcherRegistry registry; + protected String serviceName; + protected BaseUrlGenerator baseUrlGenerator; + protected String version; + protected CmisVersion cmisVersion; + protected TenantAdminService tenantAdminService; + + private Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf + + public void setTenantAdminService(TenantAdminService tenantAdminService) + { + this.tenantAdminService = tenantAdminService; + } + + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + public void setVersion(String version) + { + this.version = version; + } + + public void setBaseUrlGenerator(BaseUrlGenerator baseUrlGenerator) + { + this.baseUrlGenerator = baseUrlGenerator; + } + + public void setRegistry(CMISDispatcherRegistry registry) + { + this.registry = registry; + } + + public void setCmisServiceFactory(CmisServiceFactory cmisServiceFactory) + { + this.cmisServiceFactory = cmisServiceFactory; + } + + public void setServiceName(String serviceName) + { + this.serviceName = serviceName; + } + + public String getServiceName() + { + return serviceName; + } + + public void setCmisVersion(String cmisVersion) + { + this.cmisVersion = CmisVersion.fromValue(cmisVersion); + } + + public void setNonAttachContentTypes(Set nonAttachWhiteList) + { + this.nonAttachContentTypes = nonAttachWhiteList; + } + + protected synchronized Descriptor getCurrentDescriptor() + { + if(this.currentDescriptor == null) + { + this.currentDescriptor = descriptorService.getCurrentRepositoryDescriptor(); + } + + return this.currentDescriptor; + } + + public void init() + { + Endpoint endpoint = new Endpoint(getBinding(), version); + registry.registerDispatcher(endpoint, this); + + try + { + // fake the CMIS servlet + ServletConfig config = getServletConfig(); + this.servlet = getServlet(); + servlet.init(config); + } + catch(ServletException e) + { + throw new AlfrescoRuntimeException("Failed to initialise CMIS servlet dispatcher", e); + } + } + + /* + * Implement getBinding to provide the appropriate CMIS binding. + */ + protected abstract Binding getBinding(); + + /* + * Implement getServlet to provide the appropriate servlet implementation. + */ + protected abstract HttpServlet getServlet(); + + protected Object getServletAttribute(String attrName) + { + if(attrName.equals(CmisRepositoryContextListener.SERVICES_FACTORY)) + { + return cmisServiceFactory; + } + + return null; + } + + protected ServletConfig getServletConfig() + { + ServletConfig config = new CMISServletConfig(); + return config; + } + + protected CMISHttpServletRequest getHttpRequest(WebScriptRequest req) + { + String serviceName = getServiceName(); + CMISHttpServletRequest httpReqWrapper = new CMISHttpServletRequest(req, serviceName, baseUrlGenerator, + getBinding(), currentDescriptor, tenantAdminService); + return httpReqWrapper; + } + + protected CMISHttpServletResponse getHttpResponse(WebScriptResponse res) + { + CMISHttpServletResponse httpResWrapper = new CMISHttpServletResponse(res, nonAttachContentTypes); + + return httpResWrapper; + } + + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + try + { + // wrap request & response + CMISHttpServletResponse httpResWrapper = getHttpResponse(res); + CMISHttpServletRequest httpReqWrapper = getHttpRequest(req); + + servlet.service(httpReqWrapper, httpResWrapper); + } + catch(ServletException e) + { + throw new AlfrescoRuntimeException("", e); + } + } + + /** + * Fake a CMIS servlet config. + * + * @author steveglover + * + */ + @SuppressWarnings("rawtypes") + private class CMISServletConfig implements ServletConfig + { + private List parameterNames = new ArrayList(); + + @SuppressWarnings("unchecked") + CMISServletConfig() + { + parameterNames.add(CmisAtomPubServlet.PARAM_CALL_CONTEXT_HANDLER); + parameterNames.add(CmisAtomPubServlet.PARAM_CMIS_VERSION); + } + + @Override + public String getInitParameter(String arg0) + { + if(arg0.equals(CmisAtomPubServlet.PARAM_CALL_CONTEXT_HANDLER)) + { + return PublicApiCallContextHandler.class.getName(); + } + else if(arg0.equals(CmisAtomPubServlet.PARAM_CMIS_VERSION)) + { + return (cmisVersion != null ? cmisVersion.value() : CmisVersion.CMIS_1_0.value()); + } + return null; + } + + @Override + public Enumeration getInitParameterNames() + { + final Iterator it = parameterNames.iterator(); + + Enumeration e = new Enumeration() + { + @Override + public boolean hasMoreElements() + { + return it.hasNext(); + } + + @Override + public Object nextElement() + { + return it.next(); + } + }; + return e; + } + + // fake a servlet context. Note that getAttribute is the only method that the servlet uses, + // hence the other methods are not implemented. + @Override + public ServletContext getServletContext() + { + return new ServletContext() + { + + @Override + public Object getAttribute(String arg0) + { + return getServletAttribute(arg0); + } + + @Override + public Enumeration getAttributeNames() + { + return null; + } + + @Override + public String getContextPath() + { + return null; + } + + @Override + public ServletContext getContext(String arg0) + { + return null; + } + + @Override + public String getInitParameter(String arg0) + { + return null; + } + + @Override + public Enumeration getInitParameterNames() + { + return null; + } + + @Override + public boolean setInitParameter(String name, String value) + { + return false; + } + + @Override + public int getMajorVersion() + { + return 0; + } + + @Override + public String getMimeType(String arg0) + { + return null; + } + + @Override + public int getMinorVersion() + { + return 0; + } + + @Override + public int getEffectiveMajorVersion() + { + return 0; + } + + @Override + public int getEffectiveMinorVersion() + { + return 0; + } + + @Override + public RequestDispatcher getNamedDispatcher(String arg0) + { + return null; + } + + @Override + public String getRealPath(String arg0) + { + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(String arg0) + { + return null; + } + + @Override + public URL getResource(String arg0) throws MalformedURLException + { + return null; + } + + @Override + public InputStream getResourceAsStream(String arg0) + { + return null; + } + + @Override + public Set getResourcePaths(String arg0) + { + return null; + } + + @Override + public String getServerInfo() + { + return null; + } + + @Override + public Servlet getServlet(String arg0) throws ServletException + { + return null; + } + + @Override + public String getServletContextName() + { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) + { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) + { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Class servletClass) + { + return null; + } + + @Override + public T createServlet(Class clazz) throws ServletException + { + return null; + } + + @Override + public ServletRegistration getServletRegistration(String servletName) + { + return null; + } + + @Override + public Map getServletRegistrations() + { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) + { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) + { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) + { + return null; + } + + @Override + public T createFilter(Class clazz) throws ServletException + { + return null; + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) + { + return null; + } + + @Override + public Map getFilterRegistrations() + { + return null; + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + return null; + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) + { + + } + + @Override + public Set getDefaultSessionTrackingModes() + { + return null; + } + + @Override + public Set getEffectiveSessionTrackingModes() + { + return null; + } + + @Override + public void addListener(String className) + { + + } + + @Override + public void addListener(T t) + { + + } + + @Override + public void addListener(Class listenerClass) + { + + } + + @Override + public T createListener(Class clazz) throws ServletException + { + return null; + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + return null; + } + + @Override + public ClassLoader getClassLoader() + { + return null; + } + + @Override + public void declareRoles(String... roleNames) + { + + } + + @Override + public Enumeration getServletNames() + { + return null; + } + + @Override + public Enumeration getServlets() + { + return null; + } + + @Override + public void log(String arg0) + { + } + + @Override + public void log(Exception arg0, String arg1) + { + } + + @Override + public void log(String arg0, Throwable arg1) + { + } + + @Override + public void removeAttribute(String arg0) + { + } + + @Override + public void setAttribute(String arg0, Object arg1) + { + } + }; + } + + @Override + public String getServletName() + { + return "OpenCMIS"; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/CMISWebScript.java b/remote-api/src/main/java/org/alfresco/opencmis/CMISWebScript.java new file mode 100644 index 00000000000..beddd717f75 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/CMISWebScript.java @@ -0,0 +1,61 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.io.IOException; + +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * An Alfresco web script that handles dispatch of OpenCMIS requests. + * + * @author steveglover + * + */ +public class CMISWebScript extends AbstractWebScript +{ + private CMISDispatcherRegistry registry; + + public void setRegistry(CMISDispatcherRegistry registry) + { + this.registry = registry; + } + + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + CMISDispatcher dispatcher = registry.getDispatcher(req); + if(dispatcher == null) + { + res.setStatus(404); + } + else + { + dispatcher.execute(req, res); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/DefaultBaseUrlGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/DefaultBaseUrlGenerator.java new file mode 100644 index 00000000000..098c7223826 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/DefaultBaseUrlGenerator.java @@ -0,0 +1,74 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +/** + * Generates an OpenCMIS base url based on the request, repository id and binding. The url scheme, host and port + * are overridden by a property from repository.properties or in an override file. + * + * @author steveglover + * + */ +public class DefaultBaseUrlGenerator extends AbstractBaseUrlGenerator +{ + private boolean overrideServer; + private String serverOverride; + + public DefaultBaseUrlGenerator() + { + } + + public void setOverrideServer(boolean overrideServer) + { + this.overrideServer = overrideServer; + } + + public void setServerOverride(String serverOverride) + { + this.serverOverride = serverOverride; + } + + protected String getServerPath(HttpServletRequest request) + { + if(overrideServer) + { + return serverOverride; + } + else + { + StringBuilder sb = new StringBuilder(); + sb.append(request.getScheme()); + sb.append("://"); + sb.append(request.getServerName()); + sb.append(":"); + sb.append(request.getServerPort()); + return sb.toString(); + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/DefaultPathGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/DefaultPathGenerator.java new file mode 100644 index 00000000000..b5b59f7f23d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/DefaultPathGenerator.java @@ -0,0 +1,55 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.alfresco.repo.tenant.TenantUtil; + +/** + * Default generator for OpenCMIS paths based on the repositoryId and binding. + * + * @author steveglover + * + */ +public class DefaultPathGenerator implements PathGenerator +{ + public void generatePath(HttpServletRequest req, StringBuilder url, String repositoryId, Binding binding) + { + url.append(binding.toString()); + url.append("/"); + if(repositoryId != null) + { + url.append(repositoryId); + } + else + { + url.append(TenantUtil.DEFAULT_TENANT); + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PathGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/PathGenerator.java new file mode 100644 index 00000000000..f7031cdc476 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PathGenerator.java @@ -0,0 +1,41 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; + +/** + * Generates an OpenCMIS path based on the repositoryId and binding. + * + * @author steveglover + * + */ +public interface PathGenerator +{ + public void generatePath(HttpServletRequest req, StringBuilder url, String repositoryId, Binding binding); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/opencmis/ProxyBaseUrlGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/ProxyBaseUrlGenerator.java new file mode 100644 index 00000000000..2b5c25bbb6f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/ProxyBaseUrlGenerator.java @@ -0,0 +1,101 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +/** + * Generates an OpenCMIS base url based on the request, repository id and binding. The url scheme, host and port + * are overridden by any proxy http header parameters, if present. + * + * @author steveglover + * + */ +public class ProxyBaseUrlGenerator extends AbstractBaseUrlGenerator +{ + public static final String FORWARDED_HOST_HEADER = "X-Forwarded-Host"; + public static final String FORWARDED_PROTO_HEADER = "X-Forwarded-Proto"; + public static final String HTTPS_SCHEME = "https"; + public static final String HTTP_SCHEME = "http"; + + @Override + protected String getServerPath(HttpServletRequest request) + { + String scheme = request.getHeader(FORWARDED_PROTO_HEADER); + String serverName; + int serverPort; + + if (!HTTP_SCHEME.equalsIgnoreCase(scheme) && !HTTPS_SCHEME.equalsIgnoreCase(scheme)) + { + scheme = request.getScheme(); + } + + serverName = request.getServerName(); + serverPort = request.getServerPort(); + + String host = request.getHeader(FORWARDED_HOST_HEADER); + if ((host != null) && (host.length() > 0)) + { + int index = host.indexOf(':'); + if (index < 0) + { + serverName = host; + serverPort = getDefaultPort(scheme); + } + else + { + serverName = host.substring(0, index); + try + { + serverPort = Integer.parseInt(host.substring(index + 1)); + } + catch (NumberFormatException e) + { + serverPort = getDefaultPort(scheme); + } + } + } + + StringBuilder sb = new StringBuilder(); + sb.append(scheme); + sb.append("://"); + sb.append(serverName); + sb.append(":"); + sb.append(serverPort); + return sb.toString(); + } + + private int getDefaultPort(String scheme) + { + if (HTTPS_SCHEME.equalsIgnoreCase(scheme)) + { + return 443; + } + + return 80; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAlfrescoCmisService.java b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAlfrescoCmisService.java new file mode 100644 index 00000000000..aa5efe6beb6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAlfrescoCmisService.java @@ -0,0 +1,225 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.tenant.Network; +import org.alfresco.repo.tenant.NetworksService; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; +import org.apache.chemistry.opencmis.commons.data.Acl; +import org.apache.chemistry.opencmis.commons.data.ContentStream; +import org.apache.chemistry.opencmis.commons.data.ExtensionsData; +import org.apache.chemistry.opencmis.commons.data.Properties; +import org.apache.chemistry.opencmis.commons.data.RepositoryInfo; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException; +import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryInfoImpl; +import org.apache.chemistry.opencmis.commons.spi.Holder; + +/** + * Override OpenCMIS service object - for public api + * + * @author sglover + * @since PublicApi1.0 + */ +public class PublicApiAlfrescoCmisService extends AlfrescoCmisServiceImpl +{ + protected CMISConnector connector; + protected TenantAdminService tenantAdminService; + protected NetworksService networksService; + + public PublicApiAlfrescoCmisService(CMISConnector connector, TenantAdminService tenantAdminService, NetworksService networksService) + { + super(connector); + + this.connector = connector; + this.networksService = networksService; + this.tenantAdminService = tenantAdminService; + } + + @Override + public String create(String repositoryId, Properties properties, String folderId, + ContentStream contentStream, VersioningState versioningState, + List policies, ExtensionsData extension) + { + FileFilterMode.setClient(Client.cmis); + try + { + return super.create( + repositoryId, + properties, + folderId, + contentStream, + versioningState, + policies, + extension); + } + finally + { + FileFilterMode.clearClient(); + } + } + + + /** + * Overridden to capture content upload for publishing to analytics service. + */ + @Override + public String createDocument(String repositoryId, Properties properties, String folderId, + ContentStream contentStream, VersioningState versioningState, + List policies, Acl addAces, Acl removeAces, ExtensionsData extension) + { + String newId = super.createDocument( + repositoryId, + properties, + folderId, + contentStream, + versioningState, + policies, + addAces, + removeAces, + extension); + return newId; + } + + /** + * Overridden to capture content upload for publishing to analytics service. + */ + @Override + public void setContentStream(String repositoryId, Holder objectId, + Boolean overwriteFlag, Holder changeToken, ContentStream contentStream, + ExtensionsData extension) + { + FileFilterMode.setClient(Client.cmis); + try + { + super.setContentStream(repositoryId, objectId, overwriteFlag, changeToken, contentStream, extension); + } + finally + { + FileFilterMode.clearClient(); + } + } + + @Override + public List getRepositoryInfos(ExtensionsData extension) + { + // for currently authenticated user + PagingResults networks = networksService.getNetworks(new PagingRequest(0, Integer.MAX_VALUE)); + List page = networks.getPage(); + final List repoInfos = new ArrayList(page.size() + 1); + for (Network network : page) + { + repoInfos.add(getRepositoryInfo(network)); + } + return repoInfos; + } + + @Override + public RepositoryInfo getRepositoryInfo(String repositoryId, ExtensionsData extension) + { + Network network = null; + + try + { + checkRepositoryId(repositoryId); + network = networksService.getNetwork(repositoryId); + } + catch(Exception e) + { + // ACE-2540: Avoid information leak. Same response if repository does not exist or if user is not a member + throw new CmisObjectNotFoundException("Unknown repository '" + repositoryId + "'!"); + } + + return getRepositoryInfo(network); + } + + private RepositoryInfo getRepositoryInfo(final Network network) + { + final String networkId = network.getTenantDomain(); + final String tenantDomain = (networkId.equals(TenantUtil.SYSTEM_TENANT) || networkId.equals(TenantUtil.DEFAULT_TENANT)) ? TenantService.DEFAULT_DOMAIN : networkId; + + return TenantUtil.runAsSystemTenant(new TenantRunAsWork() + { + public RepositoryInfo doWork() + { + RepositoryInfoImpl repoInfo = (RepositoryInfoImpl)connector.getRepositoryInfo(getContext().getCmisVersion()); + + repoInfo.setId(!networkId.equals("") ? networkId : TenantUtil.SYSTEM_TENANT); + repoInfo.setName(tenantDomain); + repoInfo.setDescription(tenantDomain); + + return repoInfo; + } + }, tenantDomain); + } + + @Override + public void checkRepositoryId(String repositoryId) + { + if(repositoryId.equals(TenantUtil.DEFAULT_TENANT) || repositoryId.equals(TenantUtil.SYSTEM_TENANT)) + { + // TODO check for super admin + return; + } + + if(!tenantAdminService.existsTenant(repositoryId) || !tenantAdminService.isEnabledTenant(repositoryId)) + { + throw new CmisObjectNotFoundException("Unknown repository '" + repositoryId + "'!"); + } + } + + @Override + public void beforeCall() + { + // NOTE: Don't invoke super beforeCall to exclude authentication which is already supported by + // Web Script F/W + //super.beforeCall(); + } + + @Override + public void afterCall() + { + // NOTE: Don't invoke super afterCall to exclude authentication which is already supported by + // Web Script F/W + //super.afterCall(); + } + + @Override + public void close() + { + super.close(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAlfrescoCmisServiceFactory.java b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAlfrescoCmisServiceFactory.java new file mode 100644 index 00000000000..658f5f75420 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAlfrescoCmisServiceFactory.java @@ -0,0 +1,58 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import org.alfresco.repo.tenant.NetworksService; +import org.alfresco.repo.tenant.TenantAdminService; + +/** + * Override factory for OpenCMIS service objects - for public api + * + * @author steveglover + * @author janv + * @since PublicApi1.0 + */ +public class PublicApiAlfrescoCmisServiceFactory extends AlfrescoCmisServiceFactory +{ + private TenantAdminService tenantAdminService; + private NetworksService networksService; + + public void setNetworksService(NetworksService networksService) + { + this.networksService = networksService; + } + + public void setTenantAdminService(TenantAdminService tenantAdminService) + { + this.tenantAdminService = tenantAdminService; + } + + @Override + protected AlfrescoCmisService getCmisServiceTarget(CMISConnector connector) + { + return new PublicApiAlfrescoCmisService(connector, tenantAdminService, networksService); + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAtomPubCMISDispatcher.java b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAtomPubCMISDispatcher.java new file mode 100644 index 00000000000..bdbfc5c7a44 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiAtomPubCMISDispatcher.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Dispatches OpenCMIS requests to the OpenCMIS AtomPub servlet. + * + * @author steveglover + * + */ +public class PublicApiAtomPubCMISDispatcher extends AtomPubCMISDispatcher +{ + @Override + protected CMISHttpServletRequest getHttpRequest(WebScriptRequest req) + { + String serviceName = getServiceName(); + CMISHttpServletRequest httpReqWrapper = new PublicApiCMISHttpServletRequest(req, serviceName, baseUrlGenerator, + getBinding(), getCurrentDescriptor(), tenantAdminService); + return httpReqWrapper; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PublicApiBrowserCMISDispatcher.java b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiBrowserCMISDispatcher.java new file mode 100644 index 00000000000..660dca0f1e6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiBrowserCMISDispatcher.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Cloud-specific browser binding OpenCMIS dispatcher. + * + * @author steveglover + * + */ +public class PublicApiBrowserCMISDispatcher extends BrowserCMISDispatcher +{ + @Override + protected CMISHttpServletRequest getHttpRequest(WebScriptRequest req) + { + String serviceName = getServiceName(); + CMISHttpServletRequest httpReqWrapper = new PublicApiCMISHttpServletRequest(req, serviceName, + baseUrlGenerator, getBinding(), getCurrentDescriptor(), tenantAdminService); + return httpReqWrapper; + } +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PublicApiCMISHttpServletRequest.java b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiCMISHttpServletRequest.java new file mode 100644 index 00000000000..ac175201a5c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiCMISHttpServletRequest.java @@ -0,0 +1,67 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import java.util.Map; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.service.descriptor.Descriptor; +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Wraps an OpenCMIS HttpServletRequest, mapping urls and adding servlet attributes specific to the Alfresco implementation of OpenCMIS. + */ +public class PublicApiCMISHttpServletRequest extends CMISHttpServletRequest +{ + public PublicApiCMISHttpServletRequest(WebScriptRequest req, String serviceName, BaseUrlGenerator baseUrlGenerator, + Binding binding, Descriptor currentDescriptor, TenantAdminService tenantAdminService) + { + super(req, serviceName, baseUrlGenerator, binding, currentDescriptor, tenantAdminService); + } + + protected void addAttributes() + { + super.addAttributes(); + + Match match = req.getServiceMatch(); + Map templateVars = match.getTemplateVars(); + String apiScope = templateVars.get("apiScope"); + String apiVersion = templateVars.get("apiVersion"); + + if(apiScope != null) + { + httpReq.setAttribute("apiScope", apiScope); + } + + if(apiVersion != null) + { + httpReq.setAttribute("apiVersion", apiVersion); + } + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/opencmis/PublicApiPathGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiPathGenerator.java new file mode 100644 index 00000000000..b88cffe4371 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/PublicApiPathGenerator.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.opencmis.CMISDispatcherRegistry.Binding; + +/** + * Cloud generator for OpenCMIS paths based on the repositoryId and binding. + * + * @author steveglover + * + */ +public class PublicApiPathGenerator implements PathGenerator +{ + public void generatePath(HttpServletRequest req, StringBuilder url, String repositoryId, Binding binding) + { + url.append("{repositoryId}"); + url.append("/"); + + String scope = (String)req.getAttribute("apiScope"); + String serviceName = (String)req.getAttribute("serviceName"); + String apiVersion = (String)req.getAttribute("apiVersion"); + if(scope == null) + { + scope = "public"; + } + url.append(scope); + url.append("/"); + url.append(serviceName); + url.append("/"); + url.append("versions"); + url.append("/"); + url.append(apiVersion); + url.append("/"); + url.append(binding.toString()); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/opencmis/SysAdminParamsBaseUrlGenerator.java b/remote-api/src/main/java/org/alfresco/opencmis/SysAdminParamsBaseUrlGenerator.java new file mode 100644 index 00000000000..d3c34ebf75a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/opencmis/SysAdminParamsBaseUrlGenerator.java @@ -0,0 +1,90 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.opencmis; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.repo.admin.SysAdminParams; + +/** + * Generates an OpenCMIS base url based on the request, repository id and binding. The url scheme, host and port + * are overridden by sys admin parameters. + * + * @author steveglover + * + */ +public class SysAdminParamsBaseUrlGenerator extends AbstractBaseUrlGenerator +{ + private SysAdminParams sysAdminParams; + + protected String getServerPath(HttpServletRequest request) + { + StringBuilder sb = new StringBuilder(); + sb.append(getServerScheme(request)); + sb.append("://"); + sb.append(getServerName(request)); + sb.append(":"); + sb.append(getServerPort(request)); + return sb.toString(); + } + + protected String getServerScheme(HttpServletRequest request) + { + String scheme = sysAdminParams.getAlfrescoProtocol(); + if (scheme == null) + { + scheme = request.getScheme(); + } + scheme = request.getScheme(); + return scheme; + } + + protected String getServerName(HttpServletRequest request) + { + String hostname = sysAdminParams.getAlfrescoHost(); + if (hostname == null) + { + hostname = request.getScheme(); + } + hostname = request.getServerName(); + return hostname; + } + + protected int getServerPort(HttpServletRequest request) + { + Integer port = sysAdminParams.getAlfrescoPort(); + if (port == null) + { + port = request.getServerPort(); + } + if (port == null) + { + port = request.getServerPort(); + } + return port; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/SessionUser.java b/remote-api/src/main/java/org/alfresco/repo/SessionUser.java new file mode 100644 index 00000000000..14717d3a19c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/SessionUser.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo; + +import java.io.Serializable; + +/** + * Contract implemented by any object that represents an Alfresco "user" that + * can be persisted in an HTTP Session. + * + * @author Kevin Roast + */ +public interface SessionUser extends Serializable +{ + /** + * Return the user name + * + * @return user name + */ + String getUserName(); + + /** + * Return the ticket + * + * @return ticket + */ + String getTicket(); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/imap/scripts/ServerStatusWebScript.java b/remote-api/src/main/java/org/alfresco/repo/imap/scripts/ServerStatusWebScript.java new file mode 100644 index 00000000000..e33416e7c80 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/imap/scripts/ServerStatusWebScript.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.imap.scripts; + +import java.io.IOException; + +import org.alfresco.repo.imap.ImapService; +import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Shows the availability of the IMAP server via web script request. + */ +public class ServerStatusWebScript extends AbstractWebScript implements ApplicationContextAware +{ + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + ChildApplicationContextFactory subsystem = (ChildApplicationContextFactory)applicationContext.getBean("imap"); + + // note: getting property (rather than getting imapService bean to check isEnabled) does not cause subsystem startup (if stopped) + // hence providing ability for subsystem to be disabled (whilst still supporting ability to check status and/or dynamically start via JMX) + String isEnabled = (String)subsystem.getProperty("imap.server.enabled"); + + if (new Boolean(isEnabled).booleanValue()) + { + res.getWriter().write("enabled"); + } + else + { + res.getWriter().write("disabled"); + } + res.getWriter().flush(); + res.getWriter().close(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/surf/policy/SurfConfigCleaner.java b/remote-api/src/main/java/org/alfresco/repo/surf/policy/SurfConfigCleaner.java new file mode 100644 index 00000000000..6cf5ed7bf56 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/surf/policy/SurfConfigCleaner.java @@ -0,0 +1,102 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.surf.policy; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.web.scripts.bean.ADMRemoteStore; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Delete Node Policy to remove surf-config files for a deleted user. + * + * @author Dmitry Velichkevich + * @author Kevin Roast + */ +public class SurfConfigCleaner extends ADMRemoteStore implements BeforeDeleteNodePolicy +{ + private PolicyComponent policyComponent; + + public void init() + { + this.policyComponent.bindClassBehaviour( + BeforeDeleteNodePolicy.QNAME, + ContentModel.TYPE_PERSON, + new JavaBehaviour(this, BeforeDeleteNodePolicy.QNAME.getLocalName())); + } + + @Override + public void beforeDeleteNode(NodeRef nodeRef) + { + final String userName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME); + final NodeRef componentsRef = getGlobalComponentsNodeRef(); + final NodeRef usersFolderRef = getGlobalUserFolderNodeRef(); + + // Remove the user Surf config folder, contains dynamic page definitions such as dashboard.xml + // For example, qname path to user folder: + // /app:company_home/st:sites/cm:surf-config/cm:pages/cm:user/cm:admin + // ^^^^^ encoded username + if (usersFolderRef != null) + { + NodeRef userFolderNodeRef = nodeService.getChildByName(usersFolderRef, ContentModel.ASSOC_CONTAINS, encodePath(userName)); + if (userFolderNodeRef != null) + { + // CLOUD-2053: Need to set as temporary to delete node instead of archiving. + nodeService.addAspect(userFolderNodeRef, ContentModel.ASPECT_TEMPORARY, null); + nodeService.deleteNode(userFolderNodeRef); + } + } + + // Remove each component Surf config file related to the user, such as the dashboard dashlet component references + // For example, qname path to user component file: + // /app:company_home/st:sites/cm:surf-config/cm:components/cm:page.component-1-1.user~admin~dashboard.xml + // ^^^^^ encoded username + if (componentsRef != null) + { + List configNodes = getFileNodes( + fileFolderService.getFileInfo(componentsRef), + buildUserConfigSearchPattern(userName), + true).getPage(); + + for (FileInfo fileInfo : configNodes) + { + // CLOUD-2053: Need to set as temporary to delete node instead of archiving. + nodeService.addAspect(fileInfo.getNodeRef(), ContentModel.ASPECT_TEMPORARY, null); + nodeService.deleteNode(fileInfo.getNodeRef()); + } + } + } + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/AuthenticationListener.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/AuthenticationListener.java new file mode 100644 index 00000000000..54df637f314 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/AuthenticationListener.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + +/** + * AuthenticationListener implementations can receive notifications of successful and unsuccessful + * authentication requests, made during web script, WebDav or Sharepoint requests. + * + * @author Alex Miller + */ +public interface AuthenticationListener +{ + /** + * A user was successfully authenticated credentials. + */ + public void userAuthenticated(WebCredentials credentials); + + /** + * An authentication attempt, using credentials, failed with exception, ex. + */ + public void authenticationFailed(WebCredentials credentials, Exception ex); + + /** + * An authentication attempt, using credentials, failed. + */ + public void authenticationFailed(WebCredentials credentials); + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/BasicAuthCredentials.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/BasicAuthCredentials.java new file mode 100644 index 00000000000..262ab358f7a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/BasicAuthCredentials.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + +import org.apache.commons.codec.digest.DigestUtils; + +/** + * {@link WebCredentials} holding username and the md5 hash of the password. + * + * @author Alex Miller + */ +public class BasicAuthCredentials implements WebCredentials +{ + private static final long serialVersionUID = 2626445241420904072L; + + private String userName; + private String password; + + /** + * Default constructor + */ + public BasicAuthCredentials(String userName, String password) + { + this.userName = userName; + this.password = DigestUtils.md5Hex(password); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.password == null) ? 0 : this.password.hashCode()); + result = prime * result + ((this.userName == null) ? 0 : this.userName.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; } + BasicAuthCredentials other = (BasicAuthCredentials) obj; + if (this.password == null) + { + if (other.password != null) { return false; } + } + else if (!this.password.equals(other.password)) { return false; } + if (this.userName == null) + { + if (other.userName != null) { return false; } + } + else if (!this.userName.equals(other.userName)) { return false; } + return true; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/GuestCredentials.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/GuestCredentials.java new file mode 100644 index 00000000000..34267828521 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/GuestCredentials.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + + +/** + * {@link WebCredentials} representing a guest user. + * + * @author Alex Miller + */ +public class GuestCredentials implements WebCredentials +{ + private static final long serialVersionUID = 1L; + + @Override + public boolean equals(Object obj) + { + return getClass().equals(obj.getClass()); + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/KerberosCredentials.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/KerberosCredentials.java new file mode 100644 index 00000000000..d7b37427e2e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/KerberosCredentials.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + +import org.alfresco.jlan.server.auth.spnego.NegTokenInit; +import org.alfresco.jlan.server.auth.spnego.NegTokenTarg; + +/** + * {@link WebCredentials} implementation for holding Kerberos credentials. + */ +public class KerberosCredentials implements WebCredentials +{ + private static final long serialVersionUID = 4625258932647351551L; + + private NegTokenInit negToken; + private NegTokenTarg negTokenTarg; + + public KerberosCredentials(NegTokenInit negToken, NegTokenTarg negTokenTarg) + { + this.negToken = negToken; + this.negTokenTarg = negTokenTarg; + } + + public KerberosCredentials(NegTokenInit negToken) + { + this.negToken = negToken; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/NoopAuthenticationListener.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/NoopAuthenticationListener.java new file mode 100644 index 00000000000..5cc7d8af3fb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/NoopAuthenticationListener.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + + +/** + * {@link AuthenticationListener} that does nothing. + * + * @author Alex Miller + */ +public class NoopAuthenticationListener implements AuthenticationListener +{ + @Override + public void userAuthenticated(WebCredentials credentials) + { + // Noop + } + + @Override + public void authenticationFailed(WebCredentials credentials) + { + // Noop + } + + @Override + public void authenticationFailed(WebCredentials credentials, Exception ex) + { + // Noop + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/TenantAuthentication.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/TenantAuthentication.java new file mode 100644 index 00000000000..3ee0e25d7a8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/TenantAuthentication.java @@ -0,0 +1,38 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + +public interface TenantAuthentication +{ + /** + * Authenticate user against tenant + * + * @param username String + * @param tenant String + * @return true => authenticated, false => not authenticated + */ + boolean authenticateTenant(String username, String tenant); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/TicketCredentials.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/TicketCredentials.java new file mode 100644 index 00000000000..15c91f302fb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/TicketCredentials.java @@ -0,0 +1,68 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + + +/** + * {@link WebCredentials} class for holding Alfresco tickets. + * + * @author Alex Miller + */ +public class TicketCredentials implements WebCredentials +{ + private static final long serialVersionUID = -8255499275655719748L; + + private String ticket; + + public TicketCredentials(String ticket) + { + this.ticket = ticket; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.ticket == null) ? 0 : this.ticket.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; } + TicketCredentials other = (TicketCredentials) obj; + if (this.ticket == null) + { + if (other.ticket != null) { return false; } + } + else if (!this.ticket.equals(other.ticket)) { return false; } + return true; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/UnknownCredentials.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/UnknownCredentials.java new file mode 100644 index 00000000000..4ac54349ea6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/UnknownCredentials.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + + +/** + * {@link WebCredentials} where credentials are undetermined. + * + * @author Alex Miller + */ +public class UnknownCredentials implements WebCredentials +{ + private static final long serialVersionUID = 1L; + + @Override + public boolean equals(Object obj) + { + return getClass().equals(obj.getClass()); + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/auth/WebCredentials.java b/remote-api/src/main/java/org/alfresco/repo/web/auth/WebCredentials.java new file mode 100644 index 00000000000..64305057089 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/auth/WebCredentials.java @@ -0,0 +1,40 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.auth; + +import java.io.Serializable; + +/** + * WebScriptCredentials interface. + * + * Passed to {@link AuthenticationListener}s with credentials used in an authentication attempt. + * + * @author Alex Miller + */ +public interface WebCredentials extends Serializable +{ + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/BeanProxyFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/BeanProxyFilter.java new file mode 100644 index 00000000000..5d0535ec50d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/BeanProxyFilter.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.filter.beans; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * An adapter from the servlet filter world into the Spring dependency injected world. Simply looks up a + * {@link DependencyInjectedFilter} with a configured bean name and delegates the + * {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} call to that. This allows us to swap in and out + * different implementations for different 'hook points' in web.xml. + * + * @author dward + */ +public class BeanProxyFilter implements Filter +{ + /** + * Name of the init parameter that carries the proxied bean name + */ + private static final String INIT_PARAM_BEAN_NAME = "beanName"; + + private DependencyInjectedFilter filter; + private ServletContext context; + + /** + * Initialize the filter. + * + * @param args + * FilterConfig + * @throws ServletException + * the servlet exception + * @exception ServletException + */ + public void init(FilterConfig args) throws ServletException + { + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(args.getServletContext()); + this.filter = (DependencyInjectedFilter)ctx.getBean(args.getInitParameter(INIT_PARAM_BEAN_NAME)); + this.context = args.getServletContext(); + } + + /* (non-Javadoc) + * @see javax.servlet.Filter#destroy() + */ + public void destroy() + { + this.filter = null; + } + + /* (non-Javadoc) + * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException + { + this.filter.doFilter(this.context, request, response, chain); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/DependencyInjectedFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/DependencyInjectedFilter.java new file mode 100644 index 00000000000..ebe501ef0f9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/DependencyInjectedFilter.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.filter.beans; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/** + * A bean-like equivalent of a servlet filter, designed to be managed by a Spring container. + * + * @see BeanProxyFilter + * @author dward + */ +public interface DependencyInjectedFilter +{ + /** + * The doFilter method of the Filter is called by the container each time a request/response pair is + * passed through the chain due to a client request for a resource at the end of the chain. The FilterChain passed + * in to this method allows the Filter to pass on the request and response to the next entity in the chain. + *

+ * A typical implementation of this method would follow the following pattern:-
+ * 1. Examine the request
+ * 2. Optionally wrap the request object with a custom implementation to filter content or headers for input + * filtering
+ * 3. Optionally wrap the response object with a custom implementation to filter content or headers for output + * filtering
+ * 4. a) Either invoke the next entity in the chain using the FilterChain object ( + * chain.doFilter()),
+ * 4. b) or not pass on the request/response pair to the next entity in the filter chain to block + * the request processing
+ * 5. Directly set headers on the response after invocation of the next entity in the filter chain. + **/ + public void doFilter(ServletContext context, ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, + ServletException; +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/NullFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/NullFilter.java new file mode 100644 index 00000000000..a542cdeb265 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/NullFilter.java @@ -0,0 +1,79 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.filter.beans; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.alfresco.repo.management.subsystems.ActivateableBean; + +/** + * A Benign filter that does nothing more than invoke the filter chain. Allows strategic points of the filter chain to + * be configured in and out according to the authentication subsystem in use. + * + * @author dward + */ +public class NullFilter implements DependencyInjectedFilter, ActivateableBean +{ + private boolean isActive = true; + + /** + * Activates or deactivates the bean + * + * @param active + * true if the bean is active and initialization should complete + */ + public void setActive(boolean active) + { + this.isActive = active; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive() + */ + public boolean isActive() + { + return this.isActive; + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.web.filter.beans.DependencyInjectedFilter#doFilter(javax.servlet.ServletContext, + * javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + public void doFilter(ServletContext context, ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/SessionSynchronizedFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/SessionSynchronizedFilter.java new file mode 100644 index 00000000000..3d398d0acb3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/filter/beans/SessionSynchronizedFilter.java @@ -0,0 +1,83 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.filter.beans; + +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.HttpServletRequest; +import javax.servlet.http.HttpSession; + +/** + * A filter that will use the HttpSession (if it exists) as the monitor for a + * synchronized block so that only one request per session is processed at any + * time. + * + * Originally created to avoid having to make 200+ JSF session scoped beans thread + * safe. + * + * @author Alan Davis + * @deprecated 5.0 not exposed in web-client web.xml + */ +public class SessionSynchronizedFilter implements Filter +{ + @Override + public void init(FilterConfig arg0) throws ServletException + { + } + + @Override + public void destroy() + { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException + { + HttpSession session = null; + if (request instanceof HttpServletRequest) + { + session = ((HttpServletRequest)request).getSession(false); + } + if (session != null) + { + synchronized(session) + { + chain.doFilter(request, response); + } + } + else + { + chain.doFilter(request, response); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/AlfrescoRhinoScriptDebugger.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/AlfrescoRhinoScriptDebugger.java new file mode 100644 index 00000000000..0904fecff88 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/AlfrescoRhinoScriptDebugger.java @@ -0,0 +1,96 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.tools.debugger.Dim; +import org.mozilla.javascript.tools.debugger.SwingGui; +import org.springframework.extensions.webscripts.ScriptDebugger; + + +/** + * Alfresco implementation of Rhino JavaScript debugger + * + * Provides support for authenticated access to object inspection. + * + * @author davidc + */ +public class AlfrescoRhinoScriptDebugger extends ScriptDebugger +{ + private static final Log logger = LogFactory.getLog(AlfrescoRhinoScriptDebugger.class); + + // Logger + private ContextFactory factory = null; + private SwingGui gui = null; + + + @Override + protected void initDebugger() + { + dim = new AlfrescoDim(); + } + + @Override + public void start() + { + if (logger.isDebugEnabled()) + { + activate(); + show(); + } + } + + @Override + protected String getTitle() + { + return "Alfresco Repository JavaScript Debugger"; + } + + + public static class AlfrescoDim extends Dim + { + /* (non-Javadoc) + * @see org.mozilla.javascript.tools.debugger.Dim#objectToString(java.lang.Object) + */ + @Override + public String objectToString(final Object arg0) + { + // execute command in context of currently selected user + return AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public String doWork() throws Exception + { + return AlfrescoDim.super.objectToString(arg0); + } + }, AuthenticationUtil.getSystemUserName()); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/AlfrescoWebScriptServlet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/AlfrescoWebScriptServlet.java new file mode 100644 index 00000000000..a09470deaa2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/AlfrescoWebScriptServlet.java @@ -0,0 +1,50 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.springframework.extensions.webscripts.servlet.WebScriptServlet; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class AlfrescoWebScriptServlet extends WebScriptServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException + { + try + { + super.service(req, res); + } + finally + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java new file mode 100644 index 00000000000..a655e2b6312 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/BufferedRequest.java @@ -0,0 +1,405 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.Description.FormatStyle; +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.Runtime; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.util.FileCopyUtils; + +public class BufferedRequest implements WrappingWebScriptRequest +{ + private TempOutputStreamFactory streamFactory; + private WebScriptRequest req; + private TempOutputStream bufferStream; + private InputStream contentStream; + private BufferedReader contentReader; + + public BufferedRequest(WebScriptRequest req, TempOutputStreamFactory streamFactory) + { + this.req = req; + this.streamFactory = streamFactory; + } + + private TempOutputStream getBufferedBodyAsTempStream() throws IOException + { + if (bufferStream == null) + { + bufferStream = streamFactory.createOutputStream(); + + try + { + // Copy the stream + FileCopyUtils.copy(req.getContent().getInputStream(), bufferStream); + } + catch (IOException e) + { + bufferStream.destroy(); + throw e; + } + } + + return bufferStream; + } + + private InputStream bufferInputStream() throws IOException + { + if (contentReader != null) + { + throw new IllegalStateException("Reader in use"); + } + if (contentStream == null) + { + contentStream = getBufferedBodyAsTempStream().getInputStream(); + } + + return contentStream; + } + + public void reset() + { + if (contentStream != null) + { + try + { + contentStream.close(); + } + catch (Exception e) + { + } + contentStream = null; + } + if (contentReader != null) + { + try + { + contentReader.close(); + } + catch (Exception e) + { + } + contentReader = null; + } + } + + public void close() + { + reset(); + if (bufferStream != null) + { + try + { + bufferStream.destroy(); + } + catch (Exception e) + { + } + bufferStream = null; + } + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WrappingWebScriptRequest#getNext() + */ + @Override + public WebScriptRequest getNext() + { + return req; + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#forceSuccessStatus() + */ + @Override + public boolean forceSuccessStatus() + { + return req.forceSuccessStatus(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getAgent() + */ + @Override + public String getAgent() + { + return req.getAgent(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getContent() + */ + @Override + public Content getContent() + { + final Content wrapped = req.getContent(); + return new Content(){ + + @Override + public String getContent() throws IOException + { + return wrapped.getContent(); + } + + @Override + public String getEncoding() + { + return wrapped.getEncoding(); + } + + @Override + public String getMimetype() + { + return wrapped.getMimetype(); + } + + + @Override + public long getSize() + { + return wrapped.getSize(); + } + + @Override + public InputStream getInputStream() + { + if (BufferedRequest.this.contentReader != null) + { + throw new IllegalStateException("Reader in use"); + } + if (BufferedRequest.this.contentStream == null) + { + try + { + BufferedRequest.this.contentStream = bufferInputStream(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + return BufferedRequest.this.contentStream; + } + + @Override + public BufferedReader getReader() throws IOException + { + if (BufferedRequest.this.contentStream != null) + { + throw new IllegalStateException("Input Stream in use"); + } + if (BufferedRequest.this.contentReader == null) + { + String encoding = wrapped.getEncoding(); + InputStream in = bufferInputStream(); + BufferedRequest.this.contentReader = new BufferedReader(new InputStreamReader(in, encoding == null ? "ISO-8859-1" : encoding)); + } + return BufferedRequest.this.contentReader; + } + }; + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getContentType() + */ + @Override + public String getContentType() + { + return req.getContentType(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getContextPath() + */ + @Override + public String getContextPath() + { + return req.getContextPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getExtensionPath() + */ + @Override + public String getExtensionPath() + { + return req.getExtensionPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getFormat() + */ + @Override + public String getFormat() + { + return req.getFormat(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getFormatStyle() + */ + @Override + public FormatStyle getFormatStyle() + { + return req.getFormatStyle(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeader(java.lang.String) + */ + @Override + public String getHeader(String name) + { + return req.getHeader(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeaderNames() + */ + @Override + public String[] getHeaderNames() + { + return req.getHeaderNames(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getHeaderValues(java.lang.String) + */ + @Override + public String[] getHeaderValues(String name) + { + return req.getHeaderValues(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getJSONCallback() + */ + @Override + public String getJSONCallback() + { + return req.getJSONCallback(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameter(java.lang.String) + */ + @Override + public String getParameter(String name) + { + return req.getParameter(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameterNames() + */ + @Override + public String[] getParameterNames() + { + return req.getParameterNames(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getParameterValues(java.lang.String) + */ + @Override + public String[] getParameterValues(String name) + { + return req.getParameterValues(name); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getPathInfo() + */ + @Override + public String getPathInfo() + { + return req.getPathInfo(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getQueryString() + */ + @Override + public String getQueryString() + { + return req.getQueryString(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getRuntime() + */ + @Override + public Runtime getRuntime() + { + return req.getRuntime(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServerPath() + */ + @Override + public String getServerPath() + { + return req.getServerPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServiceContextPath() + */ + @Override + public String getServiceContextPath() + { + return req.getServiceContextPath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServiceMatch() + */ + @Override + public Match getServiceMatch() + { + return req.getServiceMatch(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getServicePath() + */ + @Override + public String getServicePath() + { + return req.getServicePath(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#getURL() + */ + @Override + public String getURL() + { + return req.getURL(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#isGuest() + */ + @Override + public boolean isGuest() + { + return req.isGuest(); + } + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptRequest#parseContent() + */ + @Override + public Object parseContent() + { + return req.parseContent(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java new file mode 100644 index 00000000000..f5811b01adc --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/BufferedResponse.java @@ -0,0 +1,281 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.StringBuilderWriter; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Runtime; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Transactional Buffered Response + */ +public class BufferedResponse implements WrappingWebScriptResponse +{ + // Logger + protected static final Log logger = LogFactory.getLog(BufferedResponse.class); + + private TempOutputStreamFactory streamFactory; + private WebScriptResponse res; + private int bufferSize; + private TempOutputStream outputStream = null; + private StringBuilderWriter outputWriter = null; + + + /** + * Construct + * + * @param res WebScriptResponse + * @param bufferSize int + */ + public BufferedResponse(WebScriptResponse res, int bufferSize, TempOutputStreamFactory streamFactory) + { + this.res = res; + this.bufferSize = bufferSize; + this.streamFactory = streamFactory; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WrappingWebScriptResponse#getNext() + */ + public WebScriptResponse getNext() + { + return res; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#addHeader(java.lang.String, java.lang.String) + */ + public void addHeader(String name, String value) + { + res.addHeader(name, value); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#encodeScriptUrl(java.lang.String) + */ + public String encodeScriptUrl(String url) + { + return res.encodeScriptUrl(url); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getEncodeScriptUrlFunction(java.lang.String) + */ + public String getEncodeScriptUrlFunction(String name) + { + return res.getEncodeScriptUrlFunction(name); + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptResponse#encodeResourceUrl(java.lang.String) + */ + public String encodeResourceUrl(String url) + { + return res.encodeResourceUrl(url); + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.WebScriptResponse#getEncodeResourceUrlFunction(java.lang.String) + */ + public String getEncodeResourceUrlFunction(String name) + { + return res.getEncodeResourceUrlFunction(name); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getOutputStream() + */ + public OutputStream getOutputStream() throws IOException + { + if (outputStream == null) + { + if (outputWriter != null) + { + throw new AlfrescoRuntimeException("Already buffering output writer"); + } + outputStream = streamFactory.createOutputStream(); + } + return outputStream; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getRuntime() + */ + public Runtime getRuntime() + { + return res.getRuntime(); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#getWriter() + */ + public Writer getWriter() throws IOException + { + if (outputWriter == null) + { + if (outputStream != null) + { + throw new AlfrescoRuntimeException("Already buffering output stream"); + } + outputWriter = new StringBuilderWriter(bufferSize); + } + return outputWriter; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#reset() + */ + public void reset() + { + if (outputStream != null) + { + outputStream = null; + } + else if (outputWriter != null) + { + outputWriter = null; + } + res.reset(); + } + + /* (non-Javadoc) + * @see org./alfresco.web.scripts.WebScriptResponse#resetjava.lang.String) + */ + public void reset(String preserveHeadersPattern) + { + if (outputStream != null) + { + outputStream = null; + } + else if (outputWriter != null) + { + outputWriter = null; + } + res.reset(preserveHeadersPattern); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setCache(org.alfresco.web.scripts.Cache) + */ + public void setCache(Cache cache) + { + res.setCache(cache); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setContentType(java.lang.String) + */ + public void setContentType(String contentType) + { + res.setContentType(contentType); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setContentEncoding(java.lang.String) + */ + public void setContentEncoding(String contentEncoding) + { + res.setContentEncoding(contentEncoding); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setHeader(java.lang.String, java.lang.String) + */ + public void setHeader(String name, String value) + { + res.setHeader(name, value); + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptResponse#setStatus(int) + */ + public void setStatus(int status) + { + res.setStatus(status); + } + + /** + * Write buffered response to underlying response + */ + public void writeResponse() + { + try + { + if (logger.isDebugEnabled() && outputStream != null) + { + logger.debug("Writing Transactional response: size=" + outputStream.getLength()); + } + + if (outputWriter != null) + { + outputWriter.flush(); + res.getWriter().write(outputWriter.toString()); + } + else if (outputStream != null) + { + if (logger.isDebugEnabled()) + logger.debug("Writing Transactional response: size=" + outputStream.getLength()); + + try + { + outputStream.flush(); + FileCopyUtils.copy(outputStream.getInputStream(), res.getOutputStream()); + } + finally + { + outputStream.destroy(); + } + } + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to commit buffered response", e); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/DeclarativeSpreadsheetWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/DeclarativeSpreadsheetWebScript.java new file mode 100644 index 00000000000..25ad2231d2c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/DeclarativeSpreadsheetWebScript.java @@ -0,0 +1,427 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVFormat; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * Parent of Declarative Webscripts that generate Excel files, + * usually based on some sort of dictionary model. + * + * @author Nick Burch + */ +public abstract class DeclarativeSpreadsheetWebScript extends DeclarativeWebScript +{ + public static final String MODEL_CSV = "csv"; + public static final String MODEL_EXCEL = "excel"; + public static final String PARAM_REQ_DELIMITER = "delimiter"; + + private CSVFormat csvFormat; + + protected DictionaryService dictionaryService; + protected String filenameBase; + + /** + * @param dictionaryService the DictionaryService to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Identifies the resource for the webscript. + */ + protected abstract Object identifyResource(String format, WebScriptRequest req); + + /** + * If the format is requested as HTML, should an exception be raised, + * or should an HTML version be called? + */ + protected abstract boolean allowHtmlFallback(); + + /** + * Returns the QNames of the model properties to be output in + * the header, and if they're required or not + */ + protected abstract List> buildPropertiesForHeader(Object resource, String format, WebScriptRequest req); + + /** + * Populates the body of the Excel Workbook, once the header has been + * output. + * This is called if the format is .xls or .xlsx + */ + protected abstract void populateBody(Object resource, Workbook workbook, Sheet sheet, List properties) + throws IOException; + + /** + * Populates the body of the CSV file, once the header has been + * output. + * This is called if the format is .csv + */ + protected abstract void populateBody(Object resource, CSVPrinter csv, List properties) + throws IOException; + + /** + * Set the CSVFormat + * + * @param csvFormat CSVFormat + */ + public void setCsvFormat(CSVFormat csvFormat) + { + this.csvFormat = csvFormat; + } + + /** + * Get the CSVFormat. Returns {@link CSVFormat#EXCEL} if none was set. + * + * @return CSVFormat + */ + public CSVFormat getCsvFormat() + { + if (csvFormat == null) + { + return CSVFormat.EXCEL + .withQuote('"') + .withRecordSeparator("\n") + .withFirstRecordAsHeader(); + } + else + { + return csvFormat; + } + } + + /** + * @see org.springframework.extensions.webscripts.DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Map model = new HashMap(); + model.put("success", Boolean.TRUE); + + // What format are they after? + String format = req.getFormat(); + if("csv".equals(format) || "xls".equals(format) || + "xlsx".equals(format) || "excel".equals(format)) + { + // Identify the thing to process + Object resource = identifyResource(format, req); + + // Generate the spreadsheet + try + { + generateSpreadsheet(resource, format, req, status, model); + return model; + } + catch(IOException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Unable to generate template file", e); + } + } + + // If we get here, then it isn't a spreadsheet version + if(allowHtmlFallback()) + { + // There's some sort of help / upload form + return model; + } + else + { + throw new WebScriptException("Web Script format '" + format + "' is not supported"); + } + } + + /** + * Generates the spreadsheet, based on the properties in the header + * and a callback for the body. + */ + public void generateSpreadsheet(Object resource, String format, WebScriptRequest req, + Status status, Map model) throws IOException + { + Pattern qnameMunger = Pattern.compile("([A-Z][a-z]+)([A-Z].*)"); + String delimiterParam = req.getParameter(PARAM_REQ_DELIMITER); + CSVFormat reqCSVFormat = null; + + if (delimiterParam != null && !delimiterParam.isEmpty()) + { + reqCSVFormat = CSVFormat.EXCEL + .withDelimiter(delimiterParam.charAt(0)) + .withQuote('"') + .withRecordSeparator("\n") + .withFirstRecordAsHeader(); + } + + // Build up the details of the header + List> propertyDetails = buildPropertiesForHeader(resource, format, req); + String[] headings = new String[propertyDetails.size()]; + String[] descriptions = new String[propertyDetails.size()]; + boolean[] required = new boolean[propertyDetails.size()]; + for(int i=0; i property = propertyDetails.get(i); + if(property == null || property.getFirst() == null) + { + headings[i] = ""; + required[i] = false; + } + else + { + QName column = property.getFirst(); + required[i] = property.getSecond(); + + // Ask the dictionary service nicely for the details + PropertyDefinition pd = dictionaryService.getProperty(column); + if(pd != null && pd.getTitle(dictionaryService) != null) + { + // Use the friendly titles, which may even be localised! + headings[i] = pd.getTitle(dictionaryService); + descriptions[i] = pd.getDescription(dictionaryService); + } + else + { + // Nothing friendly found, try to munge the raw qname into + // something we can show to a user... + String raw = column.getLocalName(); + raw = raw.substring(0, 1).toUpperCase() + raw.substring(1); + + Matcher m = qnameMunger.matcher(raw); + if(m.matches()) + { + headings[i] = m.group(1) + " " + m.group(2); + } + else + { + headings[i] = raw; + } + } + } + } + + // Build a list of just the properties + List properties = new ArrayList(propertyDetails.size()); + for(Pair p : propertyDetails) + { + QName qn = null; + if(p != null) + { + qn = p.getFirst(); + } + properties.add(qn); + } + + // Output + if("csv".equals(format)) + { + StringWriter sw = new StringWriter(); + CSVPrinter csv = new CSVPrinter(sw, reqCSVFormat != null ? reqCSVFormat : getCsvFormat()); + csv.printRecord(headings); + + populateBody(resource, csv, properties); + + model.put(MODEL_CSV, sw.toString()); + } + else + { + Workbook wb; + if("xlsx".equals(format)) + { + wb = new XSSFWorkbook(); + // TODO Properties + } + else + { + wb = new HSSFWorkbook(); + // TODO Properties + } + + // Add our header row + Sheet sheet = wb.createSheet("Export"); + Row hr = sheet.createRow(0); + sheet.createFreezePane(0, 1); + + Font fb = wb.createFont(); + fb.setBold(true); + Font fi = wb.createFont(); + fi.setBold(true); + fi.setItalic(true); + + CellStyle csReq = wb.createCellStyle(); + csReq.setFont(fb); + CellStyle csOpt = wb.createCellStyle(); + csOpt.setFont(fi); + + // Populate the header + Drawing draw = null; + for(int i=0; i 0) + { + // Add a description for it too + if(draw == null) + { + draw = sheet.createDrawingPatriarch(); + } + ClientAnchor ca = wb.getCreationHelper().createClientAnchor(); + ca.setCol1(c.getColumnIndex()); + ca.setCol2(c.getColumnIndex()+1); + ca.setRow1(hr.getRowNum()); + ca.setRow2(hr.getRowNum()+2); + + Comment cmt = draw.createCellComment(ca); + cmt.setAuthor(""); + cmt.setString(wb.getCreationHelper().createRichTextString(descriptions[i])); + cmt.setVisible(false); + c.setCellComment(cmt); + } + } + + // Have the contents populated + populateBody(resource, wb, sheet, properties); + + // Save it for the template + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + wb.write(baos); + model.put(MODEL_EXCEL, baos.toByteArray()); + } + } + + @Override + protected Map createTemplateParameters(WebScriptRequest req, WebScriptResponse res, + Map customParams) + { + Map model = super.createTemplateParameters(req, res, customParams); + // We sometimes need to monkey around to do the binary output... + model.put("req", req); + model.put("res", res); + model.put("writeExcel", new WriteExcel(res, model, req.getFormat(), filenameBase)); + return model; + } + + public static class WriteExcel + { + private String format; + private String filenameBase; + private WebScriptResponse res; + private Map model; + private WriteExcel(WebScriptResponse res, Map model, String format, String filenameBase) + { + this.res = res; + this.model = model; + this.format = format; + this.filenameBase = filenameBase; + } + public void write() throws IOException + { + String filename = filenameBase + "." + format; + + // If it isn't a CSV, reset so we can send binary + if(! "csv".equals(format)) + { + res.reset(); + } + + // Tell the browser it's a file download + res.addHeader("Content-Disposition", "attachment; filename=" + filename); + + // Now send that data + if("csv".equals(format)) + { + res.getWriter().append((String)model.get(MODEL_CSV)); + } + else + { + // Set the mimetype, as we've reset + if("xlsx".equals(format)) + { + res.setContentType(MimetypeMap.MIMETYPE_OPENXML_SPREADSHEET); + } else { + res.setContentType(MimetypeMap.MIMETYPE_EXCEL); + } + + // Send the raw excel bytes + byte[] excel = (byte[])model.get(MODEL_EXCEL); + res.getOutputStream().write(excel); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/ExtensibilityContainer.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/ExtensibilityContainer.java new file mode 100644 index 00000000000..1b77dbf91da --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/ExtensibilityContainer.java @@ -0,0 +1,869 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + + + +import java.io.IOException; + +import java.io.Writer; + +import java.util.ArrayList; + +import java.util.HashMap; + +import java.util.List; + +import java.util.Map; + +import java.util.ResourceBundle; + + + +import org.apache.commons.logging.Log; + +import org.apache.commons.logging.LogFactory; + +import org.dom4j.Element; + +import org.springframework.extensions.config.ConfigImpl; + +import org.springframework.extensions.config.ConfigSection; + +import org.springframework.extensions.config.ConfigService; + +import org.springframework.extensions.config.evaluator.Evaluator; + +import org.springframework.extensions.config.xml.XMLConfigService; + +import org.springframework.extensions.config.xml.elementreader.ConfigElementReader; + +import org.springframework.extensions.surf.extensibility.BasicExtensionModule; + +import org.springframework.extensions.surf.extensibility.ExtensibilityModel; + +import org.springframework.extensions.surf.extensibility.HandlesExtensibility; + +import org.springframework.extensions.surf.extensibility.WebScriptExtensibilityModuleHandler; + +import org.springframework.extensions.surf.extensibility.impl.ExtensibilityModelImpl; + +import org.springframework.extensions.surf.extensibility.impl.MarkupDirective; + +import org.springframework.extensions.webscripts.Authenticator; + +import org.springframework.extensions.webscripts.ExtendedScriptConfigModel; + +import org.springframework.extensions.webscripts.ExtendedTemplateConfigModel; + +import org.springframework.extensions.webscripts.ScriptConfigModel; + +import org.springframework.extensions.webscripts.TemplateConfigModel; + +import org.springframework.extensions.webscripts.WebScriptPropertyResourceBundle; + +import org.springframework.extensions.webscripts.WebScriptRequest; + +import org.springframework.extensions.webscripts.WebScriptResponse; + + + +/** + + *

A simple extensibility {@link org.springframework.extensions.webscripts.Container} for processing WebScripts. This extends the {@link RepositoryContainer} and + + * implements the {@link HandlesExtensibility} interface to provide extensibility capabilities.

+ + * + + * @author David Draper + + */ + +public class ExtensibilityContainer extends RepositoryContainer implements HandlesExtensibility + +{ + + private static final Log logger = LogFactory.getLog(ExtensibilityContainer.class); + + + + public boolean isExtensibilitySuppressed() + + { + + return false; + + } + + + + /** + + *

Opens a new {@link ExtensibilityModel}, defers execution to the extended {@link RepositoryContainer} and + + * then closes the {@link ExtensibilityModel}.

+ + */ + + @Override + + public void executeScript(WebScriptRequest scriptReq, + + WebScriptResponse scriptRes, + + Authenticator auth) throws IOException + + { + + ExtensibilityModel extModel = this.openExtensibilityModel(); + + try + + { + + super.executeScript(scriptReq, scriptRes, auth); + + } + + finally + + { + + // It's only necessary to close the model if it's actually been used. Not all WebScripts will make use of the + + // model. An example of this would be the StreamContent WebScript. It is important not to attempt to close + + // an unused model since the WebScript executed may have already flushed the response if it has overridden + + // the default .execute() method. + + if (this.modelUsed.get()) + + { + + try + + { + + this.closeExtensibilityModel(extModel, scriptRes.getWriter()); + + } + + catch (IOException e) + + { + + logger.error("An error occurred getting the Writer when closing an ExtensibilityModel", e); + + } + + } + + } + + } + + + + /** + + *

This keeps track of whether or not the {@link ExtensibilityModel} for the current thread has been used. The + + * thread local value will only be set to true if the getCurrentExtensibilityModel method + + * is called.

+ + */ + + private ThreadLocal modelUsed = new ThreadLocal(); + + + + /** + + *

A {@link WebScriptExtensibilityModuleHandler} is required for retrieving information on what + + * {@link BasicExtensionModule} instances have been configured and the extension files that need + + * to be processed. This variable should be set thorugh the Spring application context configuration.

+ + */ + + private WebScriptExtensibilityModuleHandler extensibilityModuleHandler = null; + + + + /** + + *

Sets the {@link WebScriptExtensibilityModuleHandler} for this {@link org.springframework.extensions.webscripts.Container}.

+ + * @param extensibilityModuleHandler WebScriptExtensibilityModuleHandler + + */ + + public void setExtensibilityModuleHandler(WebScriptExtensibilityModuleHandler extensibilityModuleHandler) + + { + + this.extensibilityModuleHandler = extensibilityModuleHandler; + + } + + + + /** + + *

Maintains a list of all the {@link ExtensibilityModel} instances being used across all the + + * available threads.

+ + */ + + private ThreadLocal extensibilityModel = new ThreadLocal(); + + + + /** + + *

Creates a new {@link ExtensibilityModel} and sets it on the current thread

+ + */ + + public ExtensibilityModel openExtensibilityModel() + + { + + if (logger.isDebugEnabled()) + + { + + logger.debug("Opening for thread: " + Thread.currentThread().getName()); + + } + + this.extendedBundleCache.set(new HashMap()); + + this.evaluatedModules.set(null); + + this.fileBeingProcessed.set(null); + + this.globalConfig.set(null); + + this.sections.set(null); + + this.sectionsByArea.set(null); + + + + ExtensibilityModel model = new ExtensibilityModelImpl(null, this); + + this.extensibilityModel.set(model); + + this.modelUsed.set(Boolean.FALSE); + + return model; + + } + + + + /** + + *

Flushes the {@link ExtensibilityModel} provided and sets its parent as the current {@link ExtensibilityModel} + + * for the current thread.

+ + */ + + public void closeExtensibilityModel(ExtensibilityModel model, Writer out) + + { + + if (logger.isDebugEnabled()) + + { + + logger.debug("Closing for thread: " + Thread.currentThread().getName()); + + } + + + + model.flushModel(out); + + this.modelUsed.set(Boolean.FALSE); + + this.extensibilityModel.set(null); + + } + + + + /** + + *

Returns the {@link ExtensibilityModel} for the current thread.

+ + */ + + public ExtensibilityModel getCurrentExtensibilityModel() + + { + + if (logger.isDebugEnabled()) + + { + + logger.debug("Getting current for thread: " + Thread.currentThread().getName()); + + } + + this.modelUsed.set(Boolean.TRUE); + + return this.extensibilityModel.get(); + + } + + + + /** + + *

This method is implemented to perform no action as it is not necessary for a standalone WebScript + + * container to add dependencies for processing.

+ + */ + + public void updateExtendingModuleDependencies(String pathBeingProcessed, Map model) + + { + + // NOT REQUIRED FOR STANDALONE WEBSCRIPT CONTAINER + + } + + + + /** + + *

A thread-safe cache of extended {@link ResourceBundle} instances for the current request.

+ + */ + + private ThreadLocal> extendedBundleCache = new ThreadLocal>(); + + + + /** + + *

Checks the cache to see if it has cached an extended bundle (that is a basic {@link ResourceBundle} that + + * has had extension modules applied to it. Extended bundles can only be safely cached once per request as the modules + + * applied can vary for each request.

+ + * + + * @param webScriptId The id of the WebScript to retrieve the extended bundle for. + + * @return A cached bundle or null if the bundle has not previously been cached. + + */ + + public ResourceBundle getCachedExtendedBundle(String webScriptId) + + { + + ResourceBundle cachedExtendedBundle = null; + + Map threadLocal = this.extendedBundleCache.get(); + + if (threadLocal != null) + + { + + cachedExtendedBundle = this.extendedBundleCache.get().get(webScriptId); + + } + + return cachedExtendedBundle; + + } + + + + /** + + *

Adds a new extended bundle to the cache. An extended bundle is a WebScript {@link ResourceBundle} that has had + + * {@link ResourceBundle} instances merged into it from extension modules that have been applied. These can only be cached + + * for the lifetime of the request as different modules may be applied to the same WebScript for different requests.

+ + * + + * @param webScriptId The id of the WebScript to cache the extended bundle against. + + * @param extensionBundle The extended bundle to cache. + + */ + + public void addExtensionBundleToCache(String webScriptId, WebScriptPropertyResourceBundle extensionBundle) + + { + + Map threadLocal = this.extendedBundleCache.get(); + + if (threadLocal == null) + + { + + // This should never be the case because when a new model is opened this value should be reset + + // but we will double-check to avoid the potential of NPEs... + + threadLocal = new HashMap(); + + this.extendedBundleCache.set(threadLocal); + + } + + threadLocal.put(webScriptId, extensionBundle); + + } + + + + /** + + *

A {@link ThreadLocal} reference to the file currently being processed in the model.

+ + */ + + private ThreadLocal fileBeingProcessed = new ThreadLocal(); + + + + /** + + *

Returns the path of the file currently being processed in the model by the current thread. + + * This information is primarily provided for the purposes of generating debug information.

+ + * + + * @return The path of the file currently being processed. + + */ + + public String getFileBeingProcessed() + + { + + return this.fileBeingProcessed.get(); + + } + + + + /** + + *

Sets the path of the file currently being processed in the model by the current thread. + + * This information should be collected to assist with providing debug information.

+ + * @param file The path of the file currently being processed. + + */ + + public void setFileBeingProcessed(String file) + + { + + this.fileBeingProcessed.set(file); + + } + + + + /** + + *

Retrieves an files for the evaluated modules that are extending the WebScript files being processed.

+ + */ + + public List getExtendingModuleFiles(String pathBeingProcessed) + + { + + List extendingModuleFiles = new ArrayList(); + + for (BasicExtensionModule module: this.getEvaluatedModules()) + + { + + extendingModuleFiles.addAll(this.extensibilityModuleHandler.getExtendingModuleFiles(module, pathBeingProcessed)); + + } + + return extendingModuleFiles; + + } + + + + /** + + *

The list of {@link org.springframework.extensions.surf.types.ExtensionModule} instances that have been evaluated as applicable to + + * this RequestContext. This is set to null when during instantiation and is only + + * properly set the first time the getEvaluatedModules method is invoked. This ensures + + * that module evaluation only occurs once per request.

+ + */ + + private ThreadLocal> evaluatedModules = new ThreadLocal>(); + + + + /** + + *

Retrieve the list of {@link org.springframework.extensions.surf.types.ExtensionModule} instances that have been evaluated as applicable + + * for the current request. If this list has not yet been populated then use the {@link org.springframework.extensions.surf.extensibility.ExtensibilityModuleHandler} + + * configured in the Spring application context to evaluate them.

+ + * + + * @return A list of {@link org.springframework.extensions.surf.types.ExtensionModule} instances that are applicable to the current request. + + */ + + public List getEvaluatedModules() + + { + + List evaluatedModules = this.evaluatedModules.get(); + + if (evaluatedModules == null) + + { + + if (this.extensibilityModuleHandler == null) + + { + + if (logger.isErrorEnabled()) + + { + + logger.error("No 'extensibilityModuleHandler' has been configured for this request context. Extensions cannot be processed"); + + } + + evaluatedModules = new ArrayList(); + + this.evaluatedModules.set(evaluatedModules); + + } + + else + + { + + evaluatedModules = this.extensibilityModuleHandler.getExtensionModules(); + + this.evaluatedModules.set(evaluatedModules); + + } + + } + + return evaluatedModules; + + } + + + + /** + + *

This is a local {@link ConfigImpl} instance that will only be used when extension modules are employed. It will + + * initially be populated with the default "static" global configuration taken from the {@link ConfigService} associated + + * with this {@link org.springframework.extensions.surf.RequestContext} but then updated to include global configuration provided by extension modules that + + * have been evaluated to be applied to the current request.

+ + */ + + private ThreadLocal globalConfig = new ThreadLocal(); + + + + /** + + *

This map represents {@link ConfigSection} instances mapped by area. It will only be used when extension modules are + + * employed. It will initially be populated with the default "static" configuration taken from the {@link ConfigService} associated + + * with this {@link org.springframework.extensions.surf.RequestContext} but then updated to include configuration provided by extension modules that have been evaluated + + * to be applied to the current request.

+ + */ + + private ThreadLocal>> sectionsByArea = new ThreadLocal>>(); + + + + /** + + *

A list of {@link ConfigSection} instances that are only applicable to the current request. It will only be used when extension modules are + + * employed. It will initially be populated with the default "static" configuration taken from the {@link ConfigService} associated + + * with this {@link org.springframework.extensions.surf.RequestContext} but then updated to include configuration provided by extension modules that have been evaluated + + * to be applied to the current request.

+ + */ + + private ThreadLocal> sections = new ThreadLocal>(); + + + + /** + + *

Creates a new {@link ExtendedScriptConfigModel} instance using the local configuration generated for this request. + + * If configuration for the request will be generated if it does not yet exist. It is likely that this method will be + + * called multiple times within the context of a single request and although the configuration containers will always + + * be the same a new {@link ExtendedScriptConfigModel} instance will always be created as the the supplied xmlConfig + + * string could be different for each call (because each WebScript invoked in the request will supply different + + * configuration.

+ + */ + + public ScriptConfigModel getExtendedScriptConfigModel(String xmlConfig) + + { + + if (this.globalConfig.get() == null && this.sectionsByArea.get() == null && this.sections.get() == null) + + { + + this.getConfigExtensions(); + + } + + return new ExtendedScriptConfigModel(getConfigService(), xmlConfig, this.globalConfig.get(), this.sectionsByArea.get(), this.sections.get()); + + } + + + + /** + + *

Creates a new {@link TemplateConfigModel} instance using the local configuration generated for this request. + + * If configuration for the request will be generated if it does not yet exist. It is likely that this method will be + + * called multiple times within the context of a single request and although the configuration containers will always + + * be the same a new {@link TemplateConfigModel} instance will always be created as the the supplied xmlConfig + + * string could be different for each call (because each WebScript invoked in the request will supply different + + * configuration.

+ + */ + + public TemplateConfigModel getExtendedTemplateConfigModel(String xmlConfig) + + { + + if (this.globalConfig.get() == null && this.sectionsByArea.get() == null && this.sections.get() == null) + + { + + this.getConfigExtensions(); + + } + + return new ExtendedTemplateConfigModel(getConfigService(), xmlConfig, this.globalConfig.get(), this.sectionsByArea.get(), this.sections.get()); + + } + + + + /** + + *

Creates and populates the request specific configuration container objects (globalConfig, sectionsByArea & + + * sections with a combination of the default static configuration (taken from files accessed by the {@link ConfigService}) and + + * dynamic configuration taken from extension modules evaluated for the current request.

+ + */ + + private void getConfigExtensions() + + { + + // Extended configuration is only possible if config service is an XMLConfigService... + + // + + // ...also, it's only necessary to populate the configuration containers if they have not already been populated. This test should also + + // be carried out by the two methods ("getExtendedTemplateConfigModel" & "getExtendedTemplateConfigModel") to prevent duplication + + // of effort... but in case other methods attempt to access it we will make these additional tests. + + if (getConfigService() instanceof XMLConfigService && this.globalConfig == null && this.sectionsByArea == null && this.sections == null) + + { + + // Cast the config service for ease of access + + XMLConfigService xmlConfigService = (XMLConfigService) getConfigService(); + + + + // Get the current configuration from the ConfigService - we don't want to permanently pollute + + // the standard configuration with additions from the modules... + + this.globalConfig.set(new ConfigImpl((ConfigImpl)xmlConfigService.getGlobalConfig())); // Make a copy of the current global config + + + + // Initialise these with the config service values... + + this.sectionsByArea.set(new HashMap>(xmlConfigService.getSectionsByArea())); + + this.sections.set(new ArrayList(xmlConfigService.getSections())); + + + + // Check to see if there are any modules that we need to apply... + + List evaluatedModules = this.getEvaluatedModules(); + + if (evaluatedModules != null && !evaluatedModules.isEmpty()) + + { + + for (BasicExtensionModule currModule: evaluatedModules) + + { + + for (Element currentConfigElement: currModule.getConfigurations()) + + { + + // Set up containers for our request specific configuration - this will contain data taken from the evaluated modules... + + Map parsedElementReaders = new HashMap(); + + Map parsedEvaluators = new HashMap(); + + List parsedConfigSections = new ArrayList(); + + + + // Parse and process the parses configuration... + + String currentArea = xmlConfigService.parseFragment(currentConfigElement, parsedElementReaders, parsedEvaluators, parsedConfigSections); + + for (Map.Entry entry : parsedEvaluators.entrySet()) + + { + + // add the evaluators to the config service + + parsedEvaluators.put(entry.getKey(), entry.getValue()); + + } + + for (Map.Entry entry : parsedElementReaders.entrySet()) + + { + + // add the element readers to the config service + + parsedElementReaders.put(entry.getKey(), entry.getValue()); + + } + + for (ConfigSection section : parsedConfigSections) + + { + + // Update local configuration with our updated data... + + xmlConfigService.addConfigSection(section, currentArea, this.globalConfig.get(), this.sectionsByArea.get(), this.sections.get()); + + } + + } + + } + + } + + } + + } + + + + /** + + *

Adds the <{@code}@markup> directive to the container which allows FreeMarker templates to be extended.

+ + */ + + public void addExtensibilityDirectives(Map freeMarkerModel, ExtensibilityModel extModel) + + { + + MarkupDirective mud = new MarkupDirective("markup", extModel); + + freeMarkerModel.put("markup", mud); + + } + +} + diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/FileTypeImageUtils.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/FileTypeImageUtils.java new file mode 100644 index 00000000000..6a33e735236 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/FileTypeImageUtils.java @@ -0,0 +1,138 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.service.cmr.repository.FileTypeImageSize; + +/** + * Contains utility methods + * + * @author Roy Wetherall + */ +@AlfrescoPublicApi +public class FileTypeImageUtils +{ + private static final String IMAGE_PREFIX16 = "/images/filetypes/"; + private static final String IMAGE_PREFIX32 = "/images/filetypes32/"; + private static final String IMAGE_PREFIX64 = "/images/filetypes64/"; + private static final String IMAGE_POSTFIX_GIF = ".gif"; + private static final String IMAGE_POSTFIX_PNG = ".png"; + private static final String DEFAULT_FILE_IMAGE16 = IMAGE_PREFIX16 + "_default" + IMAGE_POSTFIX_GIF; + private static final String DEFAULT_FILE_IMAGE32 = IMAGE_PREFIX32 + "_default" + IMAGE_POSTFIX_GIF; + private static final String DEFAULT_FILE_IMAGE64 = IMAGE_PREFIX64 + "_default" + IMAGE_POSTFIX_PNG; + + private static final Map s_fileExtensionMap = new HashMap(89, 1.0f); + + /** + * Return the image path to the filetype icon for the specified file name string + * + * @param sc ServletContext + * @param name File name to build filetype icon path for + * @param small True for the small 16x16 icon or false for the large 32x32 + * + * @return the image path for the specified node type or the default icon if not found + */ + public static String getFileTypeImage(ServletContext sc, String name, boolean small) + { + return getFileTypeImage(sc, name, (small ? FileTypeImageSize.Small : FileTypeImageSize.Medium)); + } + + /** + * Return the image path to the filetype icon for the specified file name string + * + * @param sc ServletContext + * @param name File name to build filetype icon path for + * @param size Size of the icon to return + * + * @return the image path for the specified node type or the default icon if not found + */ + public static String getFileTypeImage(ServletContext sc, String name, FileTypeImageSize size) + { + String image = null; + String defaultImage = null; + switch (size) + { + case Small: + defaultImage = DEFAULT_FILE_IMAGE16; break; + case Medium: + defaultImage = DEFAULT_FILE_IMAGE32; break; + case Large: + defaultImage = DEFAULT_FILE_IMAGE64; break; + } + + int extIndex = name.lastIndexOf('.'); + if (extIndex != -1 && name.length() > extIndex + 1) + { + String ext = name.substring(extIndex + 1).toLowerCase(); + String key = ext + ' ' + size.toString(); + + // found file extension for appropriate size image + synchronized (s_fileExtensionMap) + { + image = s_fileExtensionMap.get(key); + if (image == null) + { + // not found create for first time + if (size != FileTypeImageSize.Large) + { + image = (size == FileTypeImageSize.Small ? IMAGE_PREFIX16 : IMAGE_PREFIX32) + + ext + IMAGE_POSTFIX_GIF; + } + else + { + image = IMAGE_PREFIX64 + ext + IMAGE_POSTFIX_PNG; + } + + // does this image exist on the web-server? + if (sc != null && sc.getResourceAsStream(image) != null) + { + // found the image for this extension - save it for later + s_fileExtensionMap.put(key, image); + } + else if (sc == null) + { + // we have no ServerContext so return the default image but don't cache it + image = defaultImage; + } + else + { + // not found, save the default image for this extension instead + s_fileExtensionMap.put(key, defaultImage); + image = defaultImage; + } + } + } + } + + return (image != null ? image : defaultImage); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RegistryAsynchronouslyRefreshedCache.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RegistryAsynchronouslyRefreshedCache.java new file mode 100644 index 00000000000..8ea938bdb5c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RegistryAsynchronouslyRefreshedCache.java @@ -0,0 +1,102 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.alfresco.repo.cache.AbstractMTAsynchronouslyRefreshedCache; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.extensions.webscripts.Registry; + +/** + * Asynchronously refreshed cache for repository webscripts. + *

+ * This does not stop gratuitous calls to refresh but will ensure that, once an instance has been created, + * a version of the registry is returned even if it is slighly out of date. This can be changed so that it waits + * for reset but is probably not required. + * + * @author Derek Hulley + * @since 4.2.0 + */ +public class RegistryAsynchronouslyRefreshedCache extends AbstractMTAsynchronouslyRefreshedCache implements InitializingBean +{ + private static Log logger = LogFactory.getLog(RegistryAsynchronouslyRefreshedCache.class); + + private ObjectFactory registryFactory; + private RetryingTransactionHelper retryingTransactionHelper; + + /** + * @param registryFactory factory for web script registries + */ + public void setRegistryFactory(ObjectFactory registryFactory) + { + this.registryFactory = registryFactory; + } + + /** + * @param retryingTransactionHelper the retryingTransactionHelper to set + */ + public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + + @Override + protected Registry buildCache(final String tenantId) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Registry execute() throws Throwable + { + return doBuildCache(tenantId); + } + }, true, false); + } + + /** + * This method is thread safe as per contract of {@link #buildCache(String)}. + */ + private Registry doBuildCache(String tenantId) + { + Registry registry = registryFactory.getObject(); + registry.reset(); + logger.info("Fetching web script registry for tenant " + tenantId); + return registry; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "registryFactory", registryFactory); + PropertyCheck.mandatory(this, "retryingTransactionHelper", retryingTransactionHelper); + super.afterPropertiesSet(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepoClassPathStore.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepoClassPathStore.java new file mode 100644 index 00000000000..a5d43f2db6b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepoClassPathStore.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.IOException; + +import org.springframework.extensions.webscripts.ClassPathStore; + +import freemarker.cache.TemplateLoader; + +/** + * Extension of the SpringSurf ClassPathStore to ensure that the examination of + * last modified dates on classpath bound resources does not cause a performance + * degredation in REST heavy client applications. + *

+ * In the repository, due to the possibility of Repository bound resources, all + * WebScript search path lists have the "delay" set to either zero seconds (no delay) + * or something close to that. This means that the FreeMarker template cache is + * always or often requesting the last modified date of a classpath resource - and + * the resources do not change. Note that the /extension classpath store still uses + * the original ClassPathStore. Otherwise all stores can be refreshed as usual via + * the Refresh WebScripts command. + * + * @author Kevin Roast + */ +public class RepoClassPathStore extends ClassPathStore +{ + @Override + public TemplateLoader getTemplateLoader() + { + return new ClassPathTemplateLoader(); + } + + @Override + public long lastModified(String documentPath) + throws IOException + { + return -1L; + } + + /** + * Class Path Store implementation of a Template Loader + *

+ * Retrieves templates either from classes in the class path or classes inside of JAR files + * within the class path + *

+ * This implementation always returns a fixed last modified date of -1. + */ + private class ClassPathTemplateLoader extends ClassPathStore.ClassPathTemplateLoader + { + /** + * @see freemarker.cache.TemplateLoader#getLastModified(java.lang.Object) + */ + public long getLastModified(Object templateSource) + { + return -1L; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepoStore.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepoStore.java new file mode 100644 index 00000000000..44283e1c328 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepoStore.java @@ -0,0 +1,1109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantDeployer; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.ISO9075; +import org.alfresco.util.SearchLanguageConversion; +import org.springframework.extensions.webscripts.AbstractStore; +import org.springframework.extensions.webscripts.ScriptContent; +import org.springframework.extensions.webscripts.ScriptLoader; +import org.springframework.extensions.webscripts.WebScript; +import org.springframework.extensions.webscripts.WebScriptException; + +import freemarker.cache.TemplateLoader; + + +/** + * Repository based Web Script Store + * + * @author davidc + */ +public class RepoStore extends AbstractStore implements TenantDeployer +{ + protected boolean mustExist = false; + protected StoreRef repoStore; + protected String repoPath; + protected Map baseNodeRefs; + + // dependencies + protected RetryingTransactionHelper retryingTransactionHelper; + protected SearchService searchService; + protected NodeService nodeService; + protected ContentService contentService; + protected FileFolderService fileService; + protected NamespaceService namespaceService; + protected PermissionService permissionService; + protected TenantAdminService tenantAdminService; + + + /** + * Sets helper that provides transaction callbacks + */ + public void setTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + + /** + * Sets the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Sets the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Sets the file service + */ + public void setFileFolderService(FileFolderService fileService) + { + this.fileService = fileService; + } + + /** + * Sets the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Sets the permission service + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * Sets the tenant admin service + */ + public void setTenantAdminService(TenantAdminService tenantAdminService) + { + this.tenantAdminService = tenantAdminService; + } + + /** + * Sets whether the repo store must exist + * + * @param mustExist boolean + */ + public void setMustExist(boolean mustExist) + { + this.mustExist = mustExist; + } + + /** + * Sets the repo store + */ + public void setStore(String repoStore) + { + this.repoStore = new StoreRef(repoStore); + } + + /** + * Sets the repo path + * + * @param repoPath repoPath + */ + public void setPath(String repoPath) + { + this.repoPath = repoPath; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#init() + * @see org.alfresco.repo.tenant.TenantDeployer#init() + */ + public void init() + { + if (baseNodeRefs == null) + { + baseNodeRefs = new HashMap(1); + } + } + + /* (non-Javadoc) + * @see org.alfresco.repo.tenant.TenantDeployer#destroy() + */ + public void destroy() + { + baseNodeRefs.remove(tenantAdminService.getCurrentUserDomain()); + } + + private NodeRef getBaseNodeRef() + { + String tenantDomain = tenantAdminService.getCurrentUserDomain(); + NodeRef baseNodeRef = baseNodeRefs.get(tenantDomain); + if (baseNodeRef == null) + { + baseNodeRef = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public NodeRef doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public NodeRef execute() throws Exception + { + NodeRef repoStoreRootNodeRef = nodeService.getRootNode(repoStore); + List nodeRefs = searchService.selectNodes( + repoStoreRootNodeRef, + repoPath, + new QueryParameterDefinition[] {}, + namespaceService, + false, + SearchService.LANGUAGE_XPATH); + if (nodeRefs.size() == 1) + { + return nodeRefs.get(0); + } + else if (nodeRefs.size() > 1) + { + throw new WebScriptException( + "Web Script Store " + repoStore.toString() + repoPath + " must exist; multiple entries found."); + } + else + { + throw new WebScriptException( + "Web Script Store " + repoStore.toString() + repoPath + " must exist; it was not found"); + } + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + + // TODO clear on deleteTenant + baseNodeRefs.put(tenantDomain, baseNodeRef); + } + return baseNodeRef; + } + + private boolean isContentPresent(NodeRef nodeRef) + { + ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + if ((reader != null && reader.exists())) + { + return true; + } + return false; + } + + private String getBaseDir() + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public String doWork() throws Exception + { + return getPath(getBaseNodeRef()); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#isSecure() + */ + public boolean isSecure() + { + return false; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#exists() + */ + public boolean exists() + { + return (getBaseNodeRef() != null); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getBasePath() + */ + public String getBasePath() + { + return repoStore.toString() + repoPath; + } + + /** + * Gets the display path for the specified node + * + * @param nodeRef NodeRef + * @return display path + */ + protected String getPath(NodeRef nodeRef) + { + return nodeService.getPath(nodeRef).toDisplayPath(nodeService, permissionService) + + "/" + nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + } + + /** + * Gets the node ref for the specified path within this repo store + * + * @param documentPath String + * @return node ref + */ + protected NodeRef findNodeRef(String documentPath) + { + NodeRef node = null; + try + { + String[] pathElements = documentPath.split("/"); + List pathElementsList = Arrays.asList(pathElements); + FileInfo file = fileService.resolveNamePath(getBaseNodeRef(), pathElementsList); + node = file.getNodeRef(); + } + catch (FileNotFoundException e) + { + // NOTE: return null + } + return node; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getScriptDocumentPaths(org.alfresco.web.scripts.WebScript) + */ + public String[] getScriptDocumentPaths(final WebScript script) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public String[] doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public String[] execute() throws Exception + { + int baseDirLength = getBaseDir().length() +1; + List documentPaths = null; + String scriptPath = script.getDescription().getScriptPath(); + NodeRef scriptNodeRef = (scriptPath.length() == 0) ? getBaseNodeRef() : findNodeRef(scriptPath); + if (scriptNodeRef != null) + { + org.alfresco.service.cmr.repository.Path repoScriptPath = nodeService.getPath(scriptNodeRef); + String id = script.getDescription().getId().substring(scriptPath.length() + (scriptPath.length() > 0 ? 1 : 0)); + NodeRef repoStoreRootNodeRef = nodeService.getRootNode(repoStore); + List nodeRefs = searchService.selectNodes( + repoStoreRootNodeRef, + repoScriptPath.toPrefixString(namespaceService)+"//*[like(@cm:name, '"+SearchLanguageConversion.convert(SearchLanguageConversion.DEF_LUCENE, SearchLanguageConversion.DEF_XPATH_LIKE, id+"*")+"', false)]", + new QueryParameterDefinition[] {}, + namespaceService, + false, + SearchService.LANGUAGE_XPATH); + documentPaths = new ArrayList(nodeRefs.size()); + for (NodeRef nodeRef : nodeRefs) + { + if (isContentPresent(nodeRef)) + { + String name = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + if (name.startsWith(id)) + { + String nodeDir = getPath(nodeRef); + String documentPath = nodeDir.substring(baseDirLength); + documentPaths.add(documentPath); + } + } + } + + +// String query = "+PATH:\"" + repoScriptPath.toPrefixString(namespaceService) + +// "//*\" +QNAME:" + lucenifyNamePattern(id) + "*"; +// ResultSet resultSet = searchService.query(repoStore, SearchService.LANGUAGE_LUCENE, query); +// try +// { +// documentPaths = new ArrayList(resultSet.length()); +// List nodes = resultSet.getNodeRefs(); +// for (NodeRef nodeRef : nodes) +// { +// String name = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); +// if (name.startsWith(id)) +// { +// String nodeDir = getPath(nodeRef); +// String documentPath = nodeDir.substring(baseDirLength); +// documentPaths.add(documentPath); +// } +// } +// } +// finally +// { +// resultSet.close(); +// } + } + + return documentPaths != null ? documentPaths.toArray(new String[documentPaths.size()]) : new String[0]; + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getDocumentPaths(java.lang.String, boolean, java.lang.String) + */ + public String[] getDocumentPaths(String path, boolean includeSubPaths, String documentPattern) + { + if ((documentPattern == null) || (documentPattern.length() == 0)) + { + documentPattern = "*"; + } + + String matcher = documentPattern.replace(".","\\.").replace("*",".*"); + final Pattern pattern = Pattern.compile(matcher); + + String encPath = encodePathISO9075(path); +// final StringBuilder query = new StringBuilder(128); +// query.append("+PATH:\"").append(repoPath) +// .append(encPath.length() != 0 ? ('/' + encPath) : "") +// .append((includeSubPaths ? '/' : "")) +// .append("/*\" +QNAME:") +// .append(lucenifyNamePattern(documentPattern)); + final StringBuilder xpath = new StringBuilder(128); + xpath.append(repoPath); + xpath.append(encPath.length() != 0 ? ('/' + encPath) : ""); + xpath.append((includeSubPaths ? '/' : "")); + xpath.append("/*[like(@cm:name, '"+SearchLanguageConversion.convert(SearchLanguageConversion.DEF_LUCENE, SearchLanguageConversion.DEF_XPATH_LIKE, documentPattern+"*")+"', false)]"); + + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public String[] doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public String[] execute() throws Exception + { + int baseDirLength = getBaseDir().length() +1; + + List documentPaths; + + NodeRef repoStoreRootNodeRef = nodeService.getRootNode(repoStore); + List nodeRefs = searchService.selectNodes( + repoStoreRootNodeRef, + xpath.toString(), + new QueryParameterDefinition[] {}, + namespaceService, + false, + SearchService.LANGUAGE_XPATH); + documentPaths = new ArrayList(nodeRefs.size()); + for (NodeRef nodeRef : nodeRefs) + { + if (isContentPresent(nodeRef)) + { + String name = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + if (pattern.matcher(name).matches()) + { + String nodeDir = getPath(nodeRef); + String documentPath = nodeDir.substring(baseDirLength); + documentPaths.add(documentPath); + } + } + } +// ResultSet resultSet = searchService.query(repoStore, SearchService.LANGUAGE_LUCENE, query.toString()); +// try +// { +// documentPaths = new ArrayList(resultSet.length()); +// List nodes = resultSet.getNodeRefs(); +// for (NodeRef nodeRef : nodes) +// { +// String name = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); +// if (pattern.matcher(name).matches()) +// { +// String nodeDir = getPath(nodeRef); +// String documentPath = nodeDir.substring(baseDirLength); +// documentPaths.add(documentPath); +// } +// } +// } +// finally +// { +// resultSet.close(); +// } + return documentPaths.toArray(new String[documentPaths.size()]); + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Helper to encode the elements of a path to be used as a Lucene PATH statement + * using the ISO9075 encoding. Note that leading and trailing '/' elements will NOT + * be preserved. + * + * @param path Path to encode, elements separated by '/' + * + * @return the encoded path, a minimum of the empty string will be returned + */ + public static String encodePathISO9075(String path) + { + if (path == null || path.length() == 0) + { + return ""; + } + StringBuilder result = new StringBuilder(path.length() + 16); + for (StringTokenizer t = new StringTokenizer(path, "/"); t.hasMoreTokens(); /**/) + { + result.append(ISO9075.encode(t.nextToken())); + if (t.hasMoreTokens()) + { + result.append('/'); + } + } + return result.toString(); + } + + /** + * ALF-7059: Because we can't quote QNAME patterns, and because characters like minus have special meaning, we have + * to pass the document 'pattern' though the Lucene escaper, preserving the wildcard parts. Also, because you can't + * search for whitespace in a QNAME, we have to replace whitespace with the ? wildcard + * + * @param pattern String + * @return String + */ + private static String lucenifyNamePattern(String pattern) + { + // Assume already escaped if the pattern includes a backslash + if (pattern.indexOf('\\') != -1) + { + return pattern; + } + StringBuilder result = new StringBuilder(pattern.length() * 2); + StringTokenizer tkn = new StringTokenizer(pattern, "\t\r\n *", true); + while (tkn.hasMoreTokens()) + { + String token = tkn.nextToken(); + if (token.length() == 1) + { + char c = token.charAt(0); + if (Character.isWhitespace(c)) + { + // We can't include whitespace in a QNAME expression so we will have to use a wildcard character and + // filter the results later + result.append('?'); + } + else if (c == '*') + { + result.append(c); + } + else + { + result.append(SearchLanguageConversion.escapeLuceneQuery(token)); + } + } + else + { + result.append(SearchLanguageConversion.escapeLuceneQuery(token)); + } + } + return result.toString(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getDescriptionDocumentPaths() + */ + public String[] getDescriptionDocumentPaths() + { + return getDocumentPaths("/", true, DESC_PATH_PATTERN); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getAllDocumentPaths() + */ + public String[] getAllDocumentPaths() + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public String[] doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public String[] execute() throws Exception + { + int baseDirLength = getBaseDir().length() +1; + + List documentPaths; + + NodeRef repoStoreRootNodeRef = nodeService.getRootNode(repoStore); + List nodeRefs = searchService.selectNodes( + repoStoreRootNodeRef, + repoPath + + "//*[subtypeOf('{http://www.alfresco.org/model/content/1.0}content')]\"", + new QueryParameterDefinition[] {}, + namespaceService, + false, + SearchService.LANGUAGE_XPATH); + documentPaths = new ArrayList(nodeRefs.size()); + for (NodeRef nodeRef : nodeRefs) + { + if (isContentPresent(nodeRef)) + { + String nodeDir = getPath(nodeRef); + documentPaths.add(nodeDir.substring(baseDirLength)); + } + } + +// String query = "+PATH:\"" + repoPath + +// "//*\" +TYPE:\"{http://www.alfresco.org/model/content/1.0}content\""; +// ResultSet resultSet = searchService.query(repoStore, SearchService.LANGUAGE_LUCENE, query); +// try +// { +// documentPaths = new ArrayList(resultSet.length()); +// List nodes = resultSet.getNodeRefs(); +// for (NodeRef nodeRef : nodes) +// { +// String nodeDir = getPath(nodeRef); +// documentPaths.add(nodeDir.substring(baseDirLength)); +// } +// } +// finally +// { +// resultSet.close(); +// } + + return documentPaths.toArray(new String[documentPaths.size()]); + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#lastModified(java.lang.String) + */ + public long lastModified(final String documentPath) throws IOException + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Long doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Long execute() throws Exception + { + ContentReader reader = contentService.getReader( + findNodeRef(documentPath), ContentModel.PROP_CONTENT); + return reader.getLastModified(); + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#hasDocument(java.lang.String) + */ + public boolean hasDocument(final String documentPath) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Boolean doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Boolean execute() throws Exception + { + NodeRef nodeRef = findNodeRef(documentPath); + return (nodeRef != null); + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getDescriptionDocument(java.lang.String) + */ + public InputStream getDocument(final String documentPath) + throws IOException + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public InputStream doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public InputStream execute() throws Exception + { + NodeRef nodeRef = findNodeRef(documentPath); + if (nodeRef == null) + { + throw new IOException("Document " + documentPath + " does not exist."); + } + ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + if (reader == null || !reader.exists()) + { + throw new IOException("Failed to read content at " + documentPath + " (content reader does not exist)"); + } + return reader.getContentInputStream(); + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#createDocument(java.lang.String, java.lang.String) + */ + public void createDocument(String documentPath, String content) throws IOException + { + String[] pathElements = documentPath.split("/"); + String[] folderElements = new String[pathElements.length -1]; + System.arraycopy(pathElements, 0, folderElements, 0, pathElements.length -1); + List folderElementsList = Arrays.asList(folderElements); + + // create folder + FileInfo pathInfo; + if (folderElementsList.size() == 0) + { + pathInfo = fileService.getFileInfo(getBaseNodeRef()); + } + else + { + pathInfo = FileFolderServiceImpl.makeFolders(fileService, getBaseNodeRef(), folderElementsList, ContentModel.TYPE_FOLDER); + } + + // create file + String fileName = pathElements[pathElements.length -1]; + if (fileService.searchSimple(pathInfo.getNodeRef(), fileName) != null) + { + throw new IOException("Document " + documentPath + " already exists"); + } + FileInfo fileInfo = fileService.create(pathInfo.getNodeRef(), fileName, ContentModel.TYPE_CONTENT); + ContentWriter writer = fileService.getWriter(fileInfo.getNodeRef()); + writer.putContent(content); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#updateDocument(java.lang.String, java.lang.String) + */ + public void updateDocument(String documentPath, String content) throws IOException + { + String[] pathElements = documentPath.split("/"); + + // get parent folder + NodeRef parentRef; + if (pathElements.length == 1) + { + parentRef = getBaseNodeRef(); + } + else + { + parentRef = findNodeRef(documentPath.substring(0, documentPath.lastIndexOf('/'))); + } + + // update file + String fileName = pathElements[pathElements.length -1]; + if (fileService.searchSimple(parentRef, fileName) == null) + { + throw new IOException("Document " + documentPath + " does not exists"); + } + FileInfo fileInfo = fileService.create(parentRef, fileName, ContentModel.TYPE_CONTENT); + ContentWriter writer = fileService.getWriter(fileInfo.getNodeRef()); + writer.putContent(content); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#removeDocument(java.lang.String) + */ + public boolean removeDocument(String documentPath) + throws IOException + { + // TODO: Implement remove for Repository Store + return false; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getTemplateLoader() + */ + public TemplateLoader getTemplateLoader() + { + return new RepoTemplateLoader(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Store#getScriptLoader() + */ + public ScriptLoader getScriptLoader() + { + return new RepoScriptLoader(); + } + + + /* (non-Javadoc) + * @see org.alfresco.repo.tenant.TenantDeployer#onEnableTenant() + */ + public void onEnableTenant() + { + init(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.tenant.TenantDeployer#onDisableTenant() + */ + public void onDisableTenant() + { + destroy(); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return repoPath; + } + + /** + * Repository path based template loader + * + * @author davidc + */ + private class RepoTemplateLoader implements TemplateLoader + { + /* (non-Javadoc) + * @see freemarker.cache.TemplateLoader#findTemplateSource(java.lang.String) + */ + public Object findTemplateSource(final String name) + throws IOException + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + RepoTemplateSource source = null; + NodeRef nodeRef = findNodeRef(name); + if (nodeRef != null) + { + source = new RepoTemplateSource(nodeRef); + } + return source; + } + }, true); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see freemarker.cache.TemplateLoader#getLastModified(java.lang.Object) + */ + public long getLastModified(Object templateSource) + { + return ((RepoTemplateSource)templateSource).lastModified(); + } + + /* (non-Javadoc) + * @see freemarker.cache.TemplateLoader#getReader(java.lang.Object, java.lang.String) + */ + public Reader getReader(Object templateSource, String encoding) throws IOException + { + return ((RepoTemplateSource)templateSource).getReader(); + } + + /* (non-Javadoc) + * @see freemarker.cache.TemplateLoader#closeTemplateSource(java.lang.Object) + */ + public void closeTemplateSource(Object arg0) throws IOException + { + } + } + + /** + * Repository (content) node template source + * + * @author davidc + */ + private class RepoTemplateSource + { + protected final NodeRef nodeRef; + + /** + * Construct + * + * @param ref NodeRef + */ + private RepoTemplateSource(NodeRef ref) + { + this.nodeRef = ref; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object o) + { + if (o instanceof RepoTemplateSource) + { + return nodeRef.equals(((RepoTemplateSource)o).nodeRef); + } + else + { + return false; + } + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + public int hashCode() + { + return nodeRef.hashCode(); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() + { + return nodeRef.toString(); + } + + /** + * Gets the last modified time of the content + * + * @return last modified time + */ + public long lastModified() + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Long doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Long execute() throws Exception + { + ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + return reader.getLastModified(); + } + }); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Gets the content reader + * + * @return content reader + * @throws IOException + */ + public Reader getReader() throws IOException + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Reader doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Reader execute() throws Exception + { + ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + return new InputStreamReader(reader.getContentInputStream(), reader.getEncoding()); + } + }); + } + }, AuthenticationUtil.getSystemUserName()); + } + } + + /** + * Repository path based script loader + * + * @author davidc + */ + private class RepoScriptLoader implements ScriptLoader + { + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptLoader#getScriptLocation(java.lang.String) + */ + public ScriptContent getScript(final String path) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public ScriptContent doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public ScriptContent execute() throws Exception + { + ScriptContent location = null; + NodeRef nodeRef = findNodeRef(path); + if (nodeRef != null) + { + location = new RepoScriptContent(path, nodeRef); + } + return location; + } + }, true, false); + } + }, AuthenticationUtil.getSystemUserName()); + } + } + + /** + * Repo path script location + * + * @author davidc + */ + private class RepoScriptContent implements ScriptContent + { + protected String path; + protected NodeRef nodeRef; + + /** + * Construct + * + * @param path String + * @param nodeRef NodeRef + */ + public RepoScriptContent(String path, NodeRef nodeRef) + { + this.path = path; + this.nodeRef = nodeRef; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#getInputStream() + */ + public InputStream getInputStream() + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public InputStream doWork() throws Exception + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public InputStream execute() throws Exception + { + ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + return reader.getContentInputStream(); + } + }); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#getReader() + */ + public Reader getReader() + { + ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + try + { + return new InputStreamReader(getInputStream(), reader.getEncoding()); + } + catch (UnsupportedEncodingException e) + { + throw new AlfrescoRuntimeException("Unsupported Encoding", e); + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptContent#getPath() + */ + public String getPath() + { + return repoStore + getBaseDir() + "/" + path; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptContent#getPathDescription() + */ + public String getPathDescription() + { + return "/" + path + " (in repository store " + repoStore.toString() + getBaseDir() + ")"; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptContent#isCachable() + */ + public boolean isCachable() + { + return false; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptContent#isSecure() + */ + public boolean isSecure() + { + return false; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java new file mode 100644 index 00000000000..7686f302c4d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryContainer.java @@ -0,0 +1,755 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.File; +import java.io.IOException; +import java.net.SocketException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.error.ExceptionStackUtil; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.transaction.TooBusyException; +import org.alfresco.repo.web.scripts.bean.LoginPost; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.extensions.webscripts.AbstractRuntimeContainer; +import org.springframework.extensions.webscripts.Authenticator; +import org.springframework.extensions.webscripts.Description; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.webscripts.Description.RequiredTransaction; +import org.springframework.extensions.webscripts.Description.RequiredTransactionParameters; +import org.springframework.extensions.webscripts.Description.TransactionCapability; +import org.springframework.extensions.webscripts.ServerModel; +import org.springframework.extensions.webscripts.WebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * Repository (server-tier) container for Web Scripts + * + * @author steveglover + * @author davidc + */ +public class RepositoryContainer extends AbstractRuntimeContainer +{ + // Logger + protected static final Log logger = LogFactory.getLog(RepositoryContainer.class); + + /** Component Dependencies */ + private Repository repository; + private RepositoryImageResolver imageResolver; + private TransactionService transactionService; + private RetryingTransactionHelper fallbackTransactionHelper; + private AuthorityService authorityService; + private DescriptorService descriptorService; + + private boolean encryptTempFiles = false; + private String tempDirectoryName = null; + private int memoryThreshold = 4 * 1024 * 1024; // 4mb + private long maxContentSize = (long) 4 * 1024 * 1024 * 1024; // 4gb + private TempOutputStreamFactory streamFactory = null; + private TempOutputStreamFactory responseStreamFactory = null; + private String preserveHeadersPattern = null; + + private Class[] notPublicExceptions = new Class[] {}; + private Class[] publicExceptions = new Class[] {}; + + /* + * Shame init is already used (by TenantRepositoryContainer). + */ + public void setup() + { + File tempDirectory = TempFileProvider.getTempDir(tempDirectoryName); + this.streamFactory = new TempOutputStreamFactory(tempDirectory, memoryThreshold, maxContentSize, encryptTempFiles, false); + this.responseStreamFactory = new TempOutputStreamFactory(tempDirectory, memoryThreshold, maxContentSize, encryptTempFiles, true); + } + + public void setEncryptTempFiles(Boolean encryptTempFiles) + { + if(encryptTempFiles != null) + { + this.encryptTempFiles = encryptTempFiles.booleanValue(); + } + } + + public void setTempDirectoryName(String tempDirectoryName) + { + this.tempDirectoryName = tempDirectoryName; + } + + public void setMemoryThreshold(Integer memoryThreshold) + { + if(memoryThreshold != null) + { + this.memoryThreshold = memoryThreshold.intValue(); + } + } + + public void setMaxContentSize(Long maxContentSize) + { + if(maxContentSize != null) + { + this.maxContentSize = maxContentSize.longValue(); + } + } + + public void setPreserveHeadersPattern(String preserveHeadersPattern) + { + this.preserveHeadersPattern = preserveHeadersPattern; + } + + /** + * @param repository Repository + */ + public void setRepository(Repository repository) + { + this.repository = repository; + } + + /** + * @param imageResolver RepositoryImageResolver + */ + public void setRepositoryImageResolver(RepositoryImageResolver imageResolver) + { + this.imageResolver = imageResolver; + } + + /** + * @param transactionService TransactionService + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * @param fallbackTransactionHelper an unlimited transaction helper used to generate error responses + */ + public void setFallbackTransactionHelper(RetryingTransactionHelper fallbackTransactionHelper) + { + this.fallbackTransactionHelper = fallbackTransactionHelper; + } + + /** + * @param descriptorService DescriptorService + */ + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + /** + * @param authorityService AuthorityService + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /** + * Exceptions which may contain information that cannot be displayed in UI + * + * @param notPublicExceptions - {@link Class}<?>[] instance which contains list of not public exceptions + */ + public void setNotPublicExceptions(List> notPublicExceptions) + { + this.notPublicExceptions = new Class[] {}; + if((null != notPublicExceptions) && !notPublicExceptions.isEmpty()) + { + this.notPublicExceptions = notPublicExceptions.toArray(this.notPublicExceptions); + } + } + + public Class[] getNotPublicExceptions() + { + return notPublicExceptions; + } + + /** + * Exceptions which may contain information that need to display in UI + * + * @param publicExceptions - {@link Class}<?>[] instance which contains list of public exceptions + */ + public void setPublicExceptions(List> publicExceptions) + { + this.publicExceptions = new Class[] {}; + if((null != publicExceptions) && !publicExceptions.isEmpty()) + { + this.publicExceptions = publicExceptions.toArray(this.publicExceptions); + } + } + + public Class[] getPublicExceptions() + { + return publicExceptions; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Container#getDescription() + */ + public ServerModel getDescription() + { + return new RepositoryServerModel(descriptorService.getCurrentRepositoryDescriptor(), descriptorService.getServerDescriptor()); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getScriptParameters() + */ + public Map getScriptParameters() + { + Map params = new HashMap(); + params.putAll(super.getScriptParameters()); + addRepoParameters(params); + return params; + } + + /* + * (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getTemplateParameters() + */ + public Map getTemplateParameters() + { + // Ensure we have a transaction - we might be generating the status template after the main transaction failed + return fallbackTransactionHelper.doInTransaction(new RetryingTransactionCallback>() + { + public Map execute() throws Throwable + { + Map params = new HashMap(); + params.putAll(RepositoryContainer.super.getTemplateParameters()); + params.put(TemplateService.KEY_IMAGE_RESOLVER, imageResolver.getImageResolver()); + addRepoParameters(params); + return params; + } + }, true); + } + + /** + * Add Repository specific parameters + * + * @param params Map + */ + private void addRepoParameters(Map params) + { + if (AlfrescoTransactionSupport.getTransactionId() != null && + AuthenticationUtil.getFullAuthentication() != null) + { + NodeRef rootHome = repository.getRootHome(); + if (rootHome != null) + { + params.put("roothome", rootHome); + } + NodeRef companyHome = repository.getCompanyHome(); + if (companyHome != null) + { + params.put("companyhome", companyHome); + } + NodeRef person = repository.getFullyAuthenticatedPerson(); + if (person != null) + { + params.put("person", person); + NodeRef userHome = repository.getUserHome(person); + if (userHome != null) + { + params.put("userhome", userHome); + } + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.RuntimeContainer#executeScript(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse, org.alfresco.web.scripts.Authenticator) + */ + public void executeScript(WebScriptRequest scriptReq, WebScriptResponse scriptRes, final Authenticator auth) + throws IOException + { + try + { + executeScriptInternal(scriptReq, scriptRes, auth); + } + catch (RuntimeException e) + { + Throwable hideCause = ExceptionStackUtil.getCause(e, notPublicExceptions); + Throwable displayCause = ExceptionStackUtil.getCause(e, publicExceptions); + if (displayCause == null && hideCause != null) + { + AlfrescoRuntimeException alf = null; + if (e instanceof AlfrescoRuntimeException) + { + alf = (AlfrescoRuntimeException) e; + } + else + { + // The message will not have a numerical identifier + alf = new AlfrescoRuntimeException("WebScript execution failed", e); + } + String num = alf.getNumericalId(); + logger.error("Server error (" + num + ")", e); + throw new RuntimeException("Server error (" + num + "). Details can be found in the server logs."); + } + else + { + throw e; + } + } + } + + protected void executeScriptInternal(WebScriptRequest scriptReq, WebScriptResponse scriptRes, final Authenticator auth) + throws IOException + { + final WebScript script = scriptReq.getServiceMatch().getWebScript(); + final Description desc = script.getDescription(); + final boolean debug = logger.isDebugEnabled(); + + // Escalate the webscript declared level of authentication to the container required authentication + // eg. must be guest if MT is enabled unless credentials are empty + RequiredAuthentication containerRequiredAuthentication = getRequiredAuthentication(); + final RequiredAuthentication required = (desc.getRequiredAuthentication().compareTo(containerRequiredAuthentication) < 0 && !auth.emptyCredentials() ? containerRequiredAuthentication : desc.getRequiredAuthentication()); + final boolean isGuest = scriptReq.isGuest(); + + if (required == RequiredAuthentication.none) + { + // TODO revisit - cleared here, in-lieu of WebClient clear + //AuthenticationUtil.clearCurrentSecurityContext(); + + transactionedExecuteAs(script, scriptReq, scriptRes); + } + else if ((required == RequiredAuthentication.user || required == RequiredAuthentication.admin) && isGuest) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access."); + } + else + { + try + { + AuthenticationUtil.pushAuthentication(); + + // + // Determine if user already authenticated + // + if (debug) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + logger.debug("Current authentication: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); + logger.debug("Authentication required: " + required); + logger.debug("Guest login requested: " + isGuest); + } + + // + // Apply appropriate authentication to Web Script invocation + // + RetryingTransactionCallback authWork = new RetryingTransactionCallback() + { + public Boolean execute() throws Exception + { + if (auth == null || auth.authenticate(required, isGuest)) + { + // The user will now have been authenticated, based on HTTP Auth, Ticket etc + // Check that the user they authenticated as has appropriate access to the script + + // Check to see if they supplied HTTP Auth or Ticket as guest, on a script that needs more + if (required == RequiredAuthentication.user || required == RequiredAuthentication.admin) + { + String authenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser(); + String runAsUser = AuthenticationUtil.getRunAsUser(); + + if ( (authenticatedUser == null) || + (authenticatedUser.equals(runAsUser) && authorityService.hasGuestAuthority()) || + (!authenticatedUser.equals(runAsUser) && authorityService.isGuestAuthority(authenticatedUser)) ) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access."); + } + } + + // Check to see if they're admin or system on an Admin only script + if (required == RequiredAuthentication.admin && !(authorityService.hasAdminAuthority() || AuthenticationUtil.getFullyAuthenticatedUser().equals(AuthenticationUtil.getSystemUserName()))) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires admin authentication; however, a non-admin has attempted access."); + } + + if (debug) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + logger.debug("Authentication: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); + } + + return true; + } + return false; + } + }; + + boolean readOnly = transactionService.isReadOnly(); + boolean requiresNew = !readOnly && AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY; + if (transactionService.getRetryingTransactionHelper().doInTransaction(authWork, readOnly, requiresNew)) + { + // Execute Web Script if authentication passed + // The Web Script has its own txn management with potential runAs() user + transactionedExecuteAs(script, scriptReq, scriptRes); + } + else + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed for Web Script " + desc.getId()); + } + } + finally + { + // + // Reset authentication for current thread + // + AuthenticationUtil.popAuthentication(); + + if (debug) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + logger.debug("Authentication reset: " + (currentUser == null ? "unauthenticated" : "authenticated as " + currentUser)); + } + } + } + } + + /** + * Execute script within required level of transaction + * + * @param script WebScript + * @param scriptReq WebScriptRequest + * @param scriptRes WebScriptResponse + * @throws IOException + */ + protected void transactionedExecute(final WebScript script, final WebScriptRequest scriptReq, final WebScriptResponse scriptRes) + throws IOException + { + try + { + final Description description = script.getDescription(); + if (description.getRequiredTransaction() == RequiredTransaction.none) + { + script.execute(scriptReq, scriptRes); + } + else + { + final BufferedRequest bufferedReq; + final BufferedResponse bufferedRes; + RequiredTransactionParameters trxParams = description.getRequiredTransactionParameters(); + if (trxParams.getCapability() == TransactionCapability.readwrite) + { + if (trxParams.getBufferSize() > 0) + { + if (logger.isDebugEnabled()) + logger.debug("Creating Transactional Response for ReadWrite transaction; buffersize=" + trxParams.getBufferSize()); + + // create buffered request and response that allow transaction retrying + bufferedReq = new BufferedRequest(scriptReq, streamFactory); + bufferedRes = new BufferedResponse(scriptRes, trxParams.getBufferSize(), responseStreamFactory); + } + else + { + if (logger.isDebugEnabled()) + logger.debug("Transactional Response bypassed for ReadWrite - buffersize=0"); + bufferedReq = null; + bufferedRes = null; + } + } + else + { + bufferedReq = null; + bufferedRes = null; + } + + // encapsulate script within transaction + RetryingTransactionCallback work = new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + try + { + if (logger.isDebugEnabled()) + logger.debug("Begin retry transaction block: " + description.getRequiredTransaction() + "," + + description.getRequiredTransactionParameters().getCapability()); + + if (bufferedRes == null) + { + script.execute(scriptReq, scriptRes); + } + else + { + // Reset the request and response in case of a transaction retry + bufferedReq.reset(); + // REPO-4388 don't reset specified headers + bufferedRes.reset(preserveHeadersPattern); + script.execute(bufferedReq, bufferedRes); + } + } + catch(Exception e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Transaction exception: " + description.getRequiredTransaction() + ": " + e.getMessage()); + // Note: user transaction shouldn't be null, but just in case inside this exception handler + UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); + if (userTrx != null) + { + logger.debug("Transaction status: " + userTrx.getStatus()); + } + } + + UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); + if (userTrx != null) + { + if (userTrx.getStatus() != Status.STATUS_MARKED_ROLLBACK) + { + if (logger.isDebugEnabled()) + logger.debug("Marking web script transaction for rollback"); + try + { + userTrx.setRollbackOnly(); + } + catch(Throwable re) + { + if (logger.isDebugEnabled()) + logger.debug("Caught and ignoring exception during marking for rollback: " + re.getMessage()); + } + } + } + + // re-throw original exception for retry + throw e; + } + finally + { + if (logger.isDebugEnabled()) + logger.debug("End retry transaction block: " + description.getRequiredTransaction() + "," + + description.getRequiredTransactionParameters().getCapability()); + } + + return null; + } + }; + + boolean readonly = description.getRequiredTransactionParameters().getCapability() == TransactionCapability.readonly; + boolean requiresNew = description.getRequiredTransaction() == RequiredTransaction.requiresnew; + + // log a warning if we detect a GET webscript being run in a readwrite transaction, GET calls should + // NOT have any side effects so this scenario as a warning sign something maybe amiss, see ALF-10179. + if (logger.isDebugEnabled() && !readonly && "GET".equalsIgnoreCase(description.getMethod())) + { + logger.debug("Webscript with URL '" + scriptReq.getURL() + + "' is a GET request but it's descriptor has declared a readwrite transaction is required"); + } + + try + { + RetryingTransactionHelper transactionHelper = transactionService.getRetryingTransactionHelper(); + if(script instanceof LoginPost) + { + //login script requires read-write transaction because of authorization intercepter + transactionHelper.setForceWritable(true); + } + transactionHelper.doInTransaction(work, readonly, requiresNew); + } + catch (TooBusyException e) + { + // Map TooBusyException to a 503 status code + throw new WebScriptException(HttpServletResponse.SC_SERVICE_UNAVAILABLE, e.getMessage(), e); + } + finally + { + // Get rid of any temporary files + if (bufferedReq != null) + { + bufferedReq.close(); + } + } + + // Ensure a response is always flushed after successful execution + if (bufferedRes != null) + { + bufferedRes.writeResponse(); + } + + } + } + catch (IOException ioe) + { + Throwable socketException = ExceptionStackUtil.getCause(ioe, SocketException.class); + Class clientAbortException = null; + try + { + clientAbortException = Class.forName("org.apache.catalina.connector.ClientAbortException"); + } + catch (ClassNotFoundException e) + { + // do nothing + } + // Note: if you need to look for more exceptions in the stack, then create a static array and pass it in + if ((socketException != null && socketException.getMessage().contains("Broken pipe")) || (clientAbortException != null && ExceptionStackUtil.getCause(ioe, clientAbortException) != null)) + { + if (logger.isDebugEnabled()) + { + logger.warn("Client has cut off communication", ioe); + } + else + { + logger.info("Client has cut off communication"); + } + } + else + { + throw ioe; + } + } + } + + /** + * Execute script within required level of transaction as required effective user. + * + * @param script WebScript + * @param scriptReq WebScriptRequest + * @param scriptRes WebScriptResponse + * @throws IOException + */ + private void transactionedExecuteAs(final WebScript script, final WebScriptRequest scriptReq, + final WebScriptResponse scriptRes) throws IOException + { + String runAs = script.getDescription().getRunAs(); + if (runAs == null) + { + transactionedExecute(script, scriptReq, scriptRes); + } + else + { + RunAsWork work = new RunAsWork() + { + public Object doWork() throws Exception + { + transactionedExecute(script, scriptReq, scriptRes); + return null; + } + }; + AuthenticationUtil.runAs(work, runAs); + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + @Override + public void onApplicationEvent(ApplicationEvent event) + { + if (event instanceof ContextRefreshedEvent) + { + ContextRefreshedEvent refreshEvent = (ContextRefreshedEvent)event; + ApplicationContext refreshContext = refreshEvent.getApplicationContext(); + if (refreshContext != null && refreshContext.equals(applicationContext)) + { + RunAsWork work = new RunAsWork() + { + public Object doWork() throws Exception + { + reset(); + return null; + } + }; + AuthenticationUtil.runAs(work, AuthenticationUtil.getSystemUserName()); + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#getRequiredAuthentication() + */ + @Override + public RequiredAuthentication getRequiredAuthentication() + { + if (AuthenticationUtil.isMtEnabled()) + { + return RequiredAuthentication.guest; // user or guest (ie. at least guest) + } + + return RequiredAuthentication.none; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.RuntimeContainer#authenticate(org.alfresco.web.scripts.Authenticator, org.alfresco.web.scripts.Description.RequiredAuthentication) + */ + @Override + public boolean authenticate(Authenticator auth, RequiredAuthentication required) + { + if (auth != null) + { + AuthenticationUtil.clearCurrentSecurityContext(); + + return auth.authenticate(required, false); + } + + return false; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.AbstractRuntimeContainer#reset() + */ + @Override + public void reset() + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + internalReset(); + return null; + } + }, true, false); + } + + private void internalReset() + { + super.reset(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryImageResolver.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryImageResolver.java new file mode 100644 index 00000000000..04053ac6d33 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryImageResolver.java @@ -0,0 +1,80 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import javax.servlet.ServletContext; + +import org.alfresco.service.cmr.repository.FileTypeImageSize; +import org.alfresco.service.cmr.repository.TemplateImageResolver; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.web.context.ServletContextAware; + + +/** + * Web Scripts Image Resolver + * + * @author davidc + */ +public class RepositoryImageResolver + implements ServletContextAware, InitializingBean +{ + private ServletContext servletContext; + private TemplateImageResolver imageResolver; + + + /* (non-Javadoc) + * @see org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet.ServletContext) + */ + public void setServletContext(ServletContext context) + { + this.servletContext = context; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @SuppressWarnings("serial") + public void afterPropertiesSet() + throws Exception + { + this.imageResolver = new TemplateImageResolver() + { + public String resolveImagePathForName(String filename, FileTypeImageSize size) + { + return FileTypeImageUtils.getFileTypeImage(servletContext, filename, size); + } + }; + } + + /** + * @return image resolver + */ + public TemplateImageResolver getImageResolver() + { + return this.imageResolver; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryScriptProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryScriptProcessor.java new file mode 100644 index 00000000000..8e3b34208cd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryScriptProcessor.java @@ -0,0 +1,206 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.jscript.ValueConverter; +import org.alfresco.scripts.ScriptException; +import org.alfresco.service.cmr.repository.ScriptLocation; +import org.alfresco.service.cmr.repository.ScriptService; +import org.springframework.extensions.webscripts.MultiScriptLoader; +import org.springframework.extensions.webscripts.ScriptContent; +import org.springframework.extensions.webscripts.ScriptLoader; +import org.springframework.extensions.webscripts.ScriptProcessor; +import org.springframework.extensions.webscripts.SearchPath; +import org.springframework.extensions.webscripts.Store; +import org.springframework.extensions.webscripts.WebScriptException; + + +/** + * Repository (server-tier) Web Script Processor + * + * @author davidc + */ +public class RepositoryScriptProcessor implements ScriptProcessor +{ + // dependencies + protected ScriptService scriptService; + protected ScriptLoader scriptLoader; + protected SearchPath searchPath; + + // Javascript Converter + private final ValueConverter valueConverter = new ValueConverter(); + + + /** + * @param scriptService ScriptService + */ + public void setScriptService(ScriptService scriptService) + { + this.scriptService = scriptService; + } + + /** + * @param searchPath SearchPath + */ + public void setSearchPath(SearchPath searchPath) + { + this.searchPath = searchPath; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptProcessor#findScript(java.lang.String) + */ + public ScriptContent findScript(String path) + { + return scriptLoader.getScript(path); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptProcessor#executeScript(java.lang.String, java.util.Map) + */ + public Object executeScript(String path, Map model) + throws ScriptException + { + // locate script within web script stores + ScriptContent scriptContent = findScript(path); + if (scriptContent == null) + { + throw new WebScriptException("Unable to locate script " + path); + } + // execute script + return executeScript(scriptContent, model); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptProcessor#executeScript(org.alfresco.web.scripts.ScriptContent, java.util.Map) + */ + public Object executeScript(ScriptContent content, Map model) + { + return scriptService.executeScript("javascript", new RepositoryScriptLocation(content), model); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptProcessor#unwrapValue(java.lang.Object) + */ + public Object unwrapValue(Object value) + { + return valueConverter.convertValueForJava(value); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ScriptProcessor#reset() + */ + public void reset() + { + init(); + this.scriptService.resetScriptProcessors(); + } + + /** + * Register script loader from each Web Script Store with Script Processor + */ + private void init() + { + List loaders = new ArrayList(); + for (Store apiStore : searchPath.getStores()) + { + ScriptLoader loader = apiStore.getScriptLoader(); + if (loader == null) + { + throw new WebScriptException("Unable to retrieve script loader for Web Script store " + apiStore.getBasePath()); + } + loaders.add(loader); + } + scriptLoader = new MultiScriptLoader(loaders.toArray(new ScriptLoader[loaders.size()])); + } + + + /** + * Script Location Facade + */ + private static class RepositoryScriptLocation implements ScriptLocation + { + private ScriptContent content; + + private RepositoryScriptLocation(ScriptContent content) + { + this.content = content; + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#getInputStream() + */ + public InputStream getInputStream() + { + return content.getInputStream(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#getReader() + */ + public Reader getReader() + { + return content.getReader(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#isCachable() + */ + public boolean isCachable() + { + return content.isCachable(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#isSecure() + */ + public boolean isSecure() + { + return content.isSecure(); + } + + /* (non-Javadoc) + * @see org.alfresco.service.cmr.repository.ScriptLocation#getPath() + */ + public String getPath() + { + return content.getPath(); + } + + @Override + public String toString() + { + return content.getPathDescription(); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryScriptProcessorFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryScriptProcessorFactory.java new file mode 100644 index 00000000000..69e67fa58e9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryScriptProcessorFactory.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.springframework.extensions.webscripts.ScriptProcessor; +import org.springframework.extensions.webscripts.ScriptProcessorFactory; + +/** + * @author Kevin Roast + */ +public class RepositoryScriptProcessorFactory implements ScriptProcessorFactory +{ + private ScriptProcessor scriptProcessor; + + + /** + * @param scriptProcessor the ScriptProcessor to set + */ + public void setScriptProcessor(ScriptProcessor scriptProcessor) + { + this.scriptProcessor = scriptProcessor; + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.ScriptProcessorFactory#newInstance() + */ + public ScriptProcessor newInstance() + { + return scriptProcessor; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryServerModel.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryServerModel.java new file mode 100644 index 00000000000..a23b391c674 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryServerModel.java @@ -0,0 +1,142 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.alfresco.service.descriptor.Descriptor; +import org.springframework.extensions.webscripts.ServerModel; + + +/** + * Script / Template Model representing Repository Server meta-data + * + * @author davidc + */ +public class RepositoryServerModel implements ServerModel +{ + private Descriptor currentDescriptor; + private Descriptor serverDescriptor; + + /** + * Construct + * + * @param currentDescriptor Descriptor + * @param serverDescriptor Descriptor + */ + /*package*/ RepositoryServerModel(Descriptor currentDescriptor, Descriptor serverDescriptor) + { + this.currentDescriptor = currentDescriptor; + this.serverDescriptor = serverDescriptor; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getContainerName() + */ + public String getContainerName() + { + return "Repository"; + } + + /*(non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getId() + */ + public String getId() + { + return currentDescriptor.getId(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getName() + */ + public String getName() + { + return currentDescriptor.getName(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getVersionMajor() + */ + public String getVersionMajor() + { + return currentDescriptor.getVersionMajor(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getVersionMinor() + */ + public String getVersionMinor() + { + return currentDescriptor.getVersionMinor(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getVersionRevision() + */ + public String getVersionRevision() + { + return currentDescriptor.getVersionRevision(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getVersionLabel() + */ + public String getVersionLabel() + { + return currentDescriptor.getVersionLabel(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getVersionBuild() + */ + public String getVersionBuild() + { + return currentDescriptor.getVersionBuild(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getVersion() + */ + public String getVersion() + { + return currentDescriptor.getVersion(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getEdition() + */ + public String getEdition() + { + return serverDescriptor.getEdition(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.ServerModel#getSchema() + */ + public int getSchema() + { + return currentDescriptor.getSchema(); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryTemplateProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryTemplateProcessor.java new file mode 100644 index 00000000000..a5e4a058e52 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryTemplateProcessor.java @@ -0,0 +1,261 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.processor.ProcessorExtension; +import org.alfresco.repo.template.FreeMarkerProcessor; +import org.alfresco.repo.template.QNameAwareObjectWrapper; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.springframework.extensions.webscripts.SearchPath; +import org.springframework.extensions.webscripts.Store; +import org.springframework.extensions.webscripts.TemplateProcessor; +import org.springframework.extensions.webscripts.WebScriptException; + +import freemarker.cache.MultiTemplateLoader; +import freemarker.cache.StrongCacheStorage; +import freemarker.cache.TemplateLoader; +import freemarker.core.TemplateClassResolver; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateExceptionHandler; +import freemarker.template.Version; + + +/** + * Repository (server-tier) Web Script Template Processor + * + * @author davidc + */ +public class RepositoryTemplateProcessor extends FreeMarkerProcessor + implements TemplateProcessor, ApplicationContextAware, ApplicationListener +{ + private ProcessorLifecycle lifecycle = new ProcessorLifecycle(); + protected SearchPath searchPath; + protected String defaultEncoding; + protected Configuration templateConfig; + protected FreeMarkerProcessor freeMarkerProcessor; + private int updateDelay = 1; + + + /* (non-Javadoc) + * @see org.alfresco.repo.template.FreeMarkerProcessor#setDefaultEncoding(java.lang.String) + */ + @Override + public void setDefaultEncoding(String defaultEncoding) + { + this.defaultEncoding = defaultEncoding; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.TemplateProcessor#getDefaultEncoding() + */ + public String getDefaultEncoding() + { + return this.defaultEncoding; + } + + /** + * @param updateDelay the time in seconds between checks on the modified date for cached templates + */ + public void setUpdateDelay(int updateDelay) + { + this.updateDelay = updateDelay; + } + + /** + * @deprecated + * @param cacheSize not used anymore + */ + @Deprecated + public void setCacheSize(int cacheSize) + { + } + + /** + * @param searchPath SearchPath + */ + public void setSearchPath(SearchPath searchPath) + { + this.searchPath = searchPath; + } + + /** + * Set the freemarker processor + * + * @param freeMarkerProcessor the free marker processor + */ + public void setFreeMarkerProcessor(FreeMarkerProcessor freeMarkerProcessor) + { + this.freeMarkerProcessor = freeMarkerProcessor; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.template.FreeMarkerProcessor#getConfig() + */ + @Override + protected Configuration getConfig() + { + return templateConfig; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.TemplateProcessor#reset() + */ + public void reset() + { + if (templateConfig != null) + { + templateConfig.clearTemplateCache(); + } + initConfig(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.TemplateProcessor#hasTemplate(java.lang.String) + */ + public boolean hasTemplate(String templatePath) + { + boolean hasTemplate = false; + try + { + Template template = templateConfig.getTemplate(templatePath); + hasTemplate = (template != null); + } + catch(FileNotFoundException e) + { + // NOTE: return false as template is not found + } + catch(IOException e) + { + throw new WebScriptException("Failed to retrieve template " + templatePath, e); + } + return hasTemplate; + } + + /** + * Initialise FreeMarker Configuration + */ + protected void initConfig() + { + Configuration config = new Configuration(); + + // setup template cache + config.setCacheStorage(new StrongCacheStorage()); + config.setTemplateUpdateDelay(updateDelay); + + // setup template loaders + List loaders = new ArrayList(); + for (Store apiStore : searchPath.getStores()) + { + TemplateLoader loader = apiStore.getTemplateLoader(); + if (loader == null) + { + throw new WebScriptException("Unable to retrieve template loader for Web Script store " + apiStore.getBasePath()); + } + loaders.add(loader); + } + MultiTemplateLoader loader = new MultiTemplateLoader(loaders.toArray(new TemplateLoader[loaders.size()])); + config.setTemplateLoader(loader); + + // use our custom object wrapper that can deal with QNameMap objects directly + config.setObjectWrapper(new QNameAwareObjectWrapper()); + + // rethrow any exception so we can deal with them + config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + // turn off locale sensitive lookup - to save numerous wasted calls to nodeservice.exists() + config.setLocalizedLookup(false); + + // set template encoding + if (defaultEncoding != null) + { + config.setDefaultEncoding(defaultEncoding); + } + + // set output encoding + config.setOutputEncoding("UTF-8"); + config.setIncompatibleImprovements(new Version(2, 3, 20)); + config.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); + + templateConfig = config; + } + + /** + * Tempory fix to initialise this template processor with the freeMarker extensions expected by + * the templates. + */ + private void initProcessorExtensions() + { + for (ProcessorExtension processorExtension : this.freeMarkerProcessor.getProcessorExtensions()) + { + this.registerProcessorExtension(processorExtension); + } + } + + /* (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + lifecycle.setApplicationContext(applicationContext); + } + + /* (non-Javadoc) + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + public void onApplicationEvent(ApplicationEvent event) + { + lifecycle.onApplicationEvent(event); + } + + /** + * Hooks into Spring Application Lifecycle + */ + private class ProcessorLifecycle extends AbstractLifecycleBean + { + @Override + protected void onBootstrap(ApplicationEvent event) + { + initProcessorExtensions(); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryTemplateProcessorFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryTemplateProcessorFactory.java new file mode 100644 index 00000000000..5aeb0d3208d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/RepositoryTemplateProcessorFactory.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.springframework.extensions.webscripts.TemplateProcessor; +import org.springframework.extensions.webscripts.TemplateProcessorFactory; + +/** + * @author Kevin Roast + */ +public class RepositoryTemplateProcessorFactory implements TemplateProcessorFactory +{ + private TemplateProcessor templateProcessor; + + + /** + * @param templateProcessor the TemplateProcessor to set + */ + public void setTemplateProcessor(TemplateProcessor templateProcessor) + { + this.templateProcessor = templateProcessor; + } + + /* (non-Javadoc) + * @see org.springframework.extensions.webscripts.TemplateProcessorFactory#newInstance() + */ + public TemplateProcessor newInstance() + { + return templateProcessor; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/TempOutputStream.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TempOutputStream.java new file mode 100644 index 00000000000..e9f6df704e0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TempOutputStream.java @@ -0,0 +1,383 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; + +import org.alfresco.repo.content.ContentLimitViolationException; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * An output stream implementation that keeps the data in memory if is less then + * the specified memoryThreshold otherwise it writes it to a temp file. + *

+ * + * Close the stream before any call to + * {@link TempOutputStream}.getInputStream(). + *

+ * + * If deleteTempFileOnClose is false then use proper try-finally patterns + * to ensure that the temp file is destroyed after it is no longer needed. + * + *

+ *   try
+ *   {
+ *      StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), tempOutputStream);
+ *      tempOutputStream.close();
+ *   }
+ *   finally
+ *   {
+ *       tempOutputStream.destroy();
+ *   }
+ *   
+ * 
+ */ +public class TempOutputStream extends OutputStream +{ + private static final Log logger = LogFactory.getLog(TempOutputStream.class); + + private static final int DEFAULT_MEMORY_THRESHOLD = 4 * 1024 * 1024; // 4mb + private static final String ALGORITHM = "AES"; + private static final String MODE = "CTR"; + private static final String PADDING = "PKCS5Padding"; + private static final String TRANSFORMATION = ALGORITHM + '/' + MODE + '/' + PADDING; + private static final int KEY_SIZE = 128; + public static final String TEMP_FILE_PREFIX = "tempStreamFile-"; + + private final File tempDir; + private final int memoryThreshold; + private final long maxContentSize; + private boolean encrypt; + private boolean deleteTempFileOnClose; + + private long length = 0; + private OutputStream outputStream; + private File tempFile; + private TempByteArrayOutputStream tempStream; + + private Key symKey; + private byte[] iv; + + /** + * Creates a TempOutputStream. + * + * @param tempDir + * the temporary directory, i.e. isDir == true, that + * will be used as * parent directory for creating temp file backed + * streams + * @param memoryThreshold + * the memory threshold in B + * @param maxContentSize + * the max content size in B + * @param encrypt + * true if temp files should be encrypted + * @param deleteTempFileOnClose + * true if temp files should be deleted on output stream close + * (useful if we need to cache the content for further reads). If + * this is false then we need to make sure we call + * {@link TempOutputStream}.destroy to clean up properly. + */ + public TempOutputStream(File tempDir, int memoryThreshold, long maxContentSize, boolean encrypt, boolean deleteTempFileOnClose) + { + this.tempDir = tempDir; + this.memoryThreshold = (memoryThreshold < 0) ? DEFAULT_MEMORY_THRESHOLD : memoryThreshold; + this.maxContentSize = maxContentSize; + this.encrypt = encrypt; + this.deleteTempFileOnClose = deleteTempFileOnClose; + + this.tempStream = new TempByteArrayOutputStream(); + this.outputStream = this.tempStream; + } + + /** + * Returns the data as an InputStream + */ + public InputStream getInputStream() throws IOException + { + if (tempFile != null) + { + if (encrypt) + { + final Cipher cipher; + try + { + cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, symKey, new IvParameterSpec(iv)); + } + catch (Exception e) + { + destroy(); + + if (logger.isErrorEnabled()) + { + logger.error("Cannot initialize decryption cipher", e); + } + + throw new IOException("Cannot initialize decryption cipher", e); + } + + return new BufferedInputStream(new CipherInputStream(new FileInputStream(tempFile), cipher)); + } + return new BufferedInputStream(new FileInputStream(tempFile)); + } + else + { + return new ByteArrayInputStream(tempStream.getBuffer(), 0, tempStream.getCount()); + } + } + + @Override + public void write(int b) throws IOException + { + update(1); + outputStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException + { + update(len); + outputStream.write(b, off, len); + } + + @Override + public void flush() throws IOException + { + outputStream.flush(); + } + + @Override + public void close() throws IOException + { + close(deleteTempFileOnClose); + } + + /** + * Closes the stream and removes the backing file (if present). + *

+ * + * If deleteTempFileOnClose is false then use proper try-finally patterns + * to ensure that the temp file is destroyed after it is no longer needed. + * + *

+     *   try
+     *   {
+     *      StreamUtils.copy(new BufferedInputStream(new FileInputStream(file)), tempOutputStream);
+     *      tempOutputStream.close();
+     *   }
+     *   finally
+     *   {
+     *       tempOutputStream.destroy();
+     *   }
+     *   
+     * 
+ */ + public void destroy() throws IOException + { + close(true); + } + + public long getLength() + { + return length; + } + + private void closeOutputStream() + { + if (outputStream != null) + { + try + { + outputStream.flush(); + } + catch (IOException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Flushing the output stream failed", e); + } + } + + try + { + outputStream.close(); + } + catch (IOException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Closing the output stream failed", e); + } + } + } + } + + private void deleteTempFile() + { + if (tempFile != null) + { + try + { + boolean isDeleted = tempFile.delete(); + if (!isDeleted) + { + if (logger.isDebugEnabled()) + { + logger.debug("Temp file could not be deleted: " + tempFile.getAbsolutePath()); + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Deleted temp file: " + tempFile.getAbsolutePath()); + } + } + } + finally + { + tempFile = null; + } + } + } + + private void close(boolean deleteTempFileOnClose) + { + closeOutputStream(); + + if (deleteTempFileOnClose) + { + deleteTempFile(); + } + } + + private BufferedOutputStream createOutputStream(File file) throws IOException + { + BufferedOutputStream fileOutputStream; + if (encrypt) + { + try + { + // Generate a symmetric key + final KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM); + keyGen.init(KEY_SIZE); + symKey = keyGen.generateKey(); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, symKey); + + iv = cipher.getIV(); + + fileOutputStream = new BufferedOutputStream(new CipherOutputStream(new FileOutputStream(file), cipher)); + } + catch (Exception e) + { + if (logger.isErrorEnabled()) + { + logger.error("Cannot initialize encryption cipher", e); + } + + throw new IOException("Cannot initialize encryption cipher", e); + } + } + else + { + fileOutputStream = new BufferedOutputStream(new FileOutputStream(file)); + } + + return fileOutputStream; + } + + private void update(int len) throws IOException + { + if (maxContentSize > -1 && length + len > maxContentSize) + { + destroy(); + throw new ContentLimitViolationException("Content size violation, limit = " + maxContentSize); + } + + if (tempFile == null && (tempStream.getCount() + len) > memoryThreshold) + { + File file = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, ".bin", tempDir); + + BufferedOutputStream fileOutputStream = createOutputStream(file); + fileOutputStream.write(this.tempStream.getBuffer(), 0, this.tempStream.getCount()); + fileOutputStream.flush(); + + try + { + tempStream.close(); + } + catch (IOException e) + { + // Ignore exception + } + tempStream = null; + + tempFile = file; + outputStream = fileOutputStream; + } + + length += len; + } + + private static class TempByteArrayOutputStream extends ByteArrayOutputStream + { + /** + * @return The internal buffer where data is stored + */ + public byte[] getBuffer() + { + return buf; + } + + /** + * @return The number of valid bytes in the buffer. + */ + public int getCount() + { + return count; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/TempOutputStreamFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TempOutputStreamFactory.java new file mode 100644 index 00000000000..2300bb8b033 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TempOutputStreamFactory.java @@ -0,0 +1,105 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import java.io.File; + +/** + * Factory for {@link TempOutputStream} + */ +public class TempOutputStreamFactory +{ + /** + * A temporary directory, i.e. isDir == true, that will be used as + * parent directory for creating temp file backed streams. + */ + private final File tempDir; + private int memoryThreshold; + private long maxContentSize; + private boolean encrypt; + private boolean deleteTempFileOnClose; + + /** + * Creates a {@link TempOutputStream} factory. + * + * @param tempDir + * the temporary directory, i.e. isDir == true, that + * will be used as * parent directory for creating temp file backed + * streams + * @param memoryThreshold + * the memory threshold in B + * @param maxContentSize + * the max content size in B + * @param encrypt + * true if temp files should be encrypted + * @param deleteTempFileOnClose + * true if temp files should be deleted on output stream close + * (useful if we need to cache the content for further reads). If + * this is false then we need to make sure we call + * {@link TempOutputStream}.destroy to clean up properly. + */ + public TempOutputStreamFactory(File tempDir, int memoryThreshold, long maxContentSize, boolean encrypt, boolean deleteTempFileOnClose) + { + this.tempDir = tempDir; + this.memoryThreshold = memoryThreshold; + this.maxContentSize = maxContentSize; + this.encrypt = encrypt; + this.deleteTempFileOnClose = deleteTempFileOnClose; + } + + /** + * Creates a new {@link TempOutputStream} object + */ + public TempOutputStream createOutputStream() + { + return new TempOutputStream(tempDir, memoryThreshold, maxContentSize, encrypt, deleteTempFileOnClose); + } + + public File getTempDir() + { + return tempDir; + } + + public int getMemoryThreshold() + { + return memoryThreshold; + } + + public long getMaxContentSize() + { + return maxContentSize; + } + + public boolean isEncrypt() + { + return encrypt; + } + + public boolean isDeleteTempFileOnClose() + { + return deleteTempFileOnClose; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantRepositoryContainer.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantRepositoryContainer.java new file mode 100644 index 00000000000..82ca94a9cd0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantRepositoryContainer.java @@ -0,0 +1,130 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.alfresco.repo.cache.AsynchronouslyRefreshedCache; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantDeployer; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Registry; + +/** + * Tenant-aware Repository (server-tier) container for Web Scripts + * + * @author davidc + */ +public class TenantRepositoryContainer extends RepositoryContainer implements TenantDeployer +{ + // Logger + protected static final Log logger = LogFactory.getLog(TenantRepositoryContainer.class); + + /* Component Dependencies */ + protected TenantAdminService tenantAdminService; + protected TransactionService transactionService; + private AsynchronouslyRefreshedCache registryCache; + + /** + * @param registryCache asynchronously maintained cache for script registries + */ + public void setWebScriptsRegistryCache(AsynchronouslyRefreshedCache registryCache) + { + this.registryCache = registryCache; + } + + /** + * @param tenantAdminService service to sort out tenant context + */ + public void setTenantAdminService(TenantAdminService tenantAdminService) + { + this.tenantAdminService = tenantAdminService; + } + + /** + * @param transactionService service to give transactions when reading from the container + */ + public void setTransactionService(TransactionService transactionService) + { + super.setTransactionService(transactionService); + this.transactionService = transactionService; + } + + @Override + public Registry getRegistry() + { + Registry registry = registryCache.get(); + boolean isUpToDate = registryCache.isUpToDate(); + if (!isUpToDate && logger.isDebugEnabled()) + { + logger.debug("Retrieved out of date web script registry for tenant " + tenantAdminService.getCurrentUserDomain()); + } + return registry; + } + + @Override + public void onEnableTenant() + { + init(); + } + + @Override + public void onDisableTenant() + { + destroy(); + } + + @Override + public void init() + { + tenantAdminService.register(this); + registryCache.refresh(); + + super.reset(); + } + + @Override + public void destroy() + { + registryCache.refresh(); + } + + @Override + public void reset() + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + destroy(); + init(); + + return null; + } + }, true, false); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServlet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServlet.java new file mode 100644 index 00000000000..608f4fe6681 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServlet.java @@ -0,0 +1,110 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.servlet.WebScriptServlet; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + + +/** + * Entry point for web scripts which can accept a tenant id in their servlet path + * + * @author davidc + */ +public class TenantWebScriptServlet extends WebScriptServlet +{ +// public static final String SYSTEM_TENANT = "-system-"; +// public static final String DEFAULT_TENANT = "-default-"; + + private static final long serialVersionUID = 2954663814419046489L; + + // Logger + private static final Log logger = LogFactory.getLog(TenantWebScriptServlet.class); + + protected WebScriptServletRuntime getRuntime(HttpServletRequest req, HttpServletResponse res) + { + WebScriptServletRuntime runtime = new TenantWebScriptServletRuntime(container, authenticatorFactory, req, res, serverProperties); + return runtime; + } + + /* (non-Javadoc) + * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException + { + if (logger.isDebugEnabled()) + logger.debug("Processing tenant request (" + req.getMethod() + ") " + req.getRequestURL() + (req.getQueryString() != null ? "?" + req.getQueryString() : "")); + + if (req.getCharacterEncoding() == null) + { + req.setCharacterEncoding("UTF-8"); + } + + setLanguageFromRequestHeader(req); + + try + { + WebScriptServletRuntime runtime = getRuntime(req, res); + runtime.executeScript(); + } + catch (IllegalStateException e) + { + if(e.getMessage().contains("getOutputStream() has already been called for this response")) + { + if(logger.isDebugEnabled()) + { + logger.warn("Client has cut off communication", e); + } + else + { + logger.warn("Client has cut off communication"); + } + } + else + { + throw e; + } + } + finally + { + // clear threadlocal + I18NUtil.setLocale(null); + // clear authentication and tenant context + AuthenticationUtil.clearCurrentSecurityContext(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServletRequest.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServletRequest.java new file mode 100644 index 00000000000..6dcfcf2e2f7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServletRequest.java @@ -0,0 +1,117 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.extensions.config.ServerProperties; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.Runtime; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + + +/** + * Web Script Request which can handle a tenant id in their servlet path + * + * @author davidc + */ +public class TenantWebScriptServletRequest extends WebScriptServletRequest +{ + protected String tenant; + protected String pathInfo; + + protected void parse() + { + String realPathInfo = getRealPathInfo(); + + // remove tenant + int idx = realPathInfo.indexOf('/', 1); + tenant = realPathInfo.substring(1, idx == -1 ? realPathInfo.length() : idx); + pathInfo = realPathInfo.substring(tenant.length() + 1); + } + + /** + * Construction + * + * @param container Runtime + * @param req HttpServletRequest + * @param serviceMatch Match + * @param serverProperties ServerProperties + */ + public TenantWebScriptServletRequest(Runtime container, HttpServletRequest req, Match serviceMatch, ServerProperties serverProperties) + { + super(container, req, serviceMatch, serverProperties); + parse(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRequest#getServiceContextPath() + */ + public String getServiceContextPath() + { + return getHttpServletRequest().getContextPath() + getHttpServletRequest().getServletPath() + "/" + tenant; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRequest#getPathInfo() + */ + public String getPathInfo() + { + return pathInfo; + } + + public String getTenant() + { + return tenant; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRequest#getPathInfo() + */ + protected String getRealPathInfo() + { + // NOTE: Don't use req.getPathInfo() - it truncates the path at first semi-colon in Tomcat + final String requestURI = getHttpServletRequest().getRequestURI(); + final String serviceContextPath = getHttpServletRequest().getContextPath() + getHttpServletRequest().getServletPath(); + String pathInfo; + + if (serviceContextPath.length() > requestURI.length()) + { + // NOTE: assume a redirect has taken place e.g. tomcat welcome-page + // NOTE: this is unlikely, and we'll take the hit if the path contains a semi-colon + pathInfo = getHttpServletRequest().getPathInfo(); + } + else + { + pathInfo = URLDecoder.decode(requestURI.substring(serviceContextPath.length())); + } + + return pathInfo; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServletRuntime.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServletRuntime.java new file mode 100644 index 00000000000..4daad92b953 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/TenantWebScriptServletRuntime.java @@ -0,0 +1,108 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.extensions.config.ServerProperties; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.RuntimeContainer; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.servlet.ServletAuthenticatorFactory; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + + +/** + * HTTP Servlet Web Script Runtime which can handle a tenant id in a web script path + * + * @author davidc + */ +public class TenantWebScriptServletRuntime extends WebScriptServletRuntime +{ + public TenantWebScriptServletRuntime(RuntimeContainer container, ServletAuthenticatorFactory authFactory, HttpServletRequest req, HttpServletResponse res, ServerProperties serverProperties) + { + super(container, authFactory, req, res, serverProperties); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRuntime#getScriptUrl() + */ + @Override + protected String getScriptUrl() + { + // NOTE: Don't use req.getPathInfo() - it truncates the path at first semi-colon in Tomcat + final String requestURI = req.getRequestURI(); + final String serviceContextPath = req.getContextPath() + req.getServletPath(); + String pathInfo; + + if (serviceContextPath.length() > requestURI.length()) + { + // NOTE: assume a redirect has taken place e.g. tomcat welcome-page + // NOTE: this is unlikely, and we'll take the hit if the path contains a semi-colon + pathInfo = req.getPathInfo(); + } + else + { + pathInfo = URLDecoder.decode(requestURI.substring(serviceContextPath.length())); + } + + // ensure tenant is specified at beginning of path + // NOTE: must contain at least root / and single character for tenant name + if (pathInfo.length() < 2) + { + throw new WebScriptException("Missing tenant name in path: " + pathInfo); + } + // remove tenant + int idx = pathInfo.indexOf('/', 1); + pathInfo = pathInfo.substring(idx == -1 ? pathInfo.length() : idx); + return pathInfo; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRuntime#createRequest(org.alfresco.web.scripts.WebScriptMatch) + */ + @Override + protected WebScriptRequest createRequest(Match match) + { + // TODO: construct org.springframework.extensions.webscripts.servlet.WebScriptServletResponse when + // org.alfresco.web.scripts.WebScriptServletResponse (deprecated) is removed + servletReq = new TenantWebScriptServletRequest(this, req, match, serverProperties); + return servletReq; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptContainer#getName() + */ + public String getName() + { + return "TenantServletRuntime"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/WebScriptUtil.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/WebScriptUtil.java new file mode 100644 index 00000000000..aa73ba61cea --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/WebScriptUtil.java @@ -0,0 +1,184 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import org.alfresco.repo.nodelocator.CompanyHomeNodeLocator; +import org.alfresco.repo.nodelocator.NodeLocatorService; +import org.alfresco.repo.nodelocator.SharedHomeNodeLocator; +import org.alfresco.repo.nodelocator.SitesHomeNodeLocator; +import org.alfresco.repo.nodelocator.UserHomeNodeLocator; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.ISO8601DateFormat; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public class WebScriptUtil +{ + // General Keys + public static final String DATA_KEY = "data"; + + // NodeRef Keys + public static final String STORE_PROTOCOL = "store_protocol"; + public static final String STORE_ID = "store_id"; + public static final String NODE_ID = "node_id"; + + //Date/Calendar Keys + public static final String DATE_TIME= "dateTime"; + public static final String FORMAT= "format"; + public static final String TIME_ZONE= "timeZone"; + public static final String ISO8601 = "ISO8601"; + + public static String getContent(WebScriptRequest request) throws IOException + { + Content content = request.getContent(); + return content.getContent(); + } + + public static Map buildCalendarModel(Calendar calendar) + { + Map model = buildDateModel(calendar.getTime()); + model.put(TIME_ZONE, calendar.getTimeZone().getID()); + return model; + } + + public static Map buildDateModel(Date dateTime) + { + String dateStr = ISO8601DateFormat.format(dateTime); + Map model = new HashMap(); + model.put(DATE_TIME, dateStr); + model.put(FORMAT, ISO8601); + return model; + } + + public static Calendar getCalendar(JSONObject json) throws ParseException + { + Date date = getDate(json); + if(date == null) + { + return null; + } + Calendar calendar = Calendar.getInstance(); + String timeZone = json.optString(TIME_ZONE); + if(timeZone != null) + { + TimeZone zone = TimeZone.getTimeZone(timeZone); + calendar.setTimeZone(zone); + } + calendar.setTime(date); + return calendar; + } + + public static Date getDate(JSONObject json) throws ParseException + { + if(json == null) + { + return null; + } + String dateTime = json.optString(DATE_TIME); + if(dateTime == null) + { + return null; + } + String format = json.optString(FORMAT); + if(format!= null && ISO8601.equals(format) == false) + { + SimpleDateFormat dateFormat = new SimpleDateFormat(format); + return dateFormat.parse(dateTime); + } + return ISO8601DateFormat.parse(dateTime); + } + + + public static Map createBaseModel(Map result) + { + Map model = new HashMap(); + model.put(DATA_KEY, result); + return model; + } + + public static Map createBaseModel(List> results) + { + Map model = new HashMap(); + model.put(DATA_KEY, results); + return model; + } + + public static NodeRef getNodeRef(Map params) + { + String protocol = params.get(STORE_PROTOCOL); + String storeId= params.get(STORE_ID); + String nodeId= params.get(NODE_ID); + if(protocol == null || storeId == null || nodeId==null ) + { + return null; + } + return new NodeRef(protocol, storeId, nodeId); + } + + + public static NodeRef resolveNodeReference(String reference, NodeLocatorService nodeLocatorService) + { + NodeRef nodeRef = null; + switch (reference) + { + case "alfresco://company/home": + nodeRef = nodeLocatorService.getNode(CompanyHomeNodeLocator.NAME, null, null); + break; + case "alfresco://user/home": + nodeRef = nodeLocatorService.getNode(UserHomeNodeLocator.NAME, null, null); + break; + case "alfresco://company/shared": + nodeRef = nodeLocatorService.getNode(SharedHomeNodeLocator.NAME, null, null); + break; + case "alfresco://sites/home": + nodeRef = nodeLocatorService.getNode(SitesHomeNodeLocator.NAME, null, null); + break; + default: + nodeRef = new NodeRef(reference); + break; + } + + return nodeRef; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/AbstractActionWebscript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/AbstractActionWebscript.java new file mode 100644 index 00000000000..d62d44d2417 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/AbstractActionWebscript.java @@ -0,0 +1,160 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.Map; +import java.util.NoSuchElementException; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.ActionTrackingServiceImpl; +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ActionStatus; +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.repository.NodeService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public abstract class AbstractActionWebscript extends DeclarativeWebScript +{ + protected NodeService nodeService; + protected ActionService actionService; + protected RuntimeActionService runtimeActionService; + protected ActionTrackingService actionTrackingService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + public void setRuntimeActionService(RuntimeActionService runtimeActionService) + { + this.runtimeActionService = runtimeActionService; + } + + public void setActionTrackingService(ActionTrackingService actionTrackingService) + { + this.actionTrackingService = actionTrackingService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + RunningActionModelBuilder modelBuilder = new RunningActionModelBuilder( + nodeService, actionService, actionTrackingService + ); + return buildModel(modelBuilder, req, status, cache); + } + + protected abstract Map buildModel( + RunningActionModelBuilder modelBuilder, + WebScriptRequest req, + Status status, Cache cache + ); + + + /** + * Takes a running action ID, and returns an + * ExecutionSummary object for it. Note - doesn't + * check to see if the object exists in the + * cache though! + */ + public static ExecutionSummary getSummaryFromKey(String key) + { + return WrappedActionTrackingService.getSummaryFromKey(key); + } + + /** + * Returns the ExecutionSummary for the given action if it + * is currently executing, or null if it isn't + */ + public static ExecutionSummary getSummaryFromAction(Action action) + { + // Is it running? + if(action.getExecutionStatus() == ActionStatus.Running) { + return WrappedActionTrackingService.buildExecutionSummary(action); + } + // Has it been given a execution id? + // (eg has already finished, but this one was run) + if( ((ActionImpl)action).getExecutionInstance() != -1 ) { + return WrappedActionTrackingService.buildExecutionSummary(action); + } + // Not running, and hasn't run, we can't help + return null; + } + + /** + * Returns the running action ID for the given + * ExecutionSummary + */ + public static String getRunningId(ExecutionSummary summary) + { + return WrappedActionTrackingService.getRunningId(summary); + } + + /** + * So we can get at protected methods, which we need as + * we use the same naming scheme as the cache in the + * interests of simplicity. + */ + private static class WrappedActionTrackingService extends ActionTrackingServiceImpl + { + private static String getRunningId(ExecutionSummary summary) + { + return ActionTrackingServiceImpl.generateCacheKey(summary); + } + + protected static ExecutionSummary buildExecutionSummary(Action action) + { + return ActionTrackingServiceImpl.buildExecutionSummary(action); + } + + private static ExecutionSummary getSummaryFromKey(String key) + { + try { + // Try to have the key turned into a summary for us + return ActionTrackingServiceImpl.buildExecutionSummary(key); + } catch(NoSuchElementException e) { + // Wrong format + return null; + } + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/AbstractExecuteActionWebscript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/AbstractExecuteActionWebscript.java new file mode 100644 index 00000000000..227fea41948 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/AbstractExecuteActionWebscript.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.Map; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public abstract class AbstractExecuteActionWebscript extends AbstractActionWebscript +{ + protected Map buildModel( + RunningActionModelBuilder modelBuilder, + WebScriptRequest req, + Status status, Cache cache) + { + try { + // Have the action to run be identified + Action action = identifyAction(req, status, cache); + if(action == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Runnable Action found with the supplied details" + ); + } + + // Ask for it to be run in the background + // It will be available to execute once the webscript finishes + actionService.executeAction( + action, null, + false, true + ); + + // Return the details if we can + ExecutionSummary summary = getSummaryFromAction(action); + if(summary == null) { + throw new WebScriptException( + Status.STATUS_EXPECTATION_FAILED, + "Action failed to be added to the pending queue" + ); + } + + return modelBuilder.buildSimpleModel(summary); + } catch(Exception e) { + // Transaction broke + throw new RuntimeException(e); + } + } + + protected abstract Action identifyAction( + WebScriptRequest req, + Status status, Cache cache + ); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionDelete.java new file mode 100644 index 00000000000..cacbe6bd731 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionDelete.java @@ -0,0 +1,79 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.Map; + +import org.alfresco.service.cmr.action.ExecutionDetails; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public class RunningActionDelete extends AbstractActionWebscript +{ + @Override + protected Map buildModel( + RunningActionModelBuilder modelBuilder, WebScriptRequest req, + Status status, Cache cache) { + // Which action did they ask for? + String actionTrackingId = + req.getServiceMatch().getTemplateVars().get("action_tracking_id"); + + // Check it exists + ExecutionSummary action = + getSummaryFromKey(actionTrackingId); + if(action == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Running Action found with that tracking id" + ); + } + + ExecutionDetails details = + actionTrackingService.getExecutionDetails(action); + if(details == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Running Action found with that tracking id" + ); + } + + // Request the cancel + actionTrackingService.requestActionCancellation(action); + + // Report it as having been cancelled + status.setCode(Status.STATUS_NO_CONTENT); + status.setMessage("Action cancellation requested"); + status.setRedirect(true); + return null; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionGet.java new file mode 100644 index 00000000000..2c58011948e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionGet.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.Map; + +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public class RunningActionGet extends AbstractActionWebscript +{ + @Override + protected Map buildModel( + RunningActionModelBuilder modelBuilder, WebScriptRequest req, + Status status, Cache cache) { + // Which action did they ask for? + String actionTrackingId = + req.getServiceMatch().getTemplateVars().get("action_tracking_id"); + + ExecutionSummary action = + getSummaryFromKey(actionTrackingId); + + // Get the details, if we can + Map model = modelBuilder.buildSimpleModel(action); + + if(model == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Running Action found with that tracking id" + ); + } + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionModelBuilder.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionModelBuilder.java new file mode 100644 index 00000000000..b17813bb6ae --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionModelBuilder.java @@ -0,0 +1,145 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.ExecutionDetails; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.util.ISO8601DateFormat; + +/** + * Builds up models for running actions + * + * @author Nick Burch + * @since 3.4 + */ +public class RunningActionModelBuilder +{ + protected static final String MODEL_DATA_ITEM = "runningAction"; + protected static final String MODEL_DATA_LIST = "runningActions"; + + protected static final String ACTION_ID = "id"; + protected static final String ACTION_TYPE = "type"; + protected static final String ACTION_INSTANCE = "instance"; + protected static final String ACTION_NODE_REF = "nodeRef"; + protected static final String ACTION_STARTED_AT = "startedAt"; + protected static final String ACTION_RUNNING_ON = "runningOn"; + protected static final String ACTION_CANCEL_REQUESTED = "cancelRequested"; + protected static final String ACTION_KEY = "key"; + + + protected NodeService nodeService; + protected ActionService actionService; + protected ActionTrackingService actionTrackingService; + + public RunningActionModelBuilder(NodeService nodeService, ActionService actionService, + ActionTrackingService actionTrackingService) + { + this.nodeService = nodeService; + this.actionService = actionService; + this.actionTrackingService = actionTrackingService; + } + + + /** + * Build a model containing a single running action + */ + protected Map buildSimpleModel(ExecutionSummary summary) + { + Map ram = buildModel(summary); + if(ram != null) { + Map model = new HashMap(); + model.put(MODEL_DATA_ITEM, ram); + return model; + } + return null; + } + + /** + * Build a model containing a list of running actions for the given + * list of Running Actions + */ + protected Map buildSimpleList(List runningActions) + { + List> models = new ArrayList>(); + + for(ExecutionSummary summary : runningActions) { + Map ram = buildModel(summary); + if(ram != null) { + models.add(ram); + } + } + + // Finish up + Map model = new HashMap(); + model.put(MODEL_DATA_LIST, models); + return model; + } + + /** + * Build a model for a single action + */ + private Map buildModel(ExecutionSummary summary) + { + if(summary == null) { + return null; + } + + // Get the details, if we can + ExecutionDetails details = actionTrackingService.getExecutionDetails(summary); + + // Only record if still running - may have finished + // between getting the list and now + if(details != null) { + Map ram = new HashMap(); + ram.put(ACTION_ID, summary.getActionId()); + ram.put(ACTION_TYPE, summary.getActionType()); + ram.put(ACTION_INSTANCE, summary.getExecutionInstance()); + ram.put(ACTION_KEY, AbstractActionWebscript.getRunningId(summary)); + + ram.put(ACTION_NODE_REF, details.getPersistedActionRef()); + ram.put(ACTION_RUNNING_ON, details.getRunningOn()); + ram.put(ACTION_CANCEL_REQUESTED, details.isCancelRequested()); + + if(details.getStartedAt() != null) { + ram.put(ACTION_STARTED_AT, ISO8601DateFormat.format(details.getStartedAt())); + } else { + ram.put(ACTION_STARTED_AT, null); + } + + return ram; + } + + return null; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionsGet.java new file mode 100644 index 00000000000..28ae98ff52e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionsGet.java @@ -0,0 +1,67 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public class RunningActionsGet extends AbstractActionWebscript +{ + @Override + protected Map buildModel( + RunningActionModelBuilder modelBuilder, WebScriptRequest req, + Status status, Cache cache) { + List actions = null; + + // Do they want all actions, or only certain ones? + String type = req.getParameter("type"); + String nodeRef = req.getParameter("nodeRef"); + + if(type != null) { + actions = actionTrackingService.getExecutingActions(type); + } else if(nodeRef != null) { + NodeRef actionNodeRef = new NodeRef(nodeRef); + Action action = runtimeActionService.createAction(actionNodeRef); + actions = actionTrackingService.getExecutingActions(action); + } else { + actions = actionTrackingService.getAllExecutingActions(); + } + + // Build the model list + return modelBuilder.buildSimpleList(actions); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionsPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionsPost.java new file mode 100644 index 00000000000..445de8656c0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningActionsPost.java @@ -0,0 +1,79 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.io.IOException; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public class RunningActionsPost extends AbstractExecuteActionWebscript +{ + @Override + protected Action identifyAction(WebScriptRequest req, Status status, + Cache cache) { + // Which action did they ask for? + String nodeRef = req.getParameter("nodeRef"); + if(nodeRef == null) { + try { + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + if(! json.has("nodeRef")) { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not find required 'nodeRef' parameter"); + } + nodeRef = json.getString("nodeRef"); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from request.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from request.", je); + } + } + + // Does it exist in the repo? + NodeRef actionNodeRef = new NodeRef(nodeRef); + if(! nodeService.exists(actionNodeRef)) { + return null; + } + + // Load the specified action + Action action = runtimeActionService.createAction(actionNodeRef); + return action; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningReplicationActionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningReplicationActionsGet.java new file mode 100644 index 00000000000..c4d97fc6af6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningReplicationActionsGet.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.replication.ReplicationDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.alfresco.service.cmr.replication.ReplicationService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public class RunningReplicationActionsGet extends AbstractActionWebscript +{ + private ReplicationService replicationService; + + @Override + protected Map buildModel( + RunningActionModelBuilder modelBuilder, WebScriptRequest req, + Status status, Cache cache) { + List actions = null; + + // Do they want all replication actions, or only certain ones? + String name = req.getParameter("name"); + + if(name != null) { + // Try to find a replication definition with this name + ReplicationDefinition rd = replicationService.loadReplicationDefinition(name); + + // Look up what's running + if(rd != null) { + actions = actionTrackingService.getExecutingActions(rd); + } + } else { + // All replication actions + actions = actionTrackingService.getExecutingActions( + ReplicationDefinitionImpl.EXECUTOR_NAME + ); + } + + // Build the model list + return modelBuilder.buildSimpleList(actions); + } + + public void setReplicationService(ReplicationService replicationService) + { + this.replicationService = replicationService; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningReplicationActionsPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningReplicationActionsPost.java new file mode 100644 index 00000000000..2176f2d99dc --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/action/RunningReplicationActionsPost.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.action; + +import java.io.IOException; + +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.alfresco.service.cmr.replication.ReplicationService; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public class RunningReplicationActionsPost extends AbstractExecuteActionWebscript +{ + private ReplicationService replicationService; + + @Override + protected Action identifyAction(WebScriptRequest req, Status status, + Cache cache) { + // Which action did they ask for? + String name = req.getParameter("name"); + if(name == null) { + try { + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + if(! json.has("name")) { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not find required 'name' parameter"); + } + name = json.getString("name"); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from request.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from request.", je); + } + } + + // Load the specified replication definition + ReplicationDefinition replicationDefinition = + replicationService.loadReplicationDefinition(name); + return replicationDefinition; + } + + public void setReplicationService(ReplicationService replicationService) + { + this.replicationService = replicationService; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/TemplateWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/TemplateWebScript.java new file mode 100644 index 00000000000..2a2919daa60 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/TemplateWebScript.java @@ -0,0 +1,111 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.activities; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.SearchPath; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Java-backed WebScript to get a Template from a Template Store + */ +public class TemplateWebScript extends DeclarativeWebScript +{ + // Logger + protected static final Log logger = LogFactory.getLog(TemplateWebScript.class); + + private SearchPath searchPath; + + public void setSearchPath(SearchPath searchPath) + { + this.searchPath = searchPath; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Map model = new HashMap(); + + // process extension + String path = req.getExtensionPath(); // required + + if ((path == null) || (path.length() == 0)) + { + String msg = "Failed to getTemplate: missing {path}"; + logger.error(msg); + throw new AlfrescoRuntimeException(msg); + } + else + { + if (path.endsWith(".ftl")) + { + try + { + InputStream is = searchPath.getDocument(path); + if (is != null) + { + BufferedReader br = null; + try + { + br = new BufferedReader(new InputStreamReader(is)); + String line = null; + StringBuffer sb = new StringBuffer(); + while(((line = br.readLine()) !=null)) + { + sb.append(line); + } + + model.put("template", sb.toString()); + } + finally + { + if (br != null) { br.close(); }; + } + } + } + catch (IOException ioe) + { + logger.error("Failed to getTemplate: " + ioe); + } + } + } + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/TemplatesWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/TemplatesWebScript.java new file mode 100644 index 00000000000..3e04944cafa --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/TemplatesWebScript.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.activities; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.SearchPath; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.Store; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Java-backed WebScript to get list of Activity Templates from a Template Store + */ +public class TemplatesWebScript extends DeclarativeWebScript +{ + private SearchPath searchPath; + + public void setSearchPath(SearchPath searchPath) + { + this.searchPath = searchPath; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + String path = "/"; + String templatePattern = "*.ftl"; + + // process extension + String p = req.getExtensionPath(); // optional + + if ((p != null) && (p.length() > 0)) + { + int idx = p.lastIndexOf("/"); + if (idx != -1) + { + path = p.substring(0, idx); + templatePattern = p.substring(idx+1) + ".ftl"; + } + } + + Set templatePaths = new HashSet(); + for (Store apiStore : searchPath.getStores()) + { + try + { + for (String templatePath : apiStore.getDocumentPaths(path, false, templatePattern)) + { + templatePaths.add(templatePath); + } + } + catch (IOException e) + { + throw new WebScriptException("Failed to search for templates from store " + apiStore, e); + } + } + + Map model = new HashMap(); + model.put("paths", templatePaths); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java new file mode 100644 index 00000000000..7cc99d54ffc --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/feed/SiteFeedRetrieverWebScript.java @@ -0,0 +1,138 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.activities.feed; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.activities.feed.FeedTaskProcessor; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.util.JSONtoFmModel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Java-backed WebScript to retrieve Activity Site Feed + */ +public class SiteFeedRetrieverWebScript extends DeclarativeWebScript +{ + private static final Log logger = LogFactory.getLog(SiteFeedRetrieverWebScript.class); + + private ActivityService activityService; + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + // retrieve requested format + String format = req.getFormat(); + if (format == null || format.length() == 0) + { + format = getDescription().getDefaultFormat(); + } + + String extensionPath = req.getExtensionPath(); + String[] extParts = extensionPath == null ? new String[1] : extensionPath.split("/"); + + String siteId = null; + if (extParts.length == 1) + { + siteId = extParts[0]; + } + else + { + throw new AlfrescoRuntimeException("Unexpected extension: " + extensionPath); + } + + // map feed collection format to feed entry format (if not the same), eg. + // atomfeed -> atomentry + // atom -> atomentry + if (format.equals("atomfeed") || format.equals("atom")) + { + format = "atomentry"; + } + + Map model = new HashMap(); + + try + { + List feedEntries = activityService.getSiteFeedEntries(siteId); + + + if (format.equals(FeedTaskProcessor.FEED_FORMAT_JSON)) + { + model.put("feedEntries", feedEntries); + model.put("siteId", siteId); + } + else + { + List> activityFeedModels = new ArrayList>(); + try + { + for (String feedEntry : feedEntries) + { + activityFeedModels.add(JSONtoFmModel.convertJSONObjectToMap(feedEntry)); + } + } + catch (JSONException je) + { + throw new AlfrescoRuntimeException("Unable to get user feed entries: " + je.getMessage()); + } + + model.put("feedEntries", activityFeedModels); + model.put("siteId", siteId); + } + } + catch (AccessDeniedException ade) + { + // implies that site either does not exist or is private (and current user is not admin or a member) - hence return 401 (unauthorised) + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + status.setCode(Status.STATUS_UNAUTHORIZED); + logger.warn("Unable to get site feed entries for '" + siteId + "' (site does not exist or is private) - currently logged in as '" + currentUser +"'"); + + model.put("feedEntries", null); + model.put("siteId", ""); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java new file mode 100644 index 00000000000..8da6c68744b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/activities/feed/UserFeedRetrieverWebScript.java @@ -0,0 +1,217 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.activities.feed; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.activities.feed.FeedTaskProcessor; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; +import org.alfresco.util.JSONtoFmModel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Java-backed WebScript to retrieve Activity User Feed + */ +public class UserFeedRetrieverWebScript extends DeclarativeWebScript +{ + private static final Log logger = LogFactory.getLog(UserFeedRetrieverWebScript.class); + + // URL request parameter names + public static final String PARAM_SITE_ID = "s"; + public static final String PARAM_EXCLUDE_THIS_USER = "exclUser"; + public static final String PARAM_EXCLUDE_OTHER_USERS = "exclOthers"; + public static final String PARAM_ONLY_FOLLOWING = "following"; + public static final String PARAM_ACTIVITY_FILTER = "activityFilter"; + + private ActivityService activityService; + private SubscriptionService subscriptionService; + + private boolean userNamesAreCaseSensitive = false; + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setSubscriptionService(SubscriptionService subscriptionService) + { + this.subscriptionService = subscriptionService; + } + + public void setUserNamesAreCaseSensitive(boolean userNamesAreCaseSensitive) + { + this.userNamesAreCaseSensitive = userNamesAreCaseSensitive; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + // retrieve requested format + String format = req.getFormat(); + if (format == null || format.length() == 0) + { + format = getDescription().getDefaultFormat(); + } + + // process extension + String extensionPath = req.getExtensionPath(); + String[] extParts = extensionPath == null ? new String[1] : extensionPath.split("/"); + + String feedUserId = null; + if (extParts.length == 1) + { + feedUserId = extParts[0]; + } + else if (extParts.length > 1) + { + throw new AlfrescoRuntimeException("Unexpected extension: " + extensionPath); + } + + // process arguments + String siteId = req.getParameter(PARAM_SITE_ID); // optional + String exclThisUserStr = req.getParameter(PARAM_EXCLUDE_THIS_USER); // optional + String exclOtherUsersStr = req.getParameter(PARAM_EXCLUDE_OTHER_USERS); // optional + String onlyFollowingStr = req.getParameter(PARAM_ONLY_FOLLOWING); // optional + String activityFilterStr = req.getParameter(PARAM_ACTIVITY_FILTER); // optional + + boolean exclThisUser = false; + if ((exclThisUserStr != null) && (exclThisUserStr.equalsIgnoreCase("true") || exclThisUserStr.equalsIgnoreCase("t"))) + { + exclThisUser = true; + } + + boolean exclOtherUsers = false; + if ((exclOtherUsersStr != null) && (exclOtherUsersStr.equalsIgnoreCase("true") || exclOtherUsersStr.equalsIgnoreCase("t"))) + { + exclOtherUsers = true; + } + + Set userFilter = null; + if ((onlyFollowingStr != null) && (onlyFollowingStr.equalsIgnoreCase("true") || onlyFollowingStr.equalsIgnoreCase("t"))) + { + userFilter = new HashSet(); + if (subscriptionService.isActive()) { + PagingFollowingResults following = subscriptionService.getFollowing(AuthenticationUtil.getRunAsUser(), new PagingRequest(-1, null)); + if (following.getPage() != null) + { + for (String userName : following.getPage()) + { + userFilter.add(this.userNamesAreCaseSensitive ? userName : userName.toLowerCase()); + } + } + } + } + + Set activityFilter = null; + if (activityFilterStr != null) + { + activityFilter = new HashSet(); + String[] activities = activityFilterStr.split(","); + for (String s : activities) + { + if (s.trim().length() > 0) + { + activityFilter.add(s.trim()); + } + } + if (activityFilter.size() == 0) + { + activityFilter = null; + } + } + + if ((feedUserId == null) || (feedUserId.length() == 0)) + { + feedUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + } + + // map feed collection format to feed entry format (if not the same), eg. + // atomfeed -> atomentry + // atom -> atomentry + if (format.equals("atomfeed") || format.equals("atom")) + { + format = "atomentry"; + } + + Map model = new HashMap(); + + try + { + List feedEntries = activityService.getUserFeedEntries(feedUserId, siteId, exclThisUser, exclOtherUsers, userFilter, activityFilter); + + if (format.equals(FeedTaskProcessor.FEED_FORMAT_JSON)) + { + model.put("feedEntries", feedEntries); + model.put("siteId", siteId); + } + else + { + List> activityFeedModels = new ArrayList>(); + try + { + for (String feedEntry : feedEntries) + { + activityFeedModels.add(JSONtoFmModel.convertJSONObjectToMap(feedEntry)); + } + } + catch (JSONException je) + { + throw new AlfrescoRuntimeException("Unable to get user feed entries: " + je.getMessage()); + } + + model.put("feedEntries", activityFeedModels); + model.put("feedUserId", feedUserId); + } + } + catch (AccessDeniedException ade) + { + status.setCode(Status.STATUS_UNAUTHORIZED); + logger.warn("Unable to get user feed entries for '" + feedUserId + "' - currently logged in as '" + AuthenticationUtil.getFullyAuthenticatedUser() +"'"); + return null; + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/AbstractAdminWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/AbstractAdminWebScript.java new file mode 100644 index 00000000000..015a06a4b54 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/AbstractAdminWebScript.java @@ -0,0 +1,122 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.Collections; +import java.util.Map; + +import org.alfresco.service.cmr.admin.RepoAdminService; +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsageStatus.RepoUsageLevel; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.license.LicenseDescriptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.DeclarativeWebScript; + +/** + * Abstract implementation for scripts that access the {@link RepoAdminService}. + * + * @author Derek Hulley + * @since 3.4 + */ +public abstract class AbstractAdminWebScript extends DeclarativeWebScript +{ + public static final String JSON_KEY_LAST_UPDATE = "lastUpdate"; + public static final String JSON_KEY_USERS = "users"; + public static final String JSON_KEY_DOCUMENTS = "documents"; + public static final String JSON_KEY_LICENSE_MODE = "licenseMode"; + public static final String JSON_KEY_READ_ONLY = "readOnly"; + public static final String JSON_KEY_UPDATED = "updated"; + public static final String JSON_KEY_LICENSE_VALID_UNTIL = "licenseValidUntil"; + public static final String JSON_KEY_LICENSE_HOLDER = "licenseHolder"; + public static final String JSON_KEY_LEVEL = "level"; + public static final String JSON_KEY_WARNINGS = "warnings"; + public static final String JSON_KEY_ERRORS = "errors"; + + /** + * Logger that can be used by subclasses. + */ + protected final Log logger = LogFactory.getLog(this.getClass()); + + protected RepoAdminService repoAdminService; + protected DescriptorService descriptorService; + + /** + * @param repoAdminService the service that provides the functionality + */ + public void setRepoAdminService(RepoAdminService repoAdminService) + { + this.repoAdminService = repoAdminService; + } + + /** + * @param descriptorService the service that provides the functionality + */ + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + /** + * Return an I18N'd message for the given key or the key itself if not present + * + * @param args arguments to replace the variables in the message + */ + protected String getI18NMessage(String key, Object ... args) + { + return I18NUtil.getMessage(key, args); + } + + /** + * Helper to assign JSON return variables based on the repository usage data. + */ + protected void putUsageInModel( + Map model, + RepoUsage repoUsage, + boolean updated) + { + model.put(JSON_KEY_LAST_UPDATE, repoUsage.getLastUpdate()); + model.put(JSON_KEY_USERS, repoUsage.getUsers()); + model.put(JSON_KEY_DOCUMENTS, repoUsage.getDocuments()); + model.put(JSON_KEY_LICENSE_MODE, repoUsage.getLicenseMode()); + model.put(JSON_KEY_READ_ONLY, repoUsage.isReadOnly()); + model.put(JSON_KEY_LICENSE_VALID_UNTIL, repoUsage.getLicenseExpiryDate()); + model.put(JSON_KEY_UPDATED, updated); + + // Add license holder + LicenseDescriptor license = descriptorService.getLicenseDescriptor(); + if (license != null) + { + model.put(JSON_KEY_LICENSE_HOLDER, license.getHolderOrganisation()); + } + + model.put(JSON_KEY_LEVEL, RepoUsageLevel.OK.ordinal()); + model.put(JSON_KEY_WARNINGS, Collections.emptyList()); + model.put(JSON_KEY_ERRORS, Collections.emptyList()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/DynamicInterpreterExtension.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/DynamicInterpreterExtension.java new file mode 100644 index 00000000000..70916a417a4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/DynamicInterpreterExtension.java @@ -0,0 +1,121 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.admin; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.alfresco.repo.admin.BaseInterpreter; +import org.alfresco.repo.processor.BaseProcessorExtension; + +/** + * Console Interpeter script extension - dynamically binds to the configured BaseInterpreter instance. + * This avoids the need for a specific bean class per script interpreter. + * + * @see org.alfresco.repo.admin.BaseInterpreter + * See script beans configured in 'web-scripts-application-context.xml'. + * + * @author Kevin Roast + * @since 5.1 + */ +public class DynamicInterpreterExtension extends BaseProcessorExtension +{ + private BaseInterpreter interpreter; + private long duration; + private String result = ""; + private String command = ""; + + /** + * Set the BaseInterpreter to use when executing commands and retrieving the command result. + * + * @param interpreter For example, repoAdminInterpreter + */ + public void setInterpreter(BaseInterpreter interpreter) + { + this.interpreter = interpreter; + } + + private BaseInterpreter getInterpreter() + { + return this.interpreter; + } + + /** + * Script execute command gateway. + * + * @param command string to execute + */ + public void executeCmd(String command) + { + this.command = command; + this.interpretCommand(command); + } + + /** + * @return the command duration + */ + public long getDuration() + { + return this.duration; + } + + /** + * @return the command result + */ + public String getResult() + { + return this.result; + } + + /** + * @return the command last executed + */ + public String getCommand() + { + return this.command; + } + + /** + * Interpret console command using the configured Interpreter + * + * @param command command + */ + private void interpretCommand(String command) + { + try + { + long startms = System.currentTimeMillis(); + this.result = getInterpreter().interpretCommand(command); + this.duration = System.currentTimeMillis() - startms; + } + catch (Throwable e) + { + StringWriter stackTrace = new StringWriter(); + e.printStackTrace(new PrintWriter(stackTrace)); + this.result = stackTrace.toString(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/NodeBrowserPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/NodeBrowserPost.java new file mode 100644 index 00000000000..ca13383a9ff --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/NodeBrowserPost.java @@ -0,0 +1,1414 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.admin; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.GUID; +import org.alfresco.util.ISO9075; +import org.springframework.extensions.surf.util.URLEncoder; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * Admin Console NodeBrowser WebScript POST controller. + *

+ * Implements a low-level node browser client for the Admin Console tool. + * + * @author Kevin Roast + * @since 5.1 + */ +public class NodeBrowserPost extends DeclarativeWebScript implements Serializable +{ + private static final long serialVersionUID = 8464392337270665212L; + + // stores and node + transient private List stores = null; + + // supporting repository services + transient private TransactionService transactionService; + transient private NodeService nodeService; + transient private DictionaryService dictionaryService; + transient private SearchService searchService; + transient private NamespaceService namespaceService; + transient private PermissionService permissionService; + transient private OwnableService ownableService; + transient private LockService lockService; + transient private CheckOutCheckInService cociService; + + /** + * @param transactionService transaction service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + protected TransactionService getTransactionService() + { + return transactionService; + } + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + protected NodeService getNodeService() + { + return nodeService; + } + + /** + * @param searchService search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + protected SearchService getSearchService() + { + return searchService; + } + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + protected DictionaryService getDictionaryService() + { + return dictionaryService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + protected NamespaceService getNamespaceService() + { + return this.namespaceService; + } + + /** + * @param permissionService permission service + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + protected PermissionService getPermissionService() + { + return permissionService; + } + + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + protected OwnableService getOwnableService() + { + return ownableService; + } + + public void setLockService(LockService lockService) + { + this.lockService = lockService; + } + + protected LockService getLockService() + { + return this.lockService; + } + + public void setCheckOutCheckInService(CheckOutCheckInService cociService) + { + this.cociService = cociService; + } + + protected CheckOutCheckInService getCheckOutCheckInService() + { + return this.cociService; + } + + /** + * Gets the list of repository stores + * + * @return stores + */ + public List getStores() + { + if (stores == null) + { + stores = getNodeService().getStores(); + } + return stores; + } + + /** + * Gets the current node type + * + * @return node type + */ + public QName getNodeType(NodeRef nodeRef) + { + return getNodeService().getType(nodeRef); + } + + /** + * Gets the current node primary path + * + * @return primary path + */ + public String getPrimaryPath(NodeRef nodeRef) + { + Path primaryPath = getNodeService().getPath(nodeRef); + return ISO9075.decode(primaryPath.toString()); + } + + /** + * Gets the current node primary path + * + * @return primary path + */ + public String getPrimaryPrefixedPath(NodeRef nodeRef) + { + Path primaryPath = getNodeService().getPath(nodeRef); + return ISO9075.decode(primaryPath.toPrefixString(getNamespaceService())); + } + + /** + * Gets the current node primary parent reference + * + * @return primary parent ref + */ + public NodeRef getPrimaryParent(NodeRef nodeRef) + { + Path primaryPath = getNodeService().getPath(nodeRef); + Path.Element element = primaryPath.last(); + NodeRef parentRef = ((Path.ChildAssocElement) element).getRef().getParentRef(); + return parentRef; + } + + /** + * Gets the current node aspects + * + * @return node aspects + */ + public List getAspects(NodeRef nodeRef) + { + Set qnames = getNodeService().getAspects(nodeRef); + List aspects = new ArrayList(qnames.size()); + for (QName qname : qnames) + { + aspects.add(new Aspect(qname)); + } + return aspects; + } + + /** + * Gets the current node parents + * + * @return node parents + */ + public List getParents(NodeRef nodeRef) + { + List parents = getNodeService().getParentAssocs(nodeRef); + List assocs = new ArrayList(parents.size()); + for (ChildAssociationRef ref : parents) + { + assocs.add(new ChildAssociation(ref)); + } + return assocs; + } + + /** + * Gets the current node properties + * + * @return properties + */ + public List getProperties(NodeRef nodeRef) + { + Map propertyValues = getNodeService().getProperties(nodeRef); + List properties = new ArrayList(propertyValues.size()); + for (Map.Entry property : propertyValues.entrySet()) + { + properties.add(new Property(property.getKey(), property.getValue(), nodeRef)); + } + return properties; + } + + /** + * Gets whether the current node inherits its permissions from a parent node + * + * @return true => inherits permissions + */ + public boolean getInheritPermissions(NodeRef nodeRef) + { + Boolean inheritPermissions = this.getPermissionService().getInheritParentPermissions(nodeRef); + return inheritPermissions.booleanValue(); + } + + /** + * Gets the current node permissions + * + * @return the permissions + */ + public List getPermissions(NodeRef nodeRef) + { + List permissions = null; + AccessStatus readPermissions = this.getPermissionService().hasPermission(nodeRef, PermissionService.READ_PERMISSIONS); + if (readPermissions.equals(AccessStatus.ALLOWED)) + { + List nodePermissions = new ArrayList(); + for (Iterator iterator = getPermissionService().getAllSetPermissions(nodeRef).iterator(); iterator + .hasNext();) + { + AccessPermission ap = iterator.next(); + nodePermissions.add(new Permission(ap.getPermission(), ap.getAuthority(), ap.getAccessStatus().toString())); + } + permissions = nodePermissions; + } + else + { + List noReadPermissions = new ArrayList(1); + noReadPermissions.add(new NoReadPermissionGranted()); + permissions = noReadPermissions; + } + return permissions; + } + + /** + * Gets the current node permissions + * + * @return the permissions + */ + public List getStorePermissionMasks(NodeRef nodeRef) + { + List permissionMasks = new ArrayList(1); + permissionMasks.add(new NoStoreMask()); + return permissionMasks; + } + + /** + * Gets the current node children + * + * @return node children + */ + public List getChildren(NodeRef nodeRef) + { + List refs = getNodeService().getChildAssocs(nodeRef); + List assocs = new ArrayList(refs.size()); + for (ChildAssociationRef ref : refs) + { + assocs.add(new ChildAssociation(ref)); + } + return assocs; + } + + /** + * Gets the current node associations + * + * @return associations + */ + public List getAssocs(NodeRef nodeRef) + { + List refs = null; + try + { + refs = getNodeService().getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL); + } + catch (UnsupportedOperationException err) + { + // some stores do not support associations + // but we doesn't want NPE in code below + refs = new ArrayList(); + } + List assocs = new ArrayList(refs.size()); + for (AssociationRef ref : refs) + { + assocs.add(new PeerAssociation(ref.getTypeQName(), ref.getSourceRef(), ref.getTargetRef())); + } + return assocs; + } + + /** + * Gets the current source associations + * + * @return associations + */ + public List getSourceAssocs(NodeRef nodeRef) + { + List refs = null; + try + { + refs = getNodeService().getSourceAssocs(nodeRef, RegexQNamePattern.MATCH_ALL); + } + catch (UnsupportedOperationException err) + { + // some stores do not support associations + // but we doesn't want NPE in code below + refs = new ArrayList(); + } + List assocs = new ArrayList(refs.size()); + for (AssociationRef ref : refs) + { + assocs.add(new PeerAssociation(ref.getTypeQName(), ref.getSourceRef(), ref.getTargetRef())); + } + return assocs; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map result = new HashMap<>(16); + + // gather inputs + Map returnParams = new HashMap<>(16); + String store = req.getParameter("nodebrowser-store"); + String searcher = req.getParameter("nodebrowser-search"); + String query = req.getParameter("nodebrowser-query"); + String maxResults = req.getParameter("nodebrowser-query-maxresults"); + String skipCount = req.getParameter("nodebrowser-query-skipcount"); + String error = null; + + StoreRef storeRef = new StoreRef(store); + + // always a list of assoc refs from some result + List assocRefs = Collections.emptyList(); + NodeRef currentNode = null; + + // what action should be processed? + long timeStart = System.currentTimeMillis(); + String actionValue = req.getParameter("nodebrowser-action-value"); + String action = req.getParameter("nodebrowser-action"); + final String execute = req.getParameter("nodebrowser-execute"); + final String executeValue = req.getParameter("nodebrowser-execute-value"); + String message = null; + try + { + // 'execute' is an action that performs an operation on a node e.g. delete + // the 'executeValue' param provides context + // this is done before the view action to ensure node state is correct + if (execute != null) + { + switch (execute) + { + case "delete": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // delete the node using the standard NodeService + nodeService.deleteNode(new NodeRef(executeValue)); + return null; + } + }, false, true); + message = "nodebrowser.message.delete"; + break; + } + case "fdelete": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // delete the node - but ensure that it is not archived + NodeRef ref = new NodeRef(executeValue); + nodeService.addAspect(ref, ContentModel.ASPECT_TEMPORARY, null); + nodeService.deleteNode(ref); + return null; + } + }, false, true); + message = "nodebrowser.message.delete"; + break; + } + case "restore": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + nodeService.restoreNode(new NodeRef(executeValue), null, null, null); + return null; + } + }, false, true); + message = "nodebrowser.message.restore"; + break; + } + case "take-ownership": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + ownableService.takeOwnership(new NodeRef(executeValue)); + return null; + } + }, false, true); + message = "nodebrowser.message.take-ownership"; + break; + } + case "delete-permissions": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + NodeRef ref = new NodeRef(executeValue); + permissionService.deletePermissions(ref); + permissionService.setInheritParentPermissions(ref, true); + return null; + } + }, false, true); + message = "nodebrowser.message.delete-permissions"; + break; + } + case "delete-property": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // argument value contains "NodeRef|QName" packed string + String[] parts = executeValue.split("\\|"); + nodeService.removeProperty(new NodeRef(parts[0]), QName.createQName(parts[1])); + return null; + } + }, false, true); + message = "nodebrowser.message.delete-property"; + break; + } + case "unlock": + { + transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + NodeRef ref = new NodeRef(executeValue); + if (cociService.isCheckedOut(ref)) + { + NodeRef wcRef = cociService.getWorkingCopy(ref); + if (wcRef != null) + { + cociService.cancelCheckout(wcRef); + } + } + else + { + lockService.unlock(ref); + } + return null; + } + }, false, true); + message = "nodebrowser.message.unlock"; + break; + } + } + } + + // 'action' is a view action that request an update of the admin console view state e.g. 'search' or 'children' + // the 'actionValue' param provides context as may other parameters such as 'query' + switch (action) + { + // on Execute btn press and query present, perform search + case "search": + { + if (query != null && query.trim().length() != 0) + { + switch (searcher) + { + case "noderef": + { + // ensure node exists - or throw error + NodeRef nodeRef = new NodeRef(query); + boolean exists = getNodeService().exists(nodeRef); + if (!exists) + { + throw new AlfrescoRuntimeException("Node " + nodeRef + " does not exist."); + } + currentNode = nodeRef; + // this is not really a search for results, it is a direct node reference + // so gather the child assocs as usual and update the action value for the UI location + assocRefs = getNodeService().getChildAssocs(currentNode); + actionValue = query; + action = "parent"; + break; + } + case "selectnodes": + { + List nodes = getSearchService().selectNodes( + getNodeService().getRootNode(storeRef), query, null, getNamespaceService(), false); + assocRefs = new ArrayList<>(nodes.size()); + for (NodeRef node: nodes) + { + assocRefs.add(getNodeService().getPrimaryParent(node)); + } + break; + } + default: + { + // perform search + SearchParameters params = new SearchParameters(); + params.setQuery(query); + params.addStore(storeRef); + params.setLanguage(searcher); + if (maxResults != null && maxResults.length() != 0) + { + params.setMaxItems(Integer.parseInt(maxResults)); + params.setLimit(Integer.parseInt(maxResults)); + } + if (skipCount != null && skipCount.length() != 0) + { + params.setSkipCount(Integer.parseInt(skipCount)); + } + ResultSet rs = getSearchService().query(params); + assocRefs = rs.getChildAssocRefs(); + break; + } + } + } + break; + } + case "root": + { + // iterate the properties and children of a store root node + currentNode = getNodeService().getRootNode(storeRef); + assocRefs = getNodeService().getChildAssocs(currentNode); + break; + } + case "parent": + case "children": + { + currentNode = new NodeRef(actionValue); + assocRefs = getNodeService().getChildAssocs(currentNode); + break; + } + } + + // get the required information from the assocRefs list and wrap objects + List wrappers = new ArrayList<>(assocRefs.size()); + for (ChildAssociationRef ref : assocRefs) + { + wrappers.add(new ChildAssocRefWrapper(ref)); + } + result.put("children", wrappers); + } + catch (Throwable e) + { + // empty child list on error - current node will still be null + result.put("children", new ArrayList<>(0)); + error = e.getMessage(); + } + + // current node info if any + if (currentNode != null) + { + // node info + Map info = new HashMap<>(8); + info.put("nodeRef", currentNode.toString()); + info.put("path", getNodeService().getPath(currentNode).toPrefixString(getNamespaceService())); + info.put("type", getNodeService().getType(currentNode).toPrefixString(getNamespaceService())); + ChildAssociationRef parent = getNodeService().getPrimaryParent(currentNode); + info.put("parent", parent.getParentRef() != null ? parent.getParentRef().toString() : ""); + result.put("info", info); + + // node properties + result.put("properties", getProperties(currentNode)); + + // parents + List parents = getNodeService().getParentAssocs(currentNode); + List assocs = new ArrayList(parents.size()); + for (ChildAssociationRef ref : parents) + { + assocs.add(new ChildAssociation(ref)); + } + result.put("parents", assocs); + + // aspects + List aspects = getAspects(currentNode); + result.put("aspects", aspects); + + // target assocs + List targetAssocs = getAssocs(currentNode); + result.put("assocs", targetAssocs); + + // source assocs + List sourceAssocs = getSourceAssocs(currentNode); + result.put("sourceAssocs", sourceAssocs); + + // permissions + Map permissionInfo = new HashMap(); + permissionInfo.put("entries", getPermissions(currentNode)); + permissionInfo.put("owner", getOwnableService().getOwner(currentNode)); + permissionInfo.put("inherit", getInheritPermissions(currentNode)); + result.put("permissions", permissionInfo); + } + + // store result in session for the resulting GET request webscript + final String resultId = GUID.generate(); + HttpServletRequest request = ((WebScriptServletRequest)req).getHttpServletRequest(); + HttpSession session = request.getSession(); + session.putValue(resultId, result); + + // return params + returnParams.put("resultId", resultId); + returnParams.put("action", action); + returnParams.put("actionValue", actionValue); + returnParams.put("query", query); + returnParams.put("store", store); + returnParams.put("searcher", searcher); + returnParams.put("maxResults", maxResults); + returnParams.put("skipCount", skipCount); + returnParams.put("in", Long.toString(System.currentTimeMillis()-timeStart)); + returnParams.put("e", error); + returnParams.put("m", message); + + // redirect as all admin console pages do (follow standard pattern) + // The logic to generate the navigation section and server meta-data is all tied into alfresco-common.lib.js + // which is great for writing JS based JMX surfaced pages, but not so great for Java backed WebScripts. + status.setCode(301); + status.setRedirect(true); + status.setLocation(buildUrl(req, returnParams, execute != null && execute.length() != 0 ? execute : action)); + + return null; + } + + private static String buildUrl(WebScriptRequest req, Map params, String hash) + { + StringBuilder url = new StringBuilder(256); + + url.append(req.getServicePath()); + if (!params.isEmpty()) + { + boolean first = true; + for (String key: params.keySet()) + { + String val = params.get(key); + if (val != null && val.length() != 0) + { + url.append(first ? '?' : '&'); + url.append(key); + url.append('='); + url.append(URLEncoder.encode(val)); + first = false; + } + } + } + if (hash != null && hash.length() != 0) + { + url.append('#').append(hash); + } + + return url.toString(); + } + + /** + * Node wrapper class + */ + public class Node implements Serializable + { + private static final long serialVersionUID = 12608347204513848L; + + private String qnamePath; + + private String prefixedQNamePath; + + private NodeRef nodeRef; + + private NodeRef parentNodeRef; + + private QNameBean childAssoc; + + private QNameBean type; + + public Node(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + Path path = getNodeService().getPath(nodeRef); + this.qnamePath = path.toString(); + this.prefixedQNamePath = path.toPrefixString(getNamespaceService()); + this.parentNodeRef = getPrimaryParent(nodeRef); + ChildAssociationRef ref = getNodeService().getPrimaryParent(nodeRef); + this.childAssoc = ref.getQName() != null ? new QNameBean(ref.getQName()) : null; + this.type = new QNameBean(getNodeService().getType(nodeRef)); + } + + public String getQnamePath() + { + return qnamePath; + } + + public String getPrefixedQNamePath() + { + return prefixedQNamePath; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public String getId() + { + return nodeRef.getId(); + } + + public String getName() + { + return childAssoc != null ? childAssoc.getName() : ""; + } + + public String getPrefixedName() + { + return childAssoc != null ? childAssoc.getPrefixedName() : ""; + } + + public QNameBean getType() + { + return type; + } + + public void setNodeRef(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + + public NodeRef getParentNodeRef() + { + return parentNodeRef; + } + + public void setParentNodeRef(NodeRef parentNodeRef) + { + this.parentNodeRef = parentNodeRef; + } + } + + /** + * Qname wrapper class + */ + public class QNameBean implements Serializable + { + private static final long serialVersionUID = 6982292337846270774L; + + protected QName name; + private String prefixString = null; + + public QNameBean(QName name) + { + this.name = name; + } + + public String getName() + { + return name.toString(); + } + + public String getPrefixedName() + { + try + { + return prefixString != null ? prefixString : (prefixString = name.toPrefixString(getNamespaceService())); + } + catch(NamespaceException e) + { + return name.getLocalName(); + } + } + + public String toString() + { + return getName(); + } + } + + /** + * Aspect wrapper class + */ + public class Aspect extends QNameBean implements Serializable + { + private static final long serialVersionUID = -6448182941386934326L; + + public Aspect(QName name) + { + super(name); + } + } + + /** + * Association wrapper class + */ + public class Association implements Serializable + { + private static final long serialVersionUID = 1078430803027004L; + + protected QNameBean name; + protected QNameBean typeName; + + public Association(QName name, QName typeName) + { + this.name = name != null ? new QNameBean(name) : null; + this.typeName = new QNameBean(typeName); + } + + public QNameBean getName() + { + return name; + } + + public QNameBean getTypeName() + { + return typeName; + } + } + + /** + * Child assoc wrapper class + */ + public class ChildAssociation extends Association implements Serializable + { + private static final long serialVersionUID = -52439282250891063L; + + protected NodeRef childRef; + protected NodeRef parentRef; + protected QNameBean childType; + protected QNameBean parentType; + protected boolean primary; + + // from Association + protected QNameBean name; + protected QNameBean typeName; + + public ChildAssociation(ChildAssociationRef ref) + { + super(ref.getQName() != null ? ref.getQName() : null, + ref.getTypeQName() != null ? ref.getTypeQName() : null); + + this.childRef = ref.getChildRef(); + this.parentRef = ref.getParentRef(); // could be null + if (childRef != null) + this.childType = new QNameBean(getNodeType(childRef)); + if (parentRef != null) + this.parentType = new QNameBean(getNodeType(parentRef)); + this.primary = ref.isPrimary(); + } + + public NodeRef getChildRef() + { + return childRef; + } + + public QNameBean getChildTypeName() + { + return childType; + } + + public NodeRef getParentRef() + { + return parentRef; + } + + public QNameBean getParentTypeName() + { + return parentType; + } + + public boolean isPrimary() + { + return primary; + } + + public boolean getPrimary() + { + return this.isPrimary(); + } + } + + /** + * Wrapper to resolve Assoc Type and QName to short form with resolved prefix + */ + public class ChildAssocRefWrapper implements Serializable + { + private static final long serialVersionUID = 4321292337846270665L; + + final private ChildAssociationRef ref; + private String qname = null; + private String typeqname = null; + + public ChildAssocRefWrapper(ChildAssociationRef r) + { + ref = r; + } + + public String getTypeQName() + { + return typeqname != null ? typeqname : ( + typeqname = ref.getTypeQName() != null ? ref.getTypeQName().toPrefixString(getNamespaceService()) : ""); + } + + public String getQName() + { + return qname != null ? qname : ( + qname = ref.getQName() != null ? ref.getQName().toPrefixString(getNamespaceService()) : ""); + } + + public NodeRef getChildRef() + { + return ref.getChildRef(); + } + + public NodeRef getParentRef() + { + return ref.getParentRef(); + } + + public boolean isPrimary() + { + return ref.isPrimary(); + } + + public boolean isChildLocked() + { + return lockService != null && lockService.getLockType(ref.getChildRef()) != null; + } + } + + /** + * Peer assoc wrapper class + */ + public class PeerAssociation extends Association implements Serializable + { + private static final long serialVersionUID = 4833278311416507L; + + protected NodeRef sourceRef; + protected NodeRef targetRef; + protected QNameBean sourceType; + protected QNameBean targetType; + + // from Association + protected QNameBean name; + protected QNameBean typeName; + + public PeerAssociation(QName typeName, NodeRef sourceRef, NodeRef targetRef) + { + super(null, typeName); + + this.sourceRef = sourceRef; + this.targetRef = targetRef; + if (sourceRef != null) + this.sourceType = new QNameBean(getNodeType(sourceRef)); + if (targetRef != null) + this.targetType = new QNameBean(getNodeType(targetRef)); + } + + public NodeRef getSourceRef() + { + return sourceRef; + } + + public QNameBean getSourceTypeName() + { + return sourceType; + } + + public NodeRef getTargetRef() + { + return targetRef; + } + + public QNameBean getTargetTypeName() + { + return targetType; + } + } + + /** + * Property wrapper class + */ + public class Property implements Serializable + { + private static final long serialVersionUID = 7755924782250077L; + + private QNameBean name; + + private boolean isCollection = false; + + private List values; + + private boolean residual; + + private QNameBean typeName; + + /** + * Construct + * + * @param qname property name + * @param value property values + */ + @SuppressWarnings("unchecked") + public Property(QName qname, Serializable value, NodeRef nodeRef) + { + this.name = new QNameBean(qname); + + residual = true; + + PropertyDefinition propDef = getDictionaryService().getProperty(qname); + if (propDef != null) + { + QName qn = propDef.getDataType().getName(); + typeName = qn != null ? new QNameBean(propDef.getDataType().getName()) : null; + + // ALF-21950 We need to check if the property belongs to the type of the node or to their ancestors + if(propDef.getContainerClass().isAspect()) + { + residual = false; + } + else + { + ClassDefinition classDef = getDictionaryService().getClass(getNodeService().getType(nodeRef)); + boolean found = false; + + while(classDef != null) + { + found = searchInClassDefinition(qname, classDef); + if(found) + { + break; + } + classDef = classDef.getParentClassDefinition(); + } + residual = !found; + } + } + + // handle multi/single values + final List values; + if (value instanceof Collection) + { + Collection oldValues = (Collection) value; + values = new ArrayList(oldValues.size()); + isCollection = true; + for (Serializable multiValue : oldValues) + { + values.add(new Value(multiValue instanceof QName ? new QNameBean((QName) multiValue) : multiValue)); + } + } + else + { + values = Collections.singletonList(new Value(value instanceof QName ? new QNameBean((QName) value) : value)); + } + this.values = values; + } + + private boolean searchInClassDefinition(QName qname, ClassDefinition classDef) + { + if (classDef != null) + { + for (QName definedPropQName : classDef.getProperties().keySet()) + { + if(qname.isMatch(definedPropQName)) + { + return true; + } + } + } + return false; + } + + /** + * Gets the property name + * + * @return name + */ + public QNameBean getName() + { + return name; + } + + public QNameBean getTypeName() + { + return typeName; + } + + /** + * Gets the prefixed property name + * + * @return prefixed name + */ + public String getPrefixedName() + { + return name.getPrefixedName(); + } + + /** + * Gets the property value + * + * @return value + */ + public List getValues() + { + return values; + } + + /** + * Determines whether the property is residual + * + * @return true => property is not defined in dictionary + */ + public boolean getResidual() + { + return residual; + } + + /** + * Determines whether the property is of ANY type + * + * @return true => is any + */ + public boolean isAny() + { + return (getTypeName() == null) ? false : getTypeName().getName().equals(DataTypeDefinition.ANY.toString()); + } + + /** + * Determines whether the property is a collection + * + * @return true => is collection + */ + public boolean isCollection() + { + return isCollection; + } + + /** + * Value wrapper + */ + public class Value implements Serializable + { + private static final long serialVersionUID = 47235536691732705L; + + private Serializable value; + + /** + * Construct + * + * @param value value + */ + public Value(Serializable value) + { + this.value = value; + } + + /** + * Gets the value + * + * @return the value + */ + public Serializable getValue() + { + return value; + } + + /** + * Gets the value datatype + * + * @return the value datatype + */ + public String getDataType() + { + String datatype = null; + if (Property.this.getTypeName() != null) + { + datatype = Property.this.getTypeName().getName(); + } + if (datatype == null || datatype.equals(DataTypeDefinition.ANY.toString())) + { + if (value != null) + { + DataTypeDefinition dataTypeDefinition = getDictionaryService().getDataType(value.getClass()); + if (dataTypeDefinition != null) + { + datatype = getDictionaryService().getDataType(value.getClass()).getName().toString(); + } + } + } + return datatype; + } + + /** + * Determines whether the value is content + * + * @return true => is content + */ + public boolean isContent() + { + String datatype = getDataType(); + return (datatype == null) ? false : datatype.equals(DataTypeDefinition.CONTENT.toString()); + } + + /** + * Determines whether the value is a node ref + * + * @return true => is node ref + */ + public boolean isNodeRef() + { + String datatype = getDataType(); + return (datatype == null) ? false : datatype.equals(DataTypeDefinition.NODE_REF.toString()) || datatype.equals(DataTypeDefinition.CATEGORY.toString()); + } + + /** + * Determines whether the value is null + * + * @return true => value is null + */ + public boolean isNullValue() + { + return value == null; + } + } + } + + /** + * Permission bean + */ + public static class Permission implements Serializable + { + private static final long serialVersionUID = 1235536691732705L; + + private final String permission; + private final String authority; + private final String accessStatus; + + public Permission(String permission, String authority, String accessStatus) + { + this.permission = permission; + this.authority = authority; + this.accessStatus = accessStatus; + } + + public String getPermission() + { + return permission; + } + + public String getAuthority() + { + return authority; + } + + public String getAccessStatus() + { + return accessStatus; + } + } + + /** + * Permission representing the fact that "Read Permissions" has not been granted + */ + public static class NoReadPermissionGranted extends Permission implements Serializable + { + private static final long serialVersionUID = 1236786691732705L; + + public NoReadPermissionGranted() + { + super(PermissionService.READ_PERMISSIONS, "[Current Authority]", "Not Granted"); + } + } + + public static class NoStoreMask extends Permission implements Serializable + { + private static final long serialVersionUID = 3125536691732705L; + + public NoStoreMask() + { + super("All ", "All", "Allowed"); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoRestrictionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoRestrictionsGet.java new file mode 100644 index 00000000000..5a339f821e2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoRestrictionsGet.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.admin.RepoUsage; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET the repository {@link RepoUsage restrictions}. + * + * @author Derek Hulley + * @since 3.4 + */ +public class RepoRestrictionsGet extends AbstractAdminWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + RepoUsage restrictions = repoAdminService.getRestrictions(); + putUsageInModel(model, restrictions, false); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoUsageGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoUsageGet.java new file mode 100644 index 00000000000..64dd0241aaf --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoUsageGet.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsageStatus; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET the repository {@link RepoUsage usage}. + * + * @author Derek Hulley + * @since 3.4 + */ +public class RepoUsageGet extends AbstractAdminWebScript +{ + @Override + protected Map executeImpl(final WebScriptRequest req, final Status status, final Cache cache) + { + // Runas system to obtain the info + RunAsWork> runAs = new RunAsWork>() + { + @Override + public Map doWork() throws Exception + { + Map model = new HashMap(7); + + RepoUsageStatus usageStatus = repoAdminService.getUsageStatus(); + RepoUsage usage = usageStatus.getUsage(); + + putUsageInModel( + model, + usage, + false); + + // Add usage messages + model.put(JSON_KEY_LEVEL, usageStatus.getLevel().ordinal()); + model.put(JSON_KEY_WARNINGS, usageStatus.getWarnings()); + model.put(JSON_KEY_ERRORS, usageStatus.getErrors()); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } + }; + return AuthenticationUtil.runAs(runAs, AuthenticationUtil.getSystemUserName()); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoUsagePost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoUsagePost.java new file mode 100644 index 00000000000..b9a134de622 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/admin/RepoUsagePost.java @@ -0,0 +1,62 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.admin.RepoUsage; +import org.alfresco.service.cmr.admin.RepoUsage.UsageType; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET the repository {@link RepoUsage usage}. + * + * @author Derek Hulley + * @since 3.4 + */ +public class RepoUsagePost extends AbstractAdminWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + boolean updated = repoAdminService.updateUsage(UsageType.USAGE_ALL); + RepoUsage repoUsage = repoAdminService.getUsage(); + + putUsageInModel(model, repoUsage, updated); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/AbstractArchivedNodeWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/AbstractArchivedNodeWebScript.java new file mode 100644 index 00000000000..c294dadbf3b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/AbstractArchivedNodeWebScript.java @@ -0,0 +1,201 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.archive; + +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.query.PagingResults; +import org.alfresco.repo.node.archive.ArchivedNodesCannedQueryBuilder; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.ScriptPagingDetails; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is an abstract base class for the various webscript controllers in the + * NodeArchiveService. + * + * @author Neil McErlean, Jamal Kaabi-Mofrad + * @since 3.5 + */ +public abstract class AbstractArchivedNodeWebScript extends DeclarativeWebScript +{ + public static final String NAME = "name"; + public static final String TITLE = "title"; + public static final String DESCRIPTION = "description"; + public static final String NODEREF = "nodeRef"; + public static final String ARCHIVED_BY = "archivedBy"; + public static final String ARCHIVED_DATE = "archivedDate"; + public static final String DISPLAY_PATH = "displayPath"; + public static final String USER_NAME = "userName"; + public static final String FIRST_NAME = "firstName"; + public static final String LAST_NAME = "lastName"; + public static final String NODE_TYPE = "nodeType"; + public static final String DELETED_NODES = "deletedNodes"; + + public static final int DEFAULT_MAX_ITEMS_PER_PAGE = 50; + + // Injected services + protected ServiceRegistry serviceRegistry; + protected NodeArchiveService nodeArchiveService; + protected int maxSizeView = 1000; + + /** + * Sets the serviceRegistry instance + * + * @param serviceRegistry the serviceRegistry to set + */ + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + /** + * Sets the nodeArchiveService instance + * + * @param nodeArchiveService the nodeArchiveService to set + */ + public void setNodeArchiveService(NodeArchiveService nodeArchiveService) + { + this.nodeArchiveService = nodeArchiveService; + } + + /** + * Sets the maxSizeView + * + * @param maxSizeView the maxSizeView + */ + public void setMaxSizeView(int maxSizeView) + { + this.maxSizeView = maxSizeView; + } + + protected StoreRef parseRequestForStoreRef(WebScriptRequest req) + { + // get the parameters that represent the StoreRef, we know they are present + // otherwise this webscript would not have matched + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + + // create the StoreRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + + return storeRef; + } + + protected NodeRef parseRequestForNodeRef(WebScriptRequest req) + { + // get the parameters that represent the NodeRef. They may not all be there + // for all deletednodes webscripts. + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + String id = templateVars.get("id"); + + if (id == null || id.trim().length() == 0) + { + return null; + } + else + { + return new NodeRef(storeType, storeId, id); + } + } + + /** + * Retrieves the named parameter as an integer, if the parameter is not present the default value is returned + * + * @param req The WebScript request + * @param paramName The name of parameter to look for + * @param defaultValue The default value that should be returned if parameter is not present in request or if it is not positive + * @return The request parameter or default value + */ + protected int getIntParameter(WebScriptRequest req, String paramName, int defaultValue) + { + String paramString = req.getParameter(paramName); + + if (paramString != null) + { + try + { + int param = Integer.valueOf(paramString); + + if (param >= 0) + { + return param; + } + } + catch (NumberFormatException e) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); + } + } + + return defaultValue; + } + + /** + * * This method gets all nodes from the archive which were originally + * contained within the specified StoreRef. + * + * @param storeRef mandatory store ref + * @param paging mandatory paging + * @param filter optional filter + */ + protected PagingResults getArchivedNodesFrom(StoreRef storeRef, ScriptPagingDetails paging, String filter) + { + NodeService nodeService = serviceRegistry.getNodeService(); + NodeRef archiveStoreRootNodeRef = nodeService.getStoreArchiveNode(storeRef); + + // Create canned query + ArchivedNodesCannedQueryBuilder queryBuilder = new ArchivedNodesCannedQueryBuilder.Builder( + archiveStoreRootNodeRef, paging).filter(filter) + .sortOrderAscending(false).build(); + + // Query the DB + PagingResults result = nodeArchiveService.listArchivedNodes(queryBuilder); + + return result; + } + + protected void validatePermission(NodeRef nodeRef, String currentUser) + { + if (!nodeArchiveService.hasFullAccess(nodeRef)) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "You don't have permission to act on the node."); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodePut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodePut.java new file mode 100644 index 00000000000..64e995759dd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodePut.java @@ -0,0 +1,97 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.node.archive.RestoreNodeReport; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the archivednode.put webscript. + * + * @author Neil Mc Erlean + * @since 3.5 + */ +public class ArchivedNodePut extends AbstractArchivedNodeWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Current user + String userID = AuthenticationUtil.getFullyAuthenticatedUser(); + if (userID == null) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script [" + + req.getServiceMatch().getWebScript().getDescription() + + "] requires user authentication."); + } + + NodeRef nodeRefToBeRestored = parseRequestForNodeRef(req); + if (nodeRefToBeRestored == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "nodeRef not recognised. Could not restore."); + } + + // check if the current user has the permission to restore the node + validatePermission(nodeRefToBeRestored, userID); + + RestoreNodeReport report = nodeArchiveService.restoreArchivedNode(nodeRefToBeRestored); + + // Handling of some error scenarios + if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_INVALID_ARCHIVE_NODE)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find archive node: " + nodeRefToBeRestored); + } + else if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_PERMISSION)) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Unable to restore archive node: " + nodeRefToBeRestored); + } + else if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_DUPLICATE_CHILD_NODE_NAME)) + { + throw new WebScriptException(HttpServletResponse.SC_CONFLICT, "Unable to restore archive node: " + nodeRefToBeRestored +". Duplicate child node name"); + } + else if (report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_INVALID_PARENT) || + report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_INTEGRITY) || + report.getStatus().equals(RestoreNodeReport.RestoreStatus.FAILURE_OTHER)) + { + throw new WebScriptException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to restore archive node: " + nodeRefToBeRestored); + } + + model.put("restoreNodeReport", report); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodeState.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodeState.java new file mode 100644 index 00000000000..1cab5d2b3ac --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodeState.java @@ -0,0 +1,164 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.archive; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.PathUtil; + +/** + * A simple POJO class for the state of an archived node. For easier passing to the FTL model. + * + * @author Neil McErlean + * @since 3.5 + */ +public class ArchivedNodeState +{ + private NodeRef archivedNodeRef; + private String archivedBy; + private Date archivedDate; + private String name; + private String title; + private String description; + private String displayPath; + private String firstName; + private String lastName; + private String nodeType; + private boolean isContentType; + + /** + * To prevent unauthorised construction. + */ + private ArchivedNodeState() { /* Intentionally empty*/ } + + public static ArchivedNodeState create(NodeRef archivedNode, ServiceRegistry serviceRegistry) + { + ArchivedNodeState result = new ArchivedNodeState(); + + NodeService nodeService = serviceRegistry.getNodeService(); + Map properties = nodeService.getProperties(archivedNode); + + result.archivedNodeRef = archivedNode; + result.archivedBy = (String) properties.get(ContentModel.PROP_ARCHIVED_BY); + result.archivedDate = (Date) properties.get(ContentModel.PROP_ARCHIVED_DATE); + result.name = (String) properties.get(ContentModel.PROP_NAME); + result.title = (String) properties.get(ContentModel.PROP_TITLE); + result.description = (String) properties.get(ContentModel.PROP_DESCRIPTION); + QName type = nodeService.getType(archivedNode); + result.isContentType = (type.equals(ContentModel.TYPE_CONTENT) || serviceRegistry.getDictionaryService().isSubClass(type, ContentModel.TYPE_CONTENT)); + result.nodeType = type.toPrefixString(serviceRegistry.getNamespaceService()); + + PersonService personService = serviceRegistry.getPersonService(); + if (result.archivedBy != null && personService.personExists(result.archivedBy)) + { + NodeRef personNodeRef = personService.getPerson(result.archivedBy, false); + Map personProps = nodeService.getProperties(personNodeRef); + + result.firstName = (String) personProps.get(ContentModel.PROP_FIRSTNAME); + result.lastName = (String) personProps.get(ContentModel.PROP_LASTNAME); + } + + ChildAssociationRef originalParentAssoc = (ChildAssociationRef) properties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + + if (serviceRegistry.getPermissionService().hasPermission(originalParentAssoc.getParentRef(), PermissionService.READ).equals(AccessStatus.ALLOWED) + && nodeService.exists(originalParentAssoc.getParentRef())) + { + result.displayPath = PathUtil.getDisplayPath(nodeService.getPath(originalParentAssoc.getParentRef()), true); + } + else + { + result.displayPath = ""; + } + + return result; + } + + public NodeRef getNodeRef() + { + return this.archivedNodeRef; + } + + public String getArchivedBy() + { + return this.archivedBy; + } + + public Date getArchivedDate() + { + return this.archivedDate; + } + + public String getName() + { + return this.name; + } + + public String getTitle() + { + return this.title; + } + + public String getDescription() + { + return this.description; + } + + public String getDisplayPath() + { + return this.displayPath; + } + + public String getFirstName() + { + return this.firstName; + } + + public String getLastName() + { + return this.lastName; + } + + public String getNodeType() + { + return this.nodeType; + } + + public boolean getIsContentType() + { + return this.isContentType; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesDelete.java new file mode 100644 index 00000000000..40e1fd47ac5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesDelete.java @@ -0,0 +1,110 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.archive; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.ScriptPagingDetails; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the deletednodes.delete web script. + * + * @author Neil McErlean + * @since 3.5 + */ +public class ArchivedNodesDelete extends AbstractArchivedNodeWebScript +{ + private static Log log = LogFactory.getLog(ArchivedNodesDelete.class); + + public static final String PURGED_NODES = "purgedNodes"; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Current user + String userID = AuthenticationUtil.getFullyAuthenticatedUser(); + if (userID == null) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script [" + + req.getServiceMatch().getWebScript().getDescription() + + "] requires user authentication."); + } + + StoreRef storeRef = parseRequestForStoreRef(req); + NodeRef nodeRef = parseRequestForNodeRef(req); + + List nodesToBePurged = new ArrayList(); + if (nodeRef != null) + { + // check if the current user has the permission to purge the node + validatePermission(nodeRef, userID); + + // If there is a specific NodeRef, then that is the only Node that should be purged. + // In this case, the NodeRef points to the actual node to be purged i.e. the node in + // the archive store. + nodesToBePurged.add(nodeRef); + } + else + { + // But if there is no specific NodeRef and instead there is only a StoreRef, then + // all nodes which were originally in that StoreRef should be purged. + // Create paging + ScriptPagingDetails paging = new ScriptPagingDetails(maxSizeView, 0); + PagingResults result = getArchivedNodesFrom(storeRef, paging, null); + nodesToBePurged.addAll(result.getPage()); + } + + if (log.isDebugEnabled()) + { + log.debug("Purging " + nodesToBePurged.size() + " nodes"); + } + + // Now having identified the nodes to be purged, we simply have to do it. + nodeArchiveService.purgeArchivedNodes(nodesToBePurged); + + model.put(PURGED_NODES, nodesToBePurged); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesFilter.java new file mode 100644 index 00000000000..667d00d13df --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesFilter.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.archive; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * This interface defines a filter for ArchivedNodes. + * + * @author Neil Mc Erlean + * @since 3.5 + */ +public interface ArchivedNodesFilter +{ + /** + * This method checks whether or not the specified {@link NodeRef} should be included, + * as defined by the concrete filter implementation. + * @param nodeRef the NodeRef to be checked for filtering. + * @return true if the {@link NodeRef} is acceptable, else false. + */ + boolean accept(NodeRef nodeRef); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesGet.java new file mode 100644 index 00000000000..437aa717ac8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/ArchivedNodesGet.java @@ -0,0 +1,99 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.archive; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.ModelUtil; +import org.alfresco.util.ScriptPagingDetails; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the archivednodes.get web script. + * + * @author Neil McErlean, Jamal Kaabi-Mofrad + * @since 3.5 + */ +public class ArchivedNodesGet extends AbstractArchivedNodeWebScript +{ + private static final String MAX_ITEMS = "maxItems"; + private static final String SKIP_COUNT = "skipCount"; + private static final String NAME_FILTER = "nf"; + + List nodeFilters = new ArrayList(); + + /** + * This method is used to inject {@link org.alfresco.repo.web.scripts.archive.ArchivedNodesFilter node filters} on this GET call. + * + */ + public void setArchivedNodeFilters(List nodeFilters) + { + this.nodeFilters = nodeFilters; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // We want to get all nodes in the archive which were originally + // contained in the following StoreRef. + StoreRef storeRef = parseRequestForStoreRef(req); + + // Create paging + ScriptPagingDetails paging = new ScriptPagingDetails(getIntParameter(req, MAX_ITEMS, DEFAULT_MAX_ITEMS_PER_PAGE), + getIntParameter(req, SKIP_COUNT, 0)); + + PagingResults result = getArchivedNodesFrom(storeRef, paging, req.getParameter(NAME_FILTER)); + List nodeRefs = result.getPage(); + + List deletedNodes = new ArrayList(nodeRefs.size()); + for (NodeRef archivedNode : nodeRefs) + { + ArchivedNodeState state = ArchivedNodeState.create(archivedNode, serviceRegistry); + deletedNodes.add(state); + } + + // Now do the paging + // ALF-19111. Note: Archived nodes CQ, supports Paging, + // so no need to use the ModelUtil.page method to build the page again. + model.put(DELETED_NODES, deletedNodes); + // Because we haven't used ModelUtil.page method, we need to set the total items manually. + paging.setTotalItems(deletedNodes.size()); + model.put("paging", ModelUtil.buildPaging(paging)); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/NodeTypeFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/NodeTypeFilter.java new file mode 100644 index 00000000000..a4888ab9f41 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/archive/NodeTypeFilter.java @@ -0,0 +1,93 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.archive; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * This class is used to filter nodes based on node type. + * + * @author Neil McErlean + * @since 3.5 + */ +public class NodeTypeFilter implements ArchivedNodesFilter +{ + private NodeService nodeService; + private NamespaceService namespaceService; + private List excludedTypes; + + /** + * This method sets the NamespaceService object. + * @param namespaceService the namespaceService. + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * This method sets the NodeService object. + * @param nodeService the node service. + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the List of node types to exclude. These node types should be in the + * short form e.g. cm:myType. + * + * @param excludedTypesStg a List of node types which are to be excluded. + */ + public void setExcludedTypes(List excludedTypesStg) + { + // Convert the Strings to QNames. + this.excludedTypes = new ArrayList(excludedTypesStg.size()); + for (String s : excludedTypesStg) + { + QName typeQName = QName.createQName(s, namespaceService); + this.excludedTypes.add(typeQName); + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.web.scripts.archive.ArchivedNodesFilter#accept(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean accept(NodeRef nodeRef) + { + boolean typeIsExcluded = this.excludedTypes.contains(nodeService.getType(nodeRef)); + return !typeIsExcluded; + } +} + diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java new file mode 100644 index 00000000000..7d9f4c00248 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java @@ -0,0 +1,299 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.audit; + +import java.util.Map; + +import org.alfresco.service.cmr.audit.AuditService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Abstract implementation for scripts that access the {@link AuditService}. + * + * @author Derek Hulley + * @since 3.4 + */ +public abstract class AbstractAuditWebScript extends DeclarativeWebScript +{ + public static final String PARAM_APPLICATION = "application"; + public static final String PARAM_PATH="path"; + public static final String PARAM_ENABLE = "enable"; + public static final String PARAM_VALUE = "value"; + public static final String PARAM_VALUE_TYPE = "valueType"; + public static final String PARAM_FROM_TIME = "fromTime"; + public static final String PARAM_TO_TIME = "toTime"; + public static final String PARAM_FROM_ID = "fromId"; + public static final String PARAM_TO_ID = "toId"; + public static final String PARAM_USER = "user"; + public static final String PARAM_FORWARD = "forward"; + public static final String PARAM_LIMIT = "limit"; + public static final String PARAM_VERBOSE = "verbose"; + + public static final Long DEFAULT_FROM_TIME = null; + public static final Long DEFAULT_TO_TIME = null; + public static final Long DEFAULT_FROM_ID = null; + public static final Long DEFAULT_TO_ID = null; + public static final String DEFAULT_USER = null; + public static final boolean DEFAULT_FORWARD = true; + public static final int DEFAULT_LIMIT = 100; + public static final boolean DEFAULT_VERBOSE = false; + public static final boolean DEFAULT_ENABLE = false; + + public static final String JSON_KEY_ENABLED = "enabled"; + public static final String JSON_KEY_APPLICATIONS = "applications"; + public static final String JSON_KEY_NAME = "name"; + public static final String JSON_KEY_PATH = "path"; + public static final String JSON_KEY_CLEARED = "cleared"; + public static final String JSON_KEY_DELETED = "deleted"; + + public static final String JSON_KEY_ENTRY_COUNT = "count"; + public static final String JSON_KEY_ENTRIES = "entries"; + public static final String JSON_KEY_ENTRY_ID = "id"; + public static final String JSON_KEY_ENTRY_APPLICATION = "application"; + public static final String JSON_KEY_ENTRY_USER = "user"; + public static final String JSON_KEY_ENTRY_TIME = "time"; + public static final String JSON_KEY_ENTRY_VALUES = "values"; + + /** + * Logger that can be used by subclasses. + */ + protected final Log logger = LogFactory.getLog(this.getClass()); + + protected AuditService auditService; + + /** + * @param auditService the service that provides the actual data + */ + public void setAuditService(AuditService auditService) + { + this.auditService = auditService; + } + + /** + * Return an I18N'd message for the given key or the key itself if not present + * + * @param args arguments to replace the variables in the message + */ + protected String getI18NMessage(String key, Object ... args) + { + return I18NUtil.getMessage(key, args); + } + + /** + * Get the application name from the request. + * + * @return Returns the application name or null if not present + */ + protected final String getParamAppName(WebScriptRequest req) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + String app = templateVars.get(PARAM_APPLICATION); + if (app == null || app.length() == 0) + { + return null; + } + else + { + return app; + } + } + + /** + * Get the entry id from the request. + * + * @return Returns the id or null if not present + */ + protected Long getId(WebScriptRequest req) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + String id = templateVars.get("id"); + if (id == null || id.length() == 0) + { + return null; + } + else + { + try + { + return Long.parseLong(id); + } + catch (NumberFormatException e) + { + return null; + } + } + } + + /** + * Get the path from the request. + * + * @return Returns the path or null if not present + */ + protected String getParamPath(WebScriptRequest req) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + String paramPath = templateVars.get(PARAM_PATH); + if (paramPath == null || paramPath.length() == 0) + { + paramPath = null; + } + else if (!paramPath.startsWith("/")) + { + // It won't ever, so we can expect to be here all the time + paramPath = "/" + paramPath; + } + return paramPath; + } + + protected boolean getParamEnableDisable(WebScriptRequest req) + { + return getBooleanParam(req.getParameter(PARAM_ENABLE), DEFAULT_ENABLE); + } + + protected String getParamValue(WebScriptRequest req) + { + return req.getParameter(PARAM_VALUE); + } + + protected String getParamValueType(WebScriptRequest req) + { + return req.getParameter(PARAM_VALUE_TYPE); + } + + /** + * @see #DEFAULT_FROM_TIME + */ + protected Long getParamFromTime(WebScriptRequest req) + { + return getLongParam(req.getParameter(PARAM_FROM_TIME), DEFAULT_FROM_TIME); + } + + /** + * @see #DEFAULT_TO_TIME + */ + protected Long getParamToTime(WebScriptRequest req) + { + return getLongParam(req.getParameter(PARAM_TO_TIME), DEFAULT_TO_TIME); + } + + /** + * @see #DEFAULT_FROM_ID + */ + protected Long getParamFromId(WebScriptRequest req) + { + return getLongParam(req.getParameter(PARAM_FROM_ID), DEFAULT_FROM_ID); + } + + /** + * @see #DEFAULT_TO_ID + */ + protected Long getParamToId(WebScriptRequest req) + { + return getLongParam(req.getParameter(PARAM_TO_ID), DEFAULT_TO_ID); + } + + /** + * @see #DEFAULT_USER + */ + protected String getParamUser(WebScriptRequest req) + { + return req.getParameter(PARAM_USER); + } + + /** + * @see #DEFAULT_FORWARD + */ + protected boolean getParamForward(WebScriptRequest req) + { + return getBooleanParam(req.getParameter(PARAM_FORWARD), DEFAULT_FORWARD); + } + + /** + * @see #DEFAULT_LIMIT + */ + protected int getParamLimit(WebScriptRequest req) + { + return getIntParam(req.getParameter(PARAM_LIMIT), DEFAULT_LIMIT); + } + + /** + * @see #DEFAULT_VERBOSE + */ + protected boolean getParamVerbose(WebScriptRequest req) + { + return getBooleanParam(req.getParameter(PARAM_VERBOSE), DEFAULT_VERBOSE); + } + + private Long getLongParam(String paramStr, Long defaultVal) + { + if (paramStr == null) + { + // note: defaultVal can be null + return defaultVal; + } + try + { + return Long.parseLong(paramStr); + } + catch (NumberFormatException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, e.getMessage()); + } + } + + private boolean getBooleanParam(String paramStr, boolean defaultVal) + { + if (paramStr == null) + { + return defaultVal; + } + + // note: will return false if paramStr does not equals "true" (ignoring case) + return Boolean.parseBoolean(paramStr); + } + + private int getIntParam(String paramStr, int defaultVal) + { + if (paramStr == null) + { + return defaultVal; + } + try + { + return Integer.parseInt(paramStr); + } + catch (NumberFormatException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, e.getMessage()); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditClearPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditClearPost.java new file mode 100644 index 00000000000..7575941bb79 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditClearPost.java @@ -0,0 +1,74 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.audit; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.audit.AuditService.AuditApplication; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Derek Hulley + * @since 3.4 + */ +public class AuditClearPost extends AbstractAuditWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + String appName = getParamAppName(req); + if (appName == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.app.notProvided"); + } + AuditApplication app = auditService.getAuditApplications().get(appName); + if (app == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "audit.err.app.notFound", appName); + } + // Get from/to times + Long fromTime = getParamFromTime(req); // might be null + Long toTime = getParamToTime(req); // might be null + + // Clear + int cleared = auditService.clearAudit(appName, fromTime, toTime); + + model.put(JSON_KEY_CLEARED, cleared); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditControlGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditControlGet.java new file mode 100644 index 00000000000..eddf01c133a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditControlGet.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.audit; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.audit.AuditService.AuditApplication; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Derek Hulley + * @since 3.4 + */ +public class AuditControlGet extends AbstractAuditWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + String appName = getParamAppName(req); + String path = getParamPath(req); + boolean enabledGlobal = auditService.isAuditEnabled(); + Map appsByName = auditService.getAuditApplications(); + + // Check that the application exists + if (appName != null) + { + if (path == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.path.notProvided"); + } + + AuditApplication app = appsByName.get(appName); + if (app == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "audit.err.app.notFound", appName); + } + // Discard all the other applications + appsByName = Collections.singletonMap(appName, app); + } + + model.put(JSON_KEY_ENABLED, enabledGlobal); + model.put(JSON_KEY_APPLICATIONS, appsByName.values()); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditControlPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditControlPost.java new file mode 100644 index 00000000000..156f96d308b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditControlPost.java @@ -0,0 +1,77 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.audit; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Derek Hulley + * @since 3.4 + */ +public class AuditControlPost extends AbstractAuditWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + String appName = getParamAppName(req); + String path = getParamPath(req); + + boolean enable = getParamEnableDisable(req); + + if (appName == null) + { + // Global operation + auditService.setAuditEnabled(enable); + } + else + { + // Apply to a specific application + if (enable) + { + auditService.enableAudit(appName, path); + } + else + { + auditService.disableAudit(appName, path); + } + } + model.put(JSON_KEY_ENABLED, enable); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditEntryDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditEntryDelete.java new file mode 100644 index 00000000000..05c07ca274f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditEntryDelete.java @@ -0,0 +1,111 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.audit; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.audit.AuditQueryParameters; +import org.alfresco.service.cmr.audit.AuditService.AuditApplication; +import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Mark Rogers + * @since 4.2 + */ +public class AuditEntryDelete extends AbstractAuditWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(7); + + Long id = getId(req); + if (id == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.entry.id.notProvided"); + } + final List auditEntryIds = new ArrayList(); + + // Need to check that the audit entry actually exists - otherwise we get into a concurrency retry loop + AuditQueryParameters params = new AuditQueryParameters(); + AuditQueryCallback callback = new AuditQueryCallback(){ + + @Override + public boolean valuesRequired() { + return false; + } + + @Override + public boolean handleAuditEntry(Long entryId, + String applicationName, + String user, long time, + Map values) { + auditEntryIds.add(entryId); + return false; + } + + @Override + public boolean handleAuditEntryError(Long entryId, String errorMsg, + Throwable error) + { + return true; + } + + }; + + params.setToId(id); + params.setFromId(id); + auditService.auditQuery(callback, params, 1); + + if(auditEntryIds.size() > 0) + { + // + int deleted = auditService.clearAudit(auditEntryIds); + + model.put(JSON_KEY_DELETED, deleted); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } + else + { + // Not found + throw new WebScriptException(Status.STATUS_NOT_FOUND, "audit.err.entry.id.notfound", id); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java new file mode 100644 index 00000000000..82cdbebb23f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java @@ -0,0 +1,197 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.audit; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.audit.AuditQueryParameters; +import org.alfresco.service.cmr.audit.AuditService.AuditApplication; +import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; +import org.alfresco.service.cmr.repository.MLText; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Derek Hulley + * @since 3.4 + */ +public class AuditQueryGet extends AbstractAuditWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + final Map model = new HashMap(7); + + String appName = getParamAppName(req); + String path = getParamPath(req); + + Serializable value = getParamValue(req); + String valueType = getParamValueType(req); + Long fromTime = getParamFromTime(req); + Long toTime = getParamToTime(req); + Long fromId = getParamFromId(req); + Long toId = getParamToId(req); + String user = getParamUser(req); + boolean forward = getParamForward(req); + int limit = getParamLimit(req); + final boolean verbose = getParamVerbose(req); + + if (appName == null) + { + Map appsByName = auditService.getAuditApplications(); + AuditApplication app = appsByName.get(appName); + if (app == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "audit.err.app.notFound", appName); + } + } + + // Transform the value to the correct type + if (value != null && valueType != null) + { + try + { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(valueType); + value = DefaultTypeConverter.INSTANCE.convert(clazz, value); + } + catch (ClassNotFoundException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.value.classNotFound", valueType); + } + catch (Throwable e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.value.convertFailed", value, valueType); + } + } + + // Execute the query + AuditQueryParameters params = new AuditQueryParameters(); + params.setApplicationName(appName); + params.setFromTime(fromTime); + params.setToTime(toTime); + params.setFromId(fromId); + params.setToId(toId); + params.setUser(user); + params.setForward(forward); + if (path != null || value != null) + { + params.addSearchKey(path, value); + } + + final List> entries = new ArrayList>(limit); + AuditQueryCallback callback = new AuditQueryCallback() + { + @Override + public boolean valuesRequired() + { + return verbose; + } + + @Override + public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) + { + return true; + } + + @Override + public boolean handleAuditEntry( + Long entryId, + String applicationName, + String user, + long time, + Map values) + { + Map entry = new HashMap(11); + entry.put(JSON_KEY_ENTRY_ID, entryId); + entry.put(JSON_KEY_ENTRY_APPLICATION, applicationName); + if (user != null) + { + entry.put(JSON_KEY_ENTRY_USER, user); + } + entry.put(JSON_KEY_ENTRY_TIME, new Date(time)); + if (values != null) + { + // Convert values to Strings + Map valueStrings = new HashMap(values.size() * 2); + for (Map.Entry mapEntry : values.entrySet()) + { + String key = mapEntry.getKey(); + Serializable value = mapEntry.getValue(); + try + { + String valueString = DefaultTypeConverter.INSTANCE.convert(String.class, value); + valueStrings.put(key, valueString); + } + catch (ClassCastException e) + { + // Fix for symptoms of MNT-20992. It is possible to have MLText values whose underlying + // map's keys are not Locale, as MLText map keys should be. In this case we can expect + // a ClassCastException. + if (!(value instanceof MLText)) + { + // Rethrow if the exception was not caused by the expected MLText conversion. + throw e; + } + valueStrings.put(key, value.toString()); + } + catch (TypeConversionException e) + { + // Use the toString() + valueStrings.put(key, value.toString()); + } + + } + entry.put(JSON_KEY_ENTRY_VALUES, valueStrings); + } + entries.add(entry); + + return true; + } + }; + + auditService.auditQuery(callback, params, limit); + + model.put(JSON_KEY_ENTRY_COUNT, entries.size()); + model.put(JSON_KEY_ENTRIES, entries); + + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/ADMRemoteStore.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/ADMRemoteStore.java new file mode 100644 index 00000000000..2fd361d67d6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/ADMRemoteStore.java @@ -0,0 +1,1162 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.io.Writer; +import java.net.SocketException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.query.CannedQueryPageDetails; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.model.filefolder.HiddenAspect; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileFolderUtil; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * ADM Remote Store service. + *

+ * This implementation of the RemoteStore is tied to the current SiteService implementation. + *

+ * It remaps incoming generic document path requests to the appropriate folder structure + * in the Sites folder. Dashboard pages and component bindings are remapped to take advantage + * of inherited permissions in the appropriate root site folder, ensuring that only valid + * users can write to the appropriate configuration objects. + * + * @see BaseRemoteStore for the available API methods. + * + * @author Kevin Roast + */ +public class ADMRemoteStore extends BaseRemoteStore +{ + private static final Log logger = LogFactory.getLog(ADMRemoteStore.class); + + // name of the surf config folder + private static final String SURF_CONFIG = "surf-config"; + + // patterns used to match site and user specific configuration locations + private static final String PATH_COMPONENTS = "components"; + private static final String PATH_PAGES = "pages"; + private static final String PATH_USER = "user"; + private static final String PATH_SITE = "site"; + private static final String USER_CONFIG = ".*\\." + PATH_USER + "~(.*)~.*"; + private static final String USER_CONFIG_PATTERN = USER_CONFIG.replaceAll("\\.\\*", "*").replace("\\", ""); + private static final Pattern USER_PATTERN_1 = Pattern.compile(".*/" + PATH_COMPONENTS + "/" + USER_CONFIG); + private static final Pattern USER_PATTERN_2 = Pattern.compile(".*/" + PATH_PAGES + "/" + PATH_USER + "/(.*?)(/.*)?$"); + private static final Pattern SITE_PATTERN_1 = Pattern.compile(".*/" + PATH_COMPONENTS + "/.*\\." + PATH_SITE + "~(.*)~.*"); + private static final Pattern SITE_PATTERN_2 = Pattern.compile(".*/" + PATH_PAGES + "/" + PATH_SITE + "/(.*?)(/.*)?$"); + + + // service beans + protected NodeService nodeService; + protected NodeService unprotNodeService; + protected FileFolderService fileFolderService; + protected NamespaceService namespaceService; + protected SiteService siteService; + protected ContentService contentService; + protected HiddenAspect hiddenAspect; + protected PermissionService permissionService; + protected OwnableService ownableService; + private BehaviourFilter behaviourFilter; + + /** + * Date format pattern used to parse HTTP date headers in RFC 1123 format. + */ + private static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + + /** + * @param nodeService the NodeService to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param nodeService the NodeService to set + */ + public void setUnprotectedNodeService(NodeService nodeService) + { + this.unprotNodeService = nodeService; + } + + /** + * @param fileFolderService the FileFolderService to set + */ + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + /** + * @param namespaceService the NamespaceService to set + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param siteService the SiteService to set + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /** + * @param contentService the ContentService to set + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.hiddenAspect = hiddenAspect; + } + + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + /** + * Gets the last modified timestamp for the document. + *

+ * The output will be the last modified date as a long toString(). + * + * @param path document path to an existing document + */ + @Override + protected void lastModified(final WebScriptResponse res, final String store, final String path) + throws IOException + { + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveFilePath(encpath); + if (fileInfo == null) + { + throw new WebScriptException("Unable to locate file: " + encpath); + } + + Writer out = res.getWriter(); + out.write(Long.toString(fileInfo.getModifiedDate().getTime())); + out.close(); + if (logger.isDebugEnabled()) + logger.debug("lastModified: " + Long.toString(fileInfo.getModifiedDate().getTime())); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Gets a document. + *

+ * The output will be the document content stream. + * + * @param path document path + */ + @Override + protected void getDocument(final WebScriptResponse res, final String store, final String path) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveFilePath(encpath); + if (fileInfo == null || fileInfo.isFolder()) + { + res.setStatus(Status.STATUS_NOT_FOUND); + return null; + } + + final ContentReader reader; + try + { + reader = contentService.getReader(fileInfo.getNodeRef(), ContentModel.PROP_CONTENT); + if (reader == null || !reader.exists()) + { + throw new WebScriptException("No content found for file: " + encpath); + } + + // establish mimetype + String mimetype = reader.getMimetype(); + if (mimetype == null || mimetype.length() == 0) + { + mimetype = MimetypeMap.MIMETYPE_BINARY; + int extIndex = encpath.lastIndexOf('.'); + if (extIndex != -1) + { + String ext = encpath.substring(extIndex + 1); + String mt = mimetypeService.getMimetypesByExtension().get(ext); + if (mt != null) + { + mimetype = mt; + } + } + } + + // set mimetype for the content and the character encoding + length for the stream + res.setContentType(mimetype); + res.setContentEncoding(reader.getEncoding()); + SimpleDateFormat formatter = new SimpleDateFormat(PATTERN_RFC1123, Locale.US); + formatter.setTimeZone(GMT); + res.setHeader("Last-Modified", formatter.format(fileInfo.getModifiedDate())); + res.setHeader("Content-Length", Long.toString(reader.getSize())); + + if (logger.isDebugEnabled()) + logger.debug("getDocument: " + fileInfo.toString()); + + // get the content and stream directly to the response output stream + // assuming the repository is capable of streaming in chunks, this should allow large files + // to be streamed directly to the browser response stream. + try + { + reader.getContent(res.getOutputStream()); + } + catch (SocketException e1) + { + // the client cut the connection - our mission was accomplished apart from a little error message + if (logger.isDebugEnabled()) + logger.debug("Client aborted stream read:\n\tnode: " + encpath + "\n\tcontent: " + reader); + } + catch (ContentIOException e2) + { + if (logger.isInfoEnabled()) + logger.info("Client aborted stream read:\n\tnode: " + encpath + "\n\tcontent: " + reader); + } + catch (Throwable err) + { + if (err.getCause() instanceof SocketException) + { + if (logger.isDebugEnabled()) + logger.debug("Client aborted stream read:\n\tnode: " + encpath + "\n\tcontent: " + reader); + } + else + { + if (logger.isInfoEnabled()) + logger.info(err.getMessage()); + res.setStatus(Status.STATUS_INTERNAL_SERVER_ERROR); + } + } + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + } + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Determines if the document exists. + * + * The output will be either the string "true" or the string "false". + * + * @param path document path + */ + @Override + protected void hasDocument(final WebScriptResponse res, final String store, final String path) throws IOException + { + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveFilePath(encpath); + + Writer out = res.getWriter(); + out.write(Boolean.toString(fileInfo != null && !fileInfo.isFolder())); + out.close(); + if (logger.isDebugEnabled()) + logger.debug("hasDocument: " + Boolean.toString(fileInfo != null && !fileInfo.isFolder())); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Creates a document. + *

+ * Create methods are user authenticated, so the creation of site config must be + * allowed for the current user. + * + * @param path document path + * @param content content of the document to write + */ + @Override + protected void createDocument(final WebScriptResponse res, final String store, final String path, final InputStream content) + { + try + { + writeDocument(path, content); + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + throw ae; + } + catch (FileExistsException feeErr) + { + res.setStatus(Status.STATUS_CONFLICT); + throw feeErr; + } + } + + /** + * Creates multiple XML documents encapsulated in a single one. + * + * @param res WebScriptResponse + * @param store String + * @param in XML document containing multiple document contents to write + */ + @Override + protected void createDocuments(WebScriptResponse res, String store, InputStream in) + { + try + { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document document; + document = documentBuilder.parse(in); + Element docEl = document.getDocumentElement(); + Transformer transformer = ADMRemoteStore.this.transformer.get(); + for (Node n = docEl.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (!(n instanceof Element)) + { + continue; + } + final String path = ((Element) n).getAttribute("path"); + + // Turn the first element child into a document + Document doc = documentBuilder.newDocument(); + Node child; + for (child = n.getFirstChild(); child != null ; child=child.getNextSibling()) + { + if (child instanceof Element) + { + doc.appendChild(doc.importNode(child, true)); + break; + } + } + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + out.close(); + + writeDocument(path, new ByteArrayInputStream(out.toByteArray())); + } + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + throw ae; + } + catch (FileExistsException feeErr) + { + res.setStatus(Status.STATUS_CONFLICT); + throw feeErr; + } + catch (Exception e) + { + // various annoying checked SAX/IO exceptions related to XML processing can be thrown + // none of them should occur if the XML document is well formed + logger.error(e); + res.setStatus(Status.STATUS_INTERNAL_SERVER_ERROR); + throw new AlfrescoRuntimeException(e.getMessage(), e); + } + } + + protected void writeDocument(final String path, final InputStream content) + { + final String encpath = encodePath(path); + final int off = encpath.lastIndexOf('/'); + if (off != -1) + { + // check we actually are the user we are creating a user specific path for + final String runAsUser = getPathRunAsUser(path); + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + final FileInfo parentFolder = resolveNodePath(encpath, true, false); + if (parentFolder == null) + { + throw new IllegalStateException("Unable to aquire parent folder reference for path: " + path); + } + + // ALF-17729 / ALF-17796 - disable auditable on parent folder + NodeRef parentFolderRef = parentFolder.getNodeRef(); + behaviourFilter.disableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE); + + try + { + final String name = encpath.substring(off + 1); + // existence check - convert to an UPDATE - could occur if multiple threads request + // a write to the same document - a valid possibility but rare + if (nodeService.getChildByName(parentFolderRef, ContentModel.ASSOC_CONTAINS, name) == null) + { + FileInfo fileInfo = fileFolderService.create( + parentFolderRef, name, ContentModel.TYPE_CONTENT); + final NodeRef nodeRef = fileInfo.getNodeRef(); + // MNT-16371: Revoke ownership privileges for surf-config folder contents, to tighten access for former SiteManagers. + ownableService.setOwner(nodeRef, AuthenticationUtil.getAdminUserName()); + + Map aspectProperties = new HashMap(1, 1.0f); + aspectProperties.put(ContentModel.PROP_IS_INDEXED, false); + unprotNodeService.addAspect(nodeRef, ContentModel.ASPECT_INDEX_CONTROL, aspectProperties); + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + writer.guessMimetype(fileInfo.getName()); + writer.putContent(content); + if (logger.isDebugEnabled()) + logger.debug("createDocument: " + fileInfo.toString()); + } + else + { + ContentWriter writer = contentService.getWriter( + nodeService.getChildByName(parentFolderRef, ContentModel.ASSOC_CONTAINS, name), + ContentModel.PROP_CONTENT, + true); + writer.guessMimetype(name); + writer.putContent(content); + if (logger.isDebugEnabled()) + logger.debug("createDocument (updated): " + name); + } + } + finally + { + behaviourFilter.enableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE); + } + + return null; + } + }, runAsUser); + } + } + + /** + * Get the RunAs user need to execute a Write operation on the given path. + * + * @param path Document path + * @return runas user - will be the Full Authenticated User or System as required + */ + protected String getPathRunAsUser(final String path) + { + // check we actually are the user we are creating a user specific path for + String runAsUser = AuthenticationUtil.getFullyAuthenticatedUser(); + String userId = null; + Matcher matcher; + if ((matcher = USER_PATTERN_1.matcher(path)).matches()) + { + userId = matcher.group(1); + } + else if ((matcher = USER_PATTERN_2.matcher(path)).matches()) + { + userId = matcher.group(1); + } + if (userId != null && userId.equals(runAsUser)) + { + runAsUser = AuthenticationUtil.getSystemUserName(); + } + return runAsUser; + } + + /** + * Updates an existing document. + *

+ * Update methods are user authenticated, so the modification of site config must be + * allowed for the current user. + * + * @param path document path to update + * @param content content to update the document with + */ + @Override + protected void updateDocument(final WebScriptResponse res, String store, final String path, final InputStream content) + { + final String runAsUser = getPathRunAsUser(path); + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveFilePath(encpath); + if (fileInfo == null || fileInfo.isFolder()) + { + res.setStatus(Status.STATUS_NOT_FOUND); + return null; + } + + try + { + ContentWriter writer = contentService.getWriter(fileInfo.getNodeRef(), ContentModel.PROP_CONTENT, true); + writer.putContent(content); + if (logger.isDebugEnabled()) + logger.debug("updateDocument: " + fileInfo.toString()); + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + throw ae; + } + return null; + } + }, runAsUser); + } + + /** + * Deletes an existing document. + *

+ * Delete methods are user authenticated, so the deletion of the document must be + * allowed for the current user. + * + * @param path document path + */ + @Override + protected void deleteDocument(final WebScriptResponse res, final String store, final String path) + { + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveFilePath(encpath); + if (fileInfo == null || fileInfo.isFolder()) + { + res.setStatus(Status.STATUS_NOT_FOUND); + return; + } + + final String runAsUser = getPathRunAsUser(path); + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + try + { + final NodeRef fileRef = fileInfo.getNodeRef(); + // MNT-16371: Revoke ownership privileges for surf-config folder contents, to tighten access for former SiteManagers. + nodeService.addAspect(fileRef, ContentModel.ASPECT_TEMPORARY, null); + + // ALF-17729 + NodeRef parentFolderRef = unprotNodeService.getPrimaryParent(fileRef).getParentRef(); + behaviourFilter.disableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE); + + try + { + nodeService.deleteNode(fileRef); + } + finally + { + behaviourFilter.enableBehaviour(parentFolderRef, ContentModel.ASPECT_AUDITABLE); + } + + if (logger.isDebugEnabled()) + logger.debug("deleteDocument: " + fileInfo.toString()); + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + throw ae; + } + return null; + } + }, runAsUser); + } + + /** + * Lists the document paths under a given path. + *

+ * The output will be the list of relative document paths found under the path. + * Separated by newline characters. + * + * @param path document path + * @param recurse true to peform a recursive list, false for direct children only. + * + * @throws IOException if an error occurs listing the documents + */ + @Override + protected void listDocuments(final WebScriptResponse res, final String store, final String path, final boolean recurse) + throws IOException + { + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + res.setContentType("text/plain;charset=UTF-8"); + + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveNodePath(encpath, false, true); + if (fileInfo == null || !fileInfo.isFolder()) + { + res.setStatus(Status.STATUS_NOT_FOUND); + return null; + } + + try + { + outputFileNodes(res.getWriter(), fileInfo, aquireSurfConfigRef(encpath, false), "*", recurse); + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + } + finally + { + res.getWriter().close(); + } + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Lists the document paths matching a file pattern under a given path. + * + * The output will be the list of relative document paths found under the path that + * match the given file pattern. Separated by newline characters. + * + * @param path document path + * @param pattern file pattern to match - allows wildcards e.g. page.*.site.xml + * + * @throws IOException if an error occurs listing the documents + */ + @Override + protected void listDocuments(final WebScriptResponse res, final String store, final String path, final String pattern) + throws IOException + { + AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public Void doWork() throws Exception + { + res.setContentType("text/plain;charset=UTF-8"); + + String filePattern; + if (pattern == null || pattern.length() == 0) + { + filePattern = "*"; + } + else + { + // need to ensure match pattern is path encoded - but don't encode * character! + StringBuilder buf = new StringBuilder(pattern.length()); + for (StringTokenizer t = new StringTokenizer(pattern, "*"); t.hasMoreTokens(); /**/) + { + buf.append(encodePath(t.nextToken())); + if (t.hasMoreTokens()) + { + buf.append('*'); + } + } + // ensure the escape character is itself escaped + filePattern = buf.toString().replace("\\", "\\\\"); + } + + // ensure we pass in the file pattern as it is used as part of the folder match - i.e. + // for a site component set e.g. /alfresco/site-data/components/page.*.site~xyz~dashboard.xml + final String encpath = encodePath(path); + final FileInfo fileInfo = resolveNodePath(encpath, filePattern, false, true); + if (fileInfo == null || !fileInfo.isFolder()) + { + res.setStatus(Status.STATUS_NOT_FOUND); + return null; + } + + if (logger.isDebugEnabled()) + logger.debug("listDocuments() pattern: " + filePattern); + + try + { + outputFileNodes( + res.getWriter(), fileInfo, + aquireSurfConfigRef(encpath + "/" + filePattern, false), + filePattern, false); + } + catch (AccessDeniedException ae) + { + res.setStatus(Status.STATUS_UNAUTHORIZED); + } + finally + { + res.getWriter().close(); + } + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * @param path cm:name based root relative path + * example: /alfresco/site-data/pages/customise-user-dashboard.xml + * + * @return FileInfo representing the file/folder at the specified path location + * or null if the supplied path does not exist in the store + */ + private FileInfo resolveFilePath(final String path) + { + return resolveNodePath(path, false, false); + } + + /** + * @param path cm:name based root relative path + * example: /alfresco/site-data/pages/customise-user-dashboard.xml + * /alfresco/site-data/components + * @param create if true create the config and folder dirs for the given path returning + * the FileInfo for the last parent in the path, if false only attempt to + * resolve the folder path if it exists returning the last element. + * @param isFolder True if the path is for a folder, false if it ends in a filename + * + * @return FileInfo representing the file/folder at the specified path location (see create + * parameter above) or null if the supplied path does not exist in the store. + */ + private FileInfo resolveNodePath(final String path, final boolean create, final boolean isFolder) + { + return resolveNodePath(path, null, create, isFolder); + } + + /** + * @param path cm:name based root relative path + * example: /alfresco/site-data/pages/customise-user-dashboard.xml + * /alfresco/site-data/components + * @param pattern optional pattern that is used as part of the match to aquire the surf-config + * folder under the appropriate sites or user location. + * @param create if true create the config and folder dirs for the given path returning + * the FileInfo for the last parent in the path, if false only attempt to + * resolve the folder path if it exists returning the last element. + * @param isFolder True if the path is for a folder, false if it ends in a filename + * + * @return FileInfo representing the file/folder at the specified path location (see create + * parameter above) or null if the supplied path does not exist in the store. + */ + private FileInfo resolveNodePath(final String path, final String pattern, final boolean create, final boolean isFolder) + { + if (logger.isDebugEnabled()) + logger.debug("Resolving path: " + path); + + final String adminUserName = AuthenticationUtil.getAdminUserName(); + + FileInfo result = null; + if (path != null) + { + // break down the path into its component elements + List pathElements = new ArrayList(4); + final StringTokenizer t = new StringTokenizer(path, "/"); + // the store requires paths of the form /alfresco/site-data/[/]/.xml + if (t.countTokens() >= 3) + { + t.nextToken(); // skip /alfresco + t.nextToken(); // skip /site-data + // collect remaining folder path (and file) + while (t.hasMoreTokens()) + { + pathElements.add(t.nextToken()); + } + + NodeRef surfConfigRef = aquireSurfConfigRef(path + (pattern != null ? ("/" + pattern) : ""), create); + try + { + if (surfConfigRef != null) + { + if (create) + { + List folders = isFolder ? pathElements : pathElements.subList(0, pathElements.size() - 1); + + List folderDetails = new ArrayList<>(pathElements.size()); + Map prop = new HashMap<>(2); + prop.put(ContentModel.PROP_IS_INDEXED, false); + prop.put(ContentModel.PROP_IS_CONTENT_INDEXED, false); + for (String element : folders) + { + Map> aspects = Collections.singletonMap(ContentModel.ASPECT_INDEX_CONTROL, prop); + folderDetails.add(new FileFolderUtil.PathElementDetails(element, aspects)); + } + // ensure folders exist down to the specified parent + // ALF-17729 / ALF-17796 - disable auditable on parent folders + Set allCreatedFolders = new LinkedHashSet<>(); + result = FileFolderUtil.makeFolders( + this.fileFolderService,nodeService, + surfConfigRef, + folderDetails, + ContentModel.TYPE_FOLDER, + behaviourFilter, + new HashSet(Arrays.asList(new QName[]{ContentModel.ASPECT_AUDITABLE})), allCreatedFolders); + + // MNT-16371: Revoke ownership privileges for surf-config folder, to tighten access for former SiteManagers. + for(NodeRef nodeRef : allCreatedFolders) + { + ownableService.setOwner(nodeRef, adminUserName); + } + } + else + { + // perform the cm:name path lookup against our config root node + result = this.fileFolderService.resolveNamePath(surfConfigRef, pathElements); + } + } + } + catch (FileNotFoundException fnfErr) + { + // this is a valid condition - we return null to indicate failed lookup + } + } + } + return result; + } + + /** + * Aquire (optionally create) the NodeRef to the "surf-config" folder as appropriate + * for the given path. + *

+ * Disassmbles the path to correct match either user, site or generic folder path. + * + * @param path String + * @param create boolean + * + * @return NodeRef to the "surf-config" folder, or null if it does not exist yet. + */ + private NodeRef aquireSurfConfigRef(final String path, final boolean create) + { + // remap the path into the appropriate Sites or site relative folder location + // by first matching the path to appropriate user or site regex + final boolean debug = logger.isDebugEnabled(); + String userId = null; + String siteName = null; + Matcher matcher; + if (debug) + { + // user data is stored directly under the Sites folder along with + // other generic config files - there is actually no need to match + // anything other than site specific config other than for debug + if ((matcher = USER_PATTERN_1.matcher(path)).matches()) + { + userId = matcher.group(1); + } + else if ((matcher = USER_PATTERN_2.matcher(path)).matches()) + { + userId = matcher.group(1); + } + else if ((matcher = SITE_PATTERN_1.matcher(path)).matches()) + { + siteName = matcher.group(1); + } + else if ((matcher = SITE_PATTERN_2.matcher(path)).matches()) + { + siteName = matcher.group(1); + } + } + else if ((matcher = SITE_PATTERN_1.matcher(path)).matches()) + { + siteName = matcher.group(1); + } + else if ((matcher = SITE_PATTERN_2.matcher(path)).matches()) + { + siteName = matcher.group(1); + } + + NodeRef surfConfigRef = null; + if (siteName != null) + { + if (debug) logger.debug("...resolved site path id: " + siteName); + NodeRef siteRef = getSiteNodeRef(siteName); + if (siteRef != null) + { + surfConfigRef = getSurfConfigNodeRef(siteRef, create); + } + } + else + { + if (debug) + { + if (userId != null) + { + logger.debug("...resolved user path id: " + userId); + } + else + { + logger.debug("...resolved to generic path."); + } + } + surfConfigRef = getSurfConfigNodeRef(getRootNodeRef(), create); + } + return surfConfigRef; + } + + /** + * Return the "surf-config" noderef under the given root. No attempt will be made + * to create the node if it does not exist yet. + * + * @param rootRef Root node reference where the "surf-config" folder should live + * + * @return surf-config folder ref if found, null otherwise + */ + private NodeRef getSurfConfigNodeRef(final NodeRef rootRef) + { + return getSurfConfigNodeRef(rootRef, false); + } + + /** + * Return the "surf-config" noderef under the given root. Optionally create the + * folder if it does not exist yet. NOTE: must only be set to create if within a + * WRITE transaction context. + *

+ * Adds the "isIndexed = false" property to the surf-config folder node. + * + * @param rootRef Root node reference where the "surf-config" folder should live + * @param create True to create the folder if missing, false otherwise + * + * @return surf-config folder ref if found, null otherwise if not creating + */ + protected NodeRef getSurfConfigNodeRef(final NodeRef rootRef, final boolean create) + { + NodeRef surfConfigRef = this.unprotNodeService.getChildByName( + rootRef, ContentModel.ASSOC_CONTAINS, SURF_CONFIG); + if (create && surfConfigRef == null) + { + if (logger.isDebugEnabled()) + logger.debug("'surf-config' folder not found under path, creating..."); + QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, SURF_CONFIG); + Map properties = new HashMap(1, 1.0f); + properties.put(ContentModel.PROP_NAME, (Serializable) SURF_CONFIG); + ChildAssociationRef ref = this.unprotNodeService.createNode( + rootRef, ContentModel.ASSOC_CONTAINS, assocQName, ContentModel.TYPE_FOLDER, properties); + surfConfigRef = ref.getChildRef(); + // surf-config needs to be hidden - applies index control aspect as part of the hidden aspect + hiddenAspect.hideNode(ref.getChildRef(), false, false, false); + + // MNT-16371: Revoke inherited permission + permissionService.setInheritParentPermissions(surfConfigRef, false); + String siteName = siteService.getSiteShortName(rootRef); + if (siteName != null) + { + // Revoke ownership privileges for surf-config folder, to tighten access for former SiteManagers. + ownableService.setOwner(surfConfigRef, AuthenticationUtil.getAdminUserName()); + // Set site manager group permission + String siteManagerGroup = siteService.getSiteRoleGroup(siteName, SiteModel.SITE_MANAGER); + permissionService.setPermission(surfConfigRef, siteManagerGroup, SiteModel.SITE_MANAGER, true); + } + } + return surfConfigRef; + } + + /** + * @return NodeRef to the shared components config folder + */ + protected NodeRef getGlobalComponentsNodeRef() + { + NodeRef result = null; + + NodeRef surfRef = getSurfConfigNodeRef(siteService.getSiteRoot()); + if (surfRef != null) + { + result = nodeService.getChildByName(surfRef, ContentModel.ASSOC_CONTAINS, PATH_COMPONENTS); + } + + return result; + } + + /** + * @return NodeRef to the shared user config folder + */ + protected NodeRef getGlobalUserFolderNodeRef() + { + NodeRef result = null; + + NodeRef surfRef = getSurfConfigNodeRef(siteService.getSiteRoot()); + if (surfRef != null) + { + NodeRef pagesRef = nodeService.getChildByName(surfRef, ContentModel.ASSOC_CONTAINS, PATH_PAGES); + if (pagesRef != null) + { + result = nodeService.getChildByName(pagesRef, ContentModel.ASSOC_CONTAINS, PATH_USER); + } + } + + return result; + } + + /** + * Generate the search pattern for a Surf config location for a user name. + * + * @param userName to build pattern for + * @return the search pattern + */ + protected String buildUserConfigSearchPattern(String userName) + { + return USER_CONFIG_PATTERN.replace("(*)", encodePath(userName)); + } + + /** + * @return the Sites folder root node reference + */ + private NodeRef getRootNodeRef() + { + return this.siteService.getSiteRoot(); + } + + /** + * @param shortName Site shortname + * + * @return the given Site folder node reference + */ + private NodeRef getSiteNodeRef(String shortName) + { + SiteInfo siteInfo = this.siteService.getSite(shortName); + return siteInfo != null ? siteInfo.getNodeRef() : null; + } + + /** + * Output the matching file paths a node contains based on a pattern search. + * + * @param out Writer for output - relative paths separated by newline characters + * @param surfConfigRef Surf-Config folder + * @param fileInfo The FileInfo node to use as the parent + * @param pattern Optional pattern to match filenames against ("*" is match all) + * @param recurse True to recurse sub-directories + * @throws IOException + */ + private void outputFileNodes(Writer out, FileInfo fileInfo, NodeRef surfConfigRef, String pattern, boolean recurse) throws IOException + { + if (surfConfigRef != null) + { + final boolean debug = logger.isDebugEnabled(); + PagingResults files = getFileNodes(fileInfo, pattern, recurse); + + final Map nameCache = new HashMap(); + for (final FileInfo file : files.getPage()) + { + // walking up the parent tree manually until the "surf-config" parent is hit + // and manually appending the rest of the cm:name path down to the node. + final StringBuilder displayPath = new StringBuilder(64); + NodeRef ref = unprotNodeService.getPrimaryParent(file.getNodeRef()).getParentRef(); + while (!ref.equals(surfConfigRef)) + { + String name = nameCache.get(ref); + if (name == null) + { + name = (String) unprotNodeService.getProperty(ref, ContentModel.PROP_NAME); + nameCache.put(ref, name); + } + displayPath.insert(0, '/'); + displayPath.insert(0, name); + ref = unprotNodeService.getPrimaryParent(ref).getParentRef(); + } + + out.write("/alfresco/site-data/"); + out.write(URLDecoder.decode(displayPath.toString())); + out.write(URLDecoder.decode(file.getName())); + out.write('\n'); + if (debug) logger.debug(" /alfresco/site-data/" + displayPath.toString() + file.getName()); + } + } + } + + protected PagingResults getFileNodes(FileInfo fileInfo, String pattern, boolean recurse) + { + return fileFolderService.list( + fileInfo.getNodeRef(), true, false, + pattern, null, null, + new PagingRequest(CannedQueryPageDetails.DEFAULT_PAGE_SIZE)); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/AbstractLoginBean.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/AbstractLoginBean.java new file mode 100644 index 00000000000..660d92482b6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/AbstractLoginBean.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.sync.events.types.Event; +import org.alfresco.sync.events.types.RepositoryEventImpl; +import org.alfresco.sync.repo.events.EventPreparator; +import org.alfresco.sync.repo.events.EventPublisher; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Common code between Get based login and POST based login + */ +/* package scope */ abstract class AbstractLoginBean extends DeclarativeWebScript +{ + // dependencies + private AuthenticationService authenticationService; + protected EventPublisher eventPublisher; + + /** + * @param authenticationService AuthenticationService + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * @param eventPublisher EventPublisher + */ + public void setEventPublisher(EventPublisher eventPublisher) + { + this.eventPublisher = eventPublisher; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + return null; + } + + protected Map login(final String username, String password) + { + try + { + // get ticket + authenticationService.authenticate(username, password.toCharArray()); + + eventPublisher.publishEvent(new EventPreparator(){ + @Override + public Event prepareEvent(String user, String networkId, String transactionId) + { + // TODO need to fix up to pass correct seqNo and alfrescoClientId + return new RepositoryEventImpl(-1l, "login", transactionId, networkId, new Date().getTime(), + username, null); + } + }); + + // add ticket to model for javascript and template access + Map model = new HashMap(7, 1.0f); + model.put("username", username); + model.put("ticket", authenticationService.getCurrentTicket()); + + return model; + } + catch(AuthenticationException e) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Login failed"); + } + finally + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + } + + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Authentication.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Authentication.java new file mode 100644 index 00000000000..5f4a2d3360c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Authentication.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * WebScript java backed bean implementation - to return information about the + * authentication system, such as account mutability. + * + * @author Kevin Roast + */ +public class Authentication extends DeclarativeWebScript +{ + private MutableAuthenticationService authenticationService; + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = (MutableAuthenticationService)authenticationService; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Map model = new HashMap(2); + + model.put("creationAllowed", this.authenticationService.isAuthenticationCreationAllowed()); + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/BaseRemoteStore.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/BaseRemoteStore.java new file mode 100644 index 00000000000..17e333683e8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/BaseRemoteStore.java @@ -0,0 +1,566 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import javax.servlet.http.HttpServletRequest; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactory; + +import org.alfresco.service.cmr.repository.MimetypeService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * Remote Store service. + *

+ * Responsible for providing remote HTTP based access to a store. Designed to be accessed + * from a web-tier application to remotely mirror a WebScript Store instance. + *

+ * Request format: + *

+ *      //[?]
+ *      //s//[?]
+ *      //s//w//[?]
+ * 

+ * Example: + *

+ *      /service/remotestore/lastmodified/sites/xyz/pages/page.xml
+ * 

+ * where: + *

+ *      /service/remotestore -> service path
+ *      /lastmodified        -> method name
+ *      /sites/../page.xml   -> document path
+ * 

+ * optional request parameters: + *

+ *      s                    -> the store id
+ * 

+ * Note: path is relative to the root path as configured for this webscript bean + *

+ * Further URL arguments may be provided if required by specific API methods. + *

+ * For content create and update the request should be POSTed and the content sent as the + * payload of the request content. + *

+ * Supported API methods: + *

+ *      GET lastmodified -> return timestamp of a document in ms since 1970 as a long string value
+ *      GET has -> return true or false string as existence for a document
+ *      GET get -> return raw document content - in addition the appropriate HTTP headers for the
+ *                 character encoding, content type, length and modified date will be set
+ *      GET list -> return the list of available document paths under a path - UTF-8 response text
+ *      GET listall -> return the list of available document paths (recursively) under a given path
+ *                     - UTF-8 response text
+ *      GET listpattern -> return the list of document paths matching a file pattern under a given path
+ *                         - UTF-8 response text
+ *      POST create -> create a new document with request content payload
+ *      POST createmulti -> create multiple new documents with request content payload
+ *      POST update -> update an existing document with request content payload
+ *      DELETE delete -> delete an existing document 
+ * 
+ * @author Kevin Roast + */ +public abstract class BaseRemoteStore extends AbstractWebScript +{ + public static final String TOKEN_STORE = "s"; + + public static final String REQUEST_PARAM_STORE = "s"; + + private static final Log logger = LogFactory.getLog(BaseRemoteStore.class); + + protected String defaultStore; + protected MimetypeService mimetypeService; + + protected static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance(); + protected static ThreadLocal transformer = new ThreadLocal() + { + @Override + protected Transformer initialValue() + { + try + { + return TRANSFORMER_FACTORY.newTransformer(); + } + catch (TransformerConfigurationException e) + { + throw new RuntimeException(e); + } + } + }; + + + /** + * @param defaultStore the default store name of the store to process document requests against + */ + public void setStore(String defaultStore) + { + this.defaultStore = defaultStore; + } + + /** + * @param mimetypeService the MimetypeService to set + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + /** + * Execute the webscript based on the request parameters + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + // NOTE: This web script must be executed in a HTTP Servlet environment + + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + + if (webScriptServletRequest == null) + { + throw new WebScriptException("Remote Store access must be executed in HTTP Servlet environment"); + } + + HttpServletRequest httpReq = webScriptServletRequest.getHttpServletRequest(); + + // the request path for the remote store + String extPath = req.getExtensionPath(); + + // values that we need to determine + String methodName = null; + String store = null; + StringBuilder pathBuilder = new StringBuilder(128); + + // tokenize the path and figure out tokenized values + StringTokenizer tokenizer = new StringTokenizer(extPath, "/"); + if (tokenizer.hasMoreTokens()) + { + methodName = tokenizer.nextToken(); + + if (tokenizer.hasMoreTokens()) + { + String el = tokenizer.nextToken(); + + if (TOKEN_STORE.equals(el)) + { + // if the token is TOKEN_STORE, then the next token is the id of the store + store = tokenizer.nextToken(); + + // reset element + el = (tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null); + } + + while (el != null) + { + pathBuilder.append('/'); + pathBuilder.append(el); + + el = (tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null); + } + } + } + else + { + throw new WebScriptException("Unable to tokenize web path: " + extPath); + } + + // if we don't have a store, check whether it came in on a request parameter + if (store == null) + { + store = req.getParameter(REQUEST_PARAM_STORE); + if (store == null) + { + store = this.defaultStore; + } + if (store == null) + { + // not good, we should have a store by this point + // this means that a store was not passed in and that we also didn't have a configured store + throw new WebScriptException("Unable to determine which store to operate against." + + " A store was not specified and a default was not provided."); + } + } + + String path = pathBuilder.toString(); + + long start = 0; + if (logger.isDebugEnabled()) + { + logger.debug("Remote method: " + methodName.toUpperCase() + " Store Id: " + store + " Path: " + path); + start = System.nanoTime(); + } + + try + { + // generate enum from string method name - so we can use a fast switch table lookup + APIMethod method = APIMethod.valueOf(methodName.toUpperCase()); + switch (method) + { + case LASTMODIFIED: + validatePath(path); + lastModified(res, store, path); + break; + + case HAS: + validatePath(path); + hasDocument(res, store, path); + break; + + case GET: + validatePath(path); + getDocument(res, store, path); + break; + + case LIST: + listDocuments(res, store, path, false); + break; + + case LISTALL: + listDocuments(res, store, path, true); + break; + + case LISTPATTERN: + listDocuments(res, store, path, req.getParameter("m")); + break; + + case CREATE: + validatePath(path); + if (logger.isDebugEnabled()) + logger.debug("CREATE: content length=" + httpReq.getContentLength()); + createDocument(res, store, path, httpReq.getInputStream()); + break; + + case CREATEMULTI: + if (logger.isDebugEnabled()) + logger.debug("CREATEMULTI: content length=" + httpReq.getContentLength()); + createDocuments(res, store, httpReq.getInputStream()); + break; + + case UPDATE: + validatePath(path); + if (logger.isDebugEnabled()) + logger.debug("UPDATE: content length=" + httpReq.getContentLength()); + updateDocument(res, store, path, httpReq.getInputStream()); + break; + + case DELETE: + validatePath(path); + deleteDocument(res, store, path); + break; + } + } + catch (IllegalArgumentException enumErr) + { + throw new WebScriptException("Unknown method specified to remote store API: " + methodName); + } + catch (IOException ioErr) + { + throw new WebScriptException("Error during remote store API: " + ioErr.getMessage()); + } + + if (logger.isDebugEnabled()) + { + long end = System.nanoTime(); + logger.debug("Time to execute method: " + (end - start)/1000000f + "ms"); + } + } + + /** + * Validate we have a path argument. + */ + private static void validatePath(String path) + { + if (path == null) + { + throw new WebScriptException("Remote Store expecting document path elements."); + } + } + + /** + * Helper to break down webscript extension path into path component elements + */ + protected List getPathParts(String[] extPaths) + { + List pathParts = new ArrayList(extPaths.length - 1); + for (int i=1; i> 6)]); + sb.append(hex[0x80 | (ch & 0x3F)]); + } + else // 0x7FF < ch <= 0xFFFF + { + if (sb == null) + { + final String soFar = s.substring(0, i); + sb = new StringBuilder(len + 16); + sb.append(soFar); + } + sb.append(hex[0xe0 | (ch >> 12)]); + sb.append(hex[0x80 | ((ch >> 6) & 0x3F)]); + sb.append(hex[0x80 | (ch & 0x3F)]); + } + } + return (sb != null ? sb.toString() : s); + } + + private final static String[] hex = { + "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", + "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f", + "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", + "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f", + "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27", + "%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f", + "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37", + "%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f", + "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47", + "%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f", + "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", + "%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f", + "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67", + "%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f", + "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77", + "%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f", + "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87", + "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f", + "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", + "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f", + "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7", + "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af", + "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7", + "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf", + "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7", + "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf", + "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7", + "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df", + "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7", + "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef", + "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7", + "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff" + }; +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/KeywordSearch.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/KeywordSearch.java new file mode 100644 index 00000000000..4987a91c508 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/KeywordSearch.java @@ -0,0 +1,414 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.io.StringWriter; +import java.io.Writer; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.alfresco.repo.template.TemplateNode; +import org.alfresco.repo.web.scripts.RepositoryImageResolver; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.util.GUID; +import org.alfresco.util.SearchLanguageConversion; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.surf.util.ParameterCheck; +import org.springframework.extensions.surf.util.URLEncoder; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Alfresco Keyword (simple) Search Service + * + * @author davidc + */ +public class KeywordSearch extends DeclarativeWebScript +{ + // Logger + private static final Log logger = LogFactory.getLog(KeywordSearch.class); + + // search parameters + // TODO: allow configuration of search store + protected static final StoreRef SEARCH_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + protected static final int DEFAULT_ITEMS_PER_PAGE = 10; + protected static final String QUERY_FORMAT = "query_"; + + // dependencies + protected ServiceRegistry serviceRegistry; + protected RepositoryImageResolver imageResolver; + protected SearchService searchService; + protected NodeService nodeService; + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setRepositoryImageResolver(RepositoryImageResolver imageResolver) + { + this.imageResolver = imageResolver; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + // + // process arguments + // + + String searchTerms = req.getParameter("q"); + ParameterCheck.mandatoryString("q", searchTerms); + String startPageArg = req.getParameter("p"); + int startPage = 1; + try + { + startPage = new Integer(startPageArg); + } + catch(NumberFormatException e) + { + // NOTE: use default startPage + } + String itemsPerPageArg = req.getParameter("c"); + int itemsPerPage = DEFAULT_ITEMS_PER_PAGE; + try + { + itemsPerPage = new Integer(itemsPerPageArg); + } + catch(NumberFormatException e) + { + // NOTE: use default itemsPerPage + } + Locale locale = I18NUtil.getLocale(); + String language = req.getParameter("l"); + if (language != null && language.length() > 0) + { + // NOTE: Simple conversion from XML Language Id to Java Locale Id + locale = new Locale(language.replace("-", "_")); + } + + // + // execute the search + // + + SearchResult results = search(searchTerms, startPage, itemsPerPage, locale, req); + + // + // create model + // + + Map model = new HashMap(7, 1.0f); + model.put("search", results); + return model; + } + + /** + * Execute the search + */ + private SearchResult search(String searchTerms, int startPage, int itemsPerPage, Locale locale, WebScriptRequest req) + { + SearchResult searchResult = null; + ResultSet results = null; + + try + { + // construct search statement + String[] terms = searchTerms.split(" "); + searchTerms = searchTerms.replaceAll("\"", """); + + // Escape special characters in the terms, so that they can't confuse the parser + for (int i=0; i statementModel = new HashMap(7, 1.0f); + statementModel.put("args", createArgs(req)); + statementModel.put("terms", terms); + Writer queryWriter = new StringWriter(1024); + renderFormatTemplate(QUERY_FORMAT, statementModel, queryWriter); + String query = queryWriter.toString(); + + // execute query + if (logger.isDebugEnabled()) + { + logger.debug("Search parameters: searchTerms=" + searchTerms + ", startPage=" + startPage + ", itemsPerPage=" + itemsPerPage + ", search locale=" + locale.toString()); + logger.debug("Issuing lucene search: " + query); + } + + SearchParameters parameters = new SearchParameters(); + parameters.addStore(SEARCH_STORE); + parameters.setLanguage(SearchService.LANGUAGE_LUCENE); + parameters.setQuery(query); + if (locale != null) + { + parameters.addLocale(locale); + } + results = searchService.query(parameters); + int totalResults = results.length(); + + if (logger.isDebugEnabled()) + logger.debug("Results: " + totalResults + " rows (limited: " + results.getResultSetMetaData().getLimitedBy() + ")"); + + // are we out-of-range + int totalPages = (totalResults / itemsPerPage); + totalPages += (totalResults % itemsPerPage != 0) ? 1 : 0; + if (totalPages != 0 && (startPage < 1 || startPage > totalPages)) + { + throw new WebScriptException("Start page " + startPage + " is outside boundary of " + totalPages + " pages"); + } + + // construct search result + searchResult = new SearchResult(); + searchResult.setSearchTerms(searchTerms); + searchResult.setLocale(locale); + searchResult.setItemsPerPage(itemsPerPage); + searchResult.setStartPage(startPage); + searchResult.setTotalResults(totalResults); + if (totalResults == 0) + { + searchResult.setTotalPages(0); + searchResult.setStartIndex(0); + searchResult.setTotalPageItems(0); + } + else + { + searchResult.setTotalPages(totalPages); + searchResult.setStartIndex(((startPage -1) * itemsPerPage) + 1); + searchResult.setTotalPageItems(Math.min(itemsPerPage, totalResults - searchResult.getStartIndex() + 1)); + } + SearchTemplateNode[] nodes = new SearchTemplateNode[searchResult.getTotalPageItems()]; + for (int i = 0; i < searchResult.getTotalPageItems(); i++) + { + NodeRef node = results.getNodeRef(i + searchResult.getStartIndex() - 1); + // Make the search resilient to invalid nodes + if (!nodeService.exists(node)) + { + continue; + } + float score = results.getScore(i + searchResult.getStartIndex() - 1); + nodes[i] = new SearchTemplateNode(node, score); + } + searchResult.setResults(nodes); + return searchResult; + } + finally + { + if (results != null) + { + results.close(); + } + } + } + + /** + * Search Result + * + * @author davidc + */ + public static class SearchResult + { + private String id; + private String searchTerms; + private Locale locale; + private int itemsPerPage; + private int totalPages; + private int totalResults; + private int totalPageItems; + private int startPage; + private int startIndex; + private SearchTemplateNode[] results; + + + public int getItemsPerPage() + { + return itemsPerPage; + } + + /*package*/ void setItemsPerPage(int itemsPerPage) + { + this.itemsPerPage = itemsPerPage; + } + + public TemplateNode[] getResults() + { + return results; + } + + /*package*/ void setResults(SearchTemplateNode[] results) + { + this.results = results; + } + + public int getStartIndex() + { + return startIndex; + } + + /*package*/ void setStartIndex(int startIndex) + { + this.startIndex = startIndex; + } + + public int getStartPage() + { + return startPage; + } + + /*package*/ void setStartPage(int startPage) + { + this.startPage = startPage; + } + + public int getTotalPageItems() + { + return totalPageItems; + } + + /*package*/ void setTotalPageItems(int totalPageItems) + { + this.totalPageItems = totalPageItems; + } + + public int getTotalPages() + { + return totalPages; + } + + /*package*/ void setTotalPages(int totalPages) + { + this.totalPages = totalPages; + } + + public int getTotalResults() + { + return totalResults; + } + + /*package*/ void setTotalResults(int totalResults) + { + this.totalResults = totalResults; + } + + public String getSearchTerms() + { + return searchTerms; + } + + /*package*/ void setSearchTerms(String searchTerms) + { + this.searchTerms = searchTerms; + } + + public Locale getLocale() + { + return locale; + } + + /** + * @return XML 1.0 Language Identification + */ + public String getLocaleId() + { + return locale.toString().replace('_', '-'); + } + + /*package*/ void setLocale(Locale locale) + { + this.locale = locale; + } + + public String getId() + { + if (id == null) + { + id = GUID.generate(); + } + return id; + } + } + + /** + * Search result row template node + */ + public class SearchTemplateNode extends TemplateNode + { + protected final static String URL = "/api/node/content/{0}/{1}/{2}/{3}"; + + private static final long serialVersionUID = -1791913270786140012L; + private float score; + + /** + * Construct + */ + public SearchTemplateNode(NodeRef nodeRef, float score) + { + super(nodeRef, serviceRegistry, KeywordSearch.this.imageResolver.getImageResolver()); + this.score = score; + } + + /** + * Gets the result row score + */ + public float getScore() + { + return score; + } + + @Override + public String getUrl() + { + return MessageFormat.format(URL, new Object[] { + getNodeRef().getStoreRef().getProtocol(), + getNodeRef().getStoreRef().getIdentifier(), + getNodeRef().getId(), + URLEncoder.encode(getName()) } ); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Login.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Login.java new file mode 100644 index 00000000000..7c4bd1a6ee9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Login.java @@ -0,0 +1,72 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Login and establish a ticket + * + * @author davidc + */ +public class Login extends AbstractLoginBean +{ + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + protected Map executeImpl(WebScriptRequest req, Status status) + { + // extract username and password + String username = req.getParameter("u"); + if (username == null || username.length() == 0) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Username not specified"); + } + String password = req.getParameter("pw"); + if (password == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Password not specified"); + } + + try + { + return login(username, password); + } + catch(WebScriptException e) + { + status.setCode(e.getStatus()); + status.setMessage(e.getMessage()); + status.setRedirect(true); + return null; + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginPost.java new file mode 100644 index 00000000000..32d60054664 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginPost.java @@ -0,0 +1,100 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Post based login script + */ +public class LoginPost extends AbstractLoginBean +{ + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + protected Map executeImpl(WebScriptRequest req, Status status) + { + // Extract user and password from JSON POST + Content c = req.getContent(); + if (c == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Missing POST body."); + } + + // TODO accept xml type. + + // extract username and password from JSON object + JSONObject json; + try + { + json = new JSONObject(c.getContent()); + String username = json.getString("username"); + String password = json.getString("password"); + + if (username == null || username.length() == 0) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Username not specified"); + } + + if (password == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Password not specified"); + } + + try + { + return login(username, password); + } + catch(WebScriptException e) + { + status.setCode(e.getStatus()); + status.setMessage(e.getMessage()); + status.setRedirect(true); + return null; + } + } + catch (JSONException jErr) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Unable to parse JSON POST body: " + jErr.getMessage()); + } + catch (IOException ioErr) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, + "Unable to retrieve POST body: " + ioErr.getMessage()); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java new file mode 100644 index 00000000000..7915500cca5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginTicket.java @@ -0,0 +1,103 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.TicketComponent; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Login Ticket + * + * @author davidc + */ +public class LoginTicket extends DeclarativeWebScript +{ + // dependencies + private TicketComponent ticketComponent; + + /** + * @param ticketComponent TicketComponent + */ + public void setTicketComponent(TicketComponent ticketComponent) + { + this.ticketComponent = ticketComponent; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + // retrieve ticket from request and current ticket + String ticket = req.getExtensionPath(); + if (ticket == null || ticket.length() == 0) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Ticket not specified"); + } + + // construct model for ticket + Map model = new HashMap(1, 1.0f); + model.put("ticket", ticket); + + try + { + String ticketUser = ticketComponent.validateTicket(ticket); + + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + + // do not go any further if tickets are different + // or the user is not fully authenticated + if (currentUser == null || !currentUser.equals(ticketUser)) + { + status.setRedirect(true); + status.setCode(HttpServletResponse.SC_NOT_FOUND); + status.setMessage("Ticket not found"); + } + } + catch (AuthenticationException e) + { + status.setRedirect(true); + status.setCode(HttpServletResponse.SC_NOT_FOUND); + status.setMessage("Ticket not found"); + } + + return model; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java new file mode 100644 index 00000000000..19503c7a2ef --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/LoginTicketDelete.java @@ -0,0 +1,115 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.TicketComponent; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Delete Login Ticket + * + * @author davidc + */ +public class LoginTicketDelete extends DeclarativeWebScript +{ + // dependencies + private AuthenticationService authenticationService; + private TicketComponent ticketComponent; + + /** + * @param ticketComponent TicketComponent + */ + public void setTicketComponent(TicketComponent ticketComponent) + { + this.ticketComponent = ticketComponent; + } + + /** + * @param authenticationService AuthenticationService + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + // retrieve ticket from request and current ticket + String ticket = req.getExtensionPath(); + if (ticket == null || ticket.length() == 0) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Ticket not specified"); + } + + // construct model for ticket + Map model = new HashMap(1, 1.0f); + model.put("ticket", ticket); + + try + { + String ticketUser = ticketComponent.validateTicket(ticket); + + // do not go any further if tickets are different + if (!AuthenticationUtil.getFullyAuthenticatedUser().equals(ticketUser)) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND); + status.setMessage("Ticket not found"); + } + else + { + // delete the ticket + authenticationService.invalidateTicket(ticket); + status.setMessage("Deleted Ticket " + ticket); + } + } + catch(AuthenticationException e) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND); + status.setMessage("Ticket not found"); + } + + status.setRedirect(true); + return model; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/SearchEngines.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/SearchEngines.java new file mode 100644 index 00000000000..9169c5476b5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/SearchEngines.java @@ -0,0 +1,211 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.extensions.config.Config; +import org.springframework.extensions.config.ConfigService; +import org.springframework.extensions.surf.util.I18NUtil; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.web.scripts.config.OpenSearchConfigElement; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * List of (server-side) registered Search Engines + * + * @author davidc + */ +public class SearchEngines extends DeclarativeWebScript +{ + // url argument values + public static final String URL_ARG_DESCRIPTION = "description"; + public static final String URL_ARG_TEMPLATE = "template"; + public static final String URL_ARG_ALL = "all"; + + // Logger + private static final Log logger = LogFactory.getLog(SearchEngines.class); + + // dependencies + protected ConfigService configService; + protected SearchProxy searchProxy; + + /** + * @param configService ConfigService + */ + public void setConfigService(ConfigService configService) + { + this.configService = configService; + } + + /** + * @param searchProxy SearchProxy + */ + public void setSearchProxy(SearchProxy searchProxy) + { + this.searchProxy = searchProxy; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @SuppressWarnings("deprecation") + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + String urlType = req.getParameter("type"); + if (urlType == null || urlType.length() == 0) + { + urlType = URL_ARG_DESCRIPTION; + } + else if (!urlType.equals(URL_ARG_DESCRIPTION) && !urlType.equals(URL_ARG_TEMPLATE) && !urlType.equals(URL_ARG_ALL)) + { + urlType = URL_ARG_DESCRIPTION; + } + + // + // retrieve open search engines configuration + // + + Set urls = getUrls(urlType); + Map model = new HashMap(7, 1.0f); + model.put("urltype", urlType); + model.put("engines", urls); + return model; + } + + /** + * Retrieve registered search engines + * + * @return set of search engines + */ + private Set getUrls(String urlType) + { + if (logger.isDebugEnabled()) + logger.debug("Search Engine parameters: urltype=" + urlType); + + Set urls = new HashSet(); + Config config = configService.getConfig("OpenSearch"); + + OpenSearchConfigElement searchConfig = (OpenSearchConfigElement)config.getConfigElement(OpenSearchConfigElement.CONFIG_ELEMENT_ID); + for (OpenSearchConfigElement.EngineConfig engineConfig : searchConfig.getEngines()) + { + Map engineUrls = engineConfig.getUrls(); + for (Map.Entry engineUrl : engineUrls.entrySet()) + { + String type = engineUrl.getKey(); + String url = searchProxy.createUrl(engineConfig, type); + + if ((urlType.equals(URL_ARG_ALL)) || + (urlType.equals(URL_ARG_DESCRIPTION) && type.equals(MimetypeMap.MIMETYPE_OPENSEARCH_DESCRIPTION)) || + (urlType.equals(URL_ARG_TEMPLATE) && !type.equals(MimetypeMap.MIMETYPE_OPENSEARCH_DESCRIPTION))) + { + String label = engineConfig.getLabel(); + String labelId = engineConfig.getLabelId(); + if (labelId != null && labelId.length() > 0) + { + String i18nLabel = I18NUtil.getMessage(labelId); + if (i18nLabel == null && label == null) + { + label = (i18nLabel == null) ? "$$" + labelId + "$$" : i18nLabel; + } + } + urls.add(new UrlTemplate(label, type, url)); + } + + // TODO: Extract URL templates from OpenSearch description + else if (urlType.equals(URL_ARG_TEMPLATE) && + type.equals(MimetypeMap.MIMETYPE_OPENSEARCH_DESCRIPTION)) + { + } + } + } + + if (logger.isDebugEnabled()) + logger.debug("Retrieved " + urls.size() + " engine registrations"); + + return urls; + } + + /** + * Model object for representing a registered search engine + */ + public static class UrlTemplate + { + private String type; + private String label; + private String url; + private UrlTemplate engine; + + public UrlTemplate(String label, String type, String url) + { + this.label = label; + this.type = type; + this.url = url; + this.engine = null; + } + + public UrlTemplate(String label, String type, String url, UrlTemplate engine) + { + this(label, type, url); + this.engine = engine; + } + + public String getLabel() + { + return label; + } + + public String getType() + { + return type; + } + + public String getUrl() + { + return url; + } + + public String getUrlType() + { + return (type.equals(MimetypeMap.MIMETYPE_OPENSEARCH_DESCRIPTION) ? "description" : "template"); + } + + public UrlTemplate getEngine() + { + return engine; + } + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/SearchProxy.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/SearchProxy.java new file mode 100644 index 00000000000..f0eaf4473af --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/SearchProxy.java @@ -0,0 +1,342 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URLConnection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.web.scripts.config.OpenSearchConfigElement; +import org.alfresco.repo.web.scripts.config.OpenSearchConfigElement.EngineConfig; +import org.alfresco.repo.web.scripts.config.OpenSearchConfigElement.ProxyConfig; +import org.alfresco.web.app.servlet.HTTPProxy; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.XPath; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.config.Config; +import org.springframework.extensions.config.ConfigService; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.FormatRegistry; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; + + +/** + * Alfresco OpenSearch Proxy Service + * + * Provides the ability to submit a request to a registered search engine + * via the Alfresco server. + * + * @author davidc + */ +public class SearchProxy extends AbstractWebScript implements InitializingBean +{ + // Logger + private static final Log logger = LogFactory.getLog(SearchProxy.class); + + // dependencies + protected FormatRegistry formatRegistry; + protected ConfigService configService; + protected OpenSearchConfigElement searchConfig; + protected String proxyPath; + + /** + * @param formatRegistry FormatRegistry + */ + public void setFormatRegistry(FormatRegistry formatRegistry) + { + this.formatRegistry = formatRegistry; + } + + /** + * @param configService ConfigService + */ + public void setConfigService(ConfigService configService) + { + this.configService = configService; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception + { + Config config = configService.getConfig("OpenSearch"); + searchConfig = (OpenSearchConfigElement)config.getConfigElement(OpenSearchConfigElement.CONFIG_ELEMENT_ID); + if (searchConfig == null) + { + throw new WebScriptException("OpenSearch configuration not found"); + } + ProxyConfig proxyConfig = searchConfig.getProxy(); + if (proxyConfig == null) + { + throw new WebScriptException("OpenSearch proxy configuration not found"); + } + proxyPath = proxyConfig.getUrl(); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScript#execute(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) + throws IOException + { + String extensionPath = req.getExtensionPath(); + String[] extensionPaths = extensionPath.split("/"); + if (extensionPaths.length != 2) + { + throw new WebScriptException("OpenSearch engine has not been specified as /{engine}/{format}"); + } + + // retrieve search engine configuration + String engine = extensionPaths[0]; + EngineConfig engineConfig = searchConfig.getEngine(engine); + if (engineConfig == null) + { + throw new WebScriptException("OpenSearch engine '" + engine + "' does not exist"); + } + + // retrieve engine url as specified by format + String format = extensionPaths[1]; + String mimetype = formatRegistry.getMimeType(null, format); + if (mimetype == null) + { + throw new WebScriptException("Format '" + format + "' does not map to a registered mimetype"); + } + Map engineUrls = engineConfig.getUrls(); + String engineUrl = engineUrls.get(mimetype); + if (engineUrl == null) + { + throw new WebScriptException("Url mimetype '" + mimetype + "' does not exist for engine '" + engine + "'"); + } + + // replace template url arguments with actual arguments specified on request + int engineUrlArgIdx = engineUrl.indexOf("?"); + if (engineUrlArgIdx != -1) + { + engineUrl = engineUrl.substring(0, engineUrlArgIdx); + } + if (req.getQueryString() != null) + { + engineUrl += "?" + req.getQueryString(); + } + + if (logger.isDebugEnabled()) + logger.debug("Mapping engine '" + engine + "' (mimetype '" + mimetype + "') to url '" + engineUrl + "'"); + + // issue request against search engine + // NOTE: This web script must be executed in a HTTP servlet environment + if (!(res.getRuntime() instanceof WebScriptServletRuntime)) + { + throw new WebScriptException("Search Proxy must be executed in HTTP Servlet environment"); + } + + HttpServletResponse servletRes = WebScriptServletRuntime.getHttpServletResponse(res); + SearchEngineHttpProxy proxy = new SearchEngineHttpProxy(req.getServerPath() + req.getServiceContextPath(), + engine, engineUrl, servletRes, Collections.singletonMap("User-Agent", req.getHeader("User-Agent"))); + proxy.service(); + } + + /** + * OpenSearch HTTPProxy + * + * This proxy remaps OpenSearch links (e.g. previous, next) found in search results. + * + * @author davidc + */ + private class SearchEngineHttpProxy extends HTTPProxy + { + private final static String ATOM_NS_URI = "http://www.w3.org/2005/Atom"; + private final static String ATOM_NS_PREFIX = "atom"; + private final static String ATOM_LINK_XPATH = "atom:link[@rel=\"first\" or @rel=\"last\" or @rel=\"next\" or @rel=\"previous\" or @rel=\"self\" or @rel=\"alternate\"]"; + private String engine; + private String rootPath; + private Map headers; + + /** + * Construct + * + * @param rootPath String + * @param engine String + * @param engineUrl String + * @param response HttpServletResponse + * @param headers request headers + * @throws MalformedURLException + */ + public SearchEngineHttpProxy(String rootPath, String engine, String engineUrl, HttpServletResponse response, Map headers) + throws MalformedURLException + { + super(engineUrl.startsWith("/") ? rootPath + engineUrl : engineUrl, response); + this.engine = engine; + this.rootPath = rootPath; + this.headers = headers; + } + + /* (non-Javadoc) + * @see org.alfresco.web.app.servlet.HTTPProxy#setRequestHeaders(java.net.URLConnection) + */ + @Override + protected void setRequestHeaders(URLConnection urlConnection) + { + if (headers != null) + { + for (Map.Entry entry: headers.entrySet()) + { + urlConnection.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.app.servlet.HTTPProxy#writeResponse(java.io.InputStream, java.io.OutputStream) + */ + @Override + protected void writeResponse(InputStream input, OutputStream output) + throws IOException + { + if (response.getContentType().startsWith(MimetypeMap.MIMETYPE_ATOM) || + response.getContentType().startsWith(MimetypeMap.MIMETYPE_RSS)) + { + // Only post-process ATOM and RSS feeds + // Replace all navigation links with "proxied" versions + SAXReader reader = new SAXReader(); + try + { + Document document = reader.read(input); + Element rootElement = document.getRootElement(); + + XPath xpath = rootElement.createXPath(ATOM_LINK_XPATH); + Map uris = new HashMap(); + uris.put(ATOM_NS_PREFIX, ATOM_NS_URI); + xpath.setNamespaceURIs(uris); + + List nodes = xpath.selectNodes(rootElement); + Iterator iter = nodes.iterator(); + while (iter.hasNext()) + { + Element element = (Element)iter.next(); + Attribute hrefAttr = element.attribute("href"); + String mimetype = element.attributeValue("type"); + if (mimetype == null || mimetype.length() == 0) + { + mimetype = MimetypeMap.MIMETYPE_HTML; + } + String url = createUrl(engine, hrefAttr.getValue(), mimetype); + if (url.startsWith("/")) + { + url = rootPath + url; + } + hrefAttr.setValue(url); + } + + OutputFormat outputFormat = OutputFormat.createPrettyPrint(); + XMLWriter writer = new XMLWriter(output, outputFormat); + writer.write(rootElement); + writer.flush(); + } + catch(DocumentException e) + { + throw new IOException(e.toString()); + } + } + else + { + super.writeResponse(input, output); + } + } + } + + /** + * Construct a "proxied" search engine url + * + * @param engine engine name (as identified by ) + * @param mimetype url to proxy (as identified by mimetype) + * @return "proxied" url + */ + public String createUrl(OpenSearchConfigElement.EngineConfig engine, String mimetype) + { + Map urls = engine.getUrls(); + String url = urls.get(mimetype); + if (url != null) + { + String proxy = engine.getProxy(); + if (proxy != null && !mimetype.equals(MimetypeMap.MIMETYPE_OPENSEARCH_DESCRIPTION)) + { + url = createUrl(proxy, url, mimetype); + } + } + return url; + } + + /** + * Construct a "proxied" search engine url + * + * @param engine engine name (as identified by ) + * @param url engine url + * @param mimetype mimetype of url + * @return "proxied" url + */ + public String createUrl(String engine, String url, String mimetype) + { + String format = formatRegistry.getFormat(null, mimetype); + if (format == null) + { + throw new WebScriptException("Mimetype '" + mimetype + "' is not registered."); + } + + String proxyUrl = null; + int argIdx = url.indexOf("?"); + if (argIdx == -1) + { + proxyUrl = proxyPath + "/" + engine + "/" + format; + } + else + { + proxyUrl = proxyPath + "/" + engine + "/" + format + url.substring(argIdx); + } + return proxyUrl; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Touch.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Touch.java new file mode 100644 index 00000000000..dbdb782f6d0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bean/Touch.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.bean; + +import java.io.IOException; + +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * WebScript java backed bean implementation - to simple return a STATUS_OK message + * as a touch point for SSO authentication mechanisms on the web-tier. Such as NTLM. + * + * @author Kevin Roast + */ +public class Touch extends AbstractWebScript +{ + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScript#execute(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + res.setStatus(Status.STATUS_OK); + res.getWriter().close(); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/AbstractBlogWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/AbstractBlogWebScript.java new file mode 100644 index 00000000000..fa0e0d157a1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/AbstractBlogWebScript.java @@ -0,0 +1,311 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs; + +import java.io.IOException; +import java.util.Map; + +import org.alfresco.repo.activities.post.lookup.PostLookup; +import org.alfresco.repo.blog.BlogServiceImpl; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.model.Repository; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.blog.BlogService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONStringer; +import org.json.JSONWriter; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Neil Mc Erlean + * @since 4.0 + */ +public abstract class AbstractBlogWebScript extends DeclarativeWebScript +{ + // Various common parameter strings in the blog webscripts. + protected static final String CONTAINER = "container"; + protected static final String CONTENT = "content"; + protected static final String DATA = "data"; + protected static final String DRAFT = "draft"; + protected static final String EXTERNAL_BLOG_CONFIG = "externalBlogConfig"; + protected static final String POST = "post"; + protected static final String ITEM = "item"; + protected static final String NODE = "node"; + protected static final String PAGE = "page"; + protected static final String SITE = "site"; + protected static final String TAGS = "tags"; + protected static final String TITLE = "title"; + + private static Log logger = LogFactory.getLog(AbstractBlogWebScript.class); + + // Injected services + protected Repository repository; + protected BlogService blogService; + protected NodeService nodeService; + protected SiteService siteService; + protected ActivityService activityService; + + //TODO Remove this after full refactor + protected ServiceRegistry services; + + public void setServiceRegistry(ServiceRegistry services) + { + this.services = services; + } + + public void setRepository(Repository repository) + { + this.repository = repository; + } + + public void setBlogService(BlogService blogService) + { + this.blogService = blogService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + /** + * Generates an activity entry for the discussion item + * + * @param event One of created, updated, deleted + * @param blog Either post or reply + * @param site site + * @param req request + * @param json json + * @param nodeRef NodeRef + */ + protected void addActivityEntry(String event, BlogPostInfo blog, + SiteInfo site, WebScriptRequest req, JSONObject json, NodeRef nodeRef) + { + // We can only add activities against a site + if (site == null) + { + logger.info("Unable to add activity entry for blog " + event + " as no site given"); + return; + } + + // What page is this for? + String page = req.getParameter("page"); + if (page == null && json != null) + { + if (json.containsKey("page")) + { + page = (String)json.get("page"); + } + } + if (page == null) + { + // Default + page = "blog-postview"; + } + if (page.indexOf('?') == -1) + { + page += "?postId=" + blog.getSystemName(); + } + + // Get the title + String title = blog.getTitle(); + + try + { + JSONWriter jsonWriter = new JSONStringer() + .object() + .key(TITLE).value(title) + .key(PAGE).value(page); + + if (nodeRef != null) + { + // ALF-10182: the nodeRef needs to be included in the activity + // post to ensure read permissions are respected. + jsonWriter.key(PostLookup.JSON_NODEREF).value(nodeRef.toString()); + } + + String data = jsonWriter.endObject().toString(); + + activityService.postActivity( + "org.alfresco.blog.post-" + event, + site.getShortName(), + "blog", data); + } + catch(Exception e) + { + // Warn, but carry on + logger.warn("Error adding blog post " + event + " to activities feed", e); + } + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Parse the JSON, if supplied + JSONObject json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + JSONParser parser = new JSONParser(); + try + { + json = (JSONObject)parser.parse(req.getContent().getContent()); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch (ParseException pe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage()); + } + } + + + // Did they request it by node reference or site? + NodeRef nodeRef = null; + SiteInfo site = null; + BlogPostInfo blog = null; + + if (templateVars.containsKey("site")) + { + // Site, and Optionally Blog Post + String siteName = templateVars.get("site"); + site = siteService.getSite(siteName); + if (site == null) + { + String error = "Could not find site: " + siteName; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Did they give a blog post name too? + if (templateVars.containsKey("path")) + { + String name = templateVars.get("path"); + blog = blogService.getBlogPost(siteName, name); + + if (blog == null) + { + String error = "Could not find blog '" + name + "' for site '" + + site.getShortName() + "'"; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + nodeRef = blog.getNodeRef(); + } + else + { + // The NodeRef is the container (if it exists) + if (siteService.hasContainer(siteName, BlogServiceImpl.BLOG_COMPONENT)) + { + nodeRef = siteService.getContainer(siteName, BlogServiceImpl.BLOG_COMPONENT); + } + } + } + else if (templateVars.containsKey("store_type") && + templateVars.containsKey("store_id") && + templateVars.containsKey("id")) + { + // NodeRef, should be a Blog Post + StoreRef store = new StoreRef( + templateVars.get("store_type"), + templateVars.get("store_id")); + + nodeRef = new NodeRef(store, templateVars.get("id")); + if (! nodeService.exists(nodeRef)) + { + String error = "Could not find node: " + nodeRef; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Try to build the appropriate object for it + blog = blogService.getForNodeRef(nodeRef); + + // See if it's actually attached to a site + if (blog != null) + { + NodeRef container = blog.getContainerNodeRef(); + if (container != null) + { + NodeRef maybeSite = nodeService.getPrimaryParent(container).getParentRef(); + if (maybeSite != null) + { + // Try to make it a site, will return Null if it isn't one + site = siteService.getSite(maybeSite); + } + } + } + } + else + { + String error = "Unsupported template parameters found"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Have the real work done + return executeImpl(site, nodeRef, blog, req, json, status, cache); + } + + protected abstract Map executeImpl(SiteInfo site, + NodeRef nodeRef, BlogPostInfo blog, WebScriptRequest req, + JSONObject json, Status status, Cache cache); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogLibJs.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogLibJs.java new file mode 100644 index 00000000000..4759c7fce20 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogLibJs.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.BlogIntegrationModel; +import org.alfresco.service.namespace.QName; +import org.json.simple.JSONObject; + +/** + * This class is a port of a previous JavaScript library. + * + * @author Neil Mc Erlean (based on previous JavaScript) + * @since 4.0 + * @deprecated This class should not be extended or reused as it may be refactored. + */ +public class BlogLibJs +{ + /** + * Fetches the blog properties from the json object and adds them to an array + * using the correct property names as indexes. + */ + public static Map getBlogPropertiesArray(JSONObject json) + { + Map arr = new HashMap(); + + putJSONEntryInMap(json, arr, "blogType", BlogIntegrationModel.PROP_BLOG_IMPLEMENTATION); + putJSONEntryInMap(json, arr, "blogId", BlogIntegrationModel.PROP_ID); + putJSONEntryInMap(json, arr, "blogName", BlogIntegrationModel.PROP_NAME); + putJSONEntryInMap(json, arr, "blogDescription", BlogIntegrationModel.PROP_DESCRIPTION); + putJSONEntryInMap(json, arr, "blogUrl", BlogIntegrationModel.PROP_URL); + putJSONEntryInMap(json, arr, "username", BlogIntegrationModel.PROP_USER_NAME); + putJSONEntryInMap(json, arr, "password", BlogIntegrationModel.PROP_PASSWORD); + return arr; + } + + private static void putJSONEntryInMap(JSONObject json, + Map arr, String jsonKey, QName mapKey) + { + if (json.containsKey(jsonKey)) + { + arr.put(mapKey, (Serializable)json.get(jsonKey)); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogPostLibJs.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogPostLibJs.java new file mode 100644 index 00000000000..7e818eeee46 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogPostLibJs.java @@ -0,0 +1,132 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.BlogIntegrationModel; +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * This class is a port of a previous JavaScript library. + * + * @author Neil Mc Erlean (based on previous JavaScript) + * @since 4.0 + */ +public class BlogPostLibJs +{ + //FIXME It will be refactored when the other services are ported from JavaScript to Java. + + /** + * Checks whether a blog configuration is available + * This should at some point also check whether the configuration is enabled. + * + * @param node the node that should be checked. Will check all parents if + * the node itself doesn't contain a configuration. + * @return {boolean} whether a configuration could be found. + */ + public static boolean hasExternalBlogConfiguration(NodeRef node, ServiceRegistry services) + { + if (node == null) + { + return false; + } + else if (services.getNodeService().hasAspect(node, BlogIntegrationModel.ASPECT_BLOG_DETAILS)) + { + return true; + } + else + { + return hasExternalBlogConfiguration(services.getNodeService().getPrimaryParent(node).getParentRef(), services); + } + } + + public static Map getBlogPostData(NodeRef node, ServiceRegistry services) + { + Map data = new HashMap(); + data.put("node", node); + String creator = (String)services.getNodeService().getProperty(node, ContentModel.PROP_CREATOR); + //ALF-18527 + NodeRef person = services.getPersonService().getPersonOrNull(creator); + if (person != null) + { + data.put("author", person); + } + + data.put("commentCount", CommentsLibJs.getCommentsCount(node, services)); + + // is the post published + Serializable published = services.getNodeService().getProperty(node, ContentModel.PROP_PUBLISHED); + boolean isPublished = published != null; + if (isPublished) + { + data.put("releasedDate", published); + } + + // draft + data.put("isDraft", !isPublished); + + // set the isUpdated flag + Date updatedDate = (Date) services.getNodeService().getProperty(node, ContentModel.PROP_UPDATED); + boolean isUpdated = updatedDate != null; + data.put("isUpdated", isUpdated); + if (isUpdated) + { + data.put("updatedDate", updatedDate); + } + + // fetch standard created/modified dates + data.put("createdDate", services.getNodeService().getProperty(node, ContentModel.PROP_CREATED)); + data.put("modifiedDate", services.getNodeService().getProperty(node, ContentModel.PROP_MODIFIED)); + + // does the external post require an update? + Date lastUpdate = (Date) services.getNodeService().getProperty(node, BlogIntegrationModel.PROP_LAST_UPDATE); + if (isPublished && lastUpdate != null) + { + // we either use the release or updated date + Date modifiedDate = (Date) data.get("releasedDate"); + + if (isUpdated) + { + modifiedDate = (Date) data.get("updatedDate"); + } + data.put("outOfDate", modifiedDate.getTime() - lastUpdate.getTime() > 5000L); + } + else + { + data.put("outOfDate", false); + } + + data.put("tags", services.getTaggingService().getTags(node)); + + return data; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/CommentsLibJs.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/CommentsLibJs.java new file mode 100644 index 00000000000..1da8eea028f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/CommentsLibJs.java @@ -0,0 +1,99 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.ForumModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * This class is a port of a previous JavaScript library used by the blog webscript containers. + * + * @author Neil Mc Erlean (based on existing JavaScript code) + * @since 4.0 + */ +class CommentsLibJs +{ + // TODO It will likely be refactored into the Blog REST API class framework. + + private static final String COMMENTS_TOPIC_NAME = "Comments"; + + public static int getCommentsCount(NodeRef node, ServiceRegistry services) + { + return getComments(node, services).size(); + } + + /** + * Returns all comment nodes for a given node. + * @return an array of comments. + */ + public static List getComments(NodeRef node, ServiceRegistry services) + { + List result = new ArrayList(); + + NodeRef commentsFolder = getCommentsFolder(node, services); + if (commentsFolder != null) + { + List children = services.getNodeService().getChildAssocs(commentsFolder, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + if (!children.isEmpty()) + { + result = children; + } + } + + return result; + } + + /** + * Returns the folder that contains all the comments. + * + * We currently use the fm:discussable aspect where we + * add a "Comments" topic to it. + */ + public static NodeRef getCommentsFolder(NodeRef node, ServiceRegistry services) + { + //FIXME These methods are from the original JavaScript. Should use the (soon to arrive) CommentService. + NodeRef result = null; + if (services.getNodeService().hasAspect(node, ForumModel.ASPECT_DISCUSSABLE)) + { + List forumFolders = services.getNodeService().getChildAssocs(node, ForumModel.ASSOC_DISCUSSION, RegexQNamePattern.MATCH_ALL); + // The JavaScript was retrieving the first child under this child-assoc so we'll do the same. + NodeRef forumFolder = forumFolders.get(0).getChildRef(); + + List topicFolder = services.getNodeService().getChildAssocs(forumFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, COMMENTS_TOPIC_NAME)); + result = topicFolder.isEmpty() ? null : topicFolder.get(0).getChildRef(); + } + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogGet.java new file mode 100644 index 00000000000..6cf3bd97322 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogGet.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.blog; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the blog.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogGet extends AbstractBlogWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef containerNodeRef, + BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + if (blog != null) + { + // They appear to have supplied a blog post itself... + // Oh well, let's hope for the best! + } + + if (containerNodeRef == null && site != null) + { + // They want to know about a blog container on a + // site where nothing has lazy-created the container + // Give them info on the site for now, should be close enough! + containerNodeRef = site.getNodeRef(); + } + + if (containerNodeRef == null) + { + // Looks like they've asked for something that isn't there + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Blog Not Found"); + } + + // Build the response + // (For now, we just supply the noderef, but when we have a + // proper blog details object we'll use that) + Map model = new HashMap(); + model.put(ITEM, containerNodeRef); + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogPut.java new file mode 100644 index 00000000000..e3a01ebd2b5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogPut.java @@ -0,0 +1,104 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.blog; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.BlogIntegrationModel; +import org.alfresco.repo.blog.BlogServiceImpl; +import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript; +import org.alfresco.repo.web.scripts.blogs.BlogLibJs; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.namespace.QName; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the blog.put web script. + * + * TODO Push most of the logic from this into the BlogService + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPut extends AbstractBlogWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef containerNodeRef, + BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + if (blog != null) + { + // They appear to have supplied a blog post itself... + // Oh well, let's hope for the best! + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Blog post should not be updated via this web script."); + } + + if (site != null && containerNodeRef == null) + { + // Force the lazy creation + // This is a bit icky, but it'll have to do for now... + containerNodeRef = siteService.createContainer( + site.getShortName(), BlogServiceImpl.BLOG_COMPONENT, null, null); + } + + // Do the work + updateBlog(containerNodeRef, json); + + // Record it as done + Map model = new HashMap(); + model.put("item", containerNodeRef); + + return model; + } + + /** + * Creates a post inside the passed forum node. + */ + @SuppressWarnings("deprecation") + private void updateBlog(NodeRef node, JSONObject json) + { + Map arr = BlogLibJs.getBlogPropertiesArray(json); + + if (nodeService.hasAspect(node, BlogIntegrationModel.ASPECT_BLOG_DETAILS)) + { + Map properties = nodeService.getProperties(node); + properties.putAll(arr); + nodeService.setProperties(node, properties); + } + else + { + nodeService.addAspect(node, BlogIntegrationModel.ASPECT_BLOG_DETAILS, arr); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostDelete.java new file mode 100644 index 00000000000..2c492101db5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostDelete.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.post; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; + +import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the blog-posts.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostDelete extends AbstractBlogWebScript +{ + protected static final String MSG_BLOG_DELETED = "blog-post.msg.deleted"; + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + + if (blog == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Blog Post Not Found"); + } + + // TODO Get this from the BlogPostInfo Object + final boolean isDraftBlogPost = blogService.isDraftBlogPost(blog.getNodeRef()); + + // Have it deleted + blogService.deleteBlogPost(blog); + + // If we're in a site, and it isn't a draft, add an activity + if (site != null && !isDraftBlogPost) + { + addActivityEntry("deleted", blog, site, req, json, nodeRef); + } + + // Report it as deleted + Map model = new HashMap(); + String message = rb.getString(MSG_BLOG_DELETED); + model.put("message",MessageFormat.format(message, blog.getNodeRef())); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostGet.java new file mode 100644 index 00000000000..1e3f0f81518 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostGet.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.post; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript; +import org.alfresco.repo.web.scripts.blogs.BlogPostLibJs; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the blog-posts.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostGet extends AbstractBlogWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + if (blog == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Blog Post Not Found"); + } + + // Build the response + Map model = new HashMap(); + + // TODO Fetch this from the BlogPostInfo object + NodeRef node = blog.getNodeRef(); + Map item = BlogPostLibJs.getBlogPostData(node, services); + model.put(ITEM, item); + model.put(POST, blog); + + model.put("externalBlogConfig", BlogPostLibJs.hasExternalBlogConfiguration(node, services)); + + int contentLength = -1; + String arg = req.getParameter("contentLength"); + if (arg != null) + { + try + { + contentLength = Integer.parseInt(arg); + } + catch (NumberFormatException ignored) + { + // Intentionally empty + } + } + + model.put("contentLength", contentLength); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/AbstractGetBlogWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/AbstractGetBlogWebScript.java new file mode 100644 index 00000000000..2f9a8ec0771 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/AbstractGetBlogWebScript.java @@ -0,0 +1,229 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript; +import org.alfresco.repo.web.scripts.blogs.BlogPostLibJs; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.blog.BlogService.RangedDateProperty; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.util.Pair; +import org.alfresco.util.ScriptPagingDetails; +import org.alfresco.util.UrlUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONObject; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public abstract class AbstractGetBlogWebScript extends AbstractBlogWebScript +{ + private static final Log log = LogFactory.getLog(AbstractGetBlogWebScript.class); + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nonSiteContainer, + BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + Map model = new HashMap(); + + // process additional parameters. + PagingRequest pagingReq = parsePagingParams(req); + pagingReq.setRequestTotalCountMax(pagingReq.getSkipCount() + pagingReq.getRequestTotalCountMax()); + + // begin and end date. + // Legacy note: these dates are URL query parameters in int form. + Date fromDate = parseDateParam(req, "fromDate"); + Date toDate = parseDateParam(req, "toDate"); + + String tag = req.getParameter("tag"); + if (tag == null || tag.length() == 0) + { + tag = null; + } + else + { + // Tags can be full unicode strings, so decode + tag = URLDecoder.decode(tag); + } + + // One webscript (blog-posts-new.get) uses a 'numdays' parameter as a 'fromDate'. + // This is a hacky solution to this special case. FIXME + if (this.getClass().equals(BlogPostsNewGet.class)) + { + // Default is for 'now' minus seven days. + final long oneDayInMilliseconds = 24 * 60 * 60 * 1000; + final long sevenDaysInMilliseconds = 7 * oneDayInMilliseconds; + fromDate = new Date(System.currentTimeMillis() - sevenDaysInMilliseconds); + + // But if there is a numdays parameter then that changes the fromDate + String numDays = req.getServiceMatch().getTemplateVars().get("numdays"); + if (numDays != null) + { + Integer numDaysInt = Integer.parseInt(numDays); + fromDate = new Date(System.currentTimeMillis() - (numDaysInt * oneDayInMilliseconds)); + } + } + + // Fetch and assign the data + PagingResults blogPostList = + getBlogPostList(site, nonSiteContainer, fromDate, toDate, tag, pagingReq); + + // We need the container for various bits + NodeRef container = nonSiteContainer; + if(container == null) + { + // Container mustn't exist yet + // Fake it with the site for permissions checking reasons + container = site.getNodeRef(); + } + + if (log.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Retrieved ").append(blogPostList.getPage().size()).append(" blog posts in page."); + log.debug(msg.toString()); + } + + createFtlModel(req, model, container, pagingReq, blogPostList); + + return model; + } + + protected void createFtlModel(WebScriptRequest req, Map model, NodeRef node, PagingRequest pagingReq, + PagingResults blogPostList) + { + Map blogPostsData = new HashMap(); + + final Pair totalResultCount = blogPostList.getTotalResultCount(); + int total = blogPostList.getPage().size(); + if (totalResultCount != null && totalResultCount.getFirst() != null) + { + total = totalResultCount.getFirst(); + } + //FIXME What to do? null + blogPostsData.put("total", total); + blogPostsData.put("pageSize", pagingReq.getMaxItems()); + blogPostsData.put("startIndex", pagingReq.getSkipCount()); + blogPostsData.put("itemCount", blogPostList.getPage().size()); + + if (total == pagingReq.getRequestTotalCountMax()) + { + blogPostsData.put("totalRecordsUpper", true); + } + else + { + blogPostsData.put("totalRecordsUpper", false); + } + + List> blogPostDataSets = new ArrayList>(blogPostList.getPage().size()); + for (BlogPostInfo postInfo : blogPostList.getPage()) + { + Map data = BlogPostLibJs.getBlogPostData(postInfo.getNodeRef(), services); + blogPostDataSets.add(data); + } + blogPostsData.put("items", blogPostDataSets); + + model.put("data", blogPostsData); + + // fetch the contentLength param + String contentLengthStr = req.getParameter("contentLength"); + int contentLength = contentLengthStr == null ? -1 : Integer.parseInt(contentLengthStr); + model.put("contentLength", contentLength); + + // assign the blog node + model.put("blog", node); + model.put("externalBlogConfig", BlogPostLibJs.hasExternalBlogConfiguration(node, services)); + } + + private PagingRequest parsePagingParams(WebScriptRequest req) + { + return ScriptPagingDetails.buildPagingRequest(req, 1000); + } + + private Date parseDateParam(WebScriptRequest req, String paramName) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + String dateStr = templateVars.get(paramName); + if(dateStr == null) + { + // Try on the parameters instead + dateStr = req.getParameter(paramName); + } + + // Parse if available + Date result = null; + if (dateStr != null) + { + result = new Date(Long.parseLong(dateStr)); + } + return result; + } + + + /** + * Fetches all posts of the given blog + */ + private PagingResults getBlogPostList(SiteInfo site, NodeRef nonSiteContainer, + Date fromDate, Date toDate, String tag, PagingRequest pagingReq) + { + // Currently we only support CannedQuery-based gets without tags: + if (tag == null || tag.trim().isEmpty()) + { + return getBlogResultsImpl(site, nonSiteContainer, fromDate, toDate, pagingReq); + } + else + { + RangedDateProperty dateRange = new RangedDateProperty(fromDate, toDate, ContentModel.PROP_CREATED); + if(site != null) + { + return blogService.findBlogPosts(site.getShortName(), dateRange, tag, pagingReq); + } + else + { + return blogService.findBlogPosts(nonSiteContainer, dateRange, tag, pagingReq); + } + } + } + + protected abstract PagingResults getBlogResultsImpl( + SiteInfo site, NodeRef nonSiteContainer, Date fromDate, Date toDate, PagingRequest pagingReq); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsGet.java new file mode 100644 index 00000000000..8d38f0bdc8d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsGet.java @@ -0,0 +1,61 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.Date; + +import org.alfresco.query.EmptyPagingResults; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * This class is the controller for the blog-posts.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostsGet extends AbstractGetBlogWebScript +{ + @SuppressWarnings("deprecation") + @Override + protected PagingResults getBlogResultsImpl( + SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq) + { + // As it uses deprecated methods, this bit can be a bit hacky... + if (node == null) + { + // Site based request, but no container exists yet + return new EmptyPagingResults(); + } + + // This intentionally uses the deprecated method in the foundation service. + // In fact the method is there specifically for this class. + return blogService.getMyDraftsAndAllPublished(node, fromDate, toDate, pagingReq); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyDraftsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyDraftsGet.java new file mode 100644 index 00000000000..3152c8390ca --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyDraftsGet.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.Date; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * This class is the controller for the blog-posts-mydrafts.get web script. + * Based on the original JavaScript webscript controller + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostsMyDraftsGet extends AbstractGetBlogWebScript +{ + @Override + protected PagingResults getBlogResultsImpl(SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq) + { + String user = AuthenticationUtil.getFullyAuthenticatedUser(); + if(site != null) + { + return blogService.getDrafts(site.getShortName(), user, pagingReq); + } + else + { + return blogService.getDrafts(node, user, pagingReq); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyPublishedGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyPublishedGet.java new file mode 100644 index 00000000000..d226a12fc4d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyPublishedGet.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.Date; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * This class is the controller for the blog-posts-mypublished.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostsMyPublishedGet extends AbstractGetBlogWebScript +{ + @Override + protected PagingResults getBlogResultsImpl( + SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq) + { + String fullyAuthenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser(); + if(site != null) + { + return blogService.getPublished(site.getShortName(), fromDate, toDate, fullyAuthenticatedUser, pagingReq); + } + else + { + return blogService.getPublished(node, fromDate, toDate, fullyAuthenticatedUser, pagingReq); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsNewGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsNewGet.java new file mode 100644 index 00000000000..2166418dcfe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsNewGet.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.Date; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * This class is the controller for the blog-posts-publishedext.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostsNewGet extends AbstractGetBlogWebScript +{ + @Override + protected PagingResults getBlogResultsImpl( + SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq) + { + if(site != null) + { + return blogService.getPublished(site.getShortName(), fromDate, toDate, null, pagingReq); + } + else + { + return blogService.getPublished(node, fromDate, toDate, null, pagingReq); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPerMonthGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPerMonthGet.java new file mode 100644 index 00000000000..7b45916e9c8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPerMonthGet.java @@ -0,0 +1,170 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the blog-posts-mypublished.get web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostsPerMonthGet extends AbstractGetBlogWebScript +{ + @Override + protected PagingResults getBlogResultsImpl( + SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq) + { + if(site != null) + { + return blogService.getPublished(site.getShortName(), fromDate, toDate, null, pagingReq); + } + else + { + return blogService.getPublished(node, fromDate, toDate, null, pagingReq); + } + } + + @Override + protected void createFtlModel(WebScriptRequest req, Map model, NodeRef node, PagingRequest pagingReq, PagingResults blogPostList) + { + model.put(DATA, getBlogPostMonths(blogPostList)); + } + + + /** + * Ported from blog-posts-per-month.get.js + */ + @SuppressWarnings("deprecation") + private Date getBeginOfMonthDate(Date date) + { + //TODO These date processing methods are copied almost verbatim from JavaScript to preserve behaviour. + // However they should be updated to use java.util.Calendar as the current implementation assumes a Gregorian calendar. + Calendar calendar = Calendar.getInstance(); + calendar.set(date.getYear(), date.getMonth(), 1); + return calendar.getTime(); + } + + /** + * Returns the date representing the last second of a month (23:59:59) + * Ported from blog-posts-per-month.get.js + */ + @SuppressWarnings("deprecation") + private Date getEndOfMonthDate(Date date) + { + Calendar calendar = Calendar.getInstance(); + calendar.set(date.getYear(), date.getMonth(), date.getDay()); + // In Gregorian calendar, this would be 31 for January, 30 for March, 28 or 29 for February. + int lastDayOfSpecifiedMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); + calendar.set(date.getYear(), date.getMonth(), lastDayOfSpecifiedMonth, 23, 59, 59); + + return calendar.getTime(); + } + + /** + * Create an object containing information about the month specified by date. + * Ported from blog-posts-per-month.get.js + */ + @SuppressWarnings("deprecation") + private Map getMonthDataObject(Date date) + { + Map data = new HashMap(); + data.put("year", date.getYear() + 1900); + data.put("month", date.getMonth()); + data.put("firstPostInMonth", date); + data.put("beginOfMonth", getBeginOfMonthDate(date)); + data.put("endOfMonth", getEndOfMonthDate(date)); + data.put("count", 1); + + return data; + } + + /** + * Fetches data for each month for which posts exist, plus the count of each. + * Note: If no posts could be found, this method will return the current month + * but with a count of posts equals zero. + * Ported from blog-posts-per-month.get.js + */ + @SuppressWarnings("deprecation") + private List> getBlogPostMonths(PagingResults nodes) + { + // will hold the months information + List> data = new ArrayList>(); + + // do we have posts? + if (!nodes.getPage().isEmpty()) + { + int currYear = -1; + int currMonth = -1; + Map currData = null; + for (int x = 0; x < nodes.getPage().size(); x++) + { + NodeRef node = nodes.getPage().get(x).getNodeRef(); + Date date = (Date) nodeService.getProperty(node, ContentModel.PROP_PUBLISHED); + + // is this a new month? + if (currYear != date.getYear() + 1900 || currMonth != date.getMonth()) + { + currYear = date.getYear() + 1900; + currMonth = date.getMonth(); + currData = getMonthDataObject(date); + data.add(currData); + } + // otherwise just increment the counter + else + { + Object countObj = currData.get("count"); + Integer countInt = countObj == null ? 0 : (Integer)countObj; + + currData.put("count", countInt + 1); + } + } + } + // if not, add the current month with count = 0 + else + { + Map emptyData = getMonthDataObject(new Date()); + emptyData.put("count", 0); + data.add(emptyData); + } + + return data; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPost.java new file mode 100644 index 00000000000..0cd2843f7d0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPost.java @@ -0,0 +1,304 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.blogs.posts; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript; +import org.alfresco.repo.web.scripts.blogs.BlogPostLibJs; +import org.alfresco.service.cmr.blog.BlogPostInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.tagging.TaggingService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the blog-posts.post web script. + * + * @author Neil Mc Erlean (based on existing JavaScript webscript controllers) + * @since 4.0 + */ +public class BlogPostsPost extends AbstractBlogWebScript +{ + private static final Log log = LogFactory.getLog(BlogPostsPost.class); + + // Injected services + private TaggingService taggingService; + + public void setTaggingService(TaggingService taggingService) + { + this.taggingService = taggingService; + } + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + Map model = new HashMap(); + + // If they're doing Path Based rather than Site Based, ensure + // that the Container is a Tag Scope + if(site == null && nodeRef != null) + { + ensureTagScope(nodeRef); + } + + // Have the Blog Post created + JsonParams jsonPostParams = parsePostParams(json); + BlogPostInfo post = createBlogPost(jsonPostParams, site, nodeRef); + + Map blogPostData = BlogPostLibJs.getBlogPostData(post.getNodeRef(), services); + model.put(ITEM, blogPostData); + model.put(EXTERNAL_BLOG_CONFIG, BlogPostLibJs.hasExternalBlogConfiguration(nodeRef, services)); + + boolean isDraft = blogPostData.get("isDraft") != null && + ((Boolean)blogPostData.get("isDraft")).booleanValue(); + if (jsonPostParams.getSite() != null && + jsonPostParams.getContainer() != null && + jsonPostParams.getPage() != null && + !isDraft) + { + addActivityEntry("created", post, site, req, json, post.getNodeRef()); + } + + return model; + } + + private JsonParams parsePostParams(JSONObject json) + { + JsonParams result = new JsonParams(); + if (json.containsKey(TITLE)) + { + result.setTitle((String)json.get(TITLE)); + } + if (json.containsKey(CONTENT)) + { + result.setContent((String)json.get(CONTENT)); + } + if (json.containsKey(DRAFT)) + { + Object draft = json.get(DRAFT); + if (draft instanceof Boolean) + { + result.setIsDraft((Boolean)draft); + } + else + { + result.setIsDraft( Boolean.parseBoolean((String)draft) ); + } + } + + // If there are no tags, this is a java.lang.String "". + // If there are any tags, it's a JSONArray of strings. One or more. + if (json.containsKey(TAGS)) + { + Object tagsObj = json.get(TAGS); + List tags = new ArrayList(); + if (tagsObj instanceof JSONArray) + { + JSONArray tagsJsonArray = (JSONArray)tagsObj; + for (int i = 0; i < tagsJsonArray.size(); i++) + { + tags.add( (String)tagsJsonArray.get(i) ); + } + } + else + { + tags.add(tagsObj.toString()); + } + result.setTags(tags); + } + if (json.containsKey(SITE)) + { + result.setSite((String)json.get(SITE)); + } + if (json.containsKey(PAGE)) + { + result.setPage((String)json.get(PAGE)); + } + if (json.containsKey(CONTAINER)) + { + result.setContainer((String)json.get(CONTAINER)); + } + + return result; + } + + /** + * Taken from JS + * @param node NodeRef + */ + private void ensureTagScope(NodeRef node) + { + if (!taggingService.isTagScope(node)) + { + taggingService.addTagScope(node); + } + + + // also check the parent (the site!) + NodeRef parent = nodeService.getPrimaryParent(node).getParentRef(); + if (!taggingService.isTagScope(parent)) + { + taggingService.addTagScope(parent); + } + } + + /** + * Creates a blog post + */ + private BlogPostInfo createBlogPost(JsonParams jsonParams, SiteInfo site, NodeRef blogNode) + { + String titleParam = jsonParams.getTitle() == null ? "" : jsonParams.getTitle(); + String contentParam = jsonParams.getContent() == null ? "" : jsonParams.getContent(); + boolean isDraftParam = jsonParams.getIsDraft(); + + if (log.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Creating blog-post '").append(titleParam).append("'"); + if (isDraftParam) + { + msg.append(" DRAFT"); + } + log.debug(msg.toString()); + } + + List tagsParam = new ArrayList(); + if (jsonParams.getTags() != null) + { + tagsParam.addAll(jsonParams.getTags()); + } + + BlogPostInfo newPostNode; + if(site != null) + { + newPostNode = blogService.createBlogPost( + site.getShortName(), titleParam, contentParam, isDraftParam); + } + else + { + newPostNode = blogService.createBlogPost( + blogNode, titleParam, contentParam, isDraftParam); + } + + // Ignore empty string tags + List nonEmptyTags = new ArrayList(); + for (String tag : tagsParam) + { + if (!tag.trim().isEmpty()) + { + nonEmptyTags.add(tag); + } + } + if (!nonEmptyTags.isEmpty()) + { + taggingService.setTags(newPostNode.getNodeRef(), nonEmptyTags); + } + + return newPostNode; + } + + /** + * A simple POJO class for the parsed JSON from the POST body. + */ + class JsonParams + { + private String title; + private String content; + private boolean isDraft = false; + private List tags; + private String site; + private String container; + private String page; + + public String getTitle() + { + return title; + } + public void setTitle(String title) + { + this.title = title; + } + public String getContent() + { + return content; + } + public void setContent(String content) + { + this.content = content; + } + public boolean getIsDraft() + { + return isDraft; + } + public void setIsDraft(boolean isDraft) + { + this.isDraft = isDraft; + } + public List getTags() + { + return tags; + } + public void setTags(List tags) + { + this.tags = tags; + } + public String getSite() + { + return site; + } + public void setSite(String site) + { + this.site = site; + } + public String getContainer() + { + return container; + } + public void setContainer(String container) + { + this.container = container; + } + public String getPage() + { + return page; + } + public void setPage(String page) + { + this.page = page; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/AbstractBulkFileSystemImportWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/AbstractBulkFileSystemImportWebScript.java new file mode 100644 index 00000000000..3e98a9d142f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/AbstractBulkFileSystemImportWebScript.java @@ -0,0 +1,222 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.bulkimport; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Date; + +import org.alfresco.repo.bulkimport.BulkFilesystemImporter; +import org.alfresco.repo.model.Repository; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.WebScriptException; + +/** + * contains common fields and methods for the import web scripts. + */ +public class AbstractBulkFileSystemImportWebScript extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(BulkFilesystemImporter.class); + + protected static final String WEB_SCRIPT_URI_BULK_FILESYSTEM_IMPORT_STATUS = "/bulkfsimport/status"; + + protected static final String PARAMETER_TARGET_NODEREF = "targetNodeRef"; + protected static final String PARAMETER_TARGET_PATH = "targetPath"; + + protected static final String COMPANY_HOME_NAME = "Company Home"; + protected static final String COMPANY_HOME_PATH = "/" + COMPANY_HOME_NAME; + + // Web scripts parameters (common) + protected static final String PARAMETER_REPLACE_EXISTING = "replaceExisting"; + protected static final String PARAMETER_EXISTING_FILE_MODE = "existingFileMode"; + protected static final String PARAMETER_VALUE_REPLACE_EXISTING = "replaceExisting"; + protected static final String PARAMETER_SOURCE_DIRECTORY = "sourceDirectory"; + protected static final String PARAMETER_DISABLE_RULES = "disableRules"; + protected static final String PARAMETER_VALUE_DISABLE_RULES = "disableRules"; + + protected static final String IMPORT_ALREADY_IN_PROGRESS_MODEL_KEY = "importInProgress"; + protected static final String IMPORT_ALREADY_IN_PROGRESS_ERROR_KEY ="bfsit.error.importAlreadyInProgress"; + + protected static final String PARAMETER_BATCH_SIZE = "batchSize"; + protected static final String PARAMETER_NUM_THREADS = "numThreads"; + + protected FileFolderService fileFolderService; + protected Repository repository; + + protected volatile boolean importInProgress; + + protected NodeRef getTargetNodeRef(String targetNodeRefStr, String targetPath) throws FileNotFoundException + { + NodeRef targetNodeRef; + + if (targetNodeRefStr == null || targetNodeRefStr.trim().length() == 0) + { + if (targetPath == null || targetPath.trim().length() == 0) + { + throw new WebScriptException("Error: neither parameter '" + PARAMETER_TARGET_NODEREF + + "' nor parameter '" + PARAMETER_TARGET_PATH + + "' was provided, but at least one is required !"); + } + targetNodeRef = convertPathToNodeRef(targetPath.trim()); + } + else + { + targetNodeRef = new NodeRef(targetNodeRefStr.trim()); + } + + return targetNodeRef; + } + + protected NodeRef convertPathToNodeRef(String targetPath) throws FileNotFoundException + { + NodeRef result = null; + NodeRef companyHome = repository.getCompanyHome(); + String cleanTargetPath = targetPath.replaceAll("/+", "/"); + + if (cleanTargetPath.startsWith(COMPANY_HOME_PATH)) + cleanTargetPath = cleanTargetPath.substring(COMPANY_HOME_PATH.length()); + + if (cleanTargetPath.startsWith("/")) + cleanTargetPath = cleanTargetPath.substring(1); + + if (cleanTargetPath.endsWith("/")) + cleanTargetPath = cleanTargetPath.substring(0, cleanTargetPath.length() - 1); + + if (cleanTargetPath.length() == 0) + result = companyHome; + else + { + FileInfo info = fileFolderService.resolveNamePath(companyHome, Arrays.asList(cleanTargetPath.split("/"))); + if(info == null) + throw new WebScriptException("could not determine NodeRef for path :'"+cleanTargetPath+"'"); + + result = info.getNodeRef(); + } + + return(result); + } + + protected String buildTextMessage(Throwable t) + { + StringBuffer result = new StringBuffer(); + String timeOfFailure = (new Date()).toString(); + String hostName = null; + String ipAddress = null; + + try + { + hostName = InetAddress.getLocalHost().getHostName(); + ipAddress = InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException uhe) + { + hostName = "unknown"; + ipAddress = "unknown"; + } + + result.append("\nTime of failure: " + timeOfFailure); + result.append("\nHost where failure occurred: " + hostName + " (" + ipAddress + ")"); + + if (t != null) + { + result.append("\nRoot exception:"); + result.append(renderExceptionStackAsText(t)); + } + else + { + result.append("\nNo exception was provided."); + } + + return(result.toString()); + } + + private String renderExceptionStackAsText( Throwable t) + { + StringBuffer result = new StringBuffer(); + + if (t != null) + { + String message = t.getMessage(); + Throwable cause = t.getCause(); + + if (cause != null) + { + result.append(renderExceptionStackAsText(cause)); + result.append("\nWrapped by:"); + } + + if (message == null) + { + message = ""; + } + + result.append("\n"); + result.append(t.getClass().getName()); + result.append(": "); + result.append(message); + result.append("\n"); + result.append(renderStackTraceElements(t.getStackTrace())); + } + + return(result.toString()); + } + + private String renderStackTraceElements(StackTraceElement[] elements) + { + StringBuffer result = new StringBuffer(); + + if (elements != null) + { + for (int i = 0; i < elements.length; i++) + { + result.append("\tat " + elements[i].toString() + "\n"); + } + } + + return(result.toString()); + } + + // boilerplate setters + + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + public void setRepository(Repository repository) + { + this.repository = repository; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/BulkFilesystemImportStatusWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/BulkFilesystemImportStatusWebScript.java new file mode 100644 index 00000000000..11b8fa95ace --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/BulkFilesystemImportStatusWebScript.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.bulkimport; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.bulkimport.BulkFilesystemImporter; +import org.alfresco.service.cmr.admin.RepoUsage.LicenseMode; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.service.license.LicenseDescriptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web Script class that provides status information on the bulk filesystem import process. + * + * @since 4.0 + */ +public class BulkFilesystemImportStatusWebScript extends DeclarativeWebScript +{ + private final static Log logger = LogFactory.getLog(BulkFilesystemImportStatusWebScript.class); + + // Output parameters (for Freemarker) + private final static String RESULT_IMPORT_STATUS = "importStatus"; + private final static String IS_ENTERPRISE = "isEnterprise"; + + // Attributes + private BulkFilesystemImporter bulkImporter; + private DescriptorService descriptorService; + + public void setBulkImporter(BulkFilesystemImporter bulkImporter) + { + this.bulkImporter = bulkImporter; + } + + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + /** + * @see org.springframework.extensions.webscripts.DeclarativeWebScript#executeImpl(WebScriptRequest, Status, Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest request, Status status, Cache cache) + { + Map result = new HashMap(); + + cache.setNeverCache(true); + + LicenseDescriptor licenseDescriptor = descriptorService.getLicenseDescriptor(); + boolean isEnterprise = (licenseDescriptor == null ? false : (licenseDescriptor.getLicenseMode() == LicenseMode.ENTERPRISE)); + + result.put(IS_ENTERPRISE, Boolean.valueOf(isEnterprise)); + result.put(RESULT_IMPORT_STATUS, bulkImporter.getStatus()); + + return(result); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/copy/BulkFilesystemImportWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/copy/BulkFilesystemImportWebScript.java new file mode 100644 index 00000000000..ef7461193df --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/bulkimport/copy/BulkFilesystemImportWebScript.java @@ -0,0 +1,238 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.bulkimport.copy; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.bulkimport.BulkImportParameters; +import org.alfresco.repo.bulkimport.NodeImporter; +import org.alfresco.repo.bulkimport.impl.MultiThreadedBulkFilesystemImporter; +import org.alfresco.repo.bulkimport.impl.StreamingNodeImporterFactory; +import org.alfresco.repo.web.scripts.bulkimport.AbstractBulkFileSystemImportWebScript; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web Script class that invokes a BulkFilesystemImporter implementation. + * + * @since 4.0 + */ +public class BulkFilesystemImportWebScript extends AbstractBulkFileSystemImportWebScript +{ + private MultiThreadedBulkFilesystemImporter bulkImporter; + private StreamingNodeImporterFactory nodeImporterFactory; + + public void setBulkImporter(MultiThreadedBulkFilesystemImporter bulkImporter) + { + this.bulkImporter = bulkImporter; + } + + public void setNodeImporterFactory(StreamingNodeImporterFactory nodeImporterFactory) + { + this.nodeImporterFactory = nodeImporterFactory; + } + + /** + * @see org.springframework.extensions.webscripts.DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status, org.springframework.extensions.webscripts.Cache) + */ + @Override + protected Map executeImpl(final WebScriptRequest request, final Status status, final Cache cache) + { + Map model = new HashMap(); + String targetNodeRefStr = null; + String targetPath = null; + String sourceDirectoryStr = null; + @Deprecated String replaceExistingStr = null; + String existingFileModeStr = null; + String batchSizeStr = null; + String numThreadsStr = null; + String disableRulesStr = null; + + cache.setNeverCache(true); + + try + { + if(!bulkImporter.getStatus().inProgress()) + { + NodeRef targetNodeRef = null; + File sourceDirectory = null; + boolean replaceExisting = false; + BulkImportParameters.ExistingFileMode existingFileMode = null; + int batchSize = bulkImporter.getDefaultBatchSize(); + int numThreads = bulkImporter.getDefaultNumThreads(); + boolean disableRules = false; + + // Retrieve, validate and convert parameters + targetNodeRefStr = request.getParameter(PARAMETER_TARGET_NODEREF); + targetPath = request.getParameter(PARAMETER_TARGET_PATH); + sourceDirectoryStr = request.getParameter(PARAMETER_SOURCE_DIRECTORY); + replaceExistingStr = request.getParameter(PARAMETER_REPLACE_EXISTING); + existingFileModeStr = request.getParameter(PARAMETER_EXISTING_FILE_MODE); + + batchSizeStr = request.getParameter(PARAMETER_BATCH_SIZE); + numThreadsStr = request.getParameter(PARAMETER_NUM_THREADS); + disableRulesStr = request.getParameter(PARAMETER_DISABLE_RULES); + + targetNodeRef = getTargetNodeRef(targetNodeRefStr, targetPath); + + if (sourceDirectoryStr == null || sourceDirectoryStr.trim().length() == 0) + { + throw new RuntimeException("Error: mandatory parameter '" + PARAMETER_SOURCE_DIRECTORY + "' was not provided."); + } + + sourceDirectory = new File(sourceDirectoryStr.trim()); + + if (replaceExistingStr != null && existingFileModeStr != null) + { + // Check that we haven't had both the deprecated and new (existingFileMode) + // parameters supplied. + throw new IllegalStateException( + String.format("Only one of these parameters may be used, not both: %s, %s", + PARAMETER_REPLACE_EXISTING, + PARAMETER_EXISTING_FILE_MODE)); + } + + if (replaceExistingStr != null && replaceExistingStr.trim().length() > 0) + { + replaceExisting = PARAMETER_VALUE_REPLACE_EXISTING.equals(replaceExistingStr); + } + + if (existingFileModeStr != null && existingFileModeStr.trim().length() > 0) + { + existingFileMode = BulkImportParameters.ExistingFileMode.valueOf(existingFileModeStr); + } + + if (disableRulesStr != null && disableRulesStr.trim().length() > 0) + { + disableRules = PARAMETER_VALUE_DISABLE_RULES.equals(disableRulesStr); + } + + // Initiate the import + NodeImporter nodeImporter = nodeImporterFactory.getNodeImporter(sourceDirectory); + BulkImportParameters bulkImportParameters = new BulkImportParameters(); + + if (numThreadsStr != null && numThreadsStr.trim().length() > 0) + { + try + { + numThreads = Integer.parseInt(numThreadsStr); + if(numThreads < 1) + { + throw new RuntimeException("Error: parameter '" + PARAMETER_NUM_THREADS + "' must be an integer > 0."); + } + bulkImportParameters.setNumThreads(numThreads); + } + catch(NumberFormatException e) + { + throw new RuntimeException("Error: parameter '" + PARAMETER_NUM_THREADS + "' must be an integer > 0."); + } + } + + if (batchSizeStr != null && batchSizeStr.trim().length() > 0) + { + try + { + batchSize = Integer.parseInt(batchSizeStr); + if(batchSize < 1) + { + throw new RuntimeException("Error: parameter '" + PARAMETER_BATCH_SIZE + "' must be an integer > 0."); + } + bulkImportParameters.setBatchSize(batchSize); + } + catch(NumberFormatException e) + { + throw new RuntimeException("Error: parameter '" + PARAMETER_BATCH_SIZE + "' must be an integer > 0."); + } + } + + if (existingFileMode != null) + { + bulkImportParameters.setExistingFileMode(existingFileMode); + } + else + { + // Fall back to the old/deprecated way. + bulkImportParameters.setReplaceExisting(replaceExisting); + } + + bulkImportParameters.setTarget(targetNodeRef); + bulkImportParameters.setDisableRulesService(disableRules); + + bulkImporter.asyncBulkImport(bulkImportParameters, nodeImporter); + + // ACE-3047 fix, since bulk import is started asynchronously there is a chance that client + // will get into the status page before import is actually started. + // In this case wrong information (for previous import) will be displayed. + // So lets ensure that import started before redirecting client to status page. + int i = 0; + while (!bulkImporter.getStatus().inProgress() && i < 10) + { + Thread.sleep(100); + i++; + } + + // redirect to the status Web Script + status.setCode(Status.STATUS_MOVED_TEMPORARILY); + status.setRedirect(true); + status.setLocation(request.getServiceContextPath() + WEB_SCRIPT_URI_BULK_FILESYSTEM_IMPORT_STATUS); + } + else + { + model.put(IMPORT_ALREADY_IN_PROGRESS_MODEL_KEY, I18NUtil.getMessage(IMPORT_ALREADY_IN_PROGRESS_ERROR_KEY)); + } + } + catch (WebScriptException wse) + { + status.setCode(Status.STATUS_BAD_REQUEST, wse.getMessage()); + status.setRedirect(true); + } + catch (FileNotFoundException fnfe) + { + status.setCode(Status.STATUS_BAD_REQUEST,"The repository path '" + targetPath + "' does not exist !"); + status.setRedirect(true); + } + catch(IllegalArgumentException iae) + { + status.setCode(Status.STATUS_BAD_REQUEST,iae.getMessage()); + status.setRedirect(true); + } + catch (Throwable t) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, buildTextMessage(t), t); + } + + return model; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/AbstractCalendarListingWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/AbstractCalendarListingWebScript.java new file mode 100644 index 00000000000..d9be3a030e9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/AbstractCalendarListingWebScript.java @@ -0,0 +1,277 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.calendar.CalendarModel; +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarRecurrenceHelper; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO8601DateFormat; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +/** + * This class provides functionality common across the webscripts + * which list events. + * + * @author Nick Burch + * @since 4.0 + */ +public abstract class AbstractCalendarListingWebScript extends AbstractCalendarWebScript +{ + protected static final String RESULT_EVENT = "event"; + protected static final String RESULT_NAME = "name"; + protected static final String RESULT_TITLE = "title"; + protected static final String RESULT_START = "start"; + protected static final String RESULT_END = "end"; + + /** + * Returns a Comparator for (re-)sorting events, typically used after + * expanding out recurring instances. + */ + protected static Comparator> getEventDetailsSorter() + { + return new Comparator>() + { + public int compare(Map resultA, + Map resultB) + { + DateTimeFormatter fmtNoTz = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + DateTimeFormatter fmtTz = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"); + + String startA = (String)resultA.get(RESULT_START); + String startB = (String)resultB.get(RESULT_START); + + startA = startA.replace("Z", "+00:00"); + startB = startB.replace("Z", "+00:00"); + + //check and parse iso8601 date without time zone (All day events are stripped of time zone) + DateTime sa = startA.length()>23?fmtTz.parseDateTime(startA):fmtNoTz.parseDateTime(startA); + DateTime sb = startB.length()>23?fmtTz.parseDateTime(startB):fmtNoTz.parseDateTime(startB); + + int cmp = sa.compareTo(sb); + if (cmp == 0) + { + String endA = (String)resultA.get(RESULT_END); + String endB = (String)resultB.get(RESULT_END); + + DateTime ea = endA.length()>23?fmtTz.parseDateTime(endA):fmtNoTz.parseDateTime(endA); + DateTime eb = endB.length()>23?fmtTz.parseDateTime(endB):fmtNoTz.parseDateTime(endB); + + cmp = ea.compareTo(eb); + if (cmp == 0) + { + String nameA = (String)resultA.get(RESULT_NAME); + String nameB = (String)resultB.get(RESULT_NAME); + return nameA.compareTo(nameB); + } + return cmp; + } + return cmp; + } + }; + } + + /** + * Do what's needed for recurring events. + * + * @return If dates have been tweaked, and a sort may be required + */ + protected boolean handleRecurring(CalendarEntry entry, Map entryResult, + List> allResults, Date from, Date until, boolean repeatingFirstOnly) + { + if (entry.getRecurrenceRule() == null) + { + // Nothing to do + return false; + } + + // If no date is given, start looking for occurrences from the event itself + if (from == null) + { + from = entry.getStart(); + } + + // Do we need to limit ourselves? + // Should we limit ourselves? + if (!repeatingFirstOnly) + { + if (until == null) + { + // If no end date was given, only allow repeating instances + // for next 60 days, to keep the list sane + // (It's normally only used for a month view anyway) + Calendar c = Calendar.getInstance(); + c.setTime(from); + c.add(Calendar.DATE, 60); + until = c.getTime(); + } + } + + // How long is it? + long duration = entry.getEnd().getTime() - entry.getStart().getTime(); + + // if some instances were deleted from series ignore them + Set childNodeTypeQNames = new HashSet(); + childNodeTypeQNames.add(CalendarModel.TYPE_IGNORE_EVENT); + List ignoreEventList = nodeService.getChildAssocs(entry.getNodeRef(), childNodeTypeQNames); + Set ignoredDates = new HashSet(); + for (ChildAssociationRef ignoreEvent : ignoreEventList) + { + NodeRef nodeRef = ignoreEvent.getChildRef(); + Date ignoredDate = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_IGNORE_EVENT_DATE); + ignoredDates.add(ignoredDate); + } + + // Get it's recurring instances + List dates = CalendarRecurrenceHelper.getRecurrencesOnOrAfter( + entry, from, until, repeatingFirstOnly, ignoredDates); + if (dates == null) + { + dates = new ArrayList(); + } + + // Add on the original event time itself if needed + if (entry.getStart().getTime() >= from.getTime()) + { + if (dates.size() == 0 || dates.get(0).getTime() != entry.getStart().getTime()) + { + // Original event is after the start time, and not on the recurring list + dates.add(0, entry.getStart()); + } + } + + // If we got no dates, then no recurrences in the period so zap + if (dates.size() == 0) + { + allResults.remove(entryResult); + return false; // Remains sorted despite delete + } + + // if some instances were updated + SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd"); + childNodeTypeQNames = new HashSet(); + childNodeTypeQNames.add(CalendarModel.TYPE_UPDATED_EVENT); + List updatedEventList = nodeService.getChildAssocs(entry.getNodeRef(), childNodeTypeQNames); + Map updatedDates = new HashMap(); + for (ChildAssociationRef updatedEvent : updatedEventList) + { + NodeRef nodeRef = updatedEvent.getChildRef(); + Date updatedDate = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_EVENT_DATE); + Date newStart = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_START); + Date newEnd = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_END); + String newWhere = (String) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_WHERE); + String newWhat = (String) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_WHAT); + updatedDates.put(fmt.format(updatedDate), new Date[] { newStart, newEnd }); + updatedDates.put(fmt.format(updatedDate).toString() + "where", newWhere); + updatedDates.put(fmt.format(updatedDate).toString() + "what", newWhat); + } + + // first occurrence can be edited as separate event + Date liveEntry = dates.get(0); + + // If first result only, alter title and finish + if (repeatingFirstOnly) + { + entryResult.put(RESULT_TITLE, entry.getTitle() + " (Repeating)"); + + updateRepeating(entry, updatedDates, entryResult, duration, fmt, liveEntry); + return true; // Date has been changed + } + else + { + // Otherwise generate one entry per extra date + for (int i = 1; i < dates.size(); i++) + { + // Clone the properties + Map newResult = new HashMap(entryResult); + + Date extra = dates.get(i); + + updateRepeating(entry, updatedDates, newResult, duration, fmt, extra); + + // Save as a new event + allResults.add(newResult); + } + + updateRepeating(entry, updatedDates, entryResult, duration, fmt, liveEntry); + } + + // TODO Skip ignored instances + + // New dates have been added + return true; + } + + private void updateRepeatingStartEnd(Date newStart, long duration, Map result) + { + Date newEnd = new Date(newStart.getTime() + duration); + result.put(RESULT_START, ISO8601DateFormat.format(newStart)); + result.put(RESULT_END, ISO8601DateFormat.format(newEnd)); + String legacyDateFormat = "yyyy-MM-dd"; + SimpleDateFormat ldf = new SimpleDateFormat(legacyDateFormat); + String legacyTimeFormat ="HH:mm"; + SimpleDateFormat ltf = new SimpleDateFormat(legacyTimeFormat); + result.put("legacyDateFrom", ldf.format(newStart)); + result.put("legacyTimeFrom", ltf.format(newStart)); + result.put("legacyDateTo", ldf.format(newEnd)); + result.put("legacyTimeTo", ltf.format(newEnd)); + } + + private void updateRepeating(CalendarEntry entry, Map updatedDates, Map entryResult, long duration, SimpleDateFormat fmt, Date date) + { + if (updatedDates.keySet().contains(fmt.format(date))) + { + // there is day that was edited + Date[] newValues = (Date[]) updatedDates.get(fmt.format(date)); + long newDuration = newValues[1].getTime() - newValues[0].getTime(); + + entryResult.put(RESULT_TITLE, (String) updatedDates.get(fmt.format(date).toString() + "what")); + entryResult.put("where", (String) updatedDates.get(fmt.format(date).toString() + "where")); + + updateRepeatingStartEnd(newValues[0], newDuration, entryResult); + } + else + { + // Update entry + updateRepeatingStartEnd(date, duration, entryResult); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/AbstractCalendarWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/AbstractCalendarWebScript.java new file mode 100644 index 00000000000..330f2bf7b1f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/AbstractCalendarWebScript.java @@ -0,0 +1,563 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.calendar.CalendarModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.alfresco.util.ISO8601DateFormat; +import org.alfresco.util.ScriptPagingDetails; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.json.JSONException; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.json.JSONWriter; + +/** + * @author Nick Burch + * @since 4.0 + */ +public abstract class AbstractCalendarWebScript extends DeclarativeWebScript +{ + private static final String ALL_DAY_DATETIME_PATTERN = "yyyy-MM-dd'T00:00:00.000'"; + + private static final DateTimeFormatter ALL_DAY_DATETIME_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd'T00:00:00.000'"); + + public static final String CALENDAR_SERVICE_ACTIVITY_APP_NAME = "calendar"; + + protected static final String MSG_EVENT_NOT_FOUND = "calendar.err.event.not.found"; + protected static final String MSG_INVALID_JSON = "calendar.err.invalid.json"; + + protected static final String PARAM_TIMEZONE = "timeZone"; + protected static final String PARAM_START_AT = "startAt"; + protected static final String PARAM_END_AT = "endAt"; + protected static final String PARAM_ISO8601 = "iso8601"; + + private static Log logger = LogFactory.getLog(AbstractCalendarWebScript.class); + + /** + * When no maximum or paging info is given, what should we use? + */ + protected static final int MAX_QUERY_ENTRY_COUNT = 1000; + + // Injected services + protected NodeService nodeService; + protected SiteService siteService; + protected ActivityService activityService; + protected CalendarService calendarService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setCalendarService(CalendarService calendarService) + { + this.calendarService = calendarService; + } + + /** + * Gets the date from the String, trying the various formats + * (New and Legacy) until one works... + */ + protected Date parseDate(String date) + { + // Is there one at all? + if (date == null || date.length() == 0) + { + return null; + } + + // Today's Date - special case + if (date.equalsIgnoreCase("NOW")) + { + // We want all of today, so go back to midnight + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTime(); + } + + // Try as ISO8601 + try + { + return ISO8601DateFormat.parse(date); + } + catch (Exception e) {} + + // Try YYYY/MM/DD + SimpleDateFormat slashtime = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + SimpleDateFormat slash = new SimpleDateFormat("yyyy/MM/dd"); + try + { + return slashtime.parse(date); + } + catch (ParseException e) {} + try + { + return slash.parse(date); + } + catch (ParseException e) {} + + // Try YYYY-MM-DD + SimpleDateFormat dashtime = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + SimpleDateFormat dash = new SimpleDateFormat("yyyy-MM-dd"); + try + { + return dashtime.parse(date); + } + catch (ParseException e) {} + try + { + return dash.parse(date); + } + catch (ParseException e) {} + + // We don't know what it is, object + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid date '" + date + "'"); + } + + /** + * Extracts the Start and End details, along with the All Day flag + * from the JSON, and returns if the event is all day or not + */ + protected boolean extractDates(CalendarEntry entry, JSONObject json) throws JSONException + { + boolean isAllDay = false; + if (json.containsKey("allday")) + { + isAllDay = true; + } + + if (json.containsKey(PARAM_START_AT) && json.containsKey(PARAM_END_AT)) + { + // New style ISO8601 based dates and times + Object startAtO = json.get(PARAM_START_AT); + Object endAtO = json.get(PARAM_END_AT); + + // Grab the details + String startAt; + String endAt; + String timezoneName = null; + if(startAtO instanceof JSONObject) + { + // "startAt": { "iso8601":"2011-...." } + JSONObject startAtJSON = (JSONObject)startAtO; + JSONObject endAtJSON = (JSONObject)endAtO; + startAt = (String)startAtJSON.get(PARAM_ISO8601); + endAt = (String)endAtJSON.get(PARAM_ISO8601); + + if(startAtJSON.containsKey(PARAM_TIMEZONE)) + { + timezoneName = (String)startAtJSON.get(PARAM_TIMEZONE); + if(endAtJSON.containsKey(PARAM_TIMEZONE)) + { + String endTZ = (String)endAtJSON.get(PARAM_TIMEZONE); + if(! endTZ.equals(timezoneName)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Timezones must match"); + } + } + } + } + else + { + // "startAt": "2011-...." + startAt = (String)json.get(PARAM_START_AT); + endAt = (String)json.get(PARAM_END_AT); + } + if(json.containsKey(PARAM_TIMEZONE)) + { + timezoneName = (String)json.get(PARAM_TIMEZONE); + } + + + // Is this an all day event? + if (json.containsKey("allday")) + { + // Store it as UTC midnight to midnight + // Reset the time part to ensure that + String utcMidnight = "T00:00:00Z"; + startAt = startAt.substring(0, 10) + utcMidnight; + endAt = endAt.substring(0, 10) + utcMidnight; + entry.setStart(ISO8601DateFormat.parse(startAt)); + entry.setEnd(ISO8601DateFormat.parse(endAt)); + } + else + { + // Regular event start and end rules + + // Do we have explicit timezone information? + if (timezoneName != null) + { + // Get the specified timezone + TimeZone tz = TimeZone.getTimeZone(timezoneName); + + // Grab the dates and times in the specified timezone + entry.setStart(ISO8601DateFormat.parse(startAt, tz)); + entry.setEnd(ISO8601DateFormat.parse(endAt, tz)); + } + else + { + // Offset info is either in the date, or we just have to guess + entry.setStart(parseDate(startAt)); + entry.setEnd(parseDate(endAt)); + } + } + } + else if (json.containsKey("allday")) + { + // Old style all-day event + Date start = parseDate(getOrNull(json, "from")); + Date end = parseDate(getOrNull(json, "to")); + // Store it as UTC midnight to midnight + // Reset the time part to ensure that + String isoStartAt = ISO8601DateFormat.format(start); + String isoEndAt = ISO8601DateFormat.format(end); + String utcMidnight = "T00:00:00Z"; + isoStartAt = isoStartAt.substring(0, 10) + utcMidnight; + isoEndAt = isoEndAt.substring(0, 10) + utcMidnight; + entry.setStart(ISO8601DateFormat.parse(isoStartAt)); + entry.setEnd(ISO8601DateFormat.parse(isoEndAt)); + + + } + else + { + // Old style regular event + entry.setStart(parseDate((String)json.get("from") + " " + (String)json.get("start"))); + entry.setEnd(parseDate((String)json.get("to") + " " + (String)json.get("end"))); + } + + return isAllDay; + } + + protected String getOrNull(JSONObject json, String key) throws JSONException + { + if (json.containsKey(key)) + { + return (String)json.get(key); + } + return null; + } + + /** + * Builds up a listing Paging request, either using the defaults or + * the paging options specified + */ + protected PagingRequest buildPagingRequest(WebScriptRequest req) + { + return new ScriptPagingDetails(req, MAX_QUERY_ENTRY_COUNT); + } + + /** + * Normally the Calendar webscripts return a 200 with JSON + * containing the error message. Override this to switch to + * using HTTP status codes instead + */ + protected boolean useJSONErrors() + { + return true; + } + + /** + * Equivalent of jsonError in the old JavaScript controllers + */ + protected Map buildError(String message) + { + HashMap result = new HashMap(); + result.put("error", message); + + HashMap model = new HashMap(); + model.put("error", message); + model.put("result", result); + + return model; + } + + /** + * Generates an activity entry for the entry + */ + protected String addActivityEntry(String event, CalendarEntry entry, SiteInfo site, + WebScriptRequest req, JSONObject json) + { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); + String dateOpt = "?date=" + fmt.format(entry.getStart()); + + // What page is this for? + String page = req.getParameter("page"); + if (page == null && json != null) + { + if (json.containsKey("page")) + { + page = (String)json.get("page"); + } + } + if (page == null) + { + // Default + page = "calendar"; + } + + try + { + StringWriter activityJson = new StringWriter(); + JSONWriter activity = new JSONWriter(activityJson); + activity.startObject(); + activity.writeValue("title", entry.getTitle()); + activity.writeValue("page", page + dateOpt); + activity.endObject(); + + activityService.postActivity( + "org.alfresco.calendar.event-" + event, + site.getShortName(), + CALENDAR_SERVICE_ACTIVITY_APP_NAME, + activityJson.toString()); + } + catch (Exception e) + { + // Warn, but carry on + logger.warn("Error adding event " + event + " to activities feed", e); + } + + // Return the date we used + return dateOpt; + } + + /** + * For an event that is a recurring event, have an ignored child event + * generated for it + */ + protected NodeRef createIgnoreEvent(WebScriptRequest req, CalendarEntry parent) + { + // Get the date to be ignored + Map props = new HashMap(); + Date date = parseDate(req.getParameter("date")); + props.put(CalendarModel.PROP_IGNORE_EVENT_DATE, date); + + // Create a child node of the event + NodeRef ignored = nodeService.createNode( + parent.getNodeRef(), CalendarModel.ASSOC_IGNORE_EVENT_LIST, + QName.createQName(GUID.generate()), CalendarModel.TYPE_IGNORE_EVENT, props + ).getChildRef(); + + // No further setup is needed + return ignored; + } + + @Override + protected Map executeImpl(WebScriptRequest req, + Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + if (useJSONErrors()) + { + return buildError(error); + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + } + + + // Parse the JSON, if supplied + JSONObject json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + JSONParser parser = new JSONParser(); + try + { + json = (JSONObject)parser.parse(req.getContent().getContent()); + } + catch (IOException io) + { + return buildError("Invalid JSON: " + io.getMessage()); + } + catch (org.json.simple.parser.ParseException je) + { + return buildError("Invalid JSON: " + je.getMessage()); + } + } + + + // Get the site short name. Try quite hard to do so... + String siteName = templateVars.get("siteid"); + if (siteName == null) + { + siteName = templateVars.get("site"); + } + if (siteName == null) + { + siteName = req.getParameter("site"); + } + if (siteName == null && json != null) + { + if (json.containsKey("siteid")) + { + siteName = (String)json.get("siteid"); + } + else if (json.containsKey("site")) + { + siteName = (String)json.get("site"); + } + } + if (siteName == null) + { + String error = "No site given"; + if (useJSONErrors()) + { + return buildError("No site given"); + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + } + + // Grab the requested site + SiteInfo site = siteService.getSite(siteName); + if (site == null) + { + String error = "Could not find site: " + siteName; + if (useJSONErrors()) + { + return buildError(error); + } + else + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + } + + // Event name is optional + String eventName = templateVars.get("eventname"); + + // Have the real work done + return executeImpl(site, eventName, req, json, status, cache); + } + + + /** + * Removes the time zone for a given date if the Calendar Entry is an all day event + * + * @return ISO 8601 formatted date String + */ + protected String removeTimeZoneIfRequired(Date date, Boolean isAllDay, Boolean removeTimezone) + { + return removeTimeZoneIfRequired(date, isAllDay, removeTimezone, null); + } + + /** + * Removes the time zone for a given date if the Calendar Entry is an all day event + * + * @return ISO 8601 formatted date String if datePattern is null + */ + protected String removeTimeZoneIfRequired(Date date, Boolean isAllDay, Boolean removeTimezone, String datePattern) + { + if (removeTimezone) + { + DateTime dateTime = new DateTime(date, DateTimeZone.UTC); + + if (null == datePattern) + { + return dateTime.toString((isAllDay) ? (ALL_DAY_DATETIME_FORMATTER) : (ISODateTimeFormat.dateTime())); + } + else + { + // For Legacy Dates and Times. + return dateTime.toString(DateTimeFormat.forPattern(datePattern)); + } + } + + // This is for all other cases, including the case, when UTC time zone is configured + + if (!isAllDay && (null == datePattern)) + { + return ISO8601DateFormat.format(date); + } + + DateFormat simpleDateFormat = new SimpleDateFormat((null != datePattern) ? (datePattern) : (ALL_DAY_DATETIME_PATTERN)); + + return simpleDateFormat.format(date); + } + + protected abstract Map executeImpl(SiteInfo site, + String eventName, WebScriptRequest req, JSONObject json, + Status status, Cache cache); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntriesListGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntriesListGet.java new file mode 100644 index 00000000000..a2853aac9d6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntriesListGet.java @@ -0,0 +1,162 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.calendar.CalendarModel; +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarEntryDTO; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the slingshot calendar eventList.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class CalendarEntriesListGet extends AbstractCalendarListingWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, String eventName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + // Evil format needed for compatibility with old API... + SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy"); + + // Decide on date ranges and repeating rules + Date fromDate = parseDate(req.getParameter("from")); + Date toDate = parseDate(req.getParameter("to")); + + boolean resortNeeded = false; + boolean repeatingFirstOnly = true; + String repeatingEvents = req.getParameter("repeating"); + if (repeatingEvents != null) + { + if ("first".equals(repeatingEvents)) + { + repeatingFirstOnly = true; + } + else if ("all".equals(repeatingEvents)) + { + repeatingFirstOnly = false; + resortNeeded = true; + } + } + + + // Get the entries for the list + PagingRequest paging = buildPagingRequest(req); + PagingResults entries = calendarService.listCalendarEntries( + new String[] {site.getShortName()}, fromDate, toDate, paging); + + // For each one in our page, grab details of any ignored instances + List> results = new ArrayList>(); + for (CalendarEntry entry : entries.getPage()) + { + Map result = new HashMap(); + result.put(RESULT_EVENT, entry); + result.put(RESULT_NAME, entry.getSystemName()); + result.put(RESULT_TITLE, entry.getTitle()); + boolean isAllDay = CalendarEntryDTO.isAllDay(entry); + boolean removeTimezone = isAllDay && !entry.isOutlook(); + result.put(RESULT_START, removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone)); + result.put(RESULT_END, removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone)); + if (isAllDay) + { + long dayLong = 86400000; + Date newDay = new Date(entry.getEnd().getTime()+dayLong); + result.put("allDayEnd", newDay); + } + + String legacyDateFormat = "M/d/yyyy"; + String legacyTimeFormat ="HH:mm"; + result.put("legacyDateFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyTimeFormat)); + result.put("legacyDateTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyTimeFormat)); + + + result.put("fromDate", entry.getStart()); + result.put("tags", entry.getTags()); + + List ignores = nodeService.getChildAssocs( + entry.getNodeRef(), CalendarModel.TYPE_IGNORE_EVENT, + ContentModel.ASSOC_CONTAINS, true); + + List ignoreEvents = new ArrayList(); + List ignoreEventDates = new ArrayList(); + for (ChildAssociationRef ref : ignores) + { + Date date = (Date)nodeService.getProperty(ref.getChildRef(), CalendarModel.PROP_IGNORE_EVENT_DATE); + if (date != null) + { + ignoreEventDates.add(date); + ignoreEvents.add(formatter.format(date)); + } + } + result.put("ignoreEvents", ignoreEvents); + result.put("ignoreEventDates", ignoreEventDates); + + // For repeating events, push forward if needed + boolean orderChanged = handleRecurring(entry, result, results, fromDate, toDate, repeatingFirstOnly); + if (orderChanged) + { + resortNeeded = true; + } + + // All done with this one + results.add(result); + } + + // If they asked for repeating events to be expanded, then do so + if (resortNeeded) + { + Collections.sort(results, getEventDetailsSorter()); + } + + // All done + Map model = new HashMap(); + model.put("events", results); + model.put("siteId", site.getShortName()); + model.put("site", site); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryDelete.java new file mode 100644 index 00000000000..f7442686569 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryDelete.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.util.Map; + +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the slingshot calendar event.delete webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class CalendarEntryDelete extends AbstractCalendarWebScript +{ + /** + * This WebScript uses HTTP status codes for errors + */ + @Override + protected boolean useJSONErrors() + { + return false; + } + + @Override + protected Map executeImpl(SiteInfo site, String eventName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + CalendarEntry entry = calendarService.getCalendarEntry( + site.getShortName(), eventName); + + if (entry == null) + { + status.setCode(Status.STATUS_NOT_FOUND); + return null; + } + + // Special case for "deleting" an instance of a recurring event + if (req.getParameter("date") != null && entry.getRecurrenceRule() != null) + { + // Have an ignored event generated + createIgnoreEvent(req, entry); + + // Mark as ignored + status.setCode(Status.STATUS_NO_CONTENT, "Recurring entry ignored"); + return null; + } + + // Delete the calendar entry + calendarService.deleteCalendarEntry(entry); + + // Record this in the activity feed + addActivityEntry("deleted", entry, site, req, json); + + // All done + status.setCode(Status.STATUS_NO_CONTENT, "Entry deleted"); + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryGet.java new file mode 100644 index 00000000000..4b87fdbc678 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryGet.java @@ -0,0 +1,310 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.text.DateFormat; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TimeZone; + +import org.alfresco.repo.calendar.CalendarModel; +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarEntryDTO; +import org.alfresco.service.cmr.calendar.CalendarRecurrenceHelper; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONObject; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the slingshot calendar event.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class CalendarEntryGet extends AbstractCalendarWebScript +{ + private static Log logger = LogFactory.getLog(CalendarEntryGet.class); + private PermissionService permissionService; + + @Override + protected Map executeImpl(SiteInfo site, String eventName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + + CalendarEntry entry = calendarService.getCalendarEntry( + site.getShortName(), eventName); + + if (entry == null) + { + String message = rb.getString(MSG_EVENT_NOT_FOUND); + return buildError(MessageFormat.format(message, eventName)); + } + + Date date = parseDate(req.getParameter("date")); + if (date != null) + { + // if some instances were updated + SimpleDateFormat fdt = new SimpleDateFormat("yyyyMMdd"); + Set childNodeTypeQNames = new HashSet(); + childNodeTypeQNames.add(CalendarModel.TYPE_UPDATED_EVENT); + List updatedEventList = nodeService.getChildAssocs(entry.getNodeRef(), childNodeTypeQNames); + for (ChildAssociationRef updatedEvent : updatedEventList) + { + NodeRef nodeRef = updatedEvent.getChildRef(); + Date updatedDate = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_EVENT_DATE); + if (fdt.format(updatedDate).equals(fdt.format(date))) + { + entry.setStart((Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_START)); + entry.setEnd((Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_END)); + entry.setTitle((String) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_WHAT)); + entry.setLocation((String) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_WHERE)); + break; + } + } + } + + // Build the object + Map result = new HashMap(); + result.put("name", entry.getSystemName()); + result.put("what", entry.getTitle()); + result.put("description", entry.getDescription()); + result.put("location", entry.getLocation()); + boolean isAllDay = CalendarEntryDTO.isAllDay(entry); + boolean removeTimezone = isAllDay && !entry.isOutlook(); + result.put("from", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone)); + result.put("to", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone)); + + String legacyDateFormat = "M/d/yyyy"; + String legacyTimeFormat ="HH:mm"; + result.put("legacyDateFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyTimeFormat)); + result.put("legacyDateTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyTimeFormat)); + + result.put("tags", entry.getTags()); + result.put("isoutlook", entry.isOutlook()); + result.put("outlookuid", entry.getOutlookUID()); + result.put("allday", isAllDay); + result.put("docfolder", entry.getSharePointDocFolder()); + result.put("recurrence", buildRecurrenceString(entry)); + + // Replace nulls with blank strings for the JSON + for (String key : result.keySet()) + { + if (result.get(key) == null) + { + result.put(key, ""); + } + } + + // Check the permissions the user has on the entry + AccessStatus canEdit = permissionService.hasPermission(entry.getNodeRef(), PermissionService.WRITE); + AccessStatus canDelete = permissionService.hasPermission(entry.getNodeRef(), PermissionService.DELETE); + result.put("canEdit", (canEdit == AccessStatus.ALLOWED)); + result.put("canDelete", (canDelete == AccessStatus.ALLOWED)); + + // All done + Map model = new HashMap(); + model.put("result", result); + return model; + } + + /** + * This method replicates the pre-existing behaviour for recurring events. + * Rather than try to render the text for them on the client, we instead + * statically render the description text here on the server. + * When we properly support recurring events in the client (and not just + * for SharePoint ones), this can be replaced. + */ + protected String buildRecurrenceString(CalendarEntry event) + { + // If there's no recurrence rules, then there's nothing to do + String recurrence = event.getRecurrenceRule(); + if (recurrence == null || recurrence.trim().length() == 0) + { + return null; + } + + // Get our days of the week, in the current locale, for each outlook two letter code + Map days = + CalendarRecurrenceHelper.buildLocalRecurrenceDaysOfTheWeek(I18NUtil.getLocale()); + + // Get our weeks names, in the current locale + Map weeks = + CalendarRecurrenceHelper.buildLocalRecurrenceWeekNames(I18NUtil.getLocale()); + + // Turn the string into a useful map + Map params = CalendarRecurrenceHelper.extractRecurrenceRule(event); + + // To hold our result + StringBuffer text = new StringBuffer(); + + // Handle the different frequencies + if (params.containsKey("FREQ")) + { + String freq = params.get("FREQ"); + String interval = params.get("INTERVAL"); + if (interval == null) + { + interval = "1"; + } + + if ("WEEKLY".equals(freq)) + { + if ("1".equals(interval)) + { + text.append("Occurs each week on "); + } + else + { + text.append("Occurs every " + interval + " weeks on "); + } + + for (String day : params.get("BYDAY").split(",")) + { + text.append(days.get(day)); + text.append(", "); + } + } + else if ("DAILY".equals(freq)) + { + if ("1".equals(interval)) + { + text.append("Occurs every day "); + } + else + { + text.append("Occurs every " + interval + " days "); + } + } + else if ("MONTHLY".equals(freq)) + { + if (params.get("BYMONTHDAY") != null) + { + text.append("Occurs day " + params.get("BYMONTHDAY")); + } + else if (params.get("BYSETPOS") != null) + { + text.append("Occurs the "); + text.append(weeks.get((Integer.parseInt(params.get("BYSETPOS")))) + " "); + buildParams(params, text, days); + } + text.append(" of every " + interval + " month(s) "); + } + else if ("YEARLY".equals(freq)) + { + if (params.get("BYMONTHDAY") != null) + { + text.append("Occurs every " + params.get("BYMONTHDAY")); + text.append("." + params.get("BYMONTH") + " "); + } + else + { + text.append("Occurs the "); + text.append(weeks.get((Integer.parseInt(params.get("BYSETPOS")))) + " "); + buildParams(params, text, days); + text.append(" of " + params.get("BYMONTH") + " month "); + } + } + else + { + logger.warn("Unsupported recurrence frequency " + freq); + } + } + + // And the rest + DateFormat dFormat = SimpleDateFormat.getDateInstance( + SimpleDateFormat.MEDIUM, I18NUtil.getLocale()); + + DateFormat tFormat = SimpleDateFormat.getTimeInstance( + SimpleDateFormat.SHORT, I18NUtil.getLocale()); + + text.append("effective " + dFormat.format(event.getStart())); + + if (params.containsKey("COUNT")) + { + // Nothing to do, is already handled in the recurrence date + } + if (event.getLastRecurrence() != null) + { + text.append(" until " + dFormat.format(event.getLastRecurrence())); + } + + text.append(" from " + tFormat.format(event.getStart())); + text.append(" to " + tFormat.format(event.getEnd())); + + // Add timezone in which recurrence rule was parsed + TimeZone timeZone = TimeZone.getDefault(); + boolean daylight = timeZone.inDaylightTime(new Date()); + String tzDisplayName = timeZone.getDisplayName(daylight, TimeZone.SHORT); + + text.append(" ("+tzDisplayName+")"); + + // All done + return text.toString(); + } + + private static StringBuffer buildParams(Map params, StringBuffer text, Map days) + { + int day = params.get("BYDAY").split(",").length; + if(day == 7){ + text.append(CalendarRecurrenceHelper.DAY); + } + if(day == 5){ + text.append(CalendarRecurrenceHelper.WEEKDAYS); + } + if(day == 2){ + text.append(CalendarRecurrenceHelper.WEEKENDS); + } + if(day != 7 && day != 5 && day != 2){ + text.append(days.get(params.get("BYDAY"))); + } + return text; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryPost.java new file mode 100644 index 00000000000..d1eb1cc0abe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryPost.java @@ -0,0 +1,133 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.StringTokenizer; + +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarEntryDTO; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.JSONException; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the slingshot calendar event.post webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class CalendarEntryPost extends AbstractCalendarWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, String eventName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + CalendarEntry entry = new CalendarEntryDTO(); + + // TODO Handle All Day events properly, including timezones + boolean isAllDay = false; + + try + { + // Grab the properties + entry.setTitle(getOrNull(json, "what")); + entry.setDescription(getOrNull(json, "desc")); + entry.setLocation(getOrNull(json, "where")); + entry.setSharePointDocFolder(getOrNull(json, "docfolder")); + + // Handle the dates + isAllDay = extractDates(entry, json); + + // Handle tags + if (json.containsKey("tags")) + { + StringTokenizer st = new StringTokenizer((String)json.get("tags"), ","); + while (st.hasMoreTokens()) + { + entry.getTags().add(st.nextToken()); + } + } + } + catch (JSONException je) + { + String message = rb.getString(MSG_INVALID_JSON); + return buildError(MessageFormat.format(message, je.getMessage())); + } + + // Have it added + entry = calendarService.createCalendarEntry(site.getShortName(), entry); + + + // Generate the activity feed for this + String dateOpt = addActivityEntry("created", entry, site, req, json); + + + // Build the return object + Map result = new HashMap(); + result.put("name", entry.getTitle()); + result.put("desc", entry.getDescription()); + result.put("where", entry.getLocation()); + + result.put("from", removeTimeZoneIfRequired(entry.getStart(), isAllDay, isAllDay)); + result.put("to", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, isAllDay)); + + String legacyDateFormat = "yyyy-MM-dd"; + String legacyTimeFormat ="HH:mm"; + result.put("legacyDateFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, isAllDay, legacyDateFormat)); + result.put("legacyTimeFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, isAllDay, legacyTimeFormat)); + result.put("legacyDateTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, isAllDay, legacyDateFormat)); + result.put("legacyTimeTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, isAllDay, legacyTimeFormat)); + + result.put("uri", "calendar/event/" + site.getShortName() + "/" + + entry.getSystemName() + dateOpt); + + result.put("tags", entry.getTags()); + result.put("allday", isAllDay); + result.put("docfolder", entry.getSharePointDocFolder()); + + // Replace nulls with blank strings for the JSON + for (String key : result.keySet()) + { + if (result.get(key) == null) + { + result.put(key, ""); + } + } + + // All done + Map model = new HashMap(); + model.put("result", result); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryPut.java new file mode 100644 index 00000000000..1d7fb54fc4f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/CalendarEntryPut.java @@ -0,0 +1,201 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.StringTokenizer; + +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarEntryDTO; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.JSONException; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the slingshot calendar event.put webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class CalendarEntryPut extends AbstractCalendarWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, String eventName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + + CalendarEntry entry = calendarService.getCalendarEntry( + site.getShortName(), eventName); + + if (entry == null) + { + String message = rb.getString(MSG_EVENT_NOT_FOUND); + return buildError(MessageFormat.format(message, eventName)); + } + + // TODO Handle All Day events properly, including timezones + boolean isAllDay = false; + + try + { + // Doc folder is a bit special + String docFolder = (String)json.get("docfolder"); + + // Editing recurring events is special and a little bit odd... + if (entry.getRecurrenceRule() != null && !json.containsKey("recurrenceRule")) + { + // Have an ignored event generated + // Will allow us to override this one instance + createIgnoreEvent(req, entry); + + // Create a new entry for this one case + CalendarEntry newEntry = new CalendarEntryDTO(); + newEntry.setOutlook(true); + + if ("*NOT_CHANGE*".equals(docFolder)) + { + newEntry.setSharePointDocFolder(entry.getSharePointDocFolder()); + } + + // From here on, "edit" the new version + entry = newEntry; + } + + // Doc folder is a bit special + if ("*NOT_CHANGE*".equals(docFolder)) + { + // Nothing to change + } + else + { + entry.setSharePointDocFolder(docFolder); + } + + + // Grab the properties + entry.setTitle(getOrNull(json, "what")); + entry.setDescription(getOrNull(json, "desc")); + entry.setLocation(getOrNull(json, "where")); + + // Handle the dates + isAllDay = extractDates(entry, json); + + // Recurring properties, only changed if keys present + if (json.containsKey("recurrenceRule")) + { + if (json.get("recurrenceRule") == null) + { + entry.setRecurrenceRule(null); + } + else + { + entry.setRecurrenceRule((String)json.get("recurrenceRule")); + } + } + if (json.containsKey("recurrenceLastMeeting")) + { + if (json.get("recurrenceLastMeeting") == null) + { + entry.setLastRecurrence(null); + } + else + { + entry.setLastRecurrence( + parseDate((String)json.get("recurrenceLastMeeting"))); + } + } + + // Handle tags + if (json.containsKey("tags")) + { + entry.getTags().clear(); + + StringTokenizer st = new StringTokenizer((String)json.get("tags"), ","); + while (st.hasMoreTokens()) + { + entry.getTags().add(st.nextToken()); + } + } + } + catch (JSONException je) + { + String message = rb.getString(MSG_INVALID_JSON); + return buildError(MessageFormat.format(message, je.getMessage())); + } + + // Have it edited + entry = calendarService.updateCalendarEntry(entry); + + + // Generate the activity feed for this + String dateOpt = addActivityEntry("updated", entry, site, req, json); + + + // Build the return object + Map result = new HashMap(); + result.put("summary", entry.getTitle()); + result.put("description", entry.getDescription()); + result.put("location", entry.getLocation()); + boolean removeTimezone = isAllDay && !entry.isOutlook(); + result.put("dtstart", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone)); + result.put("dtend", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone)); + + String legacyDateFormat = "yyyy-MM-dd"; + String legacyTimeFormat ="HH:mm"; + result.put("legacyDateFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyTimeFormat)); + result.put("legacyDateTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyTimeFormat)); + + result.put("uri", "calendar/event/" + site.getShortName() + "/" + + entry.getSystemName() + dateOpt); + + result.put("tags", entry.getTags()); + result.put("allday", isAllDay); + result.put("docfolder", entry.getSharePointDocFolder()); + + // Replace nulls with blank strings for the JSON + for (String key : result.keySet()) + { + if (result.get(key) == null) + { + result.put(key, ""); + } + } + + // All done + Map model = new HashMap(); + model.put("result", result); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/UserCalendarEntriesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/UserCalendarEntriesGet.java new file mode 100644 index 00000000000..fc4c3c2fa7b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/calendar/UserCalendarEntriesGet.java @@ -0,0 +1,313 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.calendar; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.calendar.CalendarServiceImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.calendar.CalendarEntry; +import org.alfresco.service.cmr.calendar.CalendarEntryDTO; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the slingshot calendar userevents.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class UserCalendarEntriesGet extends AbstractCalendarListingWebScript +{ + private PermissionService permissionService; + + @Override + protected Map executeImpl(WebScriptRequest req, + Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + + // Site is optional + SiteInfo site = null; + String siteName = templateVars.get("site"); + if (siteName != null) + { + site = siteService.getSite(siteName); + + // MNT-3053 fix, siteName was provided in request but it doesn't exists or user has no permissions to access it. + if (site == null) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, "Site '" + siteName + "' does not exist or user has no permissions to access it."); + return null; + } + } + + return executeImpl(site, null, req, null, status, cache); + } + + @Override + protected Map executeImpl(SiteInfo singleSite, String eventName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + // Did they restrict by date? + Date fromDate = parseDate(req.getParameter("from")); + Date toDate = parseDate(req.getParameter("to")); + + // What should we do about repeating events? First or all? + boolean repeatingFirstOnly = true; + String repeatingEvents = req.getParameter("repeating"); + if (repeatingEvents != null) + { + if ("first".equals(repeatingEvents)) + { + repeatingFirstOnly = true; + } + else if ("all".equals(repeatingEvents)) + { + repeatingFirstOnly = false; + } + } + else + { + // Fall back to the icky old way of guessing it from + // the format of the from date, which differs between uses! + if (fromDate != null) + { + String fromDateS = req.getParameter("from"); + if (fromDateS.indexOf('-') != -1) + { + // Apparently this is the site calendar dashlet... + repeatingFirstOnly = true; + } + if (fromDateS.indexOf('/') != -1) + { + // This is something else, wants all events in range + repeatingFirstOnly = false; + } + } + } + + // One site, or all the user's ones? + List sites = new ArrayList(); + if (singleSite != null) + { + // Just one + sites.add(singleSite); + } + else + { + // All their sites (with optional limit) + int max = 0; + String strMax = req.getParameter("size"); + if (strMax != null && strMax.length() != 0) + { + max = Integer.parseInt(strMax); + } + sites = siteService.listSites(AuthenticationUtil.getRunAsUser(), max); + } + + // We need to know the Site Names, and the NodeRefs of the calendar containers + String[] siteShortNames = new String[sites.size()]; + Map containerLookup = new HashMap(); + for (int i=0; i entries = + calendarService.listCalendarEntries(siteShortNames, fromDate, toDate, paging); + + boolean resortNeeded = false; + List> results = new ArrayList>(); + for (CalendarEntry entry : entries.getPage()) + { + // Build the object + Map result = new HashMap(); + boolean isAllDay = CalendarEntryDTO.isAllDay(entry); + boolean removeTimezone = isAllDay && !entry.isOutlook(); + result.put(RESULT_EVENT, entry); + result.put(RESULT_NAME, entry.getSystemName()); + result.put(RESULT_TITLE, entry.getTitle()); + result.put("description", entry.getDescription()); + result.put("where", entry.getLocation()); + result.put(RESULT_START, removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone)); + result.put(RESULT_END, removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone)); + + String legacyDateFormat = "yyyy-MM-dd"; + String legacyTimeFormat ="HH:mm"; + result.put("legacyDateFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeFrom", removeTimeZoneIfRequired(entry.getStart(), isAllDay, removeTimezone, legacyTimeFormat)); + result.put("legacyDateTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyDateFormat)); + result.put("legacyTimeTo", removeTimeZoneIfRequired(entry.getEnd(), isAllDay, removeTimezone, legacyTimeFormat)); + + + + result.put("duration", buildDuration(entry)); + result.put("tags", entry.getTags()); + result.put("isoutlook", entry.isOutlook()); + result.put("allday", CalendarEntryDTO.isAllDay(entry)); + + // Identify the site + SiteInfo site = containerLookup.get(entry.getContainerNodeRef()); + result.put("site", site); + result.put("siteName", site.getShortName()); + result.put("siteTitle", site.getTitle()); + + // Check the permissions the user has on the entry + AccessStatus canEdit = permissionService.hasPermission(entry.getNodeRef(), PermissionService.WRITE); + AccessStatus canDelete = permissionService.hasPermission(entry.getNodeRef(), PermissionService.DELETE); + result.put("canEdit", (canEdit == AccessStatus.ALLOWED)); + result.put("canDelete", (canDelete == AccessStatus.ALLOWED)); + + // Replace nulls with blank strings for the JSON + for (String key : result.keySet()) + { + if (result.get(key) == null) + { + result.put(key, ""); + } + } + + // Save this one + results.add(result); + + // Handle recurring as needed + boolean orderChanged = handleRecurring(entry, result, results, fromDate, toDate, repeatingFirstOnly); + if (orderChanged) + { + resortNeeded = true; + } + } + + // If the recurring events meant dates changed, re-sort + if (resortNeeded) + { + Collections.sort(results, getEventDetailsSorter()); + } + + // All done + Map model = new HashMap(); + model.put("events", results); + return model; + } + + private static final long DURATION_SECOND = 1000; + private static final long DURATION_MINUTE = 60 * DURATION_SECOND; + private static final long DURATION_HOUR = 60 * DURATION_MINUTE; + private static final long DURATION_DAY = 24 * DURATION_HOUR; + private static final long DURATION_WEEK = 7 * DURATION_DAY; + + /** + * Builds the duration in iCal format, eg PT2H15M + */ + private String buildDuration(CalendarEntry entry) + { + StringBuffer duration = new StringBuffer(); + duration.append("P"); + + long timeDiff = entry.getEnd().getTime() - entry.getStart().getTime(); + + int weeks = (int)Math.floor(timeDiff / DURATION_WEEK); + if (weeks > 0) + { + duration.append(weeks); + duration.append("W"); + timeDiff -= weeks * DURATION_WEEK; + } + + int days = (int)Math.floor(timeDiff / DURATION_DAY); + if (days > 0) + { + duration.append(days); + duration.append("D"); + timeDiff -= days * DURATION_DAY; + } + + duration.append("T"); + + int hours = (int)Math.floor(timeDiff / DURATION_HOUR); + if (hours > 0) + { + duration.append(hours); + duration.append("H"); + timeDiff -= hours * DURATION_HOUR; + } + + int minutes = (int)Math.floor(timeDiff / DURATION_MINUTE); + if (minutes > 0) + { + duration.append(minutes); + duration.append("M"); + timeDiff -= minutes * DURATION_MINUTE; + } + + int seconds = (int)Math.floor(timeDiff / DURATION_SECOND); + if (seconds > 0) + { + duration.append(seconds); + timeDiff -= minutes * DURATION_MINUTE; + } + + return duration.toString(); + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java new file mode 100644 index 00000000000..2dd2356c226 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comment/ScriptCommentService.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.comment; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.jscript.BaseScopableProcessorExtension; +import org.alfresco.repo.jscript.Scopeable; +import org.alfresco.repo.jscript.ScriptNode; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * Temporary comment service API to start encapsulation of comment logic. + * + * NOTE: this has been added to resolve a specific issue and needs re-consideration + * + * @author Roy Wetherall + */ +public class ScriptCommentService extends BaseScopableProcessorExtension +{ + private static final String COMMENTS_TOPIC_NAME = "Comments"; + + private ServiceRegistry serviceRegistry; + private NodeService nodeService; + private BehaviourFilter behaviourFilter; + private PermissionService permissionService; + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + this.nodeService = serviceRegistry.getNodeService(); + this.permissionService = serviceRegistry.getPermissionService(); + } + + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + public ScriptNode createCommentsFolder(ScriptNode node) + { + final NodeRef nodeRef = node.getNodeRef(); + if (permissionService.hasPermission(nodeRef, PermissionService.ADD_CHILDREN) == AccessStatus.DENIED) + { + throw new AccessDeniedException("User '" + AuthenticationUtil.getFullyAuthenticatedUser() + "' doesn't have permission to create discussion on node '" + nodeRef + "'"); + } + + //Run as system user to allow Contributor create discussions + NodeRef commentsFolder = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public NodeRef doWork() throws Exception + { + NodeRef commentsFolder = null; + + // ALF-5240: turn off auditing round the discussion node creation to prevent + // the source document from being modified by the first user leaving a comment + behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + + try + { + nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussable"), null); + nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "commentsRollup"), null); + List assocs = nodeService.getChildAssocs(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion"), RegexQNamePattern.MATCH_ALL); + if (assocs.size() != 0) + { + NodeRef forumFolder = assocs.get(0).getChildRef(); + + Map props = new HashMap(1, 1.0f); + props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME); + commentsFolder = nodeService.createNode( + forumFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, COMMENTS_TOPIC_NAME), + QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "topic"), + props).getChildRef(); + } + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + } + + return commentsFolder; + } + + }, AuthenticationUtil.getSystemUserName()); + + return new ScriptNode(commentsFolder, serviceRegistry, getScope()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/AbstractCommentsWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/AbstractCommentsWebScript.java new file mode 100644 index 00000000000..dd59553c6d5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/AbstractCommentsWebScript.java @@ -0,0 +1,349 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.comments; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONStringer; +import org.json.JSONWriter; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the abstract controller for the comments web scripts (delete + * and post) + * + * @author Ramona Popa + * @since 4.2.6 + */ +public abstract class AbstractCommentsWebScript extends DeclarativeWebScript +{ + + protected final static String COMMENTS_TOPIC_NAME = "Comments"; + + private static Log logger = LogFactory.getLog(CommentsPost.class); + + protected final static String JSON_KEY_SITE = "site"; + protected final static String JSON_KEY_SITE_ID = "siteid"; + protected final static String JSON_KEY_ITEM_TITLE = "itemTitle"; + protected final static String JSON_KEY_PAGE = "page"; + protected final static String JSON_KEY_TITLE = "title"; + protected final static String JSON_KEY_PAGE_PARAMS = "pageParams"; + protected final static String JSON_KEY_NODEREF = "nodeRef"; + protected final static String JSON_KEY_CONTENT = "content"; + + protected final static String COMMENT_CREATED_ACTIVITY = "org.alfresco.comments.comment-created"; + protected final static String COMMENT_DELETED_ACTIVITY = "org.alfresco.comments.comment-deleted"; + + protected ServiceRegistry serviceRegistry; + protected NodeService nodeService; + protected ContentService contentService; + protected PersonService personService; + protected SiteService siteService; + protected PermissionService permissionService; + protected ActivityService activityService; + + protected BehaviourFilter behaviourFilter; + + protected static final String PARAM_MESSAGE = "message"; + protected static final String PARAM_NODE = "node"; + protected static final String PARAM_ITEM = "item"; + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + this.nodeService = serviceRegistry.getNodeService(); + this.siteService = serviceRegistry.getSiteService(); + this.contentService = serviceRegistry.getContentService(); + this.personService = serviceRegistry.getPersonService(); + this.permissionService = serviceRegistry.getPermissionService(); + } + + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + /** + * returns the nodeRef from web script request + * @param req + * @return + */ + protected NodeRef parseRequestForNodeRef(WebScriptRequest req) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + String nodeId = templateVars.get("id"); + + // create the NodeRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + return new NodeRef(storeRef, nodeId); + } + + /** + * get the value from JSON for given key if exists + * @param json + * @param key + * @return + */ + protected String getOrNull(JSONObject json, String key) + { + if (json != null && json.containsKey(key)) + { + return (String) json.get(key); + } + return null; + } + + /** + * parse JSON from request + * @param req + * @return + */ + protected JSONObject parseJSON(WebScriptRequest req) + { + JSONObject json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + JSONParser parser = new JSONParser(); + try + { + json = (JSONObject) parser.parse(req.getContent().getContent()); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch (ParseException pe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage()); + } + } + return json; + } + + /** + * parse JSON for a given input string + * @param input + * @return + */ + protected JSONObject parseJSONFromString(String input) + { + JSONObject json = null; + + JSONParser parser = new JSONParser(); + try + { + if (input != null) + { + json = (JSONObject) parser.parse(input); + return json; + } + } + catch (ParseException pe) + { + if (logger.isDebugEnabled()) + { + logger.debug("Invalid JSON: " + pe.getMessage()); + } + } + + return null; + } + + /** + * Post an activity entry for the comment added or deleted + * + * @param json + * - is not sent null with this activity type - only for delete + * @param req + * @param nodeRef + * @param activityType + */ + protected void postActivity(JSONObject json, WebScriptRequest req, NodeRef nodeRef, String activityType) + { + String jsonActivityData = ""; + String siteId = ""; + String page = ""; + String title = ""; + + if (nodeRef == null) + { + // in case we don't have an parent nodeRef provided we do not need + // to post activity for parent node + return; + } + + String strNodeRef = nodeRef.toString(); + + SiteInfo siteInfo = getSiteInfo(req, COMMENT_CREATED_ACTIVITY.equals(activityType)); + + // post an activity item, but only if we've got a site + if (siteInfo == null || siteInfo.getShortName() == null || siteInfo.getShortName().length() == 0) + { + return; + } + else + { + siteId = siteInfo.getShortName(); + } + + // json is not sent null with this activity type - only for delete + if (COMMENT_CREATED_ACTIVITY.equals(activityType)) + { + try + { + org.json.JSONObject params = new org.json.JSONObject(getOrNull(json, JSON_KEY_PAGE_PARAMS)); + String strParams = ""; + + Iterator itr = params.keys(); + while (itr.hasNext()) + { + String strParam = itr.next().toString(); + strParams += strParam + "=" + params.getString(strParam) + "&"; + } + page = getOrNull(json, JSON_KEY_PAGE) + "?" + (strParams != "" ? strParams.substring(0, strParams.length() - 1) : ""); + title = getOrNull(json, JSON_KEY_ITEM_TITLE); + + } + catch (Exception e) + { + logger.warn("Error parsing JSON", e); + } + } + else + { + // COMMENT_DELETED_ACTIVITY + title = req.getParameter(JSON_KEY_ITEM_TITLE); + page = req.getParameter(JSON_KEY_PAGE) + "?" + JSON_KEY_NODEREF + "=" + strNodeRef; + } + + try + { + JSONWriter jsonWriter = new JSONStringer().object(); + jsonWriter.key(JSON_KEY_TITLE).value(title); + jsonWriter.key(JSON_KEY_PAGE).value(page); + jsonWriter.key(JSON_KEY_NODEREF).value(strNodeRef); + + jsonActivityData = jsonWriter.endObject().toString(); + activityService.postActivity(activityType, siteId, COMMENTS_TOPIC_NAME, jsonActivityData); + } + catch (Exception e) + { + logger.warn("Error adding comment to activities feed", e); + } + + } + + /** + * returns SiteInfo needed for post activity + * @param req + * @return + */ + protected SiteInfo getSiteInfo(WebScriptRequest req, boolean searchForSiteInJSON) + { + String siteName = req.getParameter(JSON_KEY_SITE); + + if (siteName == null && searchForSiteInJSON ) + { + JSONObject json = parseJSON(req); + if (json != null){ + if (json.containsKey(JSON_KEY_SITE)) + { + siteName = (String) json.get(JSON_KEY_SITE); + } + else if (json.containsKey(JSON_KEY_SITE_ID)) + { + siteName = (String) json.get(JSON_KEY_SITE_ID); + } + } + } + if (siteName != null) + { + SiteInfo site = siteService.getSite(siteName); + return site; + } + + return null; + } + + /** + * Overrides DeclarativeWebScript with parse request for nodeRef + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // get requested node + NodeRef nodeRef = parseRequestForNodeRef(req); + + // Have the real work done + return executeImpl(nodeRef, req, status, cache); + } + + /** + * + * @param nodeRef + * @param req + * @param status + * @param cache + * @return + */ + protected abstract Map executeImpl(NodeRef nodeRef, WebScriptRequest req, Status status, Cache cache); + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/CommentDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/CommentDelete.java new file mode 100644 index 00000000000..3434ef77886 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/CommentDelete.java @@ -0,0 +1,156 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.comments; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.ForumModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the comment.delete web script. + * + * @author Ramona Popa + * @since 4.2.6 + */ + +public class CommentDelete extends AbstractCommentsWebScript +{ + + private static Log logger = LogFactory.getLog(CommentDelete.class); + + /** + * Overrides AbstractCommentsWebScript to delete comment + */ + @Override + protected Map executeImpl(NodeRef nodeRef, WebScriptRequest req, Status status, Cache cache) + { + String pageParams = req.getParameter(JSON_KEY_PAGE_PARAMS); + + JSONObject jsonPageParams = parseJSONFromString(pageParams); + + String parentNodeRefStr = getOrNull(jsonPageParams, JSON_KEY_NODEREF); + NodeRef parentNodeRef = null; + if (parentNodeRefStr != null) + { + parentNodeRef = new NodeRef((String) getOrNull(jsonPageParams, JSON_KEY_NODEREF)); + } + + if (parentNodeRef == null) + { + // find the parent content node for the comment + // Example would be a blog post which will have the following structure: + // blog post -> forum node -> topic node -> the actual comment node + NodeRef topicNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + if (topicNodeRef == null || !nodeService.getType(topicNodeRef).equals(ForumModel.TYPE_TOPIC)) + { + throw new IllegalArgumentException("The NodeRef specified is not a child of the topic."); + } + NodeRef forumNodeRef = nodeService.getPrimaryParent(topicNodeRef).getParentRef(); + if (forumNodeRef == null || !nodeService.getType(forumNodeRef).equals(ForumModel.TYPE_FORUM)) + { + throw new IllegalArgumentException("The NodeRef specified doesn't belong to a correct structure of forums."); + } + parentNodeRef = nodeService.getPrimaryParent(forumNodeRef).getParentRef(); + } + + if (parentNodeRef != null) + { + this.behaviourFilter.disableBehaviour(parentNodeRef, ContentModel.ASPECT_AUDITABLE); + } + + try + { + // delete node + deleteComment(nodeRef); + + if (nodeService.exists(nodeRef)) + { + // comment was not removed + status.setCode(Status.STATUS_INTERNAL_SERVER_ERROR, "Unable to delete node: " + nodeRef); + return null; + } + + // generate response model for a comment node + Map model = generateModel(nodeRef); + + // post an activity item - it is ok to send json as null since the + // infos that we need are as parameters on request + postActivity(null, req, parentNodeRef, COMMENT_DELETED_ACTIVITY); + + status.setCode(Status.STATUS_OK); + return model; + + } + finally + { + if (parentNodeRef != null) + { + this.behaviourFilter.enableBehaviour(parentNodeRef, ContentModel.ASPECT_AUDITABLE); + } + } + } + + /** + * deletes comment node + * + * @param commentNodeRef + */ + private void deleteComment(NodeRef commentNodeRef) + { + QName nodeType = nodeService.getType(commentNodeRef); + if (!nodeType.equals(ForumModel.TYPE_POST)) + { + throw new IllegalArgumentException("Node to delete is not a comment node."); + } + + nodeService.deleteNode(commentNodeRef); + } + + /** + * generates model for delete comment script + * + * @param commentNodeRef + * @return + */ + private Map generateModel(NodeRef commentNodeRef) + { + Map model = new HashMap(2, 1.0f); + + model.put(PARAM_MESSAGE, "Node " + commentNodeRef + " deleted"); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/CommentsPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/CommentsPost.java new file mode 100644 index 00000000000..1c46b8e7974 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/comments/CommentsPost.java @@ -0,0 +1,279 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.comments; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.ForumModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the comments.post web script. + * + * @author Sergey Scherbovich (based on existing JavaScript webscript controller) + * @since 4.1.7.1 + */ +public class CommentsPost extends AbstractCommentsWebScript +{ + /** + * Overrides AbstractCommentsWebScript to add comment + */ + @Override + protected Map executeImpl(NodeRef nodeRef, WebScriptRequest req, Status status, Cache cache) + { + // get json object from request + JSONObject json = parseJSON(req); + + /* MNT-10231, MNT-9771 fix */ + this.behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + + try + { + // add a comment + NodeRef commentNodeRef = addComment(nodeRef, json); + + // generate response model for a comment node + Map model = generateModel(nodeRef, commentNodeRef); + + // post an activity item + postActivity(json, req, nodeRef, COMMENT_CREATED_ACTIVITY); + + return model; + } + finally + { + this.behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + } + } + + /** + * add the comment from json to given nodeRef + * + * @param nodeRef + * @param json + * @return + */ + private NodeRef addComment(NodeRef nodeRef, JSONObject json) + { + // fetch the parent to add the node to + NodeRef commentsFolder = getOrCreateCommentsFolder(nodeRef); + + // get a unique name + String name = getUniqueChildName("comment"); + + // create the comment + NodeRef commentNodeRef = nodeService.createNode(commentsFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(name)), + ForumModel.TYPE_POST).getChildRef(); + + // fetch the title required to create a comment + String title = getOrNull(json, JSON_KEY_TITLE); + HashMap props = new HashMap(1, 1.0f); + props.put(ContentModel.PROP_TITLE, title != null ? title : ""); + nodeService.addProperties(commentNodeRef, props); + + ContentWriter writer = contentService.getWriter(commentNodeRef, ContentModel.PROP_CONTENT, true); + // fetch the content of a comment + String contentString = getOrNull(json, JSON_KEY_CONTENT); + + writer.setMimetype(MimetypeMap.MIMETYPE_HTML); + writer.putContent(contentString); + + return commentNodeRef; + } + + /** + * generates an comment item value + * + * @param commentNodeRef + * @return + */ + private Map generateItemValue(NodeRef commentNodeRef) + { + Map result = new HashMap(4, 1.0f); + + String creator = (String)this.nodeService.getProperty(commentNodeRef, ContentModel.PROP_CREATOR); + + Serializable created = this.nodeService.getProperty(commentNodeRef, ContentModel.PROP_CREATED); + Serializable modified = this.nodeService.getProperty(commentNodeRef, ContentModel.PROP_MODIFIED); + + boolean isUpdated = false; + if (created instanceof Date && modified instanceof Date) + { + isUpdated = ((Date)modified).getTime() - ((Date)created).getTime() > 5000; + } + + // TODO refactor v0 Comments API to use CommentService (see ACE-5437) + Serializable owner = this.nodeService.getProperty(commentNodeRef, ContentModel.PROP_OWNER); + String currentUser = this.serviceRegistry.getAuthenticationService().getCurrentUserName(); + + boolean isSiteManager = this.permissionService.hasPermission(commentNodeRef, SiteModel.SITE_MANAGER) == (AccessStatus.ALLOWED); + boolean isCoordinator = this.permissionService.hasPermission(commentNodeRef, PermissionService.COORDINATOR) == (AccessStatus.ALLOWED); + boolean canEditComment = isSiteManager || isCoordinator || currentUser.equals(creator) || currentUser.equals(owner); + + result.put("node", commentNodeRef); + result.put("author", this.personService.getPerson(creator)); + result.put("isUpdated", isUpdated); + result.put("canEditComment", canEditComment); + + return result; + } + + /** + * generates the response model for adding a comment + * + * @param nodeRef + * @param commentNodeRef + * @return + */ + private Map generateModel(NodeRef nodeRef, NodeRef commentNodeRef) + { + Map model = new HashMap(2, 1.0f); + + model.put(PARAM_NODE, nodeRef); + model.put(PARAM_ITEM, generateItemValue(commentNodeRef)); + + return model; + } + + /** + * + * @param nodeRef + * @return + */ + private NodeRef getOrCreateCommentsFolder(NodeRef nodeRef) + { + NodeRef commentsFolder = getCommentsFolder(nodeRef); + // create a comment folder if it doesn't exist + if (commentsFolder == null) + { + commentsFolder = createCommentsFolder(nodeRef); + } + return commentsFolder; + } + + /** + * returns the nodeRef of the existing one + * + * @param nodeRef + * @return + */ + private NodeRef getCommentsFolder(NodeRef nodeRef) + { + if (nodeService.hasAspect(nodeRef, ForumModel.ASPECT_DISCUSSABLE)) + { + List assocs = nodeService.getChildAssocs(nodeRef, ForumModel.ASSOC_DISCUSSION, RegexQNamePattern.MATCH_ALL); + ChildAssociationRef firstAssoc = assocs.get(0); + + return nodeService.getChildByName(firstAssoc.getChildRef(), ContentModel.ASSOC_CONTAINS, COMMENTS_TOPIC_NAME); + } + else + { + return null; + } + } + + private String getUniqueChildName(String prefix) + { + return prefix + "-" + System.currentTimeMillis(); + } + + /** + * creates the comments folder if it does not exists + * + * @param nodeRef + * @return + */ + private NodeRef createCommentsFolder(final NodeRef nodeRef) + { + NodeRef commentsFolder = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public NodeRef doWork() throws Exception + { + NodeRef commentsFolder = null; + AuthenticationUtil.pushAuthentication(); + + // ALF-5240: turn off auditing round the discussion node creation to prevent + // the source document from being modified by the first user leaving a comment + behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + + try + { + // MNT-12082: set System user for creating forumFolder and commentsFolder nodes + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussable"), null); + nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "commentsRollup"), null); + List assocs = nodeService.getChildAssocs(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion"), RegexQNamePattern.MATCH_ALL); + if (assocs.size() != 0) + { + NodeRef forumFolder = assocs.get(0).getChildRef(); + + Map props = new HashMap(1, 1.0f); + props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME); + commentsFolder = nodeService.createNode( + forumFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, COMMENTS_TOPIC_NAME), + QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "topic"), + props).getChildRef(); + } + } + finally + { + AuthenticationUtil.popAuthentication(); + behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + } + + return commentsFolder; + } + + }, AuthenticationUtil.getSystemUserName()); + + return commentsFolder; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/config/OpenSearchConfigElement.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/config/OpenSearchConfigElement.java new file mode 100644 index 00000000000..c111d23a784 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/config/OpenSearchConfigElement.java @@ -0,0 +1,284 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.config; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.extensions.config.ConfigElement; +import org.springframework.extensions.config.ConfigException; +import org.springframework.extensions.config.element.ConfigElementAdapter; + + +/** + * Custom config element that represents the config data for open search + * + * @author davidc + */ +public class OpenSearchConfigElement extends ConfigElementAdapter +{ + public static final String CONFIG_ELEMENT_ID = "opensearch"; + + private ProxyConfig proxy; + private Set engines = new HashSet(8, 10f); + private Map enginesByProxy = new HashMap(); + + + /** + * Default constructor + */ + public OpenSearchConfigElement() + { + super(CONFIG_ELEMENT_ID); + } + + /** + * Constructor + * + * @param name Name of the element this config element represents + */ + public OpenSearchConfigElement(String name) + { + super(name); + } + + /** + * @see ConfigElement#getChildren() + */ + public List getChildren() + { + throw new ConfigException("Reading the open search config via the generic interfaces is not supported"); + } + + /** + * @see ConfigElement#combine(ConfigElement) + */ + public ConfigElement combine(ConfigElement configElement) + { + OpenSearchConfigElement newElement = (OpenSearchConfigElement) configElement; + OpenSearchConfigElement combinedElement = new OpenSearchConfigElement(); + + // add all the plugins from this element + for (EngineConfig plugin : this.getEngines()) + { + combinedElement.addEngine(plugin); + } + + // add all the plugins from the given element + for (EngineConfig plugin : newElement.getEngines()) + { + combinedElement.addEngine(plugin); + } + + // set the proxy configuration + ProxyConfig proxyConfig = this.getProxy(); + if (proxyConfig != null) + { + combinedElement.setProxy(proxyConfig); + } + + return combinedElement; + } + + /** + * Sets the proxy configuration + * + * @param proxyConfig ProxyConfig + */ + /*package*/ void setProxy(ProxyConfig proxyConfig) + { + this.proxy = proxyConfig; + } + + /** + * Gets the proxy configuration + * + * @return The proxy configuration + */ + public ProxyConfig getProxy() + { + return this.proxy; + } + + /** + * @return Returns a set of the engines + */ + public Set getEngines() + { + return this.engines; + } + + /** + * @param proxy name of engine proxy + * @return associated engine config (or null, if none registered against proxy) + */ + public EngineConfig getEngine(String proxy) + { + return this.enginesByProxy.get(proxy); + } + + /** + * Adds an engine + * + * @param engineConfig A pre-configured engine config object + */ + /*package*/ void addEngine(EngineConfig engineConfig) + { + this.engines.add(engineConfig); + String proxy = engineConfig.getProxy(); + if (proxy != null && proxy.length() > 0) + { + this.enginesByProxy.put(proxy, engineConfig); + } + } + + + /** + * Inner class representing the configuration of an OpenSearch engine + * + * @author davidc + */ + public static class EngineConfig + { + protected String label; + protected String labelId; + protected String proxy; + protected Map urls = new HashMap(8, 10f); + + + /** + * Construct + * + * @param label String + * @param labelId String + */ + public EngineConfig(String label, String labelId) + { + if ((label == null || label.length() == 0) && (labelId == null || labelId.length() == 0)) + { + throw new IllegalArgumentException("'label' or 'label-id' must be specified"); + } + this.label = label; + this.labelId = labelId; + } + + /** + * Construct + * + * @param label String + * @param labelId String + * @param proxy String + */ + public EngineConfig(String label, String labelId, String proxy) + { + this(label, labelId); + this.proxy = proxy; + } + + /** + * @return I18N label id + */ + public String getLabelId() + { + return labelId; + } + + /** + * @return label + */ + public String getLabel() + { + return label; + } + + /** + * @return proxy + */ + public String getProxy() + { + return proxy; + } + + /** + * Gets the urls supported by this engine + * + * @return urls + */ + public Map getUrls() + { + return urls; + } + + /** + * Adds a url + * + * @param mimetype mime type + * @param uri uri + */ + /*package*/ void addUrl(String mimetype, String uri) + { + this.urls.put(mimetype, uri); + } + + } + + + /** + * Inner class representing the configuration of the OpenSearch proxy + * + * @author davidc + */ + public static class ProxyConfig + { + protected String url; + + /** + * Construct + * + * @param url String + */ + public ProxyConfig(String url) + { + if (url == null || url.length() == 0) + { + throw new IllegalArgumentException("'url' must be specified"); + } + this.url = url; + } + + /** + * @return url + */ + public String getUrl() + { + return url; + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/config/OpenSearchElementReader.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/config/OpenSearchElementReader.java new file mode 100644 index 00000000000..aea1e0b4bef --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/config/OpenSearchElementReader.java @@ -0,0 +1,120 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.config; + +import java.util.Iterator; + +import org.springframework.extensions.config.ConfigElement; +import org.springframework.extensions.config.ConfigException; +import org.springframework.extensions.config.xml.elementreader.ConfigElementReader; +import org.alfresco.repo.web.scripts.config.OpenSearchConfigElement.EngineConfig; +import org.alfresco.repo.web.scripts.config.OpenSearchConfigElement.ProxyConfig; +import org.dom4j.Element; + + +/** + * Custom element reader to parse config for the open search + * + * @author davidc + */ +public class OpenSearchElementReader implements ConfigElementReader +{ + public static final String ELEMENT_OPENSEARCH = "opensearch"; + public static final String ELEMENT_ENGINES = "engines"; + public static final String ELEMENT_ENGINE = "engine"; + public static final String ELEMENT_URL = "url"; + public static final String ELEMENT_PROXY = "proxy"; + public static final String ATTR_TYPE = "type"; + public static final String ATTR_LABEL = "label"; + public static final String ATTR_LABEL_ID = "label-id"; + public static final String ATTR_PROXY = "proxy"; + + + /** + * @see org.springframework.extensions.config.xml.elementreader.ConfigElementReader#parse(org.dom4j.Element) + */ + @SuppressWarnings("unchecked") + public ConfigElement parse(Element element) + { + OpenSearchConfigElement configElement = null; + + if (element != null) + { + String elementName = element.getName(); + if (elementName.equals(ELEMENT_OPENSEARCH) == false) + { + throw new ConfigException("OpenSearchElementReader can only parse " + ELEMENT_OPENSEARCH + + "elements, the element passed was '" + elementName + "'"); + } + + // go through the registered engines + configElement = new OpenSearchConfigElement(); + Element pluginsElem = element.element(ELEMENT_ENGINES); + if (pluginsElem != null) + { + Iterator engines = pluginsElem.elementIterator(ELEMENT_ENGINE); + while(engines.hasNext()) + { + // construct engine + Element engineElem = engines.next(); + String label = engineElem.attributeValue(ATTR_LABEL); + String labelId = engineElem.attributeValue(ATTR_LABEL_ID); + String proxy = engineElem.attributeValue(ATTR_PROXY); + EngineConfig engineCfg = new EngineConfig(label, labelId, proxy); + + // construct urls for engine + Iterator urlsConfig = engineElem.elementIterator(ELEMENT_URL); + while (urlsConfig.hasNext()) + { + Element urlConfig = urlsConfig.next(); + String type = urlConfig.attributeValue(ATTR_TYPE); + String url = urlConfig.getTextTrim(); + engineCfg.addUrl(type, url); + } + + // register engine config + configElement.addEngine(engineCfg); + } + } + + // extract proxy configuration + String url = null; + Element proxyElem = element.element(ELEMENT_PROXY); + if (proxyElem != null) + { + Element urlElem = proxyElem.element(ELEMENT_URL); + if (urlElem != null) + { + url = urlElem.getTextTrim(); + ProxyConfig proxyCfg = new ProxyConfig(url); + configElement.setProxy(proxyCfg); + } + } + } + + return configElement; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java new file mode 100644 index 00000000000..83e32679d76 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java @@ -0,0 +1,170 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.web.context.ServletContextAware; + + +/** + * Content Retrieval Service + * + * Stream content from the Repository. + * + * @author davidc + */ +public class ContentGet extends StreamContent implements ServletContextAware +{ + // Logger + @SuppressWarnings("unused") + private static final Log logger = LogFactory.getLog(ContentGet.class); + + // Component dependencies + private ServletContext servletContext; + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + private ContentService contentService; + + /** + * @param servletContext ServletContext + */ + public void setServletContext(ServletContext servletContext) + { + this.servletContext = servletContext; + } + + /** + * @param dictionaryService DictionaryService + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param namespaceService NamespaceService + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param contentService ContentService + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @see org.springframework.extensions.webscripts.WebScript#execute(WebScriptRequest, WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) + throws IOException + { + // create map of args + String[] names = req.getParameterNames(); + Map args = new HashMap(names.length, 1.0f); + for (String name : names) + { + args.put(name, req.getParameter(name)); + } + + // create map of template vars + Map templateVars = req.getServiceMatch().getTemplateVars(); + + // create object reference from url + ObjectReference reference = createObjectReferenceFromUrl(args, templateVars); + NodeRef nodeRef = reference.getNodeRef(); + if (nodeRef == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find " + reference.toString()); + } + + // determine attachment + boolean attach = Boolean.valueOf(req.getParameter("a")); + + // render content + QName propertyQName = ContentModel.PROP_CONTENT; + String contentPart = templateVars.get("property"); + if (contentPart.length() > 0 && contentPart.charAt(0) == ';') + { + if (contentPart.length() < 2) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Content property malformed"); + } + String propertyName = contentPart.substring(1); + if (propertyName.length() > 0) + { + propertyQName = QName.createQName(propertyName, namespaceService); + } + } + + // Stream the content + streamContentLocal(req, res, nodeRef, attach, propertyQName, null); + } + + protected void streamContentLocal(WebScriptRequest req, WebScriptResponse res, NodeRef nodeRef, boolean attach, QName propertyQName, Map model) throws IOException + { + String userAgent = req.getHeader("User-Agent"); + userAgent = userAgent != null ? userAgent.toLowerCase() : ""; + boolean rfc5987Supported = (userAgent.contains("msie") || + userAgent.contains(" trident/") || + userAgent.contains(" chrome/") || + userAgent.contains(" firefox/") || + userAgent.contains(" safari/")); + + if (attach && rfc5987Supported) + { + String name = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + + // maintain the original name of the node during the download - do not modify it - see MNT-16510 + streamContent(req, res, nodeRef, propertyQName, attach, name, model); + } + else + { + streamContent(req, res, nodeRef, propertyQName, attach, null, model); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java new file mode 100644 index 00000000000..2e34694c244 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Content Info Service Get info about content from the Repository. + * + * @author alex.malinovsky + */ +public class ContentInfo extends StreamContent +{ + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + // create empty map of args + Map args = new HashMap(0, 1.0f); + // create map of template vars + Map templateVars = req.getServiceMatch().getTemplateVars(); + // create object reference from url + ObjectReference reference = createObjectReferenceFromUrl(args, templateVars); + NodeRef nodeRef = reference.getNodeRef(); + if (nodeRef == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find " + reference.toString()); + } + + // render content + QName propertyQName = ContentModel.PROP_CONTENT; + + // Stream the content + streamContent(req, res, nodeRef, propertyQName, false, null, null); + } + + protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, + ContentReader reader, NodeRef nodeRef, QName propertyQName, + boolean attach, Date modified, String eTag, String attachFileName) + throws IOException + { + delegate.setAttachment(req, res, attach, attachFileName); + + // establish mimetype + String mimetype = reader.getMimetype(); + String extensionPath = req.getExtensionPath(); + if (mimetype == null || mimetype.length() == 0) + { + mimetype = MimetypeMap.MIMETYPE_BINARY; + int extIndex = extensionPath.lastIndexOf('.'); + if (extIndex != -1) + { + String ext = extensionPath.substring(extIndex + 1); + mimetype = mimetypeService.getMimetype(ext); + } + } + + // set mimetype for the content and the character encoding + length for the stream + res.setContentType(mimetype); + res.setContentEncoding(reader.getEncoding()); + res.setHeader("Content-Length", Long.toString(reader.getSize())); + + // set caching + Cache cache = new Cache(); + cache.setNeverCache(false); + cache.setMustRevalidate(true); + cache.setMaxAge(0L); + cache.setLastModified(modified); + cache.setETag(eTag); + res.setCache(cache); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java new file mode 100644 index 00000000000..efbd743aa4d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java @@ -0,0 +1,587 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.sync.repo.events.EventPublisher; +import org.alfresco.repo.web.util.HttpRangeProcessor; +import org.alfresco.rest.framework.resource.content.CacheDirective; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.extensions.surf.util.URLEncoder; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.util.FileCopyUtils; + + +/** + * Can be used when the binary data of a content property needs to be streamed back to the client + * as the result of executing a web script. + * + * These methods are taken from the StreamContent class so they can be reused by other webscripts. + * + */ + +public class ContentStreamer implements ResourceLoaderAware +{ + // Logger + private static final Log logger = LogFactory.getLog(ContentStreamer.class); + + public static final String KEY_ALLOW_BROWSER_TO_CACHE = "allowBrowserToCache"; + public static final String KEY_CACHE_DIRECTIVE = "cacheDirective"; + + /** + * format definied by RFC 822, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 + */ + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US); + + private static final String HEADER_CONTENT_RANGE = "Content-Range"; + private static final String HEADER_CONTENT_LENGTH = "Content-Length"; + private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges"; + private static final String HEADER_RANGE = "Range"; + private static final String HEADER_USER_AGENT = "User-Agent"; + + /** + * Services + */ + // protected PermissionService permissionService; + protected NodeService nodeService; + protected ContentService contentService; + protected MimetypeService mimetypeService; + protected ResourceLoader resourceLoader; + protected EventPublisher eventPublisher; + protected SiteService siteService; + + /** + * @param mimetypeService MimetypeService + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + /** + * @param nodeService NodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param eventPublisher EventPublisher + */ + public void setEventPublisher(EventPublisher eventPublisher) + { + this.eventPublisher = eventPublisher; + } + + /** + * @param siteService SiteService + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) + { + this.resourceLoader = resourceLoader; + } + + /** + * @param contentService ContentService + */ + public void setContentService(ContentService contentService) + + { + this.contentService = contentService; + } + + + /** + * Streams content back to client from a given File. + * + * @param req The request + * @param res The response + * @param file The file whose content is to be streamed. + * @param modifiedTime The modified datetime to use for the streamed content. If null the + * file's timestamp will be used. + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + public void streamContent(WebScriptRequest req, + WebScriptResponse res, + File file, + Long modifiedTime, + boolean attach, + String attachFileName, + Map model) throws IOException + { + if (logger.isDebugEnabled()) + logger.debug("Retrieving content from file " + file.getAbsolutePath() + " (attach: " + attach + ")"); + + // determine mimetype from file extension + String filePath = file.getAbsolutePath(); + String mimetype = MimetypeMap.MIMETYPE_BINARY; + int extIndex = filePath.lastIndexOf('.'); + if (extIndex != -1) + { + mimetype = mimetypeService.getMimetype(filePath.substring(extIndex + 1)); + } + + // setup file reader and stream + FileContentReader reader = new FileContentReader(file); + reader.setMimetype(mimetype); + reader.setEncoding("UTF-8"); + + long lastModified = modifiedTime == null ? file.lastModified() : modifiedTime; + Date lastModifiedDate = new Date(lastModified); + + streamContentImpl(req, res, reader, null, null, attach, lastModifiedDate, String.valueOf(lastModifiedDate.getTime()), attachFileName, model); + } + + /** + * Streams the content on a given node's content property to the response of the web script. + * + * @param req Request + * @param res Response + * @param nodeRef The node reference + * @param propertyQName The content property name + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + public void streamContent(WebScriptRequest req, + WebScriptResponse res, + NodeRef nodeRef, + QName propertyQName, + boolean attach, + String attachFileName, + Map model) throws IOException + { + if (logger.isDebugEnabled()) + logger.debug("Retrieving content from node ref " + nodeRef.toString() + " (property: " + propertyQName.toString() + ") (attach: " + attach + ")"); + + // TODO + // This was commented out to accomadate records management permissions. We need to review how we cope with this + // hard coded permission checked. + + // check that the user has at least READ_CONTENT access - else redirect to the login page + // if (permissionService.hasPermission(nodeRef, PermissionService.READ_CONTENT) == AccessStatus.DENIED) + // { + // throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Permission denied"); + // } + + // check If-Modified-Since header and set Last-Modified header as appropriate + Date modified = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED); + if (modified != null) + { + long modifiedSince = -1; + String modifiedSinceStr = req.getHeader("If-Modified-Since"); + if (modifiedSinceStr != null) + { + try + { + modifiedSince = dateFormat.parse(modifiedSinceStr).getTime(); + } + catch (Throwable e) + { + if (logger.isInfoEnabled()) + logger.info("Browser sent badly-formatted If-Modified-Since header: " + modifiedSinceStr); + } + + if (modifiedSince > 0L) + { + // round the date to the ignore millisecond value which is not supplied by header + long modDate = (modified.getTime() / 1000L) * 1000L; + if (modDate <= modifiedSince) + { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + } + } + } + + // get the content reader + ContentReader reader = contentService.getReader(nodeRef, propertyQName); + if (reader == null || !reader.exists()) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to locate content for node ref " + nodeRef + " (property: " + propertyQName.toString() + ")"); + } + + // Stream the content + streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified, modified == null ? null : Long.toString(modified.getTime()), attachFileName, model); + } + + /** + * Streams content back to client from a given resource path. + * + * @param req The request + * @param res The response + * @param resourcePath The classpath resource path the content is required for + * @param attach Indicates whether the content should be streamed as an attachment or not + * @throws IOException + */ + public void streamContent(WebScriptRequest req, + WebScriptResponse res, + String resourcePath, + boolean attach, + Map model) throws IOException + { + streamContent(req, res, resourcePath, attach, null, model); + } + + /** + * Streams content back to client from a given resource path. + * + * @param req The request + * @param res The response + * @param resourcePath The classpath resource path the content is required for. + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + protected void streamContent(WebScriptRequest req, + WebScriptResponse res, + String resourcePath, + boolean attach, + String attachFileName, + Map model) throws IOException + { + if (logger.isDebugEnabled()) + logger.debug("Retrieving content from resource path " + resourcePath + " (attach: " + attach + ")"); + + // get extension of resource + String ext = ""; + int extIndex = resourcePath.lastIndexOf('.'); + if (extIndex != -1) + { + ext = resourcePath.substring(extIndex); + } + + // We need to retrieve the modification date/time from the resource itself. + StringBuilder sb = new StringBuilder("classpath:").append(resourcePath); + final String classpathResource = sb.toString(); + + long resourceLastModified = resourceLoader.getResource(classpathResource).lastModified(); + + // create temporary file + File file = TempFileProvider.createTempFile("streamContent-", ext); + + InputStream is = resourceLoader.getResource(classpathResource).getInputStream(); + OutputStream os = new FileOutputStream(file); + FileCopyUtils.copy(is, os); + + // stream the contents of the file, but using the modifiedDate of the original resource. + streamContent(req, res, file, resourceLastModified, attach, attachFileName, model); + } + + /** + * Stream content implementation + * + * @param req The request + * @param res The response + * @param reader The reader + * @param nodeRef The content nodeRef if applicable + * @param propertyQName The content property if applicable + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param modified Modified date of content + * @param eTag ETag to use + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + public void streamContentImpl(WebScriptRequest req, + WebScriptResponse res, + ContentReader reader, + final NodeRef nodeRef, + final QName propertyQName, + final boolean attach, + final Date modified, + String eTag, + final String attachFileName, + Map model) throws IOException + { + setAttachment(req, res, attach, attachFileName); + + // establish mimetype + String mimetype = reader.getMimetype(); + String extensionPath = req.getExtensionPath(); + if (mimetype == null || mimetype.length() == 0) + { + mimetype = MimetypeMap.MIMETYPE_BINARY; + int extIndex = extensionPath.lastIndexOf('.'); + if (extIndex != -1) + { + String ext = extensionPath.substring(extIndex + 1); + mimetype = mimetypeService.getMimetype(ext); + } + } + + res.setHeader(HEADER_ACCEPT_RANGES, "bytes"); + try + { + boolean processedRange = false; + String range = req.getHeader(HEADER_CONTENT_RANGE); + final long size = reader.getSize(); + final String encoding = reader.getEncoding(); + +// if (attach) +// { +// final String finalMimetype = mimetype; +// +// eventPublisher.publishEvent(new EventPreparator(){ +// @Override +// public Event prepareEvent(String user, String networkId, String transactionId) +// { +// String siteId = siteService.getSiteShortName(nodeRef); +// +// return new ContentEventImpl(ContentEvent.DOWNLOAD, user, networkId, transactionId, +// nodeRef.getId(), siteId, propertyQName.toString(), Client.asType(ClientType.webclient), attachFileName, finalMimetype, size, encoding); +// } +// }); +// } + + if (range == null) + { + range = req.getHeader(HEADER_RANGE); + } + if (range != null) + { + if (logger.isDebugEnabled()) + logger.debug("Found content range header: " + range); + + // ensure the range header is starts with "bytes=" and process the range(s) + if (range.length() > 6) + { + if (range.indexOf(',') != -1 && (nodeRef == null || propertyQName == null)) + { + if (logger.isInfoEnabled()) + logger.info("Multi-range only supported for nodeRefs"); + } + else { + HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService); + processedRange = rangeProcessor.processRange( + res, reader, range.substring(6), nodeRef, propertyQName, + mimetype, req.getHeader(HEADER_USER_AGENT)); + } + } + } + if (processedRange == false) + { + if (logger.isDebugEnabled()) + logger.debug("Sending complete file content..."); + + // set mimetype for the content and the character encoding for the stream + res.setContentType(mimetype); + res.setContentEncoding(encoding); + + // return the complete entity range + res.setHeader(HEADER_CONTENT_RANGE, "bytes 0-" + Long.toString(size-1L) + "/" + Long.toString(size)); + res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size)); + + // set caching + setResponseCache(res, modified, eTag, model); + + // get the content and stream directly to the response output stream + // assuming the repository is capable of streaming in chunks, this should allow large files + // to be streamed directly to the browser response stream. + reader.getContent( res.getOutputStream() ); + } + } + catch (SocketException e1) + { + // the client cut the connection - our mission was accomplished apart from a little error message + if (logger.isInfoEnabled()) + logger.info("Client aborted stream read:\n\tcontent: " + reader); + } + catch (ContentIOException e2) + { + if (logger.isInfoEnabled()) + logger.info("Client aborted stream read:\n\tcontent: " + reader); + } + } + + /** + * Set attachment header + * + * @param req WebScriptRequest + * @param res WebScriptResponse + * @param attach boolean + * @param attachFileName String + */ + public void setAttachment(WebScriptRequest req, WebScriptResponse res, boolean attach, String attachFileName) + { + if (attach == true) + { + String headerValue = "attachment"; + if (attachFileName != null && attachFileName.length() > 0) + { + if (logger.isDebugEnabled()) + logger.debug("Attaching content using filename: " + attachFileName); + + if (req == null) + { + headerValue += "; filename*=UTF-8''" + URLEncoder.encode(attachFileName) + + "; filename=\"" + filterNameForQuotedString(attachFileName) + "\""; + } + else + { + String userAgent = req.getHeader(HEADER_USER_AGENT); + boolean isLegacy = (null != userAgent) && (userAgent.contains("MSIE 8") || userAgent.contains("MSIE 7")); + if (isLegacy) + { + headerValue += "; filename=\"" + URLEncoder.encode(attachFileName); + } + else + { + headerValue += "; filename=\"" + filterNameForQuotedString(attachFileName) + "\"; filename*=UTF-8''" + + URLEncoder.encode(attachFileName); + } + } + } + + // set header based on filename - will force a Save As from the browse if it doesn't recognize it + // this is better than the default response of the browser trying to display the contents + res.setHeader("Content-Disposition", headerValue); + } + } + + protected String filterNameForQuotedString(String s) + { + StringBuilder sb = new StringBuilder(); + for(int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if(isValidQuotedStringHeaderParamChar(c)) + { + sb.append(c); + } + else + { + sb.append(" "); + } + } + return sb.toString(); + } + + protected boolean isValidQuotedStringHeaderParamChar(char c) + { + // see RFC2616 section 2.2: + // qdtext = > + // TEXT = + // CTL = + // A CRLF is allowed in the definition of TEXT only as part of a header field continuation. + // Note: we dis-allow header field continuation + return (c < 256) // message header param fields must be ISO-8859-1. Lower 256 codepoints of Unicode represent ISO-8859-1 + && (c != 127) // CTL - see RFC2616 section 2.2 + && (c != '"') // <"> + && (c > 31); // CTL - see RFC2616 section 2.2 + } + + /** + * Set the cache settings on the response + * + * @param res WebScriptResponse + * @param modified Date + * @param eTag String + */ + protected void setResponseCache(WebScriptResponse res, Date modified, String eTag, Map model) + { + Cache cache = new Cache(); + + Object obj; + if (model != null && (obj = model.get(KEY_CACHE_DIRECTIVE)) instanceof CacheDirective) + { + CacheDirective cacheDirective = (CacheDirective) obj; + cache.setNeverCache(cacheDirective.isNeverCache()); + cache.setMustRevalidate(cacheDirective.isMustRevalidate()); + cache.setMaxAge(cacheDirective.getMaxAge()); + cache.setLastModified(cacheDirective.getLastModified()); + cache.setETag(cacheDirective.getETag()); + cache.setIsPublic(cacheDirective.isPublic()); + } + else if (model == null || !getBooleanValue(model.get(KEY_ALLOW_BROWSER_TO_CACHE))) + { + // if 'allowBrowserToCache' is null or false + cache.setNeverCache(false); + cache.setMustRevalidate(true); + cache.setMaxAge(0L); + cache.setLastModified(modified); + cache.setETag(eTag); + } + else + { + cache.setNeverCache(false); + cache.setMustRevalidate(false); + cache.setMaxAge(Long.valueOf(31536000));// one year + cache.setLastModified(modified); + cache.setETag(eTag); + } + + res.setCache(cache); + } + + private boolean getBooleanValue(Object obj) + { + if (obj instanceof String) + { + return Boolean.valueOf((String) obj); + } + return Boolean.TRUE.equals(obj); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypeDescriptionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypeDescriptionsGet.java new file mode 100644 index 00000000000..c5fd0d63371 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypeDescriptionsGet.java @@ -0,0 +1,99 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.MimetypeService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Returns the descriptions of all the mimetypes known to the system. + * This is normally used so that things outside of the repo tier (eg Share) + * can display user-facing descriptions of the mimetypes in the system. + * + * @author Nick Burch + * @since 4.0.1 + */ +public class MimetypeDescriptionsGet extends DeclarativeWebScript +{ + public static final String MODEL_MIMETYPES = "mimetypes"; + public static final String MODEL_DEFAULT_EXTENSIONS = "defaultExtensions"; + public static final String MODEL_OTHER_EXTENSIONS = "otherExtensions"; + + private MimetypeService mimetypeService; + + /** + * Sets the Mimetype Service to be used to get the + * list of mime types + */ + public void setMimetypeService(MimetypeService mimetypeService) { + this.mimetypeService = mimetypeService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // Get the mimetypes and their descriptions + Map mimetypes = mimetypeService.getDisplaysByMimetype(); + + // Fetch all the extensions known to the system + Map extensions = mimetypeService.getMimetypesByExtension(); + // And the default extensions + Map defaultExtensions = mimetypeService.getExtensionsByMimetype(); + + // For each mimetype, work out the non-default extensions + Map> otherExtensions = new HashMap>(); + for (String extension : extensions.keySet()) + { + String mimetype = extensions.get(extension); + + // If this isn't the default, record it + if (! extension.equals(defaultExtensions.get(mimetype))) + { + if (! otherExtensions.containsKey(mimetype)) + { + otherExtensions.put(mimetype, new ArrayList()); + } + otherExtensions.get(mimetype).add(extension); + } + } + + // Return the model + Map model = new HashMap(); + model.put(MODEL_MIMETYPES, mimetypes); + model.put(MODEL_DEFAULT_EXTENSIONS, defaultExtensions); + model.put(MODEL_OTHER_EXTENSIONS, otherExtensions); + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java new file mode 100644 index 00000000000..d9a0b0a278f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java @@ -0,0 +1,171 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.content.metadata.MetadataExtracter; +import org.alfresco.repo.content.metadata.MetadataExtracterRegistry; +import org.alfresco.repo.content.transform.LocalTransformServiceRegistry; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Lists mimetypes. + * + * @author Nick Burch + * @since 3.4.b + */ +public class MimetypesGet extends DeclarativeWebScript +{ + public static final String MODEL_MIMETYPES = "mimetypes"; + public static final String MODEL_EXTENSIONS = "extensions"; + public static final String MODEL_MIMETYPE_DETAILS = "details"; + + private MimetypeService mimetypeService; + private TransformServiceRegistry localTransformServiceRegistry; + private MetadataExtracterRegistry metadataExtracterRegistry; + + /** + * Sets the Mimetype Service to be used to get the + * list of mime types + */ + public void setMimetypeService(MimetypeService mimetypeService) { + this.mimetypeService = mimetypeService; + } + + public void setLocalTransformServiceRegistry(TransformServiceRegistry localTransformServiceRegistry) + { + this.localTransformServiceRegistry = localTransformServiceRegistry; + } + + /** + * Sets the Metadata Extractor Registry to be used to + * decide what extractors exist + */ + public void setMetadataExtracterRegistry( + MetadataExtracterRegistry metadataExtracterRegistry) { + this.metadataExtracterRegistry = metadataExtracterRegistry; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // First up, get the list of mimetypes + // We want it to be sorted + String[] mimetypesA = mimetypeService.getMimetypes().toArray(new String[0]); + Arrays.sort(mimetypesA); + List mimetypes = new ArrayList( + Arrays.asList(mimetypesA) + ); + + // Now their extensions + Map extensions = new HashMap(); + for(String mimetype : mimetypes) + { + String ext = mimetypeService.getExtension(mimetype); + extensions.put(mimetype, ext); + } + + // Now details on those it was requested for + Map>> details = new HashMap>>(); + String reqMimetype = req.getParameter("mimetype"); + for(String mimetype : mimetypes) + { + if(mimetype.equals(reqMimetype) || "*".equals(reqMimetype)) + { + Map> mtd = new HashMap>(); + mtd.put("extractors", getExtractors(mimetype)); + mtd.put("transformFrom", getTransformersFrom(mimetype, -1, mimetypes)); + mtd.put("transformTo", getTransformersTo(mimetype, -1, mimetypes)); + details.put(mimetype, mtd); + } + } + + // Return the model + Map model = new HashMap(); + model.put(MODEL_MIMETYPES, mimetypes); + model.put(MODEL_EXTENSIONS, extensions); + model.put(MODEL_MIMETYPE_DETAILS, details); + return model; + } + + protected List getExtractors(String mimetype) + { + List exts = new ArrayList(); + MetadataExtracter extractor = metadataExtracterRegistry.getExtracter(mimetype); + if(extractor != null) { + exts.add( extractor.getClass().getName() ); + } + return exts; + } + + protected List getTransformersFrom(String mimetype, long sourceSize, List allMimetypes) + { + List transforms = new ArrayList(); + for(String toMT : allMimetypes) + { + if(toMT.equals(mimetype)) + continue; + + String details = getTransformer(mimetype, sourceSize, toMT); + if(details != null) + transforms.add(toMT + " = " + details); + } + return transforms; + } + + protected List getTransformersTo(String mimetype, long sourceSize, List allMimetypes) + { + List transforms = new ArrayList(); + for(String fromMT : allMimetypes) + { + if(fromMT.equals(mimetype)) + continue; + + String details = getTransformer(fromMT, sourceSize, mimetype); + if(details != null) + transforms.add(fromMT + " = " + details); + } + return transforms; + } + + /** Note - for now, only does the best one, not all */ + protected String getTransformer(String from, long sourceSize, String to) + { + return localTransformServiceRegistry.findTransformerName(from, sourceSize, to, Collections.emptyMap(), null); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamACP.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamACP.java new file mode 100644 index 00000000000..07740135e7a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamACP.java @@ -0,0 +1,244 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.util.GUID; +import org.alfresco.util.TempFileProvider; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Base class for Java backed webscripts that wish to generate an ACP and + * stream the contents back to the caller. + *

+ * The default implementation generates an ACP file containing the provided + * NodeRefs and all their respective children. + * + * @author Gavin Cornwell + */ +public class StreamACP extends StreamContent +{ + /** Logger */ + private static Log logger = LogFactory.getLog(StreamACP.class); + + protected static final String TEMP_FILE_PREFIX = "export_"; + protected static final String MULTIPART_FORMDATA = "multipart/form-data"; + protected static final String ZIP_EXTENSION = "zip"; + + protected static final String PARAM_NODE_REFS = "nodeRefs"; + + protected ExporterService exporterService; + + /** + * Sets the ExporterService to use + * + * @param exporterService The ExporterService + */ + public void setExporterService(ExporterService exporterService) + { + this.exporterService = exporterService; + } + + /** + * @see org.springframework.extensions.webscripts.WebScript#execute(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + File tempACPFile = null; + try + { + NodeRef[] nodeRefs = null; + String contentType = req.getContentType(); + if (MULTIPART_FORMDATA.equals(contentType)) + { + // get nodeRefs parameter from form + nodeRefs = getNodeRefs(req.getParameter(PARAM_NODE_REFS)); + } + else + { + // presume the request is a JSON request so get nodeRefs from JSON body + nodeRefs = getNodeRefs(new JSONObject(new JSONTokener(req.getContent().getContent()))); + } + + // setup the ACP parameters + ExporterCrawlerParameters params = new ExporterCrawlerParameters(); + params.setCrawlSelf(true); + params.setCrawlChildNodes(true); + params.setExportFrom(new Location(nodeRefs)); + + // create an ACP of the nodes + tempACPFile = createACP(params, ACPExportPackageHandler.ACP_EXTENSION, false); + + // stream the ACP back to the client as an attachment (forcing save as) + streamContent(req, res, tempACPFile, true, tempACPFile.getName(), null); + } + catch (IOException ioe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", ioe); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + finally + { + // try and delete the temporary file + if (tempACPFile != null) + { + if (logger.isDebugEnabled()) + logger.debug("Deleting temporary archive: " + tempACPFile.getAbsolutePath()); + + tempACPFile.delete(); + } + } + } + + /** + * Converts the given comma delimited string of NodeRefs to an array + * of NodeRefs. If the string is null a WebScriptException is thrown. + * + * @param nodeRefsParam Comma delimited string of NodeRefs + * @return Array of NodeRef objects + */ + protected NodeRef[] getNodeRefs(String nodeRefsParam) + { + // check the list of NodeRefs is present + if (nodeRefsParam == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'nodeRefs' parameter was not provided in form data"); + } + + List listNodeRefs = new ArrayList(8); + StringTokenizer tokenizer = new StringTokenizer(nodeRefsParam, ","); + while (tokenizer.hasMoreTokens()) + { + listNodeRefs.add(new NodeRef(tokenizer.nextToken().trim())); + } + + NodeRef[] nodeRefs = new NodeRef[listNodeRefs.size()]; + nodeRefs = listNodeRefs.toArray(nodeRefs); + + return nodeRefs; + } + + /** + * Attempts to retrieve and convert a JSON array of + * NodeRefs from the given JSON object. If the nodeRefs + * property is not present a WebScriptException is thrown. + * + * @param json JSONObject + * @return Array of NodeRef objects + */ + protected NodeRef[] getNodeRefs(JSONObject json) throws JSONException + { + // check the list of NodeRefs is present + if (!json.has(PARAM_NODE_REFS)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'nodeRefs' parameter was not provided in request body"); + } + + NodeRef[] nodeRefs = new NodeRef[0]; + JSONArray jsonArray = json.getJSONArray(PARAM_NODE_REFS); + if (jsonArray.length() != 0) + { + // build the list of NodeRefs + nodeRefs = new NodeRef[jsonArray.length()]; + for (int i = 0; i < jsonArray.length(); i++) + { + NodeRef nodeRef = new NodeRef(jsonArray.getString(i)); + nodeRefs[i] = nodeRef; + } + } + + return nodeRefs; + } + + /** + * Returns an ACP file containing the nodes represented by the given list of NodeRefs. + * + * @param params The parameters for the ACP exporter + * @param extension The file extenstion to use for the ACP file + * @param keepFolderStructure Determines whether the folder structure is maintained for + * the content inside the ACP file + * @return File object representing the created ACP + */ + protected File createACP(ExporterCrawlerParameters params, String extension, boolean keepFolderStructure) + { + try + { + // generate temp file and folder name + File dataFile = new File(GUID.generate()); + File contentDir = new File(GUID.generate()); + + // setup export package handler + File acpFile = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, "." + extension); + ACPExportPackageHandler handler = new ACPExportPackageHandler(new FileOutputStream(acpFile), + dataFile, contentDir, this.mimetypeService); + handler.setExportAsFolders(keepFolderStructure); + handler.setNodeService(this.nodeService); + + // perform the actual export + this.exporterService.exportView(handler, params, null); + + if (logger.isDebugEnabled()) + logger.debug("Created temporary archive: " + acpFile.getAbsolutePath()); + + return acpFile; + } + catch (FileNotFoundException fnfe) + { + throw new WebScriptException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Failed to create archive", fnfe); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamContent.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamContent.java new file mode 100644 index 00000000000..77cafae3b8c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamContent.java @@ -0,0 +1,477 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.Repository; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.ScriptProcessor; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WebScriptStatus; + +/** + * Web script 'type' that can be used when the binary data of a content property needs to be streamed back to the client + * as the result of executing the web script. + * + * Many of these methods have been moved into the ContentStreamer class so they can be reused by other webscripts. + * + * @author Roy Wetherall + */ +public class StreamContent extends AbstractWebScript +{ + // Logger + private static final Log logger = LogFactory.getLog(StreamContent.class); + + /** Services */ + protected PermissionService permissionService; + protected NodeService nodeService; + protected MimetypeService mimetypeService; + protected ContentStreamer delegate; + protected Repository repository; + + /** + * @param mimetypeService MimetypeService + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + /** + * @param permissionService PermissionService + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * @param nodeService NodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param delegate ContentStreamer + */ + public void setDelegate(ContentStreamer delegate) + { + this.delegate = delegate; + } + + /** + * @param repository Repository + */ + public void setRepository(Repository repository) + { + this.repository = repository; + } + + /** + * @see org.springframework.extensions.webscripts.WebScript#execute(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + // retrieve requested format + String format = req.getFormat(); + + try + { + // construct model for script / template + Status status = new Status(); + Cache cache = new Cache(getDescription().getRequiredCache()); + Map model = executeImpl(req, status, cache); + if (model == null) + { + model = new HashMap(8, 1.0f); + } + model.put("status", status); + model.put("cache", cache); + + // execute script if it exists + ScriptDetails executeScript = getExecuteScript(req.getContentType()); + if (executeScript != null) + { + if (logger.isDebugEnabled()) + logger.debug("Executing script " + executeScript.getContent().getPathDescription()); + + Map scriptModel = createScriptParameters(req, res, executeScript, model); + // add return model allowing script to add items to template model + Map returnModel = new HashMap(8, 1.0f); + scriptModel.put("model", returnModel); + executeScript(executeScript.getContent(), scriptModel); + mergeScriptModelIntoTemplateModel(executeScript.getContent().getPath(), returnModel, model); + } + + // is a redirect to a status specific template required? + if (status.getRedirect()) + { + // create model for template rendering + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + } + else + { + // Get the attachement property value + Boolean attachBoolean = (Boolean)model.get("attach"); + boolean attach = false; + if (attachBoolean != null) + { + attach = attachBoolean.booleanValue(); + } + + String contentPath = (String)model.get("contentPath"); + if (contentPath == null) + { + // Get the content parameters from the model + NodeRef nodeRef = (NodeRef)model.get("contentNode"); + if (nodeRef == null) + { + throw new WebScriptException( + "The content node was not specified so the content cannot be streamed to the client: " + + executeScript.getContent().getPathDescription()); + } + QName propertyQName = null; + String contentProperty = (String)model.get("contentProperty"); + if (contentProperty == null) + { + // default to the standard content property + propertyQName = ContentModel.PROP_CONTENT; + } + else + { + propertyQName = QName.createQName(contentProperty); + } + + // Stream the content + delegate.streamContent(req, res, nodeRef, propertyQName, attach, null, model); + } + else + { + // Stream the content + delegate.streamContent(req, res, contentPath, attach, model); + } + } + } + catch(Throwable e) + { + throw createStatusException(e, req, res); + } + } + + /** + * Set attachment header + * + * @param res WebScriptResponse + * @param attach boolean + * @param attachFileName String + */ + protected void setAttachment(WebScriptResponse res, boolean attach, String attachFileName) + { + delegate.setAttachment(null, res, attach, attachFileName); + } + + /** + * Streams content back to client from a given File. The Last-Modified header will reflect the + * given file's modification timestamp. + * + * @param req The request + * @param res The response + * @param file The file whose content is to be streamed. + * @throws IOException + */ + protected void streamContent(WebScriptRequest req, WebScriptResponse res, File file) throws IOException { + streamContent(req, res, file, false, null, null); + } + + /** + * Streams content back to client from a given File. The Last-Modified header will reflect the + * given file's modification timestamp. + * + * @param req The request + * @param res The response + * @param file The file whose content is to be streamed. + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + protected void streamContent(WebScriptRequest req, + WebScriptResponse res, + File file, + boolean attach, + String attachFileName, + Map model) throws IOException + { + delegate.streamContent(req, res, file, null, attach, attachFileName, model); + } + + /** + * Streams the content on a given node's content property to the response of the web script. + * + * @param req Request + * @param res Response + * @param nodeRef The node reference + * @param propertyQName The content property name + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + protected void streamContent(WebScriptRequest req, + WebScriptResponse res, + NodeRef nodeRef, + QName propertyQName, + boolean attach, + String attachFileName, + Map model) throws IOException + { + delegate.streamContent(req, res, nodeRef, propertyQName, attach, attachFileName, model); + } + + /** + * Stream content implementation + * + * @param req The request + * @param res The response + * @param reader The reader + * @param nodeRef The content nodeRef if applicable + * @param propertyQName The content property if applicable + * @param attach Indicates whether the content should be streamed as an attachment or not + * @param modified Modified date of content + * @param eTag ETag to use + * @param attachFileName Optional file name to use when attach is true + * @throws IOException + */ + protected void streamContentImpl(WebScriptRequest req, + WebScriptResponse res, + ContentReader reader, + NodeRef nodeRef, + QName propertyQName, + boolean attach, + Date modified, + String eTag, + String attachFileName, + Map model) throws IOException + { + delegate.streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified, eTag, attachFileName, model); + } + + /** + * Merge script generated model into template-ready model + * + * @param scriptPath path to script + * @param scriptModel script model + * @param templateModel template model + */ + final private void mergeScriptModelIntoTemplateModel(String scriptPath, Map scriptModel, Map templateModel) + { + int i = scriptPath.lastIndexOf("."); + if (i != -1) + { + String extension = scriptPath.substring(i+1); + ScriptProcessor processor = getContainer().getScriptProcessorRegistry().getScriptProcessorByExtension(extension); + if (processor != null) + { + for (Map.Entry entry : scriptModel.entrySet()) + { + // retrieve script model value + Object value = entry.getValue(); + Object templateValue = processor.unwrapValue(value); + templateModel.put(entry.getKey(), templateValue); + } + } + } + } + + /** + * Execute custom Java logic + * + * @param req Web Script request + * @param status Web Script status + * @return custom service model + * @deprecated + */ + protected Map executeImpl(WebScriptRequest req, WebScriptStatus status) + { + return null; + } + + /** + * Execute custom Java logic + * + * @param req Web Script request + * @param status Web Script status + * @return custom service model + * @deprecated + */ + protected Map executeImpl(WebScriptRequest req, Status status) + { + return executeImpl(req, new WebScriptStatus(status)); + } + + /** + * Execute custom Java logic + * + * @param req Web Script request + * @param status Web Script status + * @param cache Web Script cache + * @return custom service model + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // NOTE: Redirect to those web scripts implemented before cache support and v2.9 + return executeImpl(req, status); + } + + /** + * Render a template (of given format) to the Web Script Response + * + * @param format template format (null, default format) + * @param model data model to render + * @param writer where to output + */ + final protected void renderFormatTemplate(String format, Map model, Writer writer) + { + format = (format == null) ? "" : format; + String templatePath = getDescription().getId() + "." + format + ".ftl"; + + if (logger.isDebugEnabled()) + logger.debug("Rendering template '" + templatePath + "'"); + + renderTemplate(templatePath, model, writer); + } + + protected ObjectReference createObjectReferenceFromUrl(Map args, Map templateArgs) + { + String objectId = args.get("noderef"); + if (objectId != null) + { + return new ObjectReference(objectId); + } + + StoreRef storeRef = null; + String store_type = templateArgs.get("store_type"); + String store_id = templateArgs.get("store_id"); + if (store_type != null && store_id != null) + { + storeRef = new StoreRef(store_type, store_id); + } + + String id = templateArgs.get("id"); + if (storeRef != null && id != null) + { + return new ObjectReference(storeRef, id); + } + + String nodepath = templateArgs.get("nodepath"); + if (nodepath == null) + { + nodepath = args.get("nodepath"); + } + if (storeRef != null && nodepath != null) + { + return new ObjectReference(storeRef, nodepath.split("/")); + } + + return null; + } + + + class ObjectReference + { + private NodeRef ref; + + ObjectReference(String nodeRef) + { + this.ref = new NodeRef(nodeRef); + } + + ObjectReference(StoreRef ref, String id) + { + String[] relativePath = id.split("/"); + + // bug fix MNT-16380 + // for using a relative path to a node id eg. 18cc-.../folder1/.../folderN/fileA.txt + // if only one slash we don't have a relative path + if (relativePath.length <= 2) + { + if (id.indexOf('/') != -1) + { + id = id.substring(0, id.indexOf('/')); + } + this.ref = new NodeRef(ref, id); + } + else + { + String[] reference = new String[relativePath.length + 2]; + reference[0] = ref.getProtocol(); + reference[1] = ref.getIdentifier(); + System.arraycopy(relativePath, 0, reference, 2, relativePath.length); + this.ref = repository.findNodeRef("node", reference); + } + } + + ObjectReference(StoreRef ref, String[] path) + { + String[] reference = new String[path.length + 2]; + reference[0] = ref.getProtocol(); + reference[1] = ref.getIdentifier(); + System.arraycopy(path, 0, reference, 2, path.length); + this.ref = repository.findNodeRef("path", reference); + } + + public NodeRef getNodeRef() + { + return this.ref; + } + + @Override + public String toString() + { + return ref != null ? ref.toString() : super.toString(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamJMXDump.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamJMXDump.java new file mode 100644 index 00000000000..1e02e9fec52 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/StreamJMXDump.java @@ -0,0 +1,127 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.content; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.management.MBeanServerConnection; + +import org.alfresco.repo.management.JmxDumpUtil; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.util.FileCopyUtils; + +/** + * WebScript java backed bean implementation to stream the JMX property + * dump as a .zip content file. + * + * @author Kevin Roast + */ +public class StreamJMXDump extends StreamContent +{ + /** Logger */ + private static Log logger = LogFactory.getLog(StreamJMXDump.class); + + /** The MBean server. */ + private MBeanServerConnection mbeanServer; + + + /** + * @param mBeanServer MBeanServerConnection bean + */ + public void setMBeanServer(MBeanServerConnection mBeanServer) + { + this.mbeanServer = mBeanServer; + } + + /** + * @see org.springframework.extensions.webscripts.WebScript#execute(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + PrintWriter tempFileOut = null; + ZipOutputStream zout = null; + try + { + // content type and caching + res.setContentType("application/zip"); + Cache cache = new Cache(); + cache.setNeverCache(true); + cache.setMustRevalidate(true); + cache.setMaxAge(0L); + res.setCache(cache); + + Date date = new Date(); + String attachFileName = "jmxdump_" + (date.getYear()+1900) + '_' + (date.getMonth()+1) + '_' + (date.getDate()); + String headerValue = "attachment; filename=\"" + attachFileName + ".zip\""; + + // set header based on filename - will force a Save As from the browse if it doesn't recognize it + // this is better than the default response of the browser trying to display the contents + res.setHeader("Content-Disposition", headerValue); + + // write JMX data to temp file + File tempFile = TempFileProvider.createTempFile("jmxdump", ".txt"); + tempFileOut = new PrintWriter(tempFile); + JmxDumpUtil.dumpConnection(mbeanServer, tempFileOut); + tempFileOut.flush(); + tempFileOut.close(); + tempFileOut = null; + + // zip output + zout = new ZipOutputStream(res.getOutputStream()); + ZipEntry zipEntry = new ZipEntry(attachFileName + ".txt"); + zout.putNextEntry(zipEntry); + FileCopyUtils.copy(new FileInputStream(tempFile), zout); + zout = null; + } + catch (IOException ioe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not output JMX dump: " + ioe.getMessage(), ioe); + } + finally + { + if (tempFileOut != null) tempFileOut.close(); + try + { + if (zout != null) zout.close(); + } + catch (IOException e1) {} + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/custommodel/CustomModelUploadPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/custommodel/CustomModelUploadPost.java new file mode 100644 index 00000000000..0ce8b2ea965 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/custommodel/CustomModelUploadPost.java @@ -0,0 +1,313 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.custommodel; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import javax.xml.parsers.DocumentBuilder; + +import org.alfresco.repo.dictionary.CustomModelServiceImpl; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.CustomModel; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.service.cmr.dictionary.CustomModelService; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.util.TempFileProvider; +import org.alfresco.util.XMLUtil; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.servlet.FormData; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * Custom model upload POST. This class is the controller for the + * "cmm-upload.post" web scripts. + * + * @author Jamal Kaabi-Mofrad + */ +public class CustomModelUploadPost extends DeclarativeWebScript +{ + private static final String SHARE_EXT_MODULE_ROOT_ELEMENT = "module"; + private static final String TEMP_FILE_PREFIX = "cmmExport"; + private static final String TEMP_FILE_SUFFIX = ".zip"; + private static final int BUFFER_SIZE = 10 * 1024; + + private CustomModels customModels; + private CustomModelService customModelService; + + public void setCustomModels(CustomModels customModels) + { + this.customModels = customModels; + } + + public void setCustomModelService(CustomModelService customModelService) + { + this.customModelService = customModelService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + if (!customModelService.isModelAdmin(AuthenticationUtil.getFullyAuthenticatedUser())) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, PermissionDeniedException.DEFAULT_MESSAGE_ID); + } + + FormData formData = (FormData) req.parseContent(); + if (formData == null || !formData.getIsMultiPart()) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_not_multi_part_req"); + } + + ImportResult resultData = null; + boolean processed = false; + for (FormData.FormField field : formData.getFields()) + { + if (field.getIsFile()) + { + final String fileName = field.getFilename(); + File tempFile = createTempFile(field.getInputStream()); + try (ZipFile zipFile = new ZipFile(tempFile, StandardCharsets.UTF_8)) + { + resultData = processUpload(zipFile, field.getFilename()); + } + catch (ZipException ze) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_not_zip_format", new Object[] { fileName }); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "cmm.rest_api.model.import_process_zip_file_failure", io); + } + finally + { + // now the import is done, delete the temp file + tempFile.delete(); + } + processed = true; + break; + } + + } + + if (!processed) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_no_zip_file_uploaded"); + } + + // If we get here, then importing the custom model didn't throw any exceptions. + Map model = new HashMap<>(2); + model.put("importedModelName", resultData.getImportedModelName()); + model.put("shareExtXMLFragment", resultData.getShareExtXMLFragment()); + + return model; + } + + protected File createTempFile(InputStream inputStream) + { + try + { + File tempFile = TempFileProvider.createTempFile(inputStream, TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX); + return tempFile; + } + catch (Exception ex) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "cmm.rest_api.model.import_process_zip_file_failure", ex); + } + } + + protected ImportResult processUpload(ZipFile zipFile, String filename) throws IOException + { + if (zipFile.size() > 2) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_zip_package"); + } + + CustomModel customModel = null; + String shareExtModule = null; + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) + { + ZipEntry entry = entries.nextElement(); + + if (!entry.isDirectory()) + { + final String entryName = entry.getName(); + try (InputStream input = new BufferedInputStream(zipFile.getInputStream(entry), BUFFER_SIZE)) + { + if (!(entryName.endsWith(CustomModelServiceImpl.SHARE_EXT_MODULE_SUFFIX)) && customModel == null) + { + try + { + M2Model m2Model = M2Model.createModel(input); + customModel = importModel(m2Model); + } + catch (DictionaryException ex) + { + if (shareExtModule == null) + { + // Get the input stream again, as the zip file doesn't support reset. + try (InputStream moduleInputStream = new BufferedInputStream(zipFile.getInputStream(entry), BUFFER_SIZE)) + { + shareExtModule = getExtensionModule(moduleInputStream, entryName); + } + + if (shareExtModule == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_zip_entry_format", new Object[] { entryName }); + } + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_model_entry", new Object[] { entryName }); + } + } + } + else + { + shareExtModule = getExtensionModule(input, entryName); + if (shareExtModule == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_ext_module_entry", new Object[] { entryName }); + } + } + } + } + } + + return new ImportResult(customModel, shareExtModule); + } + + protected CustomModel importModel(M2Model m2Model) + { + CustomModel model = null; + try + { + model = customModels.createCustomModel(m2Model); + } + catch (Exception ex) + { + int statusCode; + if (ex instanceof ConstraintViolatedException) + { + statusCode = Status.STATUS_CONFLICT; + } + else if (ex instanceof InvalidArgumentException) + { + statusCode = Status.STATUS_BAD_REQUEST; + } + else + { + statusCode = Status.STATUS_INTERNAL_SERVER_ERROR; + } + String msg = ex.getMessage(); + // remove log numbers. regEx => match 8 or more integers + msg = (msg != null) ? msg.replaceAll("\\d{8,}", "").trim() : "cmm.rest_api.model.import_failure"; + + throw new WebScriptException(statusCode, msg); + } + + return model; + } + + protected String getExtensionModule(InputStream inputStream, String fileName) + { + Element rootElement = null; + try + { + final DocumentBuilder db = XMLUtil.getDocumentBuilder(); + rootElement = db.parse(inputStream).getDocumentElement(); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "cmm.rest_api.model.import_process_ext_module_file_failure", io); + } + catch (SAXException ex) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "cmm.rest_api.model.import_invalid_ext_module_entry", new Object[] { fileName }, ex); + } + + if (rootElement != null && SHARE_EXT_MODULE_ROOT_ELEMENT.equals(rootElement.getNodeName())) + { + StringWriter sw = new StringWriter(); + XMLUtil.print(rootElement, sw, false); + + return sw.toString(); + } + + return null; + } + + /** + * Simple POJO for model import result. + * + * @author Jamal Kaabi-Mofrad + */ + public static class ImportResult + { + private String importedModelName; + private String shareExtXMLFragment; + + public ImportResult(CustomModel customModel, String shareExtXMLFragment) + { + this.shareExtXMLFragment = shareExtXMLFragment; + if (customModel != null) + { + this.importedModelName = customModel.getName(); + } + } + + public String getImportedModelName() + { + return this.importedModelName; + } + + public String getShareExtXMLFragment() + { + return this.shareExtXMLFragment; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractAssociationGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractAssociationGet.java new file mode 100644 index 00000000000..ca3e93217e0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractAssociationGet.java @@ -0,0 +1,75 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/* + * Webscript to get the Associationdefinition for a given classname and association-name + * @author Viachaslau Tsikhanovich + */ + +public abstract class AbstractAssociationGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_ASSOCIATION_DETAILS = "assocdefs"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(1); + QName classQname = getClassQname(req); + QName associationQname = getAssociationQname(req); + + if (this.dictionaryservice.getClass(classQname).getAssociations().get(associationQname) != null) + { + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, this.dictionaryservice.getClass(classQname).getAssociations().get(associationQname)); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, this.dictionaryservice); + } + + return model; + } + + /** + * @param req - webscript request + * @return qualified name for association + */ + protected abstract QName getAssociationQname(WebScriptRequest req); + + /** + * @param req - webscript request + * @return qualified name for class + */ + protected abstract QName getClassQname(WebScriptRequest req); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractAssociationsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractAssociationsGet.java new file mode 100644 index 00000000000..72322f9e468 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractAssociationsGet.java @@ -0,0 +1,136 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Associationdefinitions for a given classname + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public abstract class AbstractAssociationsGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_ASSOCIATION_DETAILS = "assocdefs"; + private static final String MODEL_PROP_KEY_INDIVIDUAL_PROPERTY_DEFS = "individualproperty"; + private static final String REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX = "nsp"; + private static final String REQ_URL_TEMPL_VAR_NAME = "n"; + private static final String REQ_URL_TEMPL_VAR_ASSOCIATION_FILTER = "af"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String associationFilter = req.getParameter(REQ_URL_TEMPL_VAR_ASSOCIATION_FILTER); + String namespacePrefix = req.getParameter(REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX); + String name = req.getParameter(REQ_URL_TEMPL_VAR_NAME); + + Map model = new HashMap(); + Map assocdef = new HashMap(); + QName associationQname = null; + QName classQname = getClassQname(req); + + if(associationFilter == null) + { + associationFilter = "all"; + } + + //validate association filter + if(isValidAssociationFilter(associationFilter) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the associationFilter - " + associationFilter + " - parameter in the URL"); + } + + // validate for the presence of both name and namespaceprefix + if((name == null && namespacePrefix != null) || + (name != null && namespacePrefix == null)) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing either name or namespaceprefix parameter in the URL - both combination of name and namespaceprefix is needed"); + } + + // check for association filters + if(associationFilter.equals("child")) + { + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, this.dictionaryservice.getClass(classQname).getChildAssociations().values()); + } + else if(associationFilter.equals("general")) + { + for(AssociationDefinition assocname:this.dictionaryservice.getClass(classQname).getAssociations().values()) + { + if(assocname.isChild() == false) + { + assocdef.put(assocname.getName(), assocname); + } + } + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, assocdef.values()); + } + else if(associationFilter.equals("all")) + { + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, this.dictionaryservice.getClass(classQname).getAssociations().values()); + } + + // if both namespaceprefix and name parameters are given then, the combination namespaceprefix_name is used as the index to create the qname + if(name != null && namespacePrefix != null) + { + // validate the class combination namespaceprefix_name + associationQname = getAssociationQname(namespacePrefix, name); + + if(this.dictionaryservice.getClass(classQname).getAssociations().get(associationQname)== null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "not a Valid - namespaceprefix_name combination"); + } + + model.put(MODEL_PROP_KEY_INDIVIDUAL_PROPERTY_DEFS, this.dictionaryservice.getClass(classQname).getAssociations().get(associationQname)); + } + + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, this.dictionaryservice); + + return model; + } + + /** + * @param req - webscript request + * @return qualified name for class + */ + protected abstract QName getClassQname(WebScriptRequest req); + + /** + * @param namespacePrefix - namespace prefix + * @param name - name + * @return qualified name for association + */ + protected abstract QName getAssociationQname(String namespacePrefix, String name); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractClassGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractClassGet.java new file mode 100644 index 00000000000..4fcc732bfd8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractClassGet.java @@ -0,0 +1,80 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Classdefinitions for a classname eg. =>cm_author + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public abstract class AbstractClassGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_CLASS_DETAILS = "classdefs"; + private static final String MODEL_PROP_KEY_PROPERTY_DETAILS = "propertydefs"; + private static final String MODEL_PROP_KEY_ASSOCIATION_DETAILS = "assocdefs"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status) + { + Map model = new HashMap(3); + Map classdef = new HashMap(); + Map> propdef = new HashMap>(); + Map> assocdef = new HashMap>(); + + QName classQname = getClassQname(req); + classdef.put(classQname, this.dictionaryservice.getClass(classQname)); + propdef.put(classQname, this.dictionaryservice.getClass(classQname).getProperties().values()); + assocdef.put(classQname, this.dictionaryservice.getClass(classQname).getAssociations().values()); + + model.put(MODEL_PROP_KEY_CLASS_DETAILS, classdef.values()); + model.put(MODEL_PROP_KEY_PROPERTY_DETAILS, propdef.values()); + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, assocdef.values()); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, this.dictionaryservice); + + return model; + } + + /** + * @param req - webscript request + * @return qualified name for class + */ + protected abstract QName getClassQname(WebScriptRequest req); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractClassesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractClassesGet.java new file mode 100644 index 00000000000..99de37b784e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractClassesGet.java @@ -0,0 +1,193 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Classdefinitions using classfilter , namespaceprefix and name + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public abstract class AbstractClassesGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_CLASS_DEFS = "classdefs"; + private static final String MODEL_PROP_KEY_PROPERTY_DETAILS = "propertydefs"; + private static final String MODEL_PROP_KEY_ASSOCIATION_DETAILS = "assocdefs"; + + private static final String CLASS_FILTER_OPTION_TYPE1 = "all"; + private static final String CLASS_FILTER_OPTION_TYPE2 = "aspect"; + private static final String CLASS_FILTER_OPTION_TYPE3 = "type"; + + private static final String REQ_URL_TEMPL_VAR_CLASS_FILTER = "cf"; + private static final String REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX = "nsp"; + private static final String REQ_URL_TEMPL_VAR_NAME = "n"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String classFilter = getValidInput(req.getParameter(REQ_URL_TEMPL_VAR_CLASS_FILTER)); + String namespacePrefix = getValidInput(req.getParameter(REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX)); + String name = getValidInput(req.getParameter(REQ_URL_TEMPL_VAR_NAME)); + + Map classdef = new HashMap(); + Map> propdef = new HashMap>(); + Map> assocdef = new HashMap>(); + Map model = new HashMap(); + + List qnames = new ArrayList(); + QName classQname = null; + QName myModel = null; + + // if classfilter is not given, then it defaults to all + if (classFilter == null) + { + classFilter = "all"; + } + + // validate classfilter + if (isValidClassFilter(classFilter) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classfilter - " + classFilter + " provided in the URL"); + } + + // name alone has no meaning without namespaceprefix + if (namespacePrefix == null && name != null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing namespaceprefix parameter in the URL - both combination of name and namespaceprefix is needed"); + } + + // validate the namespaceprefix and name parameters => if namespaceprefix is given, then name has to be validated along with it + if (namespacePrefix != null) + { + // validate name parameter if present along with the namespaceprefix + if (name != null) + { + classQname = getClassQname(namespacePrefix, name); + classdef.put(classQname, this.dictionaryservice.getClass(classQname)); + propdef.put(classQname, this.dictionaryservice.getClass(classQname).getProperties().values()); + assocdef.put(classQname, this.dictionaryservice.getClass(classQname).getAssociations().values()); + } + else + { + // if name is not given then the model is extracted from the namespaceprefix, there can be more than one model associated with one namespaceprefix + String namespaceUri = namespaceService.getNamespaceURI(namespacePrefix); + for (QName qnameObj : this.dictionaryservice.getAllModels()) + { + if (qnameObj.getNamespaceURI().equals(namespaceUri)) + { + name = qnameObj.getLocalName(); + myModel = getQNameForModel(namespacePrefix, name); + + // check the classfilter to pull out either all or type or aspects + if (classFilter.equalsIgnoreCase(CLASS_FILTER_OPTION_TYPE1)) + { + qnames.addAll(this.dictionaryservice.getAspects(myModel)); + qnames.addAll(this.dictionaryservice.getTypes(myModel)); + } + else if (classFilter.equalsIgnoreCase(CLASS_FILTER_OPTION_TYPE3)) + { + qnames.addAll(this.dictionaryservice.getTypes(myModel)); + } + else if (classFilter.equalsIgnoreCase(CLASS_FILTER_OPTION_TYPE2)) + { + qnames.addAll(this.dictionaryservice.getAspects(myModel)); + } + } + } + } + } + + // if namespacePrefix is null, then check the classfilter to pull out either all or type or aspects + if (myModel == null) + { + if (classFilter.equalsIgnoreCase(CLASS_FILTER_OPTION_TYPE1)) + { + qnames.addAll(this.dictionaryservice.getAllAspects()); + qnames.addAll(this.dictionaryservice.getAllTypes()); + } + else if (classFilter.equalsIgnoreCase(CLASS_FILTER_OPTION_TYPE3)) + { + qnames.addAll(this.dictionaryservice.getAllTypes()); + } + else if (classFilter.equalsIgnoreCase(CLASS_FILTER_OPTION_TYPE2)) + { + qnames.addAll(this.dictionaryservice.getAllAspects()); + } + } + + if (classdef.isEmpty() == true) + { + for (QName qnameObj : qnames) + { + classdef.put(qnameObj, this.dictionaryservice.getClass(qnameObj)); + propdef.put(qnameObj, this.dictionaryservice.getClass(qnameObj).getProperties().values()); + assocdef.put(qnameObj, this.dictionaryservice.getClass(qnameObj).getAssociations().values()); + } + } + + List classDefinitions = new ArrayList(classdef.values()); + Collections.sort(classDefinitions, new DictionaryComparators.ClassDefinitionComparator(dictionaryservice)); + model.put(MODEL_PROP_KEY_CLASS_DEFS, classDefinitions); + model.put(MODEL_PROP_KEY_PROPERTY_DETAILS, reorderedValues(classDefinitions, propdef)); + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, reorderedValues(classDefinitions, assocdef)); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, dictionaryservice); + + return model; + } + + /** + * @param namespacePrefix - namespace prefix of the class + * @param name - local name of the class + * @return qualified name for model + */ + protected abstract QName getQNameForModel(String namespacePrefix, String name); + + /** + * @param namespacePrefix namespace prefix + * @param name name + * @return qualified name for class + */ + protected abstract QName getClassQname(String namespacePrefix, String name); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractPropertiesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractPropertiesGet.java new file mode 100644 index 00000000000..10940bc86d3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractPropertiesGet.java @@ -0,0 +1,157 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Propertydefinitions for a given classname eg. =>cm_person + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public abstract class AbstractPropertiesGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_PROPERTY_DETAILS = "propertydefs"; + private static final String PARAM_NAME = "name"; + private static final String REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX = "nsp"; + + /** + * This request parameter can be passed to filter the propertes based on + * type. More than one type can be supplied. + */ + private static final String REQ_PARM_ALLOWED_TYPE = "type"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + QName classQName = getClassQName(req); + + String[] names = req.getParameterValues(PARAM_NAME); + + String namespacePrefix = req.getParameter(REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX); + String namespaceURI = null; + if (namespacePrefix != null) + { + namespaceURI = this.namespaceService.getNamespaceURI(namespacePrefix); + } + + Map propMap = null; + if (classQName == null) + { + if (names != null) + { + propMap = new HashMap(names.length); + for (String name : names) + { + QName propQName = QName.createQName(name, namespaceService); + PropertyDefinition propDef = dictionaryservice.getProperty(propQName); + if (propDef != null) + { + propMap.put(propQName, propDef); + } + } + } + else + { + Collection propQNames = dictionaryservice.getAllProperties(null); + propMap = new HashMap(propQNames.size()); + for (QName propQName : propQNames) + { + propMap.put(propQName, dictionaryservice.getProperty(propQName)); + } + } + + } + else + { + // Get all the property definitions for the class + propMap = dictionaryservice.getClass(classQName).getProperties(); + } + + // Filter the properties by URI + List props = new ArrayList(propMap.size()); + for (Map.Entry entry : propMap.entrySet()) + { + if ((namespaceURI != null && namespaceURI.equals(entry.getKey().getNamespaceURI()) == true) || namespaceURI == null) + { + props.add(entry.getValue()); + } + } + + // Filter the properties by the allowed types... + String[] filterTypes = req.getParameterValues(REQ_PARM_ALLOWED_TYPE); + if (filterTypes != null && filterTypes.length > 0) + { + List typeFilteredProps = new ArrayList(props.size()); + for (PropertyDefinition prop: props) + { + for (String type: filterTypes) + { + DataTypeDefinition dtd = prop.getDataType(); + if (dtd.getName().getPrefixString().equals(type)) + { + typeFilteredProps.add(prop); + break; + } + } + } + + // Important to change the props variable to reference the type filtered properties... + props = typeFilteredProps; + } + + // Order property definitions by title + Collections.sort(props, new DictionaryComparators.PropertyDefinitionComparator(dictionaryservice)); + + // Pass list of property definitions to template + Map model = new HashMap(); + model.put(MODEL_PROP_KEY_PROPERTY_DETAILS, props); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, dictionaryservice); + return model; + } + + /** + * @param req - webscript request + * @return qualified name for class + */ + protected abstract QName getClassQName(WebScriptRequest req); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractPropertyGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractPropertyGet.java new file mode 100644 index 00000000000..ba917fc33f7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractPropertyGet.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Propertydefinition for a given classname and propname + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public abstract class AbstractPropertyGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_PROPERTY_DETAILS = "propertydefs"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(1); + QName classQname = getClassQname(req); + QName propertyQname = getPropertyQname(req); + + if (this.dictionaryservice.getClass(classQname).getProperties().get(propertyQname) != null) + { + model.put(MODEL_PROP_KEY_PROPERTY_DETAILS, this.dictionaryservice.getClass(classQname).getProperties().get(propertyQname)); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, this.dictionaryservice); + } + + return model; + } + + /** + * @param req - webscript request + * @return qualified name for property + */ + protected abstract QName getPropertyQname(WebScriptRequest req); + + /** + * @param req - webscript request + * @return qualified name for class + */ + protected abstract QName getClassQname(WebScriptRequest req); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractSubClassesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractSubClassesGet.java new file mode 100644 index 00000000000..53acfc26c04 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AbstractSubClassesGet.java @@ -0,0 +1,167 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Sub-Classdefinitions using classfilter , namespacePrefix and name + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public abstract class AbstractSubClassesGet extends DictionaryWebServiceBase +{ + private static final String MODEL_PROP_KEY_CLASS_DEFS = "classdefs"; + private static final String MODEL_PROP_KEY_PROPERTY_DETAILS = "propertydefs"; + private static final String MODEL_PROP_KEY_ASSOCIATION_DETAILS = "assocdefs"; + + private static final String REQ_URL_TEMPL_IMMEDIATE_SUB_TYPE_CHILDREN = "r"; + // private static final String REQ_URL_TEMPL_VAR_CLASS_FILTER = "cf"; + private static final String REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX = "nsp"; + private static final String REQ_URL_TEMPL_VAR_NAME = "n"; + + /** + * Override method from DeclarativeWebScript + */ + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String name = req.getParameter(REQ_URL_TEMPL_VAR_NAME); + String namespacePrefix = req.getParameter(REQ_URL_TEMPL_VAR_NAMESPACE_PREFIX); + String recursiveValue = getValidInput(req.getParameter(REQ_URL_TEMPL_IMMEDIATE_SUB_TYPE_CHILDREN)); + + boolean recursive = true; + + Map classdef = new HashMap(); + Map> propdef = new HashMap>(); + Map> assocdef = new HashMap>(); + Map model = new HashMap(); + + String namespaceUri = null; + Collection qname = null; + boolean ignoreCheck = false; + + // validate recursive parameter => can be either true or false or null + if (recursiveValue == null) + { + recursive = true; + } + else if (recursiveValue.equalsIgnoreCase("true")) + { + recursive = true; + } + else if (recursiveValue.equalsIgnoreCase("false")) + { + recursive = false; + } + else + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the value for the parameter recursive=> " + recursiveValue + " can only be either true or false"); + } + + qname = getQNameCollection(req, recursive); + + // validate the name parameter + if (name != null) + { + if (isValidModelName(name) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the name parameter - " + name + " in the URL"); + } + } + + // validate the name parameter + if (namespacePrefix == null && name != null) + { + namespaceUri = namespaceService.getNamespaceURI(getPrefixFromModelName(name)); + } + + if (namespacePrefix != null && name == null) + { + namespaceUri = namespaceService.getNamespaceURI(namespacePrefix); + } + + if (namespacePrefix == null && name == null) + { + namespaceUri = null; + ignoreCheck = true; + } + + if (namespacePrefix != null && name != null) + { + validateClassname(namespacePrefix, name); + namespaceUri = namespaceService.getNamespaceURI(namespacePrefix); + } + + for (QName qnameObj : qname) + { + if ((ignoreCheck == true) || (qnameObj.getNamespaceURI().equals(namespaceUri))) + { + classdef.put(qnameObj, this.dictionaryservice.getClass(qnameObj)); + propdef.put(qnameObj, this.dictionaryservice.getClass(qnameObj).getProperties().values()); + assocdef.put(qnameObj, this.dictionaryservice.getClass(qnameObj).getAssociations().values()); + } + } + + List classDefinitions = new ArrayList(classdef.values()); + Collections.sort(classDefinitions, new DictionaryComparators.ClassDefinitionComparator(dictionaryservice)); + model.put(MODEL_PROP_KEY_CLASS_DEFS, classDefinitions); + model.put(MODEL_PROP_KEY_PROPERTY_DETAILS, reorderedValues(classDefinitions, propdef)); + model.put(MODEL_PROP_KEY_ASSOCIATION_DETAILS, reorderedValues(classDefinitions, assocdef)); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, this.dictionaryservice); + return model; + + } + + /** + * @param req - webscript request + * @param recursive - flag to get SubAspects or SubTypes recursively + * @return collection of qualified names for subclasses + */ + protected abstract Collection getQNameCollection(WebScriptRequest req, boolean recursive); + + /** + * Throws WebScriptException if classname is invalid + * @param namespacePrefix - namespace prefix of a class + * @param name - localname of a class + */ + protected abstract void validateClassname(String namespacePrefix, String name); + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AssociationGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AssociationGet.java new file mode 100644 index 00000000000..ef60964eb62 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AssociationGet.java @@ -0,0 +1,69 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/* + * Webscript to get the Associationdefinition for a given classname and association-name + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class AssociationGet extends AbstractAssociationGet +{ + private static final String DICTIONARY_CLASS_NAME = "classname"; + private static final String DICTIONARY_ASSOCIATION_NAME = "assocname"; + + @Override + protected QName getAssociationQname(WebScriptRequest req) + { + String associationName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_ASSOCIATION_NAME); + if(associationName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing parameter association name in the URL"); + } + + return QName.createQName(getFullNamespaceURI(associationName)); + } + + @Override + protected QName getClassQname(WebScriptRequest req) + { + String className = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_NAME); + + // validate the classname + if (isValidClassname(className) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + className + " - parameter in the URL"); + } + + return QName.createQName(getFullNamespaceURI(className)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AssociationsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AssociationsGet.java new file mode 100644 index 00000000000..6c49a106db7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/AssociationsGet.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Associationdefinitions for a given classname + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class AssociationsGet extends AbstractAssociationsGet +{ + private static final String DICTIONARY_CLASS_NAME = "classname"; + + @Override + protected QName getClassQname(WebScriptRequest req) + { + String className = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_NAME); + //validate classname + if (isValidClassname(className) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + className + " - parameter in the URL"); + } + return QName.createQName(getFullNamespaceURI(className)); + } + + @Override + protected QName getAssociationQname(String namespacePrefix, String name) + { + return QName.createQName(getFullNamespaceURI(namespacePrefix + "_" + name)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/ClassGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/ClassGet.java new file mode 100644 index 00000000000..8ddf04f6081 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/ClassGet.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Classdefinitions for a classname eg. =>cm_author + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class ClassGet extends AbstractClassGet +{ + private static final String DICTIONARY_CLASS_NAME = "className"; + + /** + * Override method from AbstractClassGet + */ + @Override + protected QName getClassQname(WebScriptRequest req) + { + String className = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_NAME); + //validate the classname and throw appropriate error message + if (isValidClassname(className) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + className + " - parameter in the URL"); + } + return QName.createQName(getFullNamespaceURI(className)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/ClassesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/ClassesGet.java new file mode 100644 index 00000000000..8fd15c28b9d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/ClassesGet.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; + +/** + * Webscript to get the Classdefinitions using classfilter , namespaceprefix and name + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class ClassesGet extends AbstractClassesGet +{ + + @Override + protected QName getQNameForModel(String namespacePrefix, String name) + { + return QName.createQName(getFullNamespaceURI(namespacePrefix + "_" + name)); + } + + @Override + protected QName getClassQname(String namespacePrefix, String name) + { + String className = namespacePrefix + "_" + name; + if(isValidClassname(className) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the name - " + name + "parameter in the URL"); + } + return QName.createQName(getFullNamespaceURI(className)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryComparators.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryComparators.java new file mode 100644 index 00000000000..99c76e5ecda --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryComparators.java @@ -0,0 +1,133 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.Comparator; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; + +/** + * Comparators used when ordering dictionary elements + * + * @author Roy Wetherall + */ +public interface DictionaryComparators +{ + /** + * Class definition comparator. + * + * Used to order class definitions by title. + */ + public class ClassDefinitionComparator implements Comparator + { + private final MessageLookup messageLookup; + + public ClassDefinitionComparator(MessageLookup messageLookup) + { + this.messageLookup = messageLookup; + } + + public int compare(ClassDefinition arg0, ClassDefinition arg1) + { + int result = 0; + + String title0 = arg0.getTitle(messageLookup); + if (title0 == null) + { + title0 = arg0.getName().toPrefixString(); + } + String title1 = arg1.getTitle(messageLookup); + if (title1 == null) + { + title1 = arg1.getName().getPrefixString(); + } + + if (title0 == null && title1 != null) + { + result = 1; + } + else if (title0 != null && title1 == null) + { + result = -1; + } + else if (title0 != null && title1 != null) + { + result = String.CASE_INSENSITIVE_ORDER.compare(title0, title1); + } + + return result; + } + } + + /** + * Property definition comparator. + * + * Used to order property definitions by title. + */ + public class PropertyDefinitionComparator implements Comparator + { + private final MessageLookup messageLookup; + + public PropertyDefinitionComparator(MessageLookup messageLookup) + { + this.messageLookup = messageLookup; + } + + public int compare(PropertyDefinition arg0, PropertyDefinition arg1) + { + int result = 0; + + String title0 = arg0.getTitle(messageLookup); + if (title0 == null) + { + title0 = arg0.getName().toPrefixString(); + } + String title1 = arg1.getTitle(messageLookup); + if (title1 == null) + { + title1 = arg1.getName().getPrefixString(); + } + + if (title0 == null && title1 != null) + { + result = 1; + } + else if (title0 != null && title1 == null) + { + result = -1; + } + else if (title0 != null && title1 != null) + { + result = String.CASE_INSENSITIVE_ORDER.compare(title0, title1); + } + + return result; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryGet.java new file mode 100644 index 00000000000..65c0ddbed4d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryGet.java @@ -0,0 +1,134 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * WebScript implementation to retrieve a complete dictionary required to implement + * a lightweight remote web-tier dictionary. + * + * @author Kevin Roast + */ +public class DictionaryGet extends DictionaryWebServiceBase +{ + private static final String MODEL_CLASS_DEFS = "classdefs"; + private static final String MODEL_PROPERTY_DEFS = "propertydefs"; + private static final String MODEL_ASSOCIATION_DEFS = "assocdefs"; + + /** Set of model namespaces to ignore when outputing dictionary classes and aspects */ + private Set ignoreNamespaces = Collections.emptySet(); + + /** + * Set of model namespaces to ignore when outputing dictionary classes and aspects + * + * @param namespaces Set of model namespaces to ignore + */ + public void setIgnoreNamespaces(Set namespaces) + { + this.ignoreNamespaces = namespaces; + } + + /** + * Execute the webscript + * + * @param req WebScriptRequest + * @param status Status + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + List qnames = new ArrayList(256); + Set namespaces = new HashSet(); + Map classdef = new HashMap(); + Map> propdef = new HashMap>(); + Map> assocdef = new HashMap>(); + + // check configured list of model namespaces to ignore when retrieving all models + for (String ns : this.namespaceService.getURIs()) + { + if (!ignoreNamespaces.contains(ns)) + { + namespaces.add(ns); + } + } + // specific model qname provided or will process all available models + String strModel = req.getParameter("model"); + if (strModel != null && strModel.length() != 0) + { + // handle full QName and prefixed shortname of a model + QName modelQName = (strModel.charAt(0) == QName.NAMESPACE_BEGIN ? QName.createQName(strModel) : QName.createQName(strModel, this.namespaceService)); + ModelDefinition modelDef = this.dictionaryservice.getModel(modelQName); + if (modelDef != null) + { + qnames.addAll(this.dictionaryservice.getAspects(modelQName)); + qnames.addAll(this.dictionaryservice.getTypes(modelQName)); + } + } + else + { + // walk all models and extract the aspects and types + for (QName qname : this.dictionaryservice.getAllModels()) + { + if (namespaces.contains(qname.getNamespaceURI())) + { + qnames.addAll(this.dictionaryservice.getAspects(qname)); + qnames.addAll(this.dictionaryservice.getTypes(qname)); + } + } + } + // get the class definitions and the properties and associations + for (QName qname : qnames) + { + ClassDefinition classDef = this.dictionaryservice.getClass(qname); + classdef.put(qname, classDef); + propdef.put(qname, classDef.getProperties().values()); + assocdef.put(qname, classDef.getAssociations().values()); + } + + Map model = new HashMap(); + model.put(MODEL_CLASS_DEFS, classdef.values()); + model.put(MODEL_PROPERTY_DEFS, propdef.values()); + model.put(MODEL_ASSOCIATION_DEFS, assocdef.values()); + model.put(MODEL_PROP_KEY_MESSAGE_LOOKUP, this.dictionaryservice); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryWebServiceBase.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryWebServiceBase.java new file mode 100644 index 00000000000..fdf1efca3d5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/DictionaryWebServiceBase.java @@ -0,0 +1,382 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.InvalidQNameException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; + +/** + * Base class for Dictionary web scripts + * + * @author Saravanan Sellathurai + */ +public abstract class DictionaryWebServiceBase extends DeclarativeWebScript +{ + private static final String NAME_DELIMITER = "_"; + protected static final String MODEL_PROP_KEY_MESSAGE_LOOKUP = "messages"; + + /** Namespace service */ + protected NamespaceService namespaceService; + + /** Dictionary service */ + protected DictionaryService dictionaryservice; + + private static final String CLASS_FILTER_OPTION_TYPE1 = "all"; + private static final String CLASS_FILTER_OPTION_TYPE2 = "aspect"; + private static final String CLASS_FILTER_OPTION_TYPE3 = "type"; + + private static final String ASSOCIATION_FILTER_OPTION_TYPE1 = "all"; + private static final String ASSOCIATION_FILTER_OPTION_TYPE2 = "general"; + private static final String ASSOCIATION_FILTER_OPTION_TYPE3 = "child"; + + + /** + * Set the namespaceService property. + * + * @param namespaceservice The namespace service instance to set + */ + public void setNamespaceService(NamespaceService namespaceservice) + { + this.namespaceService = namespaceservice; + } + + /** + * Set the dictionaryService property. + * + * @param dictionaryService The dictionary service instance to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryservice = dictionaryService; + } + + protected QName createClassQName(String className) + { + QName result = null; + int index = className.indexOf(NAME_DELIMITER); + if (index > 0) + { + String prefix = className.substring(0, index); + String shortName = className.substring(index+1); + String url = namespaceService.getNamespaceURI(prefix); + if (url != null && url.length() != 0 && + shortName != null && shortName.length() != 0) + { + QName classQName = QName.createQName(url, shortName); + if (dictionaryservice.getClass(classQName) != null) + { + result = classQName; + } + } + } + + return result; + } + + /** + * @param prefix - prefix for class name + * @param shortName - short class name + * @return qualified name for class name + */ + protected QName createClassQName(String prefix, String shortName) + { + QName result = null; + String url = namespaceService.getNamespaceURI(prefix); + if (url != null && url.length() != 0 && shortName != null && shortName.length() != 0) + { + QName classQName = QName.createQName(url, shortName); + if (dictionaryservice.getClass(classQName) != null) + { + result = classQName; + } + } + return result; + } + + + /** + * @param qname QName + * @return the namespaceuri from a qname + */ + public String getNamespaceURIfromQname(QName qname) + { + return qname.getNamespaceURI(); + } + + /** + * @param classname the class name as cm_person + * @return String the full name in the following format {namespaceuri}shorname + */ + public String getFullNamespaceURI(String classname) + { + try + { + String result = null; + String prefix = this.getPrefix(classname); + String url = this.namespaceService.getNamespaceURI(prefix); + String name = this.getShortName(classname); + result = "{" + url + "}"+ name; + return result; + } + catch (Exception e) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The exact classname - " + classname + " parameter has not been provided in the URL"); + } + } + + /** + * @param prefix prefix for classname as cm + * @param shorname the short class name as person + * @return String the full name in the following format {namespaceuri}shorname + */ + public String getFullNamespaceURI(String prefix, String shorname) + { + try + { + String result = null; + String url = this.namespaceService.getNamespaceURI(prefix); + result = "{" + url + "}" + shorname; + return result; + } + catch (Exception e) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The exact classname - " + prefix + ":" + shorname + " parameter has not been provided in the URL"); + } + } + + /** + * @param classname - checks whether the classname is valid , gets the classname as input e.g cm_person + * @return true - if the class is valid , false - if the class is invalid + */ + public boolean isValidClassname(String classname) + { + QName qname = null; + try + { + qname = QName.createQName(this.getFullNamespaceURI(classname)); + return (dictionaryservice.getClass(qname) != null); + } + catch (InvalidQNameException e) + { + //just ignore + } + return false; + } + + /** + * Checks whether the classname is valid + * @param prefix - gets the prefix as input e.g cm + * @param shorname - gets the short classname as input e.g person + * @return true - if the class is valid , false - if the class is invalid + */ + public boolean isValidClassname(String prefix, String shorname) + { + QName qname = null; + try + { + qname = QName.createQName(this.getFullNamespaceURI(prefix, shorname)); + return (dictionaryservice.getClass(qname) != null); + } + catch (InvalidQNameException e) + { + //just ignore + } + return false; + } + + /** + * @param modelname String + * @return modelname from namespaceprefix - returns null if invalid namespaceprefix is given + */ + public String getPrefixFromModelName(String modelname) + { + String namespaceprefix = null; + for(QName qnameObj:this.dictionaryservice.getAllModels()) + { + if(qnameObj.getLocalName().equals(modelname)) + { + Collection prefixes = this.namespaceService.getPrefixes(qnameObj.getNamespaceURI()); + if (!prefixes.isEmpty()) + { + namespaceprefix = prefixes.iterator().next(); + } + break; + } + } + return namespaceprefix; + } + + public boolean isValidAssociationFilter(String af) + { + return (af.equalsIgnoreCase(ASSOCIATION_FILTER_OPTION_TYPE1) || + af.equalsIgnoreCase(ASSOCIATION_FILTER_OPTION_TYPE2) || + af.equalsIgnoreCase(ASSOCIATION_FILTER_OPTION_TYPE3)); + } + + /** + * @param classname as the input + * @return true if it is a aspect or false if it is a Type + */ + public boolean isValidTypeorAspect(String classname) + { + try + { + QName qname = QName.createQName(this.getFullNamespaceURI(classname)); + return ((this.dictionaryservice.getClass(qname) != null) && + (this.dictionaryservice.getClass(qname).isAspect())); + } + catch (InvalidQNameException e) + { + // ignore + } + return false; + } + + /** + * @param prefix as the input + * @param shorname as the input + * @return true if it is a aspect or false if it is a Type + */ + public boolean isValidTypeorAspect(String prefix, String shorname) + { + try + { + QName qname = QName.createQName(this.getFullNamespaceURI(prefix, shorname)); + return ((this.dictionaryservice.getClass(qname) != null) && + (this.dictionaryservice.getClass(qname).isAspect())); + } + catch (InvalidQNameException e) + { + // ignore + } + return false; + } + + /** + * @param modelname - gets the modelname as the input (modelname is without prefix ie. cm:contentmodel => where modelname = contentmodel) + * @return true if valid or false + */ + public boolean isValidModelName(String modelname) + { + boolean value = false; + for (QName qnameObj:this.dictionaryservice.getAllModels()) + { + if (qnameObj.getLocalName().equalsIgnoreCase(modelname)) + { + value = true; + break; + } + } + return value; + } + + /** + * @param classname - returns the prefix from the classname of the format namespaceprefix:name eg. cm_person + * @return prefix - returns the prefix of the classname + */ + public String getPrefix(String classname) + { + String prefix = null; + int index = classname.indexOf(NAME_DELIMITER); + if (index > 0) + { + prefix = classname.substring(0, index); + } + return prefix; + } + + /** + * @param classname String + * @return the shortname from the classname of the format cm_person + * here person represents the shortname + */ + public String getShortName(String classname) + { + String shortname = null; + int index = classname.indexOf(NAME_DELIMITER); + if (index > 0) + { + shortname = classname.substring(index+1); + } + return shortname; + } + + /** + * @param input -gets a string input and validates it + * @return null if invalid or the string itself if its valid + */ + public String getValidInput(String input) + { + if ((input != null) && (input.length() != 0)) + { + return input; + } + else + { + return null; + } + } + + /** + * @param classfilter =>valid class filters are all,apect or type + * @return true if valid or false if invalid + */ + public boolean isValidClassFilter(String classfilter) + { + return (classfilter.equals(CLASS_FILTER_OPTION_TYPE1) || + classfilter.equals(CLASS_FILTER_OPTION_TYPE2) || + classfilter.equals(CLASS_FILTER_OPTION_TYPE3)); + } + + /** + * Returns dependent collections (properties or associations) + * in order that complies to order of class definitions + * @param sortedClassDefs - list of sorted class definitions + * @param dependent - collections that depend on class definitions + * @return collection of dependent values + */ + protected Collection reorderedValues(List sortedClassDefs, Map dependent) + { + Collection result = new ArrayList(sortedClassDefs.size()); + for (ClassDefinition classDef : sortedClassDefs) + { + result.add(dependent.get(classDef.getName())); + } + return result; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/PropertiesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/PropertiesGet.java new file mode 100644 index 00000000000..c7c0880e9ba --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/PropertiesGet.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Propertydefinitions for a given classname eg. =>cm_person + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class PropertiesGet extends AbstractPropertiesGet +{ + private static final String DICTIONARY_CLASS_NAME = "classname"; + + @Override + protected QName getClassQName(WebScriptRequest req) + { + QName classQName = null; + String className = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_NAME); + if (className != null && className.length() != 0) + { + classQName = createClassQName(className); + if (classQName == null) + { + // Error + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the className - " + className + " - parameter in the URL"); + } + } + return classQName; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/PropertyGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/PropertyGet.java new file mode 100644 index 00000000000..105992e86db --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/PropertyGet.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Propertydefinition for a given classname and propname + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class PropertyGet extends AbstractPropertyGet +{ + private static final String DICTIONARY_CLASS_NAME = "classname"; + private static final String DICTIONARY_PROPERTY_NAME = "propname"; + + @Override + protected QName getPropertyQname(WebScriptRequest req) + { + String propertyName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PROPERTY_NAME); + //validate the presence of property name + if(propertyName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing parameter propertyname in the URL"); + } + + return QName.createQName(getFullNamespaceURI(propertyName)); + } + + @Override + protected QName getClassQname(WebScriptRequest req) + { + String className = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_NAME); + + // validate the classname + if (isValidClassname(className) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + className + " - parameter in the URL"); + } + + return QName.createQName(getFullNamespaceURI(className)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/SubClassesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/SubClassesGet.java new file mode 100644 index 00000000000..b2b6ae90618 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/SubClassesGet.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary; + +import java.util.Collection; + +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Sub-Classdefinitions using classfilter , namespacePrefix and name + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class SubClassesGet extends AbstractSubClassesGet +{ + private static final String DICTIONARY_CLASS_NAME = "classname"; + + @Override + protected Collection getQNameCollection(WebScriptRequest req, boolean recursive) + { + QName classQName = null; + boolean isAspect = false; + String className = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_NAME); + + //validate the className + if(isValidClassname(className) == true) + { + classQName = QName.createQName(getFullNamespaceURI(className)); + if(isValidTypeorAspect(className) == true) + { + isAspect = true; + } + } + else + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the className - " + className + " parameter in the URL"); + } + + // collect the subaspects or subtypes of the class + if(isAspect == true) + { + return this.dictionaryservice.getSubAspects(classQName, recursive); + } + else + { + return this.dictionaryservice.getSubTypes(classQName, recursive); + } + } + + @Override + protected void validateClassname(String namespacePrefix, String name) + { + if(isValidClassname(namespacePrefix + "_" + name) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the namespacePrefix - " + namespacePrefix + " and name - "+ name + " - parameter in the URL"); + } + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/AssociationGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/AssociationGet.java new file mode 100644 index 00000000000..ff0d56046dd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/AssociationGet.java @@ -0,0 +1,79 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import org.alfresco.repo.web.scripts.dictionary.AbstractAssociationGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/* + * Webscript to get the Associationdefinition for a given classname and association-name + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class AssociationGet extends AbstractAssociationGet +{ + private static final String DICTIONARY_PREFIX = "prefix"; + private static final String DICTIONARY_SHORT_CLASS_NAME = "shortClassName"; + private static final String DICTIONARY_ASSOCIATION_PREFIX = "assocprefix"; + private static final String DICTIONARY_ASSOCIATION_SHORTNAME = "assocname"; + + @Override + protected QName getClassQname(WebScriptRequest req) + { + String classPrefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PREFIX); + String shortClassName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_SHORT_CLASS_NAME); + + //validate the classname + if(isValidClassname(classPrefix, shortClassName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + classPrefix + ":" + shortClassName + " - parameter in the URL"); + } + + return QName.createQName(getFullNamespaceURI(classPrefix, shortClassName)); + } + + @Override + protected QName getAssociationQname(WebScriptRequest req) + { + String associationName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_ASSOCIATION_SHORTNAME); + String associationPrefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_ASSOCIATION_PREFIX); + + if(associationPrefix == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing parameter association prefix in the URL"); + } + if(associationName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing parameter association name in the URL"); + } + + return QName.createQName(getFullNamespaceURI(associationPrefix, associationName)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/AssociationsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/AssociationsGet.java new file mode 100644 index 00000000000..889dd1f8008 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/AssociationsGet.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import org.alfresco.repo.web.scripts.dictionary.AbstractAssociationsGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Associationdefinitions for a given classname + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class AssociationsGet extends AbstractAssociationsGet +{ + private static final String DICTIONARY_PREFIX = "prefix"; + private static final String DICTIONARY_SHORT_CLASS_NAME = "shortClassName"; + + @Override + protected QName getClassQname(WebScriptRequest req) + { + String prefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PREFIX); + String shortClassName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_SHORT_CLASS_NAME); + + //validate classname + if (isValidClassname(prefix, shortClassName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + prefix + ":" + shortClassName + " - parameter in the URL"); + } + return QName.createQName(getFullNamespaceURI(prefix, shortClassName)); + } + + @Override + protected QName getAssociationQname(String namespacePrefix, String name) + { + return QName.createQName(getFullNamespaceURI(namespacePrefix, name)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/ClassGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/ClassGet.java new file mode 100644 index 00000000000..7ddd9ceb08a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/ClassGet.java @@ -0,0 +1,62 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import org.alfresco.repo.web.scripts.dictionary.AbstractClassGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Classdefinitions for a classname eg. =>cm_author + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class ClassGet extends AbstractClassGet +{ + private static final String DICTIONARY_PREFIX = "prefix"; + private static final String DICTIONARY_SHORT_CLASS_NAME = "shortClassName"; + + /** + * Override method from AbstractClassGet + */ + @Override + protected QName getClassQname(WebScriptRequest req) + { + String prefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PREFIX); + String shortName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_SHORT_CLASS_NAME); + + //validate the classname and throw appropriate error message + if (isValidClassname(prefix, shortName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + prefix + ":" + shortName + " - parameter in the URL"); + } + return QName.createQName(getFullNamespaceURI(prefix, shortName)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/ClassesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/ClassesGet.java new file mode 100644 index 00000000000..95fc4addda5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/ClassesGet.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import org.alfresco.repo.web.scripts.dictionary.AbstractClassesGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; + +/** + * Webscript to get the Classdefinitions using classfilter , namespaceprefix and name + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class ClassesGet extends AbstractClassesGet +{ + @Override + protected QName getQNameForModel(String namespacePrefix, String name) + { + return QName.createQName(getFullNamespaceURI(namespacePrefix, name)); + } + + @Override + protected QName getClassQname(String namespacePrefix, String name) + { + if(isValidClassname(namespacePrefix, name) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the name - " + name + "parameter in the URL"); + } + return QName.createQName(getFullNamespaceURI(namespacePrefix, name)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/PropertiesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/PropertiesGet.java new file mode 100644 index 00000000000..8ab1576c366 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/PropertiesGet.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import org.alfresco.repo.web.scripts.dictionary.AbstractPropertiesGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Propertydefinitions for a given classname eg. =>cm_person + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class PropertiesGet extends AbstractPropertiesGet +{ + private static final String DICTIONARY_PREFIX = "prefix"; + private static final String DICTIONARY_SHORT_CLASS_NAME = "shortClassName"; + + @Override + protected QName getClassQName(WebScriptRequest req) + { + QName classQName = null; + String prefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PREFIX); + String shortName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_SHORT_CLASS_NAME); + if (prefix != null && prefix.length() != 0 && shortName != null && shortName.length()!= 0) + { + classQName = createClassQName(prefix, shortName); + if (classQName == null) + { + // Error + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the className - " + prefix + ":" + shortName + " - parameter in the URL"); + } + } + return classQName; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/PropertyGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/PropertyGet.java new file mode 100644 index 00000000000..7e93bf3a15f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/PropertyGet.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import org.alfresco.repo.web.scripts.dictionary.AbstractPropertyGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Propertydefinition for a given classname and propname + * + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class PropertyGet extends AbstractPropertyGet +{ + private static final String DICTIONARY_PREFIX = "prefix"; + private static final String DICTIONARY_SHORT_CLASS_NAME = "shortClassName"; + private static final String DICTIONARY_SHORTPROPERTY_NAME = "propname"; + private static final String DICTIONARY_PROPERTY_FREFIX = "proppref"; + + @Override + protected QName getPropertyQname(WebScriptRequest req) + { + String propertyName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_SHORTPROPERTY_NAME); + String propertyPrefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PROPERTY_FREFIX); + + //validate the presence of property name + if(propertyName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing parameter short propertyname in the URL"); + } + //validate the presence of property prefix + if(propertyPrefix == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Missing parameter propertyprefix in the URL"); + } + + return QName.createQName(getFullNamespaceURI(propertyPrefix, propertyName)); + } + + @Override + protected QName getClassQname(WebScriptRequest req) + { + String prefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PREFIX); + String shortClassName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_SHORT_CLASS_NAME); + + // validate the classname + if (isValidClassname(prefix, shortClassName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the classname - " + prefix + ":" + shortClassName + " - parameter in the URL"); + } + + return QName.createQName(getFullNamespaceURI(prefix, shortClassName)); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/SubClassesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/SubClassesGet.java new file mode 100644 index 00000000000..f1ab69b12ce --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/dictionary/prefixed/SubClassesGet.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.dictionary.prefixed; + +import java.util.Collection; + +import org.alfresco.repo.web.scripts.dictionary.AbstractSubClassesGet; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript to get the Sub-Classdefinitions using classfilter , namespacePrefix and name + * @author Saravanan Sellathurai, Viachaslau Tsikhanovich + */ + +public class SubClassesGet extends AbstractSubClassesGet +{ + private static final String DICTIONARY_PREFIX = "prefix"; + private static final String DICTIONARY_CLASS_SHORTNAME = "shortClassName"; + + @Override + protected Collection getQNameCollection(WebScriptRequest req, boolean recursive) + { + String prefix = req.getServiceMatch().getTemplateVars().get(DICTIONARY_PREFIX); + String shortClassName = req.getServiceMatch().getTemplateVars().get(DICTIONARY_CLASS_SHORTNAME); + QName classQName = null; + boolean isAspect = false; + + //validate the className + if(isValidClassname(prefix, shortClassName) == true) + { + classQName = QName.createQName(getFullNamespaceURI(prefix, shortClassName)); + if(isValidTypeorAspect(prefix, shortClassName) == true) + { + isAspect = true; + } + } + else + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the className - " + prefix + ":" + shortClassName + " parameter in the URL"); + } + + // collect the subaspects or subtypes of the class + if(isAspect == true) + { + return this.dictionaryservice.getSubAspects(classQName, recursive); + } + else + { + return this.dictionaryservice.getSubTypes(classQName, recursive); + } + } + + @Override + protected void validateClassname(String namespacePrefix, String name) + { + if(isValidClassname(namespacePrefix, name) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Check the namespacePrefix - " + namespacePrefix + " and name - "+ name + " - parameter in the URL"); + } + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java new file mode 100644 index 00000000000..11a4927e030 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java @@ -0,0 +1,603 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.discussion.DiscussionServiceImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.site.SiteServiceImpl; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.discussion.DiscussionService; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.util.Pair; +import org.alfresco.util.ScriptPagingDetails; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 4.0 + */ +public abstract class AbstractDiscussionWebScript extends DeclarativeWebScript +{ + public static final String DISCUSSIONS_SERVICE_ACTIVITY_APP_NAME = "discussions"; + + /** + * When no maximum or paging info is given, what should we use? + */ + protected static final int MAX_QUERY_ENTRY_COUNT = 1000; + + private static Log logger = LogFactory.getLog(AbstractDiscussionWebScript.class); + + protected static final String KEY_POSTDATA = "postData"; + protected static final String KEY_IS_TOPIC_POST = "isTopicPost"; + protected static final String KEY_TOPIC = "topic"; + protected static final String KEY_POST = "post"; + protected static final String KEY_CAN_EDIT = "canEdit"; + protected static final String KEY_AUTHOR = "author"; + + // Injected services + protected NodeService nodeService; + protected SiteService siteService; + protected PersonService personService; + protected ActivityService activityService; + protected DiscussionService discussionService; + protected PermissionService permissionService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setDiscussionService(DiscussionService discussionService) + { + this.discussionService = discussionService; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + + protected String getOrNull(JSONObject json, String key) + { + if (json.containsKey(key)) + { + return (String)json.get(key); + } + return null; + } + + /** + * Builds up a listing Paging request, based on the arguments + * specified in the URL + */ + protected PagingRequest buildPagingRequest(WebScriptRequest req) + { + return new ScriptPagingDetails(req, MAX_QUERY_ENTRY_COUNT); + } + + protected List getTags(JSONObject json) + { + List tags = null; + if (json.containsKey("tags")) + { + // Is it "tags":"" or "tags":[...] ? + if (json.get("tags") instanceof String) + { + // This is normally an empty string, skip + String tagsS = (String)json.get("tags"); + if ("".equals(tagsS)) + { + // No tags were given + return null; + } + else + { + // Log, and treat as empty + logger.warn("Unexpected tag data: " + tagsS); + return null; + } + } + else + { + tags = new ArrayList(); + JSONArray jsTags = (JSONArray)json.get("tags"); + for (int i=0; i 0) + { + title = postTitle; + } + } + + try + { + JSONObject params = new JSONObject(); + params.put("topicId", topic.getSystemName()); + + JSONObject activity = new JSONObject(); + activity.put("title", title); + activity.put("page", page + "?topicId=" + topic.getSystemName()); + activity.put("params", params); + + activityService.postActivity( + "org.alfresco.discussions." + thing + "-" + event, + site.getShortName(), + DISCUSSIONS_SERVICE_ACTIVITY_APP_NAME, + activity.toString()); + } + catch(Exception e) + { + // Warn, but carry on + logger.warn("Error adding discussions " + thing + " " + event + " to activities feed", e); + } + } + + /** + * Is the current user allowed to edit this post? + * In order to be deemed allowed, you first need write + * permissions on the underlying node of the post. + * You then also need to either be the cm:creator of + * the post node, or a site manager + */ + protected boolean canUserEditPost(PostInfo post, SiteInfo site) + { + // Are they OK on the node? + AccessStatus canEdit = permissionService.hasPermission(post.getNodeRef(), PermissionService.WRITE); + if (canEdit == AccessStatus.ALLOWED) + { + // Only the creator and site managers may edit + String user = AuthenticationUtil.getFullyAuthenticatedUser(); + if (post.getCreator().equals(user)) + { + // It's their post + return true; + } + if (site != null) + { + String role = siteService.getMembersRole(site.getShortName(), user); + if (SiteServiceImpl.SITE_MANAGER.equals(role)) + { + // Managers may edit + return true; + } + } + } + + // If in doubt, you may not edit + return false; + } + + protected Object buildPerson(String username) + { + // Empty string needed if the user can't be found + Object noSuchPersonResponse = ""; + + if (username == null || username.length() == 0) + { + return noSuchPersonResponse; + } + + try + { + // Will turn into a Script Node needed of the person + NodeRef person = personService.getPerson(username); + return person; + } + catch(NoSuchPersonException e) + { + // This is normally caused by the person having been deleted + return noSuchPersonResponse; + } + } + + /* + * Was topicpost.lib.js getReplyPostData + * + * TODO Switch the FTL to prefer the Info object rather than the ScriptNode + */ + protected Map renderPost(PostInfo post, SiteInfo site) + { + Map item = new HashMap(); + item.put(KEY_IS_TOPIC_POST, false); + item.put(KEY_POST, post.getNodeRef()); + item.put(KEY_CAN_EDIT, canUserEditPost(post, site)); + item.put(KEY_AUTHOR, buildPerson(post.getCreator())); + return item; + } + + /* + * Was topicpost.lib.js getTopicPostData / getTopicPostDataFromTopicAndPosts + * + * TODO Switch the FTL to prefer the Info object rather than the ScriptNode + */ + protected Map renderTopic(TopicInfo topic, SiteInfo site) + { + // Fetch the primary post + PostInfo primaryPost = discussionService.getPrimaryPost(topic); + if (primaryPost == null) + { + throw new WebScriptException(Status.STATUS_PRECONDITION_FAILED, + "First (primary) post was missing from the topic, can't fetch"); + } + + // Fetch the most recent reply + PostInfo mostRecentPost = discussionService.getMostRecentPost(topic); + + // Find out how many replies there are + int numReplies; + if (mostRecentPost.getNodeRef().equals( primaryPost.getNodeRef() )) + { + // Only the one post in the topic + mostRecentPost = null; + numReplies = 0; + } + else + { + // Use this trick to get the number of posts in the topic, + // but without needing to get lots of data and objects + PagingRequest paging = new PagingRequest(1); + paging.setRequestTotalCountMax(MAX_QUERY_ENTRY_COUNT); + PagingResults posts = discussionService.listPosts(topic, paging); + + // The primary post is in the list, so exclude from the reply count + numReplies = posts.getTotalResultCount().getFirst() - 1; + } + + // Build the details + Map item = new HashMap(); + item.put(KEY_IS_TOPIC_POST, true); + item.put(KEY_TOPIC, topic.getNodeRef()); + item.put(KEY_POST, primaryPost.getNodeRef()); + item.put(KEY_CAN_EDIT, canUserEditPost(primaryPost, site)); + item.put(KEY_AUTHOR, buildPerson(topic.getCreator())); + + // The reply count is one less than all posts (first is the primary one) + item.put("totalReplyCount", numReplies); + + // Add the topic site + item.put("site", topic.getShortSiteName()); + + // We want details on the most recent post + if (mostRecentPost != null) + { + item.put("lastReply", mostRecentPost.getNodeRef()); + item.put("lastReplyBy", buildPerson(mostRecentPost.getCreator())); + } + + // Include the tags + item.put("tags", topic.getTags()); + + // All done + return item; + } + + /* + * Renders out the list of topics + * TODO Fetch the post data in one go, rather than one at a time + */ + protected Map renderTopics(PagingResults topics, + PagingRequest paging, SiteInfo site) + { + return renderTopics(topics.getPage(), topics.getTotalResultCount(), paging, site); + } + /* + * Renders out the list of topics + * TODO Fetch the post data in one go, rather than one at a time + */ + protected Map renderTopics(List topics, + Pair size, PagingRequest paging, SiteInfo site) + { + Map model = new HashMap(); + + // Paging info + model.put("total", size.getFirst()); + model.put("pageSize", paging.getMaxItems()); + model.put("startIndex", paging.getSkipCount()); + model.put("itemCount", topics.size()); + + // Data + List> items = new ArrayList>(); + for (TopicInfo topic : topics) + { + // ACE-772 fix of incorrect display of topics into "My Discussions" dashlet. + // Into "My Discussions" dashlet forum topic will be displayed only if user is a member of that site. + if (null == site && null != topic.getShortSiteName()) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + String siteShortName = topic.getShortSiteName(); + boolean isSiteMember = siteService.isMember(siteShortName, currentUser); + + if (isSiteMember) + { + items.add(renderTopic(topic, site)); + } + } + // Display all topics on the forum of the site. + else + { + items.add(renderTopic(topic, site)); + } + } + model.put("items", items); + + // All done + return model; + } + + protected Map buildCommonModel(SiteInfo site, TopicInfo topic, + PostInfo post, WebScriptRequest req) + { + // Build the common model parts + Map model = new HashMap(); + model.put(KEY_TOPIC, topic); + model.put(KEY_POST, post); + + // Capture the site details only if site based + if (site != null) + { + model.put("siteId", site.getShortName()); + model.put("site", site); + } + + // The limit on the length of the content to be returned + int contentLength = -1; + String contentLengthS = req.getParameter("contentLength"); + if (contentLengthS != null) + { + try + { + contentLength = Integer.parseInt(contentLengthS); + } + catch (NumberFormatException e) + { + logger.info("Skipping invalid length " + contentLengthS); + } + } + model.put("contentLength", contentLength); + + // All done + return model; + } + + @Override + protected Map executeImpl(WebScriptRequest req, + Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Parse the JSON, if supplied + JSONObject json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + JSONParser parser = new JSONParser(); + try + { + json = (JSONObject)parser.parse(req.getContent().getContent()); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch (ParseException pe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage()); + } + } + + + // Did they request it by node reference or site? + NodeRef nodeRef = null; + SiteInfo site = null; + TopicInfo topic = null; + PostInfo post = null; + + if (templateVars.containsKey("site")) + { + // Site, and optionally topic + String siteName = templateVars.get("site"); + site = siteService.getSite(siteName); + if (site == null) + { + String error = "Could not find site: " + siteName; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Did they give a topic name too? + if (templateVars.containsKey("path")) + { + String name = templateVars.get("path"); + topic = discussionService.getTopic(site.getShortName(), name); + + if (topic == null) + { + String error = "Could not find topic '" + name + "' for site '" + + site.getShortName() + "'"; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + nodeRef = topic.getNodeRef(); + } + else + { + // The NodeRef is the container (if it exists) + if (siteService.hasContainer(siteName, DiscussionServiceImpl.DISCUSSION_COMPONENT)) + { + nodeRef = siteService.getContainer(siteName, DiscussionServiceImpl.DISCUSSION_COMPONENT); + } + } + } + else if (templateVars.containsKey("store_type") && + templateVars.containsKey("store_id") && + templateVars.containsKey("id")) + { + // NodeRef, normally Topic or Discussion + StoreRef store = new StoreRef( + templateVars.get("store_type"), + templateVars.get("store_id")); + + nodeRef = new NodeRef(store, templateVars.get("id")); + if (! nodeService.exists(nodeRef)) + { + String error = "Could not find node: " + nodeRef; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Try to build the appropriate object for it + Pair objects = discussionService.getForNodeRef(nodeRef); + if (objects != null) + { + topic = objects.getFirst(); + post = objects.getSecond(); + } + + // See if it's actually attached to a site + if (topic != null) + { + NodeRef container = topic.getContainerNodeRef(); + if (container != null) + { + NodeRef maybeSite = nodeService.getPrimaryParent(container).getParentRef(); + if (maybeSite != null) + { + // Try to make it a site, will return Null if it isn't one + site = siteService.getSite(maybeSite); + } + } + } + } + else + { + String error = "Unsupported template parameters found"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Have the real work done + return executeImpl(site, nodeRef, topic, post, req, json, status, cache); + } + + protected abstract Map executeImpl(SiteInfo site, + NodeRef nodeRef, TopicInfo topic, PostInfo post, + WebScriptRequest req, JSONObject json, Status status, Cache cache); + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java new file mode 100644 index 00000000000..68f3350b0c8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java @@ -0,0 +1,125 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.text.MessageFormat; +import java.util.Map; +import java.util.ResourceBundle; + +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the forum post deleting forum-post.delete webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumPostDelete extends AbstractDiscussionWebScript +{ + private static final String MSG_NODE_MARKED_REMOVED = "forum-post.msg.marked.removed"; + private static final String MSG_NODE_DELETED = "forum-post.msg.deleted"; + private static final String DELETED_POST_TEXT = "[[deleted]]"; + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + + + // Are we deleting a topic, or a post in it? + String message = null; + if (post != null) + { + message = doDeletePost(topic, post, rb); + } + else if (topic != null) + { + message = doDeleteTopic(topic, site, req, json, rb); + } + else + { + String error = "Node was of the wrong type, only Topic and Post are supported"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Finish the model and return + model.put("message", message); + return model; + } + + private String doDeleteTopic(TopicInfo topic, SiteInfo site, + WebScriptRequest req, JSONObject json, ResourceBundle rb) + { + // Delete the topic, which removes all its posts too + discussionService.deleteTopic(topic); + + // Add an activity entry for this if it's site based + if (site != null) + { + addActivityEntry("post", "deleted", topic, null, site, req, json); + } + + // All done + String message = rb.getString(MSG_NODE_DELETED); + + return MessageFormat.format(message, topic.getNodeRef()); + } + + /** + * We can't just delete posts with replies attached to them, + * as that breaks the reply threading. + * For that reason, we mark deleted posts with a special + * text contents. + * TODO If a post has no replies, then delete it fully + */ + private String doDeletePost(TopicInfo topic, PostInfo post, ResourceBundle rb) + { + // Set the marker text and save + post.setTitle(DELETED_POST_TEXT); + post.setContents(DELETED_POST_TEXT); + discussionService.updatePost(post); + + // Note - we don't add activity feed entries for deleted posts + // Only deleted whole topic qualify for that at the moment + + String message = rb.getString(MSG_NODE_MARKED_REMOVED); + return MessageFormat.format(message, post.getNodeRef()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostGet.java new file mode 100644 index 00000000000..03ee838c19a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostGet.java @@ -0,0 +1,74 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.Map; + +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions page fetching forum-post.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumPostGet extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + + // Did they want just one post, or the whole of the topic? + if (post != null) + { + model.put(KEY_POSTDATA, renderPost(post, site)); + } + else if (topic != null) + { + model.put(KEY_POSTDATA, renderTopic(topic, site)); + } + else + { + String error = "Node was of the wrong type, only Topic and Post are supported"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostPut.java new file mode 100644 index 00000000000..1b53e97b16a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostPut.java @@ -0,0 +1,140 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions page editing forum-post.put webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumPostPut extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + + // Did they want to change a reply or the whole topic? + if (post != null) + { + // Update the specified post + doUpdatePost(post, post.getTopic(), req, json); + + // Add the activity entry for the reply change + addActivityEntry("reply", "updated", post.getTopic(), post, site, req, json); + + // Build the JSON for just this post + model.put(KEY_POSTDATA, renderPost(post, site)); + } + else if (topic != null) + { + // Update the primary post of the topic + post = discussionService.getPrimaryPost(topic); + if (post == null) + { + throw new WebScriptException(Status.STATUS_PRECONDITION_FAILED, + "First (primary) post was missing from the topic, can't fetch"); + } + doUpdatePost(post, topic, req, json); + + // Add the activity entry for the topic change + addActivityEntry("post", "updated", topic, null, site, req, json); + + // Build the JSON for the whole topic + model.put(KEY_POSTDATA, renderTopic(topic, site)); + } + else + { + String error = "Node was of the wrong type, only Topic and Post are supported"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // All done + return model; + } + + private void doUpdatePost(PostInfo post, TopicInfo topic, WebScriptRequest req, + JSONObject json) + { + boolean updateTopic = false; + // Fetch the details from the JSON + + // Update the titles on the post and it's topic + if (json.containsKey("title")) + { + String title = (String)json.get("title"); + post.setTitle(title); + if (title.length() > 0) + { + updateTopic = true; + topic.setTitle(title); + } + } + + // Contents is on the post + if (json.containsKey("content")) + { + post.setContents((String)json.get("content")); + } + + // Tags are on the topic + if (json.containsKey("tags")) + { + topic.getTags().clear(); + + List tags = getTags(json); + if (tags != null) + { + topic.getTags().addAll(tags); + } + updateTopic = true; + } + + // Save the topic and the post + if (updateTopic == true) + { + discussionService.updateTopic(topic); + } + discussionService.updatePost(post); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesGet.java new file mode 100644 index 00000000000..35718fc9343 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesGet.java @@ -0,0 +1,111 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.PostWithReplies; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions page creating forum-post-replies.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumPostRepliesGet extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // How many levels did they want? + int levels = 1; + String levelsS = req.getParameter("levels"); + if (levelsS != null) + { + try + { + levels = Integer.parseInt(levelsS); + } + catch (NumberFormatException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Level depth parameter invalid"); + } + } + + // Fetch the replies + PostWithReplies replies; + if (post != null) + { + replies = discussionService.listPostReplies(post, levels); + } + else if (topic != null) + { + replies = discussionService.listPostReplies(topic, levels); + } + else + { + String error = "Node was of the wrong type, only Topic and Post are supported"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + + // Build the JSON for the replies + model.put("data", renderReplies(replies, site).get("children")); + + // All done + return model; + } + + private Map renderReplies(PostWithReplies replies, SiteInfo site) + { + Map reply = renderPost(replies.getPost(), site); + reply.put("childCount", replies.getReplies().size()); + + List> r = new ArrayList>(); + for (PostWithReplies child : replies.getReplies()) + { + r.add(renderReplies(child, site)); + } + reply.put("children", r); + + return reply; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesPost.java new file mode 100644 index 00000000000..a0d3e38247e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesPost.java @@ -0,0 +1,117 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions page creating forum-post-replies.post webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumPostRepliesPost extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // If they're trying to create a reply to a topic, they actually + // mean to create the reply on the primary post + if (post == null) + { + post = discussionService.getPrimaryPost(topic); + if (post == null) + { + throw new WebScriptException(Status.STATUS_PRECONDITION_FAILED, + "First (primary) post was missing from the topic, can't fetch"); + } + } + else if (topic == null) + { + String error = "Node was of the wrong type, only Topic and Post are supported"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Have the reply created + PostInfo reply = doCreatePost(post, topic, req, json); + + // Add the activity entry for the reply change + addActivityEntry("reply", "created", topic, reply, site, req, json); + + // Build the common model parts + Map model = buildCommonModel(site, topic, reply, req); + + // Build the JSON for the new reply post + model.put(KEY_POSTDATA, renderPost(reply, site)); + + // All done + return model; + } + + private PostInfo doCreatePost(PostInfo post, TopicInfo topic, WebScriptRequest req, + JSONObject json) + { + // Fetch the details from the JSON + String title = null; + if (json.containsKey("title")) + { + title = (String)json.get("title"); + } + + String contents = null; + if (json.containsKey("content")) + { + contents = (String)json.get("content"); + } + + + // Create the reply + PostInfo reply = discussionService.createReply(post, contents); + + // Set the title if needed (it normally isn't) + if (title != null && title.length() > 0) + { + nodeService.setProperty(reply.getNodeRef(), ContentModel.PROP_TITLE, title); + reply = discussionService.getPost(topic, reply.getSystemName()); + } + + // All done + return reply; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicPost.java new file mode 100644 index 00000000000..6bbe4394d76 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicPost.java @@ -0,0 +1,110 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions page editing forum-posts.post webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumTopicPost extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // They shouldn't be adding to an existing Post or Topic + if (topic != null || post != null) + { + String error = "Can't create a new Topic inside an existing Topic or Post"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Grab the details of the new Topic and Post + String title = ""; + String contents = ""; + if (json.containsKey("title")) + { + title = (String)json.get("title"); + } + if (json.containsKey("content")) + { + contents = (String)json.get("content"); + } + List tags = getTags(json); + + + // Have the topic created + if (site != null) + { + topic = discussionService.createTopic(site.getShortName(), title); + } + else + { + topic = discussionService.createTopic(nodeRef, title); + } + if (tags != null && tags.size() > 0) + { + topic.getTags().clear(); + topic.getTags().addAll(tags); + discussionService.updateTopic(topic); + } + + + // Have the primary post created + post = discussionService.createPost(topic, contents); + + + // Record the activity + addActivityEntry("post", "created", topic, post, site, req, json); + + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + + // Build the JSON for the whole topic + model.put(KEY_POSTDATA, renderTopic(topic, site)); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java new file mode 100644 index 00000000000..a4e9c406495 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java @@ -0,0 +1,414 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.alfresco.query.EmptyPagingResults; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.discussion.TopicInfoImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.LimitBy; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.util.ISO9075; +import org.alfresco.util.Pair; +import org.alfresco.util.ScriptPagingDetails; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Gets topics matching the filters passed to it in the URL. + * + * topics = 'mine' (searches for posts by the user) or 'all' (ignores the author in the search) + * history = days in the past to search + * resultSize = the number of topics returned in the results + * + * @author Jamie Allison + */ +public class ForumTopicsFilteredGet extends AbstractDiscussionWebScript +{ + //Filter Defaults + protected static final String DEFAULT_TOPIC_AUTHOR = "mine"; + protected static final int DEFAULT_TOPIC_LATEST_POST_DAYS_AGO = 1; + protected static final int DEFAULT_MAX_RESULTS = 10; + + protected static final StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + protected static final String SEARCH_QUERY = "TYPE:\"{http://www.alfresco.org/model/forum/1.0}post\"" + + " AND PATH:\"/app:company_home/st:sites/%s/cm:discussions/*/*\"" + + " AND @cm:created:[\"%s\" TO NOW]"; + + /** Spring-injected services */ + private SearchService searchService; + + /** + * Sets the searchService. + * + * @param searchService SearchService + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Overrides AbstractDiscussionWebScript to allow a null site + * + * @param req WebScriptRequest + * @param status Status + * @param cache Cache + * + * @return Map + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + SiteInfo site = null; + + if (templateVars.containsKey("site")) + { + // Site, and optionally topic + String siteName = templateVars.get("site"); + site = siteService.getSite(siteName); + if (site == null) + { + String error = "Could not find site: " + siteName; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + } + + // Have the real work done + return executeImpl(site, null, null, null, req, null, null, null); + } + + /** + * @param site SiteInfo + * @param nodeRef Not required. It is only included because it is overriding the parent class. + * @param topic Not required. It is only included because it is overriding the parent class. + * @param post Not required. It is only included because it is overriding the parent class. + * @param req WebScriptRequest + * @param status Not required. It is only included because it is overriding the parent class. + * @param cache Not required. It is only included because it is overriding the parent class. + * + * @return Map + */ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, TopicInfo topic, + PostInfo post, WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + // They shouldn't be trying to list of an existing Post or Topic + if (topic != null || post != null) + { + String error = "Can't list Topics inside an existing Topic or Post"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Set search filter to users topics or all topics + String pAuthor = req.getParameter("topics"); + String author = DEFAULT_TOPIC_AUTHOR; + if (pAuthor != null) + { + author = pAuthor; + } + // Set the number of days in the past to search from + String pDaysAgo = req.getParameter("history"); + int daysAgo = DEFAULT_TOPIC_LATEST_POST_DAYS_AGO; + if (pDaysAgo != null) + { + try + { + daysAgo = Integer.parseInt(pDaysAgo); + } + catch (NumberFormatException e) + { + //do nothing. history has already been preset to the default value. + } + } + + // Get the complete search query + Pair searchQuery = getSearchQuery(site, author, daysAgo); + + // Get the filtered topics + PagingRequest paging = buildPagingRequest(req); + PagingResults topics = doSearch(searchQuery, false, paging); + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + + // Have the topics rendered + model.put("data", renderTopics(topics, paging, site)); + + // All done + return model; + } + + /** + * Do the actual search + * + * @param searchQuery Pair with query string in first and query language in second + * @param sortAscending boolean + * @param paging PagingRequest + */ + protected PagingResults doSearch(Pair searchQuery, boolean sortAscending, PagingRequest paging) + { + ResultSet resultSet = null; + PagingResults pagedResults = new EmptyPagingResults(); + + String sortOn = "@{http://www.alfresco.org/model/content/1.0}created"; + + // Setup the search parameters + SearchParameters sp = new SearchParameters(); + sp.addStore(SPACES_STORE); + sp.setQuery(searchQuery.getFirst()); + sp.setLanguage(searchQuery.getSecond()); + sp.addSort(sortOn, sortAscending); + if (paging.getMaxItems() > 0) + { + //Multiply maxItems by 10. This is to catch topics that have multiple replies and ensure that the maximum number of topics is shown. + sp.setLimit(paging.getMaxItems()*10); + sp.setLimitBy(LimitBy.FINAL_SIZE); + } + if (paging.getSkipCount() > 0) + { + sp.setSkipCount(paging.getSkipCount()); + } + + try + { + resultSet = searchService.query(sp); + pagedResults = wrap(resultSet, paging); + } + finally + { + try + { + resultSet.close(); + } + catch(Exception e) + { + //do nothing + } + } + + return pagedResults; + } + + /** + * Build the search query from the passed in parameters and SEARCH_QUERY constant + * + * @param site SiteInfo + * @param author String + * @param daysAgo int + * @return Pair with the query string in first and query language in second + */ + protected Pair getSearchQuery(SiteInfo site, String author, int daysAgo) + { + String search = String.format(SEARCH_QUERY, + (site != null ? "cm:" + ISO9075.encode(site.getShortName()) : "*"), + getDateXDaysAgo(daysAgo) + ); + + // If author equals 'mine' add cm:creator to the search query otherwise leave out + if(author.equals(DEFAULT_TOPIC_AUTHOR)) + { + search += " AND @cm:creator:\"" + AuthenticationUtil.getFullyAuthenticatedUser() + "\""; + } + + // Add the query string and language to the returned results + Pair searchQuery = new Pair(search, SearchService.LANGUAGE_FTS_ALFRESCO); + + return searchQuery; + } + + /** + * Get the date x days ago in the format 'yyyy-MM-dd' + * + * @param daysAgo int + * @return String + */ + protected String getDateXDaysAgo(int daysAgo) + { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, (daysAgo * -1)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + return sdf.format(calendar.getTime()); + } + + /** + * Builds up a listing Paging request, based on the arguments specified in the URL + * + * @param req WebScriptRequest + * @return PagingRequest + */ + @Override + protected PagingRequest buildPagingRequest(WebScriptRequest req) + { + // Grab the number of topics to return + String pResultSize = req.getParameter("resultSize"); + int resultSize = DEFAULT_MAX_RESULTS; + if (pResultSize != null) + { + try + { + resultSize = Integer.parseInt(pResultSize); + } + catch (NumberFormatException e) + { + //do nothing. ResultSize has already been preset to the default value. + } + } + return new ScriptPagingDetails(req, resultSize); + } + + /** + * Wrap up search results as {@link TopicInfo} instances + * + * @param finalResults ResultSet + * @param paging PagingRequest + */ + protected PagingResults wrap(final ResultSet finalResults, PagingRequest paging) + { + int maxItems = paging.getMaxItems(); + Comparator lastPostDesc = new Comparator() + { + @Override + public int compare(TopicInfo t1, TopicInfo t2) + { + Date t1LastPostDate = t1.getCreatedAt(); + if(discussionService.getMostRecentPost(t1) != null) + { + t1LastPostDate = discussionService.getMostRecentPost(t1).getCreatedAt(); + } + + Date t2LastPostDate = t2.getCreatedAt(); + if(discussionService.getMostRecentPost(t2) != null) + { + t2LastPostDate = discussionService.getMostRecentPost(t2).getCreatedAt(); + } + return t2LastPostDate.compareTo(t1LastPostDate); + } + }; + + final Set topics = new TreeSet(lastPostDesc); + + for (ResultSetRow row : finalResults) + { + Pair pair = discussionService.getForNodeRef(row.getNodeRef()); + TopicInfo topic = pair.getFirst(); + if(topic != null) + { + String path = nodeService.getPath(topic.getNodeRef()).toDisplayPath(nodeService, permissionService); + String site = path.split("/")[3]; + TopicInfoImpl tii = (TopicInfoImpl)topic; + tii.setShortSiteName(site); + topics.add(tii); + + if(topics.size() >= maxItems) + { + break; + } + } + } + + // Wrap + return new PagingResults() + { + @Override + public boolean hasMoreItems() + { + try + { + return finalResults.hasMore(); + } + catch(UnsupportedOperationException e) + { + // Not all search results support paging + return false; + } + } + + @Override + public Pair getTotalResultCount() + { + int skipCount = 0; + int itemsRemainingAfterThisPage = 0; + try + { + skipCount = finalResults.getStart(); + } + catch(UnsupportedOperationException e) {} + try + { + itemsRemainingAfterThisPage = finalResults.length(); + } + catch(UnsupportedOperationException e) {} + + final int totalItemsInUnpagedResultSet = skipCount + itemsRemainingAfterThisPage; + return new Pair(totalItemsInUnpagedResultSet, totalItemsInUnpagedResultSet); + } + + @Override + public List getPage() + { + return new ArrayList(topics); + } + + @Override + public String getQueryExecutionId() + { + return null; + } + }; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsGet.java new file mode 100644 index 00000000000..662e54f1c45 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsGet.java @@ -0,0 +1,121 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions topics fetching forum-posts.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumTopicsGet extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // They shouldn't be trying to list of an existing Post or Topic + if (topic != null || post != null) + { + String error = "Can't list Topics inside an existing Topic or Post"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Do we need to list or search? + boolean tagSearch = false; + String tag = req.getParameter("tag"); + if (tag != null && tag.length() > 0) + { + tagSearch = true; + + // Tags can be full unicode strings, so decode + tag = URLDecoder.decode(tag); + } + + // Get the topics + boolean oldestTopicsFirst = false; + PagingResults topics = null; + PagingRequest paging = buildPagingRequest(req); + if (tagSearch) + { + // Tag based is a search rather than a listing + if (site != null) + { + topics = discussionService.findTopics(site.getShortName(), null, tag, oldestTopicsFirst, paging); + } + else + { + topics = discussionService.findTopics(nodeRef, null, tag, oldestTopicsFirst, paging); + } + } + else + { + if (site != null) + { + topics = discussionService.listTopics(site.getShortName(), oldestTopicsFirst, paging); + } + else + { + topics = discussionService.listTopics(nodeRef, oldestTopicsFirst, buildPagingRequest(req)); + } + } + + + // If they did a site based search, and the component hasn't + // been created yet, use the site for the permissions checking + if (site != null && nodeRef == null) + { + nodeRef = site.getNodeRef(); + } + + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + model.put("forum", nodeRef); + + // Have the topics rendered + model.put("data", renderTopics(topics, paging, site)); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsHotGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsHotGet.java new file mode 100644 index 00000000000..579bf91a50f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsHotGet.java @@ -0,0 +1,118 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.util.Pair; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions topics fetching forum-posts-hot.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumTopicsHotGet extends AbstractDiscussionWebScript +{ + protected static final int RECENT_POSTS_PERIOD_DAYS = 30; + protected static final long ONE_DAY_MS = 24*60*60*1000; + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // They shouldn't be trying to list of an existing Post or Topic + if (topic != null || post != null) + { + String error = "Can't list Topics inside an existing Topic or Post"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Grab the date range to search over + String numDaysS = req.getParameter("numdays"); + int numDays = RECENT_POSTS_PERIOD_DAYS; + if (numDaysS != null) + { + numDays = Integer.parseInt(numDaysS); + } + + Date now = new Date(); + Date since = new Date(now.getTime() - numDays*ONE_DAY_MS); + + // Get the topics with recent replies + PagingResults> topicsAndCounts = null; + PagingRequest paging = buildPagingRequest(req); + if (site != null) + { + topicsAndCounts = discussionService.listHotTopics(site.getShortName(), since, paging); + } + else + { + topicsAndCounts = discussionService.listHotTopics(nodeRef, since, paging); + } + + // For this, we actually only need the topics, not their counts too + List topics = new ArrayList(); + for (Pair tc : topicsAndCounts.getPage()) + { + topics.add(tc.getFirst()); + } + + + // If they did a site based search, and the component hasn't + // been created yet, use the site for the permissions checking + if (site != null && nodeRef == null) + { + nodeRef = site.getNodeRef(); + } + + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + model.put("forum", nodeRef); + + // Have the topics rendered + model.put("data", renderTopics(topics, topicsAndCounts.getTotalResultCount(), paging, site)); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsMineGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsMineGet.java new file mode 100644 index 00000000000..175b49042a2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsMineGet.java @@ -0,0 +1,97 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions topics fetching forum-posts-mine.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumTopicsMineGet extends AbstractDiscussionWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // They shouldn't be trying to list of an existing Post or Topic + if (topic != null || post != null) + { + String error = "Can't list Topics inside an existing Topic or Post"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Grab the current user to list for + String username = AuthenticationUtil.getFullyAuthenticatedUser(); + + // Get the topics for the user, oldest first + PagingResults topics = null; + PagingRequest paging = buildPagingRequest(req); + if (site != null) + { + topics = discussionService.listTopics(site.getShortName(), username, true, paging); + } + else + { + topics = discussionService.listTopics(nodeRef, username, true, paging); + } + + + // If they did a site based search, and the component hasn't + // been created yet, use the site for the permissions checking + if (site != null && nodeRef == null) + { + nodeRef = site.getNodeRef(); + } + + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + model.put("forum", nodeRef); + + // Have the topics rendered + model.put("data", renderTopics(topics, paging, site)); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsRecentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsRecentGet.java new file mode 100644 index 00000000000..b9ea4a644df --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsRecentGet.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.Date; +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the discussions topics fetching forum-posts-new.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumTopicsRecentGet extends AbstractDiscussionWebScript +{ + protected static final int RECENT_SEARCH_PERIOD_DAYS = 7; + protected static final long ONE_DAY_MS = 24*60*60*1000; + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // They shouldn't be trying to list of an existing Post or Topic + if (topic != null || post != null) + { + String error = "Can't list Topics inside an existing Topic or Post"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Grab the date range to search over + String numDaysS = req.getParameter("numdays"); + int numDays = RECENT_SEARCH_PERIOD_DAYS; + if (numDaysS != null) + { + numDays = Integer.parseInt(numDaysS); + } + + Date now = new Date(); + Date from = new Date(now.getTime() - numDays*ONE_DAY_MS); + Date to = new Date(now.getTime() + ONE_DAY_MS); + + // Get the recent topics, newest first + PagingResults topics = null; + PagingRequest paging = buildPagingRequest(req); + if (site != null) + { + topics = discussionService.listTopics(site.getShortName(), from, to, false, paging); + } + else + { + topics = discussionService.listTopics(nodeRef, from, to, false, paging); + } + + + // If they did a site based search, and the component hasn't + // been created yet, use the site for the permissions checking + if (site != null && nodeRef == null) + { + nodeRef = site.getNodeRef(); + } + + + // Build the common model parts + Map model = buildCommonModel(site, topic, post, req); + model.put("forum", nodeRef); + + // Have the topics rendered + model.put("data", renderTopics(topics, paging, site)); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/AbstractDocLink.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/AbstractDocLink.java new file mode 100644 index 00000000000..c52009827c0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/AbstractDocLink.java @@ -0,0 +1,196 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.doclink; + +import java.io.StringWriter; +import java.util.Map; +import java.util.StringTokenizer; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.repository.DocumentLinkService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.json.JSONWriter; + +/** + * This class contains common code for doclink webscripts controllers + * + * @author Ana Bozianu + * @since 5.1 + */ +public abstract class AbstractDocLink extends DeclarativeWebScript +{ + private static String PARAM_STORE_TYPE = "store_type"; + private static String PARAM_STORE_ID = "store_id"; + private static String PARAM_ID = "id"; + private static String PARAM_SITE = "site"; + private static String PARAM_CONTAINER = "container"; + private static String PARAM_PATH = "path"; + + private static final String ACTIVITY_TOOL = "documentLinkService"; + + protected NodeService nodeService; + protected SiteService siteService; + protected DocumentLinkService documentLinkService; + protected ActivityService activityService; + protected ServiceRegistry serviceRegistry; + + private static Log logger = LogFactory.getLog(AbstractDocLink.class); + + protected NodeRef parseNodeRefFromTemplateArgs(Map templateVars) + { + if (templateVars == null) + { + return null; + } + + String storeTypeArg = templateVars.get(PARAM_STORE_TYPE); + String storeIdArg = templateVars.get(PARAM_STORE_ID); + String idArg = templateVars.get(PARAM_ID); + + if (storeTypeArg != null) + { + ParameterCheck.mandatoryString("storeTypeArg", storeTypeArg); + ParameterCheck.mandatoryString("storeIdArg", storeIdArg); + ParameterCheck.mandatoryString("idArg", idArg); + + /* + * NodeRef based request + * URL_BASE/{store_type}/{store_id}/{id} + */ + return new NodeRef(storeTypeArg, storeIdArg, idArg); + } + else + { + String siteArg = templateVars.get(PARAM_SITE); + String containerArg = templateVars.get(PARAM_CONTAINER); + String pathArg = templateVars.get(PARAM_PATH); + + if (siteArg != null) + { + ParameterCheck.mandatoryString("siteArg", siteArg); + ParameterCheck.mandatoryString("containerArg", containerArg); + + /* + * Site based request URL_BASE/{site}/{container} or + * URL_BASE/{site}/{container}/{path} + */ + SiteInfo site = siteService.getSite(siteArg); + PropertyCheck.mandatory(this, "site", site); + + NodeRef node = siteService.getContainer(site.getShortName(), containerArg); + if (node == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid 'container' variable"); + } + + if (pathArg != null) + { + // URL_BASE/{site}/{container}/{path} + StringTokenizer st = new StringTokenizer(pathArg, "/"); + while (st.hasMoreTokens()) + { + String childName = st.nextToken(); + node = nodeService.getChildByName(node, ContentModel.ASSOC_CONTAINS, childName); + if (node == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid 'path' variable"); + } + } + } + + return node; + } + } + return null; + } + + /** + * Generates an activity entry for the link + */ + + protected void addActivityEntry(String activityType, String title, String nodeRef, String site) + { + try + { + StringWriter activityJson = new StringWriter(); + JSONWriter activity = new JSONWriter(activityJson); + activity.startObject(); + activity.writeValue("title", title); + activity.writeValue("nodeRef", nodeRef); + activity.writeValue("page", "document-details?nodeRef=" + nodeRef); + activity.endObject(); + + activityService.postActivity( + activityType, + site, + ACTIVITY_TOOL, + activityJson.toString()); + } + catch (Exception e) + { + // Warn, but carry on + logger.warn("Error adding link event to activities feed", e); + } + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setDocumentLinkService(DocumentLinkService documentLinkService) + { + this.documentLinkService = documentLinkService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/DocLinkPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/DocLinkPost.java new file mode 100644 index 00000000000..9b53f9bef11 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/DocLinkPost.java @@ -0,0 +1,192 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.doclink; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.activities.ActivityType; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.web.scripts.WebScriptUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.ParameterCheck; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the doclink.post webscript doclink.post is a + * webscript for creating a link of a document within a target destination + * + * @author Ana Bozianu + * @since 5.1 + */ +public class DocLinkPost extends AbstractDocLink +{ + private static final String PARAM_DESTINATION_NODE = "destinationNodeRef"; + private static final String PARAM_MULTIPLE_FILES = "multipleFiles"; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + NodeRef sourceNodeRef = null; + NodeRef destinationNodeRef = null; + + /* Parse the template vars */ + Map templateVars = req.getServiceMatch().getTemplateVars(); + sourceNodeRef = parseNodeRefFromTemplateArgs(templateVars); + + /* Parse the JSON content */ + JSONObject json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + try + { + json = (JSONObject) JSONValue.parseWithException(req.getContent().getContent()); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch (ParseException pe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage()); + } + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "invalid request content type"); + } + + /* Parse the destination NodeRef parameter */ + String destinationNodeParam = (String) json.get(PARAM_DESTINATION_NODE); + ParameterCheck.mandatoryString("destinationNodeParam", destinationNodeParam); + destinationNodeRef = WebScriptUtil.resolveNodeReference(destinationNodeParam, serviceRegistry.getNodeLocatorService()); + + List nodeRefs = new ArrayList(); + if (json.containsKey(PARAM_MULTIPLE_FILES)) + { + JSONArray multipleFiles = (JSONArray) json.get(PARAM_MULTIPLE_FILES); + for (int i = 0; i < multipleFiles.size(); i++) + { + String nodeRefString = (String) multipleFiles.get(i); + if (nodeRefString != null) + { + try + { + NodeRef nodeRefToCreateLink = new NodeRef(nodeRefString); + nodeRefs.add(nodeRefToCreateLink); + } + catch (AlfrescoRuntimeException ex) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid Arguments: " + ex.getMessage()); + } + } + } + } + else + { + nodeRefs.add(sourceNodeRef); + } + + // getSite for destination folder + String siteName = siteService.getSiteShortName(destinationNodeRef); + + List linksResults = new ArrayList(); + NodeRef linkNodeRef = null; + int successCount = 0; + int failureCount = 0; + + if (nodeRefs != null && nodeRefs.size() > 0) + { + for (NodeRef sourceNode : nodeRefs) + { + /* Create link */ + linkNodeRef = createLink(destinationNodeRef, sourceNode); + + if (linkNodeRef != null) + { + String sourceName = (String) nodeService.getProperty(sourceNode, ContentModel.PROP_NAME); + if (siteName != null) + { + addActivityEntry(ActivityType.DOCLINK_CREATED, sourceName, sourceNode.toString(), siteName); + } + linksResults.add(linkNodeRef.toString()); + successCount++; + } + } + } + + failureCount = nodeRefs.size() - successCount; + Map model = new HashMap(); + model.put("results", linksResults); + model.put("successCount", successCount); + model.put("failureCount", failureCount); + model.put("overallSuccess", failureCount == 0); + return model; + } + + /** + * Create link for sourceNodeRef in destinationNodeRef location + * + * @param destinationNodeRef + * @param sourceNodeRef + * @return + */ + private NodeRef createLink(NodeRef destinationNodeRef, NodeRef sourceNodeRef) + { + NodeRef linkNodeRef = null; + try + { + linkNodeRef = documentLinkService.createDocumentLink(sourceNodeRef, destinationNodeRef); + } + catch (IllegalArgumentException ex) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid Arguments: " + ex.getMessage()); + } + catch (AccessDeniedException e) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "You don't have permission to perform this operation"); + } + return linkNodeRef; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/DocLinksDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/DocLinksDelete.java new file mode 100644 index 00000000000..78ddc4e3392 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/doclink/DocLinksDelete.java @@ -0,0 +1,96 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.doclink; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.repository.DeleteLinksStatusReport; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the doclink.post webscript doclink.post is a + * webscript for creating a link of a document within a target destination + * + * @author Ana Bozianu + * @since 5.1 + */ +public class DocLinksDelete extends AbstractDocLink +{ + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + NodeRef destinationNodeRef = null; + + /* Parse the template vars */ + Map templateVars = req.getServiceMatch().getTemplateVars(); + destinationNodeRef = parseNodeRefFromTemplateArgs(templateVars); + + /* Delete links */ + DeleteLinksStatusReport report; + try + { + report = documentLinkService.deleteLinksToDocument(destinationNodeRef); + } + catch (IllegalArgumentException ex) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid Arguments: " + ex.getMessage()); + } + catch (AccessDeniedException e) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "You don't have permission to perform this operation"); + } + + /* Build response */ + Map model = new HashMap(); + model.put("total_count", report.getTotalLinksFoundCount()); + model.put("deleted_count", report.getDeletedLinksCount()); + + Map errorDetails = new HashMap(); + Iterator> it = report.getErrorDetails().entrySet().iterator(); + while (it.hasNext()) + { + Map.Entry pair = it.next(); + + Throwable th = pair.getValue(); + + errorDetails.put(pair.getKey().toString(), th.getMessage()); + } + + model.put("error_details", errorDetails); + + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/AbstractDownloadWebscript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/AbstractDownloadWebscript.java new file mode 100644 index 00000000000..bfb4214b7ba --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/AbstractDownloadWebscript.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.download; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.download.DownloadService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; + +/** + * Base class for download related webscripts. + * + * @author Alex Miller + */ +abstract class AbstractDownloadWebscript extends DeclarativeWebScript +{ + // Shared dependencies + protected DownloadService downloadService; + + public void setDownloadService(DownloadService downloadSerivce) + { + this.downloadService = downloadSerivce; + } + + /** + * Helper method to embed error informaion in a map. + */ + protected Map buildError(String message) + { + HashMap result = new HashMap(); + result.put("error", message); + + HashMap model = new HashMap(); + model.put("error", message); + model.put("result", result); + + return model; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadDelete.java new file mode 100644 index 00000000000..66078391545 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadDelete.java @@ -0,0 +1,89 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.download; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript for canceling a download. + * + * @author Alex Miller + */ +public class DownloadDelete extends AbstractDownloadWebscript +{ + + protected NodeService nodeService; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + if (! ( templateVars.containsKey("store_type") + && templateVars.containsKey("store_id") + && templateVars.containsKey("node_id")) ) + { + String error = "Missing template variables (store_type, store_id or node_id)."; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + StoreRef store = new StoreRef(templateVars.get("store_type"), templateVars.get("store_id")); + NodeRef nodeRef = new NodeRef(store, templateVars.get("node_id")); + if (! nodeService.exists(nodeRef)) + { + String error = "Could not find node: " + nodeRef; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + downloadService.cancelDownload(nodeRef); + + status.setCode(HttpServletResponse.SC_OK); + + return new HashMap(); + + } + + public void setNodeService(NodeService nodeSerivce) + { + this.nodeService = nodeSerivce; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadPost.java new file mode 100644 index 00000000000..de6b47ccf94 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadPost.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.download; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.NodeRef; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web script for creating a new download. + * + * @author Alex Miller + */ +public class DownloadPost extends AbstractDownloadWebscript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Parse the JSON, if supplied + JSONArray json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + + List nodes = new LinkedList(); + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + JSONParser parser = new JSONParser(); + try + { + json = (JSONArray)parser.parse(req.getContent().getContent()); + for (int i = 0 ; i < json.size() ; i++) + { + JSONObject obj = (JSONObject)json.get(i); + String nodeRefString = (String)obj.get("nodeRef"); + if (nodeRefString != null) + { + nodes.add(new NodeRef(nodeRefString)); + } + } + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Unexpected IOException", io); + } + catch (org.json.simple.parser.ParseException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Unexpected ParseException", je); + } + } + + if (nodes.size() <= 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "No nodeRefs provided"); + } + + NodeRef downloadNode = downloadService.createDownload(nodes.toArray(new NodeRef[nodes.size()]), true); + + Map model = new HashMap(); + model.put("downloadNodeRef", downloadNode); + + return model; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadStatusGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadStatusGet.java new file mode 100644 index 00000000000..78951258df7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/download/DownloadStatusGet.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.download; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript for retrieving the status of a download. + * + * @author Alex Miller + */ +public class DownloadStatusGet extends AbstractDownloadWebscript +{ + + protected NodeService nodeService; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + if (! ( templateVars.containsKey("store_type") + && templateVars.containsKey("store_id") + && templateVars.containsKey("node_id")) ) + { + String error = "Missing template variables (store_type, store_id or node_id)."; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + StoreRef store = new StoreRef(templateVars.get("store_type"), templateVars.get("store_id")); + NodeRef nodeRef = new NodeRef(store, templateVars.get("node_id")); + if (! nodeService.exists(nodeRef)) + { + String error = "Could not find node: " + nodeRef; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + DownloadStatus downloadStatus = downloadService.getDownloadStatus(nodeRef); + + Map result = new HashMap(); + result.put("downloadStatus", downloadStatus); + return result; + + } + + public void setNodeService(NodeService nodeSerivce) + { + this.nodeService = nodeSerivce; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/AbstractSolrFacetConfigAdminWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/AbstractSolrFacetConfigAdminWebScript.java new file mode 100644 index 00000000000..6d70623d648 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/AbstractSolrFacetConfigAdminWebScript.java @@ -0,0 +1,295 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.search.impl.solr.facet.SolrFacetModel; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is an abstract base class for the various web script controllers + * in the SolrFacetService. + * + * @author Jamal Kaabi-Mofrad + */ +public abstract class AbstractSolrFacetConfigAdminWebScript extends DeclarativeWebScript +{ + private static final Log logger = LogFactory.getLog(AbstractSolrFacetConfigAdminWebScript.class); + + protected static final String PARAM_FILTER_ID = "filterID"; + protected static final String PARAM_FACET_QNAME = "facetQName"; + protected static final String PARAM_DISPLAY_NAME = "displayName"; + protected static final String PARAM_DISPLAY_CONTROL = "displayControl"; + protected static final String PARAM_MAX_FILTERS = "maxFilters"; + protected static final String PARAM_HIT_THRESHOLD = "hitThreshold"; + protected static final String PARAM_MIN_FILTER_VALUE_LENGTH = "minFilterValueLength"; + protected static final String PARAM_SORT_BY = "sortBy"; + protected static final String PARAM_SCOPE = "scope"; + protected static final String PARAM_SCOPED_SITES = "scopedSites"; + protected static final String PARAM_INDEX = "index"; + protected static final String PARAM_IS_ENABLED = "isEnabled"; + protected static final String PARAM_CUSTOM_PROPERTIES = "customProperties"; + protected static final String CUSTOM_PARAM_NAME = "name"; + protected static final String CUSTOM_PARAM_VALUE = "value"; + + // The pattern is equivalent to the pattern defined in the forms-runtime.js + protected static final Pattern FILTER_ID_PATTERN = Pattern.compile("([\"\\*\\\\\\>\\<\\?\\/\\:\\|]+)|([\\.]?[\\.]+$)"); + + protected SolrFacetService facetService; + protected NamespaceService namespaceService; + + /** + * @param facetService the facetService to set + */ + public void setFacetService(SolrFacetService facetService) + { + this.facetService = facetService; + } + + /** + * @param namespaceService the namespaceService to set + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + @Override + protected Map executeImpl(final WebScriptRequest req, final Status status, final Cache cache) + { + validateCurrentUser(); + + return unprotectedExecuteImpl(req, status, cache); + } + + protected void validateCurrentUser() + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // check the current user access rights + if (!facetService.isSearchAdmin(currentUser)) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Access denied."); + } + } + + protected T getValue(Class clazz, Object value, T defaultValue) throws JSONException + { + if (JSONObject.NULL.equals(value)) + { + return defaultValue; + } + + try + { + return clazz.cast(value); + } + catch (Exception ex) + { + throw new JSONException("JSONObject[" + value +"] is not an instance of [" + clazz.getName() +"]"); + } + } + + protected Set getCustomProperties(JSONObject customPropsJsonObj) throws JSONException + { + if (customPropsJsonObj == null) + { + return null; + } + JSONArray keys = customPropsJsonObj.names(); + if (keys == null) + { + return Collections.emptySet(); + } + + Set customProps = new HashSet<>(keys.length()); + for (int i = 0, length = keys.length(); i < length; i++) + { + JSONObject jsonObj = customPropsJsonObj.getJSONObject((String) keys.get(i)); + + QName name = resolveToQName(getValue(String.class, jsonObj.opt(CUSTOM_PARAM_NAME), null)); + validateMandatoryCustomProps(name, CUSTOM_PARAM_NAME); + + Serializable value = null; + Object customPropValue = jsonObj.opt(CUSTOM_PARAM_VALUE); + validateMandatoryCustomProps(customPropValue, CUSTOM_PARAM_VALUE); + + if(customPropValue instanceof JSONArray) + { + JSONArray array = (JSONArray) customPropValue; + ArrayList list = new ArrayList<>(array.length()); + for(int j = 0; j < array.length(); j++) + { + list.add(getSerializableValue(array.get(j))); + } + value = list; + } + else + { + value = getSerializableValue(customPropValue); + } + + customProps.add(new CustomProperties(name, value)); + } + + if (logger.isDebugEnabled() && customProps.size() > 0) + { + logger.debug("Processed custom properties:" + customProps); + } + + return customProps; + } + + protected Set getScopedSites(JSONArray scopedSitesJsonArray) throws JSONException + { + if (scopedSitesJsonArray == null) + { + return null; + } + + Set scopedSites = new HashSet(scopedSitesJsonArray.length()); + for (int i = 0, length = scopedSitesJsonArray.length(); i < length; i++) + { + String site = scopedSitesJsonArray.getString(i); + scopedSites.add(site); + } + return scopedSites; + } + + private void validateMandatoryCustomProps(Object obj, String paramName) throws JSONException + { + if (obj == null) + { + throw new JSONException("Invalid JSONObject in the Custom Properties JSON. [" + paramName + "] cannot be null."); + } + + } + + protected void validateFilterID(String filterID) + { + Matcher matcher = FILTER_ID_PATTERN.matcher(filterID); + if (matcher.find()) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, + "Invalid Filter Id. The characters \" * \\ < > ? / : | are not allowed. The Filter Id cannot end with a dot."); + } + } + + private Serializable getSerializableValue(Object object) throws JSONException + { + if (!(object instanceof Serializable)) + { + throw new JSONException("Invalid value in the Custom Properties JSON. [" + object + "] must be an instance of Serializable."); + } + return (Serializable) object; + } + + private QName resolveToQName(String qnameStr) throws JSONException + { + QName typeQName = null; + if (qnameStr == null) + { + return typeQName; + } + if(qnameStr.charAt(0) == QName.NAMESPACE_BEGIN && qnameStr.indexOf("solrfacetcustomproperty") < 0) + { + throw new JSONException("Invalid name in the Custom Properties JSON. Namespace URL must be [" + SolrFacetModel.SOLR_FACET_CUSTOM_PROPERTY_URL + "]"); + } + else if(qnameStr.charAt(0) == QName.NAMESPACE_BEGIN) + { + typeQName = QName.createQName(qnameStr); + } + else + { + typeQName = QName.createQName(SolrFacetModel.SOLR_FACET_CUSTOM_PROPERTY_URL, qnameStr); + } + if (logger.isDebugEnabled()) + { + logger.debug("Resolved facet's custom property name [" + qnameStr + "] into [" + typeQName + "]"); + } + return typeQName; + } + + /** + * Retrieves the named parameter as an integer, if the parameter is not present the default value is returned. + * + * @param req The WebScript request + * @param paramName The name of parameter to look for. + * @param defaultValue The default value that should be returned if parameter is not present in request or is negative. + * @return The request parameter or default value + * @throws WebScriptException if the named parameter cannot be converted to int (HTTP rsp 400). + */ + protected int getNonNegativeIntParameter(WebScriptRequest req, String paramName, int defaultValue) + { + final String paramString = req.getParameter(paramName); + + final int result; + + if (paramString != null) + { + try + { + final int paramInt = Integer.valueOf(paramString); + + if (paramInt < 0) { result = defaultValue; } + else { result = paramInt; } + } + catch (NumberFormatException e) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); + } + } + else { result = defaultValue; } + + return result; + } + + abstract protected Map unprotectedExecuteImpl(WebScriptRequest req, Status status, Cache cache); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/FacetablePropertiesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/FacetablePropertiesGet.java new file mode 100644 index 00000000000..cc5c3977bbb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/FacetablePropertiesGet.java @@ -0,0 +1,252 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.alfresco.repo.i18n.StaticMessageLookup; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetService.SyntheticPropertyDefinition; +import org.alfresco.repo.web.scripts.facet.FacetablePropertyFTL.FacetablePropertyFTLComparator; +import org.alfresco.repo.web.scripts.facet.FacetablePropertyFTL.SpecialFacetablePropertyFTL; +import org.alfresco.repo.web.scripts.facet.FacetablePropertyFTL.StandardFacetablePropertyFTL; +import org.alfresco.repo.web.scripts.facet.FacetablePropertyFTL.SyntheticFacetablePropertyFTL; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; +import org.alfresco.service.namespace.NamespaceException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ModelUtil; +import org.alfresco.util.ScriptPagingDetails; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the "facetable-properties.get" web script. + * + * @since 5.0 + * @author Neil Mc Erlean + */ +public class FacetablePropertiesGet extends AbstractSolrFacetConfigAdminWebScript +{ + public static final Log logger = LogFactory.getLog(FacetablePropertiesGet.class); + + public static final String PROPERTIES_KEY = "properties"; + + private static final String QUERY_PARAM_MAX_ITEMS = "maxItems"; + private static final String QUERY_PARAM_SKIP_COUNT = "skipCount"; + private static final int DEFAULT_MAX_ITEMS_PER_PAGE = 50; + + private static final String TEMPLATE_VAR_CLASSNAME = "classname"; + private static final String QUERY_PARAM_NAMESPACE = "nsp"; + private static final String QUERY_PARAM_LOCALE = "locale"; + + private NamespaceService namespaceService; + private MessageLookup messageLookup; + + public FacetablePropertiesGet() { messageLookup = new StaticMessageLookup(); } + + public void setNamespaceService (NamespaceService service) { this.namespaceService = service; } + + @Override protected Map executeImpl(final WebScriptRequest req, final Status status, final Cache cache) + { + // Allow all authenticated users in + return unprotectedExecuteImpl(req, status, cache); + } + + @Override protected Map unprotectedExecuteImpl(WebScriptRequest req, Status status, Cache cache) + { + // We use any provided locale to localise some elements of the webscript response, but not all. + final String userLocaleString = req.getParameter(QUERY_PARAM_LOCALE); + final Locale userLocale = (userLocaleString == null) ? Locale.getDefault() : new Locale(userLocaleString); + + // There are multiple defined URIs for this REST endpoint. Some define a "classname" template var. + final Map templateVars = req.getServiceMatch().getTemplateVars(); + final String contentClassName = templateVars.get(TEMPLATE_VAR_CLASSNAME); + + final QName contentClassQName; + try + { + contentClassQName = contentClassName == null ? null : QName.createQName(contentClassName, namespaceService); + } catch (NamespaceException e) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Unrecognised classname: " + contentClassName, e); + } + + // Build an FTL model of facetable properties - this includes normal Alfresco properties and also + // 'synthetic' properties like size and mimetype. See below for more details. + final Map model = new HashMap<>(); + + final SortedSet> facetableProperties; + if (contentClassQName == null) + { + facetableProperties = toFacetablePropertyModel(facetService.getFacetableProperties(), userLocale); + + final List facetableSyntheticProperties = facetService.getFacetableSyntheticProperties(); + facetableProperties.addAll(toFacetablePropertyModel_(facetableSyntheticProperties, userLocale)); + } + else + { + facetableProperties = toFacetablePropertyModel(facetService.getFacetableProperties(contentClassQName), userLocale); + + final List facetableSyntheticProperties = facetService.getFacetableSyntheticProperties(contentClassQName); + facetableProperties.addAll(toFacetablePropertyModel_(facetableSyntheticProperties, userLocale)); + } + + // Always add some hard-coded facetable "properties" + // Note: TAG and SITE are Solr specials and don’t have namespaces + facetableProperties.add(new SpecialFacetablePropertyFTL("TAG", "Tag")); + facetableProperties.add(new SpecialFacetablePropertyFTL("SITE", "Site")); + + // The webscript allows for some further filtering of results: + List filters = new ArrayList<>(); + + // Filter by property QName namespace: + final String namespaceFilter = req.getParameter(QUERY_PARAM_NAMESPACE); + if (namespaceFilter != null) + { + filters.add(new ResultFilter() + { + @Override public boolean filter(FacetablePropertyFTL facetableProperty) + { + final QName propQName = facetableProperty.getQname(); + Collection prefixes = namespaceService.getPrefixes(propQName.getNamespaceURI()); + return prefixes.contains(namespaceFilter); + } + }); + } + + List> filteredFacetableProperties = filter(facetableProperties, filters); + + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved " + facetableProperties.size() + " available facets; filtered to " + filteredFacetableProperties.size()); + } + + // Create paging + ScriptPagingDetails paging = new ScriptPagingDetails( + getNonNegativeIntParameter(req, QUERY_PARAM_MAX_ITEMS, DEFAULT_MAX_ITEMS_PER_PAGE), + getNonNegativeIntParameter(req, QUERY_PARAM_SKIP_COUNT, 0)); + + model.put(PROPERTIES_KEY, ModelUtil.page(filteredFacetableProperties, paging)); + model.put("paging", ModelUtil.buildPaging(paging)); + + return model; + } + + /** + * This type defines the (inclusion) filtering of {@link FacetablePropertyFTL property data} + * in the response to this webscript. + */ + private static interface ResultFilter + { + /** @return {@code true} if the specified property should be included. */ + public boolean filter(FacetablePropertyFTL facetableProperty); + } + + /** + * This method returns a new List instance containing only those {@link FacetablePropertyFTL property data} + * that satisfy all {@link ResultFilter filters}. + */ + private List> filter(Collection> propsData, List filters) + { + final List> filteredResult = new ArrayList<>(); + + for (FacetablePropertyFTL prop : propsData) + { + boolean passedAllFilters = true; + for (ResultFilter filter : filters) + { + if (!filter.filter(prop)) { passedAllFilters = false; } + } + if (passedAllFilters) { filteredResult.add(prop); } + } + + return filteredResult; + } + + /** This method returns a {@link FacetablePropertyFTL} for the specified {@link PropertyDefinition}. */ + private FacetablePropertyFTL toFacetablePropertyModel(PropertyDefinition propDef, Locale locale) + { + String title = propDef.getTitle(messageLookup, locale); + return new StandardFacetablePropertyFTL(propDef, title); + } + + /** This method returns a {@link FacetablePropertyFTL} for the specified {@link SyntheticPropertyDefinition}. */ + private FacetablePropertyFTL toFacetablePropertyModel(SyntheticPropertyDefinition propDef, + Locale locale) + { + // Note the hard-coded assumption here that all synthetic properties are defined only + // within the cm:content property type. This code is not designed to be extended. + // TODO We may need to make this code extensible in a future release. + // + // See e.g. content-model.properties for usage of this i18n key. + final String i18nKeyPrefix = "cm_contentmodel.property.cm_content.cm_content."; + final String localisedTitle = I18NUtil.getMessage(i18nKeyPrefix + propDef.syntheticPropertyName, locale); + + return new SyntheticFacetablePropertyFTL(propDef.containingPropertyDef, + localisedTitle, + propDef.syntheticPropertyName, + propDef.dataTypeDefinition); + } + + private SortedSet> toFacetablePropertyModel(Collection propDefs, + Locale locale) + { + SortedSet> result = new TreeSet<>(new FacetablePropertyFTLComparator()); + for (PropertyDefinition propDef : propDefs) + { + result.add(toFacetablePropertyModel(propDef, locale)); + } + return result; + } + + // Note: the trailing underscore in this method name is to prevent a clash between this method and the + // one that takes a Collection as Java's type erasure means that both methods would have the + // same signature, without the trailing underscore. + private SortedSet> toFacetablePropertyModel_(Collection propDefs, + Locale locale) + { + SortedSet> result = new TreeSet<>(new FacetablePropertyFTLComparator()); + for (SyntheticPropertyDefinition propDef : propDefs) + { + result.add(toFacetablePropertyModel(propDef, locale)); + } + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/FacetablePropertyFTL.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/FacetablePropertyFTL.java new file mode 100644 index 00000000000..981b1722d0e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/FacetablePropertyFTL.java @@ -0,0 +1,371 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; + +/** + * This interface defines a simple POJO/DTO for use in the FTL model and rendering in the JSON API. + * @param a type to ensure that the comparator is implemented in a typesafe way. + * @since 5.0 + */ +public abstract class FacetablePropertyFTL implements Comparable +{ + /** The localised title for this property. */ + protected final String localisedTitle; + + public FacetablePropertyFTL(String localisedTitle) { this.localisedTitle = localisedTitle; } + + // We use "*Qname*" (small 'n') in these accessors to make the FTL less ambiguous. + public abstract String getShortQname(); + public abstract QName getQname(); + public abstract String getDisplayName(); + public abstract QName getContainerClassType(); + public abstract QName getDataType(); + public abstract QName getModelQname(); + + public String getTitle() { return localisedTitle; } + + /** This class represents a normal Alfresco property which is facetable. */ + public static class StandardFacetablePropertyFTL extends FacetablePropertyFTL + { + /** The Alfresco property definition which declares this facetable property. */ + protected final PropertyDefinition propDef; + + /** A display name for this property. */ + protected final String displayName; + + /** + * @param propDef The {@link PropertyDefinition}. + * @param localisedTitle The localised title for this property e.g. "Titre". + */ + public StandardFacetablePropertyFTL(PropertyDefinition propDef, String localisedTitle) + { + super(localisedTitle); + + this.propDef = propDef; + this.displayName = getShortQname() + (localisedTitle == null ? "" : " (" + localisedTitle + ")"); + } + + @Override public String getShortQname() { return propDef.getName().getPrefixString(); } + + @Override public QName getQname() { return propDef.getName(); } + + @Override public String getDisplayName() { return displayName; } + + @Override public QName getContainerClassType() { return propDef.getContainerClass().getName(); } + + @Override public QName getDataType() { return propDef.getDataType().getName(); } + + @Override public QName getModelQname() { return propDef.getModel().getName(); } + + @Override public boolean equals(Object obj) + { + if (this == obj) { return true; } + if (obj == null) { return false; } + if (getClass() != obj.getClass()) { return false; } + + StandardFacetablePropertyFTL other = (StandardFacetablePropertyFTL) obj; + if (displayName == null) + { + if (other.displayName != null) { return false; } + } else if (!displayName.equals(other.displayName)) { return false; } + if (localisedTitle == null) + { + if (other.localisedTitle != null) + return false; + } else if (!localisedTitle.equals(other.localisedTitle)) + return false; + if (propDef == null) + { + if (other.propDef != null) + return false; + } else if (!propDef.equals(other.propDef)) + return false; + return true; + } + + @Override public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime * result + ((localisedTitle == null) ? 0 : localisedTitle.hashCode()); + result = prime * result + ((propDef == null) ? 0 : propDef.hashCode()); + return result; + } + + @Override public int compareTo(StandardFacetablePropertyFTL that) + { + final int modelComparison = this.propDef.getModel().getName().compareTo(that.propDef.getModel().getName()); + final int classComparison = this.propDef.getContainerClass().getName().compareTo(that.propDef.getContainerClass().getName()); + final int propComparison = this.propDef.getName().compareTo(that.propDef.getName()); + + final int result; + if (modelComparison != 0) { result = modelComparison; } + else if (classComparison != 0) { result = classComparison; } + else { result = propComparison; } + + return result; + } + } + + /** + * This class represents a facetable property, which is not actually an Alfresco + * content property, but is closely associated with one. + * Examples are the {@code size} and {@code MIME type} fields within the {@code cm:content} property type. + */ + public static class SyntheticFacetablePropertyFTL extends FacetablePropertyFTL + { + /** The PropertyDefinition which 'contains' this synthetic property. */ + private final PropertyDefinition containingPropDef; + + /** This is the name of the synthetic property e.g. "size". Not localised. */ + private final String syntheticPropertyName; + + /** The type of this synthetic data property. */ + private final QName datatype; + + private final String displayName; + + /** + * @param containingPropDef The {@link PropertyDefinition}. + * @param localisedTitle The localised title of this synthetic property e.g. "taille". + * @param syntheticPropertyName The synthetic property name e.g. "size". + * @param datatype The datatype of the synthetic property. + */ + public SyntheticFacetablePropertyFTL(PropertyDefinition containingPropDef, + String localisedTitle, + String syntheticPropertyName, + QName datatype) + { + super(localisedTitle); + this.containingPropDef = containingPropDef; + this.syntheticPropertyName = syntheticPropertyName; + this.datatype = datatype; + this.displayName = getShortQname() + (localisedTitle == null ? "" : " (" + localisedTitle + ")"); + } + + @Override public String getShortQname() + { + return containingPropDef.getName().getPrefixString() + "." + this.syntheticPropertyName; + } + + @Override public QName getQname() + { + final QName containingPropQName = containingPropDef.getName(); + return QName.createQName(containingPropQName.getNamespaceURI(), + containingPropQName.getLocalName() + "." + this.syntheticPropertyName); + } + + @Override public QName getDataType() { return datatype; } + + @Override public QName getContainerClassType() { return containingPropDef.getContainerClass().getName(); }; + + @Override public QName getModelQname() { return containingPropDef.getModel().getName(); } + + @Override public String getDisplayName() { return displayName; } + + @Override public int compareTo(SyntheticFacetablePropertyFTL that) + { + final int modelComparison = this.containingPropDef.getModel().getName().compareTo(that.containingPropDef.getModel().getName()); + final int classComparison = this.containingPropDef.getContainerClass().getName().compareTo(that.containingPropDef.getContainerClass().getName()); + final int propComparison = this.containingPropDef.getName().compareTo(that.containingPropDef.getName()); + final int displayNameComparison = this.displayName.compareTo(that.displayName); + + final int result; + if (modelComparison != 0) { result = modelComparison; } + else if (classComparison != 0) { result = classComparison; } + else if (propComparison != 0) { result = propComparison; } + else { result = displayNameComparison; } + + return result; + } + + @Override public int hashCode() + { + final int prime = 31; + int result = super.hashCode(); + result = prime + * result + + ((containingPropDef == null) ? 0 : containingPropDef + .hashCode()); + result = prime * result + + ((datatype == null) ? 0 : datatype.hashCode()); + result = prime * result + + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime + * result + + ((syntheticPropertyName == null) ? 0 + : syntheticPropertyName.hashCode()); + return result; + } + + @Override public boolean equals(Object obj) + { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + SyntheticFacetablePropertyFTL other = (SyntheticFacetablePropertyFTL) obj; + if (containingPropDef == null) + { + if (other.containingPropDef != null) + return false; + } else if (!containingPropDef.equals(other.containingPropDef)) + return false; + if (datatype == null) + { + if (other.datatype != null) + return false; + } else if (!datatype.equals(other.datatype)) + return false; + if (displayName == null) + { + if (other.displayName != null) + return false; + } else if (!displayName.equals(other.displayName)) + return false; + if (syntheticPropertyName == null) + { + if (other.syntheticPropertyName != null) + return false; + } else if (!syntheticPropertyName + .equals(other.syntheticPropertyName)) + return false; + return true; + } + } + + /** + * This class represents a hard-coded facetable pseudo-property. It is not an Alfresco property + * and yet it is something that Alfresco and SOLR can use for facetting. + * Examples are the {@code TAG} and {@code SITE} facets. + */ + public static class SpecialFacetablePropertyFTL extends FacetablePropertyFTL + { + /** This is the name of the property e.g. "SITE". Not localised. */ + private final String name; + + private final String displayName; + + + /** + * @param localisedTitle The localised title of this synthetic property e.g. "taille". + */ + public SpecialFacetablePropertyFTL(String name, String localisedTitle) + { + super(localisedTitle); + this.name = name; + this.displayName = localisedTitle; + } + + @Override public String getShortQname() { return name; } + + @Override public QName getQname() { return null; } + + @Override public QName getDataType() { return null; } + + @Override public QName getContainerClassType() { return null; } + + @Override public QName getModelQname() { return null; } + + @Override public String getDisplayName() { return displayName; } + + @Override public int compareTo(SpecialFacetablePropertyFTL that) + { + return this.name.compareTo(that.name); + } + + @Override public int hashCode() + { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override public boolean equals(Object obj) + { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + SpecialFacetablePropertyFTL other = (SpecialFacetablePropertyFTL) obj; + if (displayName == null) + { + if (other.displayName != null) + return false; + } else if (!displayName.equals(other.displayName)) + return false; + if (name == null) + { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + } + + /** + * In order to give deterministic responses when getting facetable properties, + * all {@link FacetablePropertyFTL} instances are sorted. This comparator provides + * the sorting implementation. + */ + public static class FacetablePropertyFTLComparator implements Comparator> + { + /** Used when sorting two objects of different types. */ + private final List> typeOrder = Arrays.asList(new Class[] { SpecialFacetablePropertyFTL.class, + SyntheticFacetablePropertyFTL.class, + StandardFacetablePropertyFTL.class} ); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override public int compare(FacetablePropertyFTL left, FacetablePropertyFTL right) + { + // First of all sort by the class according to the list above. + if ( !left.getClass().equals(right.getClass())) { return typeOrder.indexOf(left.getClass()) - + typeOrder.indexOf(right.getClass()); } + else + { + // Otherwise we have two facetable properties of the same class. + return left.compareTo(right); + } + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminDelete.java new file mode 100644 index 00000000000..7b048648bbd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminDelete.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the "solr-facet-config-admin.delete" web script. + * + * @author Jamal Kaabi-Mofrad + */ +public class SolrFacetConfigAdminDelete extends AbstractSolrFacetConfigAdminWebScript +{ + private static final Log logger = LogFactory.getLog(SolrFacetConfigAdminDelete.class); + + @Override + protected Map unprotectedExecuteImpl(WebScriptRequest req, Status status, Cache cache) + { + // get the filterID parameter. + Map templateVars = req.getServiceMatch().getTemplateVars(); + String filterID = templateVars.get("filterID"); + + if (filterID == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Filter id not provided"); + } + facetService.deleteFacet(filterID); + + Map model = new HashMap(1); + model.put("success", true); + + if (logger.isDebugEnabled()) + { + logger.debug("Facet [" + filterID + "] has been deleted successfully"); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminGet.java new file mode 100644 index 00000000000..91bf683b1dd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminGet.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the "solr-facet-config-admin.get" web script. + * + * @author Jamal Kaabi-Mofrad + */ +public class SolrFacetConfigAdminGet extends AbstractSolrFacetConfigAdminWebScript +{ + private static final Log logger = LogFactory.getLog(SolrFacetConfigAdminGet.class); + + @Override + protected Map executeImpl(final WebScriptRequest req, final Status status, final Cache cache) + { + // Allow all authenticated users view the filters + return unprotectedExecuteImpl(req, status, cache); + } + + @Override + protected Map unprotectedExecuteImpl(WebScriptRequest req, Status status, Cache cache) + { + // get the filterID parameter. + Map templateVars = req.getServiceMatch().getTemplateVars(); + String filterID = templateVars.get("filterID"); + + Map model = new HashMap(1); + + if (filterID == null) + { + model.put("filters", facetService.getFacets()); + } + else + { + SolrFacetProperties fp = facetService.getFacet(filterID); + if (fp == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Filter not found"); + } + model.put("filter", fp); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved all available facets: " + model.values()); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminPost.java new file mode 100644 index 00000000000..31c6adf3f57 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminPost.java @@ -0,0 +1,133 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.search.impl.solr.facet.FacetQNameUtils; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the "solr-facet-config-admin.post" web scripts. + * + * @author Jamal Kaabi-Mofrad + */ +public class SolrFacetConfigAdminPost extends AbstractSolrFacetConfigAdminWebScript +{ + private static final Log logger = LogFactory.getLog(SolrFacetConfigAdminPost.class); + + @Override + protected Map unprotectedExecuteImpl(WebScriptRequest req, Status status, Cache cache) + { + try + { + SolrFacetProperties fp = parseRequestForFacetProperties(req); + facetService.createFacetNode(fp); + + if (logger.isDebugEnabled()) + { + logger.debug("Created facet node: " + fp); + } + } + catch (Throwable t) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not save the facet configuration.", t); + } + + return new HashMap<>(); // Needs to be mutable. + } + + private SolrFacetProperties parseRequestForFacetProperties(WebScriptRequest req) + { + JSONObject json = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + final String filterID = json.getString(PARAM_FILTER_ID); + validateFilterID(filterID); + + final String facetQNameStr = json.getString(PARAM_FACET_QNAME); + // Note: we're using this util class here because we need to be able to deal with + // qnames without a URI e.g. "{}SITE" and *not* have them default to the cm: namespace + // which happens with the 'normal' Alfresco QName code. + final QName facetQName = FacetQNameUtils.createQName(facetQNameStr, namespaceService); + final String displayName = json.getString(PARAM_DISPLAY_NAME); + final String displayControl = json.getString(PARAM_DISPLAY_CONTROL); + final int maxFilters = json.getInt(PARAM_MAX_FILTERS); + final int hitThreshold = json.getInt(PARAM_HIT_THRESHOLD); + final int minFilterValueLength = json.getInt(PARAM_MIN_FILTER_VALUE_LENGTH); + final String sortBy = json.getString(PARAM_SORT_BY); + // Optional params + final String scope = getValue(String.class, json.opt(PARAM_SCOPE), "ALL"); + final boolean isEnabled = getValue(Boolean.class, json.opt(PARAM_IS_ENABLED), false); + JSONArray scopedSitesJsonArray = getValue(JSONArray.class, json.opt(PARAM_SCOPED_SITES), null); + final Set scopedSites = getScopedSites(scopedSitesJsonArray); + final JSONObject customPropJsonObj = getValue(JSONObject.class, json.opt(PARAM_CUSTOM_PROPERTIES), null); + final Set customProps = getCustomProperties(customPropJsonObj); + + SolrFacetProperties fp = new SolrFacetProperties.Builder() + .filterID(filterID) + .facetQName(facetQName) + .displayName(displayName) + .displayControl(displayControl) + .maxFilters(maxFilters) + .hitThreshold(hitThreshold) + .minFilterValueLength(minFilterValueLength) + .sortBy(sortBy) + .scope(scope) + .isEnabled(isEnabled) + .scopedSites(scopedSites) + .customProperties(customProps).build(); + return fp; + } + catch (IOException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", e); + } + catch (JSONException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", e); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminPut.java new file mode 100644 index 00000000000..2c2dd2f3cd7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/facet/SolrFacetConfigAdminPut.java @@ -0,0 +1,190 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.facet; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.search.impl.solr.facet.Exceptions.UnrecognisedFacetId; +import org.alfresco.repo.search.impl.solr.facet.FacetQNameUtils; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.collections.CollectionUtils; +import org.alfresco.util.collections.Function; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the "solr-facet-config-admin.put" web scripts. + * + * @author Jamal Kaabi-Mofrad + * @author Neil Mc Erlean + * @since 5.0 + */ +public class SolrFacetConfigAdminPut extends AbstractSolrFacetConfigAdminWebScript +{ + private static final Log logger = LogFactory.getLog(SolrFacetConfigAdminPost.class); + + protected static final String PARAM_RELATIVE_POS = "relativePos"; + protected static final String URL_PARAM_FILTER_ID = "filterID"; + + @Override + protected Map unprotectedExecuteImpl(WebScriptRequest req, Status status, Cache cache) + { + final String relativePosString = req.getParameter(PARAM_RELATIVE_POS); + try + { + if (relativePosString != null) + { + // This is a request to 'move' (reposition) the specified facet. + + // We need the relative position that the facet will move. + final int relativePos; + + try { relativePos = Integer.parseInt(relativePosString); } + catch (NumberFormatException nfe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Cannot move facet as could not parse relative position: '" + relativePosString + "'"); + } + + // And we need the filterID for the facet we're moving. + final Map templateVars = req.getServiceMatch().getTemplateVars(); + String filterId = templateVars.get(URL_PARAM_FILTER_ID); + + if (filterId == null) { throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Illegal null filterId"); } + + // So let's move the filter... + try + { + // Get the current sequence of filter IDs. + List facets = facetService.getFacets(); + List facetIDs = CollectionUtils.transform(facets, new Function() + { + @Override public String apply(SolrFacetProperties value) + { + return value.getFilterID(); + } + }); + + List reorderedIDs = CollectionUtils.moveRight(relativePos, filterId, facetIDs); + + this.facetService.reorderFacets(reorderedIDs); + + if (logger.isDebugEnabled()) { logger.debug("Moved facet " + filterId + " to relative position: " + relativePos); } + } + catch (UnrecognisedFacetId ufi) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Unrecognised filter ID: " + ufi.getFacetId()); + } + } + // TODO Allow for simultaneous move and update of facet. + else + { + SolrFacetProperties fp = parseRequestForFacetProperties(req); + facetService.updateFacet(fp); + + if (logger.isDebugEnabled()) + { + logger.debug("Updated facet node: " + fp); + } + } + } + catch (Throwable t) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not update the facet configuration.", t); + } + + Map model = new HashMap(1); + return model; + } + + private SolrFacetProperties parseRequestForFacetProperties(WebScriptRequest req) + { + JSONObject json = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + final String filterID = json.getString(PARAM_FILTER_ID); // Must exist + + final String facetQNameStr = getValue(String.class, json.opt(PARAM_FACET_QNAME), null); + + // Note that in resolving the QName string here, we expect there to be some facet QNames which are not + // really QNames. These are SOLR/SearchService 'specials', examples being "SITE" or "TAG". + // These will be resolved here to a QName with no namespace. + final QName facetQName = (facetQNameStr == null) ? null : FacetQNameUtils.createQName(facetQNameStr, namespaceService); + final String displayName = getValue(String.class, json.opt(PARAM_DISPLAY_NAME), null); + final String displayControl = getValue(String.class, json.opt(PARAM_DISPLAY_CONTROL), null); + final int maxFilters = getValue(Integer.class, json.opt(PARAM_MAX_FILTERS), -1); + final int hitThreshold = getValue(Integer.class, json.opt(PARAM_HIT_THRESHOLD), -1); + final int minFilterValueLength = getValue(Integer.class, json.opt(PARAM_MIN_FILTER_VALUE_LENGTH), -1); + final String sortBy = getValue(String.class, json.opt(PARAM_SORT_BY), null); + final String scope = getValue(String.class, json.opt(PARAM_SCOPE), null); + final Boolean isEnabled = getValue(Boolean.class, json.opt(PARAM_IS_ENABLED), null); + JSONArray scopedSitesJsonArray = getValue(JSONArray.class, json.opt(PARAM_SCOPED_SITES), null); + final Set scopedSites = getScopedSites(scopedSitesJsonArray); + final JSONObject customPropJsonObj = getValue(JSONObject.class, json.opt(PARAM_CUSTOM_PROPERTIES), null); + final Set customProps = getCustomProperties(customPropJsonObj); + + SolrFacetProperties fp = new SolrFacetProperties.Builder() + .filterID(filterID) + .facetQName(facetQName) + .displayName(displayName) + .displayControl(displayControl) + .maxFilters(maxFilters) + .hitThreshold(hitThreshold) + .minFilterValueLength(minFilterValueLength) + .sortBy(sortBy) + .scope(scope) + .isEnabled(isEnabled) + .scopedSites(scopedSites) + .customProperties(customProps).build(); + return fp; + } + catch (IOException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", e); + } + catch (JSONException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", e); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/groups/package-info.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/groups/package-info.java new file mode 100644 index 00000000000..89ba167429b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/groups/package-info.java @@ -0,0 +1,33 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +/** + * Provides a unit tests for the web scripts of the REST bindings of the groups service. + * @since 3.1 + */ +@PackageMarker +package org.alfresco.repo.web.scripts.groups; +import org.alfresco.util.PackageMarker; + diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/invitation/InvitationDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invitation/InvitationDelete.java new file mode 100644 index 00000000000..e196c9f05e3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invitation/InvitationDelete.java @@ -0,0 +1,159 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.invitation; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.service.cmr.invitation.Invitation; +import org.alfresco.service.cmr.invitation.InvitationExceptionForbidden; +import org.alfresco.service.cmr.invitation.InvitationService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Cancel invitation for a web site; This is the controller for the + * org/alfresco/repository/site/invitation/invitation.delete.desc.xml webscript + */ +public class InvitationDelete extends DeclarativeWebScript +{ + // services + private InvitationService invitationService; + private SiteService siteService; + + public void setInvitationService(InvitationService invitationService) + { + this.invitationService = invitationService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + + Map model = new HashMap(); + + Map templateVars = req.getServiceMatch().getTemplateVars(); + final String siteShortName = templateVars.get("shortname"); + final String invitationId = templateVars.get("invitationId"); + validateParameters(siteShortName, invitationId); + + try + { + // MNT-9905 Pending Invites created by one site manager aren't visible to other site managers + String currentUser = AuthenticationUtil.getRunAsUser(); + + if (siteShortName != null && (SiteModel.SITE_MANAGER).equals(siteService.getMembersRole(siteShortName, currentUser))) + { + + RunAsWork runAsSystem = new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + checkAndCancelTheInvitation(invitationId, siteShortName); + return null; + } + }; + + AuthenticationUtil.runAs(runAsSystem, AuthenticationUtil.getSystemUserName()); + } + else + { + checkAndCancelTheInvitation(invitationId, siteShortName); + } + } + catch (InvitationExceptionForbidden fe) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "Unable to cancel workflow", fe); + } + catch (AccessDeniedException ade) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "Unable to cancel workflow", ade); + } + + return model; + } + + private void validateParameters(String siteShortName, String invitationId) + { + if ((invitationId == null) || (invitationId.length() == 0)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid invitation id provided"); + } + + SiteInfo site = siteService.getSite(siteShortName); + if (site == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Invalid site id provided"); + } + } + + protected void checkAndCancelTheInvitation(final String invId, String siteShortName) + { + Invitation invitation = null; + try + { + invitation = invitationService.getInvitation(invId); + } + catch (org.alfresco.service.cmr.invitation.InvitationExceptionNotFound ienf) + { + throwInvitationNotFoundException(invId, siteShortName); + } + if (invitation == null) + { + throwInvitationNotFoundException(invId, siteShortName); + } + + // check that this invitation really belongs to the specified siteShortName + if (invitation != null && invitation.getResourceName() != null && !siteShortName.equals(invitation.getResourceName())) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "Unable to cancel workflow"); + } + + invitationService.cancel(invId); + } + + protected void throwInvitationNotFoundException(final String invId, String siteShortName) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, + "The invitation :" + invId + " for web site :" + siteShortName + ", does not exist."); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/invitation/package-info.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invitation/package-info.java new file mode 100644 index 00000000000..cd343a1d717 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invitation/package-info.java @@ -0,0 +1,33 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +/** + * Provides a unit tests for the web scripts of the REST bindings of the invitation service. + * @since 3.1 + */ +@PackageMarker +package org.alfresco.repo.web.scripts.invitation; +import org.alfresco.util.PackageMarker; + diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/InviteByTicket.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/InviteByTicket.java new file mode 100644 index 00000000000..cf563cb3f78 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/InviteByTicket.java @@ -0,0 +1,234 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.invite; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.invitation.WorkflowModelNominatedInvitation; +import org.alfresco.repo.invitation.site.InviteInfo; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.template.TemplateNode; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.invitation.Invitation; +import org.alfresco.service.cmr.invitation.InvitationExceptionNotFound; +import org.alfresco.service.cmr.invitation.InvitationService; +import org.alfresco.service.cmr.invitation.NominatedInvitation; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.cmr.workflow.WorkflowTaskQuery; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web Script which returns invite information given an inviteId and inviteTicket. + * + * Note: This Web Script is accessible without authentication. + * + * @author glen dot johnson at alfresco dot com + */ +public class InviteByTicket extends DeclarativeWebScript +{ + // request parameter names + private static final String PARAM_INVITEE_USER_NAME = "inviteeUserName"; + + // service instances + private ServiceRegistry serviceRegistry; + private SiteService siteService; + private InvitationService invitationService; + private TenantService tenantService; + + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public void setInvitationService(InvitationService invitationService) + { + this.invitationService = invitationService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + /* + * (non-Javadoc) + * + * @see + * org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco + * .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(final WebScriptRequest req, final Status status) + { + String tenantDomain = TenantService.DEFAULT_DOMAIN; + + if (tenantService.isEnabled()) + { + String inviteeUserName = req.getParameter(PARAM_INVITEE_USER_NAME); + if (inviteeUserName != null) + { + tenantDomain = tenantService.getUserDomain(inviteeUserName); + } + } + + // run as system user + String mtAwareSystemUser = tenantService.getDomainUser(AuthenticationUtil.getSystemUserName(), tenantDomain); + + Map ret = TenantUtil.runAsSystemTenant(new TenantRunAsWork>() + { + public Map doWork() throws Exception + { + return execute(req, status); + } + }, tenantDomain); + + // authenticate as system for the rest of the webscript + AuthenticationUtil.setRunAsUser(mtAwareSystemUser); + + return ret; + } + + private Map execute(WebScriptRequest req, Status status) + { + // initialise model to pass on for template to render + Map model = new HashMap(); + + // get inviteId and inviteTicket + String inviteId = req.getServiceMatch().getTemplateVars().get("inviteId"); + String inviteTicket = req.getServiceMatch().getTemplateVars().get("inviteTicket"); + + try + { + Invitation invitation = invitationService.getInvitation(inviteId); + + if (invitation instanceof NominatedInvitation) + { + NominatedInvitation theInvitation = (NominatedInvitation)invitation; + String ticket = theInvitation.getTicket(); + if (ticket == null || (! ticket.equals(inviteTicket))) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Ticket mismatch"); + } + // return the invite info + model.put("invite", toInviteInfo(theInvitation)); + return model; + } + else + { + // Not a nominated invitation + throw new WebScriptException(Status.STATUS_FORBIDDEN, "Not a nominated invitation"); + } + } + catch (InvitationExceptionNotFound nfe) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, + "No invite found for given id"); + } + } + + private InviteInfo toInviteInfo(NominatedInvitation invitation) + { + final PersonService personService = serviceRegistry.getPersonService(); + + // get the site info + SiteInfo siteInfo = siteService.getSite(invitation.getResourceName()); + String invitationStatus = getInvitationStatus(invitation); + + NodeRef inviterRef = personService.getPerson(invitation.getInviterUserName()); + TemplateNode inviterPerson = null; + if (inviterRef != null) + { + inviterPerson = new TemplateNode(inviterRef, serviceRegistry, null); + } + + // fetch the person node for the invitee + NodeRef inviteeRef = personService.getPerson(invitation.getInviteeUserName()); + TemplateNode inviteePerson = null; + if (inviteeRef != null) + { + inviteePerson = new TemplateNode(inviteeRef, serviceRegistry, null); + } + + InviteInfo ret = new InviteInfo(invitationStatus, + invitation.getInviterUserName(), + inviterPerson, + invitation.getInviteeUserName(), + inviteePerson, + invitation.getRoleName(), + invitation.getResourceName(), + siteInfo, + invitation.getSentInviteDate(), + invitation.getInviteId()); + + return ret; + } + + private String getInvitationStatus(NominatedInvitation invitation) + { + String invitee = invitation.getInviteeUserName(); + String site = invitation.getResourceName(); + // check is invitee is site member + boolean isUserMember = serviceRegistry.getSiteService().isMember(site, invitee); + + WorkflowTaskQuery query = new WorkflowTaskQuery(); + query.setTaskName(WorkflowModelNominatedInvitation.WF_TASK_ACTIVIT_INVITE_PENDING); + query.setProcessId(invitation.getInviteId()); + // query current workflow's task activitiInvitePendingTask + List pendingInvitationTasks = serviceRegistry.getWorkflowService().queryTasks(query, false); + // if it's here - pending invitation + if (!pendingInvitationTasks.isEmpty()) + { + return InviteInfo.INVITATION_STATUS_PENDING; + } + else if (isUserMember) + { + return InviteInfo.INVITATION_STATUS_ACCEPTED; + } + else + { + return InviteInfo.INVITATION_STATUS_REJECTED; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/InviteResponse.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/InviteResponse.java new file mode 100644 index 00000000000..33992716217 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/InviteResponse.java @@ -0,0 +1,183 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.invite; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.service.cmr.invitation.Invitation; +import org.alfresco.service.cmr.invitation.InvitationExceptionForbidden; +import org.alfresco.service.cmr.invitation.InvitationExceptionUserError; +import org.alfresco.service.cmr.invitation.InvitationService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web Script invoked by Invitee to either accept (response='accept') an + * invitation from a Site Manager (Inviter) to join a Site as a Site + * Collaborator, or to reject (response='reject') an invitation that has already + * been sent out + * + * @author glen dot johnson at alfresco dot com + */ +public class InviteResponse extends DeclarativeWebScript +{ + private static final String RESPONSE_ACCEPT = "accept"; + private static final String RESPONSE_REJECT = "reject"; + private static final String MODEL_PROP_KEY_RESPONSE = "response"; + private static final String MODEL_PROP_KEY_SITE_SHORT_NAME = "siteShortName"; + + // request parameter names + private static final String PARAM_INVITEE_USER_NAME = "inviteeUserName"; + + // properties for services + private InvitationService invitationService; + private TenantService tenantService; + + + public void setInvitationService(InvitationService invitationService) + { + this.invitationService = invitationService; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + /* + * (non-Javadoc) + * + * @see + * org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco + * .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(final WebScriptRequest req, final Status status) + { + String tenantDomain = TenantService.DEFAULT_DOMAIN; + final String inviteeUserName = req.getParameter(PARAM_INVITEE_USER_NAME); + + if (tenantService.isEnabled()) + { + if (inviteeUserName != null) + { + tenantDomain = tenantService.getUserDomain(inviteeUserName); + } + } + + // run as system user + return TenantUtil.runAsSystemTenant(new TenantRunAsWork>() + { + public Map doWork() throws Exception + { + String oldUser = null; + try + { + if (inviteeUserName != null && !inviteeUserName.equals(oldUser)) + { + oldUser = AuthenticationUtil.getFullyAuthenticatedUser(); + AuthenticationUtil.setFullyAuthenticatedUser(inviteeUserName); + } + return execute(req, status); + } + finally + { + if (oldUser != null && !oldUser.equals(inviteeUserName)) + { + AuthenticationUtil.setFullyAuthenticatedUser(oldUser); + } + } + } + }, tenantDomain); + } + + private Map execute(WebScriptRequest req, Status status) + { + // initialise model to pass on for template to render + Map model = new HashMap(); + + String inviteId = req.getServiceMatch().getTemplateVars().get("inviteId"); + String inviteTicket = req.getServiceMatch().getTemplateVars().get("inviteTicket"); + + // Check that the task is still open. + //if(inviteStart) + + // process response + String action = req.getServiceMatch().getTemplateVars().get("action"); + if (action.equals("accept")) + { + try + { + Invitation invitation = invitationService.accept(inviteId, inviteTicket); + // add model properties for template to render + model.put(MODEL_PROP_KEY_RESPONSE, RESPONSE_ACCEPT); + model.put(MODEL_PROP_KEY_SITE_SHORT_NAME, invitation.getResourceName()); + } + catch (InvitationExceptionForbidden fe) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, fe.toString()); + } + catch (InvitationExceptionUserError fe) + { + throw new WebScriptException(Status.STATUS_CONFLICT, fe.toString()); + } + } + else if (action.equals("reject")) + { + try + { + Invitation invitation = invitationService.reject(inviteId, "Rejected"); + // add model properties for template to render + model.put(MODEL_PROP_KEY_RESPONSE, RESPONSE_REJECT); + model.put(MODEL_PROP_KEY_SITE_SHORT_NAME, invitation.getResourceName()); + } + catch (InvitationExceptionForbidden fe) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, fe.toString()); + } + catch (InvitationExceptionUserError fe) + { + throw new WebScriptException(Status.STATUS_CONFLICT, fe.toString()); + } + } + else + { + /* handle unrecognised method */ + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "action " + action + " is not supported by this webscript."); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/Invites.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/Invites.java new file mode 100644 index 00000000000..899f05f8398 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/invite/Invites.java @@ -0,0 +1,323 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.invite; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.invitation.InvitationSearchCriteriaImpl; +import org.alfresco.repo.invitation.site.InviteInfo; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.repo.template.TemplateNode; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.invitation.Invitation; +import org.alfresco.service.cmr.invitation.InvitationSearchCriteria; +import org.alfresco.service.cmr.invitation.InvitationService; +import org.alfresco.service.cmr.invitation.NominatedInvitation; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web Script which returns pending Site invitations matching at least one of + * + * (1a) inviter (inviter user name). i.e. pending invitations which have been + * sent by that inviter, but which have not been responded to (accepted or + * rejected) by the invitee, and have not been cancelled by that inviter + * + * (1b) invitee (invitee user name), i.e. pending invitations which have not + * been accepted or rejected yet by that inviter + * + * (1c) site (site short name), i.e. pending invitations sent out to join that + * Site. If only the site is given, then all pending invites are returned, + * irrespective of who the inviters or invitees are + * + * or + * + * (2) matching the given invite ID + * + * + * @author glen dot johnson at alfresco dot com + */ +public class Invites extends DeclarativeWebScript +{ + // request parameter names + private static final String PARAM_INVITER_USER_NAME = "inviterUserName"; + private static final String PARAM_INVITEE_USER_NAME = "inviteeUserName"; + private static final String PARAM_SITE_SHORT_NAME = "siteShortName"; + private static final String PARAM_INVITE_ID = "inviteId"; + + // model key names + private static final String MODEL_KEY_NAME_INVITES = "invites"; + + // service instances + private WorkflowService workflowService; + private ServiceRegistry serviceRegistry; + private SiteService siteService; + private InvitationService invitationService; + + /** + * Set the workflow service property + * + * @param workflowService + * the workflow service to set + */ + public void setWorkflowService(WorkflowService workflowService) + { + this.workflowService = workflowService; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setInvitationService(InvitationService invitationService) + { + this.invitationService = invitationService; + } + + public InvitationService getInvitationService() + { + return invitationService; + } + + /* + * (non-Javadoc) + * + * @see + * org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco + * .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, + Status status) + { + // initialise model to pass on for template to render + Map model = new HashMap(); + + // Get parameter names + String[] paramNames = req.getParameterNames(); + + // handle no parameters given on URL + if ((paramNames == null) || (paramNames.length == 0)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "No parameters have been provided on URL"); + } + + // get URL request parameters, checking if at least one has been provided + + // check if 'inviterUserName' parameter provided + String inviterUserName = req.getParameter(PARAM_INVITER_USER_NAME); + boolean inviterUserNameProvided = (inviterUserName != null) + && (inviterUserName.length() != 0); + + // check if 'inviteeUserName' parameter provided + String inviteeUserName = req.getParameter(PARAM_INVITEE_USER_NAME); + boolean inviteeUserNameProvided = (inviteeUserName != null) + && (inviteeUserName.length() != 0); + + // check if 'siteShortName' parameter provided + String siteShortName = req.getParameter(PARAM_SITE_SHORT_NAME); + boolean siteShortNameProvided = (siteShortName != null) + && (siteShortName.length() != 0); + + // check if 'inviteId' parameter provided + String inviteId = req.getParameter(PARAM_INVITE_ID); + boolean inviteIdProvided = (inviteId != null) + && (inviteId.length() != 0); + + // throw web script exception if at least one of 'inviterUserName', + // 'inviteeUserName', 'siteShortName', + // 'inviteId' URL request parameters has not been provided + if (!(inviterUserNameProvided || inviteeUserNameProvided + || siteShortNameProvided || inviteIdProvided)) + { + throw new WebScriptException( + Status.STATUS_BAD_REQUEST, + "At least one of the following URL request parameters must be provided in URL " + + "'inviterUserName', 'inviteeUserName', 'siteShortName' or 'inviteId'"); + } + + // InviteInfo List to place onto model + List inviteInfoList = new ArrayList(); + + // if 'inviteId' has been provided then set that as the workflow query + // process ID + // - since this is unique don't bother about setting the other workflow + // query properties + if (inviteIdProvided) + { + NominatedInvitation invitation = (NominatedInvitation)invitationService.getInvitation(inviteId); + Map siteInfoCache = new HashMap(2); + inviteInfoList.add(toInviteInfo(siteInfoCache, invitation)); + } + else + // 'inviteId' has not been provided, so create the query properties from + // the invite URL request + // parameters + // - because this web script class will terminate with a web script + // exception if none of the required + // request parameters are provided, at least one of these query + // properties will be set + // at this point + { + InvitationSearchCriteriaImpl criteria = new InvitationSearchCriteriaImpl(); + criteria.setInvitationType(InvitationSearchCriteria.InvitationType.NOMINATED); + criteria.setResourceType(Invitation.ResourceType.WEB_SITE); + + + if (inviterUserNameProvided) + { + criteria.setInviter(inviterUserName); + } + if (inviteeUserNameProvided) + { + criteria.setInvitee(inviteeUserName); + } + if (siteShortNameProvided) + { + criteria.setResourceName(siteShortName); + } + + //MNT-9905 Pending Invites created by one site manager aren't visible to other site managers + String currentUser = AuthenticationUtil.getRunAsUser(); + List invitations; + + if (siteShortNameProvided == true && (SiteModel.SITE_MANAGER).equals(siteService.getMembersRole(siteShortName, currentUser)) && inviterUserNameProvided == false && inviteeUserNameProvided == false) + { + final InvitationSearchCriteriaImpl crit = criteria; + + RunAsWork> runAsSystem = new RunAsWork>() + { + @Override + public List doWork() throws Exception + { + return invitationService.searchInvitation(crit); + } + }; + + invitations = AuthenticationUtil.runAs(runAsSystem, AuthenticationUtil.getSystemUserName()); + } + else + { + invitations = invitationService.searchInvitation(criteria); + } + + // Put InviteInfo objects (containing workflow path properties + // wf:inviterUserName, wf:inviteeUserName, wf:siteShortName, + // and invite id property (from workflow instance id)) + // onto model for each invite workflow task returned by the query + Map siteInfoCache = new HashMap( + invitations.size() * 2); + for (Invitation invitation : invitations) + { + inviteInfoList.add(toInviteInfo(siteInfoCache, (NominatedInvitation) invitation)); + } + } + + // put the list of invite infos onto model to be passed onto template + // for rendering + model.put(MODEL_KEY_NAME_INVITES, inviteInfoList); + + return model; + } + + + + private InviteInfo toInviteInfo(Map siteInfoCache, final NominatedInvitation invitation) + { + // get the site info + String resourceName = invitation.getResourceName(); + SiteInfo siteInfo = siteInfoCache.get(resourceName); + if (siteInfo == null) + { + siteInfo = siteService.getSite(resourceName); + siteInfoCache.put(resourceName, siteInfo); + } + String invitationStatus = InviteInfo.INVITATION_STATUS_PENDING; + + TemplateNode inviterPerson = getPersonIfAllowed(invitation.getInviterUserName()); + + // fetch the person node for the invitee + TemplateNode inviteePerson = getPersonIfAllowed(invitation.getInviteeUserName()); + + InviteInfo ret = new InviteInfo(invitationStatus, + invitation.getInviterUserName(), + inviterPerson, + invitation.getInviteeUserName(), + inviteePerson, + invitation.getRoleName(), + invitation.getResourceName(), + siteInfo, + invitation.getSentInviteDate(), + invitation.getInviteId()); + + return ret; + } + + private TemplateNode getPersonIfAllowed(final String userName) + { + final PersonService personService = serviceRegistry.getPersonService(); + NodeRef inviterRef = AuthenticationUtil.runAs(new RunAsWork() + { + public NodeRef doWork() throws Exception + { + if (!personService.personExists(userName)) + { + return null; + } + return personService.getPerson(userName, false); + } + }, AuthenticationUtil.getSystemUserName()); + if (inviterRef != null + && serviceRegistry.getPermissionService().hasPermission(inviterRef, PermissionService.READ_PROPERTIES) + .equals(AccessStatus.ALLOWED)) + { + return new TemplateNode(inviterRef, serviceRegistry, null); + } + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/AbstractLinksWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/AbstractLinksWebScript.java new file mode 100644 index 00000000000..1af0ea32350 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/AbstractLinksWebScript.java @@ -0,0 +1,433 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.links.LinksService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.util.ScriptPagingDetails; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.json.JSONWriter; + +/** + * @author Nick Burch + * @since 4.0 + */ +public abstract class AbstractLinksWebScript extends DeclarativeWebScript +{ + public static final String LINKS_SERVICE_ACTIVITY_APP_NAME = "links"; + + protected static final String PARAM_MESSAGE = "message"; + protected static final String PARAM_ITEM = "item"; + + private static Log logger = LogFactory.getLog(AbstractLinksWebScript.class); + + // Injected services + protected NodeService nodeService; + protected SiteService siteService; + protected LinksService linksService; + protected PersonService personService; + protected ActivityService activityService; + + private String protocolsWhiteList = "http,https,ftp,mailto"; + private ArrayList allowedProtocols; + private ArrayList xssPatterns; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setLinksService(LinksService linksService) + { + this.linksService = linksService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setProtocolsWhiteList(String protocolsWhiteList) + { + this.protocolsWhiteList = protocolsWhiteList; + } + + public void setXssRegexp(ArrayList xssRegexp) + { + xssPatterns = new ArrayList<>(xssRegexp.size()); + for (String xssRegexpStr : xssRegexp) + { + xssPatterns.add(Pattern.compile(xssRegexpStr)); + } + } + + private boolean isProtocolAllowed(String protocol) + { + // will be used default protocol prefix + if (protocol.length() == 0) + { + return true; + } + + if (allowedProtocols == null) + { + allowedProtocols = new ArrayList(); + for (String delimProtocol : protocolsWhiteList.split(",")) + { + if (delimProtocol.trim().length() == 0) + { + continue; + } + allowedProtocols.add(delimProtocol.trim()); + } + } + + return allowedProtocols.contains(protocol); + } + + private boolean isPossibleXSS(String url) + { + // check for null + if (xssPatterns == null) + { + return false; + } + + boolean result = false; + for (Pattern pattern : xssPatterns) + { + if (pattern.matcher(url).matches()) + { + result = true; + } + } + return result; + } + + private boolean isUrlCorrect(String url) + { + //default behavior if url absent + if (url == null) + { + return true; + } + + if (url.trim().length() == 0 || isPossibleXSS(url)) + { + return false; + } + + int colonPos = url.indexOf(":"); + colonPos = colonPos > 0 ? colonPos : 0; + String protocol = url.substring(0, colonPos); + + boolean result = isProtocolAllowed(protocol); + //check for record host:port e.g.: localhost:8080 + if (!result) + { + String secondUrlPart = url.substring(colonPos+1); + int slashPos = secondUrlPart.indexOf("/"); + slashPos = slashPos > 0 ? slashPos : secondUrlPart.length(); + String port = secondUrlPart.substring(0, slashPos); + + Pattern p = Pattern.compile("^[0-9]*$"); + if (p.matcher(port).matches()) + { + result = true; + } + } + return result; + } + + + protected String getOrNull(JSONObject json, String key) + { + if (json.containsKey(key)) + { + return (String)json.get(key); + } + return null; + } + + protected List getTags(JSONObject json) + { + List tags = null; + if (json.containsKey("tags")) + { + // Is it "tags":"" or "tags":[...] ? + if (json.get("tags") instanceof String) + { + // This is normally an empty string, skip + String tagsS = (String)json.get("tags"); + if ("".equals(tagsS)) + { + // No tags were given + return null; + } + else + { + // Log, and treat as empty + // (We don't support "tags":"a,b,c" in these webscripts) + logger.warn("Unexpected tag data: " + tagsS); + return null; + } + } + else + { + tags = new ArrayList(); + JSONArray jsTags = (JSONArray)json.get("tags"); + for (int i=0; i renderLink(LinkInfo link) + { + Map res = new HashMap(); + res.put("node", link.getNodeRef()); + res.put("name", link.getSystemName()); + res.put("title", link.getTitle()); + res.put("description", link.getDescription()); + res.put("url", link.getURL()); + res.put("createdOn", link.getCreatedAt()); + res.put("modifiedOn", link.getModifiedAt()); + res.put("tags", link.getTags()); + res.put("internal", link.isInternal()); + + // FTL needs a script node of the person, if available + String creator = link.getCreator(); + Object creatorO; + if ((null == creator) || !personService.personExists(creator)) + { + creatorO = ""; + } + else + { + NodeRef person = personService.getPerson(creator); + creatorO = person; + } + res.put("creator", creatorO); + + // We want blank instead of null + for (String key : res.keySet()) + { + if (res.get(key) == null) + { + res.put(key, ""); + } + } + + return res; + } + + @Override + protected Map executeImpl(WebScriptRequest req, + Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if (templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Parse the JSON, if supplied + JSONObject json = null; + String contentType = req.getContentType(); + if (contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if (MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + JSONParser parser = new JSONParser(); + try + { + json = (JSONObject)parser.parse(req.getContent().getContent()); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch(ParseException pe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage()); + } + } + + + // Get the site short name. Try quite hard to do so... + String siteName = templateVars.get("site"); + if (siteName == null) + { + siteName = req.getParameter("site"); + } + if (siteName == null && json != null) + { + if (json.containsKey("siteid")) + { + siteName = (String)json.get("siteid"); + } + else if (json.containsKey("site")) + { + siteName = (String)json.get("site"); + } + } + if (siteName == null) + { + String error = "No site given"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Grab the requested site + SiteInfo site = siteService.getSite(siteName); + if (site == null) + { + String error = "Could not find site: " + siteName; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Link name is optional + String linkName = templateVars.get("path"); + + //sanitise url + if (json != null) + { + String url = getOrNull(json, "url"); + if (!isUrlCorrect(url)) + { + String error = "Url not allowed"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + } + + // Have the real work done + return executeImpl(site, linkName, req, json, status, cache); + } + + protected abstract Map executeImpl(SiteInfo site, + String linkName, WebScriptRequest req, JSONObject json, + Status status, Cache cache); + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkDelete.java new file mode 100644 index 00000000000..de2cdd3e3fa --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkDelete.java @@ -0,0 +1,77 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the link deleting link.delete webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class LinkDelete extends AbstractLinksWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, String linkName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + Map model = new HashMap(); + + // Try to find the link + LinkInfo link = linksService.getLink(site.getShortName(), linkName); + if (link == null) + { + String message = "No link found with that name"; + throw new WebScriptException(Status.STATUS_NOT_FOUND, message); + } + + // Delete it + try + { + linksService.deleteLink(link); + } + catch (AccessDeniedException e) + { + String message = "You don't have permission to delete that link"; + throw new WebScriptException(Status.STATUS_FORBIDDEN, message); + } + + // Mark it as gone + status.setCode(Status.STATUS_NO_CONTENT); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkGet.java new file mode 100644 index 00000000000..d8174343098 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkGet.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the link fetching link.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class LinkGet extends AbstractLinksWebScript +{ + @Override + protected Map executeImpl(SiteInfo site, String linkName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + Map model = new HashMap(); + + // Try to find the link + LinkInfo link = linksService.getLink(site.getShortName(), linkName); + if (link == null) + { + String message = "No link found with that name"; + throw new WebScriptException(Status.STATUS_NOT_FOUND, message); + } + + // Build the model + model.put(PARAM_ITEM, renderLink(link)); + model.put("node", link.getNodeRef()); + model.put("link", link); + model.put("site", site); + model.put("siteId", site.getShortName()); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkPut.java new file mode 100644 index 00000000000..bc125ad086f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinkPut.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.regex.Pattern; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the link fetching links.put webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class LinkPut extends AbstractLinksWebScript +{ + private static final String MSG_ACCESS_DENIED= "links.err.access.denied"; + private static final String MSG_NOT_FOUND= "links.err.not.found"; + + @Override + protected Map executeImpl(SiteInfo site, String linkName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + Map model = new HashMap(); + + // Try to find the link + LinkInfo link = linksService.getLink(site.getShortName(), linkName); + if (link == null) + { + String message = "No link found with that name"; + + status.setCode(Status.STATUS_NOT_FOUND); + status.setMessage(message); + model.put(PARAM_MESSAGE, rb.getString(MSG_NOT_FOUND)); + return model; + } + + // Get the new link details from the JSON + // Update the main properties + link.setTitle(getOrNull(json, "title")); + link.setDescription(getOrNull(json, "description")); + String url = getOrNull(json, "url"); + + link.setURL(url); + + // Handle internal / not internal + if (json.containsKey("internal")) + { + link.setInternal(true); + } + else + { + link.setInternal(false); + } + + // Do the tags + link.getTags().clear(); + List tags = getTags(json); + if (tags != null && tags.size() > 0) + { + link.getTags().addAll(tags); + } + + + // Update the link + try + { + link = linksService.updateLink(link); + } + catch (AccessDeniedException e) + { + String message = "You don't have permission to update that link"; + + status.setCode(Status.STATUS_FORBIDDEN); + status.setMessage(message); + model.put(PARAM_MESSAGE, rb.getString(MSG_ACCESS_DENIED)); + return model; + } + + // Generate an activity for the change + addActivityEntry("updated", link, site, req, json); + + + // Build the model + model.put(PARAM_MESSAGE, "Node " + link.getNodeRef() + " updated"); + model.put("link", link); + model.put("site", site); + model.put("siteId", site.getShortName()); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinksDeletePost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinksDeletePost.java new file mode 100644 index 00000000000..58f66dbfd77 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinksDeletePost.java @@ -0,0 +1,130 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the links deleting links-delete.post webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class LinksDeletePost extends AbstractLinksWebScript +{ + private static final String MSG_NAME_NOT_FOUND= "links-delete.err.not.found"; + private static final String MSG_ACCESS_DENIED= "links-delete.access.denied"; + private static final String MSG_DELETED= "links-delete.msg.deleted"; + + protected static final int RECENT_SEARCH_PERIOD_DAYS = 7; + protected static final long ONE_DAY_MS = 24*60*60*1000; + + @Override + protected Map executeImpl(SiteInfo site, String linkName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + Map model = new HashMap(); + + // Get the requested nodes from the JSON + // Silently skips over any invalid ones specified + List links = new ArrayList(); + if (json.containsKey("items")) + { + JSONArray items = (JSONArray)json.get("items"); + for (int i=0; i. + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.links.LinksServiceImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the links listing links.get webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class LinksListGet extends AbstractLinksWebScript +{ + protected static final int RECENT_SEARCH_PERIOD_DAYS = 7; + protected static final long ONE_DAY_MS = 24*60*60*1000; + + @Override + protected Map executeImpl(SiteInfo site, String linkName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + // Decide on what kind of request they wanted + String filter = req.getParameter("filter"); + + // Tagging? + boolean tagFiltering = true; + String tag = req.getParameter("tag"); + if (tag == null || tag.length() == 0) + { + tagFiltering = false; + } + + // User? + boolean userFiltering = false; + String user = null; + if ("user".equals(filter)) + { + userFiltering = true; + user = AuthenticationUtil.getFullyAuthenticatedUser(); + } + + // Date? + boolean dateFiltering = false; + Date from = null; + Date to = null; + if ("recent".equals(filter)) + { + dateFiltering = true; + Date now = new Date(); + from = new Date(now.getTime() - RECENT_SEARCH_PERIOD_DAYS*ONE_DAY_MS); + to = new Date(now.getTime() + ONE_DAY_MS); + } + + + // Get the links for the list + PagingRequest paging = buildPagingRequest(req); + paging.setRequestTotalCountMax(paging.getSkipCount() + paging.getRequestTotalCountMax()); + PagingResults links; + if (tagFiltering) + { + links = linksService.findLinks(site.getShortName(), user, from, to, tag, paging); + } + else + { + if (userFiltering) + { + links = linksService.listLinks(site.getShortName(), user, paging); + } + else if (dateFiltering) + { + links = linksService.listLinks(site.getShortName(), from, to, paging); + } + else + { + links = linksService.listLinks(site.getShortName(), paging); + } + } + + + // For each one in our page, grab details of any ignored instances + List> items = new ArrayList>(); + for (LinkInfo link : links.getPage()) + { + Map result = renderLink(link); + items.add(result); + } + Map data = new HashMap(); + data.put("items", items); + data.put("pageSize", paging.getMaxItems()); + data.put("startIndex", paging.getSkipCount()); + data.put("itemCount", items.size()); + + int total = items.size(); + if (links.getTotalResultCount() != null && links.getTotalResultCount().getFirst() != null) + { + total = links.getTotalResultCount().getFirst(); + } + data.put("total", total); + + if (total == paging.getRequestTotalCountMax()) + { + data.put("totalRecordsUpper", true); + } + else + { + data.put("totalRecordsUpper", false); + } + + // We need the container node for permissions checking + NodeRef container; + if (links.getPage().size() > 0) + { + container = links.getPage().get(0).getContainerNodeRef(); + } + else + { + // Find the container (if it's been created yet) + container = siteService.getContainer( + site.getShortName(), LinksServiceImpl.LINKS_COMPONENT); + + if (container == null) + { + // Brand new site, no write operations on links have happened + // Fudge it for now with the site itself, the first write call + // will have the container created + container = site.getNodeRef(); + } + } + + // All done + Map model = new HashMap(); + model.put("data", data); + model.put("links", container); + model.put("siteId", site.getShortName()); + model.put("site", site); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinksPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinksPost.java new file mode 100644 index 00000000000..381470a6850 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/links/LinksPost.java @@ -0,0 +1,115 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.links; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.links.LinkInfo; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the link fetching links.post webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class LinksPost extends AbstractLinksWebScript +{ + private static final String MSG_ACCESS_DENIED= "links.err.access.denied"; + + @Override + protected Map executeImpl(SiteInfo site, String linkName, + WebScriptRequest req, JSONObject json, Status status, Cache cache) + { + final ResourceBundle rb = getResources(); + Map model = new HashMap(); + + // Get the new link details from the JSON + String title; + String description; + String url; + boolean internal; + List tags; + + // Fetch the main properties + title = getOrNull(json, "title"); + description = getOrNull(json, "description"); + url = getOrNull(json, "url"); + + // Handle internal / not internal + internal = json.containsKey("internal"); + + // Do the tags + tags = getTags(json); + + + // Create the link + LinkInfo link; + try + { + link = linksService.createLink(site.getShortName(), title, description, url, internal); + } + catch (AccessDeniedException e) + { + String message = "You don't have permission to create a link"; + + status.setCode(Status.STATUS_FORBIDDEN); + status.setMessage(message); + model.put(PARAM_MESSAGE, rb.getString(MSG_ACCESS_DENIED)); + return model; + } + + // Set the tags if required + if (tags != null && tags.size() > 0) + { + link.getTags().addAll(tags); + linksService.updateLink(link); + } + + // Generate an activity for the change + addActivityEntry("created", link, site, req, json); + + + // Build the model + model.put(PARAM_MESSAGE, link.getSystemName()); // Really! + model.put(PARAM_ITEM, renderLink(link)); + model.put("node", link.getNodeRef()); + model.put("link", link); + model.put("site", site); + model.put("siteId", site.getShortName()); + + // All done + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/metadata/BulkMetadataGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/metadata/BulkMetadataGet.java new file mode 100644 index 00000000000..a5d1667e39a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/metadata/BulkMetadataGet.java @@ -0,0 +1,191 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.metadata; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.json.JSONWriter; + +/** + * This class is the webscript implementation for the url api/bulkmetadata + * It returns basic metadata such as title, parent noderef, name, ... for each noderef that is passed in + * + * @since 3.4 + */ +public class BulkMetadataGet extends AbstractWebScript +{ + private ServiceRegistry services; + private NodeService nodeService; + private DictionaryService dictionaryService; + private PermissionService permissionService; + + private String getMimeType(ContentData contentProperty) + { + String mimetype = null; + + if(contentProperty != null) + { + mimetype = contentProperty.getMimetype(); + } + + return mimetype; + } + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + JSONObject jsonIn; + JSONArray nodeRefsArray; + + try + { + Content c = req.getContent(); + if (c == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Missing POST body."); + } + + jsonIn = new JSONObject(c.getContent()); + + nodeRefsArray = jsonIn.getJSONArray("nodeRefs"); + if(nodeRefsArray == null || nodeRefsArray.length() == 0) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Must provide node refs"); + } + + JSONWriter jsonOut = new JSONWriter(res.getWriter()); + + res.setContentType("application/json"); + res.setContentEncoding(Charset.defaultCharset().displayName()); // TODO: Should be settable on JSONWriter + + jsonOut.startObject(); + { + jsonOut.startValue("nodes"); + { + jsonOut.startArray(); + { + for(int i = 0; i < nodeRefsArray.length(); i++) + { + NodeRef nodeRef = new NodeRef(nodeRefsArray.getString(i)); + + if(nodeService.exists(nodeRef)) + { + NodeRef parentNodeRef = null; + ChildAssociationRef childAssocRef = nodeService.getPrimaryParent(nodeRef); + if(childAssocRef != null) + { + parentNodeRef = childAssocRef.getParentRef(); + } + QName type = nodeService.getType(nodeRef); + + String shortType = type.toPrefixString(services.getNamespaceService()); + Map properties = nodeService.getProperties(nodeRef); + + jsonOut.startObject(); + { + jsonOut.writeValue("nodeRef", nodeRef.toString()); + jsonOut.writeValue("parentNodeRef", parentNodeRef.toString()); + jsonOut.writeValue("type", type.toString()); + jsonOut.writeValue("shortType", shortType); + TypeDefinition typeDef = dictionaryService.getType(type); + jsonOut.writeValue("typeTitle", typeDef.getTitle(dictionaryService)); + + jsonOut.writeValue("name", (String)properties.get(ContentModel.PROP_NAME)); + jsonOut.writeValue("title", (String)properties.get(ContentModel.PROP_TITLE)); + jsonOut.writeValue("mimeType", getMimeType((ContentData)properties.get(ContentModel.PROP_CONTENT))); + jsonOut.writeValue("hasWritePermission", permissionService.hasPermission(nodeRef, PermissionService.WRITE) == AccessStatus.ALLOWED); + jsonOut.writeValue("hasDeletePermission", permissionService.hasPermission(nodeRef, PermissionService.DELETE) == AccessStatus.ALLOWED); + } + jsonOut.endObject(); + } + else + { + jsonOut.startObject(); + { + jsonOut.writeValue("nodeRef", nodeRef.toString()); + jsonOut.writeValue("error", "true"); + jsonOut.writeValue("errorCode", "invalidNodeRef"); + jsonOut.writeValue("errorText", I18NUtil.getMessage("msg.invalidNodeRef", nodeRef.toString())); + } + jsonOut.endObject(); + } + } + } + jsonOut.endArray(); + } + jsonOut.endValue(); + } + jsonOut.endObject(); + } + catch (JSONException jErr) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Unable to parse JSON POST body: " + jErr.getMessage()); + } + + res.getWriter().close(); + + res.setStatus(Status.STATUS_OK); + } + + /** + * Set the service registry + * + * @param services the service registry + */ + public void setServiceRegistry(ServiceRegistry services) + { + this.services = services; + this.nodeService = services.getNodeService(); + this.dictionaryService = services.getDictionaryService(); + this.permissionService = services.getPermissionService(); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/model/filefolder/FileFolderLoaderPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/model/filefolder/FileFolderLoaderPost.java new file mode 100644 index 00000000000..188353b4c2a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/model/filefolder/FileFolderLoaderPost.java @@ -0,0 +1,171 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.model.filefolder; + +import java.io.IOException; +import java.io.OutputStream; + +import org.alfresco.repo.model.filefolder.FileFolderLoader; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Link to {@link FileFolderLoader} + */ +public class FileFolderLoaderPost extends AbstractWebScript implements ApplicationContextAware +{ + public static final String KEY_FOLDER_PATH = "folderPath"; + public static final String KEY_FILE_COUNT = "fileCount"; + public static final String KEY_FILES_PER_TXN = "filesPerTxn"; + public static final String KEY_MIN_FILE_SIZE = "minFileSize"; + public static final String KEY_MAX_FILE_SIZE = "maxFileSize"; + public static final String KEY_MAX_UNIQUE_DOCUMENTS = "maxUniqueDocuments"; + public static final String KEY_FORCE_BINARY_STORAGE = "forceBinaryStorage"; + public static final String KEY_DESCRIPTION_COUNT = "descriptionCount"; + public static final String KEY_DESCRIPTION_SIZE = "descriptionSize"; + public static final String KEY_COUNT = "count"; + + public static final int DEFAULT_FILE_COUNT = 100; + public static final int DEFAULT_FILES_PER_TXN = 100; + public static final long DEFAULT_MIN_FILE_SIZE = 80*1024L; + public static final long DEFAULT_MAX_FILE_SIZE = 120*1024L; + public static final long DEFAULT_MAX_UNIQUE_DOCUMENTS = Long.MAX_VALUE; + public static final int DEFAULT_DESCRIPTION_COUNT = 1; + public static final long DEFAULT_DESCRIPTION_SIZE = 128L; + public static final boolean DEFAULT_FORCE_BINARY_STORAGE = false; + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + FileFolderLoader loader = (FileFolderLoader) applicationContext.getBean("fileFolderLoader"); + + int count = 0; + String folderPath = ""; + try + { + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + folderPath = json.getString(KEY_FOLDER_PATH); + if (folderPath == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, KEY_FOLDER_PATH + " not supplied."); + } + int fileCount = 100; + if (json.has(KEY_FILE_COUNT)) + { + fileCount = json.getInt(KEY_FILE_COUNT); + } + int filesPerTxn = DEFAULT_FILES_PER_TXN; + if (json.has(KEY_FILES_PER_TXN)) + { + filesPerTxn = json.getInt(KEY_FILES_PER_TXN); + } + long minFileSize = DEFAULT_MIN_FILE_SIZE; + if (json.has(KEY_MIN_FILE_SIZE)) + { + minFileSize = json.getInt(KEY_MIN_FILE_SIZE); + } + long maxFileSize = DEFAULT_MAX_FILE_SIZE; + if (json.has(KEY_MAX_FILE_SIZE)) + { + maxFileSize = json.getInt(KEY_MAX_FILE_SIZE); + } + long maxUniqueDocuments = DEFAULT_MAX_UNIQUE_DOCUMENTS; + if (json.has(KEY_MAX_UNIQUE_DOCUMENTS)) + { + maxUniqueDocuments = json.getInt(KEY_MAX_UNIQUE_DOCUMENTS); + } + boolean forceBinaryStorage = DEFAULT_FORCE_BINARY_STORAGE; + if (json.has(KEY_FORCE_BINARY_STORAGE)) + { + forceBinaryStorage = json.getBoolean(KEY_FORCE_BINARY_STORAGE); + } + int descriptionCount = DEFAULT_DESCRIPTION_COUNT; + if (json.has(KEY_DESCRIPTION_COUNT)) + { + descriptionCount = json.getInt(KEY_DESCRIPTION_COUNT); + } + long descriptionSize = DEFAULT_DESCRIPTION_SIZE; + if (json.has(KEY_DESCRIPTION_SIZE)) + { + descriptionSize = json.getLong(KEY_DESCRIPTION_SIZE); + } + + // Perform the load + count = loader.createFiles( + folderPath, + fileCount, filesPerTxn, + minFileSize, maxFileSize, + maxUniqueDocuments, + forceBinaryStorage, + descriptionCount, descriptionSize); + } + catch (FileNotFoundException e) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Folder not found: ", folderPath); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + // Write the response + OutputStream os = res.getOutputStream(); + try + { + JSONObject json = new JSONObject(); + json.put(KEY_COUNT, count); + os.write(json.toString().getBytes("UTF-8")); + } + catch (JSONException e) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Failed to write JSON", e); + } + finally + { + os.close(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/node/NodeFolderPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/node/NodeFolderPost.java new file mode 100644 index 00000000000..fe8a3862b78 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/node/NodeFolderPost.java @@ -0,0 +1,223 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.node; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the webscript for creating child folders, of either a Node or a Site Container. + * + * @since 4.1 + */ +public class NodeFolderPost extends DeclarativeWebScript +{ + private NodeService nodeService; + private SiteService siteService; + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // Identify the Node they want to create a child of + SiteInfo site = null; + String container = null; + NodeRef parentNodeRef = null; + + Map templateArgs = req.getServiceMatch().getTemplateVars(); + if (templateArgs.get("site") != null && templateArgs.get("container") != null) + { + // Site based request + site = siteService.getSite(templateArgs.get("site")); + if (site == null) + { + status.setCode(Status.STATUS_NOT_FOUND); + status.setRedirect(true); + return null; + } + + // Check the container exists + container = templateArgs.get("container"); + NodeRef containerNodeRef = siteService.getContainer(site.getShortName(), container); + if (containerNodeRef == null) + { + status.setCode(Status.STATUS_NOT_FOUND); + status.setRedirect(true); + return null; + } + + // Work out where to put it + if (templateArgs.get("path") != null) + { + // Nibble our way along the / delimited path, starting from the container + parentNodeRef = containerNodeRef; + StringTokenizer st = new StringTokenizer(templateArgs.get("path"), "/"); + while (st.hasMoreTokens()) + { + String childName = st.nextToken(); + parentNodeRef = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, childName); + if (parentNodeRef == null) + { + status.setCode(Status.STATUS_NOT_FOUND); + status.setRedirect(true); + return null; + } + } + } + else + { + // Direct child of the container + parentNodeRef = containerNodeRef; + } + } + else if (templateArgs.get("store_type") != null && templateArgs.get("store_id") != null + && templateArgs.get("id") != null) + { + // NodeRef based creation + parentNodeRef = new NodeRef(templateArgs.get("store_type"), + templateArgs.get("store_id"), templateArgs.get("id")); + if (! nodeService.exists(parentNodeRef)) + { + status.setCode(Status.STATUS_NOT_FOUND); + status.setRedirect(true); + return null; + } + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "No parent details found"); + } + + + // Process the JSON post details + JSONObject json = null; + JSONParser parser = new JSONParser(); + try + { + json = (JSONObject)parser.parse(req.getContent().getContent()); + } + catch (IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch (ParseException pe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage()); + } + + + // Fetch the name, title and description + String name = (String)json.get("name"); + if (name == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Name is required"); + } + + String title = (String)json.get("title"); + if (title == null) + { + title = name; + } + String description = (String)json.get("description"); + + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, name); + props.put(ContentModel.PROP_TITLE, title); + props.put(ContentModel.PROP_DESCRIPTION, description); + + + // Verify the type is allowed + QName type = ContentModel.TYPE_FOLDER; + if (json.get("type") != null) + { + type = QName.createQName( (String)json.get("type"), namespaceService ); + if (! dictionaryService.isSubClass(type, ContentModel.TYPE_FOLDER)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Specified type is not a folder"); + } + } + + + // Have the node created + NodeRef nodeRef = null; + try + { + nodeRef = nodeService.createNode(parentNodeRef, ContentModel.ASSOC_CONTAINS, + QName.createQName(name), type, props).getChildRef(); + } + catch (AccessDeniedException e) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "You don't have permission to create the node"); + } + + // Report the details + Map model = new HashMap(); + model.put("nodeRef", nodeRef); + model.put("site", site); + model.put("container", container); + model.put("parentNodeRef", parentNodeRef); + return model; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/nodelocator/NodeLocatorGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/nodelocator/NodeLocatorGet.java new file mode 100644 index 00000000000..b21e967918b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/nodelocator/NodeLocatorGet.java @@ -0,0 +1,112 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.nodelocator; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.nodelocator.NodeLocatorService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Smith + * @since 4.0 + */ +public class NodeLocatorGet extends DeclarativeWebScript +{ + private static final String NODE_ID = "node_id"; + private static final String STORE_ID = "store_id"; + private static final String STORE_TYPE = "store_type"; + private static final String NODE_LOCATOR_NAME = "node_locator_name"; + private NodeLocatorService locatorService; + + /** + * {@inheritDoc} + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Map vars = req.getServiceMatch().getTemplateVars(); + // getting task id from request parameters + String locatorName = vars.get(NODE_LOCATOR_NAME); + + // No locatorname specified -> return 404 + if (locatorName == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "No NodeLocator strategy was specified!"); + } + + NodeRef source = null; + String storeType = vars.get(STORE_TYPE); + String storeId= vars.get(STORE_ID); + String nodeId= vars.get(NODE_ID); + if (storeType != null && storeId != null && nodeId != null) + { + source = new NodeRef(storeType, storeId, nodeId); + } + + Map params = mapParams(req); + + NodeRef node = locatorService.getNode(locatorName, source, params); + + Map model = new HashMap(); + model.put("nodeRef", node==null ? null : node.toString()); + return model; + } + + private Map mapParams(WebScriptRequest req) + { + Map params = new HashMap(); + for (String key: req.getParameterNames()) + { + String value = req.getParameter(key); + if (value != null) + { + String decodedValue = URLDecoder.decode(value); + // TODO Handle type conversions here. + params.put(key, decodedValue); + } + } + return params; + } + + /** + * @param locatorService the locatorService to set + */ + public void setNodeLocatorService(NodeLocatorService locatorService) + { + this.locatorService = locatorService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/ChangePasswordPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/ChangePasswordPost.java new file mode 100644 index 00000000000..870d1d745de --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/ChangePasswordPost.java @@ -0,0 +1,145 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.person; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript implementation for the POST method for 'changepassword' API. + * + * @author Kevin Roast + */ +public class ChangePasswordPost extends DeclarativeWebScript +{ + private static final String PARAM_NEWPW = "newpw"; + private static final String PARAM_OLDPW = "oldpw"; + private MutableAuthenticationService authenticationService; + private AuthorityService authorityService; + + + /** + * @param authenticationService the AuthenticationService to set + */ + public void setAuthenticationService(MutableAuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * @param authorityService the AuthorityService to set + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + // Extract user name from the URL - cannot be null or webscript desc would not match + String userName = req.getExtensionPath(); + + // Extract old and new password details from JSON POST + Content c = req.getContent(); + if (c == null) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Missing POST body."); + } + JSONObject json; + try + { + json = new JSONObject(c.getContent()); + + String oldPassword = null; + String newPassword; + + // admin users can change/set a password without knowing the old one + boolean isAdmin = authorityService.hasAdminAuthority(); + if (!isAdmin || (userName.equalsIgnoreCase(authenticationService.getCurrentUserName()))) + { + if (!json.has(PARAM_OLDPW) || json.getString(PARAM_OLDPW).length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Old password 'oldpw' is a required POST parameter."); + } + oldPassword = json.getString(PARAM_OLDPW); + } + if (!json.has(PARAM_NEWPW) || json.getString(PARAM_NEWPW).length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "New password 'newpw' is a required POST parameter."); + } + newPassword = json.getString(PARAM_NEWPW); + + // update the password + // an Admin user can update without knowing the original pass - but must know their own! + if (!isAdmin || (userName.equalsIgnoreCase(authenticationService.getCurrentUserName()))) + { + authenticationService.updateAuthentication(userName, oldPassword.toCharArray(), newPassword.toCharArray()); + } + else + { + authenticationService.setAuthentication(userName, newPassword.toCharArray()); + } + } + catch (AuthenticationException err) + { + throw new WebScriptException(Status.STATUS_UNAUTHORIZED, + "Do not have appropriate auth or wrong auth details provided."); + } + catch (JSONException jErr) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, + "Unable to parse JSON POST body: " + jErr.getMessage()); + } + catch (IOException ioErr) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, + "Unable to retrieve POST body: " + ioErr.getMessage()); + } + + Map model = new HashMap(1, 1.0f); + model.put("success", Boolean.TRUE); + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/UserCSVUploadGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/UserCSVUploadGet.java new file mode 100644 index 00000000000..66439eda593 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/UserCSVUploadGet.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.person; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.repo.web.scripts.DeclarativeSpreadsheetWebScript; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.csv.CSVPrinter; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * Webscript implementation for giving information on uploading + * users via a CSV. + * + * @author Nick Burch + */ +public class UserCSVUploadGet extends DeclarativeSpreadsheetWebScript +{ + public UserCSVUploadGet() + { + this.filenameBase = "ExampleUserUpload"; + } + + /** + * We have a HTML version + */ + @Override + protected boolean allowHtmlFallback() { + return true; + } + + /** + * We don't have a resource + */ + @Override + protected Object identifyResource(String format, WebScriptRequest req) { + return null; + } + + @Override + protected List> buildPropertiesForHeader( + Object resource, String format, WebScriptRequest req) { + List> properties = + new ArrayList>(UserCSVUploadPost.COLUMNS.length); + boolean required = true; + for(QName qname : UserCSVUploadPost.COLUMNS) + { + Pair p = null; + if(qname != null) + { + p = new Pair(qname, required); + } + else + { + required = false; + } + properties.add(p); + } + return properties; + } + + @Override + protected void populateBody(Object resource, CSVPrinter csv, List properties) throws IOException { + // Just a blank line is needed + csv.println(); + } + + @Override + protected void populateBody(Object resource, Workbook workbook, Sheet sheet, + List properties) throws IOException { + // Set the sheet name + workbook.setSheetName(0, "UsersToAdd"); + + // Add a blank line + sheet.createRow(1); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/UserCSVUploadPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/UserCSVUploadPost.java new file mode 100644 index 00000000000..91ebb705808 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/person/UserCSVUploadPost.java @@ -0,0 +1,604 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.person; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.person.PersonServiceImpl; +import org.alfresco.repo.tenant.TenantDomainMismatchException; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.util.PaneInformation; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * Webscript implementation for the POST method for uploading a + * CSV of users to have them created. + * + * @author Nick Burch + * @since 3.5 + */ +public class UserCSVUploadPost extends DeclarativeWebScript +{ + protected static final QName[] COLUMNS = new QName[] { + ContentModel.PROP_USERNAME, ContentModel.PROP_FIRSTNAME, + ContentModel.PROP_LASTNAME, ContentModel.PROP_EMAIL, + null, + ContentModel.PROP_PASSWORD, ContentModel.PROP_ORGANIZATION, + ContentModel.PROP_JOBTITLE, ContentModel.PROP_LOCATION, + ContentModel.PROP_TELEPHONE, ContentModel.PROP_MOBILE, + ContentModel.PROP_SKYPE, ContentModel.PROP_INSTANTMSG, + ContentModel.PROP_GOOGLEUSERNAME, + ContentModel.PROP_COMPANYADDRESS1, ContentModel.PROP_COMPANYADDRESS2, + ContentModel.PROP_COMPANYADDRESS3, ContentModel.PROP_COMPANYPOSTCODE, + ContentModel.PROP_COMPANYTELEPHONE, ContentModel.PROP_COMPANYFAX, + ContentModel.PROP_COMPANYEMAIL + }; + + private static final String ERROR_BAD_FORM = "person.err.userCSV.invalidForm"; + private static final String ERROR_NO_FILE = "person.err.userCSV.noFile"; + private static final String ERROR_CORRUPT_FILE = "person.err.userCSV.corruptFile"; + private static final String ERROR_GENERAL = "person.err.userCSV.general"; + private static final String ERROR_BLANK_COLUMN = "person.err.userCSV.blankColumn"; + private static final String MSG_CREATED = "person.msg.userCSV.created"; + private static final String MSG_EXISTING = "person.msg.userCSV.existing"; + + private static Log logger = LogFactory.getLog(UserCSVUploadPost.class); + + private MutableAuthenticationService authenticationService; + private AuthorityService authorityService; + private PersonService personService; + private TenantService tenantService; + private DictionaryService dictionaryService; + private RetryingTransactionHelper retryingTransactionHelper; + + /** + * @param authenticationService the AuthenticationService to set + */ + public void setAuthenticationService(MutableAuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * @param authorityService the AuthorityService to set + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /** + * @param personService the PersonService to set + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * @param tenantService the TenantService to set + */ + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + /** + * @param dictionaryService the DictionaryService to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param retryingTransactionHelper the helper for running transactions to set + */ + public void setTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + + + /** + * @see DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + final List> users = new ArrayList>(); + final ResourceBundle rb = getResources(); + + + // Try to load the user details from the upload + FormData form = (FormData)req.parseContent(); + if (form == null || !form.getIsMultiPart()) + { + throw new ResourceBundleWebScriptException(Status.STATUS_BAD_REQUEST, rb, ERROR_BAD_FORM); + } + + boolean processed = false; + for (FormData.FormField field : form.getFields()) + { + if (field.getIsFile()) + { + processUpload( + field.getInputStream(), + field.getFilename(), + users); + processed = true; + break; + } + } + + if (!processed) + { + throw new ResourceBundleWebScriptException(Status.STATUS_BAD_REQUEST, rb, ERROR_NO_FILE); + } + + // Should we send emails? + boolean sendEmails = true; + if (req.getParameter("email") != null) + { + sendEmails = Boolean.parseBoolean(req.getParameter("email")); + } + + if (form.hasField("email")) + { + sendEmails = Boolean.parseBoolean(form.getParameters().get("email")[0]); + } + + // Now process the users + final MutableInt totalUsers = new MutableInt(0); + final MutableInt addedUsers = new MutableInt(0); + final Map results = new HashMap(); + final boolean doSendEmails = sendEmails; + + // Do the work in a new transaction, so that if we hit a problem + // during the commit stage (eg too many users) then we get to + // hear about it, and handle it ourselves. + // Otherwise, commit exceptions occur deep inside RepositoryContainer + // and we can't control the status code + RetryingTransactionCallback work = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + try + { + doAddUsers(totalUsers, addedUsers, results, users, rb, doSendEmails); + return null; + } + catch (Throwable t) + { + // Make sure we rollback from this + UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction(); + if (userTrx != null && userTrx.getStatus() != javax.transaction.Status.STATUS_MARKED_ROLLBACK) + { + try + { + userTrx.setRollbackOnly(); + } + catch (Throwable t2) {} + } + // Report the problem further down + throw t; + } + } + }; + + try + { + retryingTransactionHelper.doInTransaction(work); + } + catch (Throwable t) + { + // Tell the client of the problem + if (t instanceof WebScriptException) + { + // We've already wrapped it properly, all good + throw (WebScriptException)t; + } + else + { + // Something unexpected has ripped up + // Return the details with a 200, so that Share does the right thing + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, rb, ERROR_GENERAL, t); + } + } + + + // If we get here, then adding the users didn't throw any exceptions, + // so tell the client which users went in and which didn't + Map model = new HashMap(); + model.put("totalUsers", totalUsers); + model.put("addedUsers", addedUsers); + model.put("users", results); + + return model; + } + + private void doAddUsers(final MutableInt totalUsers, final MutableInt addedUsers, + final Map results, final List> users, + final ResourceBundle rb, final boolean sendEmails) + { + for (Map user : users) + { + totalUsers.setValue( totalUsers.intValue()+1 ); + + // Grab the username, and do any MT magic on it + String username = user.get(ContentModel.PROP_USERNAME); + try + { + username = PersonServiceImpl.updateUsernameForTenancy(username, tenantService); + } + catch (TenantDomainMismatchException e) + { + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, rb, + ERROR_GENERAL, e); + } + + // Do they already exist? + if (personService.personExists(username)) + { + results.put(username, rb.getString(MSG_EXISTING)); + if (logger.isDebugEnabled()) + { + logger.debug("Not creating user as already exists: " + username + " for " + user); + } + } + else + { + String password = user.get(ContentModel.PROP_PASSWORD); + user.remove(ContentModel.PROP_PASSWORD); // Not for the person service + + try + { + // Add the person + personService.createPerson( (Map)(Map)user ); + + // Add the underlying user + authenticationService.createAuthentication(username, password.toCharArray()); + + // If required, notify the user + if (sendEmails) + { + personService.notifyPerson(username, password); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Creating user from upload: " + username + " for " + user); + } + + // All done + String msg = MessageFormat.format( + rb.getString(MSG_CREATED), + new Object[] { user.get(ContentModel.PROP_EMAIL) }); + results.put(username, msg); + addedUsers.setValue( addedUsers.intValue()+1 ); + } + catch (Throwable t) + { + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, rb, ERROR_GENERAL, t); + } + } + } + } + + protected void processUpload(InputStream input, String filename, List> users) + { + try + { + if (filename != null && filename.length() > 0) + { + if (filename.endsWith(".csv")) + { + processCSVUpload(input, users); + return; + } + if (filename.endsWith(".xls")) + { + processXLSUpload(input, users); + return; + } + if (filename.endsWith(".xlsx")) + { + processXLSXUpload(input, users); + return; + } + } + + // If in doubt, assume it's probably a .csv + processCSVUpload(input, users); + } + catch (IOException e) + { + // Return the error as a 200 so the user gets a friendly + // display of the error message in share + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, getResources(), + ERROR_CORRUPT_FILE, e); + } + } + + protected void processCSVUpload(InputStream input, List> users) + throws IOException + { + InputStreamReader reader = new InputStreamReader(input, Charset.forName("UTF-8")); + CSVFormat format = CSVFormat.EXCEL; + CSVParser csv = format.parse(reader); + + String[][] data = csv.getRecords().stream() + .map(record -> { + List recordValues = new ArrayList<>(); + record.iterator().forEachRemaining(recordValues::add); + return recordValues.toArray(String[]::new); + }).toArray(String[][]::new); + + if (data.length > 0) + { + processSpreadsheetUpload(data, users); + } + } + + protected void processXLSUpload(InputStream input, List> users) + throws IOException + { + Workbook wb = new HSSFWorkbook(input); + processSpreadsheetUpload(wb, users); + } + + protected void processXLSXUpload(InputStream input, List> users) + throws IOException + { + Workbook wb = new XSSFWorkbook(input); + processSpreadsheetUpload(wb, users); + } + + private void processSpreadsheetUpload(Workbook wb, List> users) + throws IOException + { + if (wb.getNumberOfSheets() > 1) + { + logger.info("Uploaded Excel file has " + wb.getNumberOfSheets() + + " sheets, ignoring all except the first one"); + } + + int firstRow = 0; + Sheet s = wb.getSheetAt(0); + DataFormatter df = new DataFormatter(); + + String[][] data = new String[s.getLastRowNum()+1][]; + + // If there is a heading freezepane row, skip it + PaneInformation pane = s.getPaneInformation(); + if (pane != null && pane.isFreezePane() && pane.getHorizontalSplitTopRow() > 0) + { + firstRow = pane.getHorizontalSplitTopRow(); + logger.debug("Skipping excel freeze header of " + firstRow + " rows"); + } + + // Process each row in turn, getting columns up to our limit + for (int row=firstRow; row <= s.getLastRowNum(); row++) + { + Row r = s.getRow(row); + if (r != null) + { + String[] d = new String[COLUMNS.length]; + for (int cn=0; cn> users) + { + // What we consider to be the literal string "user name" when detecting + // if a row is an example/header one or not + // Note - all of these want to be lower case! + List usernameIsUsername = new ArrayList(); + // The English literals + usernameIsUsername.add("username"); + usernameIsUsername.add("user name"); + // And the localised form too if found + PropertyDefinition unPD = dictionaryService.getProperty(ContentModel.PROP_USERNAME); + if (unPD != null) + { + if(unPD.getTitle(dictionaryService) != null) + usernameIsUsername.add(unPD.getTitle(dictionaryService).toLowerCase()); + if(unPD.getDescription(dictionaryService) != null) + usernameIsUsername.add(unPD.getDescription(dictionaryService).toLowerCase()); + } + + + // Process the contents of the spreadsheet + for (int lineNumber=0; lineNumber user = new HashMap(); + String[] userData = data[lineNumber]; + + if (userData == null || userData.length == 0 || + (userData.length == 1 && userData[0].trim().length() == 0)) + { + // Empty line, skip + continue; + } + + boolean required = true; + for (int i=0; i i) + { + value = userData[i]; + } + if (value == null || value.length() == 0) + { + if (required) + { + throw new ResourceBundleWebScriptException( + Status.STATUS_OK, getResources(), + ERROR_BLANK_COLUMN, new Object[] { + COLUMNS[i].getLocalName(), (i+1), (lineNumber+1)}); + } + } + else + { + user.put(COLUMNS[i], value); + } + } + + // If no password was given, use their surname + if (!user.containsKey(ContentModel.PROP_PASSWORD)) + { + user.put(ContentModel.PROP_PASSWORD, ""); + } + + // Skip any user who looks like an example file heading + // i.e. a username of "username" or "user name" + String username = user.get(ContentModel.PROP_USERNAME).toLowerCase(); + if (usernameIsUsername.contains(username)) + { + // Skip + } + else + { + // Looks like a real line, keep it + users.add(user); + } + } + } + + protected static class ResourceBundleWebScriptException extends WebScriptException + { + private String message; + + public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId, Object... args) + { + super(status, msgId, args); + buildMessageIfPossible(rb, msgId, args); + } + + public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId) + { + super(status, msgId); + buildMessageIfPossible(rb, msgId, new Object[0]); + } + + public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId, Throwable cause) + { + super(status, msgId, cause); + buildMessageIfPossible(rb, msgId, new Object[0]); + } + + public String getMessage() + { + return message; + } + + private void buildMessageIfPossible(ResourceBundle rb, String msgId, Object... args) + { + String msg = rb.getString(msgId); + if (msg != null) + { + if (args == null || args.length == 0) + { + message = msg; + } + else + { + message = MessageFormat.format(msg, args); + } + } + if (getCause() != null) + { + message = message + "\n" + getCause().getMessage(); + } + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/AbstractQuickShareContent.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/AbstractQuickShareContent.java new file mode 100644 index 00000000000..5aed93f4dfe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/AbstractQuickShareContent.java @@ -0,0 +1,58 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import org.alfresco.service.cmr.quickshare.QuickShareService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; + + +/** + * QuickShare/PublicView + * + * @author janv + * @since Cloud/4.2 + */ +public abstract class AbstractQuickShareContent extends DeclarativeWebScript +{ + protected QuickShareService quickShareService; + + private boolean enabled = true; + + public void setQuickShareService(QuickShareService quickShareService) + { + this.quickShareService = quickShareService; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + protected boolean isEnabled() + { + return this.enabled; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/MetaDataGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/MetaDataGet.java new file mode 100644 index 00000000000..19d146ce1e6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/MetaDataGet.java @@ -0,0 +1,85 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.web.scripts.WebScriptUtil; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * QuickShare/PublicView + * + * GET web script to get limited metadata (including thumbnail defs) => authenticated web script (using a nodeRef) + * + * Note: authenticated web script (equivalent to unauthenticated version - see QuickShareMetaDataGet) + * + * @author janv + * @since Cloud/4.2 + */ +public class MetaDataGet extends AbstractQuickShareContent +{ + private static final Log logger = LogFactory.getLog(QuickShareMetaDataGet.class); + + @Override + protected Map executeImpl(final WebScriptRequest req, Status status, Cache cache) + { + // create map of params (template vars) + Map params = req.getServiceMatch().getTemplateVars(); + final NodeRef nodeRef = WebScriptUtil.getNodeRef(params); + if (nodeRef == null) + { + String msg = "A valid NodeRef must be specified!"; + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, msg); + } + + try + { + Map model = quickShareService.getMetaData(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved limited metadata: "+nodeRef+" ["+model+"]"); + } + + return model; + } + catch (InvalidNodeRefException inre) + { + logger.error("Unable to find node: "+inre.getNodeRef()); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find nodeRef: "+inre.getNodeRef()); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareContentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareContentGet.java new file mode 100644 index 00000000000..29429860364 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareContentGet.java @@ -0,0 +1,187 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.QuickShareModel; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.repo.web.scripts.content.ContentGet; +import org.alfresco.repo.web.scripts.content.StreamContent; +import org.alfresco.service.cmr.quickshare.InvalidSharedIdException; +import org.alfresco.service.cmr.quickshare.QuickShareService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.web.context.ServletContextAware; + + +/** + * QuickShare/PublicView + * + * GET web script to get stream "shared" content (ie. enabled for public/unauthenticated access) from the repository + * + * WARNING: **unauthenticated** web script (equivalent to authenticated version - see ContentGet.java) + * + * @author janv + * @since Cloud/4.2 + */ +public class QuickShareContentGet extends ContentGet implements ServletContextAware +{ + private static final Log logger = LogFactory.getLog(QuickShareContentGet.class); + + private NodeService nodeService; + private NamespaceService namespaceService; + private QuickShareService quickShareSerivce; + + private boolean enabled = true; + + + public void setServletContext(ServletContext servletContext) + { + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + super.setNodeService(nodeService); + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setQuickShareService(QuickShareService quickShareService) + { + this.quickShareSerivce = quickShareService; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + protected boolean isEnabled() + { + return this.enabled; + } + + + @Override + public void execute(final WebScriptRequest req, final WebScriptResponse res) throws IOException + { + if (! isEnabled()) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "QuickShare is disabled system-wide"); + } + + // create map of template vars (params) + final Map params = req.getServiceMatch().getTemplateVars(); + final String sharedId = params.get("shared_id"); + if (sharedId == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "A valid sharedId must be specified !"); + } + + try + { + Pair pair = quickShareSerivce.getTenantNodeRefFromSharedId(sharedId); + final String tenantDomain = pair.getFirst(); + final NodeRef nodeRef = pair.getSecond(); + + TenantUtil.runAsSystemTenant(new TenantRunAsWork() + { + public Void doWork() throws Exception + { + if (! nodeService.getAspects(nodeRef).contains(QuickShareModel.ASPECT_QSHARE)) + { + throw new InvalidNodeRefException(nodeRef); + } + + // MNT-21118 (XSS prevention) + // Force the attachment in case of asking for the content file only + // (will be overridden for thumbnails) + executeImpl(nodeRef, params, req, res, null, true); + + return null; + } + }, tenantDomain); + + if (logger.isDebugEnabled()) + { + logger.debug("QuickShare - retrieved content: "+sharedId+" ["+nodeRef+"]"); + } + + } + catch (InvalidSharedIdException ex) + { + logger.error("Unable to find: "+sharedId); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.error("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + } + + protected void executeImpl(NodeRef nodeRef, Map templateVars, WebScriptRequest req, WebScriptResponse res, Map model, boolean attach) throws IOException + { + // render content + QName propertyQName = ContentModel.PROP_CONTENT; + String contentPart = templateVars.get("property"); + if (contentPart.length() > 0 && contentPart.charAt(0) == ';') + { + if (contentPart.length() < 2) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Content property malformed"); + } + String propertyName = contentPart.substring(1); + if (propertyName.length() > 0) + { + propertyQName = QName.createQName(propertyName, namespaceService); + } + } + + // Stream the content + streamContentLocal(req, res, nodeRef, attach, propertyQName, model); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareEnabledGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareEnabledGet.java new file mode 100644 index 00000000000..ad7d9675348 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareEnabledGet.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * QuickShare/PublicView + * + * GET web script to get property system.quickshare.enabled value + * + * @author sergey.shcherbovich + * @since 4.2 + */ +public class QuickShareEnabledGet extends AbstractQuickShareContent +{ + @Override + protected Map executeImpl(final WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + model.put("enabled", isEnabled()); + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareMetaDataGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareMetaDataGet.java new file mode 100644 index 00000000000..c7b9e12f964 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareMetaDataGet.java @@ -0,0 +1,87 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.quickshare.InvalidSharedIdException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * QuickShare/PublicView + * + * GET web script to get limited metadata (including thumbnail defs) for some "shared" content + * + * WARNING: **unauthenticated** web script (equivalent to authenticated version - see MetaDataGet.java) + * + * @author janv + * @since Cloud/4.2 + */ +public class QuickShareMetaDataGet extends MetaDataGet +{ + private static final Log logger = LogFactory.getLog(QuickShareMetaDataGet.class); + + @Override + protected Map executeImpl(final WebScriptRequest req, Status status, Cache cache) + { + if (! isEnabled()) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "QuickShare is disabled system-wide"); + } + + // create map of params (template vars) + Map params = req.getServiceMatch().getTemplateVars(); + final String sharedId = params.get("shared_id"); + if (sharedId == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "A valid sharedId must be specified !"); + } + + try + { + return quickShareService.getMetaData(sharedId); + } + catch (InvalidSharedIdException ex) + { + logger.error("Unable to find: "+sharedId); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.error("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareThumbnailContentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareThumbnailContentGet.java new file mode 100644 index 00000000000..3706121f4f1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/QuickShareThumbnailContentGet.java @@ -0,0 +1,201 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.jscript.BaseScopableProcessorExtension; +import org.alfresco.repo.jscript.ScriptNode; +import org.alfresco.repo.thumbnail.script.ScriptThumbnailService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mozilla.javascript.Scriptable; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * QuickShare/PublicView + * + * GET web script to stream "shared" thumbnail content (ie. enabled for public/unauthenticated access) from the repository + * + * WARNING: **unauthenticated** web script (equivalent to authenticated version - see "thumbnail.get.js") + * + * @author janv + * @since Cloud/4.2 + */ +public class QuickShareThumbnailContentGet extends QuickShareContentGet +{ + private static final Log logger = LogFactory.getLog(QuickShareContentGet.class); + + private ThumbnailService thumbnailService; + private ScriptThumbnailService scriptThumbnailService; + private ServiceRegistry serviceRegistry; + + public void setThumbnailService(ThumbnailService thumbnailService) + { + this.thumbnailService = thumbnailService; + } + + public void setScriptThumbnailService(ScriptThumbnailService scriptThumbnailService) + { + this.scriptThumbnailService = scriptThumbnailService; + } + + public void setServiceRegistry(ServiceRegistry services) + { + this.serviceRegistry = services; + } + + @Override + protected void executeImpl(NodeRef nodeRef, Map templateVars, WebScriptRequest req, WebScriptResponse res, Map model, boolean attach) throws IOException + { + String thumbnailName = templateVars.get("thumbnailname"); + if (thumbnailName == null) + { + logger.error("Thumbnail name was not provided: "+nodeRef); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find " + nodeRef); + } + + // Indicate whether or not the thumbnail can be cached by the browser. Caching is allowed if the lastModified + // argument is provided as this is an indication of request uniqueness and therefore the browser will have + // the latest thumbnail image. + if (model == null) + { + model = new HashMap(1); + } + + if (req.getParameter("lastModified") != null) + { + model.put("allowBrowserToCache", "true"); // note: must be String not boolean + } + else + { + model.put("allowBrowserToCache", "false"); // note: must be String not boolean + } + + + NodeRef thumbnailNodeRef = thumbnailService.getThumbnailByName(nodeRef, ContentModel.PROP_CONTENT, thumbnailName); + + if (thumbnailNodeRef == null) + { + // Get the queue/force create setting + boolean qc = false; + boolean fc = false; + String c = req.getParameter("c"); + if (c != null) + { + if (c.equals("queue")) + { + qc = true; + } + else if (c.equals("force")) + { + fc = true; + } + } + + // Get the place holder flag + boolean ph = false; + String phString = req.getParameter("ph"); + if (phString != null) + { + ph = new Boolean(phString); + } + + Scriptable scope = new BaseScopableProcessorExtension().getScope(); // note: required for ValueConverter (collection) + ScriptNode node = new ScriptNode(nodeRef, serviceRegistry, scope); + + // Queue the creation of the thumbnail if appropriate + if (fc) + { + ScriptNode thumbnailNode = node.createThumbnail(thumbnailName, false); + if (thumbnailNode != null) + { + thumbnailNodeRef = thumbnailNode.getNodeRef(); + } + } + else + { + if (qc) + { + node.createThumbnail(thumbnailName, true); + } + } + + if (thumbnailNodeRef == null) + { + if (ph == true) + { + // Try and get the place holder resource. We use a method in the thumbnail service + // that by default gives us a resource based on the content's mime type. + String phPath = null; + ContentData contentData = (ContentData)this.serviceRegistry.getNodeService().getProperty(nodeRef, ContentModel.PROP_CONTENT); + if (contentData != null) + { + phPath = scriptThumbnailService.getMimeAwarePlaceHolderResourcePath(thumbnailName, contentData.getMimetype()); + } + + if (phPath == null) + { + // 404 since no thumbnail was found + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Thumbnail was not found and no place holder resource set for '" + thumbnailName + "'"); + } + else + { + // Set the resouce path in the model ready for the content stream to send back to the client + model.put("contentPath", phPath); + } + } + else + { + // 404 since no thumbnail was found + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Thumbnail was not found"); + } + } + } + + // determine attachment + attach = Boolean.valueOf(req.getParameter("a")); + + super.executeImpl(thumbnailNodeRef, templateVars, req, res, model, attach); + + if (logger.isDebugEnabled()) + { + logger.debug("QuickShare - retrieved thumbnail content: "+thumbnailNodeRef+" ["+nodeRef+","+thumbnailName+"]"); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ReadGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ReadGet.java new file mode 100644 index 00000000000..5b6ad4bf301 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ReadGet.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.quickshare.InvalidSharedIdException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * QuickShare/PublicView + * + * GET web script that returns whether or not a user can read the shared content. + * + * @author Alex Miller + */ +public class ReadGet extends AbstractQuickShareContent +{ + private static final Log logger = LogFactory.getLog(ReadGet.class); + + @Override + protected Map executeImpl(final WebScriptRequest req, Status status, Cache cache) + { + if (! isEnabled()) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "QuickShare is disabled system-wide"); + } + + // create map of params (template vars) + Map params = req.getServiceMatch().getTemplateVars(); + final String sharedId = params.get("shared_id"); + if (sharedId == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "A valid sharedId must be specified !"); + } + + try + { + boolean canRead = quickShareService.canRead(sharedId); + Map result = new HashMap(); + result.put("canRead", canRead); + return result; + } + catch (InvalidSharedIdException ex) + { + logger.error("Unable to find: "+sharedId); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.error("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ShareContentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ShareContentGet.java new file mode 100644 index 00000000000..c7a4ac31109 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ShareContentGet.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * QuickShare/PublicView + * + * GET web script to lookup some context (nodeRef, tenantDomain, siteId) for a given "Share" + * + * Note: authenticated web script + * + * @author janv + * @since Cloud/4.2 + */ +public class ShareContentGet extends AbstractQuickShareContent +{ + private static final Log logger = LogFactory.getLog(ShareContentPost.class); + + protected SiteService siteService; + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + if (! isEnabled()) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "QuickShare is disabled system-wide"); + } + + // create map of params (template vars) + Map params = req.getServiceMatch().getTemplateVars(); + final String sharedId = params.get("shared_id"); + if (sharedId == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "A valid sharedId must be specified !"); + } + + try + { + Pair pair = quickShareService.getTenantNodeRefFromSharedId(sharedId); + final String tenantDomain = pair.getFirst(); + final NodeRef nodeRef = pair.getSecond(); + + String siteId = siteService.getSiteShortName(nodeRef); + + Map model = new HashMap(3); + model.put("sharedId", sharedId); + model.put("nodeRef", nodeRef.toString()); + model.put("siteId", siteId); + model.put("tenantDomain", tenantDomain); + + if (logger.isInfoEnabled()) + { + logger.info("QuickShare - get shared context: "+sharedId+" ["+model+"]"); + } + + return model; + } + catch (InvalidNodeRefException inre) + { + logger.error("Unable to find: "+sharedId+" ["+inre.getNodeRef()+"]"); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: "+sharedId); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ShareContentPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ShareContentPost.java new file mode 100644 index 00000000000..1519d10b73e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/ShareContentPost.java @@ -0,0 +1,85 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.web.scripts.WebScriptUtil; +import org.alfresco.service.cmr.quickshare.QuickShareDTO; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * QuickShare/PublicView + * + * POST web script to "Share" access to some content (ie. enable unauthenticated access to this node) + * + * Note: authenticated web script + * + * @author janv + * @since Cloud/4.2 + */ +public class ShareContentPost extends AbstractQuickShareContent +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + if (! isEnabled()) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "QuickShare is disabled system-wide"); + } + + // create map of params (template vars) + Map params = req.getServiceMatch().getTemplateVars(); + final NodeRef nodeRef = WebScriptUtil.getNodeRef(params); + if (nodeRef == null) + { + String msg = "A valid NodeRef must be specified!"; + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, msg); + } + + try + { + QuickShareDTO dto = quickShareService.shareContent(nodeRef); + + Map model = new HashMap(1); + model.put("sharedDTO", dto); + return model; + } + catch (InvalidNodeRefException inre) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find node: " + nodeRef); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/UnshareContentDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/UnshareContentDelete.java new file mode 100644 index 00000000000..d33a4119433 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/quickshare/UnshareContentDelete.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.quickshare; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.QuickShareModel; +import org.alfresco.service.cmr.quickshare.InvalidSharedIdException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * QuickShare/PublicView + * + * DELETE web script to "Unshare" access to some content (ie. disable unauthenticated access to this node) + * + * Note: authenticated web script + * + * @author janv + * @since Cloud/4.2 + */ +public class UnshareContentDelete extends AbstractQuickShareContent +{ + private static final Log logger = LogFactory.getLog(ShareContentPost.class); + + private NodeService nodeService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + if (! isEnabled()) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "QuickShare is disabled system-wide"); + } + + // create map of params (template vars) + Map params = req.getServiceMatch().getTemplateVars(); + final String sharedId = params.get("shared_id"); + if (sharedId == null) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "A valid sharedId must be specified !"); + } + + try + { + NodeRef nodeRef = quickShareService.getTenantNodeRefFromSharedId(sharedId).getSecond(); + + String sharedBy = (String) nodeService.getProperty(nodeRef, QuickShareModel.PROP_QSHARE_SHAREDBY); + if (!quickShareService.canDeleteSharedLink(nodeRef, sharedBy)) + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Can't perform unshare action: " + sharedId); + } + quickShareService.unshareContent(sharedId); + + Map model = new HashMap<>(1); + model.put("success", Boolean.TRUE); + return model; + } + catch (InvalidSharedIdException ex) + { + logger.error("Unable to find: " + sharedId); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: " + sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.error("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find: " + sharedId); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/AbstractRatingWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/AbstractRatingWebScript.java new file mode 100644 index 00000000000..162771d9e06 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/AbstractRatingWebScript.java @@ -0,0 +1,104 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rating; + +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.rating.RatingService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is an abstract base class for the various webscript controllers in the + * RatingService. + * + * @author Neil McErlean + * @since 3.4 + */ +public abstract class AbstractRatingWebScript extends DeclarativeWebScript +{ + // Web script parameters. + protected static final String RATING_SCHEME = "ratingScheme"; + protected static final String RATING = "rating"; + protected static final String RATED_NODE = "ratedNode"; + protected static final String NODE_REF = "nodeRef"; + protected static final String RATINGS = "ratings"; + + protected static final String AVERAGE_RATINGS = "averageRatings"; + protected static final String RATINGS_TOTALS = "ratingsTotals"; + protected static final String RATINGS_COUNTS = "ratingsCounts"; + + // Injected services + protected NodeService nodeService; + protected RatingService ratingService; + + /** + * Sets the node service instance + * + * @param nodeService the node service to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the rating service instance + * + * @param ratingService the rating service to set + */ + public void setRatingService(RatingService ratingService) + { + this.ratingService = ratingService; + } + + protected NodeRef parseRequestForNodeRef(WebScriptRequest req) + { + // get the parameters that represent the NodeRef, we know they are present + // otherwise this webscript would not have matched + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + String nodeId = templateVars.get("id"); + + // create the NodeRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + NodeRef nodeRef = new NodeRef(storeRef, nodeId); + + if (!this.nodeService.exists(nodeRef)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find node: " + nodeRef.toString()); + } + + return nodeRef; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingDefinitionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingDefinitionsGet.java new file mode 100644 index 00000000000..11dbe496c32 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingDefinitionsGet.java @@ -0,0 +1,55 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rating; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.rating.RatingScheme; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller class for the rating.definitions.get webscript. + * + * @author Neil McErlean + * @since 3.4 + */ +public class RatingDefinitionsGet extends AbstractRatingWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + Map schemes = this.ratingService.getRatingSchemes(); + + model.put("schemeDefs", schemes); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java new file mode 100644 index 00000000000..03977d0bcea --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingDelete.java @@ -0,0 +1,85 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rating; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.rating.Rating; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the rating.delete web script. + * + * @author Neil McErlean + * @since 3.4 + */ +public class RatingDelete extends AbstractRatingWebScript +{ + private final static String AVERAGE_RATING = "averageRating"; + private final static String RATINGS_TOTAL = "ratingsTotal"; + private final static String RATINGS_COUNT = "ratingsCount"; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + NodeRef nodeRef = parseRequestForNodeRef(req); + String ratingSchemeName = parseRequestForScheme(req); + + Rating deletedRating = ratingService.removeRatingByCurrentUser(nodeRef, ratingSchemeName); + if (deletedRating == null) + { + // There was no rating in the specified scheme to delete. + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Unable to delete non-existent rating: " + + ratingSchemeName + " from " + nodeRef.toString()); + } + + model.put(NODE_REF, nodeRef.toString()); + model.put(AVERAGE_RATING, ratingService.getAverageRating(nodeRef, ratingSchemeName)); + model.put(RATINGS_TOTAL, ratingService.getTotalRating(nodeRef, ratingSchemeName)); + model.put(RATINGS_COUNT, ratingService.getRatingsCount(nodeRef, ratingSchemeName)); + + return model; + } + + private String parseRequestForScheme(WebScriptRequest req) + { + // We know the 'scheme' URL element is there because if it wasn't + // the URL would not have matched. + Map templateVars = req.getServiceMatch().getTemplateVars(); + String scheme = templateVars.get("scheme"); + + return scheme; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingPost.java new file mode 100644 index 00000000000..c54eb1552a3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingPost.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rating; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.rating.RatingScheme; +import org.alfresco.service.cmr.repository.NodeRef; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the rating.post webscript. + * + * @author Neil McErlean + * @since 3.4 + */ +public class RatingPost extends AbstractRatingWebScript +{ + // Url format + private final static String NODE_RATINGS_URL_FORMAT = "/api/node/{0}/ratings"; + + private final static String AVERAGE_RATING = "averageRating"; + private final static String RATINGS_TOTAL = "ratingsTotal"; + private final static String RATINGS_COUNT = "ratingsCount"; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + NodeRef nodeRefToBeRated = parseRequestForNodeRef(req); + + JSONObject json = null; + try + { + // read request json + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // Check mandatory parameters. + if (json.has(RATING) == false) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "rating parameter missing when applying rating"); + } + if (json.has(RATING_SCHEME) == false) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "schemeName parameter missing when applying rating"); + } + + // Check that the scheme name actually exists + final String schemeName = json.getString(RATING_SCHEME); + RatingScheme scheme = ratingService.getRatingScheme(schemeName); + if (scheme == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Unknown scheme name: " + schemeName); + } + + // Range checking of the rating score will be done within the RatingService. + // So we can just apply the rating. + final float rating = (float)json.getDouble(RATING); + ratingService.applyRating(nodeRefToBeRated, rating, schemeName); + + // We'll return the URL to the ratings of the just-rated node. + String ratedNodeUrlFragment = nodeRefToBeRated.toString().replace("://", "/"); + String ratedNodeUrl = MessageFormat.format(NODE_RATINGS_URL_FORMAT, ratedNodeUrlFragment); + + model.put(RATED_NODE, ratedNodeUrl); + model.put(RATING, rating); + model.put(RATING_SCHEME, schemeName); + model.put(AVERAGE_RATING, ratingService.getAverageRating(nodeRefToBeRated, schemeName)); + model.put(RATINGS_TOTAL, ratingService.getTotalRating(nodeRefToBeRated, schemeName)); + model.put(RATINGS_COUNT, ratingService.getRatingsCount(nodeRefToBeRated, schemeName)); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingsGet.java new file mode 100644 index 00000000000..d119b5ac5da --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rating/RatingsGet.java @@ -0,0 +1,83 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rating; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.rating.Rating; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the ratings.get web script. + * + * @author Neil McErlean + * @since 3.4 + */ +public class RatingsGet extends AbstractRatingWebScript +{ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + NodeRef nodeRef = parseRequestForNodeRef(req); + + // These are the data for the current user's ratings of this node, if any. + List myRatings = new ArrayList(); + + // These maps hold the average rating, accumulated total of all ratings and + // the number of ratings applied for this node as a function of rating scheme. + Map averageRatings = new HashMap(); + Map ratingsTotals = new HashMap(); + Map ratingsCounts = new HashMap(); + + for (String schemeName : ratingService.getRatingSchemes().keySet()) + { + final Rating ratingByCurrentUser = ratingService.getRatingByCurrentUser(nodeRef, schemeName); + if (ratingByCurrentUser != null) + { + myRatings.add(ratingByCurrentUser); + } + averageRatings.put(schemeName, ratingService.getAverageRating(nodeRef, schemeName)); + ratingsTotals.put(schemeName, ratingService.getTotalRating(nodeRef, schemeName)); + ratingsCounts.put(schemeName, ratingService.getRatingsCount(nodeRef, schemeName)); + } + + model.put(NODE_REF, nodeRef.toString()); + model.put(RATINGS, myRatings); + model.put(AVERAGE_RATINGS, averageRatings); + model.put(RATINGS_TOTALS, ratingsTotals); + model.put(RATINGS_COUNTS, ratingsCounts); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rendition/patch/PatchThumbnailsAsRenditionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rendition/patch/PatchThumbnailsAsRenditionsGet.java new file mode 100644 index 00000000000..6e42a19d7e2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rendition/patch/PatchThumbnailsAsRenditionsGet.java @@ -0,0 +1,156 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rendition.patch; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.RenditionModel; +import org.alfresco.service.cmr.rendition.RenditionService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +public class PatchThumbnailsAsRenditionsGet extends DeclarativeWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(PatchThumbnailsAsRenditionsGet.class); + + private static final StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private static final String QUERY = "TYPE:\"" + ContentModel.TYPE_THUMBNAIL + + "\" AND NOT ASPECT:\"" + RenditionModel.ASPECT_VISIBLE_RENDITION + + "\" AND NOT ASPECT:\"" + RenditionModel.ASPECT_HIDDEN_RENDITION + "\""; + + /** Spring-injected services */ + private NodeService nodeService; + private RenditionService renditionService; + private SearchService searchService; + + /** + * Sets the nodeService. + * + * @param nodeService NodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the renditionService. + * + * @param renditionService RenditionService + */ + public void setRenditionService(RenditionService renditionService) + { + this.renditionService = renditionService; + } + + /** + * Sets the searchService. + * + * @param searchService SearchService + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + if (logger.isInfoEnabled()) + { + logger.debug("Patching legacy thumbnails by applying appropriate rendition aspect"); + } + List resultNodeRefs = null; + ResultSet types = null; + + try + { + types = searchService.query(SPACES_STORE, SearchService.LANGUAGE_LUCENE, QUERY); + resultNodeRefs = types.getNodeRefs(); + } + finally + { + if (types != null) {types.close();} + } + + long patchedNodeRefs = 0; + + for (NodeRef nodeRef : resultNodeRefs) + { + if (nodeService.exists(nodeRef) == false || + renditionService.isRendition(nodeRef)) + { + continue; + } + + // Now add one of the two aspects depending on parent location. + ChildAssociationRef sourceNode = renditionService.getSourceNode(nodeRef); + ChildAssociationRef primaryParent = nodeService.getPrimaryParent(nodeRef); + QName aspectToApply; + if (primaryParent.getParentRef().equals(sourceNode.getParentRef())) + { + aspectToApply = RenditionModel.ASPECT_HIDDEN_RENDITION; + } + else + { + aspectToApply = RenditionModel.ASPECT_VISIBLE_RENDITION; + } + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Applying aspect ") + .append(aspectToApply) + .append(" to node ") + .append(nodeRef); + logger.debug(msg.toString()); + } + nodeService.addAspect(nodeRef, aspectToApply, null); + patchedNodeRefs++; + } + + Map model = new HashMap(); + model.put("patchedNodeCount", new Long(patchedNodeRefs)); + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/AbstractReplicationWebscript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/AbstractReplicationWebscript.java new file mode 100644 index 00000000000..4576a0cc4e1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/AbstractReplicationWebscript.java @@ -0,0 +1,161 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.Map; + +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.scheduled.SchedulableAction.IntervalPeriod; +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.alfresco.service.cmr.replication.ReplicationService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.util.ISO8601DateFormat; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 3.4 + */ +public abstract class AbstractReplicationWebscript extends DeclarativeWebScript +{ + protected NodeService nodeService; + protected ReplicationService replicationService; + protected ActionTrackingService actionTrackingService; + + public void setReplicationService(ReplicationService replicationService) + { + this.replicationService = replicationService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setActionTrackingService(ActionTrackingService actionTrackingService) + { + this.actionTrackingService = actionTrackingService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + ReplicationModelBuilder modelBuilder = new ReplicationModelBuilder( + nodeService, replicationService, actionTrackingService + ); + return buildModel(modelBuilder, req, status, cache); + } + + /** + * Updates properties on the definition, based on the JSON. + * Doesn't save the definition + * Doesn't change the name + */ + protected void updateDefinitionProperties(ReplicationDefinition replicationDefinition, JSONObject json) + throws JSONException + { + if(json.has("description")) { + String description = json.getString("description"); + replicationDefinition.setDescription(description); + } + + if(json.has("targetName")) { + String targetName = json.getString("targetName"); + replicationDefinition.setTargetName(targetName); + } + + if(json.has("enabled")) { + boolean enabled = json.getBoolean("enabled"); + replicationDefinition.setEnabled(enabled); + } + + if(json.has("payload")) { + replicationDefinition.getPayload().clear(); + JSONArray payload = json.getJSONArray("payload"); + for(int i=0; i buildModel( + ReplicationModelBuilder modelBuilder, + WebScriptRequest req, + Status status, Cache cache + ); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionDelete.java new file mode 100644 index 00000000000..72058c33fa6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionDelete.java @@ -0,0 +1,70 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public class ReplicationDefinitionDelete extends AbstractReplicationWebscript +{ + @Override + protected Map buildModel(ReplicationModelBuilder modelBuilder, + WebScriptRequest req, Status status, Cache cache) + { + // Which definition did they ask for? + String replicationDefinitionName = + req.getServiceMatch().getTemplateVars().get("replication_definition_name"); + ReplicationDefinition replicationDefinition = + replicationService.loadReplicationDefinition(replicationDefinitionName); + + // Does it exist? + if(replicationDefinition == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Replication Definition found with that name" + ); + } + + // Delete it + replicationService.deleteReplicationDefinition(replicationDefinition); + + // Report that we have deleted it + status.setCode(Status.STATUS_NO_CONTENT); + status.setMessage("Replication Definition deleted"); + status.setRedirect(true); + return null; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionGet.java new file mode 100644 index 00000000000..5d430d1972c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionGet.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public class ReplicationDefinitionGet extends AbstractReplicationWebscript +{ + @Override + protected Map buildModel(ReplicationModelBuilder modelBuilder, + WebScriptRequest req, Status status, Cache cache) + { + // Which definition did they ask for? + String replicationDefinitionName = + req.getServiceMatch().getTemplateVars().get("replication_definition_name"); + ReplicationDefinition replicationDefinition = + replicationService.loadReplicationDefinition(replicationDefinitionName); + + // Does it exist? + if(replicationDefinition == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Replication Definition found with that name" + ); + } + + // Have it turned into simple models + return modelBuilder.buildDetails(replicationDefinition); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionPut.java new file mode 100644 index 00000000000..b3c29f121ef --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionPut.java @@ -0,0 +1,108 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.io.IOException; +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public class ReplicationDefinitionPut extends AbstractReplicationWebscript +{ + @Override + protected Map buildModel(ReplicationModelBuilder modelBuilder, + WebScriptRequest req, Status status, Cache cache) + { + // Which definition did they ask for? + String replicationDefinitionName = + req.getServiceMatch().getTemplateVars().get("replication_definition_name"); + ReplicationDefinition replicationDefinition = + replicationService.loadReplicationDefinition(replicationDefinitionName); + + // Does it exist? + if(replicationDefinition == null) { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Replication Definition found with that name" + ); + } + + // Grab the JSON, and prepare to update + try + { + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // Are they trying to rename? + if(json.has("name")) { + String jsonName = json.getString("name"); + if(! jsonName.equals(replicationDefinitionName)) { + // Name has changed, ensure the new name is spare + if(replicationService.loadReplicationDefinition(jsonName) != null) { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "The specified new name is already in use"); + } + + // Rename it + replicationService.renameReplicationDefinition( + replicationDefinitionName, + jsonName + ); + + // And grab the updated version post-rename + replicationDefinition = replicationService.loadReplicationDefinition(jsonName); + } + } + + // Update everything else + updateDefinitionProperties(replicationDefinition, json); + + // Save the changes + replicationService.saveReplicationDefinition(replicationDefinition); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from request.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from request.", je); + } + + // Return the new details on it + return modelBuilder.buildDetails(replicationDefinition); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionsGet.java new file mode 100644 index 00000000000..850af8b975a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionsGet.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public class ReplicationDefinitionsGet extends AbstractReplicationWebscript +{ + @Override + protected Map buildModel(ReplicationModelBuilder modelBuilder, + WebScriptRequest req, Status status, Cache cache) + { + // Get all the defined replication definitions + List definitions = replicationService.loadReplicationDefinitions(); + + // How do we need to sort them? + Comparator> sorter = new ReplicationModelBuilder.SimpleSorterByName(); + String sort = req.getParameter("sort"); + if(sort == null) { + // Default was set above + } else if(sort.equalsIgnoreCase("status")) { + sorter = new ReplicationModelBuilder.SimpleSorterByStatus(); + } else if(sort.equalsIgnoreCase("lastRun") || + sort.equalsIgnoreCase("lastRunTime")) { + sorter = new ReplicationModelBuilder.SimpleSorterByLastRun(); + } + + // Have them turned into simple models + return modelBuilder.buildSimpleList(definitions, sorter); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionsPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionsPost.java new file mode 100644 index 00000000000..511c4d3b02d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationDefinitionsPost.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.io.IOException; +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + + +/** + * @author Nick Burch + * @since 3.4 + */ +public class ReplicationDefinitionsPost extends AbstractReplicationWebscript +{ + @Override + protected Map buildModel(ReplicationModelBuilder modelBuilder, + WebScriptRequest req, Status status, Cache cache) + { + // Create our definition + ReplicationDefinition replicationDefinition = null; + JSONObject json; + + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // Check for the required parameters + if(! json.has("name")) + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "name is required but wasn't supplied"); + if(! json.has("description")) + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "description is required but wasn't supplied"); + + // Ensure one doesn't already exist with that name + String name = json.getString("name"); + if(replicationService.loadReplicationDefinition(name) != null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "A replication definition already exists with that name"); + } + + // Create the definition + replicationDefinition = replicationService.createReplicationDefinition( + name, json.getString("description") + ); + + // Set the extra parts + updateDefinitionProperties(replicationDefinition, json); + + // Save the changes + replicationService.saveReplicationDefinition(replicationDefinition); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from request.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from request.", je); + } + + // Return the details on it + return modelBuilder.buildDetails(replicationDefinition); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationModelBuilder.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationModelBuilder.java new file mode 100644 index 00000000000..4e1678a57b3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationModelBuilder.java @@ -0,0 +1,377 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.web.scripts.action.AbstractActionWebscript; +import org.alfresco.service.cmr.action.ActionTrackingService; +import org.alfresco.service.cmr.action.ExecutionDetails; +import org.alfresco.service.cmr.action.ExecutionSummary; +import org.alfresco.service.cmr.replication.ReplicationDefinition; +import org.alfresco.service.cmr.replication.ReplicationService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.util.ISO8601DateFormat; + +/** + * Builds up models from ReplicationDefinitions, either + * in summary or detail form. + * + * @author Nick Burch + * @since 3.4 + */ +public class ReplicationModelBuilder +{ + protected static final String MODEL_DATA_ITEM = "replicationDefinition"; + protected static final String MODEL_DATA_LIST = "replicationDefinitions"; + + protected static final String DEFINITION_NAME = "name"; + protected static final String DEFINITION_DESCRIPTION = "description"; + protected static final String DEFINITION_STATUS = "status"; + protected static final String DEFINITION_STARTED_AT = "startedAt"; + protected static final String DEFINITION_ENDED_AT = "endedAt"; + protected static final String DEFINITION_FAILURE_MESSAGE = "failureMessage"; + protected static final String DEFINITION_RUNNING_ACTION_ID = "runningActionId"; + protected static final String DEFINITION_PAYLOAD = "payload"; + protected static final String DEFINITION_TRANSFER_LOCAL_REPORT = "transferLocalReport"; + protected static final String DEFINITION_TRANSFER_REMOTE_REPORT = "transferRemoteReport"; + protected static final String DEFINITION_ENABLED = "enabled"; + protected static final String DEFINITION_TARGET_NAME = "targetName"; + + protected static final String DEFINITION_SCHEDULE_ENABLED = "scheduleEnabled"; + protected static final String DEFINITION_SCHEDULE_START = "scheduleStart"; + protected static final String DEFINITION_SCHEDULE_PERIOD = "scheduleIntervalPeriod"; + protected static final String DEFINITION_SCHEDULE_COUNT = "scheduleIntervalCount"; + + protected NodeService nodeService; + protected ReplicationService replicationService; + protected ActionTrackingService actionTrackingService; + + public ReplicationModelBuilder(NodeService nodeService, ReplicationService replicationService, + ActionTrackingService actionTrackingService) + { + this.nodeService = nodeService; + this.replicationService = replicationService; + this.actionTrackingService = actionTrackingService; + } + + + /** + * Sorts simple definitions by their status + */ + public static class SimpleSorterByStatus implements Comparator> { + private SimpleSorterByName nameSorter = new SimpleSorterByName(); + public int compare(Map simpleA, Map simpleB) { + String statusA = (String)simpleA.get(DEFINITION_STATUS); + String statusB = (String)simpleB.get(DEFINITION_STATUS); + if(statusA == null || statusB == null) { + throw new IllegalArgumentException("Status missing during sort"); + } + + int compare = statusA.compareTo(statusB); + if(compare == 0) { + return nameSorter.compare(simpleA, simpleB); + } + return compare; + } + } + /** + * Sorts simple definitions by their name + */ + public static class SimpleSorterByName implements Comparator> { + public int compare(Map simpleA, Map simpleB) { + String nameA = (String)simpleA.get(DEFINITION_NAME); + String nameB = (String)simpleB.get(DEFINITION_NAME); + if(nameA == null || nameB == null) { + throw new IllegalArgumentException("Name missing during sort"); + } + return nameA.compareTo(nameB); + } + } + /** + * Sorts simple definitions by their last run time. + * Things that have never been run go to the bottom of the list, + * so we see most recently run, least recently run, never run. + */ + public static class SimpleSorterByLastRun implements Comparator> { + /** Works on ISO8601 formatted date strings */ + public int compare(Map simpleA, Map simpleB) { + String dateA = (String)simpleA.get(DEFINITION_STARTED_AT); + String dateB = (String)simpleB.get(DEFINITION_STARTED_AT); + if(dateA == null && dateB == null) { + return 0; + } + if(dateA != null && dateB == null) { + return -1; + } + if(dateA == null && dateB != null) { + return 1; + } + // We want more recent dates first + return 0-dateA.compareTo(dateB); + } + } + + + /** + * Build a model containing a list of simple definitions for the given + * list of Replication Definitions. + */ + protected Map buildSimpleList(List replicationDefinitions, + Comparator> sorter) + { + List> models = new ArrayList>(); + + // Only bother looking up the execution status if we + // have some Replication Definitions to render + if(replicationDefinitions.size() > 0) { + List executing = + actionTrackingService.getExecutingActions(replicationDefinitions.get(0).getActionDefinitionName()); + + for(ReplicationDefinition rd : replicationDefinitions) { + // Get the executing detail(s) for this definition + ExecutionDetails details = getExecutionDetails(rd, executing); + + // Set the core details + Map rdm = new HashMap(); + rdm.put(DEFINITION_NAME, rd.getReplicationName()); + rdm.put(DEFINITION_ENABLED, rd.isEnabled()); + + // Do the status + setStatus(rd, details, rdm); + // In the summary form, we don't need end time or details + rdm.remove(DEFINITION_ENDED_AT); + rdm.remove(DEFINITION_RUNNING_ACTION_ID); + + // Add to the list of finished models + models.add(rdm); + } + } + + // Sort the entries + if(sorter != null) { + Collections.sort(models, sorter); + } + + // Finish up + Map model = new HashMap(); + model.put(MODEL_DATA_LIST, models); + return model; + } + + + /** + * Build a model containing the full, detailed definition for the given + * Replication Definition. + */ + protected Map buildDetails(ReplicationDefinition rd) { + Map rdm = new HashMap(); + + // Set the core details + rdm.put(DEFINITION_NAME, rd.getReplicationName()); + rdm.put(DEFINITION_DESCRIPTION, rd.getDescription()); + rdm.put(DEFINITION_ENABLED, rd.isEnabled()); + rdm.put(DEFINITION_TARGET_NAME, rd.getTargetName()); + + // Set the scheduling details + rdm.put(DEFINITION_SCHEDULE_ENABLED, rd.isSchedulingEnabled()); + if(rd.isSchedulingEnabled()) + { + rdm.put(DEFINITION_SCHEDULE_START, ISO8601DateFormat.format(rd.getScheduleStart())); + + rdm.put(DEFINITION_SCHEDULE_COUNT, rd.getScheduleIntervalCount()); + if(rd.getScheduleIntervalPeriod() != null) { + rdm.put(DEFINITION_SCHEDULE_PERIOD, rd.getScheduleIntervalPeriod().toString()); + } else { + rdm.put(DEFINITION_SCHEDULE_PERIOD, null); + } + } + + // Set the details of the previous run + // These will be null'd out later if replication is in progress + rdm.put(DEFINITION_FAILURE_MESSAGE, rd.getExecutionFailureMessage()); + rdm.put(DEFINITION_TRANSFER_LOCAL_REPORT, rd.getLocalTransferReport()); + rdm.put(DEFINITION_TRANSFER_REMOTE_REPORT, rd.getRemoteTransferReport()); + + // Do the status + // Includes start+end times, and running action details + setStatus(rd, rdm); + + // Only include the payload entries that still exist + // Otherwise the freemarker layer gets upset about deleted nodes + List payload = new ArrayList(); + for(NodeRef node : rd.getPayload()) { + if(nodeService.exists(node)) + payload.add(node); + } + rdm.put(DEFINITION_PAYLOAD, payload); + + // Save in the usual way + Map model = new HashMap(); + model.put(MODEL_DATA_ITEM, rdm); + return model; + } + + + /** + * Figures out the status that's one of: + * New|Running|CancelRequested|Completed|Failed|Cancelled + * by merging data from the action tracking service. + * Will also set the start and end dates, from either the + * replication definition or action tracking data, depending + * on the status. + */ + protected void setStatus(ReplicationDefinition replicationDefinition, Map model) + { + // Grab the running instance(s) of the action + List executing = + actionTrackingService.getExecutingActions(replicationDefinition); + // Now get the details of that + ExecutionDetails details = getExecutionDetails(replicationDefinition, executing); + // Finally have the status set + setStatus(replicationDefinition, details, model); + } + /** + * Figures out the status that's one of: + * New|Running|CancelRequested|Completed|Failed|Cancelled + * by merging data from the action tracking service. + * Will also set the start and end dates, from either the + * replication definition or action tracking data, depending + * on the status. + */ + protected void setStatus(ReplicationDefinition replicationDefinition, + ExecutionDetails details, Map model) + { + // Is it currently running? + if(details == null) { + // It isn't running, we can use the persisted details + model.put(DEFINITION_STATUS, replicationDefinition.getExecutionStatus().toString()); + + Date startDate = replicationDefinition.getExecutionStartDate(); + if(startDate != null) { + model.put(DEFINITION_STARTED_AT, ISO8601DateFormat.format(startDate)); + } else { + model.put(DEFINITION_STARTED_AT, null); + } + + Date endDate = replicationDefinition.getExecutionEndDate(); + if(endDate != null) { + model.put(DEFINITION_ENDED_AT, ISO8601DateFormat.format(endDate)); + } else { + model.put(DEFINITION_ENDED_AT, null); + } + + // It isn't running + model.put(DEFINITION_RUNNING_ACTION_ID, null); + + return; + } + + // As it is running / about to run, return the + // details of the running/pending version + if(details.isCancelRequested()) { + model.put(DEFINITION_STATUS, "CancelRequested"); + } else if(details.getStartedAt() == null) { + model.put(DEFINITION_STATUS, "Pending"); + } else { + model.put(DEFINITION_STATUS, "Running"); + } + + if(details.getStartedAt() != null) { + model.put(DEFINITION_STARTED_AT, ISO8601DateFormat.format(details.getStartedAt())); + } else { + model.put(DEFINITION_STARTED_AT, null); + } + model.put(DEFINITION_ENDED_AT, null); + model.put(DEFINITION_RUNNING_ACTION_ID, + AbstractActionWebscript.getRunningId(details.getExecutionSummary())); + + // Since it's running / about to run, there shouldn't + // be failure messages or transfer reports + // If these currently exist on the model, remove them + if(model.containsKey(DEFINITION_FAILURE_MESSAGE)) + model.put(DEFINITION_FAILURE_MESSAGE, null); + if(model.containsKey(DEFINITION_TRANSFER_LOCAL_REPORT)) + model.put(DEFINITION_TRANSFER_LOCAL_REPORT, null); + if(model.containsKey(DEFINITION_TRANSFER_REMOTE_REPORT)) + model.put(DEFINITION_TRANSFER_REMOTE_REPORT, null); + } + + /** + * For the given Replication Definition, and list of executing + * actions (which may or may not be only for this definition), + * return a single execution details. + * + * Returns null if no copies of the definition are executing. + * Returns a predictable instance if more than one copy is + * executing. + */ + private ExecutionDetails getExecutionDetails(ReplicationDefinition replicationDefinition, + List executing) + { + // Figure out which of the running actions are us + List ours = new ArrayList(); + for(ExecutionSummary es : executing) { + if(es.getActionType().equals(replicationDefinition.getActionDefinitionName()) && + es.getActionId().equals(replicationDefinition.getId())) { + ours.add(es); + } + } + + // Do we have anything running at the moment + if(ours.size() == 0) { + // Not executing at the moment + return null; + } + + // We have at least one copy running + ExecutionSummary es; + if(executing.size() == 1) { + // Only one copy, life is simple + es = ours.get(0); + } else { + // More than one copy runing, joy + // Go for the lowest execution instance id, so + // we're predictable + es = ours.get(0); + for(ExecutionSummary e : ours) { + if(e.getExecutionInstance() < es.getExecutionInstance()) { + es = e; + } + } + } + + // Grab the details + return actionTrackingService.getExecutionDetails(es); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationServiceStatusGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationServiceStatusGet.java new file mode 100644 index 00000000000..e466b7d4e01 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/replication/ReplicationServiceStatusGet.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.replication; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.replication.ReplicationService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Neil Mc Erlean + * @since 3.5 + */ +public class ReplicationServiceStatusGet extends DeclarativeWebScript +{ + public static final String ENABLED = "enabled"; + + private ReplicationService replicationService; + + /** + * Used to inject the {@link ReplicationService}. + * + * @param replicationService ReplicationService + */ + public void setReplicationService(ReplicationService replicationService) + { + this.replicationService = replicationService; + } + + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + boolean serviceEnabled = replicationService.isEnabled(); + + model.put(ENABLED, serviceEnabled); + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java new file mode 100644 index 00000000000..c86901bacd4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java @@ -0,0 +1,448 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.action.ParameterizedItemDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public abstract class AbstractRuleWebScript extends DeclarativeWebScript +{ + + public static final SimpleDateFormat dateFormate = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"); + + private static final String RULE_OUTBOUND = "outbound"; + private static final String ACTION_CHECK_OUT = "check-out"; + + private static final String CANNOT_CREATE_RULE = "cannot.create.rule.checkout.outbound"; + + protected NodeService nodeService; + protected RuleService ruleService; + protected DictionaryService dictionaryService; + protected ActionService actionService; + protected FileFolderService fileFolderService; + protected NamespaceService namespaceService; + + /** + * Sets the node service instance + * + * @param nodeService the node service to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set rule service instance + * + * @param ruleService the rule service to set + */ + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + /** + * Set dictionary service instance + * + * @param dictionaryService the dictionary service to set + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set action service instance + * + * @param actionService the action service to set + */ + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set file folder service instance + * + * @param fileFolderService the fileFolderService to set + */ + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + /** + * Set namespace service instance + * + * @param namespaceService the namespace service to set + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Parses the request and providing it's valid returns the NodeRef. + * + * @param req The webscript request + * @return The NodeRef passed in the request + * + */ + protected NodeRef parseRequestForNodeRef(WebScriptRequest req) + { + // get the parameters that represent the NodeRef, we know they are present + // otherwise this webscript would not have matched + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + String storeId = templateVars.get("store_id"); + String nodeId = templateVars.get("id"); + + // create the NodeRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + NodeRef nodeRef = new NodeRef(storeRef, nodeId); + + if (!this.nodeService.exists(nodeRef)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find node: " + nodeRef.toString()); + } + + return nodeRef; + } + + protected Rule parseJsonRule(JSONObject jsonRule) throws JSONException + { + Rule result = new Rule(); + + if (jsonRule.has("title") == false || jsonRule.getString("title").length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Title missing when creating rule"); + } + + result.setTitle(jsonRule.getString("title")); + + result.setDescription(jsonRule.has("description") ? jsonRule.getString("description") : ""); + + if (jsonRule.has("ruleType") == false || jsonRule.getJSONArray("ruleType").length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Rule type missing when creating rule"); + } + + JSONArray types = jsonRule.getJSONArray("ruleType"); + List ruleTypes = new ArrayList(); + + for (int i = 0; i < types.length(); i++) + { + ruleTypes.add(types.getString(i)); + } + + result.setRuleTypes(ruleTypes); + + result.applyToChildren(jsonRule.has("applyToChildren") ? jsonRule.getBoolean("applyToChildren") : false); + + result.setExecuteAsynchronously(jsonRule.has("executeAsynchronously") ? jsonRule.getBoolean("executeAsynchronously") : false); + + result.setRuleDisabled(jsonRule.has("disabled") ? jsonRule.getBoolean("disabled") : false); + + JSONObject jsonAction = jsonRule.getJSONObject("action"); + + // parse action object + Action ruleAction = parseJsonAction(jsonAction); + + result.setAction(ruleAction); + + return result; + } + + protected ActionImpl parseJsonAction(JSONObject jsonAction) throws JSONException + { + ActionImpl result = null; + + String actionId = jsonAction.has("id") ? jsonAction.getString("id") : GUID.generate(); + + if (jsonAction.getString("actionDefinitionName").equalsIgnoreCase("composite-action")) + { + result = new CompositeActionImpl(null, actionId); + } + else + { + result = new ActionImpl(null, actionId, jsonAction.getString("actionDefinitionName")); + } + + // Post Action Queue parameter + if (jsonAction.has("actionedUponNode")) + { + NodeRef actionedUponNode = new NodeRef(jsonAction.getString("actionedUponNode")); + result.setNodeRef(actionedUponNode); + } + + if (jsonAction.has("description")) + { + result.setDescription(jsonAction.getString("description")); + } + + if (jsonAction.has("title")) + { + result.setTitle(jsonAction.getString("title")); + } + + if (jsonAction.has("parameterValues")) + { + JSONObject jsonParameterValues = jsonAction.getJSONObject("parameterValues"); + result.setParameterValues(parseJsonParameterValues(jsonParameterValues, result.getActionDefinitionName(), true)); + } + + if (jsonAction.has("executeAsync")) + { + result.setExecuteAsynchronously(jsonAction.getBoolean("executeAsync")); + } + + if (jsonAction.has("runAsUser")) + { + result.setRunAsUser(jsonAction.getString("runAsUser")); + } + + if (jsonAction.has("actions")) + { + JSONArray jsonActions = jsonAction.getJSONArray("actions"); + + for (int i = 0; i < jsonActions.length(); i++) + { + JSONObject innerJsonAction = jsonActions.getJSONObject(i); + + Action innerAction = parseJsonAction(innerJsonAction); + + // we assume that only composite-action contains actions json array, so should be no cast exception + ((CompositeActionImpl) result).addAction(innerAction); + } + } + + if (jsonAction.has("conditions")) + { + JSONArray jsonConditions = jsonAction.getJSONArray("conditions"); + + for (int i = 0; i < jsonConditions.length(); i++) + { + JSONObject jsonCondition = jsonConditions.getJSONObject(i); + + // parse action conditions + ActionCondition actionCondition = parseJsonActionCondition(jsonCondition); + + result.getActionConditions().add(actionCondition); + } + } + + if (jsonAction.has("compensatingAction")) + { + Action compensatingAction = parseJsonAction(jsonAction.getJSONObject("compensatingAction")); + result.setCompensatingAction(compensatingAction); + } + + return result; + } + + protected ActionConditionImpl parseJsonActionCondition(JSONObject jsonActionCondition) throws JSONException + { + String id = jsonActionCondition.has("id") ? jsonActionCondition.getString("id") : GUID.generate(); + + ActionConditionImpl result = new ActionConditionImpl(id, jsonActionCondition.getString("conditionDefinitionName")); + + if (jsonActionCondition.has("invertCondition")) + { + result.setInvertCondition(jsonActionCondition.getBoolean("invertCondition")); + } + + if (jsonActionCondition.has("parameterValues")) + { + JSONObject jsonParameterValues = jsonActionCondition.getJSONObject("parameterValues"); + + result.setParameterValues(parseJsonParameterValues(jsonParameterValues, result.getActionConditionDefinitionName(), false)); + } + + return result; + } + + protected Map parseJsonParameterValues(JSONObject jsonParameterValues, String name, boolean isAction) throws JSONException + { + Map parameterValues = new HashMap(); + + // get parameters names + JSONArray names = jsonParameterValues.names(); + if (names == null) + { + return null; + } + + // Get the action or condition definition + ParameterizedItemDefinition definition = null; + if (isAction == true) + { + definition = actionService.getActionDefinition(name); + } + else + { + definition = actionService.getActionConditionDefinition(name); + } + if (definition == null) + { + throw new AlfrescoRuntimeException("Could not find defintion for action/condition " + name); + } + + for (int i = 0; i < names.length(); i++) + { + String propertyName = names.getString(i); + Object propertyValue = jsonParameterValues.get(propertyName); + + // Get the parameter definition we care about + ParameterDefinition paramDef = definition.getParameterDefintion(propertyName); + if (paramDef == null && !definition.getAdhocPropertiesAllowed()) + { + throw new AlfrescoRuntimeException("Invalid parameter " + propertyName + " for action/condition " + name); + } + if (paramDef != null) + { + QName typeQName = paramDef.getType(); + + // Convert the property value + Serializable value = convertValue(typeQName, propertyValue); + parameterValues.put(propertyName, value); + } + else + { + // If there is no parameter definition we can only rely on the .toString() representation of the ad-hoc property + parameterValues.put(propertyName, propertyValue.toString()); + } + + } + + return parameterValues; + } + + private Serializable convertValue(QName typeQName, Object propertyValue) throws JSONException + { + Serializable value = null; + + DataTypeDefinition typeDef = dictionaryService.getDataType(typeQName); + if (typeDef == null) + { + throw new AlfrescoRuntimeException("Action property type definition " + typeQName.toPrefixString() + " is unknown."); + } + + if (propertyValue instanceof JSONArray) + { + // Convert property type to java class + Class javaClass = null; + + String javaClassName = typeDef.getJavaClassName(); + try + { + javaClass = Class.forName(javaClassName); + } + catch (ClassNotFoundException e) + { + throw new DictionaryException("Java class " + javaClassName + " of property type " + typeDef.getName() + " is invalid", e); + } + + int length = ((JSONArray)propertyValue).length(); + List list = new ArrayList(length); + for (int i = 0; i < length; i++) + { + list.add(convertValue(typeQName, ((JSONArray)propertyValue).get(i))); + } + value = (Serializable)list; + } + else + { + if (typeQName.equals(DataTypeDefinition.QNAME) == true && + typeQName.toString().contains(":") == true) + { + value = QName.createQName(propertyValue.toString(), namespaceService); + } + else + { + value = (Serializable)DefaultTypeConverter.INSTANCE.convert(dictionaryService.getDataType(typeQName), propertyValue); + } + } + + return value; + } + + protected void checkRule(Rule rule) + { + List ruleTypes = rule.getRuleTypes(); + if (ruleTypes.contains(RULE_OUTBOUND)) + { + List actions = ((CompositeActionImpl) rule.getAction()).getActions(); + for (Action action : actions) + { + if (action.getActionDefinitionName().equalsIgnoreCase(ACTION_CHECK_OUT)) + { + throw new WebScriptException(CANNOT_CREATE_RULE); + } + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConditionDefinitionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConditionDefinitionsGet.java new file mode 100644 index 00000000000..1dc47f567c2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConditionDefinitionsGet.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.action.ActionConditionDefinition; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class ActionConditionDefinitionsGet extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(ActionConditionDefinitionsGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get all action condition definitions + List actionconditiondefinitions = actionService.getActionConditionDefinitions(); + + model.put("actionconditiondefinitions", actionconditiondefinitions); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConstraintGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConstraintGet.java new file mode 100644 index 00000000000..82a4128bccd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConstraintGet.java @@ -0,0 +1,72 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.action.ParameterConstraint; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class ActionConstraintGet extends AbstractRuleWebScript +{ + + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(ActionConstraintGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + Map templateVars = req.getServiceMatch().getTemplateVars(); + String name = templateVars.get("name"); + + // get specified parameter constraint + ParameterConstraint parameterConstraint = actionService.getParameterConstraint(name); + + if (parameterConstraint == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find parameter constraint with name: " + name); + } + + model.put("actionConstraint", parameterConstraint); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConstraintsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConstraintsGet.java new file mode 100644 index 00000000000..a256541d4f0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionConstraintsGet.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.action.ParameterConstraint; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class ActionConstraintsGet extends AbstractRuleWebScript +{ + + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(ActionConstraintsGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + String[] names = req.getParameterValues("name"); + + List parameterConstraints = null; + + if (names != null && names.length > 0) + { + // filter is present in request + parameterConstraints = new ArrayList(); + + // find specified parameter constraints + for (String name : names) + { + ParameterConstraint parameterConstraint = actionService.getParameterConstraint(name); + + if (parameterConstraint != null) + { + parameterConstraints.add(parameterConstraint); + } + } + } + else + { + // no filter was provided, return all parameter constraints + parameterConstraints = actionService.getParameterConstraints(); + } + + model.put("actionConstraints", parameterConstraints); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionDefinitionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionDefinitionsGet.java new file mode 100644 index 00000000000..4c0bba74482 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionDefinitionsGet.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.action.ActionDefinition; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class ActionDefinitionsGet extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(ActionDefinitionsGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get all action definitions + List actiondefinitions = actionService.getActionDefinitions(); + + model.put("actiondefinitions", actiondefinitions); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionQueuePost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionQueuePost.java new file mode 100644 index 00000000000..496ba034895 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ActionQueuePost.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class ActionQueuePost extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(ActionQueuePost.class); + + public static final String STATUS = "actionExecStatus"; + public static final String STATUS_SUCCESS = "success"; + public static final String STATUS_FAIL = "fail"; + public static final String STATUS_QUEUED = "queued"; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + boolean async = Boolean.parseBoolean(req.getParameter("async")); + + ActionImpl action = null; + JSONObject json = null; + + try + { + // read request json + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // parse request json + action = parseJsonAction(json); + NodeRef actionedUponNode = action.getNodeRef(); + + // clear nodeRef for action + action.setNodeRef(null); + json.remove("actionedUponNode"); + + if (async) + { + model.put(STATUS, STATUS_QUEUED); + } + else + { + model.put(STATUS, STATUS_SUCCESS); + } + + // Execute action + actionService.executeAction(action, actionedUponNode, true, async); + + // Prepair model + model.put("actionedUponNode", actionedUponNode.toString()); + model.put("action", json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/InheritedRulesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/InheritedRulesGet.java new file mode 100644 index 00000000000..56ada32364f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/InheritedRulesGet.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.web.scripts.rule.ruleset.RuleRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleType; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class InheritedRulesGet extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(InheritedRulesGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + NodeRef nodeRef = parseRequestForNodeRef(req); + String ruleType = req.getParameter("ruleType"); + + RuleType type = ruleService.getRuleType(ruleType); + + if (type == null) + { + ruleType = null; + } + + // get all rules (including inherited) filtered by rule type + List inheritedRules = ruleService.getRules(nodeRef, true, ruleType); + + // get owned rules (excluding inherited) filtered by rule type + List ownedRules = ruleService.getRules(nodeRef, false, ruleType); + + // remove owned rules + inheritedRules.removeAll(ownedRules); + + List inheritedRuleRefs = new ArrayList(); + + for (Rule rule : inheritedRules) + { + inheritedRuleRefs.add(new RuleRef(rule, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(rule)))); + } + + model.put("inheritedRuleRefs", inheritedRuleRefs); + + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleDelete.java new file mode 100644 index 00000000000..efc0a19247f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleDelete.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RuleDelete extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RuleDelete.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + NodeRef nodeRef = parseRequestForNodeRef(req); + + // get request parameters + Map templateVars = req.getServiceMatch().getTemplateVars(); + String ruleId = templateVars.get("rule_id"); + + Rule ruleToDelete = null; + + // get all rules for given nodeRef + List rules = ruleService.getRules(nodeRef, false); + + // filter by rule id + for (Rule rule : rules) + { + if (rule.getNodeRef().getId().equalsIgnoreCase(ruleId)) + { + ruleToDelete = rule; + break; + } + } + + if (ruleToDelete == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find rule with id: " + ruleId); + } + + // delete rule + ruleService.removeRule(nodeRef, ruleToDelete); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleGet.java new file mode 100644 index 00000000000..122bdbb48f1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleGet.java @@ -0,0 +1,90 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.web.scripts.rule.ruleset.RuleRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RuleGet extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RuleGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + NodeRef nodeRef = parseRequestForNodeRef(req); + + // get request parameters + Map templateVars = req.getServiceMatch().getTemplateVars(); + String ruleId = templateVars.get("rule_id"); + + Rule ruleToReturn = null; + + // get all rules for given nodeRef + List rules = ruleService.getRules(nodeRef); + + // filter by rule id + for (Rule rule : rules) + { + if (rule.getNodeRef().getId().equalsIgnoreCase(ruleId)) + { + ruleToReturn = rule; + break; + } + } + + if (ruleToReturn == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find rule with id: " + ruleId); + } + + RuleRef ruleRefToReturn = new RuleRef(ruleToReturn, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(ruleToReturn))); + + model.put("ruleRef", ruleRefToReturn); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulePost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulePost.java new file mode 100644 index 00000000000..a84a1df9ea4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulePost.java @@ -0,0 +1,94 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.web.scripts.rule.ruleset.RuleRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RulePost extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RulePost.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + NodeRef nodeRef = parseRequestForNodeRef(req); + + Rule rule = null; + JSONObject json = null; + + try + { + // read request json + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // parse request json + rule = parseJsonRule(json); + + // check the rule + checkRule(rule); + + // create rule + ruleService.saveRule(nodeRef, rule); + + RuleRef ruleRef = new RuleRef(rule, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(rule))); + + model.put("ruleRef", ruleRef); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulePut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulePut.java new file mode 100644 index 00000000000..251a3513066 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulePut.java @@ -0,0 +1,376 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.repo.web.scripts.rule.ruleset.RuleRef; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RulePut extends RulePost +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RulePut.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + NodeRef nodeRef = parseRequestForNodeRef(req); + + Map templateVars = req.getServiceMatch().getTemplateVars(); + String ruleId = templateVars.get("rule_id"); + + Rule ruleToUpdate = null; + + // get all rules for given nodeRef + List rules = ruleService.getRules(nodeRef); + + //filter by rule id + for (Rule rule : rules) + { + if (rule.getNodeRef().getId().equalsIgnoreCase(ruleId)) + { + ruleToUpdate = rule; + break; + } + } + + if (ruleToUpdate == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find rule with id: " + ruleId); + } + + JSONObject json = null; + + try + { + // read request json + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // parse request json + updateRuleFromJSON(json, ruleToUpdate); + + // check the rule + checkRule(ruleToUpdate); + + // save changes + ruleService.saveRule(nodeRef, ruleToUpdate); + + RuleRef updatedRuleRef = new RuleRef(ruleToUpdate, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(ruleToUpdate))); + + model.put("ruleRef", updatedRuleRef); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + + return model; + } + + protected void updateRuleFromJSON(JSONObject jsonRule, Rule ruleToUpdate) throws JSONException + { + if (jsonRule.has("title")) + { + ruleToUpdate.setTitle(jsonRule.getString("title")); + } + + if (jsonRule.has("description")) + { + ruleToUpdate.setDescription(jsonRule.getString("description")); + } + + if (jsonRule.has("ruleType")) + { + JSONArray jsonTypes = jsonRule.getJSONArray("ruleType"); + List types = new ArrayList(); + + for (int i = 0; i < jsonTypes.length(); i++) + { + types.add(jsonTypes.getString(i)); + } + ruleToUpdate.setRuleTypes(types); + } + + if (jsonRule.has("applyToChildren")) + { + ruleToUpdate.applyToChildren(jsonRule.getBoolean("applyToChildren")); + } + + if (jsonRule.has("executeAsynchronously")) + { + ruleToUpdate.setExecuteAsynchronously(jsonRule.getBoolean("executeAsynchronously")); + } + + if (jsonRule.has("disabled")) + { + ruleToUpdate.setRuleDisabled(jsonRule.getBoolean("disabled")); + } + + if (jsonRule.has("action")) + { + JSONObject jsonAction = jsonRule.getJSONObject("action"); + + // update rule action + Action action = updateActionFromJson(jsonAction, (ActionImpl) ruleToUpdate.getAction()); + + ruleToUpdate.setAction(action); + } + } + + protected Action updateActionFromJson(JSONObject jsonAction, ActionImpl actionToUpdate) throws JSONException + { + ActionImpl result = null; + + if (jsonAction.has("id")) + { + // update existing action + result = actionToUpdate; + } + else + { + // create new object as id was not sent by client + result = parseJsonAction(jsonAction); + return result; + } + + if (jsonAction.has("description")) + { + result.setDescription(jsonAction.getString("description")); + } + + if (jsonAction.has("title")) + { + result.setTitle(jsonAction.getString("title")); + } + + if (jsonAction.has("parameterValues")) + { + JSONObject jsonParameterValues = jsonAction.getJSONObject("parameterValues"); + result.setParameterValues(parseJsonParameterValues(jsonParameterValues, result.getActionDefinitionName(), true)); + } + + if (jsonAction.has("executeAsync")) + { + result.setExecuteAsynchronously(jsonAction.getBoolean("executeAsync")); + } + + if (jsonAction.has("runAsUser")) + { + result.setRunAsUser(jsonAction.getString("runAsUser")); + } + + if (jsonAction.has("actions")) + { + JSONArray jsonActions = jsonAction.getJSONArray("actions"); + if (jsonActions.length() == 0) + { + // empty array was sent -> clear list + ((CompositeActionImpl) result).getActions().clear(); + } + else + { + List existingActions = ((CompositeActionImpl) result).getActions(); + List newActions = new ArrayList(); + + for (int i = 0; i < jsonActions.length(); i++) + { + JSONObject innerJsonAction = jsonActions.getJSONObject(i); + + if (innerJsonAction.has("id")) + { + // update existing object + String actionId = innerJsonAction.getString("id"); + + Action existingAction = getAction(existingActions, actionId); + existingActions.remove(existingAction); + + Action updatedAction = updateActionFromJson(innerJsonAction, (ActionImpl) existingAction); + newActions.add(updatedAction); + } + else + { + //create new action as id was not sent + newActions.add(parseJsonAction(innerJsonAction)); + } + } + existingActions.clear(); + + for (Action action : newActions) + { + existingActions.add(action); + } + } + } + + if (jsonAction.has("conditions")) + { + JSONArray jsonConditions = jsonAction.getJSONArray("conditions"); + + if (jsonConditions.length() == 0) + { + // empty array was sent -> clear list + result.getActionConditions().clear(); + } + else + { + List existingConditions = result.getActionConditions(); + List newConditions = new ArrayList(); + + for (int i = 0; i < jsonConditions.length(); i++) + { + JSONObject jsonCondition = jsonConditions.getJSONObject(i); + + if (jsonCondition.has("id")) + { + // update existing object + String conditionId = jsonCondition.getString("id"); + + ActionCondition existingCondition = getCondition(existingConditions, conditionId); + existingConditions.remove(existingCondition); + + ActionCondition updatedActionCondition = updateActionConditionFromJson(jsonCondition, (ActionConditionImpl) existingCondition); + newConditions.add(updatedActionCondition); + } + else + { + // create new object as id was not sent + newConditions.add(parseJsonActionCondition(jsonCondition)); + } + } + + existingConditions.clear(); + + for (ActionCondition condition : newConditions) + { + existingConditions.add(condition); + } + } + } + + if (jsonAction.has("compensatingAction")) + { + JSONObject jsonCompensatingAction = jsonAction.getJSONObject("compensatingAction"); + Action compensatingAction = updateActionFromJson(jsonCompensatingAction, (ActionImpl) actionToUpdate.getCompensatingAction()); + + actionToUpdate.setCompensatingAction(compensatingAction); + } + return result; + } + + protected ActionCondition updateActionConditionFromJson(JSONObject jsonCondition, ActionConditionImpl conditionToUpdate) throws JSONException + { + ActionConditionImpl result = null; + + if (jsonCondition.has("id")) + { + // update exiting object + result = conditionToUpdate; + } + else + { + // create new onject as id was not sent + result = parseJsonActionCondition(jsonCondition); + return result; + } + + if (jsonCondition.has("invertCondition")) + { + result.setInvertCondition(jsonCondition.getBoolean("invertCondition")); + } + + if (jsonCondition.has("parameterValues")) + { + JSONObject jsonParameterValues = jsonCondition.getJSONObject("parameterValues"); + result.setParameterValues(parseJsonParameterValues(jsonParameterValues, result.getActionConditionDefinitionName(), false)); + } + + return result; + } + + private Action getAction(List actions, String id) + { + Action result = null; + for (Action action : actions) + { + if (action.getId().equalsIgnoreCase(id)) + { + result = action; + break; + } + } + + return result; + } + + private ActionCondition getCondition(List conditions, String id) + { + ActionCondition result = null; + for (ActionCondition condition : conditions) + { + if (condition.getId().equalsIgnoreCase(id)) + { + result = condition; + break; + } + } + + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleTypesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleTypesGet.java new file mode 100644 index 00000000000..828afa26ad5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RuleTypesGet.java @@ -0,0 +1,61 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.rule.RuleType; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RuleTypesGet extends AbstractRuleWebScript +{ + + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RuleTypesGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get all rule types + List ruletypes = ruleService.getRuleTypes(); + + model.put("ruletypes", ruletypes); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulesGet.java new file mode 100644 index 00000000000..1d69ccdf668 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulesGet.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.web.scripts.rule.ruleset.RuleRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleType; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RulesGet extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RulesGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + NodeRef nodeRef = parseRequestForNodeRef(req); + String ruleType = req.getParameter("ruleType"); + + RuleType type = ruleService.getRuleType(ruleType); + + if (type == null) + { + ruleType = null; + } + + // get all rules (excluding inherited) filtered by rule type + List rules = ruleService.getRules(nodeRef, false, ruleType); + + List ruleRefs = new ArrayList(); + + for (Rule rule : rules) + { + ruleRefs.add(new RuleRef(rule, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(rule)))); + } + + model.put("ruleRefs", ruleRefs); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulesetGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulesetGet.java new file mode 100644 index 00000000000..a7115c52025 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/RulesetGet.java @@ -0,0 +1,106 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.web.scripts.rule.ruleset.RuleRef; +import org.alfresco.repo.web.scripts.rule.ruleset.RuleSet; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleType; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * + */ +public class RulesetGet extends AbstractRuleWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RulesetGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // get request parameters + NodeRef nodeRef = parseRequestForNodeRef(req); + String ruleType = req.getParameter("ruleType"); + + RuleType type = ruleService.getRuleType(ruleType); + + if (type == null) + { + ruleType = null; + } + + RuleSet ruleset = new RuleSet(); + + // get all "owned" rules + List ownedRules = ruleService.getRules(nodeRef, false, ruleType); + + // get all rules (including inherited) + List inheritedRules = ruleService.getRules(nodeRef, true, ruleType); + + // remove "owned" rules + inheritedRules.removeAll(ownedRules); + + List rulesToSet = new ArrayList(); + + for (Rule rule : ownedRules) + { + rulesToSet.add(new RuleRef(rule, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(rule)))); + } + ruleset.setRules(rulesToSet); + + List inheritedRulesToSet = new ArrayList(); + + for (Rule rule : inheritedRules) + { + inheritedRulesToSet.add(new RuleRef(rule, fileFolderService.getFileInfo(ruleService.getOwningNodeRef(rule)))); + } + ruleset.setInheritedRules(inheritedRulesToSet); + + ruleset.setLinkedToRuleSet(ruleService.getLinkedToRuleNode(nodeRef)); + + ruleset.setLinkedFromRuleSets(ruleService.getLinkedFromRuleNodes(nodeRef)); + + ruleset.setRulesetNodeRef(nodeRef); + + model.put("ruleset", ruleset); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ruleset/RuleRef.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ruleset/RuleRef.java new file mode 100644 index 00000000000..088f435f954 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ruleset/RuleRef.java @@ -0,0 +1,92 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule.ruleset; + +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.rule.Rule; + +/** + * Rule object for REST API + * + * @author unknown + * + */ +public class RuleRef +{ + + /** Serial version UID */ + private static final long serialVersionUID = -923276130307938661L; + + private FileInfo owningFileInfo; + + private Rule rule; + + public RuleRef(Rule rule, FileInfo owningFileInfo) + { + this.rule = rule; + this.owningFileInfo = owningFileInfo; + } + + /** + * Set the rule + * + * @param rule the rule to set + */ + public void setRule(Rule rule) + { + this.rule = rule; + } + + /** + * Return the rule + * + * @return rule + */ + public Rule getRule() + { + return rule; + } + + /** + * Set the owning file info reference for rule + * + * @param owningFileInfo the owning file info reference to set + */ + public void setOwningFileInfo(FileInfo owningFileInfo) + { + this.owningFileInfo = owningFileInfo; + } + + /** + * Returns the owning file info reference for a rule. + * + * @return the owning file info reference + */ + public FileInfo getOwningFileInfo() + { + return owningFileInfo; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ruleset/RuleSet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ruleset/RuleSet.java new file mode 100644 index 00000000000..3b73a68ce87 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/ruleset/RuleSet.java @@ -0,0 +1,152 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.rule.ruleset; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author unknown + * + */ +public class RuleSet implements Serializable +{ + + private static final long serialVersionUID = 6985140035928444095L; + + private List rules = null; + + private List inheritedRules = null; + + private NodeRef rulesetNodeRef; + + private NodeRef linkedToRuleSet; + + private List linkedFromRuleSets; + + /** + * Set list of the rules "owned" by this rule set + * + * @param rules the list of rules to set + */ + public void setRules(List rules) + { + this.rules = rules; + } + + /** + * Get list of the rules "owned" by this rule set + * + * @return list of "owned" rules + */ + public List getRules() + { + return rules; + } + + /** + * Set list of the rules inherited by this rule set from parent + * + * @param inheritedRules the list of inherited rules to set + */ + public void setInheritedRules(List inheritedRules) + { + this.inheritedRules = inheritedRules; + } + + /** + * Get list of the rules inherited by this rule set from parent + * + * @return list of inherited rules + */ + public List getInheritedRules() + { + return inheritedRules; + } + + /** + * Set the nodeRef to which this ruleset belongs + * + * @param rulesetNodeRef the ruleset nodeRef to set + */ + public void setRulesetNodeRef(NodeRef rulesetNodeRef) + { + this.rulesetNodeRef = rulesetNodeRef; + } + + /** + * Get the nodeRef to which this ruleset belongs + * + * @return ruleset nodeRef + */ + public NodeRef getRulesetNodeRef() + { + return rulesetNodeRef; + } + + /** + * Set the nodeRef to which this ruleset linked to + * + * @param linkedToRuleSet the nodeRef to set + */ + public void setLinkedToRuleSet(NodeRef linkedToRuleSet) + { + this.linkedToRuleSet = linkedToRuleSet; + } + + /** + * Get the nodeRef to which this ruleset linked to + * + * @return linked to nodeRef + */ + public NodeRef getLinkedToRuleSet() + { + return linkedToRuleSet; + } + + /** + * Set the list of nodeRefs that link to this ruleset + * + * @param linkedFromRuleSets the list of nodeRefs to set + */ + public void setLinkedFromRuleSets(List linkedFromRuleSets) + { + this.linkedFromRuleSets = linkedFromRuleSets; + } + + /** + * Get the list of nodeRefs that link to this ruleset + * + * @return the list of nodeRefs + */ + public List getLinkedFromRuleSets() + { + return linkedFromRuleSets; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java new file mode 100644 index 00000000000..86c3eb89900 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/BasicHttpAuthenticatorFactory.java @@ -0,0 +1,331 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.Authorization; +import org.alfresco.repo.web.auth.AuthenticationListener; +import org.alfresco.repo.web.auth.BasicAuthCredentials; +import org.alfresco.repo.web.auth.GuestCredentials; +import org.alfresco.repo.web.auth.TicketCredentials; +import org.alfresco.repo.web.auth.WebCredentials; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.Base64; +import org.springframework.extensions.webscripts.Authenticator; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.servlet.ServletAuthenticatorFactory; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletResponse; + +/** + * HTTP Basic Authentication + * + * @author davidc + */ +public class BasicHttpAuthenticatorFactory implements ServletAuthenticatorFactory +{ + // Logger + private static Log logger = LogFactory.getLog(BasicHttpAuthenticator.class); + + // Component dependencies + protected AuthenticationService authenticationService; + protected AuthorityService authorityService; + protected AuthenticationListener listener; + + + /** + * @param authenticationService AuthenticationService + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * Set the listener for authentication events, generated by {@link BasicHttpAuthenticator} instances. + */ + public void setAuthenticationListener(AuthenticationListener listener) + { + this.listener = listener; + } + + public AuthorityService getAuthorityService() + { + return authorityService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.servlet.ServletAuthenticatorFactory#create(org.alfresco.web.scripts.servlet.WebScriptServletRequest, org.alfresco.web.scripts.servlet.WebScriptServletResponse) + */ + public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res) + { + return new BasicHttpAuthenticator(req, res, listener); + } + + /** + * Generate a {@link WebCredentials} instance from information contained in auth. + */ + private WebCredentials getCredentials(Authorization auth) + { + if (auth.isTicket()) + { + return new TicketCredentials(auth.getTicket()); + } + else + { + return new BasicAuthCredentials(auth.getUserName(), auth.getPassword()); + } + + } + + + /** + * HTTP Basic Authentication + * + * @author davidc + */ + public class BasicHttpAuthenticator implements Authenticator + { + // dependencies + protected WebScriptServletRequest servletReq; + protected WebScriptServletResponse servletRes; + + protected String authorization; + protected String ticket; + protected AuthenticationListener listener; + + /** + * Construct + * + * @param req WebScriptServletRequest + * @param res WebScriptServletResponse + * @param listener AuthenticationListener + */ + public BasicHttpAuthenticator(WebScriptServletRequest req, WebScriptServletResponse res, AuthenticationListener listener) + { + this.servletReq = req; + this.servletRes = res; + + HttpServletRequest httpReq = servletReq.getHttpServletRequest(); + + this.listener = listener; + + this.authorization = httpReq.getHeader("Authorization"); + this.ticket = httpReq.getParameter("alf_ticket"); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Authenticator#authenticate(org.alfresco.web.scripts.Description.RequiredAuthentication, boolean) + */ + public boolean authenticate(RequiredAuthentication required, boolean isGuest) + { + boolean authorized = false; + + // + // validate credentials + // + + HttpServletResponse res = servletRes.getHttpServletResponse(); + + if (logger.isDebugEnabled()) + { + logger.debug("HTTP Authorization provided: " + (authorization != null && authorization.length() > 0)); + logger.debug("URL ticket provided: " + (ticket != null && ticket.length() > 0)); + } + + boolean doNotReportUrlTicketAuthenticationFailed = true; + + // If they requested explicit guest authentication, + // Authenticate as guest (if allowed) + if (isGuest && RequiredAuthentication.guest == required) + { + if (logger.isDebugEnabled()) + logger.debug("Authenticating as Guest"); + + try + { + authenticationService.authenticateAsGuest(); + listener.userAuthenticated(new GuestCredentials()); + authorized = true; + } + catch (AuthenticationException ex) + { + // failed authentication + listener.authenticationFailed(new GuestCredentials()); + } + } + + // authenticate as specified by explicit ticket on url + else if ((null != ticket) && (ticket.length() > 0) && (doNotReportUrlTicketAuthenticationFailed = isTicketValid())) + { + if (logger.isDebugEnabled()) + { + logger.debug("Authenticating (URL argument) ticket " + ticket); + } + + // assume a ticket has been passed + listener.userAuthenticated(new TicketCredentials(ticket)); + authorized = true; + } + + // authenticate as specified by HTTP Basic Authentication + else if (authorization != null && authorization.length() > 0) + { + String[] authorizationParts = authorization.split(" "); + if (!authorizationParts[0].equalsIgnoreCase("basic")) + { + throw new WebScriptException(401, "Authorization '" + authorizationParts[0] + "' not supported."); + } + + String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); + Authorization auth = new Authorization(decodedAuthorisation); + try + { + if (auth.isTicket()) + { + if (logger.isDebugEnabled()) + logger.debug("Authenticating (BASIC HTTP) ticket " + auth.getTicket()); + + // assume a ticket has been passed + authenticationService.validate(auth.getTicket()); + listener.userAuthenticated(getCredentials(auth)); + authorized = true; + } + else + { + if (logger.isDebugEnabled()) + logger.debug("Authenticating (BASIC HTTP) user " + auth.getUserName()); + + // No longer need a special call to authenticate as guest + // Leave guest name resolution up to the services + authenticationService.authenticate(auth.getUserName(), auth.getPassword().toCharArray()); + listener.userAuthenticated(getCredentials(auth)); + authorized = true; + } + } + catch(AuthenticationException e) + { + // failed authentication + listener.authenticationFailed(getCredentials(auth)); + } + } + + // + // request credentials if not authorized + // + + if (!authorized) + { + if(!doNotReportUrlTicketAuthenticationFailed) + { + listener.authenticationFailed(new TicketCredentials(ticket)); + } + + if (logger.isDebugEnabled()) + logger.debug("Requesting authorization credentials"); + + res.setStatus(401); + res.setHeader("WWW-Authenticate", "Basic realm=\"Alfresco\""); + } + return authorized; + } + + /** + * Checks if a user ticket is still valid + * + * @return {@link Boolean} value: true if the ticket is still valid, false if the ticket is not valid any more + */ + private boolean isTicketValid() + { + try + { + authenticationService.validate(ticket); + return true; + } + catch (AuthenticationException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("User ticket is not valid. Passing to the Basic authentication handling. Reqeust information:\n" + + " ticket: " + ticket + "\n" + + " request: " + servletReq.getQueryString() + "\n" + + " error: " + e, e); + } + + return false; + } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Authenticator#emptyCredentials() + */ + public boolean emptyCredentials() + { + return ((ticket == null || ticket.length() == 0) && (authorization == null || authorization.length() == 0)); + } + + protected boolean isBasicAuthHeaderPresentForAdmin() + { + if (authorization == null || authorization.isEmpty()) + { + return false; + } + String[] authorizationParts = authorization.split(" "); + if (!authorizationParts[0].equalsIgnoreCase("basic")) + { + return false; + } + + String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); + Authorization auth = new Authorization(decodedAuthorisation); + if (auth.isTicket() || auth.getUserName() == null || auth.getUserName().isEmpty()) + { + return false; + } + // optimization: check the admin user name first + if (AuthenticationUtil.getAdminUserName().equals(auth.getUserName())) + { + return true; + } + // then check the admin group + return AuthenticationUtil.runAsSystem(() -> authorityService.isAdminAuthority(auth.getUserName())); + } + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java new file mode 100644 index 00000000000..e40e9a72ff0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java @@ -0,0 +1,428 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.servlet; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.alfresco.error.ExceptionStackUtil; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.management.subsystems.ActivateableBean; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.external.RemoteUserMapper; +import org.alfresco.repo.web.auth.AuthenticationListener; +import org.alfresco.repo.web.auth.TicketCredentials; +import org.alfresco.repo.web.auth.WebCredentials; +import org.alfresco.repo.webdav.auth.AuthenticationDriver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Authenticator; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.webscripts.WebScript; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletResponse; + +import net.sf.acegisecurity.DisabledException; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Authenticator to provide Remote User based Header authentication dropping back to Basic Auth otherwise. + * Statelessly authenticating via a secure header now does not require a Session so can be used with + * request-level load balancers which was not previously possible. + *

+ * @see web-scripts-application-context.xml and web.xml - bean id 'webscripts.authenticator.remoteuser' + *

+ * This authenticator can be bound to /service and does not require /wcservice (Session) mapping. + * + * @since 5.1 + * @author Kevin Roast + */ +public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactory +{ + private static Log logger = LogFactory.getLog(RemoteUserAuthenticatorFactory.class); + public static final long GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT = 10000L; // 10 sec + + protected RemoteUserMapper remoteUserMapper; + protected AuthenticationComponent authenticationComponent; + + private boolean alwaysAllowBasicAuthForAdminConsole = true; + List adminConsoleScriptFamilies; + long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT; + + public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper) + { + this.remoteUserMapper = remoteUserMapper; + } + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + public boolean isAlwaysAllowBasicAuthForAdminConsole() + { + return alwaysAllowBasicAuthForAdminConsole; + } + + public void setAlwaysAllowBasicAuthForAdminConsole(boolean alwaysAllowBasicAuthForAdminConsole) + { + this.alwaysAllowBasicAuthForAdminConsole = alwaysAllowBasicAuthForAdminConsole; + } + + public List getAdminConsoleScriptFamilies() + { + return adminConsoleScriptFamilies; + } + + public void setAdminConsoleScriptFamilies(List adminConsoleScriptFamilies) + { + this.adminConsoleScriptFamilies = adminConsoleScriptFamilies; + } + + public long getGetRemoteUserTimeoutMilliseconds() + { + return getRemoteUserTimeoutMilliseconds; + } + + public void setGetRemoteUserTimeoutMilliseconds(long getRemoteUserTimeoutMilliseconds) + { + this.getRemoteUserTimeoutMilliseconds = getRemoteUserTimeoutMilliseconds; + } + + @Override + public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res) + { + return new RemoteUserAuthenticator(req, res, this.listener); + } + + /** + * Remote User authenticator - adds header authentication onto Basic Auth. Stateless does not require Session. + * + * @author Kevin Roast + */ + public class RemoteUserAuthenticator extends BasicHttpAuthenticator + { + public RemoteUserAuthenticator(WebScriptServletRequest req, WebScriptServletResponse res, AuthenticationListener listener) + { + super(req, res, listener); + } + + @Override + public boolean authenticate(RequiredAuthentication required, boolean isGuest) + { + boolean authenticated = false; + + if (logger.isTraceEnabled()) + { + logger.trace("Authenticate level required: " + required + " is guest: " + isGuest); + } + + String userId = null; + if (isRemoteUserMapperActive()) + { + if (isAlwaysAllowBasicAuthForAdminConsole()) + { + final boolean useTimeoutForAdminAccessingAdminConsole = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest); + + if (useTimeoutForAdminAccessingAdminConsole && isBasicAuthHeaderPresentForAdmin()) + { + return callBasicAuthForAdminConsoleAccess(required, isGuest); + } + try + { + userId = getRemoteUserWithTimeout(useTimeoutForAdminAccessingAdminConsole); + } + catch (AuthenticationTimeoutException e) + { + //return basic auth challenge + return false; + } + } + else + { + // retrieve the remote user if configured and available - authenticate that user directly + userId = getRemoteUser(); + } + } + + if (userId != null) + { + try + { + authenticationComponent.setCurrentUser(userId); + listener.userAuthenticated(new TicketCredentials(authenticationService.getCurrentTicket())); + authenticated = true; + } + catch (AuthenticationException authErr) + { + // don't propagate if the user is disabled + Throwable disabledCause = ExceptionStackUtil.getCause(authErr, DisabledException.class); + if(disabledCause != null) + { + listener.authenticationFailed(new WebCredentials() {}); + } + else + { + throw authErr; + } + } + } + else + { + // is there a Session which might contain a valid user ticket? + HttpSession session = servletReq.getHttpServletRequest().getSession(false); + if (session != null) + { + try + { + SessionUser user = (SessionUser)session.getAttribute(AuthenticationDriver.AUTHENTICATION_USER); + if (user != null) + { + // Validate the ticket for the current SessionUser + authenticationService.validate(user.getTicket()); + if (logger.isDebugEnabled()) + { + logger.debug("Ticket is valid. Retaining cached user in session."); + } + listener.userAuthenticated(new TicketCredentials(user.getTicket())); + authenticated = true; + } + else + { + authenticated = super.authenticate(required, isGuest); + } + } + catch (AuthenticationException authErr) + { + if (logger.isDebugEnabled()) + { + logger.debug("An Authentication error occur. Removing User session.", authErr); + } + session.removeAttribute(AuthenticationDriver.AUTHENTICATION_USER); + session.invalidate(); + listener.authenticationFailed(new WebCredentials() {}); + } + } + else + { + authenticated = super.authenticate(required, isGuest); + } + } + return authenticated; + } + + private boolean callBasicAuthForAdminConsoleAccess(RequiredAuthentication required, boolean isGuest) + { + // return REST call, after a timeout/basic auth challenge + if (logger.isTraceEnabled()) + { + logger.trace("An Admin Console request has come in with Basic Auth headers present for an admin user."); + } + // In order to prompt for another password, in case it was not entered correctly, + // the output of this method should be returned by the calling "authenticate" method; + // This would also mean, that once the admin basic auth header is present, + // the authentication chain will not be used for the admin console access + return super.authenticate(required, isGuest); + } + + private boolean shouldUseTimeoutForAdminAccessingAdminConsole(RequiredAuthentication required, boolean isGuest) + { + boolean useTimeoutForAdminAccessingAdminConsole = RequiredAuthentication.admin.equals(required) && !isGuest && + servletReq.getServiceMatch() != null && isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()); + + if (logger.isTraceEnabled()) + { + logger.trace("Should ensure that the admins can login with basic auth: " + useTimeoutForAdminAccessingAdminConsole); + } + return useTimeoutForAdminAccessingAdminConsole; + } + + private boolean isRemoteUserMapperActive() + { + return remoteUserMapper != null && (!(remoteUserMapper instanceof ActivateableBean) || ((ActivateableBean) remoteUserMapper).isActive()); + } + + protected boolean isAdminConsoleWebScript(WebScript webScript) + { + if (webScript == null || adminConsoleScriptFamilies == null || webScript.getDescription() == null + || webScript.getDescription().getFamilys() == null) + { + return false; + } + + if (logger.isTraceEnabled()) + { + logger.trace("WebScript: " + webScript + " has these families: " + webScript.getDescription().getFamilys()); + } + + // intersect the "family" sets defined + Set families = new HashSet(webScript.getDescription().getFamilys()); + families.retainAll(adminConsoleScriptFamilies); + final boolean isAdminConsole = !families.isEmpty(); + + if (logger.isTraceEnabled() && isAdminConsole) + { + logger.trace("Detected an Admin Console webscript: " + webScript ); + } + + return isAdminConsole; + } + + protected String getRemoteUserWithTimeout(boolean useTimeout) throws AuthenticationTimeoutException + { + if (!useTimeout) + { + return getRemoteUser(); + } + + String returnedRemoteUser = null; + GetRemoteUserRunnable getRemoteUserRunnable = new GetRemoteUserRunnable(); + Thread workerGettingTheRemoteUser = new Thread(getRemoteUserRunnable); + workerGettingTheRemoteUser.start(); + try + { + synchronized (workerGettingTheRemoteUser) + { + workerGettingTheRemoteUser.join(getRemoteUserTimeoutMilliseconds); + } + } + catch (Exception e) + { + logger.warn("Exception trying to get the remote user: " + e.getMessage(), e); + } + + returnedRemoteUser = getRemoteUserRunnable.getReturnedRemoteUser(); + + if (workerGettingTheRemoteUser.isAlive()) + { + // we timed out + // we should request basic authentication as the chain can't be usable + cleanupThread(workerGettingTheRemoteUser); + + final String message = "Could not get the remote user in a reasonable time: " + getRemoteUserTimeoutMilliseconds + " milliseconds. " + + "Adjust the timeout property 'authentication.getRemoteUserTimeoutMilliseconds' if required."; + + if (logger.isWarnEnabled()) + { + logger.warn("Returning basic auth challenge for Admin Console. Cause: " + message); + } + HttpServletResponse res = servletRes.getHttpServletResponse(); + res.setStatus(401); + res.setHeader("WWW-Authenticate", "Basic realm=\"Alfresco\""); + + throw new AuthenticationTimeoutException(message); + } + return returnedRemoteUser; + } + + private void cleanupThread(Thread workerGettingTheRemoteUser) + { + try + { + // try to clean up the thread we created, to use resources optimally + workerGettingTheRemoteUser.interrupt(); + } + catch (Exception e) + { + // we can't really handle anything here + } + } + + /** + * Retrieve the remote user from servlet request header when using a secure connection. + * The RemoteUserMapper bean must be active and configured. + * + * @return remote user ID or null if not active or found + */ + protected String getRemoteUser() + { + String userId = null; + + // If the remote user mapper is configured, we may be able to map in an externally authenticated user + if (isRemoteUserMapperActive()) + { + userId = remoteUserMapper.getRemoteUser(this.servletReq.getHttpServletRequest()); + } + + logRemoteUserID(userId); + + return userId; + } + + private void logRemoteUserID(String userId) + { + if (logger.isDebugEnabled()) + { + String message = (userId == null) ? + "No external user ID in request." : + "Extracted external user ID from request: " + AuthenticationUtil.maskUsername(userId); + logger.debug(message); + } + } + + class GetRemoteUserRunnable implements Runnable + { + private volatile String returnedRemoteUser; + + @Override + public void run() + { + returnedRemoteUser = getRemoteUser(); + } + + public String getReturnedRemoteUser() + { + return returnedRemoteUser; + } + } + + } +} + +class AuthenticationTimeoutException extends Exception +{ + static final long serialVersionUID = -3387511013124229948L; + + public AuthenticationTimeoutException() + { + super(); + } + + public AuthenticationTimeoutException(String message) + { + super(message); + } + + public AuthenticationTimeoutException(String message, Throwable t) + { + super(message, t); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/AbstractSiteWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/AbstractSiteWebScript.java new file mode 100644 index 00000000000..8380efa6dd0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/AbstractSiteWebScript.java @@ -0,0 +1,84 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.site; + +import java.util.Map; + +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 4.0 + */ +public abstract class AbstractSiteWebScript extends DeclarativeWebScript +{ + protected SiteService siteService; + protected AuthorityService authorityService; + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + + protected static String buildSiteGroup(SiteInfo site) + { + return "GROUP_site_" + site.getShortName(); + } + + @Override + protected Map executeImpl(WebScriptRequest req, + Status status, Cache cache) + { + // Grab the site + String siteName = + req.getServiceMatch().getTemplateVars().get("shortname"); + SiteInfo site = siteService.getSite(siteName); + if (site == null) + { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Site found with that short name"); + } + + // Process + return executeImpl(site, req, status, cache); + } + protected abstract Map executeImpl(SiteInfo site, + WebScriptRequest req, Status status, Cache cache); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteAdminSitesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteAdminSitesGet.java new file mode 100644 index 00000000000..db1d2b33215 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteAdminSitesGet.java @@ -0,0 +1,181 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.site; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.node.getchildren.FilterProp; +import org.alfresco.repo.node.getchildren.FilterPropString; +import org.alfresco.repo.node.getchildren.FilterPropString.FilterTypeString; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.alfresco.util.ScriptPagingDetails; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the site-admin-sites.get web script. + * + * @author Jamal Kaabi-Mofrad + */ +public class SiteAdminSitesGet extends DeclarativeWebScript +{ + private static final String NAME_FILTER = "nf"; + private static final String MAX_ITEMS = "maxItems"; + private static final String SKIP_COUNT = "skipCount"; + private static final int DEFAULT_MAX_ITEMS_PER_PAGE = 50; + + private SiteService siteService; + private NodeService nodeService; + private PersonService personService; + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // check the current user access rights + if (!siteService.isSiteAdmin(currentUser)) + { + // Note: security, no message to indicate why + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Resource not found."); + } + + // Create paging + final ScriptPagingDetails paging = new ScriptPagingDetails(getIntParameter(req, MAX_ITEMS, + DEFAULT_MAX_ITEMS_PER_PAGE), getIntParameter(req, SKIP_COUNT, 0)); + + // request a total count of found items + paging.setRequestTotalCountMax(Integer.MAX_VALUE); + + final List filterProp = getFilterProperties(req.getParameter(NAME_FILTER)); + + final List> sortProps = new ArrayList>(); + sortProps.add(new Pair(ContentModel.PROP_NAME, true)); + + PagingResults pagingResults = AuthenticationUtil.runAs( + new AuthenticationUtil.RunAsWork>() + { + public PagingResults doWork() throws Exception + { + return siteService.listSites(filterProp, sortProps, paging); + } + }, AuthenticationUtil.getAdminUserName()); + + List result = pagingResults.getPage(); + List sites = new ArrayList(result.size()); + for (SiteInfo info : result) + { + sites.add(SiteState.create(info, + siteService.listMembers(info.getShortName(), null, SiteModel.SITE_MANAGER, 0), currentUser, + nodeService, personService)); + } + + Map sitesData = new HashMap(6); + + // Site data + sitesData.put("items", sites); + // Paging data + sitesData.put("count", result.size()); + sitesData.put("hasMoreItems", pagingResults.hasMoreItems()); + sitesData.put("totalItems", (pagingResults.getTotalResultCount() == null ? -1 : pagingResults.getTotalResultCount().getFirst())); + sitesData.put("skipCount", paging.getSkipCount()); + sitesData.put("maxItems", paging.getMaxItems()); + + // Create the model from the site and pagination data + Map model = new HashMap(1); + model.put("data", sitesData); + + return model; + } + + private int getIntParameter(WebScriptRequest req, String paramName, int defaultValue) + { + String paramString = req.getParameter(paramName); + + if (paramString != null) + { + try + { + int param = Integer.valueOf(paramString); + + if (param >= 0) + { + return param; + } + } + catch (NumberFormatException e) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); + } + } + + return defaultValue; + } + + private List getFilterProperties(String filter) + { + if (filter == null || filter.isEmpty() || filter.equals("*")) + { + return null; + } + List filterProps = new ArrayList(); + filterProps.add(new FilterPropString(ContentModel.PROP_NAME, filter, FilterTypeString.STARTSWITH_IGNORECASE)); + filterProps.add(new FilterPropString(ContentModel.PROP_TITLE, filter, FilterTypeString.STARTSWITH_IGNORECASE)); + return filterProps; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteExportGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteExportGet.java new file mode 100644 index 00000000000..5a537a896ca --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteExportGet.java @@ -0,0 +1,395 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.site; + +import java.io.File; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.repo.management.subsystems.ChildApplicationContextManager; +import org.alfresco.repo.security.authentication.RepositoryAuthenticationDao; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.ExporterService; +import org.alfresco.service.cmr.view.Location; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Exports a Site as a zip of ACPs. + * + * @author Nick Burch + * @since 3.5 + */ +public class SiteExportGet extends AbstractWebScript +{ + private static final List USERS_NOT_TO_EXPORT = Arrays.asList( + new String[] { "admin", "guest" }); + + private SiteService siteService; + private ExporterService exporterService; + private MimetypeService mimetypeService; + private AuthorityService authorityService; + private ChildApplicationContextManager authenticationContextManager; + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + // Grab the site + String siteName = + req.getServiceMatch().getTemplateVars().get("shortname"); + SiteInfo site = siteService.getSite(siteName); + if (site == null) + { + throw new WebScriptException( + Status.STATUS_NOT_FOUND, + "No Site found with that short name"); + } + + // Set things up to return them a zip file + res.setContentType( + MimetypeMap.MIMETYPE_ZIP); + + res.setHeader( + "Content-Disposition", + "attachment; fileName=" + siteName + "-export.zip"); + + ZipOutputStream mainZip = new ZipOutputStream( + res.getOutputStream()); + CloseIgnoringOutputStream outputForNesting = + new CloseIgnoringOutputStream(mainZip); + + // Export the Site's Contents + // This includes the site config such as dashboards + mainZip.putNextEntry(new ZipEntry("Contents.acp")); + doSiteACPExport(site, outputForNesting); + + // Export the person details for the users who are members of the site's groups + // Also includes the list of their site related groups + // + // If there are no users in this site (other than the built-ins like admin, guest) + // then include a special marker entry to that effect. + final List peopleNodes = getPersonNodesInSiteGroup(site); + if (peopleNodes.isEmpty()) + { + mainZip.putNextEntry(new ZipEntry("No_Persons_In_Site.txt")); + String text = "Person nodes were not exported because the site does not contain\n"+ + "any members other than the built-ins e.g. admin, guest."; + outputForNesting.write(text.getBytes("ASCII")); + } + else + { + mainZip.putNextEntry(new ZipEntry("People.acp")); + doPeopleACPExport(peopleNodes, site, outputForNesting); + } + + // Export the Site's groups listings + mainZip.putNextEntry(new ZipEntry("Groups.txt")); + doGroupExport(site, outputForNesting); + + // Export the User (authentication) details for the site users that have user(authenticator) nodes associated + // Only does this if the repository based authenticator is enabled + RepositoryAuthenticationDao authenticationDao = null; + for(String contextName : authenticationContextManager.getInstanceIds()) + { + ApplicationContext ctx = authenticationContextManager.getApplicationContext(contextName); + try + { + authenticationDao = (RepositoryAuthenticationDao) + ctx.getBean(RepositoryAuthenticationDao.class); + } catch(NoSuchBeanDefinitionException e) {} + } + List userNodes = getUserNodesInSiteGroup(site, authenticationDao); + //authenticationDao is initialized only when using a repository-based authentication subsystem + if (authenticationDao == null) + { + mainZip.putNextEntry(new ZipEntry("Users_Skipped_As_Wrong_Authentication.txt")); + String text = "Users were not exported because the Authentication\n"+ + "Subsystem you are using is not repository based"; + outputForNesting.write(text.getBytes("ASCII")); + } + else if (userNodes.isEmpty()) + { + mainZip.putNextEntry(new ZipEntry("No_Users_In_Site.txt")); + String text = "User nodes were not exported because the site does not contain\n"+ + "any members other than the built-ins e.g. admin, guest."; + outputForNesting.write(text.getBytes("ASCII")); + } + else + { + mainZip.putNextEntry(new ZipEntry("Users.acp")); + doUserACPExport(userNodes, site, outputForNesting); + } + + // Finish up + mainZip.close(); + } + + protected void doSiteACPExport(SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + // Build the parameters + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(new Location(site.getNodeRef())); + parameters.setCrawlChildNodes(true); + parameters.setCrawlSelf(true); + parameters.setCrawlContent(true); + + // And the export handler + ACPExportPackageHandler handler = new ACPExportPackageHandler( + writeTo, + new File(site.getShortName() + ".xml"), + new File(site.getShortName()), + mimetypeService); + + // Do the export + exporterService.exportView(handler, parameters, null); + } + + protected void doPeopleACPExport(final List peopleNodes, SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + if (!peopleNodes.isEmpty()) + { + // Build the parameters + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(new Location(peopleNodes.toArray(new NodeRef[peopleNodes.size()]))); + parameters.setCrawlChildNodes(true); + parameters.setCrawlSelf(true); + parameters.setCrawlContent(true); + + // And the export handler + ACPExportPackageHandler handler = new ACPExportPackageHandler( + writeTo, + new File(site.getShortName() + "-people.xml"), + new File(site.getShortName() + "-people"), + mimetypeService); + + // Do the export + exporterService.exportView(handler, parameters, null); + } + } + + /** + * Gets the NodeRefs for cm:person nodes in the specified site - excluding admin, guest. + * @since 4.1.5 + */ + private List getPersonNodesInSiteGroup(SiteInfo site) + { + // Find the root group + String siteGroup = AbstractSiteWebScript.buildSiteGroup(site); + + // Now get all people in the child groups + Set siteUsers = authorityService.getContainedAuthorities( + AuthorityType.USER, siteGroup, false); + + // Turn these all into NodeRefs + List peopleNodes = new ArrayList(siteUsers.size()); + for (String authority : siteUsers) + { + if (!USERS_NOT_TO_EXPORT.contains(authority)) + { + peopleNodes.add(authorityService.getAuthorityNodeRef(authority)); + } + } + return peopleNodes; + } + + /** + * Returns the user nodes (authentication nodes) if the authenticationDao exists + * @param site + * @param authenticationDao + * @return + */ + private List getUserNodesInSiteGroup(SiteInfo site, RepositoryAuthenticationDao authenticationDao) { + List userNodes = new ArrayList(); + if(authenticationDao == null) + { + return userNodes; + } + + // Identify all the users + String siteGroup = AbstractSiteWebScript.buildSiteGroup(site); + Set siteUsers = authorityService.getContainedAuthorities( + AuthorityType.USER, siteGroup, false); + + for (String user : siteUsers) + { + if (USERS_NOT_TO_EXPORT.contains(user)) + { + // Don't export these core users like admin + } + else + { + NodeRef userNodeRef = authenticationDao.getUserOrNull(user); + if(userNodeRef != null) + { + userNodes.add(userNodeRef); + } + } + } + return userNodes; + } + + protected void doGroupExport(SiteInfo site, CloseIgnoringOutputStream writeTo) throws IOException + { + // Find the root group + String siteGroup = AbstractSiteWebScript.buildSiteGroup(site); + + // Get all the child groups of the site (but not children of them) + Set siteGroups = authorityService.getContainedAuthorities( + AuthorityType.GROUP, siteGroup, true); + + // For each group, get all the people + // (Flattens any intermediate groups) + // Then, invert it to get the groups per person + Map> memberships = new HashMap>(); + for(String group : siteGroups) + { + Set groupUsers = authorityService.getContainedAuthorities( + AuthorityType.USER, group, false); + + for (String user : groupUsers) + { + if (!USERS_NOT_TO_EXPORT.contains(user)) + { + if (!memberships.containsKey(user)) + { + memberships.put(user, new ArrayList()); + } + memberships.get(user).add(group); + } + } + } + + // Do a simple text based export + // user=group1,group2 + PrintWriter out = new PrintWriter(new OutputStreamWriter(writeTo, "UTF-8")); + for (String user : memberships.keySet()) + { + out.print(user); + out.print('='); + + boolean first = true; + for (String group : memberships.get(user)) + { + if (first) + { + first = false; + } + else + { + out.print(','); + } + out.print(group); + } + out.println(); + } + out.close(); + } + + protected void doUserACPExport(List userNodes, SiteInfo site, + CloseIgnoringOutputStream writeTo) throws IOException + { + // Build the parameters + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(new Location(userNodes.toArray(new NodeRef[userNodes.size()]))); + parameters.setCrawlChildNodes(true); + parameters.setCrawlSelf(true); + parameters.setCrawlContent(true); + + // And the export handler + ACPExportPackageHandler handler = new ACPExportPackageHandler( + writeTo, + new File(site.getShortName() + "-users.xml"), + new File(site.getShortName() + "-users"), + mimetypeService); + + // Do the export + exporterService.exportView(handler, parameters, null); + } + + protected static class CloseIgnoringOutputStream extends FilterOutputStream + { + public CloseIgnoringOutputStream(OutputStream out) + { + super(out); + } + + @Override + public void close() throws IOException + { + // Flushes, but doesn't close + flush(); + } + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setExporterService(ExporterService exporterService) + { + this.exporterService = exporterService; + } + + public void setMimetypeService(MimetypeService mimetypeService) + { + this.mimetypeService = mimetypeService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setAuthenticationContextManager(ChildApplicationContextManager authenticationContextManager) + { + this.authenticationContextManager = authenticationContextManager; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteMembershipsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteMembershipsGet.java new file mode 100644 index 00000000000..b686c03c121 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteMembershipsGet.java @@ -0,0 +1,156 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.site; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.security.authority.script.ScriptAuthorityService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteMemberInfo; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Lists the members of a site, optionally filtering by name, role + * or authority type. + * + * Based on the old memberships.get.js webscript controller + * + * @author Nick Burch + * @since 4.0 + */ +public class SiteMembershipsGet extends AbstractSiteWebScript +{ + private PersonService personService; + private ScriptAuthorityService scriptAuthorityService; + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setScriptAuthorityService(ScriptAuthorityService scriptAuthorityService) + { + this.scriptAuthorityService = scriptAuthorityService; + } + + @Override + protected Map executeImpl(SiteInfo site, + WebScriptRequest req, Status status, Cache cache) + { + // Grab out filters + String nameFilter = req.getParameter("nf"); + if (nameFilter!=null) + { + nameFilter = nameFilter.endsWith("*") ? nameFilter : nameFilter+"*"; + } + String roleFilter = req.getParameter("rf"); + String authorityType = req.getParameter("authorityType"); + String sizeS = req.getParameter("size"); + boolean collapseGroups = false; + + // Sanity check the types + if(authorityType != null) + { + if("USER".equals(authorityType)) + { + collapseGroups = true; + } + else if("GROUP".equals(authorityType)) + { + // No extra settings needed + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "The Authority must be one of USER or GROUP"); + } + } + + // Figure out what to limit to, if anythign + int limit = 0; + if(sizeS != null) + { + try + { + limit = Integer.parseInt(sizeS); + } + catch(NumberFormatException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Invalid size specified"); + } + } + + // Fetch the membership details of the site + List members = this.siteService.listMembersInfo(site.getShortName(), + nameFilter, roleFilter, limit, collapseGroups); + + // Process it ready for FreeMarker + // Note that as usernames may be all numbers, we need to + // prefix them with an underscore otherwise FTL can get confused! + Map authorities = new HashMap(members.size()); + Map memberInfo = new LinkedHashMap(members.size()); + for (SiteMemberInfo authorityObj : members) + { + String ftlSafeName = "_" + authorityObj.getMemberName(); + + if (authorityObj.getMemberName().startsWith("GROUP_")) + { + if (authorityType == null || authorityType.equals("GROUP")) + { + // Record the details + authorities.put(ftlSafeName, scriptAuthorityService + .getGroupForFullAuthorityName(authorityObj.getMemberName())); + memberInfo.put(ftlSafeName, authorityObj); + } + } + else + { + if (authorityType == null || authorityType.equals("USER")) + { + // Record the details + authorities.put(ftlSafeName, + personService.getPerson(authorityObj.getMemberName())); + memberInfo.put(ftlSafeName, authorityObj); + } + } + } + + // Pass the details to freemarker + Map model = new HashMap(); + model.put("site", site); + model.put("authorities", authorities); + model.put("memberInfo", memberInfo); + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteState.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteState.java new file mode 100644 index 00000000000..856997466a6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/site/SiteState.java @@ -0,0 +1,130 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.site; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * A simple POJO class for the state of a site. For easier passing to the FTL model. + * + * @author jkaabimofrad + */ +public class SiteState +{ + + private SiteInfo siteInfo; + private List members; + private boolean currentUserSiteManager; + + private SiteState() + { + } + + public static SiteState create(SiteInfo siteInfo, Map members, String currentUser, + NodeService nodeService, PersonService personService) + { + SiteState result = new SiteState(); + result.members = new ArrayList(members.size()); + + result.siteInfo = siteInfo; + + boolean found = false; + Set siteMembers = members.keySet(); + for (String userName : siteMembers) + { + NodeRef person = personService.getPersonOrNull(userName); + if (person != null) + { + String firstName = (String) nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME); + String lastName = (String) nodeService.getProperty(person, ContentModel.PROP_LASTNAME); + result.members.add(new MemberState(userName, firstName, lastName)); + } + + if (!found && userName.equals(currentUser)) + { + found = true; + result.currentUserSiteManager = true; + } + } + + return result; + } + + public SiteInfo getSiteInfo() + { + return this.siteInfo; + } + + public List getMembers() + { + return this.members; + } + + public boolean isCurrentUserSiteManager() + { + return this.currentUserSiteManager; + } + + public static class MemberState + { + private String userName; + private String firstName; + private String lastName; + + public MemberState(String userName, String firstName, String lastName) + { + this.userName = userName; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getUserName() + { + return this.userName; + } + + public String getFirstName() + { + return this.firstName; + } + + public String getLastName() + { + return this.lastName; + } + + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclChangeSetsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclChangeSetsGet.java new file mode 100644 index 00000000000..2bc64f37504 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclChangeSetsGet.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.solr.AclChangeSet; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR: Track ACL Change Sets + * + * @since 4.0 + */ +public class AclChangeSetsGet extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(AclChangeSetsGet.class); + + private SOLRTrackingComponent solrTrackingComponent; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + protected Map executeImpl(WebScriptRequest req, Status status) + { + String fromIdParam = req.getParameter("fromId"); + String fromTimeParam = req.getParameter("fromTime"); + String toIdParam = req.getParameter("toId"); + String toTimeParam = req.getParameter("toTime"); + String maxResultsParam = req.getParameter("maxResults"); + + Long fromId = (fromIdParam == null ? null : Long.valueOf(fromIdParam)); + Long fromTime = (fromTimeParam == null ? null : Long.valueOf(fromTimeParam)); + Long toId = (toIdParam == null ? null : Long.valueOf(toIdParam)); + Long toTime = (toTimeParam == null ? null : Long.valueOf(toTimeParam)); + int maxResults = (maxResultsParam == null ? 1024 : Integer.valueOf(maxResultsParam)); + + List changesets = solrTrackingComponent.getAclChangeSets(fromId, fromTime, toId, toTime, maxResults); + + Map model = new HashMap(1, 1.0f); + model.put("aclChangeSets", changesets); + + Long maxChangeSetCommitTime = solrTrackingComponent.getMaxChangeSetCommitTime(); + if(maxChangeSetCommitTime != null) + { + model.put("maxChangeSetCommitTime", maxChangeSetCommitTime); + } + + Long maxChangeSetId = solrTrackingComponent.getMaxChangeSetId(); + if(maxChangeSetId != null) + { + model.put("maxChangeSetId", maxChangeSetId); + } + + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclsGet.java new file mode 100644 index 00000000000..a2bbdda4e59 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclsGet.java @@ -0,0 +1,132 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.solr.Acl; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR: Track ACLs + * + * @since 4.0 + */ +public class AclsGet extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(AclsGet.class); + + private SOLRTrackingComponent solrTrackingComponent; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + protected Map executeImpl(WebScriptRequest req, Status status) + { + try + { + Map model = buildModel(req); + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } + catch(IOException e) + { + throw new WebScriptException("IO exception parsing request", e); + } + catch(JSONException e) + { + throw new WebScriptException("Invalid JSON", e); + } + } + + private Map buildModel(WebScriptRequest req) throws JSONException, IOException + { + List aclChangeSetIds = null; + + Content content = req.getContent(); + if (content == null) + { + throw new WebScriptException("Request content is empty"); + } + JSONObject o = new JSONObject(content.getContent()); + JSONArray aclChangeSetIdsJSON = o.has("aclChangeSetIds") ? o.getJSONArray("aclChangeSetIds") : null; + if (aclChangeSetIdsJSON == null) + { + throw new WebScriptException( + Status.STATUS_BAD_REQUEST, + "Parameter 'aclChangeSetIds' not provided in request content."); + } + else if (aclChangeSetIdsJSON.length() == 0) + { + throw new WebScriptException( + Status.STATUS_BAD_REQUEST, + "Parameter 'aclChangeSetIds' must hold from 1 or more IDs."); + } + aclChangeSetIds = new ArrayList(aclChangeSetIdsJSON.length()); + for (int i = 0; i < aclChangeSetIdsJSON.length(); i++) + { + aclChangeSetIds.add(aclChangeSetIdsJSON.getLong(i)); + } + + String fromIdParam = req.getParameter("fromId"); + String maxResultsParam = req.getParameter("maxResults"); + + Long fromId = (fromIdParam == null ? null : Long.valueOf(fromIdParam)); + int maxResults = (maxResultsParam == null ? 1024 : Integer.valueOf(maxResultsParam)); + + // Request according to the paging query style required + List acls = solrTrackingComponent.getAcls(aclChangeSetIds, fromId, maxResults); + + Map model = new HashMap(1, 1.0f); + model.put("acls", acls); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclsReadersGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclsReadersGet.java new file mode 100644 index 00000000000..4fc6194c000 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AclsReadersGet.java @@ -0,0 +1,126 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.solr.AclReaders; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR: Track ACLs + * + * @since 4.0 + */ +public class AclsReadersGet extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(AclsReadersGet.class); + + private SOLRTrackingComponent solrTrackingComponent; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + protected Map executeImpl(WebScriptRequest req, Status status) + { + try + { + Map model = buildModel(req); + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } + catch(IOException e) + { + throw new WebScriptException("IO exception parsing request", e); + } + catch(JSONException e) + { + throw new WebScriptException("Invalid JSON", e); + } + } + + private Map buildModel(WebScriptRequest req) throws JSONException, IOException + { + List aclIds = null; + + Content content = req.getContent(); + if (content == null) + { + throw new WebScriptException("Request content is empty"); + } + JSONObject o = new JSONObject(content.getContent()); + JSONArray aclIdsJSON = o.has("aclIds") ? o.getJSONArray("aclIds") : null; + if (aclIdsJSON == null) + { + throw new WebScriptException( + Status.STATUS_BAD_REQUEST, + "Parameter 'aclIds' not provided in request content."); + } + else if (aclIdsJSON.length() == 0) + { + throw new WebScriptException( + Status.STATUS_BAD_REQUEST, + "Parameter 'aclIds' must hold from 1 or more IDs."); + } + aclIds = new ArrayList(aclIdsJSON.length()); + for (int i = 0; i < aclIdsJSON.length(); i++) + { + aclIds.add(aclIdsJSON.getLong(i)); + } + + // Request according to the paging query style required + List aclsReaders = solrTrackingComponent.getAclsReaders(aclIds); + + Map model = new HashMap(1, 1.0f); + model.put("aclsReaders", aclsReaders); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AlfrescoModelGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AlfrescoModelGet.java new file mode 100644 index 00000000000..18169ca2b4b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AlfrescoModelGet.java @@ -0,0 +1,100 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.util.Map; + +import org.alfresco.repo.solr.AlfrescoModel; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Support for SOLR: Get Alfresco model + * + * @since 4.0 + */ +public class AlfrescoModelGet extends AbstractWebScript +{ + protected static final Log logger = LogFactory.getLog(AlfrescoModelGet.class); + + private NamespaceService namespaceService; + private SOLRTrackingComponent solrTrackingComponent; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void execute(WebScriptRequest req, WebScriptResponse res) + { + try + { + handle(req, res); + } + catch(IOException e) + { + throw new WebScriptException("IO exception parsing request", e); + } + catch(JSONException e) + { + throw new WebScriptException("Invalid JSON", e); + } + } + + private void handle(WebScriptRequest req, WebScriptResponse res) throws JSONException, IOException + { + // create map of template vars + String modelQName = req.getParameter("modelQName"); + if(modelQName == null) + { + throw new WebScriptException( + Status.STATUS_BAD_REQUEST, + "URL parameter 'modelQName' not provided."); + } + + ModelDefinition.XMLBindingType bindingType = ModelDefinition.XMLBindingType.DEFAULT; + AlfrescoModel model = solrTrackingComponent.getModel(QName.createQName(modelQName)); + res.setHeader("XAlfresco-modelChecksum", String.valueOf(model.getModelDef().getChecksum(bindingType))); + model.getModelDef().toXML(bindingType, res.getOutputStream()); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AlfrescoModelsDiff.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AlfrescoModelsDiff.java new file mode 100644 index 00000000000..e946bdf625e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/AlfrescoModelsDiff.java @@ -0,0 +1,135 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.solr.AlfrescoModelDiff; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR: Track Alfresco model changes + * + * @since 4.0 + */ +public class AlfrescoModelsDiff extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(AlfrescoModelsDiff.class); + + private static final String MSG_IO_EXCEPTION = "IO exception parsing request "; + + private static final String MSG_JSON_EXCEPTION = "Unable to fetch model changes from "; + + private SOLRTrackingComponent solrTrackingComponent; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + protected Map executeImpl(WebScriptRequest req, Status status) + { + try + { + Map model = buildModel(req); + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + return model; + } + catch (IOException e) + { + setExceptionResponse(req, status, MSG_IO_EXCEPTION, Status.STATUS_INTERNAL_SERVER_ERROR, e); + return null; + } + catch (JSONException e) + { + setExceptionResponse(req, status, MSG_JSON_EXCEPTION, Status.STATUS_BAD_REQUEST, e); + return null; + } + } + + private void setExceptionResponse(WebScriptRequest req, Status responseStatus, String responseMessage, int statusCode, Exception e) + { + String message = responseMessage + req; + + if (logger.isDebugEnabled()) + { + logger.warn(message, e); + } + else + { + logger.warn(message); + } + + responseStatus.setCode(statusCode, message); + responseStatus.setException(e); + } + + private Map buildModel(WebScriptRequest req) throws JSONException, IOException + { + Map model = new HashMap(1, 1.0f); + + Content content = req.getContent(); + if(content == null) + { + throw new WebScriptException("Failed to convert request to String"); + } + JSONObject o = new JSONObject(content.getContent()); + JSONArray jsonModels = o.getJSONArray("models"); + Map models = new HashMap(jsonModels.length()); + for(int i = 0; i < jsonModels.length(); i++) + { + JSONObject jsonModel = jsonModels.getJSONObject(i); + models.put(QName.createQName(jsonModel.getString("name")), jsonModel.getLong("checksum")); + } + + List diffs = solrTrackingComponent.getModelDiffs(models); + model.put("diffs", diffs); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/GetNodesParameters.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/GetNodesParameters.java new file mode 100644 index 00000000000..dc998460831 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/GetNodesParameters.java @@ -0,0 +1,148 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.util.List; +import java.util.Set; + +import org.alfresco.service.namespace.QName; + +/** + * Input parameters for retrieving node details for SOLR. + * + * @since 4.0 + */ +public class GetNodesParameters +{ + private List transactionIds; + private Long fromNodeId; + private Long toNodeId; + + private String storeProtocol; + private String storeIdentifier; + + private Set includeNodeTypes; + private Set excludeNodeTypes; + + private Set includeAspects; + private Set excludeAspects; + + public boolean getStoreFilter() + { + return (storeProtocol != null || storeIdentifier != null); + } + + public void setStoreProtocol(String storeProtocol) + { + this.storeProtocol = storeProtocol; + } + + public String getStoreProtocol() + { + return storeProtocol; + } + + public void setStoreIdentifier(String storeIdentifier) + { + this.storeIdentifier = storeIdentifier; + } + + public String getStoreIdentifier() + { + return storeIdentifier; + } + + public void setTransactionIds(List txnIds) + { + this.transactionIds = txnIds; + } + + public List getTransactionIds() + { + return transactionIds; + } + + public Long getFromNodeId() + { + return fromNodeId; + } + + public void setFromNodeId(Long fromNodeId) + { + this.fromNodeId = fromNodeId; + } + + public Long getToNodeId() + { + return toNodeId; + } + + public void setToNodeId(Long toNodeId) + { + this.toNodeId = toNodeId; + } + + public Set getIncludeNodeTypes() + { + return includeNodeTypes; + } + + public Set getExcludeNodeTypes() + { + return excludeNodeTypes; + } + + public Set getIncludeAspects() + { + return includeAspects; + } + + public Set getExcludeAspects() + { + return excludeAspects; + } + + public void setIncludeNodeTypes(Set includeNodeTypes) + { + this.includeNodeTypes = includeNodeTypes; + } + + public void setExcludeNodeTypes(Set excludeNodeTypes) + { + this.excludeNodeTypes = excludeNodeTypes; + } + + public void setIncludeAspects(Set includeAspects) + { + this.includeAspects = includeAspects; + } + + public void setExcludeAspects(Set excludeAspects) + { + this.excludeAspects = excludeAspects; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NextTransactionGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NextTransactionGet.java new file mode 100644 index 00000000000..c72cd71f72e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NextTransactionGet.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.domain.node.ibatis.NodeDAOImpl; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Returns the next transaction commit time from a given commit time. + * This webscript is used to skip long periods where the repository is not ingesting new content. + * + * @author aborroy + * + */ +public class NextTransactionGet extends DeclarativeWebScript +{ + + private NodeDAOImpl nodeDAO; + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Long fromCommitTime = Long.parseLong(req.getParameter("fromCommitTime")); + + Map model = new HashMap<>(); + model.put("nextTransactionCommitTimeMs", nodeDAO.getNextTxCommitTime(fromCommitTime)); + return model; + } + + public void setNodeDAO(NodeDAOImpl nodeDAO) + { + this.nodeDAO = nodeDAO; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java new file mode 100644 index 00000000000..baf64349afe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodeContentGet.java @@ -0,0 +1,231 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.transform.UnsupportedTransformationException; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.rendition2.SynchronousTransformClient; +import org.alfresco.repo.web.scripts.content.StreamContent; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.Map; + +/** + * A web service to return the text content (transformed if required) of a node's + * content property. + * + * @since 4.0 + */ +public class NodeContentGet extends StreamContent +{ + private static final String TRANSFORM_STATUS_HEADER = "X-Alfresco-transformStatus"; + private static final String TRANSFORM_EXCEPTION_HEADER = "X-Alfresco-transformException"; + private static final String TRANSFORM_DURATION_HEADER = "X-Alfresco-transformDuration"; + + private static final Log logger = LogFactory.getLog(NodeContentGet.class); + + /** + * format definied by RFC 822, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3 + */ + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US); + + private NodeDAO nodeDAO; + private NodeService nodeService; + private ContentService contentService; + private SynchronousTransformClient synchronousTransformClient; + + public void setNodeDAO(NodeDAO nodeDAO) + { + this.nodeDAO = nodeDAO; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setSynchronousTransformClient(SynchronousTransformClient synchronousTransformClient) + { + this.synchronousTransformClient = synchronousTransformClient; + } + + /** + * + * @param req WebScriptRequest + * @param res WebScriptResponse + * @throws IOException + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + ContentReader textReader = null; + Exception transformException = null; + + String nodeIDString = req.getParameter("nodeId"); + if(nodeIDString == null) + { + throw new WebScriptException("nodeID parameter is required for GetNodeContent"); + } + long nodeId = Long.valueOf(nodeIDString).longValue(); + + String propertyQName = req.getParameter("propertyQName"); + QName propertyName = null; + if(propertyQName == null) + { + propertyName = ContentModel.PROP_CONTENT; + } + else + { + propertyName = QName.createQName(propertyQName); + } + Pair pair = nodeDAO.getNodePair(nodeId); + if(pair == null) + { + // If the node does not exists we treat it as if it has no content + // We could be trying to update the content of a node in the index that has been deleted. + res.setStatus(HttpStatus.SC_NO_CONTENT); + return; + } + NodeRef nodeRef = pair.getSecond(); + + // check If-Modified-Since header and set Last-Modified header as appropriate + Date modified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED); + // May be null - if so treat as just changed + if(modified == null) + { + modified = new Date(); + } + long modifiedSince = -1; + String modifiedSinceStr = req.getHeader("If-Modified-Since"); + if(modifiedSinceStr != null) + { + try + { + modifiedSince = dateFormat.parse(modifiedSinceStr).getTime(); + } + catch (Throwable e) + { + if (logger.isWarnEnabled()) + { + logger.warn("Browser sent badly-formatted If-Modified-Since header: " + modifiedSinceStr); + } + } + + if (modifiedSince > 0L) + { + // round the date to the ignore millisecond value which is not supplied by header + long modDate = (modified.getTime() / 1000L) * 1000L; + if (modDate <= modifiedSince) + { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + } + } + + ContentReader reader = contentService.getReader(nodeRef, propertyName); + if(reader == null) + { + res.setStatus(HttpStatus.SC_NO_CONTENT); + return; + } + + // Perform transformation catering for mimetype AND encoding + ContentWriter writer = contentService.getTempWriter(); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); // Expect transformers to produce UTF-8 + + try + { + long start = System.currentTimeMillis(); + Map options = Collections.emptyMap(); + synchronousTransformClient.transform(reader, writer, options, "SolrIndexer", nodeRef); + long transformDuration = System.currentTimeMillis() - start; + res.setHeader(TRANSFORM_DURATION_HEADER, String.valueOf(transformDuration)); + } + catch (UnsupportedTransformationException e) + { + res.setHeader(TRANSFORM_STATUS_HEADER, "noTransform"); + res.setStatus(HttpStatus.SC_NO_CONTENT); + return; + } + catch (Exception e) + { + transformException = e; + } + + if(transformException == null) + { + // point the reader to the new-written content + textReader = writer.getReader(); + // Check that the reader is a view onto something concrete + if (textReader == null || !textReader.exists()) + { + transformException = new ContentIOException( + "The transformation did not write any content, yet: \n" + + " temp writer: " + writer); + } + } + + if(transformException != null) + { + res.setHeader(TRANSFORM_STATUS_HEADER, "transformFailed"); + res.setHeader(TRANSFORM_EXCEPTION_HEADER, transformException.getMessage()); + res.setStatus(HttpStatus.SC_NO_CONTENT); + } + else + { + res.setStatus(HttpStatus.SC_OK); + streamContentImpl(req, res, textReader, null, null, false, modified, String.valueOf(modified.getTime()), null, null); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodeMetaData.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodeMetaData.java new file mode 100644 index 00000000000..8491c2bc54a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodeMetaData.java @@ -0,0 +1,115 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; + +/** + * Bean to carry node metadata + * + * @since 4.0 + */ +public class NodeMetaData +{ + private long id; + private NodeRef nodeRef; + private QName type; + private long aclId; + private Map properties; + private Set aspects; + private Path paths; + public long getId() + { + return id; + } + public void setId(long id) + { + this.id = id; + } + public NodeRef getNodeRef() + { + return nodeRef; + } + public void setNodeRef(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + public QName getType() + { + return type; + } + public void setType(QName type) + { + this.type = type; + } + public long getAclId() + { + return aclId; + } + public void setAclId(long aclId) + { + this.aclId = aclId; + } + public Map getProperties() + { + return properties; + } + public void setProperties(Map properties) + { + this.properties = properties; + } + public Set getAspects() + { + return aspects; + } + public void setAspects(Set aspects) + { + this.aspects = aspects; + } + public Path getPaths() + { + return paths; + } + public void setPaths(Path paths) + { + this.paths = paths; + } + + @Override + public String toString() + { + return "NodeMetaData [id=" + id + ", nodeRef=" + nodeRef + ", type=" + type + ", aclId=" + aclId + + ", properties=" + properties + ", aspects=" + aspects + ", paths=" + paths + "]"; + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodesGet.java new file mode 100644 index 00000000000..fdaf1baf9d9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodesGet.java @@ -0,0 +1,343 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.domain.node.Node; +import org.alfresco.repo.domain.qname.QNameDAO; +import org.alfresco.repo.search.impl.QueryParserUtils; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.solr.NodeParameters; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.alfresco.repo.solr.SOLRTrackingComponent.NodeQueryCallback; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR: Get a list of nodes in the given transactions. + *

+ * Supports fromNodeId, toNodeId, count (all optional) to control the number of nodes returned
+ * e.g. (null, null, 1000) will return at most 1000 nodes starting from the first node in the first transaction.
+ * e.g. (1234, null, 1000) will return at most 1000 nodes starting from the node id 1234.
+ * + * @since 4.0 + */ +public class NodesGet extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(NodesGet.class); + + private SOLRTrackingComponent solrTrackingComponent; + + private TenantService tenantService; + + private QNameDAO qnameDAO; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + public void setQnameDAO(QNameDAO qnameDAO) + { + this.qnameDAO = qnameDAO; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + try + { + Content content = req.getContent(); + if(content == null) + { + throw new WebScriptException("Failed to convert request to String"); + } + JSONObject o = new JSONObject(content.getContent()); + + JSONArray aTxnIds = o.has("txnIds") ? o.getJSONArray("txnIds") : null; + Long fromTxnId = o.has("fromTxnId") ? o.getLong("fromTxnId") : null; + Long toTxnId = o.has("toTxnId") ? o.getLong("toTxnId") : null; + + Long fromNodeId = o.has("fromNodeId") ? o.getLong("fromNodeId") : null; + Long toNodeId = o.has("toNodeId") ? o.getLong("toNodeId") : null; + + Set excludeAspects = null; + if(o.has("excludeAspects")) + { + JSONArray aExcludeAspects = o.getJSONArray("excludeAspects"); + excludeAspects = new HashSet(aExcludeAspects.length()); + for(int i = 0; i < aExcludeAspects.length(); i++) + { + excludeAspects.add(QName.createQName(aExcludeAspects.getString(i).trim())); + } + } + + Set includeAspects = null; + if(o.has("includeAspects")) + { + JSONArray aIncludeAspects = o.getJSONArray("includeAspects"); + includeAspects = new HashSet(aIncludeAspects.length()); + for(int i = 0; i < aIncludeAspects.length(); i++) + { + includeAspects.add(QName.createQName(aIncludeAspects.getString(i).trim())); + } + } + + Set excludeNodeTypes = null; + if(o.has("excludeNodeTypes")) + { + JSONArray aExcludeNodeTypes = o.getJSONArray("excludeNodeTypes"); + excludeNodeTypes = new HashSet(aExcludeNodeTypes.length()); + for(int i = 0; i < aExcludeNodeTypes.length(); i++) + { + excludeNodeTypes.add(QName.createQName(aExcludeNodeTypes.getString(i).trim())); + } + } + + Set includeNodeTypes = null; + if(o.has("includeNodeTypes")) + { + JSONArray aIncludeNodeTypes = o.getJSONArray("includeNodeTypes"); + includeNodeTypes = new HashSet(aIncludeNodeTypes.length()); + for(int i = 0; i < aIncludeNodeTypes.length(); i++) + { + includeNodeTypes.add(QName.createQName(aIncludeNodeTypes.getString(i).trim())); + } + } + + // 0 or Integer.MAX_VALUE => ignore + int maxResults = o.has("maxResults") ? o.getInt("maxResults") : 0; + + String storeProtocol = o.has("storeProtocol") ? o.getString("storeProtocol") : null; + String storeIdentifier = o.has("storeIdentifier") ? o.getString("storeIdentifier") : null; + String coreName = o.has("coreName") ? o.getString("coreName") : null; + + List txnIds = null; + if(aTxnIds != null) + { + txnIds = new ArrayList(aTxnIds.length()); + for(int i = 0; i < aTxnIds.length(); i++) + { + txnIds.add(aTxnIds.getLong(i)); + } + } + + String shardProperty = o.has("shardProperty") ? o.getString("shardProperty") : null; + + NodeParameters nodeParameters = new NodeParameters(); + nodeParameters.setTransactionIds(txnIds); + nodeParameters.setFromTxnId(fromTxnId); + nodeParameters.setToTxnId(toTxnId); + nodeParameters.setFromNodeId(fromNodeId); + nodeParameters.setToNodeId(toNodeId); + nodeParameters.setExcludeAspects(excludeAspects); + nodeParameters.setIncludeAspects(includeAspects); + nodeParameters.setExcludeNodeTypes(excludeNodeTypes); + nodeParameters.setIncludeNodeTypes(includeNodeTypes); + nodeParameters.setShardProperty(shardProperty); + nodeParameters.setCoreName(coreName); + + + StoreRef storeRef = null; + + if (AuthenticationUtil.isMtEnabled()) + { + // MT - use Java filter (post query) and then add tenant context for each node + storeRef = new StoreRef(storeProtocol, storeIdentifier); + } + else + { + // non-MT - use DB filter (in query) + nodeParameters.setStoreProtocol(storeProtocol); + nodeParameters.setStoreIdentifier(storeIdentifier); + } + + nodeParameters.setMaxResults(maxResults); + + WebNodeQueryCallback nodeQueryCallback = new WebNodeQueryCallback(maxResults, storeRef, tenantService, qnameDAO); + + solrTrackingComponent.getNodes(nodeParameters, nodeQueryCallback); + + Map model = new HashMap(1, 1.0f); + List nodes = nodeQueryCallback.getNodes(); + model.put("nodes", nodes); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } + catch(IOException e) + { + throw new WebScriptException("IO exception parsing request", e); + } + catch(JSONException e) + { + throw new WebScriptException("Invalid JSON", e); + } + } + + public static class NodeRecord + { + private final Long id; + private final Long txnId; + private final boolean isDeleted; + private final String nodeRef; + private final String tenant; + private final Long aclId; + private final String shardPropertyValue; + private final Integer explicitShardId; + + public NodeRecord(Node node, QNameDAO qnameDAO, TenantService tenantService) + { + this.id = node.getId(); + this.txnId = node.getTransaction().getId(); + this.isDeleted = node.getNodeStatus(qnameDAO).isDeleted(); + this.nodeRef = node.getNodeRef().toString(); + this.tenant = tenantService.getDomain(node.getNodeRef().getStoreRef().getIdentifier()); + this.aclId = node.getAclId(); + this.shardPropertyValue = node.getShardKey(); + this.explicitShardId = node.getExplicitShardId(); + } + + public Long getId() + { + return id; + } + + public Long getTxnId() + { + return txnId; + } + + public boolean isDeleted() + { + return isDeleted; + } + + public String getNodeRef() + { + return nodeRef; + } + + public String getTenant() + { + return tenant; + } + + public Long getAclId() + { + return aclId; + } + + public String getShardPropertyValue() + { + return this.shardPropertyValue; + } + + public Integer getExplicitShardId() + { + return this.explicitShardId; + } + } + + /** + * Callback for DAO get nodes query + */ + private class WebNodeQueryCallback implements NodeQueryCallback + { + private ArrayList nodes; + + private StoreRef storeRef; + + private TenantService tenantService; + + private QNameDAO qnameDAO; + + public WebNodeQueryCallback(int count, StoreRef storeRef, TenantService tenantService, QNameDAO qnameDAO) + { + super(); + + this.storeRef = storeRef; + this.tenantService = tenantService; + this.qnameDAO = qnameDAO; + + nodes = new ArrayList(count == 0 || count == Integer.MAX_VALUE ? 100 : count); + } + + @Override + public boolean handleNode(Node node) + { + if (storeRef != null) + { + // MT - since storeRef is not null, filter by store here + StoreRef tenantStoreRef = node.getStore().getStoreRef(); + StoreRef baseStoreRef = new StoreRef(tenantStoreRef.getProtocol(), tenantService.getBaseName(tenantStoreRef.getIdentifier(), true)); + if (storeRef.equals(baseStoreRef)) + { + nodes.add(new NodeRecord(node, qnameDAO, tenantService)); + } + } + else + { + nodes.add(new NodeRecord(node, qnameDAO, tenantService)); + } + + // continue - get next node + return true; + } + + public List getNodes() + { + return nodes; + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodesMetaDataGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodesMetaDataGet.java new file mode 100644 index 00000000000..a6187950dc5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/NodesMetaDataGet.java @@ -0,0 +1,460 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +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 org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.search.IndexerException; +import org.alfresco.repo.solr.MetaDataResultsFilter; +import org.alfresco.repo.solr.NodeMetaData; +import org.alfresco.repo.solr.NodeMetaDataParameters; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.alfresco.repo.solr.SOLRTrackingComponent.NodeMetaDataQueryCallback; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +// todo url parameter to remove whitespace in results - make it the default? +/** + * Support for SOLR: Get metadata for nodes given IDs, ranges of IDs, etc. + *

+ * + * @since 4.0 + */ +public class NodesMetaDataGet extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(NodesMetaDataGet.class); + private static final int INITIAL_DEFAULT_SIZE = 100; + private static final int BATCH_SIZE = 50; + + private SOLRTrackingComponent solrTrackingComponent; + private SOLRSerializer solrSerializer; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + public void setSolrSerializer(SOLRSerializer solrSerializer) + { + this.solrSerializer = solrSerializer; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + try + { + Content content = req.getContent(); + if(content == null) + { + throw new WebScriptException("Failed to convert request to String"); + } + JSONObject o = new JSONObject(content.getContent()); + + List nodeIds = null; + if(o.has("nodeIds")) + { + JSONArray jsonNodeIds = o.getJSONArray("nodeIds"); + nodeIds = new ArrayList(jsonNodeIds.length()); + for(int i = 0; i < jsonNodeIds.length(); i++) + { + Long nodeId = jsonNodeIds.getLong(i); + nodeIds.add(nodeId); + } + } + + Long fromNodeId = o.has("fromNodeId") ? o.getLong("fromNodeId") : null; + Long toNodeId = o.has("toNodeId") ? o.getLong("toNodeId") : null; + + // 0 or Integer.MAX_VALUE => ignore + int maxResults = o.has("maxResults") ? o.getInt("maxResults") : 0; + + int size = 0; + if(maxResults != 0 && maxResults != Integer.MAX_VALUE) + { + size = maxResults; + } + else if(nodeIds != null) + { + size = nodeIds.size(); + } + else if(fromNodeId != null && toNodeId != null) + { + if((toNodeId.longValue() - fromNodeId.longValue()) > Integer.MAX_VALUE) + { + throw new WebScriptException("Too many nodes expected, try changing the criteria"); + } + size = (int)(toNodeId - fromNodeId); + } + + final boolean noSizeCalculated = (size == 0); + + // filters, defaults are 'true' + MetaDataResultsFilter filter = new MetaDataResultsFilter(); + if(o.has("includeAclId")) + { + filter.setIncludeAclId(o.getBoolean("includeAclId")); + } + if(o.has("includeAspects")) + { + filter.setIncludeAspects(o.getBoolean("includeAspects")); + } + if(o.has("includeNodeRef")) + { + filter.setIncludeNodeRef(o.getBoolean("includeNodeRef")); + } + if(o.has("includeOwner")) + { + filter.setIncludeOwner(o.getBoolean("includeOwner")); + } + if(o.has("includeProperties")) + { + filter.setIncludeProperties(o.getBoolean("includeProperties")); + } + if(o.has("includePaths")) + { + filter.setIncludePaths(o.getBoolean("includePaths")); + } + if(o.has("includeType")) + { + filter.setIncludeType(o.getBoolean("includeType")); + } + if(o.has("includeParentAssociations")) + { + filter.setIncludeParentAssociations(o.getBoolean("includeParentAssociations")); + } + if(o.has("includeChildIds")) + { + filter.setIncludeChildIds(o.getBoolean("includeChildIds")); + } + if(o.has("includeTxnId")) + { + filter.setIncludeTxnId(o.getBoolean("includeTxnId")); + } + + final ArrayList nodesMetaData = + new ArrayList(size > 0 ? size : INITIAL_DEFAULT_SIZE); + NodeMetaDataParameters params = new NodeMetaDataParameters(); + params.setNodeIds(nodeIds); + params.setFromNodeId(fromNodeId); + params.setToNodeId(toNodeId); + params.setMaxResults(maxResults); + + solrTrackingComponent.getNodesMetadata(params, filter, new NodeMetaDataQueryCallback() + { + private int counter = BATCH_SIZE; + private int numBatches = 0; + + @Override + public boolean handleNodeMetaData(NodeMetaData nodeMetaData) + { + // need to perform data structure conversions that are compatible with Freemarker + // e.g. Serializable -> String, QName -> String (because map keys must be string, number) + try + { + FreemarkerNodeMetaData fNodeMetaData = new FreemarkerNodeMetaData(solrSerializer, nodeMetaData); + nodesMetaData.add(fNodeMetaData); + } + catch(Exception e) + { + throw new AlfrescoRuntimeException("Problem converting to Freemarker using node " + nodeMetaData.getNodeRef().toString(), e); + } + + if(noSizeCalculated && --counter == 0) + { + counter = BATCH_SIZE; + nodesMetaData.ensureCapacity(++numBatches*BATCH_SIZE); + } + + return true; + } + }); + + Map model = new HashMap(1, 1.0f); + model.put("nodes", nodesMetaData); + model.put("filter", filter); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } + catch(IOException e) + { + throw new WebScriptException("IO exception parsing request", e); + } + catch(JSONException e) + { + throw new WebScriptException("Invalid JSON", e); + } + } + + /** + * Bean to store node meta data for use by FreeMarker templates + * + * @since 4.0 + */ + public static class FreemarkerNodeMetaData + { + private final Long nodeId; + private final NodeRef nodeRef; + private final QName nodeType; + private final Long aclId; + private final Map properties; + private final Set aspects; + private final List paths; + private final List namePaths; + private final List childAssocs; + private final List parentAssocs; + private final Long parentAssocsCrc; + private final List childIds; + private final String owner; + private final Long txnId; + private final Set ancestors; + private final String tenantDomain; + + public FreemarkerNodeMetaData(final SOLRSerializer solrSerializer, final NodeMetaData nodeMetaData) + throws IOException, JSONException + { + this.nodeId = nodeMetaData.getNodeId(); + this.tenantDomain = nodeMetaData.getTenantDomain(); + this.aclId = nodeMetaData.getAclId(); + this.nodeRef = nodeMetaData.getNodeRef(); + this.nodeType = nodeMetaData.getNodeType(); + this.txnId = nodeMetaData.getTxnId(); + this.parentAssocs = new ArrayList<>(); + this.childAssocs = new ArrayList<>(); + + // convert Paths to Strings + List paths = new ArrayList(); + List ancestorPaths = new ArrayList(); + HashSet ancestors = new HashSet(); + if(nodeMetaData.getPaths() != null) + { + for(Pair pair : nodeMetaData.getPaths()) + { + StringBuilder ancestorPath = new StringBuilder(); + JSONObject o = new JSONObject(); + o.put("path", solrSerializer.serializeValue(String.class, pair.getFirst())); + o.put("qname", solrSerializer.serializeValue(String.class, pair.getSecond())); + + + for (NodeRef ancestor : getAncestors(pair.getFirst())) + { + ancestors.add(ancestor.toString()); + ancestorPath.insert(0, ancestor.getId()).insert(0, "/"); + } + + o.put("apath", ancestorPath); + paths.add(o.toString(3)); + } + } + this.ancestors = ancestors; + this.paths = paths; + + + // convert name Paths to Strings + List namePaths = new ArrayList(); + if(nodeMetaData.getNamePaths() != null) + { + for(Collection namePath : nodeMetaData.getNamePaths()) + { + JSONObject o = new JSONObject(); + JSONArray array = new JSONArray(); + for(String element : namePath) + { + array.put(solrSerializer.serializeValue(String.class, element)); + } + o.put("namePath", array); + namePaths.add(o.toString(3)); + } + } + this.namePaths = namePaths; + + this.owner = nodeMetaData.getOwner(); + this.childIds = nodeMetaData.getChildIds(); + this.parentAssocsCrc = nodeMetaData.getParentAssocsCrc(); + this.aspects = nodeMetaData.getAspects(); + + + if (nodeMetaData.getParentAssocs() != null) + { + for( ChildAssociationRef assRef : nodeMetaData.getParentAssocs()) + { + parentAssocs.add("\"" + solrSerializer.serializeToJSONString(assRef) + "\""); + } + } + + if (nodeMetaData.getChildAssocs() != null) + { + for( ChildAssociationRef assRef : nodeMetaData.getChildAssocs() ) + { + childAssocs.add("\"" + solrSerializer.serializeToJSONString(assRef) + "\""); + } + } + + final Map props = nodeMetaData.getProperties(); + if (props != null) + { + final Map properties = new HashMap(props.size()); + for (final QName propName : props.keySet()) + { + // need to run this in tenant context because types may be in a tenant-specific + // dictionary registry + TenantUtil.runAsTenant(new TenantRunAsWork() + { + @Override + public Void doWork() throws Exception + { + Serializable value = props.get(propName); + properties.put(solrSerializer.serializeValue(String.class, propName), + solrSerializer.serialize(propName, value)); + return null; + } + }, tenantDomain); + } + this.properties = properties; + } + else + { + this.properties = null; + } + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + public List getPaths() + { + return paths; + } + public List getNamePaths() + { + return namePaths; + } + public QName getNodeType() + { + return nodeType; + } + public Long getNodeId() + { + return nodeId; + } + public Long getAclId() + { + return aclId; + } + public Map getProperties() + { + return properties; + } + public Set getAspects() + { + return aspects; + } + public List getChildAssocs() + { + return childAssocs; + } + public List getParentAssocs() + { + return parentAssocs; + } + public Long getParentAssocsCrc() + { + return parentAssocsCrc; + } + public Set getAncestors() + { + return ancestors; + } + public List getChildIds() + { + return childIds; + } + public String getOwner() + { + return owner; + } + public Long getTxnId() + { + return txnId; + } + public String getTenantDomain() + { + return tenantDomain; + } + private ArrayList getAncestors(Path path) + { + ArrayList ancestors = new ArrayList(8); + for (Iterator elit = path.iterator(); elit.hasNext(); /**/) + { + Path.Element element = elit.next(); + if (!(element instanceof Path.ChildAssocElement)) + { + throw new IndexerException("Confused path: " + path); + } + Path.ChildAssocElement cae = (Path.ChildAssocElement) element; + NodeRef parentRef = cae.getRef().getParentRef(); + if(parentRef != null) + { + ancestors.add(0, parentRef); + } + + } + return ancestors; + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/PropertyValue.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/PropertyValue.java new file mode 100644 index 00000000000..a44abfd5386 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/PropertyValue.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +/** + * Represents a property value to be used by Freemarker + * + * @since 4.0 + */ +class PropertyValue +{ + // is value actually a string or a JSON object or array + // if true, enclose the value in double quotes (to represent a JSON string) + // when converting to a string. + private boolean isString = true; + + private String value; + + public PropertyValue(boolean isString, String value) + { + super(); + this.isString = isString; + this.value = value; + } + public boolean isString() + { + return isString; + } + public String getValue() + { + return value; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(isString) + { + sb.append("\""); // for json strings + } + sb.append(value); + if(isString) + { + sb.append("\""); // for json strings + } + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java new file mode 100644 index 00000000000..990e7054a2c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRAuthenticationFilter.java @@ -0,0 +1,285 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This filter protects the solr callback urls by verifying MACs on requests and encrypting responses + * and generating MACs on responses, if the secureComms property is set to "md5". If it is set to "https" + * or "none", the filter does nothing to the request and response. + * + * @since 4.0 + * + */ +public class SOLRAuthenticationFilter implements DependencyInjectedFilter +{ + 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"); + } + } + }; + + // Logger + private static Log logger = LogFactory.getLog(SOLRAuthenticationFilter.class); + + private SecureCommsType secureComms = SecureCommsType.HTTPS; + + public void setSecureComms(String type) + { + try + { + this.secureComms = SecureCommsType.getType(type); + } + catch(IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("", e); + } + } + + public void doFilter(ServletContext context, ServletRequest request, + ServletResponse response, FilterChain chain) throws IOException, + ServletException + { + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletResponse httpResponse = (HttpServletResponse)response; + +/* if(secureComms == SecureCommsType.ALFRESCO) + { + // Need to get as a byte array because we need to read the request twice, once for authentication + // and again by the web service. + SOLRHttpServletRequestWrapper requestWrapper = new SOLRHttpServletRequestWrapper(httpRequest, encryptionUtils); + + if(logger.isDebugEnabled()) + { + logger.debug("Authenticating " + httpRequest.getRequestURI()); + } + + if(encryptionUtils.authenticate(httpRequest, requestWrapper.getDecryptedBody())) + { + try + { + OutputStream out = response.getOutputStream(); + + GenericResponseWrapper responseWrapper = new GenericResponseWrapper(httpResponse); + + // TODO - do I need to chain to other authenticating filters - probably not? + // Could also remove sending of credentials with http request + chain.doFilter(requestWrapper, responseWrapper); + + Pair pair = encryptor.encrypt(KeyProvider.ALIAS_SOLR, null, responseWrapper.getData()); + + encryptionUtils.setResponseAuthentication(httpRequest, httpResponse, responseWrapper.getData(), pair.getSecond()); + + httpResponse.setHeader("Content-Length", Long.toString(pair.getFirst().length)); + out.write(pair.getFirst()); + out.close(); + } + catch(Exception e) + { + throw new AlfrescoRuntimeException("", e); + } + } + else + { + httpResponse.setStatus(401); + } + } + else */if(secureComms == SecureCommsType.HTTPS) + { + if(httpRequest.isSecure()) + { + // https authentication + chain.doFilter(request, response); + } + else + { + throw new AlfrescoRuntimeException("Expected a https request"); + } + } + else + { + chain.doFilter(request, response); + } + } + + protected boolean validateTimestamp(String timestampStr) + { + if(timestampStr == null || timestampStr.equals("")) + { + throw new AlfrescoRuntimeException("Missing timestamp on request"); + } + long timestamp = -1; + try + { + timestamp = Long.valueOf(timestampStr); + } + catch(NumberFormatException e) + { + throw new AlfrescoRuntimeException("Invalid timestamp on request"); + } + if(timestamp == -1) + { + throw new AlfrescoRuntimeException("Invalid timestamp on request"); + } + long currentTime = System.currentTimeMillis(); + return((currentTime - timestamp) < 30 * 1000); // 5s + } + +/* private static class SOLRHttpServletRequestWrapper extends HttpServletRequestWrapper + { + private byte[] body; + + SOLRHttpServletRequestWrapper(HttpServletRequest req, EncryptionUtils encryptionUtils) throws IOException + { + super(req); + this.body = encryptionUtils.decryptBody(req); + } + + byte[] getDecryptedBody() + { + return body; + } + + public ServletInputStream getInputStream() + { + final InputStream in = (body != null ? new ByteArrayInputStream(body) : null); + return new ServletInputStream() + { + public int read() throws IOException + { + if(in == null) + { + return -1; + } + else + { + int i = in.read(); + if(i == -1) + { + in.close(); + } + return i; + } + } + }; + } + }*/ + + private static class ByteArrayServletOutputStream extends ServletOutputStream + { + private ByteArrayOutputStream out = new ByteArrayOutputStream(); + + ByteArrayServletOutputStream() + { + } + + public byte[] getData() + { + return out.toByteArray(); + } + + @Override + public void write(int b) throws IOException + { + out.write(b); + } + } + + public static class GenericResponseWrapper extends HttpServletResponseWrapper { + private ByteArrayServletOutputStream output; + private int contentLength; + private String contentType; + + public GenericResponseWrapper(HttpServletResponse response) { + super(response); + output = new ByteArrayServletOutputStream(); + } + + public byte[] getData() { + return output.getData(); + } + + public ServletOutputStream getOutputStream() { + return output; + } + + public PrintWriter getWriter() { + return new PrintWriter(getOutputStream(),true); + } + + public void setContentLength(int length) { + this.contentLength = length; + super.setContentLength(length); + } + + public int getContentLength() { + return contentLength; + } + + public void setContentType(String type) { + this.contentType = type; + super.setContentType(type); + } + + + public String getContentType() { + return contentType; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRSerializer.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRSerializer.java new file mode 100644 index 00000000000..b483948b128 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/SOLRSerializer.java @@ -0,0 +1,502 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.domain.node.ContentDataWithId; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.MLText; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.Path.AttributeElement; +import org.alfresco.service.cmr.repository.Path.ChildAssocElement; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; +import org.alfresco.service.cmr.repository.datatype.TypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConverter.Converter; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.extensions.surf.exception.PlatformRuntimeException; +import org.springframework.extensions.webscripts.json.JSONUtils; + +/** + * SOLR conversions of values to JSON-compatible String. + * + * @since 4.0 + */ +/* package */ class SOLRSerializer +{ + protected static final Log logger = LogFactory.getLog(SOLRSerializer.class); + + private JSONUtils jsonUtils = new JSONUtils(); + + private Set NUMBER_TYPES; + + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + + private SOLRTypeConverter typeConverter; + + public void init() + { + PropertyCheck.mandatory(this, "dictionaryService", dictionaryService); + PropertyCheck.mandatory(this, "namespaceService", namespaceService); + + NUMBER_TYPES = new HashSet(4); + NUMBER_TYPES.add(DataTypeDefinition.DOUBLE); + NUMBER_TYPES.add(DataTypeDefinition.FLOAT); + NUMBER_TYPES.add(DataTypeDefinition.INT); + NUMBER_TYPES.add(DataTypeDefinition.LONG); + + typeConverter = new SOLRTypeConverter(namespaceService); + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public String serializeToJSONString(Serializable value) + { + if (value != null && typeConverter.INSTANCE.getConverter(value.getClass(), String.class) == null) + { + // There is no converter + return value.toString(); + } + else + { + return typeConverter.INSTANCE.convert(String.class, value); + } + } + + public T serializeValue(Class targetClass, Object value) throws JSONException + { + return typeConverter.INSTANCE.convert(targetClass, value); + } + + @SuppressWarnings("unchecked") + public PropertyValue serialize(QName propName, Serializable value) throws IOException, JSONException + { + if(value == null) + { + return new PropertyValue(false, "null"); + } + + PropertyDefinition propertyDef = dictionaryService.getProperty(propName); + if (propertyDef == null) + { + // Treat it as text + return new PropertyValue(true, serializeToJSONString(value)); + } + DataTypeDefinition dataType = propertyDef.getDataType(); + QName dataTypeName = dataType.getName(); + if (propertyDef.isMultiValued()) + { + if(!(value instanceof Collection)) + { + throw new IllegalArgumentException("Multi value: expected a collection, got " + value.getClass().getName()); + } + + Collection c = (Collection)value; + + JSONArray body = new JSONArray(); + for(Serializable o : c) + { + if(dataTypeName.equals(DataTypeDefinition.MLTEXT)) + { + MLText source = (MLText)o; + JSONArray array = new JSONArray(); + for(Locale locale : source.getLocales()) + { + JSONObject json = new JSONObject(); + json.put("locale", DefaultTypeConverter.INSTANCE.convert(String.class, locale)); + json.put("value", source.getValue(locale)); + array.put(json); + } + body.put(array); + } + else if(dataTypeName.equals(DataTypeDefinition.CONTENT)) + { + throw new RuntimeException("Multi-valued content properties are not supported"); + } + else + { + body.put(serializeToJSONString(o)); + } + + } + + return new PropertyValue(false, body.toString()); + } + else + { + boolean encodeString = true; + if(dataTypeName.equals(DataTypeDefinition.MLTEXT)) + { + encodeString = false; + } + else if(dataTypeName.equals(DataTypeDefinition.CONTENT)) + { + encodeString = false; + } + else + { + encodeString = true; + } + + String sValue = null; + if (value instanceof String && encodeString) { + sValue = (String)jsonUtils.encodeJSONString(value); + } else { + sValue = serializeToJSONString(value); + } + + return new PropertyValue(encodeString, sValue); + } + } + + @SuppressWarnings("rawtypes") + static class SOLRTypeConverter + { + private NamespaceService namespaceService; + TypeConverter INSTANCE = new TypeConverter(); + + @SuppressWarnings("unchecked") + SOLRTypeConverter(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + + // add all default converters to this converter + // TODO find a better way of doing this + Map, Map, Converter>> converters = DefaultTypeConverter.INSTANCE.getConverters(); + for(Class source : converters.keySet()) + { + Map, Converter> converters1 = converters.get(source); + for(Class dest : converters1.keySet()) + { + Converter converter = converters1.get(dest); + INSTANCE.addConverter(source, dest, converter); + } + } + + // MLText + INSTANCE.addConverter(MLText.class, String.class, new TypeConverter.Converter() + { + public String convert(MLText source) + { + try + { + JSONArray array = new JSONArray(); + for(Locale locale : source.getLocales()) + { + JSONObject json = new JSONObject(); + json.put("locale", DefaultTypeConverter.INSTANCE.convert(String.class, locale)); + json.put("value", source.getValue(locale)); + array.put(json); + } + + return array.toString(3); + } + catch(JSONException e) + { + throw new AlfrescoRuntimeException("Unable to serialize content data to JSON", e); + } + } + }); + + // QName + INSTANCE.addConverter(QName.class, String.class, new TypeConverter.Converter() + { + public String convert(QName source) + { + return source.toString(); + } + }); + + // content + INSTANCE.addConverter(ContentDataWithId.class, String.class, new TypeConverter.Converter() + { + public String convert(ContentDataWithId source) + { + JSONObject json = new JSONObject(); + try + { + json.put("contentId", String.valueOf(source.getId())); + String locale = INSTANCE.convert(String.class, source.getLocale()); + json.put("locale", locale == null ? JSONObject.NULL : locale); + String encoding = source.getEncoding(); + json.put("encoding", encoding == null ? JSONObject.NULL : encoding); + String mimetype = source.getMimetype(); + json.put("mimetype", mimetype == null ? JSONObject.NULL : mimetype); + json.put("size", String.valueOf(source.getSize())); + return json.toString(3); + } + catch(JSONException e) + { + throw new AlfrescoRuntimeException("Unable to serialize content data to JSON", e); + } + } + }); + + INSTANCE.addConverter(ContentData.class, String.class, new TypeConverter.Converter() + { + public String convert(ContentData source) + { + JSONObject json = new JSONObject(); + try + { + String locale = INSTANCE.convert(String.class, source.getLocale()); + json.put("locale", locale == null ? JSONObject.NULL : locale); + String encoding = source.getEncoding(); + json.put("encoding", encoding == null ? JSONObject.NULL : encoding); + String mimetype = source.getMimetype(); + json.put("mimetype", mimetype == null ? JSONObject.NULL : mimetype); + json.put("size", String.valueOf(source.getSize())); + return json.toString(3); + } + catch(JSONException e) + { + throw new AlfrescoRuntimeException("Unable to serialize content data to JSON", e); + } + } + }); + + // node refs + INSTANCE.addConverter(NodeRef.class, String.class, new TypeConverter.Converter() + { + public String convert(NodeRef source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(String.class, NodeRef.class, new TypeConverter.Converter() + { + public NodeRef convert(String source) + { + return new NodeRef(source); + } + }); + + // paths + INSTANCE.addConverter(AttributeElement.class, String.class, new TypeConverter.Converter() + { + public String convert(AttributeElement source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(ChildAssocElement.class, String.class, new TypeConverter.Converter() + { + public String convert(ChildAssocElement source) + { + return source.getRef().toString(); + } + }); + + INSTANCE.addConverter(Path.DescendentOrSelfElement.class, String.class, new TypeConverter.Converter() + { + public String convert(Path.DescendentOrSelfElement source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(Path.ParentElement.class, String.class, new TypeConverter.Converter() + { + public String convert(Path.ParentElement source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(Path.SelfElement.class, String.class, new TypeConverter.Converter() + { + public String convert(Path.SelfElement source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(String.class, AttributeElement.class, new TypeConverter.Converter() + { + public AttributeElement convert(String source) + { + return new Path.AttributeElement(source); + } + }); + + INSTANCE.addConverter(String.class, ChildAssocElement.class, new TypeConverter.Converter() + { + public ChildAssocElement convert(String source) + { + return new Path.ChildAssocElement(INSTANCE.convert(ChildAssociationRef.class, source)); + } + }); + + INSTANCE.addConverter(String.class, Path.DescendentOrSelfElement.class, new TypeConverter.Converter() + { + public Path.DescendentOrSelfElement convert(String source) + { + return new Path.DescendentOrSelfElement(); + } + }); + + INSTANCE.addConverter(String.class, Path.ParentElement.class, new TypeConverter.Converter() + { + public Path.ParentElement convert(String source) + { + return new Path.ParentElement(); + } + }); + + INSTANCE.addConverter(String.class, Path.SelfElement.class, new TypeConverter.Converter() + { + public Path.SelfElement convert(String source) + { + return new Path.SelfElement(); + } + }); + + + INSTANCE.addConverter(Path.class, List.class, new TypeConverter.Converter() + { + public List convert(Path source) + { + List pathArray = new ArrayList(source.size()); + for(Path.Element element : source) + { + pathArray.add(INSTANCE.convert(String.class, element)); + } + return pathArray; + } + }); + + INSTANCE.addConverter(Path.class, String.class, new TypeConverter.Converter() + { + public String convert(Path source) + { + return source.toString(); + } + }); + + // associations + INSTANCE.addConverter(ChildAssociationRef.class, String.class, new TypeConverter.Converter() + { + public String convert(ChildAssociationRef source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(String.class, ChildAssociationRef.class, new TypeConverter.Converter() + { + public ChildAssociationRef convert(String source) + { + return new ChildAssociationRef(source); + } + }); + + INSTANCE.addConverter(AssociationRef.class, String.class, new TypeConverter.Converter() + { + public String convert(AssociationRef source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(String.class, AssociationRef.class, new TypeConverter.Converter() + { + public AssociationRef convert(String source) + { + return new AssociationRef(source); + } + }); + + INSTANCE.addConverter(Number.class, String.class, new TypeConverter.Converter() + { + public String convert(Number source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(Boolean.class, String.class, new TypeConverter.Converter() + { + public String convert(Boolean source) + { + return source.toString(); + } + }); + + INSTANCE.addConverter(Date.class, String.class, new TypeConverter.Converter() + { + public String convert(Date source) + { + try + { + DateTime dt = new DateTime(source, DateTimeZone.UTC); + return dt.toString(); + } + catch (PlatformRuntimeException e) + { + throw new TypeConversionException("Failed to convert date " + source + " to string", e); + } + } + }); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/StatsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/StatsGet.java new file mode 100644 index 00000000000..9dbce443585 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/StatsGet.java @@ -0,0 +1,196 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.search.StatsParameters; +import org.alfresco.service.cmr.search.StatsProcessor; +import org.alfresco.service.cmr.search.StatsResultSet; +import org.alfresco.service.cmr.search.StatsService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.joda.time.LocalDate; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Retrieves statistics using solr. For a list of potential facets call it with /api/solr/stats?listFacets=true + * You can pass one of these facets in eg. facet=content.creator . The facet name can be used as a I18n resource bundle key, + * it also has a predefined structure: group.property[.type] eg. content.created.datetime. The [.type] is optional, the default is String. + * + * @author Gethin James + */ +public class StatsGet extends DeclarativeWebScript +{ + public static final String DATE_TIME_SUFFIX = "datetime"; + private StatsService stats; + private SiteService siteService; + private Map facets; + private Map postProcessors; + private String statsField; + + public void setFacets(Map facets) + { + this.facets = facets; + } + + public void setStatsField(String statsField) + { + this.statsField = statsField; + } + + public void setStats(StatsService stats) + { + this.stats = stats; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setPostProcessors(Map postProcessors) + { + this.postProcessors = postProcessors; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(2, 1.0f); + Map templateVars = req.getServiceMatch().getTemplateVars(); + SiteInfo siteInfo = null; + + String listFacets = req.getParameter("listFacets"); + if (listFacets != null) + { + model.put("facets", facets.keySet()); + model.put("resultSize", 0); + return model; + } + + if (templateVars != null && templateVars.containsKey("siteId") ) + { + siteInfo = siteService.getSite(templateVars.get("siteId")); + if (siteInfo == null) + { + throw new AccessDeniedException("No such site: " + templateVars.get("siteId")); + } + } + + String facetKey = req.getParameter("facet"); + if (facetKey == null) facetKey = facets.entrySet().iterator().next().getKey(); //default + String query; + + QName propFacet = findFacet(facetKey); + Pair startAndEnd = getStartAndEndDates(req.getParameter("startDate"),req.getParameter("endDate")); + query = buildQuery(siteInfo, facetKey, startAndEnd); + + StatsParameters params = new StatsParameters(SearchService.LANGUAGE_SOLR_FTS_ALFRESCO, query, false); + //params.addSort(new SortDefinition(SortDefinition.SortType.FIELD, this.statsField, false)); + params.addStatsParameter(StatsParameters.PARAM_FIELD, this.statsField); + params.addStatsParameter(StatsParameters.PARAM_FACET, StatsParameters.FACET_PREFIX+propFacet.toString()); + + StatsResultSet result = stats.query(params); + + if (postProcessors.containsKey(facetKey)) + { + StatsProcessor processor = postProcessors.get(facetKey); + result = processor.process(result); + } + model.put("result", result); + model.put("resultSize", result.getStats().size()); + return model; + } + + /** + * Finds a facet based on its key + * @param facetKey String + * @return QName facet + */ + private QName findFacet(String facetKey) + { + if (!facets.containsKey(facetKey)) + { + throw new AccessDeniedException("Invalid facet key:"+facetKey); + } + QName propFacet = QName.createQName(facets.get(facetKey)); + return propFacet; + + } + + protected String buildQuery(SiteInfo siteInfo, String facetKey, Pair startEndDate) + { + StringBuilder luceneQuery = new StringBuilder(); + luceneQuery.append("TYPE:\"" + ContentModel.TYPE_CONTENT + "\""); + + if (startEndDate != null) + { + //QName propFacet = QName.createQName(facets.get(facetKey)); + String dateFacet = ContentModel.PROP_CREATED.toString();//hard coded for now. + luceneQuery.append(" AND "+dateFacet.toString()+":(\""+startEndDate.getFirst()+"\"..\""+startEndDate.getSecond()+"\")"); + } + + if (siteInfo != null) + { + luceneQuery.append(" AND ANCESTOR:\""+siteInfo.getNodeRef().toString()+"\""); + } + return luceneQuery.toString(); + } + + /** + * Parses ISO8601 formatted Date Strings. + * @param start If start is null then defaults to 1 month + * @param end If end is null then it defaults to now(); + */ + public static Pair getStartAndEndDates(String start, String end) + { + if (start == null) return null; + LocalDate startDate = LocalDate.parse(start); + LocalDate endDate = end!=null?LocalDate.parse(end):LocalDate.now(); + return new Pair(startDate, endDate); + } + + /** + * Allows you to add a facet to the list of available facets for Solr Statistics + * @param facetKey e.g. content.mimetype + * @param facetType e.g. {http://www.alfresco.org/model/content/1.0}content.mimetype + */ + public void addFacet(String facetKey, String facetType) + { + facets.put(facetKey, facetType); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/TransactionIntervalGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/TransactionIntervalGet.java new file mode 100644 index 00000000000..fc551a5c204 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/TransactionIntervalGet.java @@ -0,0 +1,69 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.repo.domain.node.ibatis.NodeDAOImpl; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Returns the minimum and the maximum commit time for transactions in a node id range. + * This webscript is used by Shards using DB_ID_RANGE in order to + * identify the transaction time window that needs to be indexed. + * + * @author aborroy + * + */ +public class TransactionIntervalGet extends DeclarativeWebScript +{ + + private NodeDAOImpl nodeDAO; + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status) + { + Long fromNodeId = Long.parseLong(req.getParameter("fromNodeId")); + Long toNodeId = Long.parseLong(req.getParameter("toNodeId")); + + Map model = new HashMap<>(); + model.put("minTransactionCommitTimeMs", nodeDAO.getMinTxInNodeIdRange(fromNodeId, toNodeId)); + model.put("maxTransactionCommitTimeMs", nodeDAO.getMaxTxInNodeIdRange(fromNodeId, toNodeId)); + return model; + } + + public void setNodeDAO(NodeDAOImpl nodeDAO) + { + this.nodeDAO = nodeDAO; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/TransactionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/TransactionsGet.java new file mode 100644 index 00000000000..5ec743ed882 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/solr/TransactionsGet.java @@ -0,0 +1,166 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.solr; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.index.shard.ShardMethodEnum; +import org.alfresco.repo.index.shard.ShardState; +import org.alfresco.repo.index.shard.ShardStateBuilder; +import org.alfresco.repo.solr.SOLRTrackingComponent; +import org.alfresco.repo.solr.Transaction; +import org.alfresco.service.cmr.repository.StoreRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Support for SOLR: Get a list of transactions with a commit time greater than or equal to the given parameter. + * + * @since 4.0 + */ +public class TransactionsGet extends DeclarativeWebScript +{ + protected static final Log logger = LogFactory.getLog(TransactionsGet.class); + + private SOLRTrackingComponent solrTrackingComponent; + + public void setSolrTrackingComponent(SOLRTrackingComponent solrTrackingComponent) + { + this.solrTrackingComponent = solrTrackingComponent; + } + + protected Map executeImpl(WebScriptRequest req, Status status) + { + String minTxnIdParam = req.getParameter("minTxnId"); + String fromCommitTimeParam = req.getParameter("fromCommitTime"); + String maxTxnIdParam = req.getParameter("maxTxnId"); + String toCommitTimeParam = req.getParameter("toCommitTime"); + String maxResultsParam = req.getParameter("maxResults"); + + String baseUrl = req.getParameter("baseUrl"); + String hostName = req.getParameter("hostName"); + String template = req.getParameter("template"); + String instance = req.getParameter("instance"); + String numberOfShards = req.getParameter("numberOfShards"); + String port = req.getParameter("port"); + String stores = req.getParameter("stores"); + String isMaster = req.getParameter("isMaster"); + String hasContent = req.getParameter("hasContent"); + String shardMethod = req.getParameter("shardMethod"); + + String lastUpdated = req.getParameter("lastUpdated"); + String lastIndexedChangeSetCommitTime = req.getParameter("lastIndexedChangeSetCommitTime"); + String lastIndexedChangeSetId = req.getParameter("lastIndexedChangeSetId"); + String lastIndexedTxCommitTime = req.getParameter("lastIndexedTxCommitTime"); + String lastIndexedTxId = req.getParameter("lastIndexedTxId"); + + if(baseUrl != null) + { + ShardState shardState = ShardStateBuilder.shardState() + .withMaster(Boolean.valueOf(isMaster)) + .withLastUpdated(Long.valueOf(lastUpdated)) + .withLastIndexedChangeSetCommitTime(Long.valueOf(lastIndexedChangeSetCommitTime)) + .withLastIndexedChangeSetId(Long.valueOf(lastIndexedChangeSetId)) + .withLastIndexedTxCommitTime(Long.valueOf(lastIndexedTxCommitTime)) + .withLastIndexedTxId(Long.valueOf(lastIndexedTxId)) + .withShardInstance() + .withBaseUrl(baseUrl) + .withPort(Integer.valueOf(port)) + .withHostName(hostName) + .withShard() + .withInstance(Integer.valueOf(instance)) + .withFloc() + .withNumberOfShards(Integer.valueOf(numberOfShards)) + .withTemplate(template) + .withHasContent(Boolean.valueOf(hasContent)) + .withShardMethod(ShardMethodEnum.getShardMethod(shardMethod)) + .endFloc() + .endShard() + .endShardInstance() + .build(); + + for(String store : stores.split(",")) + { + shardState.getShardInstance().getShard().getFloc().getStoreRefs().add(new StoreRef(store)); + } + + for(String pName : req.getParameterNames()) + { + if(pName.startsWith("floc.property.")) + { + String key = pName.substring("floc.property.".length()); + String value = req.getParameter(pName); + shardState.getShardInstance().getShard().getFloc().getPropertyBag().put(key, value); + } + else if(pName.startsWith("state.property.")) + { + String key = pName.substring("state.property.".length()); + String value = req.getParameter(pName); + shardState.getPropertyBag().put(key, value); + } + } + + solrTrackingComponent.registerShardState(shardState); + + } + + + Long minTxnId = (minTxnIdParam == null ? null : Long.valueOf(minTxnIdParam)); + Long fromCommitTime = (fromCommitTimeParam == null ? null : Long.valueOf(fromCommitTimeParam)); + Long maxTxnId = (maxTxnIdParam == null ? null : Long.valueOf(maxTxnIdParam)); + Long toCommitTime = (toCommitTimeParam == null ? null : Long.valueOf(toCommitTimeParam)); + int maxResults = (maxResultsParam == null ? 1024 : Integer.valueOf(maxResultsParam)); + + List transactions = solrTrackingComponent.getTransactions(minTxnId, fromCommitTime, maxTxnId, toCommitTime, maxResults); + + Map model = new HashMap(1, 1.0f); + model.put("transactions", transactions); + + Long maxTxnCommitTime = solrTrackingComponent.getMaxTxnCommitTime(); + if(maxTxnCommitTime != null) + { + model.put("maxTxnCommitTime", maxTxnCommitTime); + } + + Long maxTxnIdOnServer = solrTrackingComponent.getMaxTxnId(); + if(maxTxnIdOnServer != null) + { + model.put("maxTxnId", maxTxnIdOnServer); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/AbstractSubscriptionServiceWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/AbstractSubscriptionServiceWebScript.java new file mode 100644 index 00000000000..b3f3790f6e9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/AbstractSubscriptionServiceWebScript.java @@ -0,0 +1,215 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Date; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.subscriptions.PrivateSubscriptionListException; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; +import org.alfresco.service.cmr.subscriptions.SubscriptionsDisabledException; +import org.alfresco.util.ISO8601DateFormat; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Format; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public abstract class AbstractSubscriptionServiceWebScript extends AbstractWebScript +{ + protected SubscriptionService subscriptionService; + protected NodeService nodeService; + protected PersonService personService; + + public void setSubscriptionService(SubscriptionService subscriptionService) + { + this.subscriptionService = subscriptionService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + if (!subscriptionService.isActive()) + { + res.setStatus(404); + return; + } + + try + { + String userId = req.getServiceMatch().getTemplateVars().get("userid"); + Object obj = executeImpl(userId, req, res); + + if (obj instanceof JSONObject || obj instanceof JSONArray) + { + res.setContentEncoding(Charset.defaultCharset().displayName()); + res.setContentType(Format.JSON.mimetype() + ";charset=UTF-8"); + + Writer writer = res.getWriter(); + if (obj instanceof JSONObject) + { + ((JSONObject) obj).writeJSONString(writer); + } else + { + ((JSONArray) obj).writeJSONString(writer); + } + writer.flush(); + } else + { + res.setStatus(204); + } + } catch (SubscriptionsDisabledException sde) + { + throw new WebScriptException(404, "Subscription service is disabled!", sde); + } catch (NoSuchPersonException nspe) + { + throw new WebScriptException(404, "Unknown user '" + nspe.getUserName() + "'!", nspe); + } catch (PrivateSubscriptionListException psle) + { + throw new WebScriptException(403, "Subscription list is private!", psle); + } catch (ParseException pe) + { + throw new WebScriptException(400, "Unable to parse JSON!", pe); + } catch (ClassCastException cce) + { + throw new WebScriptException(400, "Unable to parse JSON!", cce); + } catch (IOException ioe) + { + throw new WebScriptException(500, "Unable to serialize JSON!", ioe); + } + } + + public abstract Object executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException, + ParseException; + + protected int parseNumber(String name, String number, int def) + { + if (number != null && number.length() > 0) + { + try + { + return Integer.parseInt(number); + + } catch (NumberFormatException e) + { + throw new WebScriptException(400, name + " is not a number!", e); + } + } else + { + return def; + } + } + + protected PagingRequest createPagingRequest(WebScriptRequest req) + { + int skipCount = parseNumber("skipCount", req.getParameter("skipCount"), 0); + int maxItems = parseNumber("maxItems", req.getParameter("maxItems"), -1); + + PagingRequest result = new PagingRequest(skipCount, maxItems, null); + result.setRequestTotalCountMax(Integer.MAX_VALUE); + + return result; + } + + @SuppressWarnings("unchecked") + protected JSONObject getUserDetails(String username) + { + NodeRef node = personService.getPerson(username); + + JSONObject result = new JSONObject(); + result.put("userName", username); + result.put("firstName", nodeService.getProperty(node, ContentModel.PROP_FIRSTNAME)); + result.put("lastName", nodeService.getProperty(node, ContentModel.PROP_LASTNAME)); + result.put("jobtitle", nodeService.getProperty(node, ContentModel.PROP_JOBTITLE)); + result.put("organization", nodeService.getProperty(node, ContentModel.PROP_ORGANIZATION)); + + String status = (String) nodeService.getProperty(node, ContentModel.PROP_USER_STATUS); + if (status != null) + { + result.put("userStatus", status); + } + + Date statusTime = (Date) nodeService.getProperty(node, ContentModel.PROP_USER_STATUS_TIME); + if (statusTime != null) + { + JSONObject statusTimeJson = new JSONObject(); + statusTimeJson.put("iso8601", ISO8601DateFormat.format(statusTime)); + result.put("userStatusTime", statusTimeJson); + } + + // Get the avatar for the user id if one is available + List assocRefs = this.nodeService.getTargetAssocs(node, ContentModel.ASSOC_AVATAR); + if (!assocRefs.isEmpty()) + { + NodeRef avatarNodeRef = assocRefs.get(0).getTargetRef(); + result.put("avatar", avatarNodeRef.toString()); + } + else + { + result.put("avatar", "avatar"); // This indicates to just use a placeholder + } + + return result; + } + + @SuppressWarnings("unchecked") + protected JSONArray getUserArray(List usernames) + { + JSONArray result = new JSONArray(); + + if (usernames != null) + { + for (String username : usernames) + { + result.add(getUserDetails(username)); + } + } + + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowPost.java new file mode 100644 index 00000000000..7ae762bded5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowPost.java @@ -0,0 +1,55 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceFollowPost extends AbstractSubscriptionServiceWebScript +{ + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException, + ParseException + { + JSONArray jsonUsers = (JSONArray) JSONValue.parseWithException(req.getContent().getContent()); + + for (Object o : jsonUsers) + { + String user = (o == null ? null : o.toString()); + if (user != null) + { + subscriptionService.follow(userId, user); + } + } + + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowersCountGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowersCountGet.java new file mode 100644 index 00000000000..f2e1da4cfa4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowersCountGet.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceFollowersCountGet extends AbstractSubscriptionServiceWebScript +{ + @SuppressWarnings("unchecked") + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException + { + int count = subscriptionService.getFollowersCount(userId); + + JSONObject obj = new JSONObject(); + obj.put("count", count); + + return obj; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowersGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowersGet.java new file mode 100644 index 00000000000..142751d3231 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowersGet.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceFollowersGet extends AbstractSubscriptionServiceWebScript +{ + @SuppressWarnings("unchecked") + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException + { + PagingFollowingResults result = subscriptionService.getFollowers(userId, createPagingRequest(req)); + + JSONObject obj = new JSONObject(); + obj.put("people", getUserArray(result.getPage())); + obj.put("hasMoreItems", result.hasMoreItems()); + if (result.getTotalResultCount() != null) + { + obj.put("totalCount", result.getTotalResultCount().getFirst()); + } + + return obj; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowingCountGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowingCountGet.java new file mode 100644 index 00000000000..b6cb66c3cde --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowingCountGet.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceFollowingCountGet extends AbstractSubscriptionServiceWebScript +{ + @SuppressWarnings("unchecked") + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException + { + int count = subscriptionService.getFollowingCount(userId); + + JSONObject obj = new JSONObject(); + obj.put("count", count); + + return obj; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowingGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowingGet.java new file mode 100644 index 00000000000..9c00bea2406 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowingGet.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.json.simple.JSONObject; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceFollowingGet extends AbstractSubscriptionServiceWebScript +{ + @SuppressWarnings("unchecked") + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException + { + PagingFollowingResults result = subscriptionService.getFollowing(userId, createPagingRequest(req)); + + JSONObject obj = new JSONObject(); + obj.put("people", getUserArray(result.getPage())); + obj.put("hasMoreItems", result.hasMoreItems()); + if (result.getTotalResultCount() != null) + { + obj.put("totalCount", result.getTotalResultCount().getFirst()); + } + + return obj; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowsPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowsPost.java new file mode 100644 index 00000000000..7d24f8bd390 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceFollowsPost.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceFollowsPost extends AbstractSubscriptionServiceWebScript +{ + @SuppressWarnings("unchecked") + public JSONArray executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException, + ParseException + { + JSONArray jsonUsers = (JSONArray) JSONValue.parseWithException(req.getContent().getContent()); + + JSONArray result = new JSONArray(); + + for (Object o : jsonUsers) + { + String user = (o == null ? null : o.toString()); + if (user != null) + { + JSONObject item = new JSONObject(); + item.put(user, subscriptionService.follows(userId, user)); + result.add(item); + } + } + + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServicePrivateListGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServicePrivateListGet.java new file mode 100644 index 00000000000..62dd4c7d7fd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServicePrivateListGet.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONObject; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServicePrivateListGet extends AbstractSubscriptionServiceWebScript +{ + @SuppressWarnings("unchecked") + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException, + ParseException + { + JSONObject obj = new JSONObject(); + obj.put("private", subscriptionService.isSubscriptionListPrivate(userId)); + + return obj; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServicePrivateListPut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServicePrivateListPut.java new file mode 100644 index 00000000000..2a2f99db55c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServicePrivateListPut.java @@ -0,0 +1,58 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServicePrivateListPut extends SubscriptionServicePrivateListGet +{ + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException, + ParseException + { + JSONObject obj = (JSONObject) JSONValue.parseWithException(req.getContent().getContent()); + + Object setPrivate = obj.get("private"); + + if (setPrivate != null) + { + if (setPrivate.toString().equalsIgnoreCase("true")) + { + subscriptionService.setSubscriptionListPrivate(userId, true); + } else if (setPrivate.toString().equalsIgnoreCase("false")) + { + subscriptionService.setSubscriptionListPrivate(userId, false); + } + } + + return super.executeImpl(userId, req, res); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceUnfollowPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceUnfollowPost.java new file mode 100644 index 00000000000..2527e317fd0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/subscriptions/SubscriptionServiceUnfollowPost.java @@ -0,0 +1,55 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.subscriptions; + +import java.io.IOException; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class SubscriptionServiceUnfollowPost extends AbstractSubscriptionServiceWebScript +{ + public JSONObject executeImpl(String userId, WebScriptRequest req, WebScriptResponse res) throws IOException, + ParseException + { + JSONArray jsonUsers = (JSONArray) JSONValue.parseWithException(req.getContent().getContent()); + + for (Object o : jsonUsers) + { + String user = (o == null ? null : o.toString()); + if (user != null) + { + subscriptionService.unfollow(userId, user); + } + } + + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/AbstractTenantAdminWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/AbstractTenantAdminWebScript.java new file mode 100644 index 00000000000..b24139263f3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/AbstractTenantAdminWebScript.java @@ -0,0 +1,47 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.tenant; + +import org.alfresco.repo.tenant.TenantAdminService; +import org.springframework.extensions.webscripts.DeclarativeWebScript; + +/** + * @author janv + * @since 4.2 + */ +public abstract class AbstractTenantAdminWebScript extends DeclarativeWebScript +{ + protected static final String TENANT_DOMAIN = "tenantDomain"; + protected static final String TENANT_ADMIN_PASSWORD = "tenantAdminPassword"; + protected static final String TENANT_CONTENT_STORE_ROOT = "tenantContentStoreRoot"; + + protected TenantAdminService tenantAdminService; + + public void setTenantAdminService(TenantAdminService tenantAdminService) + { + this.tenantAdminService = tenantAdminService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantDelete.java new file mode 100644 index 00000000000..c4b8f03a828 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantDelete.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.tenant; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * REST API - delete tenant + * + * @author janv + * @since 4.2 + */ +public class TenantDelete extends AbstractTenantAdminWebScript +{ + protected static final Log logger = LogFactory.getLog(TenantPost.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // get request parameters + Map templateVars = req.getServiceMatch().getTemplateVars(); + String tenantDomain = templateVars.get("tenantDomain"); + + tenantAdminService.deleteTenant(tenantDomain); + + Map model = new HashMap(0); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantPost.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantPost.java new file mode 100644 index 00000000000..693243f2d87 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantPost.java @@ -0,0 +1,94 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.tenant; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * REST API - create tenant + * + * @author janv + * @since 4.2 + */ +public class TenantPost extends AbstractTenantAdminWebScript +{ + protected static final Log logger = LogFactory.getLog(TenantPost.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String tenantDomain = null; + String tenantAdminPassword = null; + String contentStoreRoot = null; + + try + { + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + if (! json.has(TENANT_DOMAIN)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not find required 'tenantDomain' parameter"); + } + tenantDomain = json.getString(TENANT_DOMAIN); + + if (! json.has(TENANT_ADMIN_PASSWORD)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not find required 'tenantAdminPassword' parameter"); + } + tenantAdminPassword = json.getString(TENANT_ADMIN_PASSWORD); + + if (json.has(TENANT_CONTENT_STORE_ROOT)) + { + contentStoreRoot = json.getString(TENANT_CONTENT_STORE_ROOT); + } + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + + tenantAdminService.createTenant(tenantDomain, tenantAdminPassword.toCharArray(), contentStoreRoot); + + Map model = new HashMap(0); + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantsGet.java new file mode 100644 index 00000000000..84e20b33e0f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/tenant/TenantsGet.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.tenant; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.tenant.Tenant; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * REST API - get tenants + * + * TODO filter params - eg. enabled/ disabled and name startsWith + * + * @author janv + * @since 4.2 + */ +public class TenantsGet extends AbstractTenantAdminWebScript +{ + protected static final Log logger = LogFactory.getLog(TenantsGet.class); + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + List tenants = tenantAdminService.getAllTenants(); + + Map model = new HashMap(1); + model.put("tenants", tenants); + + if (logger.isDebugEnabled()) + { + logger.debug("Result: \n\tRequest: " + req + "\n\tModel: " + model); + } + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/AbortTransferCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/AbortTransferCommandProcessor.java new file mode 100644 index 00000000000..9f3a0eaa7b6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/AbortTransferCommandProcessor.java @@ -0,0 +1,146 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.json.JSONWriter; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * This command processor is used to record the start a transfer. No other transfer can be started after this command + * has executed until the started transfer terminates. + * + * @author brian + * + */ +public class AbortTransferCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private TransferReceiver receiver; + + private static Log logger = LogFactory.getLog(AbortTransferCommandProcessor.class); + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + String transferRecordId = null; + + //Read the transfer id from the request + + // Unwrap to a WebScriptServletRequest if we have one + // TODO: Why is this necessary? + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + String transferId = servletRequest.getParameter("transferId"); + + if ((transferId == null)) + { + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + logger.debug("abort transfer:" + transferId); + + receiver.cancel(transferId); + + // return the unique transfer id (the lock id) + StringWriter stringWriter = new StringWriter(300); + JSONWriter jsonWriter = new JSONWriter(stringWriter); + jsonWriter.startObject(); + jsonWriter.writeValue("transferId", transferRecordId); + jsonWriter.endObject(); + String response = stringWriter.toString(); + + resp.setContentType("application/json"); + resp.setContentEncoding("UTF-8"); + int length = response.getBytes("UTF-8").length; + resp.addHeader("Content-Length", "" + length); + resp.setStatus(Status.STATUS_OK); + resp.getWriter().write(response); + + return Status.STATUS_OK; + + } + catch (Exception ex) + { + logger.debug("caught exception", ex); + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java new file mode 100644 index 00000000000..fd6773641fb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/BeginTransferCommandProcessor.java @@ -0,0 +1,186 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.StringWriter; + +import org.alfresco.repo.transfer.RepoTransferReceiverImpl; +import org.alfresco.repo.transfer.TransferCommons; +import org.alfresco.repo.transfer.TransferVersionImpl; +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.alfresco.service.cmr.transfer.TransferVersion; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.json.JSONWriter; + +/** + * This command processor is used to record the start a transfer. No other transfer can be started after this command + * has executed until the started transfer terminates. + * + * @author brian + * + */ +public class BeginTransferCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private TransferReceiver receiver; + + private final static Log logger = LogFactory.getLog(BeginTransferCommandProcessor.class); + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + String transferId = null; + try + { + String [] fromRepositoryIdValues = req.getParameterValues(TransferCommons.PARAM_FROM_REPOSITORYID); + String [] transferToSelfValues = req.getParameterValues(TransferCommons.PARAM_ALLOW_TRANSFER_TO_SELF); + String [] editionValues = req.getParameterValues(TransferCommons.PARAM_VERSION_EDITION); + String [] majorValues = req.getParameterValues(TransferCommons.PARAM_VERSION_MAJOR); + String [] minorValues = req.getParameterValues(TransferCommons.PARAM_VERSION_MINOR); + String [] revisionValues = req.getParameterValues(TransferCommons.PARAM_VERSION_REVISION); + String [] rootFileTransfer = req.getParameterValues(TransferCommons.PARAM_ROOT_FILE_TRANSFER); + + String fromRepositoryId = null; + if(fromRepositoryIdValues != null && fromRepositoryIdValues.length > 0) + { + fromRepositoryId = fromRepositoryIdValues[0]; + } + + boolean transferToSelf = false; + if(transferToSelfValues != null && transferToSelfValues.length > 0) + { + if(transferToSelfValues[0].equalsIgnoreCase("true")) + { + transferToSelf = true; + } + } + + String edition = "Unknown"; + if(editionValues != null && editionValues.length > 0) + { + edition = editionValues[0]; + } + String major = "0"; + if(majorValues != null && majorValues.length > 0) + { + major = majorValues[0]; + } + String minor = "0"; + if(minorValues != null && minorValues.length > 0) + { + minor = minorValues[0]; + } + String revision = "0"; + if(revisionValues != null && revisionValues.length > 0) + { + revision = revisionValues[0]; + } + + TransferVersion fromVersion = new TransferVersionImpl(major, minor, revision, edition); + + //set the root for the root node for the file transfer receiver + //It replaces the root node if transfers on file system + if(rootFileTransfer != null && rootFileTransfer.length > 0 ) + { + receiver.setTransferRootNode(rootFileTransfer[0]); + } + + // attempt to start the transfer + transferId = receiver.start(fromRepositoryId, transferToSelf, fromVersion); + + // Create a temporary folder into which we can place transferred files + receiver.getStagingFolder(transferId); + + TransferVersion version = receiver.getVersion(); + + // return the unique transfer id (the lock id) + StringWriter stringWriter = new StringWriter(1000); + JSONWriter jsonWriter = new JSONWriter(stringWriter); + jsonWriter.startObject(); + + jsonWriter.writeValue(TransferCommons.PARAM_TRANSFER_ID, transferId); + + if(version != null) + { + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_EDITION, version.getEdition()); + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_MAJOR, version.getVersionMajor()); + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_MINOR, version.getVersionMinor()); + jsonWriter.writeValue(TransferCommons.PARAM_VERSION_REVISION, version.getVersionRevision()); + } + jsonWriter.endObject(); + + String response = stringWriter.toString(); + + resp.setContentType("application/json"); + resp.setContentEncoding("UTF-8"); + int length = response.getBytes("UTF-8").length; + resp.addHeader("Content-Length", "" + length); + resp.setStatus(Status.STATUS_OK); + resp.getWriter().write(response); + + logger.debug("transfer started" + transferId); + + return Status.STATUS_OK; + + } + catch (Exception ex) + { + logger.debug("exception caught", ex); + if(transferId != null) + { + logger.debug("ending transfer", ex); + receiver.end(transferId); + } + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/CommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/CommandProcessor.java new file mode 100644 index 00000000000..3a0e4f0e64c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/CommandProcessor.java @@ -0,0 +1,40 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * @author brian + * + */ +public interface CommandProcessor +{ + int process(WebScriptRequest req, WebScriptResponse resp); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/CommitTransferCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/CommitTransferCommandProcessor.java new file mode 100644 index 00000000000..ea2077f6ef9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/CommitTransferCommandProcessor.java @@ -0,0 +1,144 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.json.JSONWriter; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * This command processor is used to record the start a transfer. No other transfer can be started after this command + * has executed until the started transfer terminates. + * + * @author brian + * + */ +public class CommitTransferCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private static Log logger = LogFactory.getLog(CommitTransferCommandProcessor.class); + + private TransferReceiver receiver; + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + + //Read the transfer id from the request + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + String transferId = servletRequest.getParameter("transferId"); + + if ((transferId == null)) + { + logger.debug("transferId is missing"); + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + receiver.commitAsync(transferId); + + // return the unique transfer id (the lock id) + StringWriter stringWriter = new StringWriter(300); + JSONWriter jsonWriter = new JSONWriter(stringWriter); + jsonWriter.startObject(); + jsonWriter.writeValue("transferId", transferId); + jsonWriter.endObject(); + String response = stringWriter.toString(); + + resp.setContentType("application/json"); + resp.setContentEncoding("UTF-8"); + int length = response.getBytes("UTF-8").length; + resp.addHeader("Content-Length", "" + length); + resp.setStatus(Status.STATUS_OK); + resp.getWriter().write(response); + + return Status.STATUS_OK; + } + catch (Exception ex) + { + if (logger.isDebugEnabled()) + { + logger.debug("caught exception :" + ex.toString(), ex); + } + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/MessagesTransferCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/MessagesTransferCommandProcessor.java new file mode 100644 index 00000000000..3dd56a26f86 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/MessagesTransferCommandProcessor.java @@ -0,0 +1,110 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.StringWriter; + +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.json.JSONWriter; + +/** + * This command processor is used to record the start a transfer. No other transfer can be started after this command + * has executed until the started transfer terminates. + * + * @author brian + * + */ +public class MessagesTransferCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private TransferReceiver receiver; + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + String transferRecordId = null; + try + { + // return the unique transfer id (the lock id) + StringWriter stringWriter = new StringWriter(300); + JSONWriter jsonWriter = new JSONWriter(stringWriter); + + jsonWriter.startValue("data"); + + jsonWriter.startArray(); + jsonWriter.startObject(); + //TODO - clearly a dummy message for now. + jsonWriter.writeValue("message", "hello world"); + jsonWriter.endObject(); + jsonWriter.startObject(); + //TODO - clearly a dummy message for now. + jsonWriter.writeValue("message", "message2"); + jsonWriter.endObject(); + jsonWriter.endArray(); + jsonWriter.endValue(); + String response = stringWriter.toString(); + + resp.setContentType("application/json"); + resp.setContentEncoding("UTF-8"); + int length = response.getBytes("UTF-8").length; + resp.addHeader("Content-Length", "" + length); + resp.setStatus(Status.STATUS_OK); + resp.getWriter().write(response); + + return Status.STATUS_OK; + + } catch (Exception ex) + { + receiver.end(transferRecordId); + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PostContentCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PostContentCommandProcessor.java new file mode 100644 index 00000000000..f2605545815 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PostContentCommandProcessor.java @@ -0,0 +1,172 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileItemStream; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; +import org.springframework.extensions.webscripts.servlet.FormData.FormField; + +/** + * This command processor is used to receive one or more content files for a given transfer. + * + * @author brian + * + */ +public class PostContentCommandProcessor implements CommandProcessor +{ + private TransferReceiver receiver; + + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private static Log logger = LogFactory.getLog(PostContentCommandProcessor.class); + + /** + * @param receiver + * the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco.web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + logger.debug("post content start"); + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + if (webScriptServletRequest == null) + { + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + + //Read the transfer id from the request + String transferId = servletRequest.getParameter("transferId"); + + if ((transferId == null) || !ServletFileUpload.isMultipartContent(servletRequest)) + { + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + + ServletFileUpload upload = new ServletFileUpload(); + FileItemIterator iter = upload.getItemIterator(servletRequest); + while (iter.hasNext()) + { + FileItemStream item = iter.next(); + String name = item.getFieldName(); + if (!item.isFormField()) + { + logger.debug("got content Mime Part : " + name); + receiver.saveContent(transferId, item.getName(), item.openStream()); + } + } + +// WebScriptServletRequest alfRequest = (WebScriptServletRequest)req; +// String[] names = alfRequest.getParameterNames(); +// for(String name : names) +// { +// FormField item = alfRequest.getFileField(name); +// +// if(item != null) +// { +// logger.debug("got content Mime Part : " + name); +// receiver.saveContent(transferId, item.getName(), item.getInputStream()); +// } +// else +// { +// //TODO - should this be an exception? +// logger.debug("Unable to get content for Mime Part : " + name); +// } +// } + + logger.debug("success"); + + resp.setStatus(Status.STATUS_OK); + } + catch (Exception ex) + { + logger.debug("exception caught", ex); + if(transferId != null) + { + logger.debug("ending transfer", ex); + receiver.end(transferId); + } + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + + } + + resp.setStatus(Status.STATUS_OK); + return Status.STATUS_OK; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PostSnapshotCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PostSnapshotCommandProcessor.java new file mode 100644 index 00000000000..afe7a29b946 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PostSnapshotCommandProcessor.java @@ -0,0 +1,163 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.OutputStream; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.repo.transfer.TransferCommons; +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileItemStream; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; +import org.springframework.extensions.webscripts.servlet.FormData.FormField; + +/** + * This command processor is used to receive the snapshot for a given transfer. + * + * @author brian + * + */ +public class PostSnapshotCommandProcessor implements CommandProcessor +{ + private TransferReceiver receiver; + + private static Log logger = LogFactory.getLog(PostSnapshotCommandProcessor.class); + + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + /* (non-Javadoc) + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + + int result = Status.STATUS_OK; + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + if (webScriptServletRequest == null) + { + logger.debug("bad request, not assignable from"); + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + //We can't use the WebScriptRequest version of getParameter, since that may cause the content stream + //to be parsed. Get hold of the raw HttpServletRequest and work with that. + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + + //Read the transfer id from the request + String transferId = servletRequest.getParameter("transferId"); + + if ((transferId == null) || !ServletFileUpload.isMultipartContent(servletRequest)) + { + logger.debug("bad request, not multipart"); + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + logger.debug("about to upload manifest file"); + + ServletFileUpload upload = new ServletFileUpload(); + FileItemIterator iter = upload.getItemIterator(servletRequest); + while (iter.hasNext()) + { + FileItemStream item = iter.next(); + if (!item.isFormField() && TransferCommons.PART_NAME_MANIFEST.equals(item.getFieldName())) + { + logger.debug("got manifest file"); + receiver.saveSnapshot(transferId, item.openStream()); + } + } + + logger.debug("success"); + resp.setStatus(Status.STATUS_OK); + + OutputStream out = resp.getOutputStream(); + resp.setContentType("text/xml"); + resp.setContentEncoding("utf-8"); + + receiver.generateRequsite(transferId, out); + + out.close(); + + } + catch (Exception ex) + { + logger.debug("exception caught", ex); + if(transferId != null) + { + logger.debug("ending transfer", ex); + receiver.end(transferId); + } + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + return result; + } + + /** + * @param receiver the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PrepareTransferCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PrepareTransferCommandProcessor.java new file mode 100644 index 00000000000..de4bbc04356 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/PrepareTransferCommandProcessor.java @@ -0,0 +1,145 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.repo.transfer.TransferServiceImpl; +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.json.JSONWriter; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * + * @author mrogers + * + */ +public class PrepareTransferCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private static Log logger = LogFactory.getLog(PrepareTransferCommandProcessor.class); + + private TransferReceiver receiver; + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + String transferRecordId = null; + + //Read the transfer id from the request + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + String transferId = servletRequest.getParameter("transferId"); + + if (transferId == null) + { + logger.debug("transferId is missing"); + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + logger.debug("prepare transferId: " + transferId); + receiver.prepare(transferId); + + // return the unique transfer id (the lock id) + StringWriter stringWriter = new StringWriter(300); + JSONWriter jsonWriter = new JSONWriter(stringWriter); + jsonWriter.startObject(); + jsonWriter.writeValue("transferId", transferRecordId); + jsonWriter.endObject(); + String response = stringWriter.toString(); + + resp.setContentType("application/json"); + resp.setContentEncoding("UTF-8"); + int length = response.getBytes("UTF-8").length; + resp.addHeader("Content-Length", "" + length); + resp.setStatus(Status.STATUS_OK); + resp.getWriter().write(response); + + logger.debug("prepared transferId: " + transferId); + + return Status.STATUS_OK; + } + catch (Exception ex) + { + logger.debug("in exception handler", ex); + receiver.end(transferRecordId); + if (ex instanceof TransferException) + { + throw (TransferException) ex; + } + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/ReportCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/ReportCommandProcessor.java new file mode 100644 index 00000000000..568e54db85b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/ReportCommandProcessor.java @@ -0,0 +1,151 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.BufferedInputStream; +import java.io.OutputStream; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferProgress; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.json.JSONWriter; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * This command processor is used to get the server side transfer report. + * + * @author brian + * + */ +public class ReportCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private TransferReceiver receiver; + + private final static Log logger = LogFactory.getLog(ReportCommandProcessor.class); + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + //Read the transfer id from the request + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + String transferId = servletRequest.getParameter("transferId"); + + if (transferId == null) + { + logger.debug("transferId is missing"); + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + OutputStream out = resp.getOutputStream(); + try + { + resp.setContentType("text/xml"); + resp.setContentEncoding("utf-8"); + + BufferedInputStream br = new BufferedInputStream(receiver.getProgressMonitor().getLogInputStream(transferId)); + try + { + byte[] buffer = new byte[1000]; + int i = br.read(buffer); + while(i > 0) + { + out.write(buffer, 0, i); + i = br.read(buffer); + } + } + finally + { + br.close(); + } + } + finally + { + out.flush(); + out.close(); + } + + return Status.STATUS_OK; + } + catch (TransferException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver + * the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/StatusCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/StatusCommandProcessor.java new file mode 100644 index 00000000000..01c0b53c244 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/StatusCommandProcessor.java @@ -0,0 +1,155 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.service.cmr.transfer.TransferProgress; +import org.alfresco.service.cmr.transfer.TransferReceiver; +import org.alfresco.util.json.ExceptionJsonSerializer; +import org.alfresco.util.json.JsonSerializer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.extensions.webscripts.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; + +/** + * This command processor is used to record the start a transfer. No other transfer can be started after this command + * has executed until the started transfer terminates. + * + * @author brian + * + */ +public class StatusCommandProcessor implements CommandProcessor +{ + private static final String MSG_CAUGHT_UNEXPECTED_EXCEPTION = "transfer_service.receiver.caught_unexpected_exception"; + + private TransferReceiver receiver; + private JsonSerializer errorSerializer = new ExceptionJsonSerializer(); + + private final static Log logger = LogFactory.getLog(StatusCommandProcessor.class); + + /* + * (non-Javadoc) + * + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco .web.scripts.WebScriptRequest, + * org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + //Read the transfer id from the request + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + HttpServletRequest servletRequest = webScriptServletRequest.getHttpServletRequest(); + String transferId = servletRequest.getParameter("transferId"); + + if (transferId == null) + { + logger.debug("transferId is missing"); + resp.setStatus(Status.STATUS_BAD_REQUEST); + return Status.STATUS_BAD_REQUEST; + } + + try + { + TransferProgress progress = receiver.getProgressMonitor().getProgress(transferId); + + if (logger.isDebugEnabled()) + { + logger.debug(progress); + } + + JSONObject progressObject = new JSONObject(); + progressObject.put("transferId", transferId); + progressObject.put("status", progress.getStatus().toString()); + progressObject.put("currentPosition", progress.getCurrentPosition()); + progressObject.put("endPosition", progress.getEndPosition()); + if (progress.getError() != null) + { + JSONObject errorObject = errorSerializer.serialize(progress.getError()); + progressObject.put("error", errorObject); + } + String response = progressObject.toString(); + + resp.setContentType("application/json"); + resp.setContentEncoding("UTF-8"); + int length = response.getBytes("UTF-8").length; + resp.addHeader("Content-Length", "" + length); + resp.setStatus(Status.STATUS_OK); + resp.getWriter().write(response); + + return Status.STATUS_OK; + + } + catch (TransferException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new TransferException(MSG_CAUGHT_UNEXPECTED_EXCEPTION, ex); + } + } + + /** + * @param receiver + * the receiver to set + */ + public void setReceiver(TransferReceiver receiver) + { + this.receiver = receiver; + } + + public void setErrorSerializer(JsonSerializer errorSerializer) + { + this.errorSerializer = errorSerializer; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/TestCredentialsCommandProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/TestCredentialsCommandProcessor.java new file mode 100644 index 00000000000..fe243017f9a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/TestCredentialsCommandProcessor.java @@ -0,0 +1,56 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * This command processor is used simply to check that the transfer receiver is enabled and that the supplied + * credentials are correct and identify an admin user. + * + * @author brian + * + */ +public class TestCredentialsCommandProcessor implements CommandProcessor +{ + + /* (non-Javadoc) + * @see org.alfresco.repo.web.scripts.transfer.CommandProcessor#process(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + public int process(WebScriptRequest req, WebScriptResponse resp) + { + //Since all the checks that are needed are actually carried out by the transfer web script, this processor + //effectively becomes a no-op. + int result = Status.STATUS_OK; + resp.setStatus(result); + return result; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/TransferWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/TransferWebScript.java new file mode 100644 index 00000000000..d36309db678 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/TransferWebScript.java @@ -0,0 +1,138 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.web.scripts.transfer; + +import java.io.IOException; +import java.util.Map; +import java.util.TreeMap; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.transfer.TransferException; +import org.alfresco.util.json.ExceptionJsonSerializer; +import org.alfresco.util.json.JsonSerializer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * @author brian + * + */ +public class TransferWebScript extends AbstractWebScript +{ + private static final Log log = LogFactory.getLog(TransferWebScript.class); + + private boolean enabled = true; + private Map processors = new TreeMap(); + private JsonSerializer errorSerializer = new ExceptionJsonSerializer(); + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public void setCommandProcessors(Map processors) + { + this.processors = new TreeMap(processors); + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScript#execute(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + if (enabled) + { + log.debug("Transfer webscript invoked by user: " + AuthenticationUtil.getFullyAuthenticatedUser() + + " running as " + AuthenticationUtil.getRunAsAuthentication().getName()); + processCommand(req.getServiceMatch().getTemplateVars().get("command"), req, res); + } + else + { + res.setStatus(Status.STATUS_NOT_FOUND); + } + } + + /** + * @param command String + * @param req WebScriptRequest + * @param res WebScriptResponse + */ + private void processCommand(String command, WebScriptRequest req, WebScriptResponse res) + { + log.debug("Received request to process transfer command: " + command); + if (command == null || (command = command.trim()).length() == 0) + { + log.warn("Empty or null command received by the transfer script. Returning \"Not Found\""); + res.setStatus(Status.STATUS_NOT_FOUND); + } + else + { + CommandProcessor processor = processors.get(command); + if (processor != null) + { + log.debug("Found appropriate command processor: " + processor); + try + { + processor.process(req, res); + log.debug("command processed"); + } + catch (TransferException ex) + { + try + { + log.debug("transfer exception caught", ex); + res.setStatus(Status.STATUS_INTERNAL_SERVER_ERROR); + JSONObject errorObject = errorSerializer.serialize(ex); + String error = errorObject.toString(); + + res.setContentType("application/json"); + res.setContentEncoding("UTF-8"); + int length = error.getBytes("UTF-8").length; + res.addHeader("Content-Length", "" + length); + + res.getWriter().write(error); + } + catch (Exception e) + { + //nothing to do at this point really. + } + } + } + else + { + log.warn("No processor found for requested command: " + command + ". Returning \"Not Found\""); + res.setStatus(Status.STATUS_NOT_FOUND); + } + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/package-info.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/package-info.java new file mode 100644 index 00000000000..987c523ef4f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/transfer/package-info.java @@ -0,0 +1,38 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +/** + * Provides the implementation of the transfer service web scripts. Which are used to receive transfers from other + * instances of Alfresco. + *

+ * The code in this package should be considered private to Alfresco and is liable to + * change with each version of Alfresco. + *

+ * @since 3.3 + */ +@PackageMarker +package org.alfresco.repo.web.scripts.transfer; +import org.alfresco.util.PackageMarker; + diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/AbstractWorkflowWebscript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/AbstractWorkflowWebscript.java new file mode 100644 index 00000000000..b7cbb712a6a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/AbstractWorkflowWebscript.java @@ -0,0 +1,418 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.ModelUtil; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.util.StringUtils; + +/** + * Base class for all workflow REST API implementations. + * + * @author Nick Smith + * @author Gavin Cornwell + * @since 3.4 + */ +public abstract class AbstractWorkflowWebscript extends DeclarativeWebScript +{ + public static final String NULL = "null"; + public static final String EMPTY = ""; + + public static final String PARAM_MAX_ITEMS = "maxItems"; + public static final String PARAM_SKIP_COUNT = "skipCount"; + public static final String PARAM_EXCLUDE = "exclude"; + + // used for results pagination: indicates that all items from list should be returned + public static final int DEFAULT_MAX_ITEMS = -1; + + // used for results pagination: indicates that no items should be skipped + public static final int DEFAULT_SKIP_COUNT = 0; + + protected NamespaceService namespaceService; + protected NodeService nodeService; + protected PersonService personService; + protected DictionaryService dictionaryService; + protected AuthenticationService authenticationService; + protected AuthorityService authorityService; + protected WorkflowService workflowService; + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + WorkflowModelBuilder modelBuilder = new WorkflowModelBuilder(namespaceService, nodeService, authenticationService, + personService, workflowService, dictionaryService); + return buildModel(modelBuilder, req, status, cache); + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setWorkflowService(WorkflowService workflowService) + { + this.workflowService = workflowService; + } + + /** + * This method uses a {@link WorkflowModelBuilder} to build up the model to return. + * @param modelBuilder A {@link WorkflowModelBuilder}. + * @param req the {@link WebScriptRequest} + * @param status the {@link Status} + * @param cache the {@link Cache} + * @return the data model. + */ + protected abstract Map buildModel( + WorkflowModelBuilder modelBuilder, + WebScriptRequest req, + Status status, Cache cache); + + + /** + * Processes the given date filter parameter from the provided webscript request. + * + * If the parameter is present but set to an empty string or to "null" the + * date is added to the given filters Map as "", if the parameter + * contains an ISO8601 date it's added as a Date object to the filters. + * + * @param req The WebScript request + * @param paramName The name of the parameter to look for + * @param filters Map of filters to add the date to + */ + protected void processDateFilter(WebScriptRequest req, String paramName, Map filters) + { + // TODO: support other keywords i.e. today, tomorrow + + String dateParam = req.getParameter(paramName); + if (dateParam != null) + { + Object date = EMPTY; + + if (!EMPTY.equals(dateParam) && !NULL.equals(dateParam)) + { + date = getDateParameter(req, paramName); + } + + filters.put(paramName, date); + } + } + + /** + * Retrieves the named paramter as a date. + * + * @param req The WebScript request + * @param paramName The name of parameter to look for + * @return The request parameter value or null if the parameter is not present + */ + protected Date getDateParameter(WebScriptRequest req, String paramName) + { + String dateString = req.getParameter(paramName); + + if (dateString != null) + { + try + { + return ISO8601DateFormat.parse(dateString.replaceAll(" ", "+")); + } + catch (Exception e) + { + String msg = "Invalid date value: " + dateString; + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, msg); + } + } + return null; + } + + /** + * Retrieves the named parameter as an integer, if the parameter is not present the default value is returned + * + * @param req The WebScript request + * @param paramName The name of parameter to look for + * @param defaultValue The default value that should be returned if parameter is not present in request or if it is not positive + * @return The request parameter or default value + */ + protected int getIntParameter(WebScriptRequest req, String paramName, int defaultValue) + { + String paramString = req.getParameter(paramName); + + if (paramString != null) + { + try + { + int param = Integer.valueOf(paramString); + + if (param > 0) + { + return param; + } + } + catch (NumberFormatException e) + { + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage()); + } + } + + return defaultValue; + } + + /** + * Builds the results model, applying pagination to the results if necessary. + * @param req The WebScript request + * @param dataPropertyName The name of the property to use in the model + * @param results The full set of results + * + * @return List of results to return to the callee + */ + protected Map createResultModel(WebScriptRequest req, String dataPropertyName, + List> results) + { + // MNT-9074 My Tasks fails to render if tasks quantity is excessive + int totalItems = 0; + try + { + totalItems = getCapacity(results); + } + catch (Exception e) + { + totalItems = results.size(); + } + + int maxItems = getIntParameter(req, PARAM_MAX_ITEMS, DEFAULT_MAX_ITEMS); + int skipCount = getIntParameter(req, PARAM_SKIP_COUNT, DEFAULT_SKIP_COUNT); + + Map model = new HashMap(); + model.put(dataPropertyName, applyPagination(results, maxItems, skipCount)); + + if (maxItems != DEFAULT_MAX_ITEMS || skipCount != DEFAULT_SKIP_COUNT) + { + // maxItems or skipCount parameter was provided so we need to include paging into response + model.put("paging", ModelUtil.buildPaging(totalItems, maxItems == DEFAULT_MAX_ITEMS ? totalItems : maxItems, skipCount)); + } + + return model; + } + + /** + * Get capacity instaead of size of list + * MNT-9074 My Tasks fails to render if tasks quantity is excessive + * @param list List + * @return capacity of list + * @throws Exception + */ + private int getCapacity(List list) throws Exception { + Field dataField = ArrayList.class.getDeclaredField("elementData"); + dataField.setAccessible(true); + return ((Object[]) dataField.get(list)).length; + } + + /** + * Make the pagination for given list of objects + * + * @param results the initial list of objects for pagination + * @param maxItems maximum count of elements that should be included in paging result + * @param skipCount the count of elements that should be skipped + * @return List of paginated results + */ + protected List> applyPagination(List> results, int maxItems, int skipCount) + { + if (maxItems == DEFAULT_MAX_ITEMS && skipCount == DEFAULT_SKIP_COUNT) + { + // no need to make pagination + return results; + } + + // Do the paging + return ModelUtil.page(results, maxItems, skipCount); + } + + /** + * Determines whether the given date is a match for the given filter value. + * + * @param date The date to check against + * @param filterValue The value of the filter, either an empty String or a Date object + * @param dateBeforeFilter true to test the date is before the filterValue, + * false to test the date is after the filterValue + * @return true if the date is a match for the filterValue + */ + protected boolean isDateMatchForFilter(Date date, Object filterValue, boolean dateBeforeFilter) + { + boolean match = true; + + if (filterValue.equals(EMPTY)) + { + if (date != null) + { + match = false; + } + } + else + { + if (date == null) + { + match = false; + } + else + { + if (dateBeforeFilter) + { + if (date.getTime() >= ((Date)filterValue).getTime()) + { + match = false; + } + } + else + { + if (date.getTime() <= ((Date)filterValue).getTime()) + { + match = false; + } + } + } + } + + return match; + } + + /** + * Helper class to check for excluded items. + */ + public class ExcludeFilter + { + private static final String WILDCARD = "*"; + + private List exactFilters; + private List wilcardFilters; + private boolean containsWildcards = false; + + /** + * Creates a new ExcludeFilter + * + * @param filters Comma separated list of filters which can optionally + * contain wildcards + */ + public ExcludeFilter(String filters) + { + // tokenize the filters + String[] filterArray = StringUtils.tokenizeToStringArray(filters, ","); + + // create a list of exact filters and wildcard filters + this.exactFilters = new ArrayList(filterArray.length); + this.wilcardFilters = new ArrayList(filterArray.length); + + for (String filter : filterArray) + { + if (filter.endsWith(WILDCARD)) + { + // at least one wildcard is present + this.containsWildcards = true; + + // add the filter without the wildcard + this.wilcardFilters.add(filter.substring(0, + (filter.length()-WILDCARD.length()))); + } + else + { + // add the exact filter + this.exactFilters.add(filter); + } + } + } + + /** + * Determines whether the given item matches one of + * the filters. + * + * @param item The item to check + * @return true if the item matches one of the filters + */ + public boolean isMatch(String item) + { + // see whether there is an exact match + boolean match = this.exactFilters.contains(item); + + // if there wasn't an exact match and wildcards are present + if (item != null && !match && this.containsWildcards) + { + for (String wildcardFilter : this.wilcardFilters) + { + if (item.startsWith(wildcardFilter)) + { + match = true; + break; + } + } + } + + return match; + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstanceGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstanceGet.java new file mode 100644 index 00000000000..1c0e4bb4a8b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstanceGet.java @@ -0,0 +1,70 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * @since 3.4 + */ +public class TaskInstanceGet extends AbstractWorkflowWebscript +{ + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + + // getting task id from request parameters + String taskId = params.get("task_instance_id"); + + // searching for task in repository + WorkflowTask workflowTask = workflowService.getTaskById(taskId); + + // task was not found -> return 404 + if (workflowTask == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find workflow task with id: " + taskId); + } + + Map model = new HashMap(); + // build the model for ftl + model.put("workflowTask", modelBuilder.buildDetailed(workflowTask)); + + return model; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstancePut.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstancePut.java new file mode 100644 index 00000000000..0d6aa9c0b83 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstancePut.java @@ -0,0 +1,209 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; +import org.alfresco.service.cmr.workflow.WorkflowException; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.namespace.QName; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * @since 3.4 + */ +public class TaskInstancePut extends AbstractWorkflowWebscript +{ + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + + // getting task id from request parameters + String taskId = params.get("task_instance_id"); + + JSONObject json = null; + + try + { + WorkflowTask workflowTask = workflowService.getTaskById(taskId); + String currentUser = authenticationService.getCurrentUserName(); + + // read request json + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // update task properties + workflowTask = workflowService.updateTask(taskId, parseTaskProperties(json, workflowTask), null, null); + + // task was not found -> return 404 + if (workflowTask == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Failed to find workflow task with id: " + taskId); + } + + // build the model for ftl + Map model = new HashMap(); + model.put("workflowTask", modelBuilder.buildDetailed(workflowTask)); + + return model; + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not read content from request.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from request.", je); + } + catch (AccessDeniedException ade) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Failed to update workflow task with id: " + taskId, ade); + } + catch (WorkflowException we) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Failed to update workflow task with id: " + taskId, we); + } + } + + @SuppressWarnings("unchecked") + private Map parseTaskProperties(JSONObject json, WorkflowTask workflowTask) throws JSONException + { + Map props = new HashMap(); + + // gets the array of properties names + String[] names = JSONObject.getNames(json); + + if (names != null) + { + // array is not empty + for (String name : names) + { + // build the qname of property + QName key = QName.createQName(name.replaceFirst("_", ":"), namespaceService); + Object jsonValue = json.get(name); + + Serializable value = null; + + // process null values + if (jsonValue.equals(JSONObject.NULL)) + { + props.put(key, null); + } + else + { + // gets the property definition from dictionary + PropertyDefinition prop = dictionaryService.getProperty(key); + + if (prop != null) + { + if (prop.isMultiValued() && jsonValue instanceof JSONArray) + { + value = new ArrayList(); + + for (int i = 0; i < ((JSONArray)jsonValue).length(); i++) + { + ((List)value).add((Serializable) DefaultTypeConverter.INSTANCE.convert(prop.getDataType(),((JSONArray)jsonValue).get(i))); + } + } + else + { + // convert property using its data type specified in model + value = (Serializable) DefaultTypeConverter.INSTANCE.convert(prop.getDataType(), jsonValue); + } + } + else + { + // property definition was not found in dictionary + if (jsonValue instanceof JSONArray) + { + value = new ArrayList(); + + for (int i = 0; i < ((JSONArray)jsonValue).length(); i++) + { + ((List)value).add(((JSONArray)jsonValue).getString(i)); + } + } + else + { + // If the JSON returns an Object which is not a String, we use that type. + // Otherwise, we try to convert the string + if (jsonValue instanceof String) + { + // Check if the task already has the property, use that type. + Serializable existingValue = workflowTask.getProperties().get(key); + if (existingValue != null) + { + try + { + value = DefaultTypeConverter.INSTANCE.convert(existingValue.getClass(), jsonValue); + } + catch(TypeConversionException tce) + { + // TODO: is this the right approach, ignoring exception? + // Ignore the exception, revert to using String-value + } + } + else + { + // Revert to using string-value + value = (String) jsonValue; + } + } + else + { + // Use the value provided by JSON + value = (Serializable) jsonValue; + } + } + } + } + + props.put(key, value); + } + } + return props; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstancesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstancesGet.java new file mode 100644 index 00000000000..7f0a825ec5a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/TaskInstancesGet.java @@ -0,0 +1,421 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.workflow.WorkflowModel; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.cmr.workflow.WorkflowTaskQuery; +import org.alfresco.service.cmr.workflow.WorkflowTaskQuery.OrderBy; +import org.alfresco.service.cmr.workflow.WorkflowTaskState; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ModelUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript impelementation to return workflow task instances. + * + * @author Nick Smith + * @author Gavin Cornwell + * @since 3.4 + */ +public class TaskInstancesGet extends AbstractWorkflowWebscript +{ + private static final Log LOGGER = LogFactory.getLog(TaskInstancesGet.class); + + public static final String PARAM_AUTHORITY = "authority"; + public static final String PARAM_STATE = "state"; + public static final String PARAM_PRIORITY = "priority"; + public static final String PARAM_DUE_BEFORE = "dueBefore"; + public static final String PARAM_DUE_AFTER = "dueAfter"; + public static final String PARAM_PROPERTIES = "properties"; + public static final String PARAM_POOLED_TASKS = "pooledTasks"; + public static final String PARAM_PROPERTY = "property"; + public static final String VAR_WORKFLOW_INSTANCE_ID = "workflow_instance_id"; + + private WorkflowTaskDueAscComparator taskComparator = new WorkflowTaskDueAscComparator(); + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + Map filters = new HashMap(4); + + // authority is not included into filters list as it will be taken into account before filtering + String authority = getAuthority(req); + + if (authority == null) + { + // ALF-11036 fix, if authority argument is omitted the tasks for the current user should be returned. + authority = authenticationService.getCurrentUserName(); + } + + // state is also not included into filters list, for the same reason + WorkflowTaskState state = getState(req); + + // look for a workflow instance id + String workflowInstanceId = params.get(VAR_WORKFLOW_INSTANCE_ID); + + // determine if pooledTasks should be included, when appropriate i.e. when an authority is supplied + Boolean pooledTasksOnly = getPooledTasks(req); + + // get list of properties to include in the response + List properties = getProperties(req); + + // get filter param values + filters.put(PARAM_PRIORITY, req.getParameter(PARAM_PRIORITY)); + filters.put(PARAM_PROPERTY, req.getParameter(PARAM_PROPERTY)); + processDateFilter(req, PARAM_DUE_BEFORE, filters); + processDateFilter(req, PARAM_DUE_AFTER, filters); + + String excludeParam = req.getParameter(PARAM_EXCLUDE); + if (excludeParam != null && excludeParam.length() > 0) + { + filters.put(PARAM_EXCLUDE, new ExcludeFilter(excludeParam)); + } + + List allTasks; + + if (workflowInstanceId != null) + { + // a workflow instance id was provided so query for tasks + WorkflowTaskQuery taskQuery = new WorkflowTaskQuery(); + taskQuery.setActive(null); + taskQuery.setProcessId(workflowInstanceId); + taskQuery.setTaskState(state); + taskQuery.setOrderBy(new OrderBy[]{OrderBy.TaskDue_Asc}); + + if (authority != null) + { + taskQuery.setActorId(authority); + } + + allTasks = workflowService.queryTasks(taskQuery); + } + else + { + // default task state to IN_PROGRESS if not supplied + if (state == null) + { + state = WorkflowTaskState.IN_PROGRESS; + } + + // no workflow instance id is present so get all tasks + if (authority != null) + { + List tasks = workflowService.getAssignedTasks(authority, state, true); + List pooledTasks = workflowService.getPooledTasks(authority, true); + if (pooledTasksOnly != null) + { + if (pooledTasksOnly.booleanValue()) + { + // only return pooled tasks the user can claim + allTasks = new ArrayList(pooledTasks.size()); + allTasks.addAll(pooledTasks); + } + else + { + // only return tasks assigned to the user + allTasks = new ArrayList(tasks.size()); + allTasks.addAll(tasks); + } + } + else + { + // include both assigned and unassigned tasks + allTasks = new ArrayList(tasks.size() + pooledTasks.size()); + allTasks.addAll(tasks); + allTasks.addAll(pooledTasks); + } + + // sort tasks by due date + Collections.sort(allTasks, taskComparator); + } + else + { + // authority was not provided -> return all active tasks in the system + WorkflowTaskQuery taskQuery = new WorkflowTaskQuery(); + taskQuery.setTaskState(state); + taskQuery.setActive(null); + taskQuery.setOrderBy(new OrderBy[] { OrderBy.TaskDue_Asc }); + allTasks = workflowService.queryTasks(taskQuery); + } + } + + int maxItems = getIntParameter(req, PARAM_MAX_ITEMS, DEFAULT_MAX_ITEMS); + int skipCount = getIntParameter(req, PARAM_SKIP_COUNT, DEFAULT_SKIP_COUNT); + int totalCount = 0; + ArrayList> results = new ArrayList>(); + + // Filter results + for (WorkflowTask task : allTasks) + { + if (matches(task, filters)) + { + // Total-count needs to be based on matching tasks only, so we can't just use allTasks.size() for this + totalCount++; + if(totalCount > skipCount && (maxItems < 0 || maxItems > results.size())) + { + // Only build the actual detail if it's in the range of items we need. This will + // drastically improve performance over paging after building the model + results.add(modelBuilder.buildSimple(task, properties)); + } + } + } + + Map model = new HashMap(); + model.put("taskInstances", results); + + if (maxItems != DEFAULT_MAX_ITEMS || skipCount != DEFAULT_SKIP_COUNT) + { + // maxItems or skipCount parameter was provided so we need to include paging into response + model.put("paging", ModelUtil.buildPaging(totalCount, maxItems == DEFAULT_MAX_ITEMS ? totalCount : maxItems, skipCount)); + } + + // create and return results, paginated if necessary + return model; + } + + /** + * Retrieves the list of property names to include in the response. + * + * @param req The WebScript request + * @return List of property names + */ + private List getProperties(WebScriptRequest req) + { + String propertiesStr = req.getParameter(PARAM_PROPERTIES); + if (propertiesStr != null) + { + return Arrays.asList(propertiesStr.split(",")); + } + return null; + } + + /** + * Retrieves the pooledTasks parameter. + * + * @param req The WebScript request + * @return null if not present, Boolean object otherwise + */ + private Boolean getPooledTasks(WebScriptRequest req) + { + Boolean result = null; + String includePooledTasks = req.getParameter(PARAM_POOLED_TASKS); + + if (includePooledTasks != null) + { + result = Boolean.valueOf(includePooledTasks); + } + + return result; + } + + /** + * Gets the specified {@link WorkflowTaskState}, null if not requested + * + * @param req WebScriptRequest + * @return WorkflowTaskState + */ + private WorkflowTaskState getState(WebScriptRequest req) + { + String stateName = req.getParameter(PARAM_STATE); + if (stateName != null) + { + try + { + return WorkflowTaskState.valueOf(stateName.toUpperCase()); + } + catch (IllegalArgumentException e) + { + String msg = "Unrecognised State parameter: " + stateName; + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, msg); + } + } + + return null; + } + + /** + * Returns the specified authority. If no authority is specified then returns the current Fully Authenticated user. + * @param req WebScriptRequest + * @return String + */ + private String getAuthority(WebScriptRequest req) + { + String authority = req.getParameter(PARAM_AUTHORITY); + if (authority == null || authority.length() == 0) + { + authority = null; + } + return authority; + } + + /** + * Determine if the given task should be included in the response. + * + * @param task The task to check + * @param filters The list of filters the task must match to be included + * @return true if the task matches and should therefore be returned + */ + private boolean matches(WorkflowTask task, Map filters) + { + // by default we assume that workflow task should be included + boolean result = true; + + for (String key : filters.keySet()) + { + Object filterValue = filters.get(key); + + // skip null filters (null value means that filter was not specified) + if (filterValue != null) + { + if (key.equals(PARAM_EXCLUDE)) + { + ExcludeFilter excludeFilter = (ExcludeFilter)filterValue; + String type = task.getDefinition().getMetadata().getName().toPrefixString(this.namespaceService); + if (excludeFilter.isMatch(type)) + { + result = false; + break; + } + } + else if (key.equals(PARAM_DUE_BEFORE)) + { + Date dueDate = (Date)task.getProperties().get(WorkflowModel.PROP_DUE_DATE); + + if (!isDateMatchForFilter(dueDate, filterValue, true)) + { + result = false; + break; + } + } + else if (key.equals(PARAM_DUE_AFTER)) + { + Date dueDate = (Date)task.getProperties().get(WorkflowModel.PROP_DUE_DATE); + + if (!isDateMatchForFilter(dueDate, filterValue, false)) + { + result = false; + break; + } + } + else if (key.equals(PARAM_PRIORITY)) + { + if (!filterValue.equals(task.getProperties().get(WorkflowModel.PROP_PRIORITY).toString())) + { + result = false; + break; + } + } + else if(key.equals(PARAM_PROPERTY)) + { + int propQNameEnd = filterValue.toString().indexOf('/'); + if (propQNameEnd < 1) + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Ignoring invalid property filter:" + filterValue.toString()); + } + break; + } + String propValue = filterValue.toString().substring(propQNameEnd + 1); + if (propValue.isEmpty()) + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Ignoring empty property value filter [" + propValue + "]"); + } + break; + } + String propQNameStr = filterValue.toString().substring(0, propQNameEnd); + QName propertyQName; + try + { + propertyQName = QName.createQName(propQNameStr, namespaceService); + } + catch (Exception ex) + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Ignoring invalid QName property filter [" + propQNameStr + "]"); + } + break; + } + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Filtering with property [" + propertyQName.toPrefixString(namespaceService) + "=" + propValue + "]"); + } + Serializable value = task.getProperties().get(propertyQName); + if (value != null && !value.equals(propValue)) + { + result = false; + break; + } + } + } + } + + return result; + } + + /** + * Comparator to sort workflow tasks by due date in ascending order. + */ + class WorkflowTaskDueAscComparator implements Comparator + { + @Override + public int compare(WorkflowTask o1, WorkflowTask o2) + { + Date date1 = (Date)o1.getProperties().get(WorkflowModel.PROP_DUE_DATE); + Date date2 = (Date)o2.getProperties().get(WorkflowModel.PROP_DUE_DATE); + + long time1 = date1 == null ? Long.MAX_VALUE : date1.getTime(); + long time2 = date2 == null ? Long.MAX_VALUE : date2.getTime(); + + long result = time1 - time2; + + return (result > 0) ? 1 : (result < 0 ? -1 : 0); + } + + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowDefinitionGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowDefinitionGet.java new file mode 100644 index 00000000000..330724eda44 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowDefinitionGet.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.workflow.WorkflowDefinition; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript impelementation to return the latest version of a workflow + * definition. + * + * @author Frederik Heremans + * @since 3.4.e + */ +public class WorkflowDefinitionGet extends AbstractWorkflowWebscript +{ + private static final String PARAM_WORKFLOW_DEFINITION_ID = "workflowDefinitionId"; + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + + // Get the definition id from the params + String workflowDefinitionId = params.get(PARAM_WORKFLOW_DEFINITION_ID); + + WorkflowDefinition workflowDefinition = workflowService.getDefinitionById(workflowDefinitionId); + + // Workflow definition is not found, 404 + if (workflowDefinition == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, + "Unable to find workflow definition with id: " + workflowDefinitionId); + } + + Map model = new HashMap(); + model.put("workflowDefinition", modelBuilder.buildDetailed(workflowDefinition)); + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowDefinitionsGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowDefinitionsGet.java new file mode 100644 index 00000000000..6d68efe6a56 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowDefinitionsGet.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.workflow.WorkflowDefinition; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Webscript impelementation to return the latest version of all deployed + * workflow definitions. + * + * @author Gavin Cornwell + * @author Nick Smith + * @since 3.4 + */ +public class WorkflowDefinitionsGet extends AbstractWorkflowWebscript +{ + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + ExcludeFilter excludeFilter = null; + String excludeParam = req.getParameter(PARAM_EXCLUDE); + if (excludeParam != null && excludeParam.length() > 0) + { + excludeFilter = new ExcludeFilter(excludeParam); + } + + // list all workflow's definitions simple representation + List workflowDefinitions = workflowService.getDefinitions(); + + ArrayList> results = new ArrayList>(); + + for (WorkflowDefinition workflowDefinition : workflowDefinitions) + { + // if present, filter out excluded definitions + if (excludeFilter == null || !excludeFilter.isMatch(workflowDefinition.getName())) + { + results.add(modelBuilder.buildSimple(workflowDefinition)); + } + } + + Map model = new HashMap(); + model.put("workflowDefinitions", results); + return model; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceDelete.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceDelete.java new file mode 100644 index 00000000000..dded581f7a5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceDelete.java @@ -0,0 +1,171 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.io.Serializable; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.workflow.WorkflowConstants; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Web Script implementation of delete or cancel workflow instance. + * + * @author Gavin Cornwell + * @since 3.4 + */ +public class WorkflowInstanceDelete extends AbstractWorkflowWebscript +{ + public static final String PARAM_FORCED = "forced"; + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + + // getting workflow instance id from request parameters + String workflowInstanceId = params.get("workflow_instance_id"); + + // determine if instance should be cancelled or deleted + boolean forced = getForced(req); + + if (canUserEndWorkflow(workflowInstanceId)) + { + if (forced) + { + workflowService.deleteWorkflow(workflowInstanceId); + } + else + { + workflowService.cancelWorkflow(workflowInstanceId); + } + + return null; + } + else + { + throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Failed to " + + (forced ? "delete" : "cancel") + " workflow instance with id: " + workflowInstanceId); + } + } + + private boolean getForced(WebScriptRequest req) + { + String forced = req.getParameter(PARAM_FORCED); + if (forced != null) + { + try + { + return Boolean.valueOf(forced); + } + catch (Exception e) + { + // do nothing, false will be returned + } + } + + // Defaults to false. + return false; + } + + /** + * Determines if the current user can cancel or delete the + * workflow instance with the given id. Throws a WebscriptException + * with status-code 404 if workflow-instance to delete wasn't found. + * + * @param instanceId The id of the workflow instance to check + * @return true if the user can end the workflow, false otherwise + */ + private boolean canUserEndWorkflow(String instanceId) + { + boolean canEnd = false; + + // get the initiator + WorkflowInstance wi = workflowService.getWorkflowById(instanceId); + + if (wi == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, + "The workflow instance to delete/cancel with id " + instanceId + " doesn't exist: "); + } + + String currentUserName = authenticationService.getCurrentUserName(); + + // ALF-17405: Admin can always delete/cancel workflows, regardless of the initiator + if(authorityService.isAdminAuthority(currentUserName)) + { + canEnd = true; + } + else + { + String ownerName = null; + // Determine if the current user is the initiator of the workflow. + // Get the username of the initiator. + NodeRef initiator = wi.getInitiator(); + if (initiator != null && nodeService.exists(initiator)) + { + ownerName = (String) nodeService.getProperty(initiator, ContentModel.PROP_USERNAME); + } + else + { + /* + * Fix for MNT-14411 : Re-created user can't cancel created task. + * If owner value can't be found on workflow properties + * because initiator nodeRef no longer exists get owner from + * initiatorhome nodeRef owner property. + */ + WorkflowTask startTask = workflowService.getStartTask(wi.getId()); + Map props = startTask.getProperties(); + ownerName = (String) props.get(ContentModel.PROP_OWNER); + if (ownerName == null) + { + NodeRef initiatorHomeNodeRef = (NodeRef) props.get(QName.createQName("", WorkflowConstants.PROP_INITIATOR_HOME)); + if (initiatorHomeNodeRef != null) + { + ownerName = (String) nodeService.getProperty(initiatorHomeNodeRef, ContentModel.PROP_OWNER); + } + } + } + + // if the current user started the workflow allow the cancel action + if (currentUserName.equals(ownerName)) + { + canEnd = true; + } + } + return canEnd; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceDiagramGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceDiagramGet.java new file mode 100644 index 00000000000..74d979e548b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceDiagramGet.java @@ -0,0 +1,92 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.web.scripts.content.StreamContent; +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.util.TempFileProvider; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.util.FileCopyUtils; + +/** + * Java backed implementation for REST API to retrieve a diagram of a workflow instance. + * + * @author Gavin Cornwell + * @since 4.0 + */ +public class WorkflowInstanceDiagramGet extends StreamContent +{ + protected WorkflowService workflowService; + + public void setWorkflowService(WorkflowService workflowService) + { + this.workflowService = workflowService; + } + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + Map params = req.getServiceMatch().getTemplateVars(); + + // getting workflow instance id from request parameters + String workflowInstanceId = params.get("workflow_instance_id"); + + WorkflowInstance workflowInstance = workflowService.getWorkflowById(workflowInstanceId); + + // workflow instance was not found -> return 404 + if (workflowInstance == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find workflow instance with id: " + workflowInstanceId); + } + + // check whether there is a diagram available + if (!workflowService.hasWorkflowImage(workflowInstanceId)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find diagram for workflow instance with id: " + workflowInstanceId); + } + + // copy image data into temporary file + File file = TempFileProvider.createTempFile("workflow-diagram-", ".png"); + InputStream imageData = workflowService.getWorkflowImage(workflowInstanceId); + OutputStream os = new FileOutputStream(file); + FileCopyUtils.copy(imageData, os); + + // stream temporary file back to client + streamContent(req, res, file); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceGet.java new file mode 100644 index 00000000000..ca38929f626 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstanceGet.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * @since 3.4 + */ +public class WorkflowInstanceGet extends AbstractWorkflowWebscript +{ + public static final String PARAM_INCLUDE_TASKS = "includeTasks"; + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + + // getting workflow instance id from request parameters + String workflowInstanceId = params.get("workflow_instance_id"); + + boolean includeTasks = getIncludeTasks(req); + + WorkflowInstance workflowInstance = workflowService.getWorkflowById(workflowInstanceId); + + // task was not found -> return 404 + if (workflowInstance == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find workflow instance with id: " + workflowInstanceId); + } + + Map model = new HashMap(); + // build the model for ftl + model.put("workflowInstance", modelBuilder.buildDetailed(workflowInstance, includeTasks)); + + return model; + } + + private boolean getIncludeTasks(WebScriptRequest req) + { + String includeTasks = req.getParameter(PARAM_INCLUDE_TASKS); + if (includeTasks != null) + { + try + { + return Boolean.valueOf(includeTasks); + } + catch (Exception e) + { + // do nothing, false will be returned + } + } + + // Defaults to false. + return false; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstancesForNodeGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstancesForNodeGet.java new file mode 100644 index 00000000000..736dcdfb666 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstancesForNodeGet.java @@ -0,0 +1,74 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author unknown + * @since 3.4 + */ +public class WorkflowInstancesForNodeGet extends AbstractWorkflowWebscript +{ + + public static final String PARAM_STORE_TYPE = "store_type"; + public static final String PARAM_STORE_ID = "store_id"; + public static final String PARAM_NODE_ID = "id"; + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + Map params = req.getServiceMatch().getTemplateVars(); + + // get nodeRef from request + NodeRef nodeRef = new NodeRef(params.get(PARAM_STORE_TYPE), params.get(PARAM_STORE_ID), params.get(PARAM_NODE_ID)); + + // list all active workflows for nodeRef + List workflows = workflowService.getWorkflowsForContent(nodeRef, true); + + List> results = new ArrayList>(workflows.size()); + + for (WorkflowInstance workflow : workflows) + { + results.add(modelBuilder.buildSimple(workflow)); + } + + Map model = new HashMap(); + // build the model for ftl + model.put("workflowInstances", results); + + return model; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstancesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstancesGet.java new file mode 100644 index 00000000000..6fda9458250 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowInstancesGet.java @@ -0,0 +1,292 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.workflow.WorkflowModel; +import org.alfresco.service.cmr.workflow.WorkflowDefinition; +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.alfresco.service.cmr.workflow.WorkflowInstanceQuery; +import org.alfresco.service.cmr.workflow.WorkflowInstanceQuery.DatePosition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.util.StringUtils; + +/** + * Java backed implementation for REST API to retrieve workflow instances. + * + * @author Gavin Cornwell + * @since 3.4 + */ +public class WorkflowInstancesGet extends AbstractWorkflowWebscript +{ + public static final String PARAM_STATE = "state"; + public static final String PARAM_INITIATOR = "initiator"; + public static final String PARAM_PRIORITY = "priority"; + public static final String PARAM_DUE_BEFORE = "dueBefore"; + public static final String PARAM_DUE_AFTER = "dueAfter"; + public static final String PARAM_STARTED_BEFORE = "startedBefore"; + public static final String PARAM_STARTED_AFTER = "startedAfter"; + public static final String PARAM_COMPLETED_BEFORE = "completedBefore"; + public static final String PARAM_COMPLETED_AFTER = "completedAfter"; + public static final String PARAM_DEFINITION_NAME = "definitionName"; + public static final String PARAM_DEFINITION_ID = "definitionId"; + public static final String VAR_DEFINITION_ID = "workflow_definition_id"; + + public static final QName QNAME_INITIATOR = QName.createQName(NamespaceService.DEFAULT_URI, "initiator"); + + private WorkflowInstanceDueAscComparator workflowComparator = new WorkflowInstanceDueAscComparator(); + + @Override + protected Map buildModel(WorkflowModelBuilder modelBuilder, WebScriptRequest req, Status status, Cache cache) + { + WorkflowInstanceQuery workflowInstanceQuery = new WorkflowInstanceQuery(); + + Map params = req.getServiceMatch().getTemplateVars(); + + // state is not included into filters list as it will be taken into account before filtering + WorkflowState state = getState(req); + + // get filter param values + Map filters = new HashMap(9); + + if (req.getParameter(PARAM_INITIATOR) != null) + filters.put(QNAME_INITIATOR, personService.getPerson(req.getParameter(PARAM_INITIATOR))); + + if (req.getParameter(PARAM_PRIORITY) != null) + filters.put(WorkflowModel.PROP_WORKFLOW_PRIORITY, req.getParameter(PARAM_PRIORITY)); + + String excludeParam = req.getParameter(PARAM_EXCLUDE); + if (excludeParam != null && excludeParam.length() > 0) + { + workflowInstanceQuery.setExcludedDefinitions(Arrays.asList(StringUtils.tokenizeToStringArray(excludeParam, ","))); + } + + // process all the date related parameters + Map dateParams = new HashMap(); + Date dueBefore = getDateFromRequest(req, PARAM_DUE_BEFORE); + if (dueBefore != null) + { + dateParams.put(DatePosition.BEFORE, dueBefore); + } + Date dueAfter = getDateFromRequest(req, PARAM_DUE_AFTER); + if (dueAfter != null) + { + dateParams.put(DatePosition.AFTER, dueAfter); + } + if (dateParams.isEmpty()) + { + if (req.getParameter(PARAM_DUE_BEFORE) != null || req.getParameter(PARAM_DUE_AFTER) != null) + { + filters.put(WorkflowModel.PROP_WORKFLOW_DUE_DATE, null); + } + } + else + { + filters.put(WorkflowModel.PROP_WORKFLOW_DUE_DATE, dateParams); + } + + workflowInstanceQuery.setStartBefore(getDateFromRequest(req, PARAM_STARTED_BEFORE)); + workflowInstanceQuery.setStartAfter(getDateFromRequest(req, PARAM_STARTED_AFTER)); + workflowInstanceQuery.setEndBefore(getDateFromRequest(req, PARAM_COMPLETED_BEFORE)); + workflowInstanceQuery.setEndAfter(getDateFromRequest(req, PARAM_COMPLETED_AFTER)); + + // determine if there is a definition id to filter by + String workflowDefinitionId = params.get(VAR_DEFINITION_ID); + if (workflowDefinitionId == null) + { + workflowDefinitionId = req.getParameter(PARAM_DEFINITION_ID); + } + + // default workflow state to ACTIVE if not supplied + if (state == null) + { + state = WorkflowState.ACTIVE; + } + + workflowInstanceQuery.setActive(state == WorkflowState.ACTIVE); + workflowInstanceQuery.setCustomProps(filters); + + List workflows = new ArrayList(); + + int total = 0; + // MNT-9074 My Tasks fails to render if tasks quantity is excessive + int maxItems = getIntParameter(req, PARAM_MAX_ITEMS, DEFAULT_MAX_ITEMS); + int skipCount = getIntParameter(req, PARAM_SKIP_COUNT, DEFAULT_SKIP_COUNT); + + if (workflowDefinitionId == null && req.getParameter(PARAM_DEFINITION_NAME) != null) + { + /** + * If we are searching by workflow definition name then there may be many workflow definition instances. + */ + int workingSkipCount = skipCount; + + /** + * Yes there could be multiple process definitions with that definition name + */ + String definitionName = req.getParameter(PARAM_DEFINITION_NAME); + + List defs = workflowService.getAllDefinitionsByName(definitionName); + + int itemsToQuery = maxItems; + + for(WorkflowDefinition def : defs) + { + workflowDefinitionId = def.getId(); + workflowInstanceQuery.setWorkflowDefinitionId(workflowDefinitionId); + if(maxItems < 0 || itemsToQuery > 0) + { + workflows.addAll(workflowService.getWorkflows(workflowInstanceQuery, itemsToQuery, workingSkipCount)); + } + if(maxItems > 0) + { + itemsToQuery = maxItems - workflows.size(); + } + + total += (int) workflowService.countWorkflows(workflowInstanceQuery); + + if(workingSkipCount > 0) + { + workingSkipCount = skipCount - total; + + if(workingSkipCount < 0) + { + workingSkipCount = 0; + } + } + } + } + else + { + /** + * This is the old single task implementation + */ + if (workflowDefinitionId != null) + { + workflowInstanceQuery.setWorkflowDefinitionId(workflowDefinitionId); + } + workflows.addAll(workflowService.getWorkflows(workflowInstanceQuery, maxItems, skipCount)); + + total = (int) workflowService.countWorkflows(workflowInstanceQuery); + } + + List> results = new ArrayList>(total); + + // init empty list + results.addAll(Arrays.asList((Map[]) new Map[total])); + + for (WorkflowInstance workflow : workflows) + { + // set to special index + results.set(skipCount, modelBuilder.buildSimple(workflow)); + + skipCount++; + } + + // create and return results, paginated if necessary + return createResultModel(req, "workflowInstances", results); + } + + /** + * Gets the specified {@link WorkflowState}, null if not requested. + * + * @param req The WebScript request + * @return The workflow state or null if not requested + */ + private WorkflowState getState(WebScriptRequest req) + { + String stateName = req.getParameter(PARAM_STATE); + if (stateName != null) + { + try + { + return WorkflowState.valueOf(stateName.toUpperCase()); + } + catch (IllegalArgumentException e) + { + String msg = "Unrecognised State parameter: " + stateName; + throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, msg); + } + } + + return null; + } + + // enum to represent workflow states + private enum WorkflowState + { + ACTIVE, COMPLETED; + } + + private Date getDateFromRequest(WebScriptRequest req, String paramName) + { + String dateParam = req.getParameter(paramName); + if (dateParam != null) + { + if (!EMPTY.equals(dateParam) && !NULL.equals(dateParam)) + { + return getDateParameter(req, paramName); + } + } + + return null; + } + + /** + * Comparator to sort workflow instances by due date in ascending order. + */ + class WorkflowInstanceDueAscComparator implements Comparator + { + @Override + public int compare(WorkflowInstance o1, WorkflowInstance o2) + { + Date date1 = o1.getDueDate(); + Date date2 = o2.getDueDate(); + + long time1 = date1 == null ? Long.MAX_VALUE : date1.getTime(); + long time2 = date2 == null ? Long.MAX_VALUE : date2.getTime(); + + long result = time1 - time2; + + return (result > 0) ? 1 : (result < 0 ? -1 : 0); + } + + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowModelBuilder.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowModelBuilder.java new file mode 100644 index 00000000000..e2b3e3d2d21 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/workflow/WorkflowModelBuilder.java @@ -0,0 +1,732 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.scripts.workflow; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.constraint.ListOfValuesConstraint; +import org.alfresco.repo.workflow.WorkflowModel; +import org.alfresco.repo.workflow.WorkflowQNameConverter; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.Constraint; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.workflow.WorkflowDefinition; +import org.alfresco.service.cmr.workflow.WorkflowInstance; +import org.alfresco.service.cmr.workflow.WorkflowNode; +import org.alfresco.service.cmr.workflow.WorkflowPath; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.cmr.workflow.WorkflowTaskDefinition; +import org.alfresco.service.cmr.workflow.WorkflowTaskQuery; +import org.alfresco.service.cmr.workflow.WorkflowTaskState; +import org.alfresco.service.cmr.workflow.WorkflowTransition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.alfresco.util.collections.CollectionUtils; +import org.alfresco.util.collections.Function; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.surf.util.ISO8601DateFormat; + +/** + * @author Nick Smith + * @since 3.4 + */ +public class WorkflowModelBuilder +{ + public static final String PERSON_LAST_NAME = "lastName"; + public static final String PERSON_FIRST_NAME = "firstName"; + public static final String PERSON_USER_NAME = "userName"; + public static final String PERSON_AVATAR = "avatarUrl"; + + public static final String TASK_PROPERTIES = "properties"; + public static final String TASK_PROPERTIY_LABELS = "propertyLabels"; + public static final String TASK_OWNER = "owner"; + public static final String TASK_CREATOR = "creator"; + public static final String TASK_STATE = "state"; + public static final String TASK_DESCRIPTION = "description"; + public static final String TASK_TITLE = "title"; + public static final String TASK_NAME = "name"; + public static final String TASK_URL = "url"; + public static final String TASK_IS_POOLED = "isPooled"; + public static final String TASK_IS_EDITABLE = "isEditable"; + public static final String TASK_IS_REASSIGNABLE = "isReassignable"; + public static final String TASK_IS_CLAIMABLE = "isClaimable"; + public static final String TASK_IS_RELEASABLE = "isReleasable"; + public static final String TASK_ID = "id"; + public static final String TASK_PATH = "path"; + public static final String TASK_DEFINITION = "definition"; + public static final String TASK_OUTCOME = "outcome"; + + public static final String TASK_DEFINITION_ID = "id"; + public static final String TASK_DEFINITION_URL = "url"; + public static final String TASK_DEFINITION_TYPE = "type"; + public static final String TASK_DEFINITION_NODE = "node"; + + public static final String TASK_WORKFLOW_INSTANCE = "workflowInstance"; + + public static final String TASK_WORKFLOW_INSTANCE_ID = "id"; + public static final String TASK_WORKFLOW_INSTANCE_URL = "url"; + public static final String TASK_WORKFLOW_INSTANCE_NAME = "name"; + public static final String TASK_WORKFLOW_INSTANCE_TITLE = "title"; + public static final String TASK_WORKFLOW_INSTANCE_DESCRIPTION = "description"; + public static final String TASK_WORKFLOW_INSTANCE_MESSAGE = "message"; + public static final String TASK_WORKFLOW_INSTANCE_IS_ACTIVE = "isActive"; + public static final String TASK_WORKFLOW_INSTANCE_START_DATE = "startDate"; + public static final String TASK_WORKFLOW_INSTANCE_DUE_DATE = "dueDate"; + public static final String TASK_WORKFLOW_INSTANCE_END_DATE = "endDate"; + public static final String TASK_WORKFLOW_INSTANCE_PRIORITY = "priority"; + public static final String TASK_WORKFLOW_INSTANCE_INITIATOR = "initiator"; + public static final String TASK_WORKFLOW_INSTANCE_CONTEXT = "context"; + public static final String TASK_WORKFLOW_INSTANCE_PACKAGE = "package"; + public static final String TASK_WORKFLOW_INSTANCE_START_TASK_INSTANCE_ID = "startTaskInstanceId"; + public static final String TASK_WORKFLOW_INSTANCE_DEFINITION = "definition"; + public static final String TASK_WORKFLOW_INSTANCE_TASKS = "tasks"; + public static final String TASK_WORKFLOW_INSTANCE_DEFINITION_URL = "definitionUrl"; + public static final String TASK_WORKFLOW_INSTANCE_DIAGRAM_URL = "diagramUrl"; + + public static final String TASK_WORKFLOW_INSTANCE_INITIATOR_USERNAME = "userName"; + public static final String TASK_WORKFLOW_INSTANCE_INITIATOR_FIRSTNAME = "firstName"; + public static final String TASK_WORKFLOW_INSTANCE_INITIATOR_LASTNAME = "lastName"; + + public static final String TYPE_DEFINITION_NAME = "name"; + public static final String TYPE_DEFINITION_TITLE = "title"; + public static final String TYPE_DEFINITION_DESCRIPTION = "description"; + public static final String TYPE_DEFINITION_URL = "url"; + + public static final String WORKFLOW_NODE_NAME = "name"; + public static final String WORKFLOW_NODE_TITLE = "title"; + public static final String WORKFLOW_NODE_DESCRIPTION = "description"; + public static final String WORKFLOW_NODE_IS_TASK_NODE = "isTaskNode"; + public static final String WORKFLOW_NODE_TRANSITIONS = "transitions"; + + public static final String WORKFLOW_NODE_TRANSITION_ID = "id"; + public static final String WORKFLOW_NODE_TRANSITION_TITLE = "title"; + public static final String WORKFLOW_NODE_TRANSITION_DESCRIPTION = "description"; + public static final String WORKFLOW_NODE_TRANSITION_IS_DEFAULT = "isDefault"; + public static final String WORKFLOW_NODE_TRANSITION_IS_HIDDEN = "isHidden"; + + public static final String WORKFLOW_DEFINITION_ID = "id"; + public static final String WORKFLOW_DEFINITION_URL = "url"; + public static final String WORKFLOW_DEFINITION_NAME = "name"; + public static final String WORKFLOW_DEFINITION_TITLE = "title"; + public static final String WORKFLOW_DEFINITION_DESCRIPTION = "description"; + public static final String WORKFLOW_DEFINITION_VERSION = "version"; + public static final String WORKFLOW_DEFINITION_START_TASK_DEFINITION_URL = "startTaskDefinitionUrl"; + public static final String WORKFLOW_DEFINITION_START_TASK_DEFINITION_TYPE = "startTaskDefinitionType"; + public static final String WORKFLOW_DEFINITION_TASK_DEFINITIONS = "taskDefinitions"; + + public static final String TASK_OUTCOME_MESSAGE_PREFIX = "workflowtask.outcome."; + + private final NodeService nodeService; + private final PersonService personService; + private final WorkflowService workflowService; + private final DictionaryService dictionaryService; + private final AuthenticationService authenticationService; + private final WorkflowQNameConverter qNameConverter; + + public WorkflowModelBuilder(NamespaceService namespaceService, NodeService nodeService, + AuthenticationService authenticationService, PersonService personService, + WorkflowService workflowService, DictionaryService dictionaryService) + { + this.nodeService = nodeService; + this.personService = personService; + this.workflowService = workflowService; + this.authenticationService = authenticationService; + this.qNameConverter = new WorkflowQNameConverter(namespaceService); + this.dictionaryService = dictionaryService; + } + + /** + * Returns a simple representation of a {@link WorkflowTask}. + * @param task The task to be represented. + * @param propertyFilters Specify which properties to include. + * @return Map + */ + public Map buildSimple(WorkflowTask task, Collection propertyFilters) + { + // get current username + String currentUser = this.authenticationService.getCurrentUserName(); + + HashMap model = new HashMap(); + model.put(TASK_ID, task.getId()); + model.put(TASK_URL, getUrl(task)); + model.put(TASK_NAME, task.getName()); + model.put(TASK_TITLE, task.getTitle()); + model.put(TASK_DESCRIPTION, task.getDescription()); + model.put(TASK_STATE, task.getState().name()); + model.put(TASK_PATH, getUrl(task.getPath())); + model.put(TASK_OUTCOME, getOutcome(task)); + model.put(TASK_IS_POOLED, isPooled(task.getProperties())); + model.put(TASK_IS_EDITABLE, this.workflowService.isTaskEditable(task, currentUser, false)); + model.put(TASK_IS_REASSIGNABLE, this.workflowService.isTaskReassignable(task, currentUser, false)); + model.put(TASK_IS_CLAIMABLE, this.workflowService.isTaskClaimable(task, currentUser, false)); + model.put(TASK_IS_RELEASABLE, this.workflowService.isTaskReleasable(task, currentUser, false)); + + Serializable owner = task.getProperties().get(ContentModel.PROP_OWNER); + model.put(TASK_OWNER, getPersonModel(owner)); + + Serializable creator = task.getProperties().get(ContentModel.PROP_CREATOR); + model.put(TASK_CREATOR, getPersonModel(creator)); + + // task properties + Map propertyModel = buildProperties(task, propertyFilters); + model.put(TASK_PROPERTIES, propertyModel); + model.put(TASK_PROPERTIY_LABELS, buildPropertyLabels(task, propertyModel)); + + // workflow instance part + model.put(TASK_WORKFLOW_INSTANCE, buildSimple(task.getPath().getInstance())); + + return model; + } + + /** + * Returns a detailed representation of a {@link WorkflowTask}. + * @param workflowTask The task to be represented. + * @return Map + */ + public Map buildDetailed(WorkflowTask workflowTask) + { + Map model = buildSimple(workflowTask, null); + + // definition part + model.put(TASK_DEFINITION, buildTaskDefinition(workflowTask.getDefinition(), workflowTask)); + + return model; + } + + /** + * Returns a simple representation of a {@link WorkflowInstance}. + * @param workflowInstance The workflow instance to be represented. + * @return Map + */ + public Map buildSimple(WorkflowInstance workflowInstance) + { + Map model = new HashMap(); + + model.put(TASK_WORKFLOW_INSTANCE_ID, workflowInstance.getId()); + model.put(TASK_WORKFLOW_INSTANCE_URL, getUrl(workflowInstance)); + model.put(TASK_WORKFLOW_INSTANCE_NAME, workflowInstance.getDefinition().getName()); + model.put(TASK_WORKFLOW_INSTANCE_TITLE, workflowInstance.getDefinition().getTitle()); + model.put(TASK_WORKFLOW_INSTANCE_DESCRIPTION, workflowInstance.getDefinition().getDescription()); + model.put(TASK_WORKFLOW_INSTANCE_MESSAGE, workflowInstance.getDescription()); + model.put(TASK_WORKFLOW_INSTANCE_IS_ACTIVE, workflowInstance.isActive()); + model.put(TASK_WORKFLOW_INSTANCE_PRIORITY, workflowInstance.getPriority()); + model.put(TASK_WORKFLOW_INSTANCE_DEFINITION_URL, getUrl(workflowInstance.getDefinition())); + + if (workflowInstance.getWorkflowPackage() != null) + { + model.put(TASK_WORKFLOW_INSTANCE_PACKAGE, workflowInstance.getWorkflowPackage().toString()); + } + + if (workflowInstance.getContext() != null) + { + model.put(TASK_WORKFLOW_INSTANCE_CONTEXT, workflowInstance.getContext().toString()); + } + + if (workflowInstance.getStartDate() == null) + { + model.put(TASK_WORKFLOW_INSTANCE_START_DATE, workflowInstance.getStartDate()); + } + else + { + model.put(TASK_WORKFLOW_INSTANCE_START_DATE, ISO8601DateFormat.format(workflowInstance.getStartDate())); + } + + if (workflowInstance.getDueDate() == null) + { + model.put(TASK_WORKFLOW_INSTANCE_DUE_DATE, workflowInstance.getDueDate()); + } + else + { + model.put(TASK_WORKFLOW_INSTANCE_DUE_DATE, ISO8601DateFormat.format(workflowInstance.getDueDate())); + } + + if (workflowInstance.getEndDate() == null) + { + model.put(TASK_WORKFLOW_INSTANCE_END_DATE, workflowInstance.getEndDate()); + } + else + { + model.put(TASK_WORKFLOW_INSTANCE_END_DATE, ISO8601DateFormat.format(workflowInstance.getEndDate())); + } + + if (workflowInstance.getInitiator() == null || !nodeService.exists(workflowInstance.getInitiator())) + { + model.put(TASK_WORKFLOW_INSTANCE_INITIATOR, null); + } + else + { + model.put(TASK_WORKFLOW_INSTANCE_INITIATOR, getPersonModel(nodeService.getProperty(workflowInstance.getInitiator(), ContentModel.PROP_USERNAME))); + } + + return model; + } + + /** + * Returns a detailed representation of a {@link WorkflowInstance}. + * @param workflowInstance The workflow instance to be represented. + * @param includeTasks should we include task in model? + * @return Map + */ + public Map buildDetailed(WorkflowInstance workflowInstance, boolean includeTasks) + { + Map model = buildSimple(workflowInstance); + + Serializable startTaskId = null; + WorkflowTask startTask = workflowService.getStartTask(workflowInstance.getId()); + if (startTask != null) + { + startTaskId = startTask.getId(); + } + + if (workflowService.hasWorkflowImage(workflowInstance.getId())) + { + model.put(TASK_WORKFLOW_INSTANCE_DIAGRAM_URL, getDiagramUrl(workflowInstance)); + } + + model.put(TASK_WORKFLOW_INSTANCE_START_TASK_INSTANCE_ID, startTaskId); + model.put(TASK_WORKFLOW_INSTANCE_DEFINITION, buildDetailed(workflowInstance.getDefinition())); + + if (includeTasks) + { + // get all tasks for workflow + WorkflowTaskQuery tasksQuery = new WorkflowTaskQuery(); + tasksQuery.setTaskState(null); + tasksQuery.setActive(null); + tasksQuery.setProcessId(workflowInstance.getId()); + List tasks = workflowService.queryTasks(tasksQuery); + + ArrayList> results = new ArrayList>(tasks.size()); + + for (WorkflowTask task : tasks) + { + results.add(buildSimple(task, null)); + } + + model.put(TASK_WORKFLOW_INSTANCE_TASKS, results); + } + + return model; + } + + /** + * Returns a simple representation of a {@link WorkflowDefinition}. + * + * @param workflowDefinition the WorkflowDefinition object to be represented. + * @return Map + */ + public Map buildSimple(WorkflowDefinition workflowDefinition) + { + HashMap model = new HashMap(); + + model.put(WORKFLOW_DEFINITION_ID, workflowDefinition.getId()); + model.put(WORKFLOW_DEFINITION_URL, getUrl(workflowDefinition)); + model.put(WORKFLOW_DEFINITION_NAME, workflowDefinition.getName()); + model.put(WORKFLOW_DEFINITION_TITLE, workflowDefinition.getTitle()); + model.put(WORKFLOW_DEFINITION_DESCRIPTION, workflowDefinition.getDescription()); + model.put(WORKFLOW_DEFINITION_VERSION, workflowDefinition.getVersion()); + + return model; + } + + /** + * Returns a detailed representation of a {@link WorkflowDefinition}. + * + * @param workflowDefinition the WorkflowDefinition object to be represented. + * @return Map + */ + public Map buildDetailed(WorkflowDefinition workflowDefinition) + { + Map model = buildSimple(workflowDefinition); + + model.put(WORKFLOW_DEFINITION_START_TASK_DEFINITION_URL, getUrl(workflowDefinition.getStartTaskDefinition().getMetadata())); + model.put(WORKFLOW_DEFINITION_START_TASK_DEFINITION_TYPE, workflowDefinition.getStartTaskDefinition().getMetadata().getName()); + + ArrayList> results = new ArrayList>(); + for (WorkflowTaskDefinition taskDefinition : workflowService.getTaskDefinitions(workflowDefinition.getId())) + { + if (taskDefinition.getId().equals(workflowDefinition.getStartTaskDefinition().getId())) + { + continue; + } + + Map result = new HashMap(); + + result.put(TASK_DEFINITION_URL, getUrl(taskDefinition.getMetadata())); + result.put(TASK_DEFINITION_TYPE, taskDefinition.getMetadata().getName()); + + results.add(result); + } + model.put(WORKFLOW_DEFINITION_TASK_DEFINITIONS, results); + + return model; + } + + private Object isPooled(Map properties) + { + Collection actors = (Collection) properties.get(WorkflowModel.ASSOC_POOLED_ACTORS); + return actors != null && !actors.isEmpty(); + } + + private Map buildProperties(WorkflowTask task, Collection propertyFilters) + { + Map properties = task.getProperties(); + Collection keys; + if (propertyFilters == null || propertyFilters.size() == 0) + { + TypeDefinition taskType = task.getDefinition().getMetadata(); + Map propDefs = taskType.getProperties(); + Map assocDefs = taskType.getAssociations(); + Set propKeys = properties.keySet(); + keys = new HashSet(propDefs.size() + assocDefs.size() + propKeys.size()); + keys.addAll(propDefs.keySet()); + keys.addAll(assocDefs.keySet()); + keys.addAll(propKeys); + keys.add(WorkflowModel.PROP_HIDDEN_TRANSITIONS); + } + else + { + keys = buildQNameKeys(propertyFilters); + } + + Map result = buildQNameProperties(properties, keys, task); + + // ALF-18092: Special handling for the "hiddenTransitions" property, as it can be an empty string + if (keys.contains(WorkflowModel.PROP_HIDDEN_TRANSITIONS)) + { + List hiddenTransitions = getHiddenTransitions(properties); + if (hiddenTransitions != null) + { + result.put(qNameConverter.mapQNameToName(WorkflowModel.PROP_HIDDEN_TRANSITIONS), hiddenTransitions); + } + } + return result; + } + + private Map buildPropertyLabels(WorkflowTask task, Map properties) + { + TypeDefinition taskType = task.getDefinition().getMetadata(); + final Map propDefs = taskType.getProperties(); + return CollectionUtils.transform(properties, new Function, Pair>() + { + @Override + public Pair apply(Entry entry) + { + String propName = entry.getKey(); + PropertyDefinition propDef = propDefs.get(qNameConverter.mapNameToQName(propName)); + if(propDef != null ) + { + List constraints = propDef.getConstraints(); + for (ConstraintDefinition constraintDef : constraints) + { + Constraint constraint = constraintDef.getConstraint(); + if (constraint instanceof ListOfValuesConstraint) + { + ListOfValuesConstraint listConstraint = (ListOfValuesConstraint) constraint; + String label = listConstraint.getDisplayLabel(String.valueOf(entry.getValue()), dictionaryService); + return new Pair(propName, label); + } + } + } + return null; + } + }); + } + + private Map buildQNameProperties(Map properties, Collection keys, + WorkflowTask task) + { + Map propDefs = task.getDefinition().getMetadata().getProperties(); + Map model = new HashMap(); + for (QName key : keys) + { + Object value = convertValue(properties.get(key)); + String strKey = qNameConverter.mapQNameToName(key); + PropertyDefinition propDef = propDefs.get(key); + if ((value == null) && (propDef != null)) + { + value = propDef.getDefaultValue(); + } + model.put(strKey, value); + } + return model; + } + + private Object convertValue(Object value) + { + if (value == null || value instanceof Boolean || value instanceof Number || value instanceof String) + { + return value; + } + + if (value instanceof Collection) + { + Collection collection = (Collection) value; + ArrayList results = new ArrayList(collection.size()); + for (Object obj : collection) + { + results.add(convertValue(obj)); + } + return results; + } + + return DefaultTypeConverter.INSTANCE.convert(String.class, value); + } + + private Collection buildQNameKeys(Collection keys) + { + return CollectionUtils.transform(keys, new Function() + { + @Override + public QName apply(String name) + { + return qNameConverter.mapNameToQName(name); + } + }); + } + + private Map getPersonModel(Serializable nameSer) + { + if (!(nameSer instanceof String)) + return null; + + String name = (String) nameSer; + + // TODO Person URL? + Map model = new HashMap(); + model.put(PERSON_USER_NAME, name); + + if (personService.personExists(name)) + { + NodeRef person = personService.getPerson(name); + Map properties = nodeService.getProperties(person); + model.put(PERSON_FIRST_NAME, properties.get(ContentModel.PROP_FIRSTNAME)); + model.put(PERSON_LAST_NAME, properties.get(ContentModel.PROP_LASTNAME)); + + // add the avatar, id present + List avatar = nodeService.getTargetAssocs(person, ContentModel.ASSOC_AVATAR); + if (avatar != null && !avatar.isEmpty()) + { + model.put(PERSON_AVATAR, getAvatarUrl(avatar.get(0).getTargetRef())); + } + } + + return model; + } + + private Map buildTaskDefinition(WorkflowTaskDefinition workflowTaskDefinition, WorkflowTask workflowTask) + { + Map model = new HashMap(); + + model.put(TASK_DEFINITION_ID, workflowTaskDefinition.getId()); + model.put(TASK_DEFINITION_URL, getUrl(workflowTaskDefinition)); + model.put(TASK_DEFINITION_TYPE, buildTypeDefinition(workflowTaskDefinition.getMetadata())); + model.put(TASK_DEFINITION_NODE, buildWorkflowNode(workflowTaskDefinition.getNode(), workflowTask)); + + return model; + } + + private Map buildTypeDefinition(TypeDefinition typeDefinition) + { + Map model = new HashMap(); + + model.put(TYPE_DEFINITION_NAME, typeDefinition.getName()); + model.put(TYPE_DEFINITION_TITLE, typeDefinition.getTitle(dictionaryService)); + model.put(TYPE_DEFINITION_DESCRIPTION, typeDefinition.getDescription(dictionaryService)); + model.put(TYPE_DEFINITION_URL, getUrl(typeDefinition)); + + return model; + } + + private Map buildWorkflowNode(WorkflowNode workflowNode, WorkflowTask workflowTask) + { + Map model = new HashMap(); + + model.put(WORKFLOW_NODE_NAME, workflowNode.getName()); + model.put(WORKFLOW_NODE_TITLE, workflowNode.getTitle()); + model.put(WORKFLOW_NODE_DESCRIPTION, workflowNode.getDescription()); + model.put(WORKFLOW_NODE_IS_TASK_NODE, workflowNode.isTaskNode()); + + List> transitions = new ArrayList>(); + List hiddenTransitions = getHiddenTransitions(workflowTask.getProperties()); + for (WorkflowTransition workflowTransition : workflowNode.getTransitions()) + { + Map transitionModel = buildTransition(workflowTransition, hiddenTransitions); + transitions.add(transitionModel); + } + model.put(WORKFLOW_NODE_TRANSITIONS, transitions); + return model; + } + + private Map buildTransition(WorkflowTransition workflowTransition, List hiddenTransitions) + { + Map model = new HashMap(); + String id = workflowTransition.getId(); + model.put(WORKFLOW_NODE_TRANSITION_ID, id == null ? "" : id); + model.put(WORKFLOW_NODE_TRANSITION_TITLE, workflowTransition.getTitle()); + model.put(WORKFLOW_NODE_TRANSITION_DESCRIPTION, workflowTransition.getDescription()); + model.put(WORKFLOW_NODE_TRANSITION_IS_DEFAULT, workflowTransition.isDefault()); + model.put(WORKFLOW_NODE_TRANSITION_IS_HIDDEN, isHiddenTransition(id, hiddenTransitions)); + return model; + } + + /** + * @param properties Map + * @return List + */ + private List getHiddenTransitions(Map properties) + { + Serializable hiddenSer = properties.get(WorkflowModel.PROP_HIDDEN_TRANSITIONS); + if (hiddenSer instanceof List) + { + return (List) hiddenSer; + } + else if (hiddenSer instanceof String) + { + if(((String) hiddenSer).isEmpty()) + { + return Collections.emptyList(); + } + else + { + String hiddenStr = (String) hiddenSer; + return Arrays.asList(hiddenStr.split(",")); + } + } + else if (hiddenSer == null) + { + return Collections.emptyList(); + } + return null; + } + + private boolean isHiddenTransition(String transitionId, List hiddenTransitions) + { + if (hiddenTransitions == null) + return false; + + return hiddenTransitions.contains(transitionId); + } + + private String getOutcome(WorkflowTask task) + { + String outcomeLabel = null; + + // there will only be an outcome if the task is completed + if (task.getState().equals(WorkflowTaskState.COMPLETED)) + { + String outcomeId = (String)task.getProperties().get(WorkflowModel.PROP_OUTCOME); + if (outcomeId != null) + { + // find the transition with the matching id and get the label + WorkflowTransition[] transitions = task.getDefinition().getNode().getTransitions(); + for (WorkflowTransition transition : transitions) + { + if (transition.getId() != null && transition.getId().equals(outcomeId)) + { + outcomeLabel = transition.getTitle(); + break; + } + } + if (outcomeLabel == null) + { + String translatedOutcome = I18NUtil.getMessage(TASK_OUTCOME_MESSAGE_PREFIX+outcomeId); + if (translatedOutcome != null) + { + outcomeLabel = translatedOutcome; + } + else + { + outcomeLabel = outcomeId; + } + } + } + } + + return outcomeLabel; + } + + private String getUrl(WorkflowTask task) + { + return "api/task-instances/" + task.getId(); + } + + private String getUrl(WorkflowDefinition workflowDefinition) + { + return "api/workflow-definitions/" + workflowDefinition.getId(); + } + + private String getUrl(WorkflowTaskDefinition workflowTaskDefinition) + { + return "api/task-definitions/" + workflowTaskDefinition.getId(); + } + + private String getUrl(TypeDefinition typeDefinition) + { + return "api/classes/" + qNameConverter.mapQNameToName(typeDefinition.getName()); + } + + private String getUrl(WorkflowPath path) + { + return "api/workflow-paths/" + path.getId(); + } + + private String getUrl(WorkflowInstance workflowInstance) + { + return "api/workflow-instances/" + workflowInstance.getId(); + } + + private String getDiagramUrl(WorkflowInstance workflowInstance) + { + return "api/workflow-instances/" + workflowInstance.getId() + "/diagram"; + } + + private String getAvatarUrl(NodeRef avatarRef) + { + return "api/node/" + avatarRef.toString().replace("://", "/") + "/content/thumbnails/avatar"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/HttpRangeProcessor.java b/remote-api/src/main/java/org/alfresco/repo/web/util/HttpRangeProcessor.java new file mode 100644 index 00000000000..49007d3d51b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/HttpRangeProcessor.java @@ -0,0 +1,660 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Generates HTTP response for "Range" scoped HTTP requests for content. + */ +public class HttpRangeProcessor +{ + private static final Log logger = LogFactory.getLog(HttpRangeProcessor.class); + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final String HEADER_CONTENT_RANGE = "Content-Range"; + private static final String HEADER_CONTENT_LENGTH = "Content-Length"; + private static final String MULTIPART_BYTERANGES_BOUNDRY = ""; + private static final String MULTIPART_BYTERANGES_HEADER = "multipart/byteranges; boundary=" + MULTIPART_BYTERANGES_BOUNDRY; + private static final String MULTIPART_BYTERANGES_BOUNDRY_SEP = "--" + MULTIPART_BYTERANGES_BOUNDRY; + private static final String MULTIPART_BYTERANGES_BOUNDRY_END = MULTIPART_BYTERANGES_BOUNDRY_SEP + "--"; + /** size of a multi-part byte range output buffer */ + private static final int CHUNKSIZE = 64*1024; + private ContentService contentService; + + + /** + * Constructor. + * + * @param contentService ContentService + */ + public HttpRangeProcessor(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Process a range header for a HttpServletResponse - handles single and multiple range requests. + * + * @param res the HTTP servlet response + * @param reader the content reader + * @param range the byte range + * @param ref the content NodeRef + * @param property the content property + * @param mimetype the content mimetype + * @param userAgent the user agent string + * @return whether or not the range could be processed + * @throws IOException + */ + public boolean processRange(HttpServletResponse res, ContentReader reader, String range, + NodeRef ref, QName property, String mimetype, String userAgent) + throws IOException + { + // test for multiple byte ranges present in header + if (range.indexOf(',') == -1) + { + return processSingleRange(res, reader, range, mimetype); + } + else + { + return processMultiRange(res, range, ref, property, mimetype, userAgent); + } + } + + /** + * Process a range header for a WebScriptResponse - handles single and multiple range requests. + * + * @param res the webscript response + * @param reader the content reader + * @param range the byte range + * @param ref the content NodeRef + * @param property the content property + * @param mimetype the content mimetype + * @param userAgent the user agent string + * @return whether or not the range could be processed + * @throws IOException + */ + public boolean processRange(WebScriptResponse res, ContentReader reader, String range, + NodeRef ref, QName property, String mimetype, String userAgent) + throws IOException + { + // test for multiple byte ranges present in header + if (range.indexOf(',') == -1) + { + return processSingleRange(res, reader, range, mimetype); + } + else + { + return processMultiRange(res, range, ref, property, mimetype, userAgent); + } + } + + /** + * Process a single range request. + * + * @param res HttpServletResponse + * @param reader ContentReader to retrieve content + * @param range Range header value + * @param mimetype Content mimetype + * + * @return true if processed range, false otherwise + */ + private boolean processSingleRange(Object res, ContentReader reader, String range, String mimetype) + throws IOException + { + // Handle either HttpServletResponse or WebScriptResponse + HttpServletResponse httpServletResponse = null; + WebScriptResponse webScriptResponse = null; + if (res instanceof HttpServletResponse) + { + httpServletResponse = (HttpServletResponse) res; + } + else if (res instanceof WebScriptResponse) + { + webScriptResponse = (WebScriptResponse) res; + } + if (httpServletResponse == null && webScriptResponse == null) + { + // Unknown response object type + return false; + } + + // return the specific set of bytes as requested in the content-range header + + /* Examples of byte-content-range-spec values, assuming that the entity contains total of 1234 bytes: + The first 500 bytes: + bytes 0-499/1234 + The second 500 bytes: + bytes 500-999/1234 + All except for the first 500 bytes: + bytes 500-1233/1234 */ + /* 'Range' header example: + bytes=10485760-20971519 */ + + boolean processedRange = false; + Range r = null; + try + { + r = Range.constructRange(range, mimetype, reader.getSize()); + } + catch (IllegalArgumentException err) + { + if (getLogger().isDebugEnabled()) + getLogger().debug("Failed to parse range header - returning 416 status code: " + err.getMessage()); + + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + httpServletResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + httpServletResponse.getOutputStream().close(); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + webScriptResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + webScriptResponse.getOutputStream().close(); + } + return true; + } + + // set Partial Content status and range headers + String contentRange = "bytes " + Long.toString(r.start) + + "-" + Long.toString(r.end) + "/" + Long.toString(reader.getSize()); + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + httpServletResponse.setContentType(mimetype); + httpServletResponse.setHeader(HEADER_CONTENT_RANGE, contentRange); + httpServletResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L)); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + webScriptResponse.setContentType(mimetype); + webScriptResponse.setHeader(HEADER_CONTENT_RANGE, contentRange); + webScriptResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString((r.end - r.start) + 1L)); + } + + if (getLogger().isDebugEnabled()) + getLogger().debug("Processing: Content-Range: " + contentRange); + + InputStream is = null; + try + { + // output the binary data for the range + OutputStream os = null; + if (httpServletResponse != null) + { + os = httpServletResponse.getOutputStream(); + } + else if (webScriptResponse != null) + { + os = webScriptResponse.getOutputStream(); + } + is = reader.getContentInputStream(); + + streamRangeBytes(r, is, os, 0L); + + os.close(); + processedRange = true; + } + catch (IOException err) + { + if (getLogger().isDebugEnabled()) + getLogger().debug("Unable to process single range due to IO Exception: " + err.getMessage()); + throw err; + } + finally + { + if (is != null) is.close(); + } + + return processedRange; + } + + /** + * Process multiple ranges. + * + * @param res HttpServletResponse + * @param range Range header value + * @param ref NodeRef to the content for streaming + * @param property Content Property for the content + * @param mimetype Mimetype of the content + * @param userAgent User Agent of the caller + * + * @return true if processed range, false otherwise + */ + private boolean processMultiRange( + Object res, String range, NodeRef ref, QName property, String mimetype, String userAgent) + throws IOException + { + final Log logger = getLogger(); + + // Handle either HttpServletResponse or WebScriptResponse + HttpServletResponse httpServletResponse = null; + WebScriptResponse webScriptResponse = null; + if (res instanceof HttpServletResponse) + { + httpServletResponse = (HttpServletResponse) res; + } + else if (res instanceof WebScriptResponse) + { + webScriptResponse = (WebScriptResponse) res; + } + if (httpServletResponse == null && webScriptResponse == null) + { + // Unknown response object type + return false; + } + + // return the sets of bytes as requested in the content-range header + // the response will be formatted as multipart/byteranges media type message + + /* Examples of byte-ranges-specifier values (assuming an entity-body of length 10000): + + - The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499 + - The second 500 bytes (byte offsets 500-999, inclusive): + bytes=500-999 + - The final 500 bytes (byte offsets 9500-9999, inclusive): + bytes=-500 + - Or bytes=9500- + - The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 + - Several legal but not canonical specifications of byte offsets 500-999, inclusive: + bytes=500-600,601-999 + bytes=500-700,601-999 */ + + boolean processedRange = false; + + // get the content reader + ContentReader reader = contentService.getReader(ref, property); + + final List ranges = new ArrayList(8); + long entityLength = reader.getSize(); + for (StringTokenizer t=new StringTokenizer(range, ", "); t.hasMoreTokens(); /**/) + { + try + { + ranges.add(Range.constructRange(t.nextToken(), mimetype, entityLength)); + } + catch (IllegalArgumentException err) + { + if (getLogger().isDebugEnabled()) + getLogger().debug("Failed to parse range header - returning 416 status code: " + err.getMessage()); + + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + httpServletResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + httpServletResponse.getOutputStream().close(); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + webScriptResponse.setHeader(HEADER_CONTENT_RANGE, "\"*\""); + webScriptResponse.getOutputStream().close(); + } + return true; + } + } + + if (ranges.size() != 0) + { + // merge byte ranges if possible - IE handles this well, FireFox not so much + if (userAgent == null || userAgent.indexOf("MSIE ") != -1) + { + Collections.sort(ranges); + + for (int i=0; i= second.start) + { + if (logger.isDebugEnabled()) + logger.debug("Merging byte range: " + first + " with " + second); + + if (first.end < second.end) + { + // merge second range into first + first.end = second.end; + } + // else we simply discard the second range - it is contained within the first + + // delete second range + ranges.remove(i + 1); + // reset loop index + i--; + } + } + } + + // calculate response content length + long length = MULTIPART_BYTERANGES_BOUNDRY_END.length() + 2; + for (Range r : ranges) + { + length += r.getLength(); + } + + // output headers as we have at least one range to process + OutputStream os = null; + if (httpServletResponse != null) + { + httpServletResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + httpServletResponse.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER); + httpServletResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length)); + os = httpServletResponse.getOutputStream(); + } + else if (webScriptResponse != null) + { + webScriptResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + webScriptResponse.setHeader(HEADER_CONTENT_TYPE, MULTIPART_BYTERANGES_HEADER); + webScriptResponse.setHeader(HEADER_CONTENT_LENGTH, Long.toString(length)); + os =webScriptResponse.getOutputStream(); + } + + InputStream is = null; + try + { + for (Range r : ranges) + { + if (logger.isDebugEnabled()) + logger.debug("Processing: " + r.getContentRange()); + + try + { + // output the header bytes for the range + if (os instanceof ServletOutputStream) + r.outputHeader((ServletOutputStream) os); + + // output the binary data for the range + // need a new reader for each new InputStream + is = contentService.getReader(ref, property).getContentInputStream(); + streamRangeBytes(r, is, os, 0L); + is.close(); + is = null; + + // section marker and flush stream + if (os instanceof ServletOutputStream) + ((ServletOutputStream) os).println(); + os.flush(); + } + catch (IOException err) + { + if (getLogger().isDebugEnabled()) + getLogger().debug("Unable to process multiple range due to IO Exception: " + err.getMessage()); + throw err; + } + } + } + finally + { + if (is != null) + { + is.close(); + } + } + + // end marker + if (os instanceof ServletOutputStream) + ((ServletOutputStream) os).println(MULTIPART_BYTERANGES_BOUNDRY_END); + os.close(); + processedRange = true; + } + + return processedRange; + } + + /** + * Stream a range of bytes from the given InputStream to the ServletOutputStream + * + * @param r Byte Range to process + * @param is InputStream + * @param os ServletOutputStream + * @param offset Assumed InputStream position - to calculate skip bytes from + * + */ + private void streamRangeBytes(final Range r, final InputStream is, final OutputStream os, long offset) + throws IOException + { + final Log logger = getLogger(); + final boolean trace = logger.isTraceEnabled(); + + // TODO: investigate using getFileChannel() on ContentReader + + if (r.start != 0L && r.start > offset) + { + long skipped = offset + is.skip(r.start - offset); + if (skipped < r.start) + { + // Nothing left to download! + return; + } + } + long span = (r.end - r.start) + 1L; + long bytesLeft = span; + int read = 0; + + // Check that bytesLeft isn't greater than int can hold + int bufSize; + if (bytesLeft >= Integer.MAX_VALUE - 8) + { + bufSize = CHUNKSIZE; + } + else + { + bufSize = ((int)bytesLeft) < CHUNKSIZE ? (int)bytesLeft : CHUNKSIZE; + } + byte[] buf = new byte[bufSize]; + + while ((read = is.read(buf)) > 0 && bytesLeft != 0L) + { + os.write(buf, 0, read); + + bytesLeft -= (long)read; + + if (bytesLeft != 0L) + { + int resize; + if (bytesLeft >= Integer.MAX_VALUE - 8) + { + resize = CHUNKSIZE; + } + else + { + resize = ((int)bytesLeft) < CHUNKSIZE ? (int)bytesLeft : CHUNKSIZE; + } + if (resize != buf.length) + { + buf = new byte[resize]; + } + } + if (trace) logger.trace("...wrote " + read + " bytes, with " + bytesLeft + " to go..."); + } + } + + + /** + * Representation of a single byte range. + */ + private static class Range implements Comparable + { + private long start; + private long end; + private long entityLength; + private String contentType; + private String contentRange; + + /** + * Constructor + * + * @param contentType Mimetype of the range content + * @param start Start position in the parent entity + * @param end End position in the parent entity + * @param entityLength Length of the parent entity + */ + Range(String contentType, long start, long end, long entityLength) + { + this.contentType = HEADER_CONTENT_TYPE + ": " + contentType; + this.start = start; + this.end = end; + this.entityLength = entityLength; + } + + /** + * Factory method to construct a byte range from a range header value. + * + * @param range Range header value + * @param contentType Mimetype of the range + * @param entityLength Length of the parent entity + * + * @return Range + * + * @throws IllegalArgumentException for an invalid range + */ + static Range constructRange(String range, String contentType, long entityLength) + { + if (range == null) + { + throw new IllegalArgumentException("Range argument is mandatory"); + } + + // strip total if present - it does not give us anything useful + if (range.indexOf('/') != -1) + { + range = range.substring(0, range.indexOf('/')); + } + + // find the separator + int separator = range.indexOf('-'); + if (separator == -1) + { + throw new IllegalArgumentException("Invalid range: " + range); + } + + try + { + // split range and parse values + long start = 0L; + if (separator != 0) + { + start = Long.parseLong(range.substring(0, separator)); + } + long end = entityLength - 1L; + if (separator != range.length() - 1) + { + end = Long.parseLong(range.substring(separator + 1)); + } + + if (start > end) + { + throw new IllegalArgumentException("Range start can not be less than range end: " + range); + } + // return object to represent the byte-range + return new Range(contentType, start, end, entityLength); + } + catch (NumberFormatException err) + { + throw new IllegalArgumentException("Unable to parse range value: " + range); + } + } + + + /** + * Output the header bytes for a multi-part byte range header + */ + void outputHeader(ServletOutputStream os) throws IOException + { + // output multi-part boundry separator + os.println(MULTIPART_BYTERANGES_BOUNDRY_SEP); + // output content type and range size sub-header for this part + os.println(this.contentType); + os.println(getContentRange()); + os.println(); + } + + /** + * @return the length in bytes of the byte range content including the header bytes + */ + int getLength() + { + // length in bytes of range plus it's header plus section marker and line feed bytes + return MULTIPART_BYTERANGES_BOUNDRY_SEP.length() + 2 + + this.contentType.length() + 2 + + getContentRange().length() + 4 + (int)(this.end - this.start + 1L) + 2; + } + + /** + * @return the Content-Range header string value for this byte range + */ + private String getContentRange() + { + if (this.contentRange == null) + { + this.contentRange = "Content-Range: bytes " + Long.toString(this.start) + "-" + + Long.toString(this.end) + "/" + Long.toString(this.entityLength); + } + return this.contentRange; + } + + @Override + public String toString() + { + return this.start + "-" + this.end; + } + + /** + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + public int compareTo(Range o) + { + return this.start > o.start ? 1 : -1; + } + } + + /** + * @return the logger + */ + private static Log getLogger() + { + return logger; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/PagingCursor.java b/remote-api/src/main/java/org/alfresco/repo/web/util/PagingCursor.java new file mode 100644 index 00000000000..66aba132131 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/PagingCursor.java @@ -0,0 +1,455 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util; + +/** + * Paging cursor. A utility for maintaining paged indexes for a collection of N items. + * + * There are two types of cursor: + * + * a) Paged + * + * This type of cursor is driven from a page number and page size. Random access within + * the collection is possible by jumping straight to a page. A simple scroll through + * the collection is supported by iterating through each next page. + * + * b) Rows + * + * This type of cursor is driven from a skip row count and maximum number of rows. Random + * access is not supported. The collection of items is simply scrolled through from + * start to end by iterating through each next set of rows. + * + * In either case, a paging cursor provides a start row and end row which may be used + * to extract the items for the page from the collection of N items. + * + * A zero (or less) page size or row maximum means "unlimited". + * + * Zero or one based Page and Rows indexes are supported. By default, Pages are 1 based and + * Rows are 0 based. + * + * At any time, -1 is returned to represent "out of range" i.e. for next, previous, last page + * and next skip count. + * + * Pseudo-code for traversing through a collection of N items (10 at a time): + * + * PagingCursor cursor = new PagingCursor(); + * Page page = cursor.createPageCursor(N, 10, 1); + * while (page.isInRange()) + * { + * for (long i = page.getStartRow(); i <= page.getEndRow(); i++) + * { + * ...collection[i]... + * } + * page = cursor.createPageCursor(N, 10, page.getNextPage()); + * } + * + * Rows rows = cursor.createRowsCursor(N, 10, 0); + * while (rows.isInRange()) + * { + * for (long i = page.getStartRow(); i <= page.getEndRow(); i++) + * { + * ...collection[i]... + * } + * rows = cursor.createRowsCursor(N, 10, rows.getNextSkipRows()); + * } + * + * @author davidc + */ +public class PagingCursor +{ + boolean zeroBasedPage = false; + boolean zeroBasedRow = true; + + /** + * Sets zero based page index + * + * Note: scoped to this paging cursor instance + * + * @param zeroBasedPage true => 0 based, false => 1 based + */ + public void setZeroBasedPage(boolean zeroBasedPage) + { + this.zeroBasedPage = zeroBasedPage; + } + + /** + * Is zero based page index? + * + * Note: scoped to this paging cursor instance + * + * @return true => 0 based, false => 1 based + */ + public boolean isZeroBasedPage() + { + return zeroBasedPage; + } + + /** + * Sets zero based row index + * + * Note: scoped to this paging cursor instance + * + * @param zeroBasedRow true => 0 based, false => 1 based + */ + public void setZeroBasedRow(boolean zeroBasedRow) + { + this.zeroBasedRow = zeroBasedRow; + } + + /** + * Is zero based row index? + * + * Note: scoped to this paging cursor instance + * + * @return true => 0 based, false => 1 based + */ + public boolean isZeroBasedRow() + { + return zeroBasedRow; + } + + /** + * Create a Page based Cursor + * + * @param totalRows total rows in collection + * @param rowsPerPage page size + * @param page page number (0 or 1 based) + * @return Page Cursor + */ + public Page createPageCursor(long totalRows, int rowsPerPage, int page) + { + return new Page(totalRows, rowsPerPage, page, zeroBasedPage, zeroBasedRow); + } + + /** + * Create a Page based Cursor + * + * @param totalRows total rows in collection + * @param rowsPerPage page size + * @param page page number (0 or 1 based) + * @param zeroBasedPage true => 0 based, false => 1 based + * @param zeroBasedRow true => 0 based, false => 1 based + * @return Page Cursor + */ + public Page createPageCursor(long totalRows, int rowsPerPage, int page, boolean zeroBasedPage, boolean zeroBasedRow) + { + return new Page(totalRows, rowsPerPage, page, zeroBasedPage, zeroBasedRow); + } + + /** + * Create a Rows based Cursor + * + * @param totalRows total rows in collection + * @param maxRows maximum number of rows in page + * @param skipRows number of rows to skip (0 - none) + * @return Rows Cursor + */ + public Rows createRowsCursor(long totalRows, long maxRows, long skipRows) + { + return new Rows(totalRows, maxRows, skipRows, zeroBasedRow); + } + + /** + * Create a Rows based Cursor + * + * @param totalRows total rows in collection + * @param maxRows maximum number of rows in page + * @param skipRows number of rows to skip (0 - none) + * @param zeroBasedRow true => 0 based, false => 1 based + * @return Rows Cursor + */ + public Rows createRowsCursor(long totalRows, long maxRows, long skipRows, boolean zeroBasedRow) + { + return new Rows(totalRows, maxRows, skipRows, zeroBasedRow); + } + + + /** + * Page based Cursor + */ + public static class Page + { + boolean zeroBasedPage; + boolean zeroBasedRow; + long totalRows; + int rowsPerPage; + long pageSize; + int currentPage; + int currentRow; + + /** + * Create a Page based Cursor + * + * @param totalRows total rows in collection + * @param rowsPerPage page size + * @param page page number (0 or 1 based) + * @param zeroBasedPage true => 0 based, false => 1 based + * @param zeroBasedRow true => 0 based, false => 1 based + */ + public Page(long totalRows, int rowsPerPage, int page, boolean zeroBasedPage, boolean zeroBasedRow) + { + this.zeroBasedPage = zeroBasedPage; + this.zeroBasedRow = zeroBasedRow; + this.totalRows = totalRows; + this.rowsPerPage = rowsPerPage; + this.pageSize = (rowsPerPage <=0) ? totalRows : rowsPerPage; + this.currentPage = (zeroBasedPage) ? page : page - 1; + } + + /** + * Gets total rows + * + * @return total rows + */ + public long getTotalRows() + { + return totalRows; + } + + /** + * Gets total number of pages + * + * @return total number of pages + */ + public int getTotalPages() + { + if (totalRows == 0) + return 0; + + int totalPages = (int)(totalRows / pageSize); + totalPages += (totalRows % pageSize != 0) ? 1 : 0; + return totalPages; + } + + /** + * Gets page size + * + * @return page size + */ + public int getRowsPerPage() + { + return rowsPerPage; + } + + /** + * Is the cursor within range of the total number of rows + * + * @return true => within range of total rows + */ + public boolean isInRange() + { + return currentPage >= 0 && getCurrentPage() <= getLastPage(); + } + + /** + * Gets the current page number + * + * @return current page number + */ + public int getCurrentPage() + { + return currentPage + (zeroBasedPage ? 0 : 1); + } + + /** + * Gets the next page number + * + * @return next page number (-1 if no more pages) + */ + public int getNextPage() + { + return getCurrentPage() < getLastPage() ? getCurrentPage() + 1 : - 1; + } + + /** + * Gets the previous page number + * + * @return previous page number (-1 if no previous pages) + */ + public int getPreviousPage() + { + return currentPage > 0 ? getCurrentPage() - 1 : - 1; + } + + /** + * Gets the first page number + * + * @return first page number + */ + public int getFirstPage() + { + if (totalRows == 0) + return -1; + + return zeroBasedPage ? 0 : 1; + } + + /** + * Gets the last page number + * + * @return last page number + */ + public int getLastPage() + { + if (totalRows == 0) + return -1; + + return getTotalPages() - (zeroBasedPage ? 1 : 0); + } + + /** + * Gets the start row within collection for this page + * + * @return start row index + */ + public long getStartRow() + { + if (totalRows == 0) + return -1; + + return (currentPage * pageSize) + (zeroBasedRow ? 0 : 1); + } + + /** + * Gets the end row within collection for this page + * + * @return end row index + */ + public long getEndRow() + { + if (totalRows == 0) + return -1; + + return getStartRow() + Math.min(pageSize, totalRows - (currentPage * pageSize)) - 1; + } + } + + /** + * Rows based Cursor + */ + public static class Rows + { + boolean zeroBasedRow; + long totalRows; + long skipRows; + long maxRows; + long pageSize; + + /** + * Create a Rows based Cursor + * + * @param totalRows total rows in collection + * @param maxRows maximum number of rows in page + * @param skipRows number of rows to skip (0 - none) + * @param zeroBasedRow true => 0 based, false => 1 based + */ + public Rows(long totalRows, long maxRows, long skipRows, boolean zeroBasedRow) + { + this.zeroBasedRow = zeroBasedRow; + this.totalRows = totalRows; + this.maxRows = maxRows; + this.skipRows = skipRows; + this.pageSize = (maxRows <= 0) ? totalRows - skipRows : maxRows; + } + + /** + * Gets the total number of rows + * + * @return total rows + */ + public long getTotalRows() + { + return totalRows; + } + + /** + * Gets the number rows skipped + * + * @return skipped row count + */ + public long getSkipRows() + { + return skipRows; + } + + /** + * Gets the maximum number of rows to include in this page + * + * @return maximum of numbers + */ + public long getMaxRows() + { + return maxRows; + } + + /** + * Is the cursor within range of the total number of rows + * + * @return true => within range of total rows + */ + public boolean isInRange() + { + return skipRows >= 0 && skipRows < totalRows; + } + + /** + * Gets the start row within collection for this page + * + * @return start row index + */ + public long getStartRow() + { + if (totalRows == 0) + return -1; + + return skipRows + (zeroBasedRow ? 0 : 1); + } + + /** + * Gets the end row within collection for this page + * + * @return end row index + */ + public long getEndRow() + { + if (totalRows == 0) + return -1; + + return getStartRow() + Math.min(pageSize, totalRows - skipRows) - 1; + } + + /** + * Gets the next skip count + * + * @return next skip row + */ + public long getNextSkipRows() + { + return (skipRows + pageSize < totalRows) ? skipRows + pageSize : -1; + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Cursor.java b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Cursor.java new file mode 100644 index 00000000000..c36365fcd1e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Cursor.java @@ -0,0 +1,155 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util.paging; + + +/** + * Cursor - Allows for scrolling through a row set. + * + * @author davidc + */ +public interface Cursor +{ + /** + * Gets the page type + * + * @return page type + */ + public String getPageType(); + + /** + * Gets the page size + * + * @return page size + */ + int getPageSize(); + + /** + * Gets total number of pages + * + * @return total number of pages + */ + int getTotalPages(); + + /** + * Gets total rows + * + * @return total rows + */ + int getTotalRows(); + + /** + * Gets the current page number + * + * @return current page number + */ + int getCurrentPage(); + + /** + * Gets the first page number + * + * @return first page number + */ + int getFirstPage(); + + /** + * Gets the last page number + * + * @return last page number + */ + int getLastPage(); + + /** + * Gets the next page number + * + * @return next page number (-1 if no more pages) + */ + int getNextPage(); + + /** + * Gets the previous page number + * + * @return previous page number (-1 if no previous pages) + */ + int getPrevPage(); + + /** + * Is the page within range of the result set + * + * @return true => page is within range + */ + boolean isInRange(); + + /** + * Is there a known first page? + * + * @return true => getFirstPage() will succeed + */ + boolean getHasFirstPage(); + + /** + * Is there a known last page? + * + * @return true => getLastPage() will succeed + */ + boolean getHasLastPage(); + + /** + * Is there a known next page? + * + * @return true => getNextPage() will succeed + */ + boolean getHasNextPage(); + + /** + * Is there a known prev page? + * + * @return true => getPrevPage() will succeed + */ + boolean getHasPrevPage(); + + /** + * Gets the start row within result set for this page + * + * @return start row index + */ + int getStartRow(); + + /** + * Gets the end row within result set for this page + * + * @return end row index + */ + int getEndRow(); + + /** + * Gets the count of rows within result set for this page + * + * @return row count + */ + int getRowCount(); + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Page.java b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Page.java new file mode 100644 index 00000000000..5c538eaa8bd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Page.java @@ -0,0 +1,97 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util.paging; + + +/** + * A Page within a Cursor. + * + * @author davidc + */ +public class Page +{ + Paging.PageType pageType; + boolean zeroBasedIdx; + int startIdx; + int pageSize; + + /** + * Construct + * + * @param pageType Page or Window + * @param zeroBasedIdx true => start index from 0 + * @param startIdx start index + * @param pageSize page size + */ + /*package*/ Page(Paging.PageType pageType, boolean zeroBasedIdx, int startIdx, int pageSize) + { + this.pageType = pageType; + this.zeroBasedIdx = zeroBasedIdx; + this.startIdx = startIdx; + this.pageSize = pageSize; + } + + /** + * Gets the Page Type + * + * @return page type + */ + /*package*/ Paging.PageType getType() + { + return pageType; + } + + /** + * Gets the page number + * + * @return page number + */ + public int getNumber() + { + return startIdx; + } + + /** + * Gets the page size + * + * @return page size + */ + public int getSize() + { + return pageSize; + } + + /** + * Is zero based page index + * + * @return true => page number starts from zero + */ + public boolean isZeroBasedIdx() + { + return zeroBasedIdx; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/paging/PagedCursor.java b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/PagedCursor.java new file mode 100644 index 00000000000..547d1358d3e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/PagedCursor.java @@ -0,0 +1,225 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util.paging; + +import java.io.Serializable; + +import org.alfresco.repo.web.util.paging.Paging.PageType; + + +/** + * Implementation of cursor based on notion of a Page. + * + * @author davidc + */ +public class PagedCursor implements Cursor, Serializable +{ + private static final long serialVersionUID = -1041155610387669590L; + + private boolean zeroBasedPage; + private boolean zeroBasedRow; + private int totalRows; + private int pageSize; + private int rowsPerPage; + private int page; + + + /** + * Construct + * + * @param zeroBasedRow true => row index starts at zero + * @param totalRows total number of rows (-1 for don't know) + * @param zeroBasedPage true => page number starts at zero + * @param page page number + * @param pageSize page size + */ + /*package*/ PagedCursor(boolean zeroBasedRow, int totalRows, boolean zeroBasedPage, int page, int pageSize) + { + this.zeroBasedRow = zeroBasedRow; + this.totalRows = totalRows; + this.zeroBasedPage = zeroBasedPage; + this.page = (zeroBasedPage) ? page : page - 1; + this.pageSize = pageSize; + this.rowsPerPage = (pageSize <=0) ? totalRows : pageSize; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getPageType() + */ + public String getPageType() + { + return PageType.PAGE.toString(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getPageSize() + */ + public int getPageSize() + { + return pageSize; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getTotalPages() + */ + public int getTotalPages() + { + if (totalRows <= 0) + return 0; + + int totalPages = (int)(totalRows / rowsPerPage); + totalPages += (totalRows % rowsPerPage != 0) ? 1 : 0; + return totalPages; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getTotalRows() + */ + public int getTotalRows() + { + return totalRows; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getCurrentPage() + */ + public int getCurrentPage() + { + return page + (zeroBasedPage ? 0 : 1); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getFirstPage() + */ + public int getFirstPage() + { + if (totalRows <= 0) + return -1; + + return zeroBasedPage ? 0 : 1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getLastPage() + */ + public int getLastPage() + { + if (totalRows <= 0) + return -1; + + return getTotalPages() - (zeroBasedPage ? 1 : 0); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getNextPage() + */ + public int getNextPage() + { + return getCurrentPage() < getLastPage() ? getCurrentPage() + 1 : - 1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getPrevPage() + */ + public int getPrevPage() + { + return page > 0 ? getCurrentPage() - 1 : - 1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#isInRange() + */ + public boolean isInRange() + { + return page >= 0 && getCurrentPage() <= getLastPage(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasFirstPage() + */ + public boolean getHasFirstPage() + { + return getFirstPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasLastPage() + */ + public boolean getHasLastPage() + { + return getLastPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasNextPage() + */ + public boolean getHasNextPage() + { + return getNextPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasPrevPage() + */ + public boolean getHasPrevPage() + { + return getPrevPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getStartRow() + */ + public int getStartRow() + { + if (totalRows <= 0) + return 0; + + return (page * rowsPerPage) + (zeroBasedRow ? 0 : 1); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getEndRow() + */ + public int getEndRow() + { + if (totalRows <= 0) + return -1; + + return getStartRow() + Math.min(rowsPerPage, totalRows - (page * rowsPerPage)) - 1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getRowCount() + */ + public int getRowCount() + { + if (totalRows <= 0) + return 0; + + return getEndRow() - getStartRow() + 1; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/paging/PagedResults.java b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/PagedResults.java new file mode 100644 index 00000000000..df8de8273a7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/PagedResults.java @@ -0,0 +1,108 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util.paging; + +import java.io.Serializable; + + +/** + * A Paged Result Set + * + * @author davidc + */ +public class PagedResults implements Serializable +{ + private static final long serialVersionUID = 5905699888354619269L; + + private Object result; + private Object[] results; + private Cursor cursor; + + + /** + * Construct + * + * @param results results for the page within cursor + * @param cursor the cursor + */ + /*Package*/ PagedResults(Object[] results, Cursor cursor) + { + this.result = results; + this.results = results; + this.cursor = cursor; + } + + /** + * Construct + * + * @param result results for the page within cursor + * @param cursor the cursor + */ + /*Package*/ PagedResults(Object result, Cursor cursor) + { + this.result = result; + this.results = null; + this.cursor = cursor; + } + + /** + * Get Results + * + * @return results + */ + public Object[] getResults() + { + if (results == null) + { + if (result != null) + { + results = new Object[] {result}; + } + } + return results; + } + + /** + * Get Result + * + * @return result + */ + public Object getResult() + { + return result; + } + + /** + * Get Cursor + * + * @return cursor + */ + public Cursor getCursor() + { + return cursor; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Paging.java b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Paging.java new file mode 100644 index 00000000000..790a84c4a55 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/Paging.java @@ -0,0 +1,311 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util.paging; + +import java.util.Map; + +/** + * Paging. A utility for maintaining paged indexes for a collection of N items. + * + * There are two types of cursor: + * + * a) Paged + * + * This type of cursor is driven from a page number and page size. Random access within + * the collection is possible by jumping straight to a page. A simple scroll through + * the collection is supported by iterating through each next page. + * + * b) Windowed + * + * This type of cursor is driven from a skip row count and maximum number of rows. Random + * access is not supported. The collection of items is simply scrolled through from + * start to end by iterating through each next set of rows. + * + * In either case, a paging cursor provides a start row and end row which may be used + * to extract the items for the page from the collection of N items. + * + * A zero (or less) page size or row maximum means "unlimited". + * + * Zero or one based Page and Rows indexes are supported. By default, Pages are 1 based and + * Rows are 0 based. + * + * At any time, -1 is returned to represent "out of range" i.e. for next, previous, last page. + * + * Pseudo-code for traversing through a collection of N items (10 at a time): + * + * Paging paging = new Paging(); + * Cursor page = paging.createCursor(N, paging.createPage(1, 10)); + * while (page.isInRange()) + * { + * for (long i = page.getStartRow(); i <= page.getEndRow(); i++) + * { + * ...collection[i]... + * } + * page = paging.createCursor(N, paging.createPage(page.getNextPage(), page.getPageSize()); + * } + * + * Cursor window = paging.createCursor(N, paging.createWindow(0, 10)); + * while (window.isInRange()) + * { + * for (long i = window.getStartRow(); i <= window.getEndRow(); i++) + * { + * ...collection[i]... + * } + * window = paging.createCursor(N, paging.createWindow(window.getNextPage(), window.getPageSize()); + * } + * + * @author davidc + */ +public class Paging +{ + public enum PageType + { + PAGE, + WINDOW + }; + + boolean zeroBasedPage = false; + boolean zeroBasedRow = true; + + /** + * Sets zero based page index + * + * Note: scoped to this paging cursor instance + * + * @param zeroBasedPage true => 0 based, false => 1 based + */ + public void setZeroBasedPage(boolean zeroBasedPage) + { + this.zeroBasedPage = zeroBasedPage; + } + + /** + * Is zero based page index? + * + * Note: scoped to this paging cursor instance + * + * @return true => 0 based, false => 1 based + */ + public boolean isZeroBasedPage() + { + return zeroBasedPage; + } + + /** + * Sets zero based row index + * + * Note: scoped to this paging cursor instance + * + * @param zeroBasedRow true => 0 based, false => 1 based + */ + public void setZeroBasedRow(boolean zeroBasedRow) + { + this.zeroBasedRow = zeroBasedRow; + } + + /** + * Is zero based row index? + * + * Note: scoped to this paging cursor instance + * + * @return true => 0 based, false => 1 based + */ + public boolean isZeroBasedRow() + { + return zeroBasedRow; + } + + /** + * Create a Page or Window from standardised request arguments + * + * For Paged based index (take precedence over window based index, if both are specified): + * + * - request args + * pageNo => page number index + * pageSize => size of page + * + * For Window based index (as defined by CMIS): + * + * - request args (take precedence over header values if both are specified) + * skipCount => row number start index + * maxItems => size of page + * + * @param args request args + * @return page (if pageNumber driven) or window (if skipCount driven) + */ + public Page createPageOrWindow(Map args) + { + // page number + Integer pageNo = null; + String strPageNo = args.get("pageNo"); + if (strPageNo != null) + { + try + { + pageNo = new Integer(strPageNo); + } + catch(NumberFormatException e) {}; + } + + // page size + Integer pageSize = null; + String strPageSize = args.get("pageSize"); + if (strPageSize != null) + { + try + { + pageSize = new Integer(strPageSize); + } + catch(NumberFormatException e) {}; + } + + // skip count + Integer skipCount = null; + String strSkipCount = args.get("skipCount"); + if (strSkipCount != null) + { + try + { + skipCount = new Integer(strSkipCount); + } + catch(NumberFormatException e) {}; + } + + // max items + Integer maxItems = null; + String strMaxItems = args.get("maxItems"); + if (strMaxItems != null) + { + try + { + maxItems = new Integer(strMaxItems); + } + catch(NumberFormatException e) {}; + } + + return createPageOrWindow(pageNo, pageSize, skipCount, maxItems); + } + + /** + * Create a Page or Window + * + * @param pageNumber page number (optional and paired with pageSize) + * @param pageSize page size (optional and paired with pageNumber) + * @param skipCount skipCount (optional and paired with maxItems) + * @param maxItems maxItems (optional and paired with skipCount) + * @return page (if pageNumber driven) or window (if skipCount driven) + */ + public Page createPageOrWindow(Integer pageNumber, Integer pageSize, Integer skipCount, Integer maxItems) + { + if (pageNumber != null || pageSize != null) + { + return createPage(pageNumber == null ? isZeroBasedPage() ? 0 : 1 : pageNumber, pageSize == null ? -1 : pageSize); + } + else if (skipCount != null || maxItems != null) + { + return createWindow(skipCount == null ? isZeroBasedRow() ? 0 : 1 : skipCount, maxItems == null ? -1 : maxItems); + } + return createUnlimitedPage(); + } + + /** + * Create a Page + * + * @param pageNumber page number + * @param pageSize page size + * @return the page + */ + public Page createPage(int pageNumber, int pageSize) + { + return new Page(PageType.PAGE, zeroBasedPage, pageNumber, pageSize); + } + + /** + * Create an unlimited Page + * + * @return page (single Page starting at first page of unlimited page size) + */ + public Page createUnlimitedPage() + { + return new Page(PageType.PAGE, zeroBasedPage, zeroBasedPage ? 0 : 1, -1); + } + + /** + * Create a Window + * @param skipRows number of rows to skip + * @param maxRows maximum number of rows in window + * @return the window + */ + public Page createWindow(int skipRows, int maxRows) + { + return new Page(PageType.WINDOW, zeroBasedRow, skipRows, maxRows); + } + + /** + * Create a Cursor + * + * @param totalRows total number of rows in cursor (< 0 for don't know) + * @param page the page / window within cursor + * @return the cursor + */ + public Cursor createCursor(int totalRows, Page page) + { + if (page.getType() == PageType.PAGE) + { + return new PagedCursor(zeroBasedRow, totalRows, page.zeroBasedIdx, page.startIdx, page.pageSize); + } + else if (page.getType() == PageType.WINDOW) + { + return new WindowedCursor(zeroBasedRow, totalRows, page.startIdx, page.pageSize); + } + return null; + } + + /** + * Create a Paged Result Set + * + * @param results the results for the page within the cursor + * @param cursor the cursor + * @return the paged result set + */ + public PagedResults createPagedResults(Object[] results, Cursor cursor) + { + return new PagedResults(results, cursor); + } + + /** + * Create a Paged Result Set + * + * @param result the results for the page within the cursor + * @param cursor the cursor + * @return the paged result set + */ + public PagedResults createPagedResult(Object result, Cursor cursor) + { + return new PagedResults(result, cursor); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/util/paging/WindowedCursor.java b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/WindowedCursor.java new file mode 100644 index 00000000000..563c9efba35 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/util/paging/WindowedCursor.java @@ -0,0 +1,213 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.web.util.paging; + +import java.io.Serializable; + +import org.alfresco.repo.web.util.paging.Paging.PageType; + + +/** + * Cursor implementation based on notion of a Window. + * + * @author davidc + */ +public class WindowedCursor implements Cursor, Serializable +{ + private static final long serialVersionUID = 521131539938276413L; + + private boolean zeroBasedRow; + private int totalRows; + private int skipRows; + private int maxRows; + private int rowsPerPage; + + /** + * Construct + * + * @param zeroBasedRow true => 0 based, false => 1 based + * @param totalRows total rows in collection + * @param skipRows number of rows to skip (0 - none) + * @param maxRows maximum number of rows in window + */ + WindowedCursor(boolean zeroBasedRow, int totalRows, int skipRows, int maxRows) + { + this.zeroBasedRow = zeroBasedRow; + this.totalRows = totalRows; + this.skipRows = skipRows; + this.maxRows = maxRows; + this.rowsPerPage = (maxRows <= 0) ? totalRows - skipRows : maxRows; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getPageType() + */ + public String getPageType() + { + return PageType.WINDOW.toString(); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getPageSize() + */ + public int getPageSize() + { + return maxRows; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getTotalPages() + */ + public int getTotalPages() + { + return -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getTotalRows() + */ + public int getTotalRows() + { + return totalRows; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getCurrentPage() + */ + public int getCurrentPage() + { + return skipRows; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getFirstPage() + */ + public int getFirstPage() + { + if (totalRows <=0) + return -1; + + return zeroBasedRow ? 0 : 1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getLastPage() + */ + public int getLastPage() + { + return -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getNextPage() + */ + public int getNextPage() + { + return (skipRows + rowsPerPage < totalRows) ? skipRows + maxRows : -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getPrevPage() + */ + public int getPrevPage() + { + return (skipRows > 0) ? Math.max(0, skipRows - maxRows) : -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#isInRange() + */ + public boolean isInRange() + { + return skipRows >= 0 && skipRows < totalRows; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasFirstPage() + */ + public boolean getHasFirstPage() + { + return getFirstPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasLastPage() + */ + public boolean getHasLastPage() + { + return getLastPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasNextPage() + */ + public boolean getHasNextPage() + { + return getNextPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#hasPrevPage() + */ + public boolean getHasPrevPage() + { + return getPrevPage() != -1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getStartRow() + */ + public int getStartRow() + { + if (totalRows <= 0) + return 0; + + return skipRows + (zeroBasedRow ? 0 : 1); + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getEndRow() + */ + public int getEndRow() + { + if (totalRows <= 0) + return -1; + + return getStartRow() + Math.min(rowsPerPage, totalRows - skipRows) - 1; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.web.util.paging.Cursor#getRowCount() + */ + public int getRowCount() + { + if (totalRows <= 0) + return 0; + + return getEndRow() - getStartRow() + 1; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/ActivityPostProducer.java b/remote-api/src/main/java/org/alfresco/repo/webdav/ActivityPostProducer.java new file mode 100644 index 00000000000..1509897b0c7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/ActivityPostProducer.java @@ -0,0 +1,39 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + + +/** + * WebDAVMethods that are able to post activity data must implement this interface + * in order that the CloudWebDAVServlet will supply the object with an + * ActivityPoster collaborator. + * + * @author Matt Ward + */ +public interface ActivityPostProducer +{ + void setActivityPoster(WebDAVActivityPoster activityPoster); +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/ActivityPosterImpl.java b/remote-api/src/main/java/org/alfresco/repo/webdav/ActivityPosterImpl.java new file mode 100644 index 00000000000..3a1e6486bf4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/ActivityPosterImpl.java @@ -0,0 +1,151 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.sync.repo.Client; +import org.alfresco.sync.repo.Client.ClientType; +import org.alfresco.repo.activities.ActivityType; +import org.alfresco.service.cmr.activities.ActivityPoster; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * WebDAV methods may use an instance of this class to post activity data. + * + * @see WebDAVActivityPoster + * @author Matt Ward + */ +public class ActivityPosterImpl implements WebDAVActivityPoster +{ + private String appTool; + private ActivityPoster poster; + + protected static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol.activity"); + + /** + * Default constructor. + */ + public ActivityPosterImpl() + { + } + + /** + * Constructor + * + * @param appTool String + * @param poster ActivityPoster + */ + public ActivityPosterImpl(String appTool, ActivityPoster poster) + { + this.appTool = appTool; + this.poster = poster; + } + + + /** + * {@inheritDoc} + */ + @Override + public void postFileFolderAdded( + String siteId, + String tenantDomain, + String path, + FileInfo nodeInfo) throws WebDAVServerException + { + postFileFolderActivity(nodeInfo.isFolder() ? ActivityType.FOLDER_ADDED : ActivityType.FILE_ADDED, + siteId, tenantDomain, nodeInfo.isFolder() ? path : null, null, nodeInfo); + } + + /** + * {@inheritDoc} + */ + @Override + public void postFileFolderUpdated( + String siteId, + String tenantDomain, + FileInfo nodeInfo) throws WebDAVServerException + { + if (! nodeInfo.isFolder()) + { + postFileFolderActivity(ActivityType.FILE_UPDATED, siteId, tenantDomain, null, null, nodeInfo); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void postFileFolderDeleted( + String siteId, + String tenantDomain, + String parentPath, + FileInfo parentNodeInfo, + FileInfo nodeInfo) throws WebDAVServerException + { + postFileFolderActivity(nodeInfo.isFolder() ? ActivityType.FOLDER_DELETED : ActivityType.FILE_DELETED, siteId, tenantDomain, parentPath, parentNodeInfo.getNodeRef(), nodeInfo); + } + + + private void postFileFolderActivity( + String activityType, + String siteId, + String tenantDomain, + String path, + NodeRef parentNodeRef, + FileInfo contentNodeInfo) throws WebDAVServerException + { + String fileName = contentNodeInfo.getName(); + NodeRef nodeRef = contentNodeInfo.getNodeRef(); + + try + { + poster.postFileFolderActivity(activityType, path, tenantDomain, siteId, + parentNodeRef, nodeRef, fileName, + appTool, Client.asType(ClientType.webdav),contentNodeInfo); + } + catch (AlfrescoRuntimeException are) + { + logger.error("Failed to post activity.", are); + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + + public void setAppTool(String appTool) + { + this.appTool = appTool; + } + + public void setPoster(ActivityPoster poster) + { + this.poster = poster; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/CopyMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/CopyMethod.java new file mode 100644 index 00000000000..fed4bfddcfe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/CopyMethod.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Implements the WebDAV COPY method + * + * @author Derek Hulley + */ +public class CopyMethod extends MoveMethod +{ + /** + * Default constructor + */ + public CopyMethod() + { + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.webdav.MoveMethod#isMove() + */ + @Override + protected boolean isMove() + { + return false; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/DeleteMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/DeleteMethod.java new file mode 100644 index 00000000000..88793f82ca7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/DeleteMethod.java @@ -0,0 +1,265 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.util.Timer; +import java.util.TimerTask; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.webdav.WebDavService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.FileFilterMode; + +/** + * Implements the WebDAV DELETE method + * + * @author gavinc + */ +public class DeleteMethod extends WebDAVMethod implements ActivityPostProducer +{ + private WebDAVActivityPoster activityPoster; + + /** + * Default constructor + */ + public DeleteMethod() + { + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + parseIfHeader(); + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // Nothing to do in this method + } + + /** + * @deprecated MNT-8704: WebDAV:Content does not disappear after being deleted + */ + @Deprecated + private static final Timer deleteDelayTimer = new Timer(); + + /** + * Execute the request + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException, Exception + { + if (logger.isDebugEnabled()) + { + logger.debug("WebDAV DELETE: " + getPath()); + } + + final FileFolderService fileFolderService = getFileFolderService(); + final PermissionService permissionService = getPermissionService(); + + NodeRef rootNodeRef = getRootNodeRef(); + + String path = getPath(); + FileInfo fileInfo = null; + try + { + // get the node to delete + fileInfo = getNodeForPath(rootNodeRef, path); + } + catch (FileNotFoundException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Node not found: " + getPath()); + } + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + checkNode(fileInfo); + + final NodeService nodeService = getNodeService(); + final NodeRef nodeRef = fileInfo.getNodeRef(); + + if (permissionService.hasPermission(nodeRef, PermissionService.DELETE) == AccessStatus.ALLOWED) + { + // As this content will be deleted, we need to extract some info before it's no longer available. + String siteId = getSiteId(); + NodeRef deletedNodeRef = fileInfo.getNodeRef(); + FileInfo parentFile = getDAVHelper().getParentNodeForPath(getRootNodeRef(), path); + + // Don't post activity data for hidden files, resource forks etc. + if (!getDAVHelper().isRenameShuffle(path)) + { + postActivity(parentFile, fileInfo, siteId); + } + + // MNT-181: working copies and versioned nodes are hidden rather than deleted + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY) || nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE)) + { + // Mark content as hidden. This breaks many contracts and will be fixed for "ALF-18619 WebDAV/SPP file shuffles" + fileFolderService.setHidden(nodeRef, true); + { + // Workaround for MNT-8704: WebDAV:Content does not disappear after being deleted + // Get the current user + final String deleteDelayUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // Add a timed task to really delete the file + TimerTask deleteDelayTask = new TimerTask() + { + @Override + public void run() + { + RunAsWork deleteDelayRunAs = new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + // Ignore if it is NOT hidden: the shuffle may have finished; the operation may have failed + if (!nodeService.exists(nodeRef) || !fileFolderService.isHidden(nodeRef)) + { + return null; + } + + // Since this will run in a different thread, the client thread-local must be set + // or else unhiding the node will not unhide it for WebDAV. + FileFilterMode.setClient(FileFilterMode.Client.webdav); + + // Unhide the node, e.g. for archiving + fileFolderService.setHidden(nodeRef, false); + + // This is the transaction-aware service + fileFolderService.delete(nodeRef); + return null; + } + }; + try + { + AuthenticationUtil.runAs(deleteDelayRunAs, deleteDelayUser); + } + catch (Throwable e) + { + // consume exception to avoid it leaking from the TimerTask and causing the Timer to + // no longer accept tasks to be scheduled. + logger.info("Exception thrown during WebDAV delete timer task.", e); + } + } + }; + // Schedule a real delete 5 seconds after the current time + deleteDelayTimer.schedule(deleteDelayTask, 5000L); + } + //MNT-16454: It should be possible to delete files with versionable aspects using webdav; check that + //node is is actually locked before unlocking to avoid access denied + if(getDAVLockService().getLockInfo(nodeRef).isLocked()) + { + getDAVLockService().unlock(nodeRef); + } + } + // We just ensure already-hidden nodes are left unlocked + else if (fileFolderService.isHidden(nodeRef)) + { + getDAVLockService().unlock(nodeRef); + } + // A 'real' delete + else + { + // Delete it + fileFolderService.delete(deletedNodeRef); + } + } + else + { + // access denied + throw new WebDAVServerException(HttpServletResponse.SC_FORBIDDEN); + } + } + + + /** + * Create a deletion activity post. + * + * @param parent The FileInfo for the deleted file's parent. + * @param deletedFile The FileInfo for the deleted file. + * @throws WebDAVServerException + */ + protected void postActivity(FileInfo parent, FileInfo deletedFile, String siteId) throws WebDAVServerException + { + WebDavService davService = getDAVHelper().getServiceRegistry().getWebDavService(); + if (!davService.activitiesEnabled()) + { + // Don't post activities if this behaviour is disabled. + return; + } + + String tenantDomain = getTenantDomain(); + + // Check there is enough information to publish site activity. + if (!siteId.equals(WebDAVHelper.EMPTY_SITE_ID)) + { + SiteService siteService = getServiceRegistry().getSiteService(); + NodeRef documentLibrary = siteService.getContainer(siteId, SiteService.DOCUMENT_LIBRARY); + String parentPath = "/"; + try + { + parentPath = getDAVHelper().getPathFromNode(documentLibrary, parent.getNodeRef()); + } + catch (FileNotFoundException error) + { + if (logger.isDebugEnabled()) + { + logger.debug("No " + SiteService.DOCUMENT_LIBRARY + " container found."); + } + } + + activityPoster.postFileFolderDeleted(siteId, tenantDomain, parentPath, parent, deletedFile); + } + } + + @Override + public void setActivityPoster(WebDAVActivityPoster activityPoster) + { + this.activityPoster = activityPoster; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/ExceptionHandler.java b/remote-api/src/main/java/org/alfresco/repo/webdav/ExceptionHandler.java new file mode 100644 index 00000000000..11d154e04bb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/ExceptionHandler.java @@ -0,0 +1,113 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Create a suitable HttpServletResponse when face with an exception. + * + * @author Matt Ward + */ +public class ExceptionHandler +{ + private static final Log logger = LogFactory.getLog(ExceptionHandler.class); + private Throwable e; + private final HttpServletRequest request; + private final HttpServletResponse response; + + /** + * Create an ExceptionHandler. + * + * @param e Throwable + * @param request HttpServletRequest + * @param response HttpServletResponse + */ + public ExceptionHandler(Throwable e, HttpServletRequest request, HttpServletResponse response) + { + this.e = e; + this.request = request; + this.response = response; + } + + + public void handle() throws IOException + { + if (!(e instanceof WebDAVServerException) && e.getCause() != null) + { + if (e.getCause() instanceof WebDAVServerException) + { + e = e.getCause(); + } + } + // Work out how to handle the error + if (e instanceof WebDAVServerException) + { + WebDAVServerException error = (WebDAVServerException) e; + if (error.getCause() != null) + { + logger.error("Exception thrown.", e); + } + + if (logger.isDebugEnabled()) + { + // Show what status code the method sent back + + logger.debug(request.getMethod() + " is returning status code: " + error.getHttpStatusCode()); + } + + if (response.isCommitted()) + { + logger.warn("Could not return the status code to the client as the response has already been committed!"); + } + else + { + response.sendError(error.getHttpStatusCode()); + } + } + else + { + logger.error("Exception thrown.", e); + + if (response.isCommitted()) + { + logger.warn("Could not return the internal server error code to the client as the response has already been committed!"); + } + else + { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/GetMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/GetMethod.java new file mode 100644 index 00000000000..c9647f6b640 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/GetMethod.java @@ -0,0 +1,786 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.net.SocketException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.StringTokenizer; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.web.util.HttpRangeProcessor; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConverter; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.surf.util.URLEncoder; + +/** + * Implements the WebDAV GET method + * + * @author gavinc + */ +public class GetMethod extends WebDAVMethod +{ + // Request parameters + + private static final String RANGE_HEADER_UNIT_SPECIFIER = "bytes="; + private static final int MAX_RECURSE_ERROR_STACK = 20; + private ArrayList ifMatchTags = null; + private ArrayList ifNoneMatchTags = null; + private Date m_ifModifiedSince = null; + private Date m_ifUnModifiedSince = null; + + protected boolean m_returnContent = true; + private String byteRanges; + + /** + * Default constructor + */ + public GetMethod() + { + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // If the range header is present output a warning, add support later + + String strRange = m_request.getHeader(WebDAV.HEADER_RANGE); + + if (strRange != null && strRange.length() > 0) + { + byteRanges = strRange; + if (logger.isDebugEnabled()) + { + logger.debug("Range header supplied: " + byteRanges); + } + } + + // Capture all the If headers, process later + + String strIfMatch = m_request.getHeader(WebDAV.HEADER_IF_MATCH); + + if (strIfMatch != null && strIfMatch.length() > 0) + { + ifMatchTags = parseETags(strIfMatch); + } + + String strIfNoneMatch = m_request.getHeader(WebDAV.HEADER_IF_NONE_MATCH); + if (strIfNoneMatch != null && strIfNoneMatch.length() > 0) + { + ifNoneMatchTags = parseETags(strIfNoneMatch); + } + + // Parse the dates + + SimpleDateFormat dateFormat = new SimpleDateFormat(WebDAV.HEADER_IF_DATE_FORMAT); + String strIfModifiedSince = m_request.getHeader(WebDAV.HEADER_IF_MODIFIED_SINCE); + + if (strIfModifiedSince != null && strIfModifiedSince.length() > 0) + { + try + { + m_ifModifiedSince = dateFormat.parse(strIfModifiedSince); + } + catch (ParseException e) + { + logger.warn("Failed to parse If-Modified-Since date of " + strIfModifiedSince); + } + } + + String strIfUnModifiedSince = m_request.getHeader(WebDAV.HEADER_IF_UNMODIFIED_SINCE); + if (strIfUnModifiedSince != null && strIfUnModifiedSince.length() > 0) + { + try + { + m_ifUnModifiedSince = dateFormat.parse(strIfUnModifiedSince); + } + catch (ParseException e) + { + logger.warn("Failed to parse If-Unmodified-Since date of " + strIfUnModifiedSince); + } + } + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // Nothing to do in this method + } + + /** + * @return Returns true always + */ + @Override + protected boolean isReadOnly() + { + return true; + } + + /** + * Exceute the WebDAV request + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException, Exception + { + FileFolderService fileFolderService = getFileFolderService(); + NodeRef rootNodeRef = getRootNodeRef(); + String path = getPath(); + + if (!m_returnContent) + { + // There are multiple cases where no content is sent (due to a HEAD request). + // All of them require that the content length is set appropriately. + m_response.setContentLength(0); + } + + FileInfo nodeInfo = null; + try + { + nodeInfo = getDAVHelper().getNodeForPath(rootNodeRef, path); + } + catch (FileNotFoundException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + FileInfo realNodeInfo = nodeInfo; + + // ALF-12008: Due to Windows Explorer's URL concatenation behaviour, we must present links as shortcuts to the real URL, rather than direct hrefs + // This is at least consistent with the way the CIFS server handles links. See org.alfresco.filesys.repo.ContentDiskDriver.openFile(). + if (realNodeInfo.isLink()) + { + Path pathToNode = getNodeService().getPath(nodeInfo.getLinkNodeRef()); + if (pathToNode.size() > 2) + { + pathToNode = pathToNode.subPath(2, pathToNode.size() -1); + } + + String rootURL = getDAVHelper().getURLForPath(m_request, pathToNode.toDisplayPath(getNodeService(), getPermissionService()), true); + if (rootURL.endsWith(WebDAVHelper.PathSeperator) == false) + { + rootURL = rootURL + WebDAVHelper.PathSeperator; + } + + String fname = (String) getNodeService().getProperty(nodeInfo.getLinkNodeRef(), ContentModel.PROP_NAME); + StringBuilder urlStr = new StringBuilder(200); + urlStr.append("[InternetShortcut]\r\n"); + urlStr.append("URL=file://"); + urlStr.append(m_request.getServerName()); + // Only append the port if it is non-default for compatibility with XP + int port = m_request.getServerPort(); + if (port != 80) + { + urlStr.append(":").append(port); + } + urlStr.append(rootURL).append(WebDAVHelper.encodeURL(fname, m_userAgent)); + urlStr.append("\r\n"); + + m_response.setHeader(WebDAV.HEADER_CONTENT_TYPE, "text/plain; charset=ISO-8859-1"); + m_response.setHeader(WebDAV.HEADER_CONTENT_LENGTH, String.valueOf(urlStr.length())); + m_response.getWriter().write(urlStr.toString()); + } + // Check if the node is a folder + else if (realNodeInfo.isFolder()) + { + // is content required + if (!m_returnContent) + { + // ALF-7883 fix, HEAD for collection (see http://www.webdav.org/specs/rfc2518.html#rfc.section.8.4) + return; + } + + // Generate a folder listing + m_response.setContentType("text/html;charset=UTF-8"); + generateDirectoryListing(nodeInfo); + } + else + { + // Return the node details, and content if requested, check that the node passes the pre-conditions + + checkPreConditions(realNodeInfo); + + // Build the response header + m_response.setHeader(WebDAV.HEADER_ETAG, getDAVHelper().makeQuotedETag(nodeInfo)); + + Date modifiedDate = realNodeInfo.getModifiedDate(); + if (modifiedDate != null) + { + long modDate = DefaultTypeConverter.INSTANCE.longValue(modifiedDate); + m_response.setHeader(WebDAV.HEADER_LAST_MODIFIED, WebDAV.formatHeaderDate(modDate)); + } + + m_response.setHeader("Content-Disposition", getContentDispositionHeader(nodeInfo)); + + ContentReader reader = fileFolderService.getReader(realNodeInfo.getNodeRef()); + // ensure that we generate something, even if the content is missing + reader = FileContentReader.getSafeContentReader( + (ContentReader) reader, + I18NUtil.getMessage(FileContentReader.MSG_MISSING_CONTENT), + realNodeInfo.getNodeRef(), reader); + + readContent(realNodeInfo, reader); + } + } + + + protected void readContent(FileInfo realNodeInfo, ContentReader reader) throws IOException, + WebDAVServerException + { + try + { + attemptReadContent(realNodeInfo, reader); + } + catch (final Throwable e) + { + boolean logAsError = true; + Throwable t = e; + // MNT-8989: Traverse the exception cause hierarchy, if we find a SocketException at fault, + // assume this is a dropped connection and do not log a stack trace. + int levels = 0; + while (t.getCause() != null) + { + if (t == t.getCause() || ++levels == MAX_RECURSE_ERROR_STACK) + { + // Avoid infinite loops. + break; + } + t = t.getCause(); + if (t instanceof SocketException || t.getClass().getSimpleName().equals("ClientAbortException")) + { + logAsError = false; + } + } + + if (logAsError && logger.isErrorEnabled()) + { + // Only log at ERROR level when not a SocketException as underlying cause. + logger.error("Error while reading content", e); + } + else if (logger.isDebugEnabled()) + { + // Log other errors at DEBUG level. + logger.debug("Error while reading content", e); + } + + // Note no cause parameter supplied - avoid logging stack trace elsewhere. + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + protected void attemptReadContent(FileInfo realNodeInfo, ContentReader reader) throws IOException + { + if (byteRanges != null && byteRanges.startsWith(RANGE_HEADER_UNIT_SPECIFIER)) + { + HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(getContentService()); + String userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT); + + if (m_returnContent) + { + m_davHelper.publishReadEvent(realNodeInfo, reader.getMimetype(), reader.getSize(), byteRanges.substring(6), reader.getEncoding()); + rangeProcessor.processRange( + m_response, + reader, + byteRanges.substring(6), + realNodeInfo.getNodeRef(), + ContentModel.PROP_CONTENT, + reader.getMimetype(), + userAgent); + } + } + else + { + if (m_returnContent) + { + // there is content associated with the node + m_response.setHeader(WebDAV.HEADER_CONTENT_LENGTH, Long.toString(reader.getSize())); + m_response.setHeader(WebDAV.HEADER_CONTENT_TYPE, reader.getMimetype()); + m_davHelper.publishReadEvent(realNodeInfo, reader.getMimetype(), reader.getSize(), null, reader.getEncoding()); + // copy the content to the response output stream + reader.getContent(m_response.getOutputStream()); + } + } + } + + protected String getContentDispositionHeader(FileInfo nodeInfo) + { + String filename = nodeInfo.getName(); + StringBuilder sb = new StringBuilder(); + sb.append("attachment; filename=\""); + for(int i = 0; i < filename.length(); i++) + { + char c = filename.charAt(i); + if(isValidQuotedStringHeaderParamChar(c)) + { + sb.append(c); + } + else + { + sb.append(" "); + } + } + sb.append("\"; filename*=UTF-8''"); + sb.append(URLEncoder.encode(filename)); + return sb.toString(); + } + + protected boolean isValidQuotedStringHeaderParamChar(char c) + { + // see RFC2616 section 2.2: + // qdtext = > + // TEXT = + // CTL = + // A CRLF is allowed in the definition of TEXT only as part of a header field continuation. + // Note: we dis-allow header field continuation + return (c < 256) // message header param fields must be ISO-8859-1. Lower 256 codepoints of Unicode represent ISO-8859-1 + && (c != 127) // CTL - see RFC2616 section 2.2 + && (c != '"') // <"> + && (c > 31); // CTL - see RFC2616 section 2.2 + } + + /** + * Checks the If header conditions + * + * @param nodeInfo the node to check + * @throws WebDAVServerException if a pre-condition is not met + */ + private void checkPreConditions(FileInfo nodeInfo) throws WebDAVServerException + { + // Make an etag for the node + + String strETag = getDAVHelper().makeQuotedETag(nodeInfo); + TypeConverter typeConv = DefaultTypeConverter.INSTANCE; + + // Check the If-Match header, don't send any content back if none of the tags in + // the list match the etag, and the wildcard is not present + + if (ifMatchTags != null) + { + if (ifMatchTags.contains(WebDAV.ASTERISK) == false && ifMatchTags.contains(strETag) == false) + { + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + } + + // Check the If-None-Match header, don't send any content back if any of the tags + // in the list match the etag, or the wildcard is present + + if (ifNoneMatchTags != null) + { + if (ifNoneMatchTags.contains(WebDAV.ASTERISK) || ifNoneMatchTags.contains(strETag)) + { + throw new WebDAVServerException(HttpServletResponse.SC_NOT_MODIFIED); + } + } + + // Check the modified since list, if the If-None-Match header was not specified + + if (m_ifModifiedSince != null && ifNoneMatchTags == null) + { + Date lastModifiedDate = nodeInfo.getModifiedDate(); + + long fileLastModified = lastModifiedDate != null ? typeConv.longValue(lastModifiedDate) : 0L; + long modifiedSince = m_ifModifiedSince.getTime(); + + if (fileLastModified != 0L && fileLastModified <= modifiedSince) + { + throw new WebDAVServerException(HttpServletResponse.SC_NOT_MODIFIED); + } + } + + // Check the un-modified since list + + if (m_ifUnModifiedSince != null) + { + Date lastModifiedDate = nodeInfo.getModifiedDate(); + + long fileLastModified = lastModifiedDate != null ? typeConv.longValue(lastModifiedDate) : 0L; + long unModifiedSince = m_ifUnModifiedSince.getTime(); + + if (fileLastModified >= unModifiedSince) + { + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + } + } + + /** + * Parses the given ETag header into a list of separate ETags + * + * @param strETagHeader The header to parse + * @return A list of ETags + */ + private ArrayList parseETags(String strETagHeader) + { + ArrayList list = new ArrayList(); + + StringTokenizer tokenizer = new StringTokenizer(strETagHeader, WebDAV.HEADER_VALUE_SEPARATOR); + while (tokenizer.hasMoreTokens()) + { + list.add(tokenizer.nextToken().trim()); + } + + return list; + } + + /** + * Generates a HTML representation of the contents of the path represented by the given node + * + * @param fileInfo the file to use + */ + private void generateDirectoryListing(FileInfo fileInfo) + { + MimetypeService mimeTypeService = getMimetypeService(); + NodeService nodeService = getNodeService(); + + Writer writer = null; + + try + { + writer = m_response.getWriter(); + + boolean wasLink = false; + if (fileInfo.isLink()) + { + fileInfo = getFileFolderService().getFileInfo(fileInfo.getLinkNodeRef()); + wasLink = true; + } + + // Get the list of child nodes for the parent node + List childNodeInfos = getDAVHelper().getChildren(fileInfo); + + // Send back the start of the HTML + writer.write(""); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.repository_title"))); + writer.write(""); + writer.write("\n"); + writer.flush(); + + // Send back the table heading + writer.write("\n"); + writer.write("\n"); + writer.write("\n"); + writer.write("
"); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.directory_listing"))); + writer.write(' '); + writer.write(WebDAVHelper.encodeHTML(getPath())); + writer.write("
"); + + writer.write("\n"); + writer.write(""); + writer.write(""); + writer.write(""); + writer.write(""); + writer.write("\n"); + + // Get the URL for the root path + String rootURL = getURLForPath(m_request, getPath(), true); + if (rootURL.endsWith(WebDAVHelper.PathSeperator) == false) + { + rootURL = rootURL + WebDAVHelper.PathSeperator; + } + + if (wasLink) + { + Path pathToNode = nodeService.getPath(fileInfo.getNodeRef()); + if (pathToNode.size() > 2) + { + pathToNode = pathToNode.subPath(2, pathToNode.size() - 1); + } + + rootURL = getURLForPath(m_request, pathToNode.toDisplayPath(nodeService, getPermissionService()), true); + if (rootURL.endsWith(WebDAVHelper.PathSeperator) == false) + { + rootURL = rootURL + WebDAVHelper.PathSeperator; + } + + rootURL = rootURL + WebDAVHelper.encodeURL(fileInfo.getName(), m_userAgent) + WebDAVHelper.PathSeperator; + } + // Start with a link to the parent folder so we can navigate back up, unless we are at the root level + if (! getDAVHelper().isRootPath(getPath(), getServletPath())) + { + writer.write(""); + writer.write("\n"); + } + + // Send back what we have generated so far + writer.flush(); + int rowId = 0; + + for (FileInfo childNodeInfo : childNodeInfos) + { + // Output the details for the current node + writer.write("\n"); + + // flush every few rows + if ((rowId & 15) == 0) + { + writer.flush(); + } + } + + writer.write("
"); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.column.name"))); + writer.write(""); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.column.size"))); + writer.write(""); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.column.type"))); + writer.write(""); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.column.modifieddate"))); + writer.write("
"); + writer.write("["); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.column.navigate_up"))); + writer.write("]"); + writer.write("
"); + writer.write(WebDAVHelper.encodeHTML(fname)); + writer.write(""); + + // size field + writer.write(""); + + ContentData contentData = null; + if (!childNodeInfo.isFolder()) + { + Serializable contentPropertyName = nodeService.getProperty(childNodeInfo.getNodeRef(), ContentModel.PROP_CONTENT_PROPERTY_NAME); + QName contentPropertyQName = DefaultTypeConverter.INSTANCE.convert(QName.class, contentPropertyName); + + if (null == contentPropertyQName) + { + contentPropertyQName = ContentModel.PROP_CONTENT; + } + + Serializable contentProperty = nodeService.getProperty(childNodeInfo.getNodeRef(), contentPropertyQName); + + if (contentProperty instanceof ContentData) + { + contentData = (ContentData) contentProperty; + } + } + + if (childNodeInfo.isFolder()) + { + writer.write(" "); + } + else + { + if (null != contentData) + { + writer.write(formatSize(Long.toString(contentData.getSize()))); + } + else + { + writer.write(" "); + } + + } + writer.write(""); + + // mimetype field + if (childNodeInfo.isFolder()) + { + writer.write(" "); + } + else + { + String mimetype = " "; + if (null != contentData) + { + mimetype = contentData.getMimetype(); + String displayType = mimeTypeService.getDisplaysByMimetype().get(mimetype); + + if (displayType != null) + { + mimetype = displayType; + } + } + writer.write(mimetype); + } + writer.write(""); + + // modified date field + Date modifiedDate = childNodeInfo.getModifiedDate(); + if (modifiedDate != null) + { + writer.write(WebDAV.formatHeaderDate(DefaultTypeConverter.INSTANCE.longValue(modifiedDate))); + } + else + { + writer.write(" "); + } + writer.write("
"); + } + catch (Throwable e) + { + logger.error(e); + + if (writer != null) + { + try + { + writer.write("
"); + writer.write(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.err.dir"))); + writer.write("
"); + writer.flush(); + } + catch (IOException ioe) + { + } + } + } + } + + /** + * Given a path, will return the parent path. For example: /a/b/c + * will return /a/b and /a/b will return /a. + * + * @param path The path to return the parent of - must be non-null. + * @return String - parent path. + */ + private String parentFolder(String path) + { + if (path.endsWith(WebDAVHelper.PathSeperator)) + { + // Strip trailing slash. + path = path.substring(0, path.length() - 1); + } + String[] paths = getDAVHelper().splitPath(path); + String parent = paths[0]; + if (parent.equals("")) + { + parent = WebDAVHelper.PathSeperator; + } + return parent; + } + + /** + * Formats the given size for display in a directory listing + * + * @param strSize The content size + * @return The formatted size + */ + private String formatSize(String strSize) + { + String strFormattedSize = strSize; + + int length = strSize.length(); + if (length < 4) + { + strFormattedSize = strSize + ' ' + WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.size.bytes")); + } + else if (length >= 4 && length < 7) + { + String strLeft = strSize.substring(0, length - 3); + String strRight = strSize.substring(length - 3, length - 2); + + StringBuilder buffer = new StringBuilder(strLeft); + if (!strRight.equals("0")) + { + buffer.append('.'); + buffer.append(strRight); + } + buffer.append(' ').append(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.size.kilobytes"))); + + strFormattedSize = buffer.toString(); + } + else + { + String strLeft = strSize.substring(0, length - 6); + String strRight = strSize.substring(length - 6, length - 5); + + StringBuilder buffer = new StringBuilder(strLeft); + if (!strRight.equals("0")) + { + buffer.append('.'); + buffer.append(strRight); + } + buffer.append(' ').append(WebDAVHelper.encodeHTML(I18NUtil.getMessage("webdav.size.megabytes"))); + + strFormattedSize = buffer.toString(); + } + + return strFormattedSize; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/HeadMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/HeadMethod.java new file mode 100644 index 00000000000..8d38790b774 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/HeadMethod.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +/** + * Implements the WebDAV HEAD method + * + * @author gavinc + */ +public class HeadMethod extends GetMethod +{ + /** + * Default constructor + */ + public HeadMethod() + { + // Do everything the GET request does apart from returning the content + + m_returnContent = false; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/HierarchicalMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/HierarchicalMethod.java new file mode 100644 index 00000000000..9e5e04ff258 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/HierarchicalMethod.java @@ -0,0 +1,119 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import javax.servlet.http.HttpServletResponse; + +/** + * Abstract base class for the hierarchical methods COPY and MOVE + * + * @author gavinc + */ +public abstract class HierarchicalMethod extends WebDAVMethod +{ + // Request parameters + + protected String m_strDestinationPath; + protected boolean m_overwrite = true; + + /** + * Default constructor + */ + public HierarchicalMethod() + { + } + + /** + * Return the destination path + * + * @return String + */ + public final String getDestinationPath() + { + return m_strDestinationPath; + } + + /** + * Return the overwrite setting + * + * @return boolean + */ + public final boolean hasOverWrite() + { + return m_overwrite; + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // Get the destination path for the copy + + String destURL = m_request.getHeader(WebDAV.HEADER_DESTINATION); + + if (logger.isDebugEnabled()) + logger.debug("Parsing Destination header: " + destURL); + + // Check that the URL is on this server and refers to the WebDAV + // path, if not then return an error + getDAVHelper().checkDestinationURL(m_request, destURL); + + + m_strDestinationPath = getDAVHelper().getDestinationPath(getContextPath(), getServletPath(), destURL); + + // Failed to fix the destination path, return an error + + if (m_strDestinationPath == null) + { + logger.warn("Failed to parse the Destination header: " + destURL); + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST); + } + + // Check if the copy should overwrite an existing file + + String strOverwrite = m_request.getHeader(WebDAV.HEADER_OVERWRITE); + if (strOverwrite != null && strOverwrite.equals(WebDAV.F)) + { + m_overwrite = false; + } + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // NOTE: Hierarchical methods do have a body to define what should + // happen + // to the properties when they are moved or copied, however, this + // feature is not implemented by many servers, including ours!! + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/LockInfoImpl.java b/remote-api/src/main/java/org/alfresco/repo/webdav/LockInfoImpl.java new file mode 100644 index 00000000000..2f883c29740 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/LockInfoImpl.java @@ -0,0 +1,450 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.alfresco.repo.lock.mem.LockStore; + +/** + * Class to represent a WebDAV lock info. Instances of this class are accessible + * my multiple threads as they are kept in the {@link LockStore}. Clients of this + * class are expected to synchronise externally using the provided + * ReentrantReadWriteLock. + * + * @author Ivan Rybnikov + * + */ +public class LockInfoImpl implements Serializable, LockInfo +{ + private static final long serialVersionUID = 1L; + + public static final String ADDINFO_WEBDAV_MARKER = "WebDAV_LockInfo"; + + // Exclusive lock token + private String exclusiveLockToken = null; + + // Lock scope + private String scope = null; + + // Lock depth + private String depth = null; + + // Shared lock tokens + private final Set sharedLockTokens = new HashSet(3); + + // User name of the lock's owner + private String owner; + + // When does the lock expire? + private Date expires; + + /** + * Default constructor + * + */ + public LockInfoImpl() + { + } + + /** + * Constructor + * + * @param token Exclusive lock token + * @param scope Lock scope (shared/exclusive) + * @param depth Lock depth (0/infinity) + */ + public LockInfoImpl(String token, String scope, String depth) + { + this.exclusiveLockToken = token; + this.scope = scope; + this.depth = depth; + } + + /** + * Returns true if node has shared or exclusive locks + * + * @return boolean + */ + @Override + @JsonIgnore + public boolean isLocked() + { + // TODO: return (owner != null || isExclusive() || isShared()); + return (isExclusive() || isShared()); + } + + /** + * Setter for exclusive lock token + * + * @param token Lock token + */ + @Override + public void setExclusiveLockToken(String token) + { + if (isShared()) + { + throw new IllegalStateException("Cannot add exclusive lock token [" + token + + "] to shared lock [" + toString() + "]"); + } + this.exclusiveLockToken = token; + } + + /** + * Getter for exclusive lock token. + * + * @return String + */ + @Override + public String getExclusiveLockToken() + { + return exclusiveLockToken; + } + + /** + * Setter for lock scope. + * + * @param scope String + */ + @Override + public void setScope(String scope) + { + this.scope = scope; + } + + /** + * Returns lock scope + * + * @return lock scope + */ + @Override + public String getScope() + { + return scope == null ? WebDAV.XML_EXCLUSIVE : scope; + } + + /** + * Setter for lock depth + * + * @param depth lock depth + */ + @Override + public void setDepth(String depth) + { + this.depth = depth; + } + + /** + * Returns lock depth + * + * @return lock depth + */ + @Override + public String getDepth() + { + return depth; + } + + /** + * Getter for sharedLockTokens list. + * + */ + @Override + public Set getSharedLockTokens() + { + return sharedLockTokens; + } + + /** + * Setter for sharedLockTokens list. + * + */ + @Override + public void setSharedLockTokens(Set sharedLockTokens) + { + if (!sharedLockTokens.isEmpty() && isExclusive()) + { + throw new IllegalStateException("Cannot add shared lock tokens [" + sharedLockTokens + + "] to exclusive lock [" + toString() + "]"); + } + this.sharedLockTokens.clear(); + this.sharedLockTokens.addAll(sharedLockTokens); + } + + /** + * Adds new shared lock token to sharedLockTokens list. + * + * @param token The token to add. + */ + @Override + public void addSharedLockToken(String token) + { + if (isExclusive()) + { + throw new IllegalStateException("Cannot add shared lock token [" + token + + "] to exclusive lock [" + toString() + "]"); + } + sharedLockTokens.add(token); + } + + /** + * Is it a shared lock? + * + * @return true if shared. + */ + @Override + @JsonIgnore + public boolean isShared() + { + return (!sharedLockTokens.isEmpty()); + } + + + /** + * Return the lock info as a string + * + * @return String + */ + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("LockInfo["); + + str.append("exclusiveLockToken="); + str.append(getExclusiveLockToken()); + str.append(", scope="); + str.append(getScope()); + str.append(", depth="); + str.append(getDepth()); + str.append(", sharedLockTokens="); + str.append(getSharedLockTokens()); + str.append(", owner="); + str.append(owner); + str.append(", expires="); + str.append(expires); + + str.append("]"); + + return str.toString(); + } + + + @Override + public String toJSON() + { + ObjectMapper objectMapper = new ObjectMapper(); + String json; + try + { + json = objectMapper.writeValueAsString(this); + } + catch (Throwable e) + { + throw new RuntimeException("Unable to generate JSON for " + toString(), e); + } + return ADDINFO_WEBDAV_MARKER + ":" + json; + } + + public static LockInfo fromJSON(String json) + { + ObjectMapper objectMapper = new ObjectMapper(); + + if (json != null && json.startsWith(ADDINFO_WEBDAV_MARKER)) + { + try + { + json = json.substring(ADDINFO_WEBDAV_MARKER.length() + 1); + LockInfo lockInfo = objectMapper.readValue(json, LockInfoImpl.class); + return lockInfo; + } + catch (Throwable e) + { + throw new IllegalArgumentException("Unable to parse JSON from [" + json + "]", e); + } + } + else + { + throw new IllegalArgumentException("Was not detected WEBDAV_LOCK marker."); + } + } + + /** + * Whether this lock has expired. If no expiry is set (i.e. expires is null) + * then false is always returned. + * + * @return true if expired. + */ + @Override + @JsonIgnore + public boolean isExpired() + { + if (expires == null) + { + return false; + } + Date now = dateNow(); + return now.after(expires); + } + + /** + * Is it an exclusive lock? + * + * @return true if exclusive. + */ + @Override + @JsonIgnore + public boolean isExclusive() + { + return (exclusiveLockToken != null && exclusiveLockToken.length() > 0); + } + + /** + * Who owns the lock? + * + * @return the owner + */ + @Override + @JsonIgnore + public String getOwner() + { + return owner; + } + + /** + * Set the username of who owns the lock. + * + * @param owner Owner's username + */ + @Override + @JsonIgnore + public void setOwner(String owner) + { + this.owner = owner; + } + + /** + * Set the expiry date/time for this lock. Set to null for never expires. + * + * @param expires the expires to set + */ + @Override + @JsonIgnore + public void setExpires(Date expires) + { + this.expires = expires; + } + + /** + * Retrieve the expiry date/time for this lock, or null if it never expires. + * + * @return the expires + */ + @Override + @JsonIgnore + public Date getExpires() + { + return expires; + } + + /** + * Remaining time before lock expires, in seconds. + */ + @Override + @JsonIgnore + public long getRemainingTimeoutSeconds() + { + Date expires = getExpires(); + if (expires == null) + { + return WebDAV.TIMEOUT_INFINITY; + } + else + { + Date now = dateNow(); + long timeout = ((expires.getTime() - now.getTime()) / 1000); + return timeout; + } + } + + /** + * Sets the expiry date/time to lockTimeout seconds into the future. Provide + * a lockTimeout of WebDAV.TIMEOUT_INFINITY for never expires. + * + * @param lockTimeoutSecs int + */ + @Override + @JsonIgnore + public void setTimeoutSeconds(int lockTimeoutSecs) + { + if (lockTimeoutSecs == WebDAV.TIMEOUT_INFINITY) + { + setExpires(null); + } + else + { + int timeoutMillis = (lockTimeoutSecs * 1000); + Date now = dateNow(); + Date nextExpiry = new Date(now.getTime() + timeoutMillis); + setExpires(nextExpiry); + } + } + + /** + * Sets the expiry date/time to lockTimeout minutes into the future. Provide + * a lockTimeout of WebDAV.TIMEOUT_INFINITY for never expires. + * + * @param lockTimeoutMins int + */ + @Override + @JsonIgnore + public void setTimeoutMinutes(int lockTimeoutMins) + { + if (lockTimeoutMins != WebDAV.TIMEOUT_INFINITY) + { + setTimeoutSeconds(lockTimeoutMins * 60); + } + else + { + setTimeoutSeconds(WebDAV.TIMEOUT_INFINITY); + } + } + + /** + * Hook to allow unit testing - gets the current date/time. + * + * @return Date + */ + protected Date dateNow() + { + return new Date(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/LockMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/LockMethod.java new file mode 100644 index 00000000000..da9c6ed686c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/LockMethod.java @@ -0,0 +1,578 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileFolderUtil; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleType; +import org.alfresco.service.namespace.QName; +import org.dom4j.io.XMLWriter; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Implements the WebDAV LOCK method + * + * @author gavinc + */ +public class LockMethod extends WebDAVMethod +{ + public static final String EMPTY_NS = ""; + + private static Timer timer = new Timer(true); + + protected int m_timeoutDuration = WebDAV.TIMEOUT_24_HOURS; + + protected LockInfo lockInfo = new LockInfoImpl(); + + protected boolean createExclusive; + + protected String lockToken= null; + + /** + * Default constructor + */ + public LockMethod() + { + } + + /** + * Returns true if request has lock token in the If header + * + * @return boolean + */ + protected final boolean hasLockToken() + { + if (m_conditions != null) + { + for (Condition condition : m_conditions) + { + if (!condition.getLockTokensMatch().isEmpty()) + { + return true; + } + } + } + return false; + } + + /** + * Return the lock timeout, in seconds. + * + * @return int + */ + protected final int getLockTimeout() + { + return m_timeoutDuration; + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // Get user Agent + + m_userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT); + + // Get the depth + + parseDepthHeader(); + + // According to the specification: "Values other than 0 or infinity MUST NOT be used with the Depth header on a LOCK method.". + // The specification does not specify the error code for this case - so we use HttpServletResponse.SC_INTERNAL_SERVER_ERROR. + if (m_depth != WebDAV.DEPTH_0 && m_depth != WebDAV.DEPTH_INFINITY) + { + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + // Parse Lock tokens and ETags, if any + + parseIfHeader(); + + // Get the lock timeout value + + String strTimeout = m_request.getHeader(WebDAV.HEADER_TIMEOUT); + + // If the timeout header starts with anything other than Second + // leave the timeout as the default + + if (strTimeout != null && strTimeout.startsWith(WebDAV.SECOND)) + { + try + { + // Some clients send header as Second-180 Seconds so we need to + // look for the space + + int idx = strTimeout.indexOf(" "); + + if (idx != -1) + { + // Get the bit after Second- and before the space + + strTimeout = strTimeout.substring(WebDAV.SECOND.length(), idx); + } + else + { + // The string must be in the correct format + + strTimeout = strTimeout.substring(WebDAV.SECOND.length()); + } + m_timeoutDuration = Integer.parseInt(strTimeout); + } + catch (Exception e) + { + // Warn about the parse failure and leave the timeout as the + // default + + logger.warn("Failed to parse Timeout header: " + strTimeout); + } + } + + // DEBUG + + if (logger.isDebugEnabled()) + logger.debug("Timeout=" + getLockTimeout() + ", depth=" + getDepth()); + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + if (m_request.getContentLength() == -1) + { + return; + } + + Document body = getRequestBodyAsDocument(); + if (body != null) + { + OUTER: for (Node currentNode = body.getDocumentElement().getFirstChild(); currentNode != null; currentNode = currentNode.getNextSibling()) + { + if (currentNode instanceof Element && WebDAV.DEFAULT_NAMESPACE_URI.equals(((Element) currentNode).getNamespaceURI()) + && WebDAV.XML_LOCK_SCOPE.equals(((Element) currentNode).getLocalName())) + { + for (Node propertiesNode = currentNode.getFirstChild(); propertiesNode != null; propertiesNode = propertiesNode.getNextSibling()) + { + if (propertiesNode instanceof Element && WebDAV.DEFAULT_NAMESPACE_URI.equals(((Element) propertiesNode).getNamespaceURI())) + { + this.createExclusive = WebDAV.XML_EXCLUSIVE.equals(propertiesNode.getLocalName()); + break OUTER; + } + } + break OUTER; + } + } + if (!createExclusive) + { + // We do not support shared locks - return 412 (section 8.10.7 of RFC 2518) + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + } + } + + /** + * Execute the request + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException, Exception + { + RuleService ruleService = getServiceRegistry().getRuleService(); + try + { + // Temporarily disable update rules. + ruleService.disableRuleType(RuleType.UPDATE); + attemptLock(); + } + finally + { + // Re-instate update rules. + ruleService.enableRuleType(RuleType.UPDATE); + } + } + + /** + * The main lock implementation method. + * + * @throws WebDAVServerException + * @throws Exception + */ + protected void attemptLock() throws WebDAVServerException, Exception + { + FileFolderService fileFolderService = getFileFolderService(); + final String path = getPath(); + NodeRef rootNodeRef = getRootNodeRef(); + // Get the active user + final String userName = getDAVHelper().getAuthenticationService().getCurrentUserName(); + + if (logger.isDebugEnabled()) + { + logger.debug("Locking node: \n" + + " user: " + userName + "\n" + + " path: " + path); + } + + FileInfo lockNodeInfo = null; + try + { + // Check if the path exists + lockNodeInfo = getNodeForPath(getRootNodeRef(), getPath()); + } + catch (FileNotFoundException e) + { + if (m_conditions != null) + { + // MNT-12303 fix, check whether this is a refresh lock request + for (Condition condition : m_conditions) + { + List lockTolensMatch = condition.getLockTokensMatch(); + List etagsMatch = condition.getETagsMatch(); + + if (m_request.getContentLength() == -1 && + (lockTolensMatch != null && !lockTolensMatch.isEmpty()) || + (etagsMatch != null && !etagsMatch.isEmpty())) + { + // LOCK method with If header and without body was sent, according to RFC 2518 section 7.8 + // this form of LOCK MUST only be used to "refresh" a lock. However resource doesn't exist -> + // so there is nothing to refresh. Return 403 Forbidden as original SharePoint Server. + throw new WebDAVServerException(HttpServletResponse.SC_FORBIDDEN); + } + } + } + + // need to create it + String[] splitPath = getDAVHelper().splitPath(path); + // check + if (splitPath[1].length() == 0) + { + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + FileInfo dirInfo = null; + List dirPathElements = getDAVHelper().splitAllPaths(splitPath[0]); + if (dirPathElements.size() == 0) + { + // if there are no path elements we are at the root so get the root node + dirInfo = fileFolderService.getFileInfo(getRootNodeRef()); + } + else + { + // make sure folder structure is present + dirInfo = FileFolderUtil.makeFolders(fileFolderService, rootNodeRef, dirPathElements, ContentModel.TYPE_FOLDER); + } + + if (dirInfo == null) + { + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + // create the file + lockNodeInfo = createNode(dirInfo.getNodeRef(), splitPath[1], ContentModel.TYPE_CONTENT); + + // ALF-10309 fix, mark created node with webdavNoContent aspect, we assume that save operation + // is performed by client, webdavNoContent aspect normally removed in put method unless there + // is a cancel before the PUT request takes place + int lockTimeout = getLockTimeout(); + if (lockTimeout > 0 && + !getNodeService().hasAspect(lockNodeInfo.getNodeRef(), ContentModel.ASPECT_WEBDAV_NO_CONTENT)) + { + final NodeRef nodeRef = lockNodeInfo.getNodeRef(); + getNodeService().addAspect(nodeRef, ContentModel.ASPECT_WEBDAV_NO_CONTENT, null); + + // Remove node after the timeout (MS Office 2003 requests 3 minutes) if the PUT or UNLOCK has not taken place + timer.schedule(new TimerTask() + { + @Override + public void run() + { + // run as current user + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + try + { + if (getNodeService().hasAspect(nodeRef, ContentModel.ASPECT_WEBDAV_NO_CONTENT)) + { + getTransactionService().getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + public String execute() throws Throwable + { + getNodeService().deleteNode(nodeRef); + if (logger.isDebugEnabled()) + { + logger.debug("Timer DELETE " + path); + } + return null; + } + }, false, true); + } + else if (logger.isDebugEnabled()) + { + logger.debug("Timer IGNORE " + path); + } + } + catch (InvalidNodeRefException e) + { + // Might get this if the node is deleted. If so just ignore. + if (logger.isDebugEnabled()) + { + logger.debug("Timer DOES NOT EXIST " + path); + } + } + return null; + } + }, userName); + } + }, lockTimeout*1000); + if (logger.isDebugEnabled()) + { + logger.debug("Timer START in " + lockTimeout + " seconds "+ path); + } + } + + if (logger.isDebugEnabled()) + { + logger.debug("Created new node for lock: \n" + + " path: " + path + "\n" + + " node: " + lockNodeInfo); + } + + m_response.setStatus(HttpServletResponse.SC_CREATED); + } + + // Check if this is a new lock or a lock refresh + if (hasLockToken()) + { + lockInfo = checkNode(lockNodeInfo); + + if (!lockInfo.isLocked() && m_request.getContentLength() == -1) + { + // MNT-11990 fix, LOCK method with If header and without body was sent, according to RFC 2518 section 7.8 + // this form of LOCK MUST only be used to "refresh" a lock. But node is not actually locked. Fail this request. + // see http://www.ics.uci.edu/~ejw/authoring/protocol/rfc2518.html#rfc.section.7.8 + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST); + } + + // If a request body is not defined and "If" header is sent we have createExclusive as false, + // but we need to check a previous LOCK was an exclusive. I.e. get the property for node. It + // is already has got in a checkNode method, so we need just get a scope from lockInfo. + // This particular case was raised as ALF-4008. + this.createExclusive = WebDAV.XML_EXCLUSIVE.equals(this.lockInfo.getScope()); + // Refresh an existing lock + refreshLock(lockNodeInfo, userName); + } + else + { + lockInfo = checkNode(lockNodeInfo, true, createExclusive); + // Create a new lock + createLock(lockNodeInfo, userName); + } + + m_response.setHeader(WebDAV.HEADER_LOCK_TOKEN, "<" + WebDAV.makeLockToken(lockNodeInfo.getNodeRef(), userName) + ">"); + m_response.setHeader(WebDAV.HEADER_CONTENT_TYPE, WebDAV.XML_CONTENT_TYPE); + + // We either created a new lock or refreshed an existing lock, send back the lock details + generateResponse(lockNodeInfo, userName); + } + + /** + * Create a new node + * + * @param parentNodeRef the parent node. + * @param name the name of the node + * @param typeQName the type to create + * @return Returns the new node's file information + * + */ + protected FileInfo createNode(NodeRef parentNodeRef, String name, QName typeQName) + { + return getFileFolderService().create(parentNodeRef, name, ContentModel.TYPE_CONTENT); + } + + /** + * Create a new lock + * + * @param lockNode NodeRef + * @param userName String + * @exception WebDAVServerException + */ + protected final void createLock(FileInfo lockNode, String userName) throws WebDAVServerException + { + // Create Lock token + lockToken = WebDAV.makeLockToken(lockNode.getNodeRef(), userName); + + if (createExclusive) + { + // Lock the node + lockInfo.setTimeoutSeconds(getLockTimeout()); + lockInfo.setExclusiveLockToken(lockToken); + } + else + { + // Shared lock creation should already have been prohibited when parsing the request body + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + + // Store lock depth + lockInfo.setDepth(WebDAV.getDepthName(m_depth)); + // Store lock scope (shared/exclusive) + String scope = createExclusive ? WebDAV.XML_EXCLUSIVE : WebDAV.XML_SHARED; + lockInfo.setScope(scope); + // Store the owner of this lock + lockInfo.setOwner(userName); + // Lock the node + getDAVLockService().lock(lockNode.getNodeRef(), lockInfo); + + if (logger.isDebugEnabled()) + { + logger.debug("Locked node " + lockNode + ": " + lockInfo); + } + } + + /** + * Refresh an existing lock + * + * @param lockNode NodeRef + * @param userName String + * @exception WebDAVServerException + */ + protected final void refreshLock(FileInfo lockNode, String userName) throws WebDAVServerException + { + if (this.createExclusive) + { + // Update the expiry for the lock + lockInfo.setTimeoutSeconds(getLockTimeout()); + getDAVLockService().lock(lockNode.getNodeRef(), lockInfo); + } + } + + /** + * Generates the XML lock discovery response body + */ + protected void generateResponse(FileInfo lockNodeInfo, String userName) throws Exception + { + String scope; + String lt; + String owner; + Date expiry; + + if (lockToken != null) + { + // In case of lock creation take the scope from request header + scope = this.createExclusive ? WebDAV.XML_EXCLUSIVE : WebDAV.XML_SHARED; + // Output created lock + lt = lockToken; + } + else + { + // In case of lock refreshing take the scope from previously stored lock + scope = this.lockInfo.getScope(); + // Output refreshed lock + lt = this.lockInfo.getExclusiveLockToken(); + } + owner = lockInfo.getOwner(); + expiry = lockInfo.getExpires(); + + XMLWriter xml = createXMLWriter(); + + xml.startDocument(); + + String nsdec = generateNamespaceDeclarations(null); + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROP + nsdec, WebDAV.XML_NS_PROP + nsdec, getDAVHelper().getNullAttributes()); + + // Output the lock details + generateLockDiscoveryXML(xml, lockNodeInfo, false, scope, WebDAV.getDepthName(m_depth), lt, owner, expiry); + + // Close off the XML + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP); + + // Send remaining data + flushXML(xml); + + } + + /** + * Generates a list of namespace declarations for the response + */ + protected String generateNamespaceDeclarations(HashMap nameSpaces) + { + StringBuilder ns = new StringBuilder(); + + ns.append(" "); + ns.append(WebDAV.XML_NS); + ns.append(":"); + ns.append(WebDAV.DAV_NS); + ns.append("=\""); + ns.append(WebDAV.DEFAULT_NAMESPACE_URI); + ns.append("\""); + + // Add additional namespaces + + if ( nameSpaces != null) + { + Iterator namespaceList = nameSpaces.keySet().iterator(); + + while (namespaceList.hasNext()) + { + String strNamespaceUri = namespaceList.next(); + String strNamespaceName = nameSpaces.get(strNamespaceUri); + + ns.append(" ").append(WebDAV.XML_NS).append(":").append(strNamespaceName).append("=\""); + ns.append(strNamespaceUri).append("\" "); + } + } + + return ns.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/MkcolMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/MkcolMethod.java new file mode 100644 index 00000000000..5efdb0e1f83 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/MkcolMethod.java @@ -0,0 +1,194 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.webdav.WebDavService; + +/** + * Implements the WebDAV MKCOL method + * + * @author gavinc + */ +public class MkcolMethod extends WebDAVMethod implements ActivityPostProducer +{ + private WebDAVActivityPoster activityPoster; + + /** + * Default constructor + */ + public MkcolMethod() + { + } + + /** + * Parse the request headers + * + * @throws WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // Nothing to do in this method + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // There should not be a body with the MKCOL request + + if (m_request.getContentLength() > 0) + { + throw new WebDAVServerException(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + } + } + + /** + * Execute the request + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException, Exception + { + FileFolderService fileFolderService = getFileFolderService(); + + // see if it exists + try + { + getDAVHelper().getNodeForPath(getRootNodeRef(), getPath()); + // already exists + throw new WebDAVServerException(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + catch (FileNotFoundException e) + { + // it doesn't exist + } + + // Trim the last path component and check if the parent path exists + String parentPath = getPath(); + int lastPos = parentPath.lastIndexOf(WebDAVHelper.PathSeperator); + + NodeRef parentNodeRef = null; + + if ( lastPos == 0) + { + // Create new folder at root + + parentPath = WebDAVHelper.PathSeperator; + parentNodeRef = getRootNodeRef(); + } + else if (lastPos != -1) + { + // Trim the last path component + parentPath = parentPath.substring(0, lastPos + 1); + try + { + FileInfo parentFileInfo = getDAVHelper().getNodeForPath(getRootNodeRef(), parentPath); + parentNodeRef = parentFileInfo.getNodeRef(); + } + catch (FileNotFoundException e) + { + // parent path is missing + throw new WebDAVServerException(HttpServletResponse.SC_CONFLICT); + } + } + else + { + // Looks like a bad path + throw new WebDAVServerException(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + // Get the new folder name + String folderName = getPath().substring(lastPos + 1); + + // Create the new folder node + FileInfo fileInfo = fileFolderService.create(parentNodeRef, folderName, ContentModel.TYPE_FOLDER); + + // Don't post activity data for hidden folder + if (!fileInfo.isHidden()) + { + postActivity(fileInfo); + } + + // Return a success status + m_response.setStatus(HttpServletResponse.SC_CREATED); + } + + /** + * Create a folder added activity post. + * + * @throws WebDAVServerException + */ + private void postActivity(FileInfo fileInfo) throws WebDAVServerException + { + WebDavService davService = getDAVHelper().getServiceRegistry().getWebDavService(); + if (!davService.activitiesEnabled()) + { + // Don't post activities if this behaviour is disabled. + return; + } + + String siteId = getSiteId(); + String tenantDomain = getTenantDomain(); + + // Check there is enough information to publish site activity. + if (!siteId.equals(WebDAVHelper.EMPTY_SITE_ID)) + { + SiteService siteService = getServiceRegistry().getSiteService(); + NodeRef documentLibrary = siteService.getContainer(siteId, SiteService.DOCUMENT_LIBRARY); + String path = "/"; + try + { + path = getDAVHelper().getPathFromNode(documentLibrary, fileInfo.getNodeRef()); + } + catch (FileNotFoundException error) + { + if (logger.isDebugEnabled()) + { + logger.debug("No " + SiteService.DOCUMENT_LIBRARY + " container found."); + } + } + + activityPoster.postFileFolderAdded(siteId, tenantDomain, path, fileInfo); + } + } + + @Override + public void setActivityPoster(WebDAVActivityPoster activityPoster) + { + this.activityPoster = activityPoster; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/MoveMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/MoveMethod.java new file mode 100644 index 00000000000..89045de23db --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/MoveMethod.java @@ -0,0 +1,347 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Implements the WebDAV MOVE method + * + * @author Derek Hulley + * @author David Ward + */ +public class MoveMethod extends HierarchicalMethod +{ + /** + * Default constructor + */ + public MoveMethod() + { + } + + protected boolean isMove() + { + return true; + } + + /** + * Exceute the request + * + * @exception WebDAVServerException + */ + protected final void executeImpl() throws WebDAVServerException, Exception + { + NodeRef rootNodeRef = getRootNodeRef(); + // Debug + if (logger.isDebugEnabled()) + { + logger.debug((isMove() ? "Move" : "Copy") + " from " + getPath() + " to " + getDestinationPath()); + } + + // the source must exist + String sourcePath = getPath(); + FileInfo sourceInfo = null; + try + { + sourceInfo = getNodeForPath(rootNodeRef, sourcePath); + } + catch (FileNotFoundException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + FileInfo sourceParentInfo = getDAVHelper().getParentNodeForPath(rootNodeRef, sourcePath); + + // the destination parent must exist + String destPath = getDestinationPath(); + FileInfo destParentInfo = null; + try + { + if (destPath.endsWith(WebDAVHelper.PathSeperator)) + { + destPath = destPath.substring(0, destPath.length() - 1); + } + destParentInfo = getDAVHelper().getParentNodeForPath(rootNodeRef, destPath); + } + catch (FileNotFoundException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Destination parent folder doesn't exist: " + destPath); + } + throw new WebDAVServerException(HttpServletResponse.SC_CONFLICT); + } + + // check for the existence of the destination node + FileInfo destInfo = null; + boolean destNotHidden = false; + try + { + destInfo = getDAVHelper().getNodeForPath(rootNodeRef, destPath); + if (!destInfo.getNodeRef().equals(sourceInfo.getNodeRef())) + { + // ALF-7079 (MNT-1601) fix, if destInfo is a hidden shuffle target then pretend it's not there + destNotHidden = !getFileFolderService().isHidden(destInfo.getNodeRef()); + if (!hasOverWrite() && destNotHidden) + { + if (logger.isDebugEnabled()) + { + logger.debug("Destination exists but overwrite is not allowed"); + } + // it exists and we may not overwrite + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + checkNode(destInfo); + } + } + catch (FileNotFoundException e) + { + // destination doesn't exist + } + + NodeRef sourceNodeRef = sourceInfo.getNodeRef(); + NodeRef sourceParentNodeRef = sourceParentInfo.getNodeRef(); + NodeRef destParentNodeRef = destParentInfo.getNodeRef(); + + String name = getDAVHelper().splitPath(destPath)[1]; + + moveOrCopy(sourceNodeRef, sourceParentNodeRef, destParentNodeRef, name); + + // Set the response status + if (!destNotHidden) + { + m_response.setStatus(HttpServletResponse.SC_CREATED); + } + else + { + m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); + } + } + + protected void parseRequestHeaders() throws WebDAVServerException + { + super.parseRequestHeaders(); + parseIfHeader(); + } + + protected void moveOrCopy( + NodeRef sourceNodeRef, + NodeRef sourceParentNodeRef, + NodeRef destParentNodeRef, + String name) throws Exception + { + FileFolderService fileFolderService = getFileFolderService(); + NodeRef rootNodeRef = getRootNodeRef(); + + String sourcePath = getPath(); + List sourcePathElements = getDAVHelper().splitAllPaths(sourcePath); + FileInfo sourceFileInfo = null; + + String destPath = getDestinationPath(); + List destPathElements = getDAVHelper().splitAllPaths(destPath); + FileInfo destFileInfo = null; + + boolean isMove = isMove(); + + try + { + // get the node to move + sourceFileInfo = fileFolderService.resolveNamePath(rootNodeRef, sourcePathElements); + destFileInfo = fileFolderService.resolveNamePath(rootNodeRef, destPathElements); + } + catch (FileNotFoundException e) + { + if (sourceFileInfo == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Source node not found: " + sourcePath); + } + // nothing to move + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + } + LockInfo lockInfo = null; + if (isMove) + { + lockInfo = checkNode(sourceFileInfo); + } + // ALF-7079 fix, if destination exists then its content is updated with source content and source is deleted if + // this is a move + if (!sourceFileInfo.isFolder() && destFileInfo != null && !sourceFileInfo.equals(destFileInfo)) + { + copyContentOnly(sourceFileInfo, destFileInfo, fileFolderService); + fileFolderService.setHidden(destFileInfo.getNodeRef(), false); + if (isMove) + { + if (getDAVHelper().isRenameShuffle(destPath) && !getDAVHelper().isRenameShuffle(sourcePath)) + { + // if temporary or backup file already exists + // don't delete source that is node with version history + fileFolderService.setHidden(sourceNodeRef, true); + // As per the WebDAV spec, we make sure the node is unlocked once moved + unlock(sourceNodeRef, lockInfo); + } + else + { + fileFolderService.delete(sourceNodeRef); + } + } + } + // If this is a copy then the source is just copied to destination. + else if (!isMove) + { + // MNT-9939 - check overwrite + if (hasOverWrite() && destFileInfo != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Destination exists and overwrite is allowed"); + } + + fileFolderService.delete(destFileInfo.getNodeRef()); + } + + fileFolderService.copy(sourceNodeRef, destParentNodeRef, name); + } + // If this is a move and the destination looks like the start of a shuffle operation, then the source is just + // copied to destination and the source is hidden. + else if (!sourceFileInfo.isFolder() && getDAVHelper().isRenameShuffle(destPath) && !getDAVHelper().isRenameShuffle(sourcePath)) + { + destFileInfo = fileFolderService.create(destParentNodeRef, name, ContentModel.TYPE_CONTENT); + copyContentOnly(sourceFileInfo, destFileInfo, fileFolderService); + fileFolderService.setHidden(sourceNodeRef, true); + + // As per the WebDAV spec, we make sure the node is unlocked once moved + unlock(sourceNodeRef, lockInfo); + } + else if (sourceParentNodeRef.equals(destParentNodeRef)) + { + // It is a simple rename operation + // MNT-9939 - check overwrite + if (hasOverWrite() && destFileInfo != null && !sourceFileInfo.equals(destFileInfo)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Destination exists and overwrite is allowed"); + } + + fileFolderService.delete(destFileInfo.getNodeRef()); + } + + fileFolderService.rename(sourceNodeRef, name); + + // MNT-13144 WebDav does not correctly version CAD drawings correctly when saved using Windows mapped drive + if (!sourceFileInfo.isFolder() && getDAVHelper().isRenameShuffle(name)) + { + fileFolderService.setHidden(sourceFileInfo.getNodeRef(), true); + } + + // As per the WebDAV spec, we make sure the node is unlocked once moved + unlock(sourceNodeRef, lockInfo); + } + else + { + // It is a simple move operation + // MNT-9939 - check overwrite + if (hasOverWrite() && destFileInfo != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Destination exists and overwrite is allowed"); + } + + fileFolderService.delete(destFileInfo.getNodeRef()); + } + + fileFolderService.moveFrom(sourceNodeRef, sourceParentNodeRef, destParentNodeRef, name); + + // As per the WebDAV spec, we make sure the node is unlocked once moved + unlock(sourceNodeRef, lockInfo); + } + } + + private void copyContentOnly(FileInfo sourceFileInfo, FileInfo destFileInfo, FileFolderService fileFolderService) throws WebDAVServerException + { + ContentService contentService = getContentService(); + ContentReader reader = contentService.getReader(sourceFileInfo.getNodeRef(), ContentModel.PROP_CONTENT); + if (reader == null) + { + // There is no content for the node if it is a folder + if (!sourceFileInfo.isFolder()) + { + // Non-folders should have content available. + logger.error("Unable to get ContentReader for source node " + sourceFileInfo.getNodeRef()); + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + } + else + { + ContentWriter contentWriter = contentService.getWriter(destFileInfo.getNodeRef(), ContentModel.PROP_CONTENT, true); + contentWriter.putContent(reader); + } + } + + /** + * Unlock only if the node was locked in the first place. + */ + private void unlock(final NodeRef nodeRef, LockInfo lockInfo) + { + if (lockInfo != null && lockInfo.isLocked()) + { + if (lockInfo.isExpired()) + { + // If the lock expired unlock as system user + AuthenticationUtil.runAs(new RunAsWork() + { + public Void doWork() throws Exception + { + getDAVHelper().getLockService().unlock(nodeRef); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + // else unlock as current user + else + { + getDAVHelper().getLockService().unlock(nodeRef); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/OptionsMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/OptionsMethod.java new file mode 100644 index 00000000000..ee925fb6074 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/OptionsMethod.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; + +/** + * Implements the WebDAV OPTIONS method + * + * @author Gavin Cornwell + */ +public class OptionsMethod extends WebDAVMethod +{ + private static final String DAV_HEADER = "DAV"; + private static final String DAV_HEADER_CONTENT = "1,2"; + private static final String ALLOW_HEADER = "Allow"; + private static final String MS_HEADER = "MS-Author-Via"; + + private static final String FILE_METHODS = "OPTIONS, GET, HEAD, POST, DELETE, PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK"; + private static final String COLLECTION_METHODS = FILE_METHODS + ", PUT"; + + /** + * Default constructor + */ + public OptionsMethod() + { + } + + /** + * Parse the request header fields + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // Nothing to do in this method + } + + /** + * Parse the request main body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // Nothing to do in this method + } + + /** + * @return Returns true always + */ + @Override + protected boolean isReadOnly() + { + return true; + } + + /** + * Perform the main request processing + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException + { + Boolean isFolder = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public Boolean doWork() throws FileNotFoundException + { + try + { + FileInfo fileInfo = getDAVHelper().getNodeForPath(getRootNodeRef(), getPath()); + return fileInfo.isFolder(); + } + catch (FileNotFoundException e) + { + // Do nothing; just default to a folder + return true; + } + } + }, AuthenticationUtil.getSystemUserName()); + // Add the header to advertise the level of support the server has + m_response.addHeader(DAV_HEADER, DAV_HEADER_CONTENT); + + // Add the proprietary Microsoft header to make Microsoft clients behave + m_response.addHeader(MS_HEADER, DAV_HEADER); + + // Add the header to show what methods are allowed + m_response.addHeader(ALLOW_HEADER, isFolder ? COLLECTION_METHODS : FILE_METHODS); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/PostMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/PostMethod.java new file mode 100644 index 00000000000..0a9c9597917 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/PostMethod.java @@ -0,0 +1,41 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +/** + * Implements the WebDAV POST method + * + * @author Gavin Cornwell + */ +public class PostMethod extends PutMethod +{ + /** + * Default constructor + */ + public PostMethod() + { + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/PropFindMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/PropFindMethod.java new file mode 100644 index 00000000000..437abe78ec0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/PropFindMethod.java @@ -0,0 +1,1050 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.webdav.auth.AuthenticationFilter; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConverter; +import org.alfresco.service.namespace.InvalidQNameException; +import org.alfresco.service.namespace.QName; +import org.dom4j.DocumentHelper; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Implements the WebDAV PROPFIND method + * + * @author Gavin Cornwell + */ +public class PropFindMethod extends WebDAVMethod +{ + // Request types + protected static final int GET_ALL_PROPS = 0; + protected static final int GET_NAMED_PROPS = 1; + protected static final int FIND_PROPS = 2; + + // Find request type + protected int m_mode = GET_ALL_PROPS; + + // Requested properties + protected ArrayList m_properties = null; + + // Available namespaces list + protected HashMap m_namespaces = null; + + /** + * Default constructor + */ + public PropFindMethod() + { + m_namespaces = new HashMap(); + } + + /** + * Return the find mode + * + * @return int + */ + public final int getMode() + { + return m_mode; + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // Store the Depth header as this is used by several WebDAV methods + + parseDepthHeader(); + + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + Document body = getRequestBodyAsDocument(); + if (body != null) + { + Element rootElement = body.getDocumentElement(); + NodeList childList = rootElement.getChildNodes(); + Node node = null; + + for (int i = 0; i < childList.getLength(); i++) + { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) + { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + if (currentNode.getNodeName().endsWith(WebDAV.XML_ALLPROP)) + { + m_mode = GET_ALL_PROPS; + } + else if (currentNode.getNodeName().endsWith(WebDAV.XML_PROP)) + { + m_mode = GET_NAMED_PROPS; + node = currentNode; + } + else if (currentNode.getNodeName().endsWith(WebDAV.XML_PROPNAME)) + { + m_mode = FIND_PROPS; + } + + break; + } + } + + if (m_mode == GET_NAMED_PROPS) + { + m_properties = new ArrayList(); + childList = node.getChildNodes(); + + for (int i = 0; i < childList.getLength(); i++) + { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) + { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + m_properties.add(createProperty(currentNode)); + break; + } + } + } + } + } + + /** + * @return Returns true always + */ + @Override + protected boolean isReadOnly() + { + return true; + } + + /** + * Execute the main WebDAV request processing + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException, Exception + { + m_response.setStatus(WebDAV.WEBDAV_SC_MULTI_STATUS); + + FileInfo pathNodeInfo = null; + try + { + // Check that the path exists + pathNodeInfo = getDAVHelper().getNodeForPath(getRootNodeRef(), m_strPath); + } + catch (FileNotFoundException e) + { + // The path is not valid - send a 404 error back to the client + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + // A node hidden during a 'shuffle' operation - send a 404 error back to the client, as some Mac clients need this + // Note the null check, as root node may be null in cloud. + if (pathNodeInfo.getNodeRef() != null && getFileFolderService().isHidden(pathNodeInfo.getNodeRef())) + { + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + // Set the response content type + + m_response.setContentType(WebDAV.XML_CONTENT_TYPE); + + // Create multistatus response + + XMLWriter xml = createXMLWriter(); + + xml.startDocument(); + + String nsdec = generateNamespaceDeclarations(m_namespaces); + xml.startElement( + WebDAV.DAV_NS, + WebDAV.XML_MULTI_STATUS + nsdec, + WebDAV.XML_NS_MULTI_STATUS + nsdec, + getDAVHelper().getNullAttributes()); + + // Create the path for the current location in the tree + StringBuilder baseBuild = new StringBuilder(256); + baseBuild.append(getPath()); + if (baseBuild.length() == 0 || baseBuild.charAt(baseBuild.length() - 1) != WebDAVHelper.PathSeperatorChar) + { + baseBuild.append(WebDAVHelper.PathSeperatorChar); + } + String basePath = baseBuild.toString(); + + // Output the response for the root node, depth zero + generateResponseForNode(xml, pathNodeInfo, basePath); + + // If additional levels are required and the root node is a folder then recurse to the required + // level and output node details a level at a time + if (getDepth() != WebDAV.DEPTH_0 && pathNodeInfo.isFolder()) + { + // Create the initial list of nodes to report + List nodeInfos = new ArrayList(10); + nodeInfos.add(pathNodeInfo); + + int curDepth = WebDAV.DEPTH_1; + + // Save the base path length + int baseLen = baseBuild.length(); + + // List of next level of nodes to report + List nextNodeInfos = null; + if (getDepth() > WebDAV.DEPTH_1) + { + nextNodeInfos = new ArrayList(10); + } + + // Loop reporting each level of nodes to the requested depth + while (curDepth <= getDepth() && nodeInfos != null) + { + // Clear out the next level of nodes, if required + if (nextNodeInfos != null) + { + nextNodeInfos.clear(); + } + + // Output the current level of node(s), the node list should + // only contain folder nodes + + for (FileInfo curNodeInfo : nodeInfos) + { + // Get the list of child nodes for the current node + List childNodeInfos = getDAVHelper().getChildren(curNodeInfo); + + // can skip the current node if it doesn't have children + if (childNodeInfos.size() == 0) + { + continue; + } + + // Output the child node details + // Generate the base path for the current parent node + + baseBuild.setLength(baseLen); + try + { + String pathSnippet = null; + if ((pathNodeInfo.getNodeRef() == null) && (curNodeInfo.getNodeRef() == null)) + { + // TODO review - note: can be null in case of Thor + pathSnippet = "/"; + } + else + { + pathSnippet = getDAVHelper().getPathFromNode(pathNodeInfo.getNodeRef(), curNodeInfo.getNodeRef()); + } + + baseBuild.append(pathSnippet); + } + catch (FileNotFoundException e) + { + // move to the next node + continue; + } + + int curBaseLen = baseBuild.length(); + + // Output the child node details + for (FileInfo curChildInfo : childNodeInfos) + { + // Build the path for the current child node + baseBuild.setLength(curBaseLen); + + baseBuild.append(curChildInfo.getName()); + + // Output the current child node details + generateResponseForNode(xml, curChildInfo, baseBuild.toString()); + + // If the child is a folder add it to the list of next level nodes + if (nextNodeInfos != null && curChildInfo.isFolder()) + { + nextNodeInfos.add(curChildInfo); + } + } + } + + // Update the current tree depth + curDepth++; + + // Move the next level of nodes to the current node list + nodeInfos = nextNodeInfos; + } + } + + // Close the outer XML element + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_MULTI_STATUS, WebDAV.XML_NS_MULTI_STATUS); + + // Send remaining data + flushXML(xml); + } + + @Override + protected OutputFormat getXMLOutputFormat() + { + String userAgent = m_request.getHeader("User-Agent"); + return ((null != userAgent) && userAgent.toLowerCase().startsWith("microsoft-webdav-miniredir/5.1.")) ? OutputFormat.createCompactFormat() : super.getXMLOutputFormat(); + + } + + /** + * Creates a WebDAVProperty from the given XML node + */ + protected WebDAVProperty createProperty(Node node) + { + WebDAVProperty property = null; + + String strName = node.getLocalName(); + String strNamespaceUri = node.getNamespaceURI(); + + if (WebDAV.DEFAULT_NAMESPACE_URI.equals(strNamespaceUri)) + { + property = new WebDAVProperty(strName); + } + else + { + property = new WebDAVProperty(strName, strNamespaceUri, getNamespaceName(strNamespaceUri)); + } + + return property; + } + + /** + * Retrieves the namespace name for the given namespace URI, one is + * generated if it doesn't exist + */ + private String getNamespaceName(String strNamespaceUri) + { + if (strNamespaceUri == null) + { + return null; + } + String strNamespaceName = m_namespaces.get(strNamespaceUri); + if (strNamespaceName == null) + { + strNamespaceName = "ns" + m_namespaces.size(); + m_namespaces.put(strNamespaceUri, strNamespaceName); + } + + return strNamespaceName; + } + + /** + * Generates the required response XML for the current node + * + * @param xml XMLWriter + * @param nodeInfo FileInfo + * @param path String + */ + protected void generateResponseForNode(XMLWriter xml, FileInfo nodeInfo, String path) throws Exception + { + boolean isFolder = nodeInfo.isFolder(); + + // Output the response block for the current node + xml.startElement( + WebDAV.DAV_NS, + WebDAV.XML_RESPONSE, + WebDAV.XML_NS_RESPONSE, + getDAVHelper().getNullAttributes()); + + // Build the href string for the current node + String strHRef = getURLForPath(m_request, path, isFolder); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_HREF, WebDAV.XML_NS_HREF, getDAVHelper().getNullAttributes()); + xml.write(strHRef); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_HREF, WebDAV.XML_NS_HREF); + + switch (m_mode) + { + case GET_NAMED_PROPS: + generateNamedPropertiesResponse(xml, nodeInfo, isFolder); + break; + case GET_ALL_PROPS: + generateAllPropertiesResponse(xml, nodeInfo, isFolder); + break; + case FIND_PROPS: + generateFindPropertiesResponse(xml, nodeInfo, isFolder); + break; + } + + // Close off the response element + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_RESPONSE, WebDAV.XML_NS_RESPONSE); + } + + /** + * Generates the XML response for a PROPFIND request that asks for a + * specific set of properties + * + * @param xml XMLWriter + * @param nodeInfo FileInfo + * @param isDir boolean + */ + private void generateNamedPropertiesResponse(XMLWriter xml, FileInfo nodeInfo, boolean isDir) throws Exception + { + // Get the properties for the node + Map props = nodeInfo.getProperties(); + Map deadProperties = null; + + // Output the start of the properties element + Attributes nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT, nullAttr); + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP, nullAttr); + + ArrayList propertiesNotFound = new ArrayList(); + + TypeConverter typeConv = DefaultTypeConverter.INSTANCE; + + // Loop through the requested property list + for (WebDAVProperty property : m_properties) + { + // Get the requested property details + + String propName = property.getName(); + String propNamespaceUri = property.getNamespaceUri(); + + // Check if the property is a standard WebDAV property + + Object davValue = null; + + if (WebDAV.DEFAULT_NAMESPACE_URI.equals(propNamespaceUri)) + { + // Check if the client is requesting lock information + if (propName.equals(WebDAV.XML_LOCK_DISCOVERY)) // && metaData.isLocked()) + { + generateLockDiscoveryResponse(xml, nodeInfo, isDir); + } + else if (propName.equals(WebDAV.XML_SUPPORTED_LOCK)) + { + // Output the supported lock types + writeLockTypes(xml); + } + + // Check if the client is requesting the resource type + + else if (propName.equals(WebDAV.XML_RESOURCE_TYPE)) + { + // If the node is a folder then return as a collection type + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_RESOURCE_TYPE, WebDAV.XML_NS_RESOURCE_TYPE, nullAttr); + if (isDir) + { + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_COLLECTION)); + } + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_RESOURCE_TYPE, WebDAV.XML_NS_RESOURCE_TYPE); + } + else if (propName.equals(WebDAV.XML_DISPLAYNAME)) + { + // Get the node name + if (getRootNodeRef().equals(nodeInfo.getNodeRef())) + { + // Output an empty name for the root node + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_SOURCE)); + } + else + { + // Get the node name + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_DISPLAYNAME); + + // Output the node name + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_DISPLAYNAME, WebDAV.XML_NS_DISPLAYNAME, nullAttr); + if (davValue != null) + { + String name = typeConv.convert(String.class, davValue); + if (name == null || name.length() == 0) + { + logger.error("WebDAV name is null, value=" + davValue.getClass().getName() + ", node=" + nodeInfo.getNodeRef()); + } + xml.write(name); + } + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_DISPLAYNAME, WebDAV.XML_NS_DISPLAYNAME); + } + } + else if (propName.equals(WebDAV.XML_SOURCE)) + { + // NOTE: source is always a no content element in our + // implementation + + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_SOURCE)); + } + else if (propName.equals(WebDAV.XML_GET_LAST_MODIFIED)) + { + // Get the modifed date/time + + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_GET_LAST_MODIFIED); + + // Output the last modified date of the node + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_LAST_MODIFIED, WebDAV.XML_NS_GET_LAST_MODIFIED, + nullAttr); + if (davValue != null) + xml.write(WebDAV.formatModifiedDate(typeConv.convert(Date.class, davValue))); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_LAST_MODIFIED, WebDAV.XML_NS_GET_LAST_MODIFIED); + } + else if (propName.equals(WebDAV.XML_GET_CONTENT_LANGUAGE) && !isDir) + { + // Get the content language + // TODO: + // Output the content language + xml.startElement( + WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LANGUAGE, + WebDAV.XML_NS_GET_CONTENT_LANGUAGE, nullAttr); + // TODO: + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LANGUAGE, WebDAV.XML_NS_GET_CONTENT_LANGUAGE); + } + else if (propName.equals(WebDAV.XML_GET_CONTENT_TYPE) && !isDir) + { + // Get the content type + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_GET_CONTENT_TYPE); + + // Output the content type + xml.startElement( + WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_TYPE, + WebDAV.XML_NS_GET_CONTENT_TYPE, nullAttr); + if (davValue != null) + xml.write(typeConv.convert(String.class, davValue)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_TYPE, WebDAV.XML_NS_GET_CONTENT_TYPE); + } + else if (propName.equals(WebDAV.XML_GET_ETAG) && !isDir) + { + // Output the etag + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_ETAG, WebDAV.XML_NS_GET_ETAG, nullAttr); + xml.write(getDAVHelper().makeETag(nodeInfo)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_ETAG, WebDAV.XML_NS_GET_ETAG); + } + else if (propName.equals(WebDAV.XML_GET_CONTENT_LENGTH)) + { + // Get the content length, if it's not a folder + long len = 0; + + if (!isDir) + { + ContentData contentData = (ContentData) props.get(ContentModel.PROP_CONTENT); + if (contentData != null) + len = contentData.getSize(); + } + + // Output the content length + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LENGTH, WebDAV.XML_NS_GET_CONTENT_LENGTH, + nullAttr); + xml.write("" + len); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LENGTH, WebDAV.XML_NS_GET_CONTENT_LENGTH); + } + else if (propName.equals(WebDAV.XML_CREATION_DATE)) + { + // Get the creation date + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_CREATION_DATE); + + // Output the creation date + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_CREATION_DATE, WebDAV.XML_NS_CREATION_DATE, nullAttr); + if (davValue != null) + xml.write(WebDAV.formatCreationDate(typeConv.convert(Date.class, davValue))); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_CREATION_DATE, WebDAV.XML_NS_CREATION_DATE); + } + else if ( propName.equals( WebDAV.XML_ALF_AUTHTICKET)) + { + // Get the users authentication ticket + + SessionUser davUser = (SessionUser) m_request.getSession().getAttribute( AuthenticationFilter.AUTHENTICATION_USER); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_ALF_AUTHTICKET, WebDAV.XML_NS_ALF_AUTHTICKET, nullAttr); + if ( davUser != null) + xml.write( davUser.getTicket()); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_ALF_AUTHTICKET, WebDAV.XML_NS_ALF_AUTHTICKET); + } + else + { + // Could not map the requested property to an Alfresco property + if (property.getName().equals(WebDAV.XML_HREF) == false) + propertiesNotFound.add(property); + } + } + else + { + // Look in the custom properties + +// String qualifiedName = propNamespaceUri + WebDAV.NAMESPACE_SEPARATOR + propName; + + String value = (String) nodeInfo.getProperties().get(property.createQName()); + if (value == null) + { + if (deadProperties == null) + { + deadProperties = loadDeadProperties(nodeInfo.getNodeRef()); + } + value = deadProperties.get(property.createQName()); + } + + if (value == null) + { + propertiesNotFound.add(property); + } + else + { + if (property.hasNamespaceName()) + { + xml.startElement(property.getNamespaceName(), property.getName(), property.getNamespaceName() + WebDAV.NAMESPACE_SEPARATOR + property.getName(), nullAttr); + xml.write(value); + xml.endElement(property.getNamespaceName(), property.getName(), property.getNamespaceName() + WebDAV.NAMESPACE_SEPARATOR + property.getName()); + } + else + { + xml.startElement("", property.getName(), property.getName(), nullAttr); + xml.write(value); + xml.endElement("", property.getName(), property.getName()); + } + } + + } + } + + // Close off the successful part of the response + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS, nullAttr); + xml.write(WebDAV.HTTP1_1 + " " + HttpServletResponse.SC_OK + " " + WebDAV.SC_OK_DESC); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT); + + // If some of the requested properties were not found return another + // status section + + if (propertiesNotFound.size() > 0) + { + // Start the second status section + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT, nullAttr); + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP, nullAttr); + + // Loop through the list of properties that were not found + + for (WebDAVProperty property : propertiesNotFound) + { + // Output the property not found status block + + String propName = property.getName(); + String propNamespaceName = property.getNamespaceName(); + String propQName = propName; + if (propNamespaceName != null && propNamespaceName.length() > 0) + propQName = propNamespaceName + ":" + propName; + + xml.write(DocumentHelper.createElement(propQName)); + } + + // Close the unsuccessful part of the response + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS, nullAttr); + xml.write(WebDAV.HTTP1_1 + " " + HttpServletResponse.SC_NOT_FOUND + " " + WebDAV.SC_NOT_FOUND_DESC); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT); + } + } + + /** + * Generates the XML response for a PROPFIND request that asks for all known + * properties + * + * @param xml XMLWriter + * @param nodeInfo FileInfo + * @param isDir boolean + */ + protected void generateAllPropertiesResponse(XMLWriter xml, FileInfo nodeInfo, boolean isDir) throws Exception + { + // Get the properties for the node + + Map props = nodeInfo.getProperties(); + + // Output the start of the properties element + + Attributes nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT, nullAttr); + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP, nullAttr); + + // Generate a lock status report, if locked + + generateLockDiscoveryResponse(xml, nodeInfo, isDir); + + // Output the supported lock types + + writeLockTypes(xml); + + // If the node is a folder then return as a collection type + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_RESOURCE_TYPE, WebDAV.XML_NS_RESOURCE_TYPE, nullAttr); + if (isDir) + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_COLLECTION)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_RESOURCE_TYPE, WebDAV.XML_NS_RESOURCE_TYPE); + + // Get the node name + + Object davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_DISPLAYNAME); + + TypeConverter typeConv = DefaultTypeConverter.INSTANCE; + + // Output the node name + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_DISPLAYNAME, WebDAV.XML_NS_DISPLAYNAME, nullAttr); + if (davValue != null) + { + String name = typeConv.convert(String.class, davValue); + if (name == null || name.length() == 0) + { + logger.error("WebDAV name is null, value=" + davValue.getClass().getName() + ", node=" + nodeInfo.getNodeRef()); + } + xml.write(name); + } + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_DISPLAYNAME, WebDAV.XML_NS_DISPLAYNAME); + + // Output the source + // + // NOTE: source is always a no content element in our implementation + + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_SOURCE)); + + // Get the creation date + + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_CREATION_DATE); + + // Output the creation date + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_CREATION_DATE, WebDAV.XML_NS_CREATION_DATE, nullAttr); + if (davValue != null) + xml.write(WebDAV.formatCreationDate(typeConv.convert(Date.class, davValue))); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_CREATION_DATE, WebDAV.XML_NS_CREATION_DATE); + + // Get the modifed date/time + + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_GET_LAST_MODIFIED); + + // Output the last modified date of the node + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_LAST_MODIFIED, WebDAV.XML_NS_GET_LAST_MODIFIED, nullAttr); + if (davValue != null) + xml.write(WebDAV.formatModifiedDate(typeConv.convert(Date.class, davValue))); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_LAST_MODIFIED, WebDAV.XML_NS_GET_LAST_MODIFIED); + + // For a file node output the content language and content type + + if (isDir == false) + { + // Get the content language + + // TODO: + // Output the content language + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LANGUAGE, WebDAV.XML_NS_GET_CONTENT_LANGUAGE, + nullAttr); + // TODO: + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LANGUAGE, WebDAV.XML_NS_GET_CONTENT_LANGUAGE); + + // Get the content type + davValue = WebDAV.getDAVPropertyValue(props, WebDAV.XML_GET_CONTENT_TYPE); + + // Output the content type + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_TYPE, WebDAV.XML_NS_GET_CONTENT_TYPE, nullAttr); + if (davValue != null) + xml.write(typeConv.convert(String.class, davValue)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_TYPE, WebDAV.XML_NS_GET_CONTENT_TYPE); + + // Output the etag + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_ETAG, WebDAV.XML_NS_GET_ETAG, nullAttr); + xml.write(getDAVHelper().makeETag(nodeInfo)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_ETAG, WebDAV.XML_NS_GET_ETAG); + } + + // Get the content length, if it's not a folder + + long len = 0; + + if (isDir == false) + { + ContentData contentData = (ContentData) props.get(ContentModel.PROP_CONTENT); + if (contentData != null) + len = contentData.getSize(); + } + + // Output the content length + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LENGTH, WebDAV.XML_NS_GET_CONTENT_LENGTH, nullAttr); + xml.write("" + len); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_GET_CONTENT_LENGTH, WebDAV.XML_NS_GET_CONTENT_LENGTH); + + // Print out all the custom properties + + SessionUser davUser = (SessionUser) m_request.getSession().getAttribute( AuthenticationFilter.AUTHENTICATION_USER); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_ALF_AUTHTICKET, WebDAV.XML_NS_ALF_AUTHTICKET, nullAttr); + if ( davUser != null) + xml.write( davUser.getTicket()); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_ALF_AUTHTICKET, WebDAV.XML_NS_ALF_AUTHTICKET); + + // Close off the response + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS, nullAttr); + xml.write(WebDAV.HTTP1_1 + " " + HttpServletResponse.SC_OK + " " + WebDAV.SC_OK_DESC); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT); + } + + /** + * Generates the XML response for a PROPFIND request that asks for a list of + * all known properties + * + * @param xml XMLWriter + * @param nodeInfo FileInfo + * @param isDir boolean + */ + protected void generateFindPropertiesResponse(XMLWriter xml, FileInfo nodeInfo, boolean isDir) + { + try + { + // Output the start of the properties element + + Attributes nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT, nullAttr); + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP, nullAttr); + + // Output the well-known properties + + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_LOCK_DISCOVERY)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_SUPPORTED_LOCK)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_RESOURCE_TYPE)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_DISPLAYNAME)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_GET_LAST_MODIFIED)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_GET_CONTENT_LENGTH)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_CREATION_DATE)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_GET_ETAG)); + + if (isDir) + { + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_GET_CONTENT_LANGUAGE)); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_GET_CONTENT_TYPE)); + } + + // Output the custom properties + + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_ALF_AUTHTICKET)); + + // Close off the response + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS, nullAttr); + xml.write(WebDAV.HTTP1_1 + " " + HttpServletResponse.SC_OK + " " + WebDAV.SC_OK_DESC); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT); + } + catch (Exception ex) + { + // Convert to a runtime exception + + throw new AlfrescoRuntimeException("XML processing error", ex); + } + } + + /** + * Generates the XML response snippet showing the lock information for the + * given path + * + * @param xml XMLWriter + * @param nodeInfo FileInfo + * @param isDir boolean + */ + protected void generateLockDiscoveryResponse(XMLWriter xml, FileInfo nodeInfo, boolean isDir) throws Exception + { + // Output the lock status response + LockInfo lockInfo = getNodeLockInfo(nodeInfo); + if (lockInfo.isLocked() && !lockInfo.isExpired()) + { + generateLockDiscoveryXML(xml, nodeInfo, lockInfo); + } + } + + /** + * Output the supported lock types XML element + * + * @param xml XMLWriter + */ + protected void writeLockTypes(XMLWriter xml) + { + try + { + AttributesImpl nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_SUPPORTED_LOCK, WebDAV.XML_NS_SUPPORTED_LOCK, nullAttr); + + // Output exclusive lock + // Shared locks are not supported, as they cannot be supported by the LockService (relevant to ALF-16449). + writeLock(xml, WebDAV.XML_NS_EXCLUSIVE); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_SUPPORTED_LOCK, WebDAV.XML_NS_SUPPORTED_LOCK); + } + catch (Exception ex) + { + throw new AlfrescoRuntimeException("XML write error", ex); + } + } + + /** + * Loads all dead properties persisted on the node + * + * @param nodeRef NodeRef + * @return the map of all dead properties + */ + @SuppressWarnings("unchecked") + protected Map loadDeadProperties(NodeRef nodeRef) + { + Map result; + + List deadProperties = (List)getNodeService().getProperty(nodeRef, ContentModel.PROP_DEAD_PROPERTIES); + + if (deadProperties != null) + { + result = new HashMap(deadProperties.size() * 2); + + for (String deadProperty : deadProperties) + { + int last = deadProperty.length() - 1; + int pos = deadProperty.indexOf(QName.NAMESPACE_END); + if (pos == -1 || pos == last) + { + continue; + } + pos = deadProperty.indexOf(':', pos + 1); + if (pos == -1 || pos == last) + { + continue; + } + try + { + result.put(QName.createQName(deadProperty.substring(0, pos)), deadProperty.substring(pos + 1)); + } + catch (InvalidQNameException e) + { + // Skip and continue + } + } + } + else + { + result = new HashMap(7); + } + + return result; + } + + /** + * Persists dead properties for specified resource + * + * @param nodeRef specified resource + * @param deadProperties the properties to persist + */ + protected void persistDeadProperties(NodeRef nodeRef, Map deadProperties) + { + List listToPersist = new ArrayList(deadProperties.size()); + + for (Map.Entry entry: deadProperties.entrySet()) + { + listToPersist.add(entry.getKey().toString() + ':' + entry.getValue()); + } + + getNodeService().setProperty(nodeRef, ContentModel.PROP_DEAD_PROPERTIES, (Serializable)listToPersist); + } + + /** + * Output the lockentry element of the specified type + * @param xml XMLWriter + * @param lockType lock type containing namespace. Can be WebDAV.XML_NS_EXCLUSIVE or WebDAV.XML_NS_SHARED + * @throws SAXException + * @throws IOException + */ + private void writeLock(XMLWriter xml, String lockType) throws SAXException, IOException + { + AttributesImpl nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_LOCK_ENTRY, WebDAV.XML_NS_LOCK_ENTRY, nullAttr); + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_LOCK_SCOPE, WebDAV.XML_NS_LOCK_SCOPE, nullAttr); + xml.write(DocumentHelper.createElement(lockType)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_LOCK_SCOPE, WebDAV.XML_NS_LOCK_SCOPE); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_LOCK_TYPE, WebDAV.XML_NS_LOCK_TYPE, nullAttr); + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_WRITE)); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_LOCK_TYPE, WebDAV.XML_NS_LOCK_TYPE); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_LOCK_ENTRY, WebDAV.XML_NS_LOCK_ENTRY); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/PropPatchMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/PropPatchMethod.java new file mode 100644 index 00000000000..cf66c05437c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/PropPatchMethod.java @@ -0,0 +1,484 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.util.ArrayList; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.namespace.QName; +import org.dom4j.DocumentHelper; +import org.dom4j.io.XMLWriter; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.Attributes; + +/** + * Implements the WebDAV PROPPATCH method + * + * @author Ivan Rybnikov + */ +public class PropPatchMethod extends PropFindMethod +{ + // Properties to patch + protected ArrayList m_propertyActions = null; + private String strHRef; + private WebDAVProperty failedProperty; + private String basePath; + + /** + * @return Returns false always + */ + @Override + protected boolean isReadOnly() + { + return false; + } + + @Override + protected void executeImpl() throws WebDAVServerException, Exception + { + FileInfo pathNodeInfo = null; + try + { + // Check that the path exists + pathNodeInfo = getNodeForPath(getRootNodeRef(), m_strPath); + } + catch (FileNotFoundException e) + { + // The path is not valid - send a 404 error back to the client + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + checkNode(pathNodeInfo); + + // Create the path for the current location in the tree + StringBuilder baseBuild = new StringBuilder(256); + baseBuild.append(getPath()); + if (baseBuild.length() == 0 || baseBuild.charAt(baseBuild.length() - 1) != WebDAVHelper.PathSeperatorChar) + { + baseBuild.append(WebDAVHelper.PathSeperatorChar); + } + basePath = baseBuild.toString(); + + // Build the href string for the current node + boolean isFolder = pathNodeInfo.isFolder(); + strHRef = getURLForPath(m_request, basePath, isFolder); + + // Do the real work: patch the properties + patchProperties(pathNodeInfo, basePath); + } + + + @Override + protected void generateResponseImpl() throws Exception + { + m_response.setStatus(WebDAV.WEBDAV_SC_MULTI_STATUS); + + // Set the response content type + m_response.setContentType(WebDAV.XML_CONTENT_TYPE); + + // Create multistatus response + XMLWriter xml = createXMLWriter(); + + xml.startDocument(); + + String nsdec = generateNamespaceDeclarations(m_namespaces); + xml.startElement( + WebDAV.DAV_NS, + WebDAV.XML_MULTI_STATUS + nsdec, + WebDAV.XML_NS_MULTI_STATUS + nsdec, + getDAVHelper().getNullAttributes()); + + // Output the response block for the current node + xml.startElement( + WebDAV.DAV_NS, + WebDAV.XML_RESPONSE, + WebDAV.XML_NS_RESPONSE, + getDAVHelper().getNullAttributes()); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_HREF, WebDAV.XML_NS_HREF, getDAVHelper().getNullAttributes()); + xml.write(strHRef); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_HREF, WebDAV.XML_NS_HREF); + + if (failedProperty != null) + { + generateError(xml); + } + + for (PropertyAction propertyAction : m_propertyActions) + { + WebDAVProperty property = propertyAction.getProperty(); + int statusCode = propertyAction.getStatusCode(); + String statusCodeDescription = propertyAction.getStatusCodeDescription(); + generatePropertyResponse(xml, property, statusCode, statusCodeDescription); + } + + // Close off the response element + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_RESPONSE, WebDAV.XML_NS_RESPONSE); + + // Close the outer XML element + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_MULTI_STATUS, WebDAV.XML_NS_MULTI_STATUS); + + // Send remaining data + flushXML(xml); + } + + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + @Override + protected void parseRequestBody() throws WebDAVServerException + { + Document body = getRequestBodyAsDocument(); + if (body != null) + { + Element rootElement = body.getDocumentElement(); + NodeList childList = rootElement.getChildNodes(); + + m_propertyActions = new ArrayList(); + + for (int i = 0; i < childList.getLength(); i++) + { + Node currentNode = childList.item(i); + switch (currentNode.getNodeType()) + { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + if (currentNode.getNodeName().endsWith(WebDAV.XML_SET) || currentNode.getNodeName().endsWith(WebDAV.XML_REMOVE)) + { + NodeList propertiesList = currentNode.getChildNodes(); + + for (int j = 0; j < propertiesList.getLength(); j++) + { + Node propertiesNode = propertiesList.item(j); + switch (propertiesNode.getNodeType()) + { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + if (propertiesNode.getNodeName().endsWith(WebDAV.XML_PROP)) + { + NodeList propList = propertiesNode.getChildNodes(); + + for (int k = 0; k < propList.getLength(); k++) + { + Node propNode = propList.item(k); + switch (propNode.getNodeType()) + { + case Node.TEXT_NODE: + break; + case Node.ELEMENT_NODE: + int action = currentNode.getNodeName().endsWith(WebDAV.XML_SET) ? PropertyAction.SET : PropertyAction.REMOVE; + m_propertyActions.add(new PropertyAction(action, createProperty(propNode))); + break; + } + } + } + break; + } + } + } + break; + } + } + + } + + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + @Override + protected void parseRequestHeaders() throws WebDAVServerException + { + // Parse Lock tokens and ETags, if any + parseIfHeader(); + } + + /** + * Creates a WebDAVProperty from the given XML node + */ + protected WebDAVProperty createProperty(Node node) + { + WebDAVProperty property = super.createProperty(node); + + String strValue = null; + Node value = node.getFirstChild(); + if (value != null) + { + strValue = value.getNodeValue(); + } + property.setValue(strValue); + + return property; + } + + + protected void patchProperties(FileInfo nodeInfo, String path) throws WebDAVServerException + { + failedProperty = null; + for (PropertyAction action : m_propertyActions) + { + if (action.getProperty().isProtected()) + { + failedProperty = action.getProperty(); + break; + } + } + + Map deadProperties = null; + for (PropertyAction propertyAction : m_propertyActions) + { + int statusCode; + String statusCodeDescription; + WebDAVProperty property = propertyAction.getProperty(); + + if (failedProperty == null) + { + PropertyDefinition propDef = getDAVHelper().getDictionaryService().getProperty(property.createQName()); + + boolean deadProperty = propDef == null || (!propDef.getContainerClass().isAspect() && !getDAVHelper().getDictionaryService().isSubClass(getNodeService().getType(nodeInfo.getNodeRef()), + propDef.getContainerClass().getName())); + + if (deadProperty && deadProperties == null) + { + deadProperties = loadDeadProperties(nodeInfo.getNodeRef()); + } + + if (PropertyAction.SET == propertyAction.getAction()) + { + if (deadProperty) + { + deadProperties.put(property.createQName(), property.getValue()); + } + else + { + getNodeService().setProperty(nodeInfo.getNodeRef(), property.createQName(), property.getValue()); + } + } + else if (PropertyAction.REMOVE == propertyAction.getAction()) + { + if (deadProperty) + { + deadProperties.remove(property.createQName()); + } + else + { + getNodeService().removeProperty(nodeInfo.getNodeRef(), property.createQName()); + } + } + else + { + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST); + } + statusCode = HttpServletResponse.SC_OK; + statusCodeDescription = WebDAV.SC_OK_DESC; + } + else if (failedProperty == property) + { + statusCode = HttpServletResponse.SC_FORBIDDEN; + statusCodeDescription = WebDAV.SC_FORBIDDEN_DESC; + } + else + { + statusCode = WebDAV.WEBDAV_SC_FAILED_DEPENDENCY; + statusCodeDescription = WebDAV.WEBDAV_SC_FAILED_DEPENDENCY_DESC; + } + + propertyAction.setResult(statusCode, statusCodeDescription); + } + if (deadProperties != null) + { + persistDeadProperties(nodeInfo.getNodeRef(), deadProperties); + } + } + + + /** + * Generates the XML response for a PROPFIND request that asks for a list of + * all known properties + * + * @param xml XMLWriter + * @param property WebDAVProperty + * @param status int + * @param description String + */ + protected void generatePropertyResponse(XMLWriter xml, WebDAVProperty property, int status, String description) + { + try + { + // Output the start of the properties element + Attributes nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT, nullAttr); + + // Output property name + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP, nullAttr); + if (property.hasNamespaceName()) + { + xml.write(DocumentHelper.createElement(property.getNamespaceName() + WebDAV.NAMESPACE_SEPARATOR + property.getName())); + } + else + { + xml.write(DocumentHelper.createElement(property.getName())); + } + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROP, WebDAV.XML_NS_PROP); + + // Output action result status + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS, nullAttr); + xml.write(WebDAV.HTTP1_1 + " " + status + " " + description); + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_STATUS, WebDAV.XML_NS_STATUS); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_PROPSTAT, WebDAV.XML_NS_PROPSTAT); + } + catch (Exception ex) + { + // Convert to a runtime exception + throw new AlfrescoRuntimeException("XML processing error", ex); + } + } + + + /** + * Generates the error tag + * + * @param xml XMLWriter + */ + protected void generateError(XMLWriter xml) + { + try + { + // Output the start of the error element + Attributes nullAttr = getDAVHelper().getNullAttributes(); + + xml.startElement(WebDAV.DAV_NS, WebDAV.XML_ERROR, WebDAV.XML_NS_ERROR, nullAttr); + // Output error + xml.write(DocumentHelper.createElement(WebDAV.XML_NS_CANNOT_MODIFY_PROTECTED_PROPERTY)); + + xml.endElement(WebDAV.DAV_NS, WebDAV.XML_ERROR, WebDAV.XML_NS_ERROR); + } + catch (Exception ex) + { + // Convert to a runtime exception + throw new AlfrescoRuntimeException("XML processing error", ex); + } + } + + + /** + * Stores information about PROPPATCH action(set or remove) an according property. + * + * @author Ivan Rybnikov + */ + protected class PropertyAction + { + public static final int SET = 0; + public static final int REMOVE = 1; + + // Property on which action should be performed + private WebDAVProperty property; + + // Action + private int action; + + private int statusCode; + + private String statusCodeDescription; + + /** + * Constructor + * + * @param action int + * @param property WebDAVProperty + */ + public PropertyAction(int action, WebDAVProperty property) + { + this.action = action; + this.property = property; + } + + public void setResult(int statusCode, String statusCodeDescription) + { + this.statusCode = statusCode; + this.statusCodeDescription = statusCodeDescription; + } + + public int getStatusCode() + { + return this.statusCode; + } + + public String getStatusCodeDescription() + { + return this.statusCodeDescription; + } + + public int getAction() + { + return action; + } + + public WebDAVProperty getProperty() + { + return property; + } + + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("["); + str.append("action="); + str.append(getAction() == 0 ? "SET" : "REMOVE"); + str.append(",property="); + str.append(getProperty()); + str.append(",statusCode="); + str.append(getStatusCode()); + str.append(",statusCodeDescription="); + str.append(getStatusCodeDescription()); + str.append("]"); + + return str.toString(); + } + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/PutMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/PutMethod.java new file mode 100644 index 00000000000..c1a75c37906 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/PutMethod.java @@ -0,0 +1,430 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.InputStream; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.executer.ContentMetadataExtracter; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.webdav.WebDavService; +import org.springframework.dao.ConcurrencyFailureException; + +/** + * Implements the WebDAV PUT method + * + * @author Gavin Cornwell + */ +public class PutMethod extends WebDAVMethod implements ActivityPostProducer +{ + // Request parameters + private String m_strContentType = null; + private boolean m_expectHeaderPresent = false; + // Indicates if a zero byte node was created by a LOCK call. + // Try to delete the node if the PUT fails + private boolean noContent = false; + private boolean created = false; + private WebDAVActivityPoster activityPoster; + private FileInfo contentNodeInfo; + private long fileSize; + + /** + * Default constructor + */ + public PutMethod() + { + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + m_strContentType = m_request.getHeader(WebDAV.HEADER_CONTENT_TYPE); + String strExpect = m_request.getHeader(WebDAV.HEADER_EXPECT); + + if (strExpect != null && strExpect.equals(WebDAV.HEADER_EXPECT_CONTENT)) + { + m_expectHeaderPresent = true; + } + + // Parse Lock tokens and ETags, if any + + parseIfHeader(); + } + + /** + * Clears the aspect added by a LOCK request for a new file, so + * that the Timer started by the LOCK request will not remove the + * node now that the PUT request has been received. This is needed + * for large content. + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // Nothing is done with the body by this method. The body contains + // the content it will be dealt with later. + + // This method is called ONCE just before the FIRST call to executeImpl, + // which is in a retrying transaction so may be called many times. + + // Although this method is called just before the first executeImpl, + // it is possible that the Thread could be interrupted before the first call + // or between calls. However the chances are low and the consequence + // (leaving a zero byte file) is minor. + + noContent = getTransactionService().getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + public Boolean execute() throws Throwable + { + FileInfo contentNodeInfo = null; + try + { + contentNodeInfo = getNodeForPath(getRootNodeRef(), getPath()); + checkNode(contentNodeInfo); + final NodeRef nodeRef = contentNodeInfo.getNodeRef(); + if (getNodeService().hasAspect(contentNodeInfo.getNodeRef(), ContentModel.ASPECT_WEBDAV_NO_CONTENT)) + { + getNodeService().removeAspect(nodeRef, ContentModel.ASPECT_WEBDAV_NO_CONTENT); + if (logger.isDebugEnabled()) + { + String path = getPath(); + logger.debug("Put Timer DISABLE " + path); + } + return Boolean.TRUE; + } + } + catch (FileNotFoundException e) + { + // Does not exist, so there will be no aspect. + } + return Boolean.FALSE; + } + }, false, true); + } + + /** + * Execute the WebDAV request + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException, Exception + { + if (logger.isDebugEnabled()) + { + String path = getPath(); + String userName = getDAVHelper().getAuthenticationService().getCurrentUserName(); + logger.debug("Put node: \n" + + " user: " + userName + "\n" + + " path: " + path + "\n" + + "noContent: " + noContent); + } + + FileFolderService fileFolderService = getFileFolderService(); + + // Get the status for the request path + LockInfo nodeLockInfo = null; + try + { + contentNodeInfo = getNodeForPath(getRootNodeRef(), getPath()); + // make sure that we are not trying to use a folder + if (contentNodeInfo.isFolder()) + { + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST); + } + + nodeLockInfo = checkNode(contentNodeInfo); + + // 'Unhide' nodes hidden by us and behave as though we created them + NodeRef contentNodeRef = contentNodeInfo.getNodeRef(); + if (fileFolderService.isHidden(contentNodeRef) && !getDAVHelper().isRenameShuffle(getPath())) + { + fileFolderService.setHidden(contentNodeRef, false); + created = true; + } + } + catch (FileNotFoundException e) + { + // the file doesn't exist - create it + String[] paths = getDAVHelper().splitPath(getPath()); + try + { + FileInfo parentNodeInfo = getNodeForPath(getRootNodeRef(), paths[0]); + // create file + contentNodeInfo = getDAVHelper().createFile(parentNodeInfo, paths[1]); + created = true; + + } + catch (FileNotFoundException ee) + { + // bad path + throw new WebDAVServerException(HttpServletResponse.SC_CONFLICT); + } + catch (FileExistsException ee) + { + // ALF-7079 fix, retry: it looks like concurrent access (file not found but file exists) + throw new ConcurrencyFailureException("Concurrent access was detected.", ee); + } + } + + String userName = getDAVHelper().getAuthenticationService().getCurrentUserName(); + LockInfo lockInfo = getDAVLockService().getLockInfo(contentNodeInfo.getNodeRef()); + + if (lockInfo != null) + { + if (lockInfo.isLocked() && !lockInfo.getOwner().equals(userName)) + { + if (logger.isDebugEnabled()) + { + String path = getPath(); + String owner = lockInfo.getOwner(); + logger.debug("Node locked: path=["+path+"], owner=["+owner+"], current user=["+userName+"]"); + } + // Indicate that the resource is locked + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + } + // ALF-16808: We disable the versionable aspect if we are overwriting + // empty content because it's probably part of a compound operation to + // create a new single version + boolean disabledVersioning = false; + + try + { + // Disable versioning if we are overwriting an empty file with content + NodeRef nodeRef = contentNodeInfo.getNodeRef(); + ContentData contentData = (ContentData)getNodeService().getProperty(nodeRef, ContentModel.PROP_CONTENT); + if ((contentData == null || contentData.getSize() == 0) && getNodeService().hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE)) + { + getDAVHelper().getPolicyBehaviourFilter().disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + disabledVersioning = true; + } + // Access the content + ContentWriter writer = fileFolderService.getWriter(contentNodeInfo.getNodeRef()); + + // set content properties + writer.guessMimetype(contentNodeInfo.getName()); + writer.guessEncoding(); + + // Get the input stream from the request data + InputStream is = m_request.getInputStream(); + + + // Write the new data to the content node + writer.putContent(is); + + // ALF-16756: To avoid firing inbound rules too early (while a node is still locked) apply the no content aspect + // Note, for MNT-15801, that the aspect is only applied if: + // - the node is locked AND + // - the node does not have any content (zero length binaries included) + if (nodeLockInfo != null && nodeLockInfo.isExclusive() && !(ContentData.hasContent(contentData) && contentData.getSize() > 0)) + { + getNodeService().addAspect(contentNodeInfo.getNodeRef(), ContentModel.ASPECT_NO_CONTENT, null); + } + + // Ask for the document metadata to be extracted + Action extract = getActionService().createAction(ContentMetadataExtracter.EXECUTOR_NAME); + if(extract != null) + { + extract.setExecuteAsynchronously(false); + getActionService().executeAction(extract, contentNodeInfo.getNodeRef()); + } + + // If the mime-type determined by the repository is different + // from the original specified in the request, update it. + if (m_strContentType == null || !m_strContentType.equals(writer.getMimetype())) + { + String oldMimeType = m_strContentType; + m_strContentType = writer.getMimetype(); + if (logger.isDebugEnabled()) + { + logger.debug("Mimetype originally specified as " + oldMimeType + + ", now guessed to be " + m_strContentType); + } + } + + // Record the uploaded file's size + fileSize = writer.getSize(); + + // Set the response status, depending if the node existed or not + m_response.setStatus(created ? HttpServletResponse.SC_CREATED : HttpServletResponse.SC_NO_CONTENT); + } + catch (AccessDeniedException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_FORBIDDEN, e); + } + catch (Throwable e) + { + // check if the node was marked with noContent aspect previously by lock method AND + // we are about to give up + if (noContent && RetryingTransactionHelper.extractRetryCause(e) == null) + { + // remove the 0 bytes content if save operation failed or was cancelled + final NodeRef nodeRef = contentNodeInfo.getNodeRef(); + getTransactionService().getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + public String execute() throws Throwable + { + getNodeService().deleteNode(nodeRef); + if (logger.isDebugEnabled()) + { + logger.debug("Put failed. DELETE " + getPath()); + } + return null; + } + }, false, false); + } + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + finally + { + if (disabledVersioning) + { + getDAVHelper().getPolicyBehaviourFilter().enableBehaviour(contentNodeInfo.getNodeRef(), ContentModel.ASPECT_VERSIONABLE); + } + } + + postActivity(); + + } + + /** + * Can be used after a successful {@link #execute()} invocation to + * check whether the resource was new (created) or over-writing existing + * content. + * + * @return true if the content was newly created, false if existing. + */ + protected boolean isCreated() + { + return created; + } + + /** + * Retrieve the mimetype of the content sent for the PUT request. The initial + * value specified in the request may be updated after the file contents have + * been uploaded if the repository has determined a different mimetype for the content. + * + * @return content-type + */ + public String getContentType() + { + return m_strContentType; + } + + /** + * The FileInfo for the uploaded file, or null if not yet uploaded. + * + * @return FileInfo + */ + public FileInfo getContentNodeInfo() + { + return contentNodeInfo; + } + + /** + * Returns the size of the uploaded file, zero if not yet uploaded. + * + * @return the fileSize + */ + public long getFileSize() + { + return fileSize; + } + + /** + * Create an activity post. + * + * @throws WebDAVServerException + */ + protected void postActivity() throws WebDAVServerException + { + WebDavService davService = getDAVHelper().getServiceRegistry().getWebDavService(); + if (!davService.activitiesEnabled()) + { + // Don't post activities if this behaviour is disabled. + return; + } + + String path = getPath(); + String siteId = getSiteId(); + String tenantDomain = getTenantDomain(); + + if (siteId.equals(WebDAVHelper.EMPTY_SITE_ID)) + { + // There is not enough information to publish site activity. + return; + } + + FileInfo contentNodeInfo = null; + try + { + contentNodeInfo = getNodeForPath(getRootNodeRef(), path); + NodeRef nodeRef = contentNodeInfo.getNodeRef(); + // Don't post activity data for hidden files, resource forks etc. + if (!getNodeService().hasAspect(nodeRef, ContentModel.ASPECT_HIDDEN)) + { + if (isCreated()) + { + // file added + activityPoster.postFileFolderAdded(siteId, tenantDomain, null, contentNodeInfo); + } + else + { + // file updated + activityPoster.postFileFolderUpdated(siteId, tenantDomain, contentNodeInfo); + } + } + } + catch (FileNotFoundException error) + { + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + @Override + public void setActivityPoster(WebDAVActivityPoster activityPoster) + { + this.activityPoster = activityPoster; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/UnlockMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/UnlockMethod.java new file mode 100644 index 00000000000..837afdfeac2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/UnlockMethod.java @@ -0,0 +1,284 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.lock.UnableToReleaseLockException; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleType; + +/** + * Implements the WebDAV UNLOCK method + * + * @author gavinc + */ +public class UnlockMethod extends WebDAVMethod +{ + private String m_strLockToken = null; + + /** + * Default constructor + */ + public UnlockMethod() + { + } + + /** + * Return the lock token of an existing lock + * + * @return String + */ + protected final String getLockToken() + { + return m_strLockToken; + } + + /** + * Parse the request headers + * + * @exception WebDAVServerException + */ + protected void parseRequestHeaders() throws WebDAVServerException + { + // Get the lock token, if any + String strLockTokenHeader = m_request.getHeader(WebDAV.HEADER_LOCK_TOKEN); + + // DEBUG + if (logger.isDebugEnabled()) + logger.debug("Parsing Lock-Token header: " + strLockTokenHeader); + + // Validate the lock token + if (strLockTokenHeader != null) + { + if (!(strLockTokenHeader.startsWith("<") && strLockTokenHeader.endsWith(">"))) + { + // ALF-13904: Header isn't correctly enclosed in < and > characters. Try correcting this + // to allow for Windows 7 + OpenOffice.org bug. + strLockTokenHeader = "<" + strLockTokenHeader + ">"; + } + if (strLockTokenHeader.startsWith("<" + WebDAV.OPAQUE_LOCK_TOKEN) && strLockTokenHeader.endsWith(">")) + { + try + { + m_strLockToken = strLockTokenHeader.substring( + WebDAV.OPAQUE_LOCK_TOKEN.length() + 1, + strLockTokenHeader.length() - 1); + } + catch (IndexOutOfBoundsException e) + { + logger.warn("Failed to parse If header: " + strLockTokenHeader); + } + } + } + // If there is no token this is a bad request so send an error back + if (m_strLockToken == null) + { + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST); + } + } + + /** + * Parse the request body + * + * @exception WebDAVServerException + */ + protected void parseRequestBody() throws WebDAVServerException + { + // Nothing to do in this method + } + + /** + * Execute the request + * + * @exception WebDAVServerException + */ + protected void executeImpl() throws WebDAVServerException + { + RuleService ruleService = getServiceRegistry().getRuleService(); + try + { + // Temporarily disable update rules. + ruleService.disableRuleType(RuleType.UPDATE); + attemptUnlock(); + } + finally + { + // Re-instate update rules. + ruleService.enableRuleType(RuleType.UPDATE); + } + } + + /** + * The main unlock implementation. + * + * @throws WebDAVServerException + */ + protected void attemptUnlock() throws WebDAVServerException + { + if (logger.isDebugEnabled()) + { + logger.debug("Unlock node; path=" + getPath() + ", token=" + getLockToken()); + } + + FileInfo lockNodeInfo = null; + try + { + lockNodeInfo = getNodeForPath(getRootNodeRef(), getPath()); + } + catch (FileNotFoundException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_NOT_FOUND); + } + + // Parse the lock token + String[] lockInfoFromRequest = WebDAV.parseLockToken(getLockToken()); + if (lockInfoFromRequest == null) + { + // Bad lock token + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + + NodeRef nodeRef = lockNodeInfo.getNodeRef(); + LockInfo lockInfo = getDAVLockService().getLockInfo(nodeRef); + + if (lockInfo == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unlock token=" + getLockToken() + " Not locked - no info in lock store."); + } + // Node is not locked + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + + + if (!lockInfo.isLocked()) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unlock token=" + getLockToken() + " Not locked"); + } + // Node is not locked + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + else if (lockInfo.isExpired()) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unlock token=" + getLockToken() + " Lock expired"); + } + // Return a success status + m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); + removeNoContentAspect(nodeRef); + } + else if (lockInfo.isExclusive()) + { + String currentUser = getAuthenticationService().getCurrentUserName(); + if (currentUser.equals(lockInfo.getOwner())) + { + try + { + getDAVLockService().unlock(nodeRef); + } + catch (UnableToReleaseLockException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED, e); + } + + // Indicate that the unlock was successful + m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); + removeNoContentAspect(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug("Unlock token=" + getLockToken() + " Successful"); + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Unlock token=" + getLockToken() + " Not lock owner"); + } + // Node is not locked + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + } + else if (lockInfo.isShared()) + { + Set sharedLocks = lockInfo.getSharedLockTokens(); + if (sharedLocks.contains(m_strLockToken)) + { + sharedLocks.remove(m_strLockToken); + + // Indicate that the unlock was successful + m_response.setStatus(HttpServletResponse.SC_NO_CONTENT); + removeNoContentAspect(nodeRef); + + // DEBUG + if (logger.isDebugEnabled()) + { + logger.debug("Unlock token=" + getLockToken() + " Successful"); + } + } + } + else + { + throw new IllegalStateException("Invalid LockInfo state: " + lockInfo); + } + } + + // This method removes a new zero byte node that has been locked, where the + // PUT has not taken place but the client has issued an UNLOCK. The Timer + // started in the LOCK will delete the node, but this is faster. + // I have seen it with MS Office 2003 Excel. Almost impossible to reproduce. + // Think Excel responds to a 'kill' request between the LOCK and PUT requests + // and tries to tidy down as it exits. + private void removeNoContentAspect(NodeRef nodeRef) + { + if (getNodeService().hasAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT)) + { + getNodeService().removeAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT); + } + if (getNodeService().hasAspect(nodeRef, ContentModel.ASPECT_WEBDAV_NO_CONTENT)) + { + getNodeService().removeAspect(nodeRef, ContentModel.ASPECT_WEBDAV_NO_CONTENT); + getNodeService().deleteNode(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug("Unlock Timer DISABLE and DELETE " + getPath()); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAV.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAV.java new file mode 100644 index 00000000000..878400f8d0c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAV.java @@ -0,0 +1,527 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.Serializable; +import java.util.Date; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.TimeZone; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO9075; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Helper class used by the WebDAV protocol handling classes + * + * @author gavinc + */ +public class WebDAV +{ + // Logging + + private static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol"); + + // WebDAV XML namespace + + public static final String DAV_NS = "D"; + public static final String DAV_NS_PREFIX = DAV_NS + ":"; + + // PROPFIND, LOCK depth + + public static final int DEPTH_0 = 0; + public static final int DEPTH_1 = 1; + public static final int DEPTH_INFINITY = -1; + public static final short TIMEOUT_INFINITY = -1; + public static final int TIMEOUT_24_HOURS = 86400; + + // WebDAV HTTP response codes + + public static final int WEBDAV_SC_MULTI_STATUS = 207; + public static final int WEBDAV_SC_LOCKED = 423; + public static final int WEBDAV_SC_FAILED_DEPENDENCY = 424; + + // WebDAV HTTP response code descriptions + + public static final String WEBDAV_SC_FAILED_DEPENDENCY_DESC = "Failed Dependency"; + + // HTTP response code descriptions + + public static final String SC_OK_DESC = "OK"; + public static final String SC_NOT_FOUND_DESC = "Not Found"; + public static final String SC_FORBIDDEN_DESC = "Forbidden"; + + + // HTTP methods + + public static final String METHOD_PUT = "PUT"; + public static final String METHOD_POST = "POST"; + public static final String METHOD_GET = "GET"; + public static final String METHOD_DELETE = "DELETE"; + public static final String METHOD_HEAD = "HEAD"; + public static final String METHOD_OPTIONS = "OPTIONS"; + public static final String METHOD_PROPFIND = "PROPFIND"; + public static final String METHOD_PROPPATCH = "PROPPATCH"; + public static final String METHOD_MKCOL = "MKCOL"; + public static final String METHOD_MOVE = "MOVE"; + public static final String METHOD_COPY = "COPY"; + public static final String METHOD_LOCK = "LOCK"; + public static final String METHOD_UNLOCK = "UNLOCK"; + + // HTTP headers + + public static final String HEADER_CONTENT_LENGTH = "Content-Length"; + public static final String HEADER_CONTENT_TYPE = "Content-Type"; + public static final String HEADER_DEPTH = "Depth"; + public static final String HEADER_DESTINATION = "Destination"; + public static final String HEADER_ETAG = "ETag"; + public static final String HEADER_EXPECT = "Expect"; + public static final String HEADER_EXPECT_CONTENT = "100-continue"; + public static final String HEADER_IF = "If"; + public static final String HEADER_IF_MATCH = "If-Match"; + public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + public static final String HEADER_IF_RANGE = "If-Range"; + public static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + public static final String HEADER_LAST_MODIFIED = "Last-Modified"; + public static final String HEADER_LOCK_TOKEN = "Lock-Token"; + public static final String HEADER_OVERWRITE = "Overwrite"; + public static final String HEADER_RANGE = "Range"; + public static final String HEADER_TIMEOUT = "Timeout"; + public static final String HEADER_USER_AGENT = "User-Agent"; + + // If-Modified/If-Unmodified date format + + public static final String HEADER_IF_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + // If header keyword + + public static final String HEADER_KEY_NOT = "Not"; + + public static final String AGENT_MICROSOFT_DATA_ACCESS_INTERNET_PUBLISHING_PROVIDER_DAV = "Microsoft Data Access Internet Publishing Provider DAV"; + public static final String AGENT_INTERNET_EXPLORER = "MSIE"; + // General string constants + + public static final String ASTERISK = "*"; + public static final String DEFAULT_NAMESPACE_URI = "DAV:"; + public static final String FAKE_TOKEN = "faketoken"; + public static final String HTTP1_1 = "HTTP/1.1"; + public static final String INFINITE = "Infinite"; + public static final String INFINITY = "infinity"; + public static final String OPAQUE_LOCK_TOKEN = "opaquelocktoken:"; + public static final String NAMESPACE_SEPARATOR = ":"; + public static final String SECOND = "Second-"; + public static final String HEADER_VALUE_SEPARATOR = ","; + public static final String ZERO = "0"; + public static final String ONE = "1"; + public static final String F = "F"; + + // Strings used in WebDAV XML payload + + public static final String XML_NS = "xmlns"; + + public static final String XML_ACTIVE_LOCK = "activelock"; + public static final String XML_ALLPROP = "allprop"; + public static final String XML_COLLECTION = "collection"; + public static final String XML_CREATION_DATE = "creationdate"; + public static final String XML_DEPTH = "depth"; + public static final String XML_DISPLAYNAME = "displayname"; + public static final String XML_EXCLUSIVE = "exclusive"; + public static final String XML_GET_CONTENT_LANGUAGE = "getcontentlanguage"; + public static final String XML_GET_CONTENT_LENGTH = "getcontentlength"; + public static final String XML_GET_CONTENT_TYPE = "getcontenttype"; + public static final String XML_GET_ETAG = "getetag"; + public static final String XML_GET_LAST_MODIFIED = "getlastmodified"; + public static final String XML_HREF = "href"; + public static final String XML_LOCK_DISCOVERY = "lockdiscovery"; + public static final String XML_LOCK_ENTRY = "lockentry"; + public static final String XML_LOCK_SCOPE = "lockscope"; + public static final String XML_LOCK_TOKEN = "locktoken"; + public static final String XML_LOCK_TYPE = "locktype"; + public static final String XML_MULTI_STATUS = "multistatus"; + public static final String XML_OWNER = "owner"; + public static final String XML_PROP = "prop"; + public static final String XML_PROPNAME = "propname"; + public static final String XML_PROPSTAT = "propstat"; + public static final String XML_RESOURCE_TYPE = "resourcetype"; + public static final String XML_RESPONSE = "response"; + public static final String XML_SHARED = "shared"; + public static final String XML_SOURCE = "source"; + public static final String XML_STATUS = "status"; + public static final String XML_SUPPORTED_LOCK = "supportedlock"; + public static final String XML_TIMEOUT = "timeout"; + public static final String XML_WRITE = "write"; + public static final String XML_SET = "set"; + public static final String XML_REMOVE = "remove"; + public static final String XML_ERROR = "error"; + + // Namespaced versions of payload elements + + public static final String XML_NS_ACTIVE_LOCK = DAV_NS_PREFIX + "activelock"; + public static final String XML_NS_ALLPROP = DAV_NS_PREFIX + "allprop"; + public static final String XML_NS_COLLECTION = DAV_NS_PREFIX + "collection"; + public static final String XML_NS_CREATION_DATE = DAV_NS_PREFIX + "creationdate"; + public static final String XML_NS_DEPTH = DAV_NS_PREFIX + "depth"; + public static final String XML_NS_DISPLAYNAME = DAV_NS_PREFIX + "displayname"; + public static final String XML_NS_EXCLUSIVE = DAV_NS_PREFIX + "exclusive"; + public static final String XML_NS_GET_CONTENT_LANGUAGE = DAV_NS_PREFIX + "getcontentlanguage"; + public static final String XML_NS_GET_CONTENT_LENGTH = DAV_NS_PREFIX + "getcontentlength"; + public static final String XML_NS_GET_CONTENT_TYPE = DAV_NS_PREFIX + "getcontenttype"; + public static final String XML_NS_GET_ETAG = DAV_NS_PREFIX + "getetag"; + public static final String XML_NS_GET_LAST_MODIFIED = DAV_NS_PREFIX + "getlastmodified"; + public static final String XML_NS_HREF = DAV_NS_PREFIX + "href"; + public static final String XML_NS_LOCK_DISCOVERY = DAV_NS_PREFIX + "lockdiscovery"; + public static final String XML_NS_LOCK_ENTRY = DAV_NS_PREFIX + "lockentry"; + public static final String XML_NS_LOCK_SCOPE = DAV_NS_PREFIX + "lockscope"; + public static final String XML_NS_LOCK_TOKEN = DAV_NS_PREFIX + "locktoken"; + public static final String XML_NS_LOCK_TYPE = DAV_NS_PREFIX + "locktype"; + public static final String XML_NS_MULTI_STATUS = DAV_NS_PREFIX + "multistatus"; + public static final String XML_NS_OWNER = DAV_NS_PREFIX + "owner"; + public static final String XML_NS_PROP = DAV_NS_PREFIX + "prop"; + public static final String XML_NS_PROPNAME = DAV_NS_PREFIX + "propname"; + public static final String XML_NS_PROPSTAT = DAV_NS_PREFIX + "propstat"; + public static final String XML_NS_RESOURCE_TYPE = DAV_NS_PREFIX + "resourcetype"; + public static final String XML_NS_RESPONSE = DAV_NS_PREFIX + "response"; + public static final String XML_NS_SHARED = DAV_NS_PREFIX + "shared"; + public static final String XML_NS_SOURCE = DAV_NS_PREFIX + "source"; + public static final String XML_NS_STATUS = DAV_NS_PREFIX + "status"; + public static final String XML_NS_SUPPORTED_LOCK = DAV_NS_PREFIX + "supportedlock"; + public static final String XML_NS_TIMEOUT = DAV_NS_PREFIX + "timeout"; + public static final String XML_NS_WRITE = DAV_NS_PREFIX + "write"; + public static final String XML_NS_ERROR = DAV_NS_PREFIX + "error"; + public static final String XML_NS_CANNOT_MODIFY_PROTECTED_PROPERTY = DAV_NS_PREFIX + "cannot-modify-protected-property"; + + + public static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8"; + + // Alfresco specific properties + + public static final String XML_ALF_AUTHTICKET = "authticket"; + public static final String XML_NS_ALF_AUTHTICKET = DAV_NS_PREFIX + "authticket"; + + // Path seperator + + public static final String PathSeperator = "/"; + public static final char PathSeperatorChar = '/'; + + // Lock token seperator + + public static final String LOCK_TOKEN_SEPERATOR = ":"; + + // Root path + + public static final String RootPath = PathSeperator; + + // Map WebDAV property names to Alfresco property names + + private static Hashtable _propertyNameMap; + + // WebDAV creation date/time formatter + + private static String CREATION_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + /** + * Formats the given date so that it conforms with the Last-Modified HTTP header + * + * @param date The date to format + * @return The formatted date string + */ + public static String formatModifiedDate(Date date) + { + return formatHeaderDate(date); + } + + /** + * Formats the given date so that it conforms with the Last-Modified HTTP header + * + * @param ldate long + * @return The formatted date string + */ + public static String formatModifiedDate(long ldate) + { + return formatHeaderDate(ldate); + } + + /** + * Formats the given date so that it conforms with the WebDAV creation date/time format + * + * @param date The date to format + * @return The formatted date string + */ + public static String formatCreationDate(Date date) + { + return DateFormatUtils.formatUTC(date, CREATION_DATE_FORMAT); + } + + /** + * Formats the given date so that it conforms with the WebDAV creation date/time format + * + * @param ldate long + * @return The formatted date string + */ + public static String formatCreationDate(long ldate) + { + return DateFormatUtils.formatUTC(ldate, CREATION_DATE_FORMAT); + } + + /** + * Formats the given date for use in the HTTP header + * + * @param date Date + * @return String + */ + public static String formatHeaderDate(Date date) + { + // HTTP header date/time format + // NOTE: According to RFC2616 dates should always be in English and in + // the GMT timezone see http://rfc.net/rfc2616.html#p20 for details + return DateFormatUtils.format(date, HEADER_IF_DATE_FORMAT, TimeZone.getTimeZone("GMT"), Locale.ENGLISH); + } + + /** + * Formats the given date for use in the HTTP header + * + * @param ldate long + * @return String + */ + public static String formatHeaderDate(long ldate) + { + // HTTP header date/time format + // NOTE: According to RFC2616 dates should always be in English and in + // the GMT timezone see http://rfc.net/rfc2616.html#p20 for details + return DateFormatUtils.format(ldate, HEADER_IF_DATE_FORMAT, TimeZone.getTimeZone("GMT"), Locale.ENGLISH); + } + + /** + * Return the Alfresco property value for the specified WebDAV property + * + * @param davPropName String + * @return Object + */ + public static Object getDAVPropertyValue( Map props, String davPropName) + { + // Convert the WebDAV property name to the corresponding Alfresco property + + QName propName = _propertyNameMap.get( davPropName); + if ( propName == null) + throw new AlfrescoRuntimeException("No mapping for WebDAV property " + davPropName); + + // Return the property value + Object value = props.get(propName); + if (value instanceof ContentData) + { + ContentData contentData = (ContentData) value; + if (davPropName.equals(WebDAV.XML_GET_CONTENT_TYPE)) + { + value = contentData.getMimetype(); + } + else if (davPropName.equals(WebDAV.XML_GET_CONTENT_LENGTH)) + { + value = new Long(contentData.getSize()); + } + } + return value; + } + /** + /** + * Returns a context-relative path, beginning with a "/", that represents the canonical version + * of the specified path after ".." and "." elements are resolved out. If the specified path + * attempts to go outside the boundaries of the current context (i.e. too many ".." path + * elements are present), return null instead. + * + * @param strPath The path to be decoded + */ + public static String decodeURL(String strPath) + { + if (strPath == null) + return null; + + // Resolve encoded characters in the normalized path, which also handles encoded + // spaces so we can skip that later. Placed at the beginning of the chain so that + // encoded bad stuff(tm) can be caught by the later checks + + String strNormalized = null; + + try + { + strNormalized = WebDAVHelper.decodeURL(strPath); + } + catch (Exception ex) + { + logger.error("Error in decodeURL, URL = " + strPath, ex); + } + + if (strNormalized == null) + return (null); + + // Normalize the slashes and add leading slash if necessary + + if (strNormalized.indexOf('\\') >= 0) + strNormalized = strNormalized.replace('\\', '/'); + + if (!strNormalized.startsWith("/")) + strNormalized = "/" + strNormalized; + + // Resolve occurrences of "//" in the normalized path + + while (true) + { + int index = strNormalized.indexOf("//"); + if (index < 0) + break; + strNormalized = strNormalized.substring(0, index) + strNormalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + + while (true) + { + int index = strNormalized.indexOf("/./"); + if (index < 0) + break; + strNormalized = strNormalized.substring(0, index) + strNormalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + + while (true) + { + int index = strNormalized.indexOf("/../"); + if (index < 0) + break; + if (index == 0) + return (null); // Trying to go outside our context + + int index2 = strNormalized.lastIndexOf('/', index - 1); + strNormalized = strNormalized.substring(0, index2) + strNormalized.substring(index + 3); + } + + // Return the normalized path that we have completed + + return strNormalized; + } + + /** + * Make a unique lock token + * + * @param lockNode NodeRef + * @param owner String + * @return String + */ + public static final String makeLockToken(NodeRef lockNode, String owner) + { + StringBuilder str = new StringBuilder(); + + str.append(WebDAV.OPAQUE_LOCK_TOKEN); + str.append(lockNode.getId()); + str.append(LOCK_TOKEN_SEPERATOR); + str.append(ISO9075.encode(owner)); + + return str.toString(); + } + + /** + * Parse a lock token returning the node if and username + * + * @param lockToken String + * @return String[] + */ + public static final String[] parseLockToken(String lockToken) + { + // Check if the lock token is valid + + if ( lockToken == null) + return null; + + // Check if the token contains the lock token header + + if ( lockToken.startsWith(WebDAV.OPAQUE_LOCK_TOKEN)) + lockToken = lockToken.substring(WebDAV.OPAQUE_LOCK_TOKEN.length()); + + // Split the node id and username tokens + + int pos = lockToken.indexOf(LOCK_TOKEN_SEPERATOR); + if ( pos == -1) + return null; + + String[] tokens = new String[2]; + + tokens[0] = lockToken.substring(0,pos); + tokens[1] = lockToken.substring(pos + 1); + + return tokens; + } + + /** + * Returns string representation of the depth + * + * @param depth int + * @return String + */ + public static final String getDepthName(int depth) + { + switch (depth) + { + case DEPTH_0: + return ZERO; + + case DEPTH_1: + return ONE; + + case DEPTH_INFINITY: + return INFINITY; + + default: + throw new IllegalArgumentException("Unknown depth:" + depth); + } + } + + /** + * Static initializer + */ + static + { + // Create the WebDAV to Alfresco property mapping table + + _propertyNameMap = new Hashtable(); + + _propertyNameMap.put(XML_DISPLAYNAME, ContentModel.PROP_NAME); + _propertyNameMap.put(XML_CREATION_DATE, ContentModel.PROP_CREATED); + _propertyNameMap.put(XML_GET_LAST_MODIFIED, ContentModel.PROP_MODIFIED); + _propertyNameMap.put(XML_GET_CONTENT_TYPE, ContentModel.PROP_CONTENT); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVActivityPoster.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVActivityPoster.java new file mode 100644 index 00000000000..aaa35369e95 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVActivityPoster.java @@ -0,0 +1,75 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import org.alfresco.service.cmr.model.FileInfo; + +/** + * WebDAV methods can ActivityPoster to create entries in the activity feed. + * + * @author Matt Ward + */ +public interface WebDAVActivityPoster +{ + /** + * @param siteId + * @param tenantDomain + * @param path the path to the folder or null for files + * @param nodeInfo + * @throws WebDAVServerException + */ + void postFileFolderAdded( + String siteId, + String tenantDomain, + String path, + FileInfo nodeInfo) throws WebDAVServerException; + + /** + * @param siteId + * @param tenantDomain + * @param nodeInfo + * @throws WebDAVServerException + */ + void postFileFolderUpdated( + String siteId, + String tenantDomain, + FileInfo nodeInfo) throws WebDAVServerException; + + /** + * @param siteId + * @param tenantDomain + * @param parentPath + * @param parentNodeInfo + * @param contentNodeInfo + * @throws WebDAVServerException + */ + void postFileFolderDeleted( + String siteId, + String tenantDomain, + String parentPath, + FileInfo parentNodeInfo, + FileInfo contentNodeInfo) throws WebDAVServerException; +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVHelper.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVHelper.java new file mode 100644 index 00000000000..34e7fc0cce8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVHelper.java @@ -0,0 +1,1200 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.sync.events.types.ContentEvent; +import org.alfresco.sync.events.types.ContentEventImpl; +import org.alfresco.sync.events.types.Event; +import org.alfresco.jlan.util.IPAddress; +import org.alfresco.model.ContentModel; +import org.alfresco.sync.repo.Client; +import org.alfresco.sync.repo.Client.ClientType; +import org.alfresco.sync.repo.events.EventPreparator; +import org.alfresco.sync.repo.events.EventPublisher; +import org.alfresco.repo.lock.LockUtils; +import org.alfresco.repo.model.filefolder.HiddenAspect; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.activities.ActivityPoster; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.CopyService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.EqualsHelper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.surf.util.URLEncoder; +import org.springframework.util.StringUtils; +import org.xml.sax.helpers.AttributesImpl; + +/** + * WebDAV Protocol Helper Class + * + *

Provides helper methods for repository access using the WebDAV protocol. + * + * @author GKSpencer + */ +public class WebDAVHelper +{ + // Constants + public static final String BEAN_NAME = "webDAVHelper"; + + private static final String HTTPS_SCHEME = "https://"; + private static final String HTTP_SCHEME = "http://"; + + + // Path seperator + public static final String PathSeperator = "/"; + public static final char PathSeperatorChar = '/'; + public static final String EMPTY_SITE_ID = ""; + + // Logging + protected static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol"); + + // Service registry TODO: eliminate this - not dependency injection! + private ServiceRegistry m_serviceRegistry; + + // Services + private NodeService m_nodeService; + private FileFolderService m_fileFolderService; + private SearchService m_searchService; + private NamespaceService m_namespaceService; + private DictionaryService m_dictionaryService; + private MimetypeService m_mimetypeService; + private WebDAVLockService m_lockService; + private ActionService m_actionService; + private AuthenticationService m_authService; + private PermissionService m_permissionService; + private TenantService m_tenantService; + private HiddenAspect m_hiddenAspect; + private EventPublisher eventPublisher; + private ActivityPoster poster; + + // pattern is tested against full path after it has been lower cased. + private Pattern m_renameShufflePattern = Pattern.compile("(.*/\\..*)|(.*[a-f0-9]{8}+$)|(.*\\.tmp$)|(.*atmp[0-9]+$)|(.*\\.wbk$)|(.*\\.bak$)|(.*\\~$)|(.*backup.*\\.do[ct]{1}[x]?[m]?$)|(.*\\.sb\\-\\w{8}\\-\\w{6}$)"); + + // Empty XML attribute list + + private final AttributesImpl m_nullAttribs = new AttributesImpl(); + + private BehaviourFilter m_policyBehaviourFilter; + + private String m_urlPathPrefix; + + private long sizeLimit = -1L; + + /** + * This method sets a value for the limit. If the string does not {@link Long#parseLong(String) parse} to a + * java long. + * + * @param limit a String representing a valid Java long. + */ + public void setSizeLimitString(String limit) + { + // A string parameter is used here in order to not to require end users to provide a value for the limit in a property + // file. This results in the empty string being injected to this method. + long longLimit = -1L; + try + { + longLimit = Long.parseLong(limit); + } catch (NumberFormatException ignored) + { + // Intentionally empty + } + this.sizeLimit = longLimit; + } + + /** + * Set the regular expression that will be applied to filenames during renames + * to detect whether clients are performing a renaming shuffle - common during + * file saving on various clients. + *

+ * ALF-3856, ALF-7079, MNT-181 + * + * @param renameShufflePattern a regular expression filename match + */ + public void setRenameShufflePattern(Pattern renameShufflePattern) + { + this.m_renameShufflePattern = renameShufflePattern; + } + + /** + * @return Return the limit size + */ + public long getSizeLimit() + { + return sizeLimit; + } + + /** + * @return Return the authentication service + */ + public final AuthenticationService getAuthenticationService() + { + return m_authService; + } + + /** + * @return Return the service registry + */ + public ServiceRegistry getServiceRegistry() + { + // TODO: eliminate this - not dependency injection! + return m_serviceRegistry; + } + + /** + * @return Return the node service + */ + public final NodeService getNodeService() + { + return m_nodeService; + } + + public FileFolderService getFileFolderService() + { + return m_fileFolderService; + } + + /** + * @return Return the search service + */ + public final SearchService getSearchService() + { + return m_searchService; + } + + /** + * @return Return the namespace service + */ + public final NamespaceService getNamespaceService() + { + return m_namespaceService; + } + + /** + * @return Return the dictionary service + */ + public final DictionaryService getDictionaryService() + { + return m_dictionaryService; + } + + /** + * @return Return the mimetype service + */ + public final MimetypeService getMimetypeService() + { + return m_mimetypeService; + } + + /** + * @return Return the lock service + */ + public WebDAVLockService getLockService() + { + return m_lockService; + } + + /** + * @return Return the action service + */ + public final ActionService getActionService() + { + return m_actionService; + } + + /** + * + * @return Return the permission service + */ + public final PermissionService getPermissionService() + { + return m_permissionService; + } + + /** + * @return the hidden aspect bean + */ + public final HiddenAspect getHiddenAspect() + { + return m_hiddenAspect; + } + + /** + * Retrieve the {@link TenantService} held by the helper. + * + * @return TenantService + */ + public TenantService getTenantService() + { + return m_tenantService; + } + + /** + * @return Return the copy service + */ + public final CopyService getCopyService() + { + return getServiceRegistry().getCopyService(); + } + + public void setTenantService(TenantService tenantService) + { + this.m_tenantService = tenantService; + } + + /** + * @param serviceRegistry the service registry + */ + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.m_serviceRegistry = serviceRegistry; + } + + /** + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.m_nodeService = nodeService; + } + + /** + * @param fileFolderService the fileFolder service + */ + public void setFileFolderService(FileFolderService fileFolderService) + { + this.m_fileFolderService = fileFolderService; + } + + /** + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.m_searchService = searchService; + } + + /** + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.m_namespaceService = namespaceService; + } + + /** + * @param eventPublisher the eventPublisher service + */ + public void setEventPublisher(EventPublisher eventPublisher) + { + this.eventPublisher = eventPublisher; + } + + + /** + * @param poster ActivityPoster + */ + public void setPoster(ActivityPoster poster) + { + this.poster = poster; + } + + /** + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.m_dictionaryService = dictionaryService; + } + + /** + * @param mimetypeService the mimetype service + */ + public void setMimetypeService(MimetypeService mimetypeService) + { + this.m_mimetypeService = mimetypeService; + } + + /** + * @param lockService the lock service + */ + public void setLockService(WebDAVLockService lockService) + { + this.m_lockService = lockService; + } + + /** + * @param actionService the action service + */ + public void setActionService(ActionService actionService) + { + this.m_actionService = actionService; + } + + /** + * @param authService the authentication service + */ + public void setAuthenticationService(AuthenticationService authService) + { + this.m_authService = authService; + } + + /** + * @param permissionService the permission service + */ + public void setPermissionService(PermissionService permissionService) + { + this.m_permissionService = permissionService; + } + + /** + * @param hiddenAspect the hiddenAspect to set + */ + public void setHiddenAspect(HiddenAspect hiddenAspect) + { + this.m_hiddenAspect = hiddenAspect; + } + + + public BehaviourFilter getPolicyBehaviourFilter() + { + return m_policyBehaviourFilter; + } + + public void setPolicyBehaviourFilter(BehaviourFilter behaviourFilter) + { + m_policyBehaviourFilter = behaviourFilter; + } + + /** + * Checks a new path in a move operation to detect whether clients are starting a renaming shuffle - common during + * file saving on various clients. + *

+ * ALF-3856, ALF-7079, MNT-181 + */ + public boolean isRenameShuffle(String newPath) + { + return m_renameShufflePattern.matcher(newPath.toLowerCase()).matches(); + } + + /** + * Split the path into seperate directory path and file name strings. + * If the path is not empty, then there will always be an entry for the filename + * + * @param path Full path string. + * @return Returns a String[2] with the folder path and file path. + */ + public final String[] splitPath(String path) + { + if (path == null) + throw new IllegalArgumentException("path may not be null"); + + // Create an array of strings to hold the path and file name strings + String[] pathStr = new String[] {"", ""}; + + // Check if the path has a trailing seperator, if so then there is no file name. + + int pos = path.lastIndexOf(PathSeperatorChar); + if (pos == -1 || pos == (path.length() - 1)) + { + // Set the path string in the returned string array + pathStr[1] = path; + } + else + { + pathStr[0] = path.substring(0, pos); + pathStr[1] = path.substring(pos + 1); + } + // Return the path strings + return pathStr; + } + + /** + * Split the path into all the component directories and filename + * + * @param path the string to split + * @return an array of all the path components + */ + public List splitAllPaths(String path) + { + if (path == null || path.length() == 0) + { + return Collections.emptyList(); + } + + // split the path + StringTokenizer token = new StringTokenizer(path, PathSeperator); + List results = new ArrayList(10); + while (token.hasMoreTokens()) + { + results.add(token.nextToken()); + } + return results; + } + + public String getURLForPath(HttpServletRequest request, String path, boolean isCollection) + { + return getURLForPath(request, path, isCollection, null); + } + + public String getURLForPath(HttpServletRequest request, String path, boolean isCollection, String userAgent) + { + String urlPathPrefix = getUrlPathPrefix(request); + StringBuilder urlStr = new StringBuilder(urlPathPrefix); + + if (path.equals(WebDAV.RootPath) == false) + { + // split the path and URL encode each path element + for (StringTokenizer t = new StringTokenizer(path, PathSeperator); t.hasMoreTokens(); /**/) + { + urlStr.append( WebDAVHelper.encodeURL(t.nextToken(), userAgent) ); + if (t.hasMoreTokens()) + { + urlStr.append(PathSeperator); + } + } + } + + // If the URL is to a collection add a trailing slash + if (isCollection && urlStr.charAt( urlStr.length() - 1) != PathSeperatorChar) + { + urlStr.append( PathSeperator); + } + + logger.debug("getURLForPath() path:" + path + " => url:" + urlStr); + + // Return the URL string + return urlStr.toString(); + } + + /** + * Get the file info for the given paths + * + * @param rootNodeRef the acting webdav root + * @param path the path to search for + * @return Return the file info for the path + * @throws FileNotFoundException + * if the path doesn't refer to a valid node + */ + public FileInfo getNodeForPath(NodeRef rootNodeRef, String path) throws FileNotFoundException + { + if (rootNodeRef == null) + { + throw new IllegalArgumentException("Root node may not be null"); + } + else if (path == null) + { + throw new IllegalArgumentException("Path may not be null"); + } + + FileFolderService fileFolderService = getFileFolderService(); + // Check for the root path + if ( path.length() == 0 || path.equals(PathSeperator)) + { + return fileFolderService.getFileInfo(rootNodeRef); + } + + // split the paths up + List splitPath = splitAllPaths(path); + + // find it + FileInfo fileInfo = m_fileFolderService.resolveNamePath(rootNodeRef, splitPath); + + String fileName = splitPath.get(splitPath.size() - 1); + if (!fileInfo.getName().equals(fileName)) + { + throw new FileNotFoundException("Requested filename " + fileName + + " does not match case of " + fileInfo.getName()); + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Fetched node for path: \n" + + " root: " + rootNodeRef + "\n" + + " path: " + path + "\n" + + " result: " + fileInfo); + } + return fileInfo; + } + + public boolean isRootPath(String path, String servletPath) + { + // Check for the root path + return( path.length() == 0 || path.equals(PathSeperator) || EqualsHelper.nullSafeEquals(path, servletPath)); + } + + public final FileInfo getParentNodeForPath(NodeRef rootNodeRef, String path) throws FileNotFoundException + { + if (rootNodeRef == null) + { + throw new IllegalArgumentException("Root node may not be null"); + } + else if (path == null) + { + throw new IllegalArgumentException("Path may not be null"); + } + // shorten the path + String[] paths = splitPath(path); + return getNodeForPath(rootNodeRef, paths[0]); + } + + /** + * Return the relative path for the node walking back to the specified root node + * + * @param rootNodeRef the root below which the path will be valid + * @param nodeRef the node's path to get + * @return Returns string of form /A/B/C where C represents the from node and + */ + public final String getPathFromNode(NodeRef rootNodeRef, NodeRef nodeRef) throws FileNotFoundException + { + // Check if the nodes are valid, or equal + if (rootNodeRef == null || nodeRef == null) + throw new IllegalArgumentException("Invalid node(s) in getPathFromNode call"); + + // short cut if the path node is the root node + if (rootNodeRef.equals(nodeRef)) + return ""; + + FileFolderService fileFolderService = getFileFolderService(); + + // get the path elements + List pathInfos = fileFolderService.getNameOnlyPath(rootNodeRef, nodeRef); + + // build the path string + StringBuilder sb = new StringBuilder(pathInfos.size() * 20); + for (String fileInfo : pathInfos) + { + sb.append(WebDAVHelper.PathSeperatorChar); + sb.append(fileInfo); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Build name path for node: \n" + + " root: " + rootNodeRef + "\n" + + " target: " + nodeRef + "\n" + + " path: " + sb); + } + return sb.toString(); + } + + public FileInfo createFile(FileInfo parentNodeInfo, String path) throws WebDAVServerException + { + return m_fileFolderService.create(parentNodeInfo.getNodeRef(), path, ContentModel.TYPE_CONTENT); + } + + public List getChildren(FileInfo fileInfo) throws WebDAVServerException + { + return m_fileFolderService.list(fileInfo.getNodeRef()); + } + + /** + * Make an ETag value for a node using the GUID and modify date/time + */ + public final String makeETag(FileInfo nodeInfo) + { + // Get the modify date/time property for the node + + StringBuilder etag = new StringBuilder(); + makeETagString(nodeInfo, etag); + return etag.toString(); + } + + /** + * Make an ETag value for a node using the GUID and modify date/time + */ + public final String makeQuotedETag(FileInfo nodeInfo) + { + StringBuilder etag = new StringBuilder(); + + etag.append("\""); + makeETagString(nodeInfo, etag); + etag.append("\""); + return etag.toString(); + } + + /** + * Make an ETag value for a node using the GUID and modify date/time + */ + protected final void makeETagString(FileInfo nodeInfo, StringBuilder etag) + { + // Get the modify date/time property for the node + + Object modVal = nodeInfo.getProperties().get(ContentModel.PROP_MODIFIED); + + etag.append(nodeInfo.getNodeRef().getId()); + + if ( modVal != null) + { + etag.append("_"); + etag.append(DefaultTypeConverter.INSTANCE.longValue(modVal)); + } + } + + /** + * @return Return the null XML attribute list + */ + public final AttributesImpl getNullAttributes() + { + return m_nullAttribs; + } + + /** + * Encodes the given string to valid URL format + * + * @param s the String to convert + */ + public final static String encodeURL(String s) + { + return encodeURL(s, null); + } + + public final static String encodeURL(String s, String userAgent) + { + return URLEncoder.encode(s); + } + + public final static String decodeURL(String s) + { + return URLDecoder.decode(s); + } + + /** + * Encodes the given string to valid HTML format + * + * @param string the String to convert + */ + public final static String encodeHTML(String string) + { + if (string == null) + { + return ""; + } + + StringBuilder sb = null; //create on demand + String enc; + char c; + for (int i = 0; i < string.length(); i++) + { + enc = null; + c = string.charAt(i); + switch (c) + { + case '"': enc = """; break; //" + case '&': enc = "&"; break; //& + case '<': enc = "<"; break; //< + case '>': enc = ">"; break; //> + + //misc + case '\u20AC': enc = "€"; break; + case '\u00AB': enc = "«"; break; + case '\u00BB': enc = "»"; break; + case '\u00A0': enc = " "; break; + + default: + if (((int)c) >= 0x80) + { + //encode all non basic latin characters + enc = "&#" + ((int)c) + ";"; + } + break; + } + + if (enc != null) + { + if (sb == null) + { + String soFar = string.substring(0, i); + sb = new StringBuilder(i + 8); + sb.append(soFar); + } + sb.append(enc); + } + else + { + if (sb != null) + { + sb.append(c); + } + } + } + + if (sb == null) + { + return string; + } + else + { + return sb.toString(); + } + } + + /** + * ALF-5333: Microsoft clients use ISO-8859-1 to decode WebDAV responses + * so this method should only be used for Microsoft user agents. + * + * @param string String + * @return The encoded string for Microsoft clients + * @throws UnsupportedEncodingException + */ + public final static String encodeUrlReservedSymbols(String string) throws UnsupportedEncodingException + { + if (string == null) + { + return ""; + } + + StringBuilder sb = null; // create on demand + String enc; + char c; + for (int i = 0; i < string.length(); i++) + { + enc = null; + c = string.charAt(i); + switch (c) + { + // reserved + case ';': enc = URLEncoder.encode(String.valueOf(c)); break; + case '/': enc = URLEncoder.encode(String.valueOf(c)); break; + case '?': enc = URLEncoder.encode(String.valueOf(c)); break; + case ':': enc = URLEncoder.encode(String.valueOf(c)); break; + case '@': enc = URLEncoder.encode(String.valueOf(c)); break; + case '&': enc = URLEncoder.encode(String.valueOf(c)); break; + case '=': enc = URLEncoder.encode(String.valueOf(c)); break; + case '+': enc = URLEncoder.encode(String.valueOf(c)); break; + + // unsafe + case '\"': enc = URLEncoder.encode(String.valueOf(c)); break; + case '#': enc = URLEncoder.encode(String.valueOf(c)); break; + case '%': enc = URLEncoder.encode(String.valueOf(c)); break; + case '>': enc = URLEncoder.encode(String.valueOf(c)); break; + case '<': enc = URLEncoder.encode(String.valueOf(c)); break; + default: break; + } + + if (enc != null) + { + if (sb == null) + { + String soFar = string.substring(0, i); + sb = new StringBuilder(i + 8); + sb.append(soFar); + } + sb.append(enc); + } + else + { + if (sb != null) + { + sb.append(c); + } + } + } + + if (sb == null) + { + return string; + } + else + { + return sb.toString(); + } + } + + public String determineSiteId(WebDAVMethod method) + { + return determineSiteId(method.getRootNodeRef(), method.getPath()); + } + + public String determineSiteId(NodeRef rootNodeRef, String path) + { + SiteService siteService = getServiceRegistry().getSiteService(); + String siteId; + try + { + FileInfo fileInfo = getNodeForPath(rootNodeRef, path); + siteId = siteService.getSiteShortName(fileInfo.getNodeRef()); + if (siteId == null) + { + throw new RuntimeException("Node is not contained by a site: " + path); + } + } + catch (Exception error) + { + siteId = EMPTY_SITE_ID; + } + return siteId; + } + + @Deprecated + public String determineTenantDomain(WebDAVMethod method) + { + return determineTenantDomain(); + } + + public String determineTenantDomain() + { + TenantService tenantService = getTenantService(); + String tenantDomain = tenantService.getCurrentUserDomain(); + if (tenantDomain == null) + { + return TenantService.DEFAULT_DOMAIN; + } + return tenantDomain; + } + + /** + * Extract the destination path for MOVE or COPY commands from the + * supplied destination URL header. + * + * @param servletPath Path prefix of the WebDAV servlet. + * @param destURL The Destination header. + * @return The path to move/copy the file to. + */ + public String getDestinationPath(String contextPath, String servletPath, String destURL) + { + if (destURL != null && destURL.length() > 0) + { + int offset = -1; + + if (destURL.startsWith(HTTP_SCHEME)) + { + // Set the offset to the start of the host name + offset = HTTP_SCHEME.length(); + } + else if (destURL.startsWith(HTTPS_SCHEME)) + { + // Set the offset to the start of the host name + offset = HTTPS_SCHEME.length(); + } + + // Strip the start of the path if not a relative path + + if (offset != -1) + { + offset = destURL.indexOf(WebDAV.PathSeperator, offset); + if (offset != -1) + { + // Strip the host from the beginning + String strPath = destURL.substring(offset); + + // If it starts with /contextPath/servletPath/ (e.g. /alfresco/webdav/path/to/file) - then + // strip the servlet path from the start of the path. + String pathPrefix = contextPath + servletPath + WebDAV.PathSeperator; + if (strPath.startsWith(pathPrefix)) + { + strPath = strPath.substring(pathPrefix.length()); + } + + return WebDAV.decodeURL(strPath); + } + } + } + + // Unable to get the path. + return null; + } + + /** + * Check that the destination path is on this server and is a valid WebDAV + * path for this server + * + * @param request The request made against the WebDAV server. + * @param urlStr String + * @exception WebDAVServerException + */ + public void checkDestinationURL(HttpServletRequest request, String urlStr) throws WebDAVServerException + { + try + { + // Parse the URL + + URL url = new URL(urlStr); + + // Check if the path is on this WebDAV server + + boolean localPath = true; + + if (url.getPort() != -1 && url.getPort() != request.getServerPort()) + { + // Debug + + if (logger.isDebugEnabled()) + logger.debug("Destination path, different server port"); + + localPath = false; + } + else if (url.getHost().equalsIgnoreCase(request.getServerName()) == false + && url.getHost().equals(request.getLocalAddr()) == false) + { + // The target host may contain a domain or be specified as a numeric IP address + + String targetHost = url.getHost(); + + if ( IPAddress.isNumericAddress( targetHost) == false) + { + String localHost = request.getServerName(); + + int pos = targetHost.indexOf( "."); + if ( pos != -1) + targetHost = targetHost.substring( 0, pos); + + pos = localHost.indexOf( "."); + if ( pos != -1) + localHost = localHost.substring( 0, pos); + + // compare the host names + + if ( targetHost.equalsIgnoreCase( localHost) == false) + localPath = false; + } + else + { + try + { + // Check if the target IP address is a local address + + InetAddress targetAddr = InetAddress.getByName( targetHost); + if ( NetworkInterface.getByInetAddress( targetAddr) == null) + localPath = false; + } + catch (Exception ex) + { + // DEBUG + + if ( logger.isDebugEnabled()) + logger.debug("Failed to check target IP address, " + targetHost); + + localPath = false; + } + } + + // Debug + + if (localPath == false && logger.isDebugEnabled()) + { + logger.debug("Destination path, different server name/address"); + logger.debug(" URL host=" + url.getHost() + ", ServerName=" + request.getServerName() + ", localAddr=" + request.getLocalAddr()); + } + } + else if (!url.getPath().startsWith(getUrlPathPrefix(request))) + { + // Debug + + if (logger.isDebugEnabled()) + logger.debug("Destination path, different serlet path"); + + localPath = false; + } + + // If the URL does not refer to this WebDAV server throw an + // exception + + if (localPath != true) + throw new WebDAVServerException(HttpServletResponse.SC_BAD_GATEWAY); + } + catch (MalformedURLException ex) + { + // Debug + + if (logger.isDebugEnabled()) + logger.debug("Bad destination path, " + urlStr); + + throw new WebDAVServerException(HttpServletResponse.SC_BAD_GATEWAY); + } + } + + + public void setUrlPathPrefix(String urlPathPrefix) + { + m_urlPathPrefix = urlPathPrefix; + } + + public String getUrlPathPrefix(HttpServletRequest request) + { + StringBuilder urlStr = null; + if (StringUtils.hasText(m_urlPathPrefix)) + { + // A specific prefix has been configured in, so use it. + urlStr = new StringBuilder(m_urlPathPrefix); + } + else + { + // Extract the path prefix from the request, using the servlet path as a guide. + // e.g. "/preamble/servlet-mapping/folder/file.txt" + // with a servlet path of "/servlet-mapping" + // would result in a path prefix of "/preamble/servlet-mapping" being discovered. + urlStr = new StringBuilder(request.getRequestURI()); + String servletPath = request.getServletPath(); + + int rootPos = urlStr.indexOf(servletPath); + if (rootPos != -1) + { + urlStr.setLength(rootPos + servletPath.length()); + } + } + + // Ensure the prefix ends in the path separator. + if (urlStr.length() == 0 || urlStr.charAt(urlStr.length() - 1) != PathSeperatorChar) + { + urlStr.append(PathSeperator); + } + + return urlStr.toString(); + } + + /** + * Notifies listeners that a read has taken place + * @param realNodeInfo FileInfo + * @param mimetype String + * @param size Long + * @param contentEncoding String + * @param range String + */ + protected void publishReadEvent(final FileInfo realNodeInfo, final String mimetype, final Long size, final String contentEncoding, final String range) + { + + if (!StringUtils.hasText(range)) + { + // Its not a range request + eventPublisher.publishEvent(new EventPreparator() + { + @Override + public Event prepareEvent(String user, String networkId, String transactionId) + { + return new ContentEventImpl(ContentEvent.DOWNLOAD, user, networkId, transactionId, realNodeInfo.getNodeRef().getId(), null, + realNodeInfo.getType().toString(), Client.asType(ClientType.webdav), realNodeInfo.getName(), mimetype, + size, contentEncoding); + } + }); + + } + } + + public String getRepositoryPath(HttpServletRequest request) + { + // Try and get the path + + String strPath = null; + + try + { + strPath = WebDAVHelper.decodeURL(request.getRequestURI()); + } + catch (Exception ex) {} + + // Find the servlet path and trim from the request path + + String servletPath = request.getServletPath(); + + int rootPos = strPath.indexOf(servletPath); + if ( rootPos != -1) + { + strPath = strPath.substring( rootPos); + } + + // If we failed to get the path from the request try and get the path from the servlet path + + if (strPath == null) + { + strPath = request.getServletPath(); + } + + if (strPath == null || strPath.length() == 0) + { + // If we still have not got a path then default to the root directory + strPath = WebDAV.RootPath; + } + else if (strPath.startsWith(request.getServletPath())) + { + // Check if the path starts with the base servlet path + int len = request.getServletPath().length(); + + if (strPath.length() > len) + { + strPath = strPath.substring(len); + } + else + { + strPath = WebDAV.RootPath; + } + } + + // Make sure there are no trailing slashes + + if (strPath.length() > 1 && strPath.endsWith(WebDAV.PathSeperator)) + { + strPath = strPath.substring(0, strPath.length() - 1); + } + + // Return the path + + return strPath; + } + + /** + * Indicates if the node is unlocked or the current user has a WRITE_LOCK

+ * + * @see LockService#isLockedAndReadOnly(org.alfresco.service.cmr.repository.NodeRef) + * + * @param nodeRef the node reference + */ + public boolean isLockedAndReadOnly(final NodeRef nodeRef) + { + return m_serviceRegistry.getLockService().isLockedAndReadOnly(nodeRef); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVLockService.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVLockService.java new file mode 100644 index 00000000000..a826adc6b5c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVLockService.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav; + +import javax.servlet.http.HttpSession; + +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * WebDAVLockService is used to manage file locks for WebDAV and Sharepoint protocol. It ensures a lock never persists + * for more than 24 hours, and also ensures locks are timed out on session timeout. + * + * @author Pavel.Yurkevich + * @author Matt Ward + */ +public interface WebDAVLockService +{ + static final String BEAN_NAME = "webDAVLockService"; + + @SuppressWarnings("unchecked") + void sessionDestroyed(); + + /** + * Shared method for webdav/vti protocols to lock node. If node is locked for more than 24 hours it is automatically added + * to the current session locked resources list. + * + * @param nodeRef the node to lock + * @param userName the current user's user name + * @param timeout the number of seconds before the locks expires + */ + void lock(NodeRef nodeRef, String userName, int timeout); + + void lock(NodeRef nodeRef, LockInfo lockInfo); + + /** + * Shared method for webdav/vti to unlock node. Unlocked node is automatically removed from + * current sessions's locked resources list. + * + * @param nodeRef the node to lock + */ + void unlock(NodeRef nodeRef); + + /** + * Gets the lock info for the node reference relative to the current user. + * + * @see LockService#getLockStatus(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + * + * @param nodeRef the node reference + * @return the lock status + */ + LockInfo getLockInfo(NodeRef nodeRef); + + /** + * Determines if the node is locked AND it's not a WRITE_LOCK for the current user.

+ * + * @return true if the node is locked AND it's not a WRITE_LOCK for the current user + */ + public boolean isLockedAndReadOnly(NodeRef nodeRef); + + /** + * Caches current session in a thread local variable. + * + * @param session HttpSession + */ + void setCurrentSession(HttpSession session); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java new file mode 100644 index 00000000000..ec3ea6b6153 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVLockServiceImpl.java @@ -0,0 +1,486 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.lock.LockUtils; +import org.alfresco.repo.lock.mem.Lifetime; +import org.alfresco.repo.lock.mem.LockState; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.LockStatus; +import org.alfresco.service.cmr.lock.LockType; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

+ * WebDAVLockService is used to manage file locks for WebDAV and Sharepoint protocol. It ensures a lock never persists + * for more than 24 hours, and also ensures locks are timed out on session timeout. + * + * @author Pavel.Yurkevich + */ +public class WebDAVLockServiceImpl implements WebDAVLockService +{ + /** The session attribute under which webdav/vti stores its locked documents. */ + private static final String LOCKED_RESOURCES = "_webdavLockedResources"; + + private static Log logger = LogFactory.getLog(WebDAVLockServiceImpl.class); + + private static ThreadLocal currentSession = new ThreadLocal(); + + private LockService lockService; + private NodeService nodeService; + private TransactionService transactionService; + private CheckOutCheckInService checkOutCheckInService; + + /** + * Set the LockService + * + * @param lockService LockService + */ + public void setLockService(LockService lockService) + { + this.lockService = lockService; + } + + /** + * Set the NodeService + * + * @param nodeService NodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the TransactionService + * + * @param transactionService TransactionService + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the CheckOutCheckInService + * + * @param checkOutCheckInService CheckOutCheckInService + */ + public void setCheckOutCheckInService(CheckOutCheckInService checkOutCheckInService) + { + this.checkOutCheckInService = checkOutCheckInService; + } + + /** + * Caches current session to the thread local variable + * + * @param session HttpSession + */ + @Override + public void setCurrentSession(HttpSession session) + { + currentSession.set(session); + } + + @Override + @SuppressWarnings("unchecked") + public void sessionDestroyed() + { + HttpSession session = currentSession.get(); + + if (session == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Couldn't find current session."); + } + return; + } + + // look for locked documents list in http session + final List> lockedResources = (List>) session.getAttribute(LOCKED_RESOURCES); + + if (lockedResources != null && lockedResources.size() > 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("Found " + lockedResources.size() + " locked resources for session: " + session.getId()); + } + + for (Pair lockedResource : lockedResources) + { + String runAsUser = lockedResource.getFirst(); + final NodeRef nodeRef = lockedResource.getSecond(); + + // there are some document that should be forcibly unlocked + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // check whether this document still exists in repo + if (nodeService.exists(nodeRef)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Trying to release lock for: " + nodeRef); + } + + // check the lock status of document + LockStatus lockStatus = lockService.getLockStatus(nodeRef); + + // check if document was checked out + boolean hasWorkingCopy = checkOutCheckInService.getWorkingCopy(nodeRef) != null; + boolean isWorkingCopy = nodeService.hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY); + + // forcibly unlock document if it is still locked and not checked out + if ((lockStatus.equals(LockStatus.LOCKED) || + lockStatus.equals(LockStatus.LOCK_OWNER)) && !hasWorkingCopy && !isWorkingCopy) + { + try + { + // try to unlock it + lockService.unlock(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug("Lock was successfully released for: " + + nodeRef); + } + } + catch (Exception e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unable to unlock " + nodeRef + + " cause: " + e.getMessage()); + } + } + } + else + { + // document is not locked or is checked out + if (logger.isDebugEnabled()) + { + logger.debug("Skip lock releasing for: " + nodeRef + + " as it is not locked or is checked out"); + } + } + } + else + { + // document no longer exists in repo + if (logger.isDebugEnabled()) + { + logger.debug("Skip lock releasing for an unexisting node: " + nodeRef); + } + } + return null; + } + }, transactionService.isReadOnly()); + } + }, runAsUser == null ? AuthenticationUtil.getSystemUserName() : runAsUser); + } + } + else + { + // there are no documents with unexpected lock left on it + if (logger.isDebugEnabled()) + { + logger.debug("No locked resources were found for session: " + session.getId()); + } + } + } + + public void lock(NodeRef nodeRef, LockInfo lockInfo) + { + boolean performSessionBehavior = false; + long timeout; + + timeout = lockInfo.getRemainingTimeoutSeconds(); + + // ALF-11777 fix, do not lock node for more than 24 hours (webdav and vti) + if (timeout >= WebDAV.TIMEOUT_24_HOURS || timeout == WebDAV.TIMEOUT_INFINITY) + { + timeout = WebDAV.TIMEOUT_24_HOURS; + lockInfo.setTimeoutSeconds((int) timeout); + performSessionBehavior = true; + } + + // TODO: lock children according to depth? lock type? + final String additionalInfo = lockInfo.toJSON(); + lockService.lock(nodeRef, LockType.WRITE_LOCK, (int) timeout, Lifetime.EPHEMERAL, additionalInfo); + + + if (logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was locked for " + timeout + " seconds."); + } + + if (performSessionBehavior) + { + HttpSession session = currentSession.get(); + + if (session == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Couldn't find current session."); + } + return; + } + + storeObjectInSessionList(session, LOCKED_RESOURCES, new Pair(AuthenticationUtil.getRunAsUser(), nodeRef)); + + if (logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was added to the session " + session.getId() + " for post expiration processing."); + } + } + } + + /** + * Shared method for webdav/vti protocols to lock node. If node is locked for more than 24 hours it is automatically added + * to the current session locked resources list. + * + * @param nodeRef the node to lock + * @param userName userName + * @param timeout the number of seconds before the locks expires + */ + @Override + public void lock(NodeRef nodeRef, String userName, int timeout) + { + LockInfo lockInfo = createLock(nodeRef, userName, true, timeout); + lock(nodeRef, lockInfo); + } + + /** + * Shared method for webdav/vti to unlock node. Unlocked node is automatically removed from + * current sessions's locked resources list. + * + * @param nodeRef the node to lock + */ + @Override + public void unlock(NodeRef nodeRef) + { + lockService.unlock(nodeRef); + + if (logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was unlocked."); + } + + HttpSession session = currentSession.get(); + + if (session == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Couldn't find current session."); + } + return; + } + + boolean removed = removeObjectFromSessionList(session, LOCKED_RESOURCES, new Pair(AuthenticationUtil.getRunAsUser(), nodeRef)); + + if (removed && logger.isDebugEnabled()) + { + logger.debug(nodeRef + " was removed from the session " + session.getId()); + } + } + + /** + * Gets the lock status for the node reference relative to the current user. + * + * @see LockService#getLockStatus(org.alfresco.service.cmr.repository.NodeRef, String) + * + * @param nodeRef the node reference + * @return the lock status + */ + @Override + public LockInfo getLockInfo(NodeRef nodeRef) + { + LockInfo lockInfo = null; + LockState lockState = lockService.getLockState(nodeRef); + if (lockState != null) + { + String additionalInfo = lockState.getAdditionalInfo(); + + try + { + lockInfo = LockInfoImpl.fromJSON(additionalInfo); + } + catch (IllegalArgumentException e) + { + lockInfo = new LockInfoImpl(); + } + + lockInfo.setExpires(lockState.getExpires()); + lockInfo.setOwner(lockState.getOwner()); + } + return lockInfo; + } + + /** + * Determines if the node is locked AND it's not a WRITE_LOCK for the current user.

+ * + * @return true if the node is locked AND it's not a WRITE_LOCK for the current user + */ + public boolean isLockedAndReadOnly(NodeRef nodeRef) + { + return this.lockService.isLockedAndReadOnly(nodeRef); + } + + /** + * Add the given object to the session list that is stored in session under listName attribute + * + * @param session the session + * @param listName the list name (session attribute name) + * @param object the object to store in session list + */ + @SuppressWarnings("unchecked") + private static final void storeObjectInSessionList(HttpSession session, String listName, Object object) + { + List list = null; + + synchronized (session) + { + list = (List) session.getAttribute(listName); + + if (list == null) + { + list = new ArrayList(); + session.setAttribute(listName, list); + } + } + + synchronized (list) + { + if (!list.contains(object)) + { + list.add(object); + } + } + } + + /** + * Removes the given object from the session list that is stored in session under listName attribute + * + * @param session the session + * @param listName the list name (session attribute name) + * @param object the object to store in session list + * + * @return true if session list contained the specified element, otherwise false + */ + @SuppressWarnings("unchecked") + private static final boolean removeObjectFromSessionList(HttpSession session, String listName, Object object) + { + List list = null; + + synchronized (session) + { + list = (List) session.getAttribute(listName); + } + + if (list == null) + { + return false; + } + + synchronized (list) + { + return list.remove(object); + } + } + + /** + * Create a new lock + * + * @param nodeRef NodeRef + * @param userName String + * @param createExclusive boolean + * @param timeoutSecs int + */ + private LockInfo createLock(NodeRef nodeRef, String userName, boolean createExclusive, int timeoutSecs) + { + // Create Lock token + String lockToken = WebDAV.makeLockToken(nodeRef, userName); + + LockInfo lockInfo = new LockInfoImpl(); + + if (createExclusive) + { + // Lock the node + lockInfo.setTimeoutSeconds(timeoutSecs); + lockInfo.setExclusiveLockToken(lockToken); + } + else + { + lockInfo.addSharedLockToken(lockToken); + } + + // Store lock depth + lockInfo.setDepth(WebDAV.getDepthName(WebDAV.DEPTH_INFINITY)); + // Store lock scope (shared/exclusive) + String scope = createExclusive ? WebDAV.XML_EXCLUSIVE : WebDAV.XML_SHARED; + lockInfo.setScope(scope); + // Store the owner of this lock + lockInfo.setOwner(userName); + + // TODO: to help with debugging/refactoring (remove later) + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!currentUser.equals(userName)) + { + throw new IllegalStateException("Node is being locked for user " + userName + + " by (different/current) user " + currentUser); + } + + return lockInfo; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVMethod.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVMethod.java new file mode 100644 index 00000000000..3b3fead5c4a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVMethod.java @@ -0,0 +1,1643 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.BufferedReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.SocketException; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Pattern; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.LimitedStreamCopier; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.dom4j.DocumentHelper; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; +import org.w3c.dom.Document; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + + +/** + * Abstract base class for all the WebDAV method handling classes + * + * @author gavinc + */ +public abstract class WebDAVMethod +{ + // Log output + + private static final String VERSION_NUM_PATTERN = "\\d+\\.\\d+(\\.\\d+)?"; + + protected static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol"); + + // Output formatted XML in the response + + private static final boolean XMLPrettyPrint = true; + + // Mapping of User-Agent pattern to response status code + // used to determine which status code should be returned for AccessDeniedException + + private static final Map accessDeniedStatusCodes = new LinkedHashMap(); + static + { + accessDeniedStatusCodes.put("^WebDAVLib/" + VERSION_NUM_PATTERN + "$", + HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + accessDeniedStatusCodes.put("^WebDAVFS/" + VERSION_NUM_PATTERN + " \\(\\d+\\)\\s+Darwin/" + + VERSION_NUM_PATTERN + "\\s+\\(.*\\)$", + HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + accessDeniedStatusCodes.put(".*", HttpServletResponse.SC_FORBIDDEN); + } + + // Servlet request/response + + protected HttpServletRequest m_request; + protected HttpServletResponse m_response; + private File m_requestBody; + private ServletInputStream m_inputStream; + private CharArrayWriter m_xmlWriter; + private BufferedReader m_reader; + + // WebDAV helper + + protected WebDAVHelper m_davHelper; + + // Root node + + protected NodeRef m_rootNodeRef; + + // Repository path + + protected String m_strPath = null; + + // User Agent + + protected String m_userAgent = null; + + // If header conditions + + protected LinkedList m_conditions = null; + + // If header resource-tag + + protected String m_resourceTag = null; + + // Depth header + + protected int m_depth = WebDAV.DEPTH_INFINITY; + + // request scope + protected Map m_childToParent = new HashMap(); + protected Map m_parentLockInfo = new HashMap(); + + private String siteId; + + private String tenantDomain; + + /** + * Default constructor + */ + public WebDAVMethod() + { + } + + /** + * Set the request/response details + * + * @param req + * HttpServletRequest + * @param resp + * HttpServletResponse + * @param davHelper + * WebDAVHelper + * @param rootNode + * NodeRef + */ + public void setDetails(final HttpServletRequest req, HttpServletResponse resp, WebDAVHelper davHelper, + NodeRef rootNode) + { + // Wrap the request so that it is 'retryable'. Calls to getInputStream() and getReader() will result in the + // request body being read into an intermediate file. + this.m_request = new HttpServletRequestWrapper(req) + { + + @Override + public ServletInputStream getInputStream() throws IOException + { + if (WebDAVMethod.this.m_reader != null) + { + throw new IllegalStateException("Reader in use"); + } + if (WebDAVMethod.this.m_inputStream == null) + { + final FileInputStream in = new FileInputStream(getRequestBodyAsFile(req)); + WebDAVMethod.this.m_inputStream = new ServletInputStream() + { + + @Override + public int read() throws IOException + { + return in.read(); + } + + @Override + public int read(byte b[]) throws IOException + { + return in.read(b); + } + + @Override + public int read(byte b[], int off, int len) throws IOException + { + return in.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException + { + return in.skip(n); + } + + @Override + public int available() throws IOException + { + return in.available(); + } + + @Override + public void close() throws IOException + { + in.close(); + } + + @Override + public void mark(int readlimit) + { + in.mark(readlimit); + } + + @Override + public void reset() throws IOException + { + in.reset(); + } + + @Override + public boolean markSupported() + { + return in.markSupported(); + } + }; + } + + return WebDAVMethod.this.m_inputStream; + } + + @Override + public BufferedReader getReader() throws IOException + { + if (WebDAVMethod.this.m_inputStream != null) + { + throw new IllegalStateException("Input Stream in use"); + } + if (WebDAVMethod.this.m_reader == null) + { + String encoding = req.getCharacterEncoding(); + WebDAVMethod.this.m_reader = new BufferedReader(new InputStreamReader(new FileInputStream( + getRequestBodyAsFile(req)), encoding == null ? "ISO-8859-1" : encoding)); + } + + return WebDAVMethod.this.m_reader; + } + + }; + this.m_response = resp; + this.m_davHelper = davHelper; + this.m_rootNodeRef = rootNode; + + this.m_strPath = m_davHelper.getRepositoryPath(m_request); + } + + private File getRequestBodyAsFile(HttpServletRequest req) throws IOException + { + if (this.m_requestBody == null) + { + this.m_requestBody = TempFileProvider.createTempFile("webdav_" + req.getMethod() + "_", ".bin"); + // copy the streams + LimitedStreamCopier streamCopier = new LimitedStreamCopier(); + long bytes = streamCopier.copyStreamsLong(req.getInputStream(), new FileOutputStream(this.m_requestBody), m_davHelper.getSizeLimit()); + + // get content length + long contentLength = -1; + try + { + contentLength = Long.valueOf(req.getHeader(WebDAV.HEADER_CONTENT_LENGTH)); + } + catch (NumberFormatException e) + { + ; // may be null etc. + } + + // ALF-7377: check for corrupt request + if (contentLength >= 0 && contentLength != bytes) + { + throw new IOException("Request body does not have specified Content Length"); + } + } + return this.m_requestBody; + } + + /** + * Override and return true if the method is a query method only. The default implementation + * returns false. + * + * @return Returns true if the method transaction may be read-only + */ + protected boolean isReadOnly() + { + return false; + } + + /** + * Return the property find depth + * + * @return int + */ + public final int getDepth() + { + return m_depth; + } + + /** + * Executes the method, wrapping the call to {@link #executeImpl()} in an appropriate transaction + * and handling the error conditions. + * @throws WebDAVServerException + */ + public void execute() throws WebDAVServerException + { + // Parse the HTTP headers + parseRequestHeaders(); + + // Parse the HTTP body + try + { + parseRequestBody(); + } + catch (WebDAVServerException e) + { + if (e.getCause() != null && e.getCause() instanceof SAXParseException) + { + SAXParseException saxParseEx = (SAXParseException) e.getCause(); + if (logger.isTraceEnabled()) + { + // Include stack trace. + logger.trace("Malformed request body", saxParseEx); + } + else if (logger.isDebugEnabled()) + { + // Log message only. + logger.debug("Malformed request body: " + saxParseEx.getMessage()); + } + + try + { + m_response.sendError(e.getHttpStatusCode()); + } + catch (IOException ioe) + { + if (logger.isDebugEnabled()) + { + logger.debug("Unable to send status code", ioe); + } + } + // Halt processing. + return; + } + else + { + // Rethrow the exception, as we haven't dealt with it here. + throw e; + } + } + + m_userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT); + + RetryingTransactionCallback executeImplCallback = new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + // Reset the request input stream / reader state + WebDAVMethod.this.m_inputStream = null; + WebDAVMethod.this.m_reader = null; + + // cache current session + getDAVHelper().getLockService().setCurrentSession(m_request.getSession()); + + executeImpl(); + return null; + } + }; + try + { + boolean isReadOnly = isReadOnly(); + // Execute the method + getTransactionService().getRetryingTransactionHelper().doInTransaction(executeImplCallback, isReadOnly); + generateResponseImpl(); + } + catch (AccessDeniedException e) + { + // Return a forbidden status + throw new WebDAVServerException(getStatusForAccessDeniedException()); + } + catch (Throwable e) + { + if (e instanceof WebDAVServerException) + { + throw (WebDAVServerException) e; + } + else if (e.getCause() instanceof WebDAVServerException) + { + throw (WebDAVServerException) e.getCause(); + } + else + { + boolean logOnly = false; + + Throwable t = e; + while ((t = t.getCause()) != null) + { + if (t instanceof SocketException) + { + logOnly = true; + + // The client aborted the connection - we can't do much about this, except log it. + if (logger.isTraceEnabled() || logger.isDebugEnabled()) + { + String message = "Client dropped connection [uri=" + m_request.getRequestURI() + "]"; + + if (logger.isTraceEnabled()) + { + // Include a stack trace when trace is enabled. + logger.trace(message, e); + } + else if (logger.isDebugEnabled()) + { + // Just a message for debug-level output. + logger.debug(message); + } + } + break; + } + } + + // Convert error to a server error + if (!logOnly) + { + throw new WebDAVServerException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + } + } + finally + { + cleanUp(); + } + } + + /** + * Clean up resources if about to finish processing the request. + */ + private void cleanUp() + { + // Remove temporary file if created + if (this.m_requestBody != null) + { + try + { + this.m_requestBody.delete(); + this.m_requestBody = null; + } + catch (Throwable t) + { + WebDAVMethod.logger.error("Failed to delete temp file", t); + } + } + } + + /** + * Access the content repository to satisfy the request and generates the appropriate WebDAV + * response. + * + * @throws WebDAVServerException a general server exception + * @throws Exception any unhandled exception + */ + protected abstract void executeImpl() throws WebDAVServerException, Exception; + + /** + * Does nothing unless overridden - for reasons of backwards compatibility. Subclasses + * implementing this method should separate the WebDAV method execution logic from + * response generation logic. Execution logic should be contained in the {@link #executeImpl} method + * and should NOT contain any code that writes to the response. Conversely response generation logic + * should NOT contain any code relating to the desired effect of the WebDAV method (e.g. setting properties + * on a node) and should be contained purely within this method. + *

+ * Older methods, until refactored will not override this method, relying only on {@link #executeImpl()}. + */ + protected void generateResponseImpl() throws Exception + { + } + + /** + * Parses the given request body represented as an XML document and sets any necessary context + * ready for execution. + */ + protected abstract void parseRequestBody() throws WebDAVServerException; + + /** + * Parses the HTTP headers of the request and sets any necessary context ready for execution. + */ + protected abstract void parseRequestHeaders() throws WebDAVServerException; + + /** + * Retrieves the request body as an XML document + * + * @return The body of the request as an XML document or null if there isn't a body + */ + protected Document getRequestBodyAsDocument() throws WebDAVServerException + { + Document body = null; + + if (m_request.getContentLength() > 0) + { + // TODO: Do we need to do anything for chunking support? + + try + { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + if (m_request.getCharacterEncoding() == null) + { + // Let the XML parser work out the encoding if it is not explicitly declared in the HTTP header + body = builder.parse(new InputSource(m_request.getInputStream())); + } + else + { + body = builder.parse(new InputSource(m_request.getReader())); + } + } + catch (ParserConfigurationException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST, e); + } + catch (SAXException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST, e); + } + catch (IOException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_BAD_REQUEST, e); + } + } + + return body; + } + + /** + * Parses "Depth" request header + * + * @throws WebDAVServerException + */ + protected void parseDepthHeader() throws WebDAVServerException + { + // Store the Depth header as this is used by several WebDAV methods + + String strDepth = m_request.getHeader(WebDAV.HEADER_DEPTH); + if (strDepth != null && strDepth.length() > 0) + { + if (strDepth.equals(WebDAV.ZERO)) + { + m_depth = WebDAV.DEPTH_0; + } + else if (strDepth.equals(WebDAV.ONE)) + { + m_depth = WebDAV.DEPTH_1; + } + else + { + m_depth = WebDAV.DEPTH_INFINITY; + } + } + } + + /** + * Parses "If" header of the request. + * Stores conditions that should be checked. + * Parses both No-tag-list and Tagged-list formats + * See "10.4.2 Syntax" paragraph of the WebDAV specification for "If" header format. + * + */ + protected void parseIfHeader() throws WebDAVServerException + { + //String strLockToken = null; + + String strIf = m_request.getHeader(WebDAV.HEADER_IF); + + if (logger.isDebugEnabled()) + logger.debug("Parsing If header: " + strIf); + + if (strIf != null && strIf.length() > 0) + { + if (strIf.startsWith("<")) + { + m_resourceTag = strIf.substring(1, strIf.indexOf(">")); + strIf = strIf.substring(m_resourceTag.length() + 3); + } + + m_conditions = new LinkedList(); + String[] parts = strIf.split("\\) \\("); + for (int i = 0; i < parts.length; i++) + { + + String partString = parts[i].replaceAll("\\(", "").replaceAll("\\)", ""); + + Condition c = new Condition(); + String[] conditions = partString.split(" "); + + for (int j = 0; j < conditions.length; j++) + { + boolean fNot = false; + String eTag = null; + String lockToken = null; + + if (WebDAV.HEADER_KEY_NOT.equals(conditions[j])) + { + // Check if Not keyword followed by State-token or entity-tag + if (j == (conditions.length - 1)) + { + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + fNot = true; + j++; + } + + // read State-token + int index = conditions[j].indexOf('<'); + if (index != -1) + { + try + { + String s = conditions[j].substring(index + 1, conditions[j].indexOf(">")); + if (!s.startsWith(WebDAV.OPAQUE_LOCK_TOKEN)) + { + if(!fNot) + { + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + } + else + { + lockToken = s; + c.addLockTocken(lockToken, fNot); + } + } + catch (IndexOutOfBoundsException e) + { + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + } + + // read entity-tag + index = conditions[j].indexOf("[\""); + if (index != -1) + { + // TODO: implement parsing of weak ETags: W/"123..". + int index2 = conditions[j].indexOf("]"); + if (index2 == -1) + { + logger.warn("No closing ']': "+conditions[j]); + index2 = conditions[j].length(); + } + eTag = conditions[j].substring(index + 1, index2); + c.addETag(eTag, fNot); + } + + } + m_conditions.add(c); + } + } + } + + /** + * Return the WebDAV protocol helper + * + * @return WebDAVHelper + */ + protected final WebDAVHelper getDAVHelper() + { + return m_davHelper; + } + + /** + * Return the service registry + * + * @return ServiceRegistry + */ + protected final ServiceRegistry getServiceRegistry() + { + return m_davHelper.getServiceRegistry(); + } + + /** + * Convenience method to return the transaction service + * + * @return TransactionService + */ + protected final TransactionService getTransactionService() + { + return m_davHelper.getServiceRegistry().getTransactionService(); + } + + /** + * Convenience method to return the node service + * + * @return NodeService + */ + protected final NodeService getNodeService() + { + return m_davHelper.getNodeService(); + } + + /** + * Convenience method to return the search service + * + * @return SearchService + */ + protected final SearchService getSearchService() + { + return m_davHelper.getSearchService(); + } + + /** + * Convenience method to return the namespace service + * + * @return NamespaceService + */ + protected final NamespaceService getNamespaceService() + { + return m_davHelper.getNamespaceService(); + } + + /** + * @return Returns the general file/folder manipulation service + */ + protected final FileFolderService getFileFolderService() + { + return m_davHelper.getFileFolderService(); + } + + /** + * Convenience method to return the content service + * + * @return ContentService + */ + protected final ContentService getContentService() + { + return m_davHelper.getServiceRegistry().getContentService(); + } + + /** + * Convenience method to return the mimetype service + * + * @return MimetypeService + */ + protected final MimetypeService getMimetypeService() + { + return m_davHelper.getMimetypeService(); + } + + /** + * Retrieve the (WebDAV protocol-level) locking service. + * + * @return WebDAVLockService + */ + protected final WebDAVLockService getDAVLockService() + { + return m_davHelper.getLockService(); + } + + /** + * Convenience method to return the action service + * + * @return ActionService + */ + protected final ActionService getActionService() + { + return m_davHelper.getActionService(); + } + + /** + * Convenience method to return the permission service + * + * @return PermissionService + */ + protected final PermissionService getPermissionService() + { + return m_davHelper.getPermissionService(); + } + + /** + * Convenience method to return the authentication service + * + * @return AuthenticationService + */ + protected final AuthenticationService getAuthenticationService() + { + return m_davHelper.getAuthenticationService(); + } + + /** + * @return Returns the path of the servlet, e.g. /webdav + */ + protected final String getServletPath() + { + return m_request.getServletPath(); + } + + /** + * @return Returns the context path of the servlet, e.g. /alfresco + */ + protected final String getContextPath() + { + return m_request.getContextPath(); + } + + /** + * Return the root node + * + * @return NodeRef + */ + protected final NodeRef getRootNodeRef() + { + return m_rootNodeRef; + } + + /** + * Return the relative path + * + * @return String + */ + public String getPath() + { + return m_strPath; + } + + /** + * Returns the format required for an XML response. This may vary per method. + */ + protected OutputFormat getXMLOutputFormat() + { + // Check if debug output or XML pretty printing is enabled + return (XMLPrettyPrint || logger.isDebugEnabled()) ? OutputFormat.createPrettyPrint() : OutputFormat.createCompactFormat(); + } + + /** + * Create an XML writer for the response + * + * @return XMLWriter + * @exception IOException + */ + protected final XMLWriter createXMLWriter() throws IOException + { + // Buffer the XML response, in case we have to reset mid-transaction + m_xmlWriter = new CharArrayWriter(1024); + return new XMLWriter(m_xmlWriter, getXMLOutputFormat()); + } + + /** + * Generates the lock discovery XML response + * + * @param xml XMLWriter + * @param lockNodeInfo FileInfo + * @param lockInfo LockInfo + */ + protected void generateLockDiscoveryXML(XMLWriter xml, FileInfo lockNodeInfo, LockInfo lockInfo) throws Exception + { + String owner, scope, depth; + Date expiry; + + owner = lockInfo.getOwner(); + expiry = lockInfo.getExpires(); + scope = lockInfo.getScope(); + depth = lockInfo.getDepth(); + + generateLockDiscoveryXML(xml, lockNodeInfo, false, scope, depth, + WebDAV.makeLockToken(lockNodeInfo.getNodeRef(), owner), owner, expiry); + } + + /** + * Generates the lock discovery XML response + * + * @param xml XMLWriter + * @param lockNodeInfo FileInfo + * @param emptyNamespace boolean True if namespace should be empty. Used to avoid bugs in WebDAV clients. + * @param scope String lock scope + * @param depth String lock depth + * @param lToken String locktoken + * @param owner String lock owner + * @param expiryDate the date/time the lock should expire + */ + protected void generateLockDiscoveryXML(XMLWriter xml, FileInfo lockNodeInfo, boolean emptyNamespace, + String scope, String depth, String lToken, String owner, Date expiryDate) throws Exception + { + Attributes nullAttr= getDAVHelper().getNullAttributes(); + String ns = emptyNamespace ? "" : WebDAV.DAV_NS; + if (lockNodeInfo != null) + { + // Output the XML response + + xml.startElement(ns, WebDAV.XML_LOCK_DISCOVERY, emptyNamespace ? WebDAV.XML_LOCK_DISCOVERY : WebDAV.XML_NS_LOCK_DISCOVERY, nullAttr); + xml.startElement(ns, WebDAV.XML_ACTIVE_LOCK, emptyNamespace ? WebDAV.XML_ACTIVE_LOCK : WebDAV.XML_NS_ACTIVE_LOCK, nullAttr); + + xml.startElement(ns, WebDAV.XML_LOCK_TYPE, emptyNamespace ? WebDAV.XML_LOCK_TYPE : WebDAV.XML_NS_LOCK_TYPE, nullAttr); + xml.write(DocumentHelper.createElement(emptyNamespace ? WebDAV.XML_WRITE : WebDAV.XML_NS_WRITE)); + xml.endElement(ns, WebDAV.XML_LOCK_TYPE, emptyNamespace ? WebDAV.XML_LOCK_TYPE : WebDAV.XML_NS_LOCK_TYPE); + + xml.startElement(ns, WebDAV.XML_LOCK_SCOPE, emptyNamespace ? WebDAV.XML_LOCK_SCOPE : WebDAV.XML_NS_LOCK_SCOPE, nullAttr); + xml.write(DocumentHelper.createElement(emptyNamespace ? scope : WebDAV.DAV_NS_PREFIX + scope)); + xml.endElement(ns, WebDAV.XML_LOCK_SCOPE, emptyNamespace ? WebDAV.XML_LOCK_SCOPE : WebDAV.XML_NS_LOCK_SCOPE); + + // NOTE: We only support one level of lock at the moment + + xml.startElement(ns, WebDAV.XML_DEPTH, emptyNamespace ? WebDAV.XML_DEPTH : WebDAV.XML_NS_DEPTH, nullAttr); + xml.write(depth); + xml.endElement(ns, WebDAV.XML_DEPTH, emptyNamespace ? WebDAV.XML_DEPTH : WebDAV.XML_NS_DEPTH); + + xml.startElement(ns, WebDAV.XML_OWNER, emptyNamespace ? WebDAV.XML_OWNER : WebDAV.XML_NS_OWNER, nullAttr); + xml.write(owner); + xml.endElement(ns, WebDAV.XML_OWNER, emptyNamespace ? WebDAV.XML_OWNER : WebDAV.XML_NS_OWNER); + + xml.startElement(ns, WebDAV.XML_TIMEOUT, emptyNamespace ? WebDAV.XML_TIMEOUT : WebDAV.XML_NS_TIMEOUT, nullAttr); + + // Output the expiry time + + String strTimeout = WebDAV.INFINITE; + if (expiryDate != null) + { + long timeoutRemaining = (expiryDate.getTime() - System.currentTimeMillis())/1000L; + + strTimeout = WebDAV.SECOND + timeoutRemaining; + } + xml.write(strTimeout); + + xml.endElement(ns, WebDAV.XML_TIMEOUT, emptyNamespace ? WebDAV.XML_TIMEOUT : WebDAV.XML_NS_TIMEOUT); + + xml.startElement(ns, WebDAV.XML_LOCK_TOKEN, emptyNamespace ? WebDAV.XML_LOCK_TOKEN : WebDAV.XML_NS_LOCK_TOKEN, nullAttr); + xml.startElement(ns, WebDAV.XML_HREF, emptyNamespace ? WebDAV.XML_HREF : WebDAV.XML_NS_HREF, nullAttr); + + xml.write(lToken); + + xml.endElement(ns, WebDAV.XML_HREF, emptyNamespace ? WebDAV.XML_HREF : WebDAV.XML_NS_HREF); + xml.endElement(ns, WebDAV.XML_LOCK_TOKEN, emptyNamespace ? WebDAV.XML_LOCK_TOKEN : WebDAV.XML_NS_LOCK_TOKEN); + + xml.endElement(ns, WebDAV.XML_ACTIVE_LOCK, emptyNamespace ? WebDAV.XML_ACTIVE_LOCK : WebDAV.XML_NS_ACTIVE_LOCK); + xml.endElement(ns, WebDAV.XML_LOCK_DISCOVERY, emptyNamespace ? WebDAV.XML_LOCK_DISCOVERY : WebDAV.XML_NS_LOCK_DISCOVERY); + } + } + + /** + * Generates a list of namespace declarations for the response + */ + protected String generateNamespaceDeclarations(HashMap nameSpaces) + { + StringBuilder ns = new StringBuilder(); + + ns.append(" "); + ns.append(WebDAV.XML_NS); + ns.append(":"); + ns.append(WebDAV.DAV_NS); + ns.append("=\""); + ns.append(WebDAV.DEFAULT_NAMESPACE_URI); + ns.append("\""); + + // Add additional namespaces + + if ( nameSpaces != null) + { + Iterator namespaceList = nameSpaces.keySet().iterator(); + + while (namespaceList.hasNext()) + { + String strNamespaceUri = namespaceList.next(); + String strNamespaceName = nameSpaces.get(strNamespaceUri); + + ns.append(" ").append(WebDAV.XML_NS).append(":").append(strNamespaceName).append("=\""); + ns.append(strNamespaceUri == null ? "" : strNamespaceUri).append("\" "); + } + } + + return ns.toString(); + } + + /** + * Checks if write operation can be performed on node. + * + * @param fileInfo - node's file info + * @param ignoreShared - if true ignores shared locks + * @param lockMethod - must be true if used from lock method + * @return node's lock info + * @throws WebDAVServerException if node has shared or exclusive lock + * or If header preconditions failed + */ + protected LockInfo checkNode(FileInfo fileInfo, boolean ignoreShared, boolean lockMethod) throws WebDAVServerException + { + LockInfo nodeLockInfo = getNodeLockInfo(fileInfo); + NodeRef nodeRef = fileInfo.getNodeRef(); + + // Regardless of WebDAV locks, if we can't write to this node, then it's locked! + if (getDAVHelper().isLockedAndReadOnly(nodeRef)) + { + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + + String nodeETag = getDAVHelper().makeQuotedETag(fileInfo); + + // Handle the case where there are no conditions and no lock token stored on the node. Node just needs to be writable with no shared locks + if (m_conditions == null) + { + // ALF-3681 fix. WebDrive 10 client doesn't send If header when locked resource is updated so check the node by lockOwner. + if (!nodeLockInfo.isExclusive() || (m_userAgent != null && m_userAgent.equals(WebDAV.AGENT_MICROSOFT_DATA_ACCESS_INTERNET_PUBLISHING_PROVIDER_DAV))) + { + if (!ignoreShared && nodeLockInfo.isShared() && !nodeLockInfo.getSharedLockTokens().isEmpty()) + { + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + return nodeLockInfo; + } + } + + // Checking of the If tag consists of two checks: + // 1. Check the appropriate lock token for the node has been supplied (if the node is locked) + // 2. If there are conditions, check at least one condition (corresponding to this node) is satisfied. + checkLockToken(nodeLockInfo, ignoreShared, lockMethod); + checkConditions(nodeLockInfo.getExclusiveLockToken(), nodeETag); + + return nodeLockInfo; + } + + /** + * Checks if write operation can be performed on node. + * + * @param fileInfo FileInfo + * @return LockInfo + * @throws WebDAVServerException if node has shared or exclusive lock + * or If header preconditions failed + */ + protected LockInfo checkNode(FileInfo fileInfo) throws WebDAVServerException + { + return checkNode(fileInfo, false, true); + } + + /** + * Checks if node can be accessed with WebDAV operation + * + * @param lockInfo - node's lock info + * @param ignoreShared - if true - ignores shared lock tokens + * @param lockMethod - must be true if used from lock method + * @throws WebDAVServerException if node has no appropriate lock token + */ + private void checkLockToken(LockInfo lockInfo, boolean ignoreShared, boolean lockMethod) throws WebDAVServerException + { + String nodeLockToken = lockInfo.getExclusiveLockToken(); + Set sharedLockTokens = lockInfo.getSharedLockTokens(); + + if (m_conditions != null) + { + // Request has conditions to check + if (lockInfo.isShared()) + { + // Node has shared lock. Check if conditions contains lock token of the node. + // If not throw exception + if (!sharedLockTokens.isEmpty()) + { + if (!ignoreShared) + { + for (Condition condition : m_conditions) + { + for (String sharedLockToken : sharedLockTokens) + { + if (condition.getLockTokensMatch().contains(sharedLockToken)) + { + return; + } + } + } + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + return; + } + } + else + { + // This particular node is not locked so it will not be part of the conditions + if (nodeLockToken == null) + { + return; + } + // Node has exclusive lock. Check if conditions contains lock token of the node + // If not throw exception + for (Condition condition : m_conditions) + { + if (condition.getLockTokensMatch().contains(nodeLockToken)) + { + return; + } + } + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + } + else + { + // Request has no conditions + if (lockInfo.isShared()) + { + // If lock is shared and check was called not from LOCK method return + if (!lockMethod) + { + return; + } + // Throw exception - we can't set lock on node with shared lock + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + } + + // MNT-10555 + if (lockInfo.isExpired()) + { + return; + } + + throw new WebDAVServerException(WebDAV.WEBDAV_SC_LOCKED); + } + + + /** + * Checks If header conditions. Throws WebDAVServerException with 412(Precondition failed) + * if none of the conditions success. + * + * @param nodeLockToken - node's lock token + * @param nodeETag - node's ETag + * @throws WebDAVServerException if conditions fail + */ + private void checkConditions(String nodeLockToken, String nodeETag) throws WebDAVServerException + { + // Checks If header conditions. + // Each condition can contain check of ETag and check of Lock token. + + if (m_conditions == null) + { + // No conditions were provided with "If" request header, so check successful + return; + } + + // Check the list of "If" header's conditions. + // If any condition conforms then check is successful + for (Condition condition : m_conditions) + { + // Flag for ETag conditions + boolean fMatchETag = true; + // Flag for Lock token conditions + boolean fMatchLockToken = true; + + // Check ETags that should match + if (condition.getETagsMatch() != null) + { + fMatchETag = condition.getETagsMatch().contains(nodeETag) ? true : false; + } + // Check ETags that shouldn't match + if (condition.getETagsNotMatch() != null) + { + fMatchETag = condition.getETagsNotMatch().contains(nodeETag) ? false : true; + } + // Check lock tokens that should match + if (condition.getLockTokensMatch() != null) + { + fMatchLockToken = nodeLockToken == null || condition.getLockTokensMatch().contains(nodeLockToken); + } + // Check lock tokens that shouldn't match + if (condition.getLockTokensNotMatch() != null) + { + fMatchLockToken = nodeLockToken == null || !condition.getLockTokensNotMatch().contains(nodeLockToken); + } + + if (fMatchETag && fMatchLockToken) + { + // Condition conforms + return; + } + } + + // None of the conditions successful + throw new WebDAVServerException(HttpServletResponse.SC_PRECONDITION_FAILED); + } + + + /** + * Returns node Lock token in consideration of WebDav lock depth. + * + * @param nodeInfo FileInfo + * @return String Lock token + */ + protected LockInfo getNodeLockInfo(final FileInfo nodeInfo) + { + if (nodeInfo.getNodeRef() == null) + { + // TODO review - note: can be null in case of Thor root + return new LockInfoImpl(); + } + + // perf optimisation - effectively run against unprotected nodeService (to bypass repeated permission checks) + return AuthenticationUtil.runAs(new RunAsWork() + { + public LockInfo doWork() throws Exception + { + return getNodeLockInfoImpl(nodeInfo); + } + }, AuthenticationUtil.getSystemUserName()); + } + + private LockInfo getNodeLockInfoImpl(final FileInfo nodeInfo) + { + // Check if node is locked directly. + LockInfo lockInfo = getNodeLockInfoDirect(nodeInfo); + if (lockInfo != null) + { + return lockInfo; + } + + // Node isn't locked directly, try to search for an indirect lock. + + // ALF-13472: In accordance with http://www.webdav.org/specs/rfc2518.html#rfc.section.8.10.4 lock of collection causes locking each resource within it. + // It should be possible to receive information about direct or indirect lock because it is one of the states of requested resource. + return AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public LockInfo doWork() throws Exception + { + NodeService nodeService = getNodeService(); + + NodeRef node = nodeInfo.getNodeRef(); + + while (true) + { + NodeRef parent = m_childToParent.get(node); + + if ((parent == null) && (! m_childToParent.containsKey(node))) + { + ChildAssociationRef childAssocRef = nodeService.getPrimaryParent(node); + parent = childAssocRef.getParentRef(); + + if (! childAssocRef.getTypeQName().equals(ContentModel.ASSOC_CONTAINS)) + { + parent = null; + } + + // temporarily cache - for this request + m_childToParent.put(node, parent); + } + + if (parent == null) + { + // Node has no lock and Lock token + return new LockInfoImpl(); + } + + LockInfo lockInfo = m_parentLockInfo.get(parent); + + if (lockInfo != null) + { + if (lockInfo.isLocked()) + { + return lockInfo; + } + } + + if (lockInfo == null) + { + try + { + lockInfo = getNodeLockInfoIndirect(parent); + if (lockInfo != null) + { + return lockInfo; + } + } + finally + { + if (lockInfo == null) + { + lockInfo = new LockInfoImpl(); + } + // temporarily cache - for this request + m_parentLockInfo.put(parent, lockInfo); + } + } + + node = parent; + } // end while + } + }); + + } + + /** + * Checks if a node is directly locked. A direct lock is one associated with the node itself + * rather than associated with some ancestor. + * + * @param nodeInfo FileInfo + * @return The LockInfo if the node is locked, or null otherwise. + */ + private LockInfo getNodeLockInfoDirect(FileInfo nodeInfo) + { + LockInfo lock = getDAVLockService().getLockInfo(nodeInfo.getNodeRef()); + + if (lock == null) + { + return null; + } + + if (lock.isLocked()) + { + return lock; + } + return null; + } + + /** + * Checks whether a parent node has a lock that is valid for all its descendants. + * + * @param parent NodeRef + * @return The LockInfo if the node is locked, or null otherwise. + */ + private LockInfo getNodeLockInfoIndirect(NodeRef parent) + { + LockInfo parentLock = getDAVLockService().getLockInfo(parent); + + if (parentLock == null) + { + return null; + } + + // In this case node is locked indirectly. + if (parentLock.isLocked() && WebDAV.INFINITY.equals(parentLock.getDepth())) + { + // In this case node is locked indirectly. + // Get lock scope + // Get shared lock tokens + + // Store lock information to the lockInfo object + + // Get lock token of the locked node - this is indirect lock token. + + return parentLock; + } + return null; + + // No has no exclusive lock but can be locked with shared lock + // Check node lock depth. + // If depth is WebDAV.INFINITY then return this node's Lock token. + // In this case node is locked indirectly. + + // Get lock scope + + // Node has it's own Lock token. + } + + /** + * Get the file info for the given paths + * + * @param rootNodeRef the acting webdav root + * @param path the path to search for + * @return Return the file info for the path + * @throws FileNotFoundException if the path doesn't refer to a valid node + */ + protected FileInfo getNodeForPath(NodeRef rootNodeRef, String path) throws FileNotFoundException + { + return getDAVHelper().getNodeForPath(rootNodeRef, path); + } + + /** * Returns a URL that could be used to access the given path. + * + * @param request HttpServletRequest + * @param path the path to search for + * @param isFolder indicates file or folder is requested + * @return URL that could be used to access the given path + */ + protected String getURLForPath(HttpServletRequest request, String path, boolean isFolder) + { + return getDAVHelper().getURLForPath(request, path, isFolder, m_userAgent); + } + + /** + * Determines whether the XMLWriter should be flushed when XML is flushed. For some reason this is method specific. + * @return true if the XMLWriter should be flushed when XML is flushed + */ + protected boolean shouldFlushXMLWriter() + { + return true; + } + + /** + * Flushes all XML written so far to the response + * + * @param writer XMLWriter that should be flushed + */ + protected final void flushXML(XMLWriter writer) throws IOException + { + if (shouldFlushXMLWriter()) + { + writer.flush(); + } + + m_response.getWriter().write(m_xmlWriter.toCharArray()); + + m_xmlWriter.reset(); + } + + /** + * Returns a working copy of node for current user. + * + * @param nodeRef node reference + * @return Returns the working copy's file information + */ + protected FileInfo getWorkingCopy(NodeRef nodeRef) + { + FileInfo result = null; + NodeRef workingCopy = getServiceRegistry().getCheckOutCheckInService().getWorkingCopy(nodeRef); + if (workingCopy != null) + { + String workingCopyOwner = getNodeService().getProperty(workingCopy, ContentModel.PROP_WORKING_COPY_OWNER).toString(); + if (workingCopyOwner.equals(getAuthenticationService().getCurrentUserName())) + { + result = getFileFolderService().getFileInfo(workingCopy); + } + } + return result; + } + + /** + * Determines status code for AccessDeniedException based on client's HTTP headers. + * + * @return Returns status code + */ + protected int getStatusForAccessDeniedException() + { + if (m_request != null && m_request.getHeader(WebDAV.HEADER_USER_AGENT) != null) + { + String userAgent = m_request.getHeader(WebDAV.HEADER_USER_AGENT); + + for (Entry entry : accessDeniedStatusCodes.entrySet()) + { + if (Pattern.compile(entry.getKey()).matcher(userAgent).find()) + { + return entry.getValue(); + } + } + } + return HttpServletResponse.SC_UNAUTHORIZED; + } + + /** + * Class used for storing conditions which comes with "If" header of the request + * + * @author ivanry + */ + protected class Condition + { + // These tokens will be checked on equivalence against node's lock token + private LinkedList lockTokensMatch = new LinkedList(); + + // These tokens will be checked on non-equivalence against node's lock token + private LinkedList lockTokensNotMatch = new LinkedList(); + + // These ETags will be checked on equivalence against node's ETags + private LinkedList eTagsMatch; + + // These ETags will be checked on non-equivalence against node's ETags + private LinkedList eTagsNotMatch; + + /** + * Default constructor + * + */ + public Condition() + { + } + + /** + * Returns the list of lock tokens that should be checked against node's lock token on equivalence. + * + * @return lock tokens + */ + public LinkedList getLockTokensMatch() + { + return this.lockTokensMatch; + } + + /** + * Returns the list of lock tokens that should be checked against node's lock token on non-equivalence. + * + * @return lock tokens + */ + public LinkedList getLockTokensNotMatch() + { + return this.lockTokensNotMatch; + } + + /** + * Returns the list of ETags that should be checked against node's ETag on equivalence. + * + * @return ETags list + */ + public LinkedList getETagsMatch() + { + return this.eTagsMatch; + } + + /** + * Returns the list of ETags that should be checked against node's ETag on non-equivalence. + * + * @return ETags list + */ + public LinkedList getETagsNotMatch() + { + return this.eTagsNotMatch; + } + + /** + * Adds lock token to check + * + * @param lockToken String + * @param notMatch true is lock token should be added to the list matched tokens. + * false if should be added to the list of non-matches. + */ + public void addLockTocken(String lockToken, boolean notMatch) + { + if (notMatch) + { + this.lockTokensNotMatch.add(lockToken); + } + else + { + this.lockTokensMatch.add(lockToken); + } + } + + /** + * Add ETag to check + * + * @param eTag String + * @param notMatch true is ETag should be added to the list matched ETags. + * false if should be added to the list of non-matches. + */ + public void addETag(String eTag, boolean notMatch) + { + if (notMatch) + { + if (eTagsNotMatch == null) + { + eTagsNotMatch = new LinkedList(); + } + this.eTagsNotMatch.add(eTag); + } + else + { + if (eTagsMatch == null) + { + eTagsMatch = new LinkedList(); + } + this.eTagsMatch.add(eTag); + } + } + } + + public String toString() + { + StringBuffer sb = new StringBuffer(); + + if(m_request != null) + { + sb.append("WebDAV "); + sb.append(m_request.getMethod()); + sb.append(" request for "); + sb.append(m_strPath); + } + else + { + sb.append("Inactive WebDAV request via "); + String clz = getClass().getName(); + sb.append(clz.substring(clz.lastIndexOf('.')+1)); + } + + return sb.toString(); + } + + + /** + * Get the site ID (short-name) that the current request relates to. The site ID + * will be {@code DEFAULT_SITE_ID} if not specifically set. + * + * @return The site ID + */ + protected String getSiteId() + { + if (siteId == null) + { + siteId = getDAVHelper().determineSiteId(this); + } + return siteId; + } + + /** + * Get the tenant domain for the current user and request. The tenant domain + * will be {@link TenantService#DEFAULT_DOMAIN} if not specifically set. + * + * @return The tenant domain. + */ + protected String getTenantDomain() + { + if (tenantDomain == null) + { + tenantDomain = getDAVHelper().determineTenantDomain(this); + } + return tenantDomain; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVProperty.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVProperty.java new file mode 100644 index 00000000000..e53ec15aae6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVProperty.java @@ -0,0 +1,193 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import org.alfresco.service.namespace.QName; + +/** + * Class to represent a WebDAV property + * + * @author gavinc + */ +public class WebDAVProperty +{ + private String m_strName = null; + private String m_strNamespaceUri = WebDAV.DEFAULT_NAMESPACE_URI; + private String m_strNamespaceName = null; + private String m_strValue = null; + + /** + * Constructs a WebDAVProperty + * + * @param strName String + * @param strNamespaceUri String + * @param strNamespaceName String + * @param strValue String + */ + public WebDAVProperty(String strName, String strNamespaceUri, String strNamespaceName, String strValue) + { + this(strName, strNamespaceUri, strNamespaceName); + m_strValue = strValue; + } + + /** + * Constructs a WebDAVProperty + * + * @param strName String + * @param strNamespaceUri String + * @param strNamespaceName String + */ + public WebDAVProperty(String strName, String strNamespaceUri, String strNamespaceName) + { + this(strName); + + m_strNamespaceUri = strNamespaceUri; + m_strNamespaceName = strNamespaceName; + } + + /** + * Constructs a WebDAVProperty with the default namespace details + * + * @param strName String + */ + public WebDAVProperty(String strName) + { + m_strName = strName; + } + + /** + * Returns the name of the property + * + * @return The name of the property + */ + public String getName() + { + return m_strName; + } + + /** + * Returns the namespace URI for this property + * + * @return The namespace URI for this property + */ + public String getNamespaceUri() + { + return m_strNamespaceUri; + } + + /** + * Determine if the property has a namespace + * + * @return boolean + */ + public final boolean hasNamespaceName() + { + return m_strNamespaceName != null ? true : false; + } + + /** + * Returns the namespace name for this property + * + * @return The namespace name for this property + */ + public String getNamespaceName() + { + return m_strNamespaceName; + } + + /** + * Returns the value of this property + * + * @return The value of this property + */ + public String getValue() + { + return m_strValue; + } + + /** + * Sets the property's value + * + * @param strValue The new value + */ + public void setValue(String strValue) + { + m_strValue = strValue; + } + + + /** + * Creates QName of the property + * + * @return QName + */ + public QName createQName() + { + return QName.createQName(m_strNamespaceUri, m_strName); + } + + /** + * Returns true if property is protected according to the WebDav specification + * + * @return boolean + */ + public boolean isProtected() + { + return WebDAV.XML_GET_CONTENT_LENGTH.equals(m_strName) || + WebDAV.XML_GET_ETAG.equals(m_strName) || + WebDAV.XML_GET_LAST_MODIFIED.equals(m_strName) || + WebDAV.XML_LOCK_DISCOVERY.equals(m_strName) || + WebDAV.XML_RESOURCE_TYPE.equals(m_strName) || + WebDAV.XML_SUPPORTED_LOCK.equals(m_strName); + } + + + /** + * Return the property as a string + * + * @return String + */ + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("["); + + str.append(getName()); + str.append("="); + str.append(getValue()); + str.append(",URI="); + str.append(getNamespaceUri()); + + if ( hasNamespaceName()) + { + str.append(","); + str.append(getNamespaceName()); + } + + return str.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServerException.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServerException.java new file mode 100644 index 00000000000..b822d7551ba --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServerException.java @@ -0,0 +1,100 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import javax.servlet.http.HttpServletResponse; + +/** + * Exception class that represents an error in the WebDAV protocol layer + * + * @author gavinc + */ +public class WebDAVServerException extends Exception +{ + private static final long serialVersionUID = -2949418282738082368L; + + private int m_httpStatusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + private Throwable m_cause = null; + + /** + * Constructs a WebDAVException + * + * @param httpStatusCode The HTTP status code + */ + public WebDAVServerException(int httpStatusCode) + { + this(httpStatusCode, null); + } + + /** + * Constructs a WebDAVException + * + * @param httpStatusCode The HTTP status code + * @param cause The cause of this exception + */ + public WebDAVServerException(int httpStatusCode, Throwable cause) + { + super(Integer.toString(httpStatusCode)); + + m_httpStatusCode = httpStatusCode; + m_cause = cause; + } + + /** + * Returns the HTTP status code + * + * @return The HTTP status code + */ + public int getHttpStatusCode() + { + return m_httpStatusCode; + } + + /** + * Returns the cause of this exception + * + * @return The cause of this exception + */ + public Throwable getCause() + { + return m_cause; + } + + /** + * @see java.lang.Object#toString() + */ + public String toString() + { + String strErrorMsg = "HTTP Status Code: " + m_httpStatusCode; + + if (m_cause != null) + { + strErrorMsg = strErrorMsg + " caused by: " + m_cause.toString(); + } + + return strErrorMsg; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java new file mode 100644 index 00000000000..f586edf5e32 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java @@ -0,0 +1,487 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.List; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.transaction.UserTransaction; + +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.security.authentication.AuthenticationContext; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.activities.ActivityPoster; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.FileFilterMode; +import org.alfresco.util.FileFilterMode.Client; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * Servlet that accepts WebDAV requests for the hub. The request is served by the hub's content + * repository framework and the response sent back using the WebDAV protocol. + * + * @author gavinc + */ +public class WebDAVServlet extends HttpServlet +{ + private static final long serialVersionUID = 6900069445027527165L; + + // Logging + private static Log logger = LogFactory.getLog("org.alfresco.webdav.protocol"); + + // Constants + public static final String WEBDAV_PREFIX = "webdav"; + //private static final String INTERNAL_SERVER_ERROR = "Internal Server Error: "; + + // Init parameter names + private static final String BEAN_INIT_PARAMS = "webdav.initParams"; + + // Service registry, used by methods to find services to process requests + private ServiceRegistry serviceRegistry; + + private TransactionService transactionService; + private static TenantService tenantService; + private static NodeService nodeService; + private static SearchService searchService; + private static NamespaceService namespaceService; + + // WebDAV method handlers + protected Hashtable> m_davMethods; + + // note: cache is tenant-aware (if using TransctionalCache impl) + + private static SimpleCache singletonCache; // eg. for webdavRootNodeRef + private static final String KEY_WEBDAV_ROOT_NODEREF = "key.webdavRoot.noderef"; + + private static String rootPath; + + private static NodeRef defaultRootNode; // for default domain + + // WebDAV helper class + private WebDAVHelper m_davHelper; + private WebDAVActivityPoster activityPoster; + + private WebDAVInitParameters initParams; + + /** + * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, + * javax.servlet.http.HttpServletResponse) + */ + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, + IOException + { + if (!initParams.getEnabled()) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + long startTime = 0; + if (logger.isTraceEnabled()) + { + startTime = System.currentTimeMillis(); + } + + FileFilterMode.setClient(Client.webdav); + + try + { + // Create the appropriate WebDAV method for the request and execute it + final WebDAVMethod method = createMethod(request, response); + + if (method == null) + { + if ( logger.isErrorEnabled()) + logger.error("WebDAV method not implemented - " + request.getMethod()); + + // Return an error status + + response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); + return; + } + else if (method.getRootNodeRef() == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("No root node for request [" + + request.getMethod() + " " + request.getRequestURI() + "]"); + } + + // Return an error status + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + // Execute the WebDAV request, which must take care of its own transaction + method.execute(); + } + catch (Throwable e) + { + ExceptionHandler exHandler = new ExceptionHandler(e, request, response); + exHandler.handle(); + } + finally + { + if (logger.isTraceEnabled()) + { + logger.trace(request.getMethod() + " took " + (System.currentTimeMillis()-startTime) + "ms to execute ["+request.getRequestURI()+"]"); + } + + FileFilterMode.clearClient(); + } + } + + /** + * Create a WebDAV method handler + * + * @param request HttpServletRequest + * @param response HttpServletResponse + * @return WebDAVMethod + */ + protected WebDAVMethod createMethod(HttpServletRequest request, HttpServletResponse response) + { + // Get the type of the current request + + String strHttpMethod = request.getMethod(); + + if (logger.isDebugEnabled()) + logger.debug("WebDAV request " + strHttpMethod + " on path " + + request.getRequestURI()); + + Class methodClass = m_davMethods.get(strHttpMethod); + WebDAVMethod method = null; + + if (methodClass != null) + { + try + { + // Create the handler method + method = methodClass.newInstance(); + method.setDetails(request, response, m_davHelper, getRootNodeRef()); + + // A very few WebDAV methods produce activity posts. + if (method instanceof ActivityPostProducer) + { + ActivityPostProducer activityPostProducer = (ActivityPostProducer) method; + activityPostProducer.setActivityPoster(activityPoster); + } + } + catch (Exception ex) + { + // Debug + + if ( logger.isDebugEnabled()) + logger.debug(ex); + } + } + + // Return the WebDAV method handler, or null if not supported + + return method; + } + + private static NodeRef getRootNodeRef() + { + NodeRef rootNodeRef = singletonCache.get(KEY_WEBDAV_ROOT_NODEREF); + + if (rootNodeRef == null) + { + rootNodeRef = tenantService.getRootNode(nodeService, searchService, namespaceService, rootPath, defaultRootNode); + singletonCache.put(KEY_WEBDAV_ROOT_NODEREF, rootNodeRef); + } + + return rootNodeRef; + } + + /** + * Initialize the servlet + * + * @param config ServletConfig + * @exception ServletException + */ + @SuppressWarnings("unchecked") + public void init(ServletConfig config) throws ServletException + { + super.init(config); + + // Get service registry + WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); + + // If no context has been initialised, exit silently so config changes can be made + if (context == null) + { + return; + } + + // Get global configuration properties + WebApplicationContext wc = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + initParams = (WebDAVInitParameters) wc.getBean(BEAN_INIT_PARAMS); + + // Render this servlet permanently unavailable if its enablement property is not set + if (!initParams.getEnabled()) + { + logger.info("Marking servlet WebDAV as unavailable!"); + return; + } + + // Get root paths + + String storeValue = initParams.getStoreName(); + + rootPath = initParams.getRootPath(); + + // Get beans + + serviceRegistry = (ServiceRegistry)context.getBean(ServiceRegistry.SERVICE_REGISTRY); + + transactionService = serviceRegistry.getTransactionService(); + tenantService = (TenantService) context.getBean("tenantService"); + + nodeService = (NodeService) context.getBean("NodeService"); + searchService = (SearchService) context.getBean("SearchService"); + namespaceService = (NamespaceService) context.getBean("NamespaceService"); + ActivityPoster poster = (ActivityPoster) context.getBean("activitiesPoster"); + singletonCache = (SimpleCache)context.getBean("immutableSingletonCache"); + + + + // Collaborator used by WebDAV methods to create activity posts. + activityPoster = new ActivityPosterImpl("WebDAV", poster); + + // Create the WebDAV helper + m_davHelper = (WebDAVHelper) context.getBean("webDAVHelper"); + + // Initialize the root node + initializeRootNode(storeValue, rootPath, context, nodeService, searchService, namespaceService, tenantService, transactionService); + + // Create the WebDAV methods table + + m_davMethods = new Hashtable>(); + + m_davMethods.put(WebDAV.METHOD_PROPFIND, PropFindMethod.class); + m_davMethods.put(WebDAV.METHOD_PROPPATCH, PropPatchMethod.class); + m_davMethods.put(WebDAV.METHOD_COPY, CopyMethod.class); + m_davMethods.put(WebDAV.METHOD_DELETE, DeleteMethod.class); + m_davMethods.put(WebDAV.METHOD_GET, GetMethod.class); + m_davMethods.put(WebDAV.METHOD_HEAD, HeadMethod.class); + m_davMethods.put(WebDAV.METHOD_LOCK, LockMethod.class); + m_davMethods.put(WebDAV.METHOD_MKCOL, MkcolMethod.class); + m_davMethods.put(WebDAV.METHOD_MOVE, MoveMethod.class); + m_davMethods.put(WebDAV.METHOD_OPTIONS, OptionsMethod.class); + m_davMethods.put(WebDAV.METHOD_POST, PostMethod.class); + m_davMethods.put(WebDAV.METHOD_PUT, PutMethod.class); + m_davMethods.put(WebDAV.METHOD_UNLOCK, UnlockMethod.class); + } + + protected WebDAVHelper getDAVHelper() + { + return m_davHelper; + } + + + /** + * @param storeValue String + * @param rootPath String + * @param context WebApplicationContext + * @param nodeService NodeService + * @param searchService SearchService + * @param namespaceService NamespaceService + * @param tenantService TenantService + * @param m_transactionService TransactionService + */ + private void initializeRootNode(String storeValue, String rootPath, WebApplicationContext context, NodeService nodeService, SearchService searchService, + NamespaceService namespaceService, TenantService tenantService, TransactionService m_transactionService) + { + + // Use the system user as the authenticated context for the filesystem initialization + + AuthenticationContext authComponent = (AuthenticationContext) context.getBean("authenticationContext"); + authComponent.setSystemUserAsCurrentUser(); + + // Wrap the initialization in a transaction + + UserTransaction tx = m_transactionService.getUserTransaction(true); + + try + { + // Start the transaction + + if (tx != null) + tx.begin(); + + StoreRef storeRef = new StoreRef(storeValue); + + if (nodeService.exists(storeRef) == false) + { + throw new RuntimeException("No store for path: " + storeRef); + } + + NodeRef storeRootNodeRef = nodeService.getRootNode(storeRef); + + List nodeRefs = searchService.selectNodes(storeRootNodeRef, rootPath, null, namespaceService, false); + + if (nodeRefs.size() > 1) + { + throw new RuntimeException("Multiple possible children for : \n" + " path: " + rootPath + "\n" + " results: " + nodeRefs); + } + else if (nodeRefs.size() == 0) + { + throw new RuntimeException("Node is not found for : \n" + " root path: " + rootPath); + } + + defaultRootNode = nodeRefs.get(0); + + // Commit the transaction + if (tx != null) + tx.commit(); + } + catch (Exception ex) + { + logger.error(ex); + } + finally + { + // Clear the current system user + + authComponent.clearCurrentSecurityContext(); + } + } + + /** + * + * @return root node for WebDAV + */ + public static NodeRef getWebdavRootNode() + { + return getRootNodeRef(); + } + + /** + * Bean to hold injected initialization parameters. + * + * @author Derek Hulley + * @since V3.5 Team + */ + public static class WebDAVInitParameters + { + private boolean enabled = false; + private String storeName; + private String rootPath; + private String urlPathPrefix; + + public boolean getEnabled() + { + return enabled; + } + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + /** + * @return Returns the name of the store + * @throws ServletException if the store name was not set + */ + public String getStoreName() throws ServletException + { + if (!PropertyCheck.isValidPropertyString(storeName)) + { + throw new ServletException("WebDAV missing 'storeName' value."); + } + return storeName; + } + public void setStoreName(String storeName) + { + this.storeName = storeName; + } + /** + * @return Returns the WebDAV root path within the store + * @throws ServletException if the root path was not set + */ + public String getRootPath() throws ServletException + { + if (!PropertyCheck.isValidPropertyString(rootPath)) + { + throw new ServletException("WebDAV missing 'rootPath' value."); + } + return rootPath; + } + public void setRootPath(String rootPath) + { + this.rootPath = rootPath; + } + + /** + * Get the path prefix that generated URLs should exhibit, e.g. + *

+         *   http://server.name<prefix>/path/to/file.txt
+         * 
+ * In the default set up this would be of the form /context-path/servlet-name e.g. /alfresco/webdav: + *
+         *   http://server.name/alfresco/webdav/path/to/file.txt
+         * 
+ * however if using URL rewriting rules or a reverse proxy in front of the webdav server + * you may choose to use, for example / for shorter URLs. + *
+         *   http://server.name/path/to/file.txt
+         * 
+ *

+ * Leaving this property blank will cause the prefix used to be /context-path/servlet-name + * + * @return the urlPathPrefix + */ + public String getUrlPathPrefix() + { + return urlPathPrefix; + } + + /** + * See {@link #getUrlPathPrefix()} + * + * @param urlPathPrefix String + */ + public void setUrlPathPrefix(String urlPathPrefix) + { + this.urlPathPrefix = urlPathPrefix; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVSessionListener.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVSessionListener.java new file mode 100644 index 00000000000..5c9c77c2088 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVSessionListener.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + *

WebDAVSessionListener is used to forcibly unlock documents that were + * persistently locked during user's session and were not unlocked because of some extraordinary + * situations such as network connection lost. Was introduced in ALF-11777 jira issue. + *

+ * + * @author Pavel.Yurkevich + * + */ +public class WebDAVSessionListener implements HttpSessionListener, ServletContextListener +{ + private static Log logger = LogFactory.getLog(WebDAVSessionListener.class); + private WebDAVLockService webDAVLockService; + + /** + * @param webDAVLockService + * the webDAVLockService to set + */ + public void setWebDAVLockService(WebDAVLockService webDAVLockService) + { + this.webDAVLockService = webDAVLockService; + } + + @Override + public void sessionCreated(HttpSessionEvent hse) + { + if (logger.isDebugEnabled()) + { + logger.debug("Session created " + hse.getSession().getId()); + } + } + + @SuppressWarnings("unchecked") + @Override + public void sessionDestroyed(HttpSessionEvent hse) + { + webDAVLockService.setCurrentSession(hse.getSession()); + webDAVLockService.sessionDestroyed(); + + if (logger.isDebugEnabled()) + { + logger.debug("Session destroyed " + hse.getSession().getId()); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + } + + @Override + public void contextInitialized(ServletContextEvent sce) + { + ApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext()); + this.webDAVLockService = (WebDAVLockService)context.getBean(WebDAVLockService.BEAN_NAME); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/AuthenticationDriver.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/AuthenticationDriver.java new file mode 100644 index 00000000000..829f4914fdd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/AuthenticationDriver.java @@ -0,0 +1,75 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A general interface for servlet-based authentication. Allows code to be shared by Web Client, WebDAV and Sharepoint + * authentication classes. + * + * @author dward + */ +public interface AuthenticationDriver +{ + public static final String AUTHENTICATION_USER = "_alfAuthTicket"; + + /** + * Authenticate user based on information in http request such as Authorization header or cached session + * information. + * + * @param context + * the context + * @param request + * http request + * @param response + * http response + * @return true if authentication was successful + * @throws IOException + * @throws ServletException + */ + public boolean authenticateRequest(ServletContext context, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException; + + /** + * Send a status 401 response that will restart the log in handshake. + * + * @param context + * the context + * @param request + * http request + * @param response + * http response + * @throws IOException + */ + public void restartLoginChallenge(ServletContext context, HttpServletRequest request, HttpServletResponse response) + throws IOException; +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/AuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/AuthenticationFilter.java new file mode 100644 index 00000000000..f1785b95048 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/AuthenticationFilter.java @@ -0,0 +1,331 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.Authorization; +import org.alfresco.repo.web.auth.BasicAuthCredentials; +import org.alfresco.repo.web.auth.TicketCredentials; +import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter; +import org.alfresco.repo.webdav.WebDAV; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * WebDAV Authentication Filter Class + * + * @author GKSpencer + */ +public class AuthenticationFilter extends BaseAuthenticationFilter implements DependencyInjectedFilter +{ + // Debug logging + + private static Log logger = LogFactory.getLog(AuthenticationFilter.class); + + // Authenticated user session object name + + private static final String PPT_EXTN = ".ppt"; + + /** The password encodings to try in priority order **/ + private static final String[] ENCODINGS; + + static + { + String[] encodings = new String[] { + "UTF-8", + System.getProperty("file.encoding"), + "ISO-8859-1" + }; + + Set encodingsSet = new LinkedHashSet(); + for (String encoding : encodings) + { + encodingsSet.add(encoding); + } + ENCODINGS = new String[encodingsSet.size()]; + encodingsSet.toArray(ENCODINGS); + } + + // Various services required by NTLM authenticator + + /** + * Run the authentication filter + * + * @param context ServletContext + * @param req ServletRequest + * @param resp ServletResponse + * @param chain FilterChain + * @exception ServletException + * @exception IOException + */ + @Override + public void doFilter(ServletContext context, ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException + { + try + { + doFilterInternal(context, req, resp, chain); + } + finally + { + if (logger.isTraceEnabled()) + { + logger.debug("About to clear the security context"); + } + AuthenticationUtil.clearCurrentSecurityContext(); + } + } + protected void doFilterInternal(ServletContext context, ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException + { + if (logger.isTraceEnabled()) + { + logger.trace("Entering AuthenticationFilter."); + } + + // Assume it's an HTTP request + + HttpServletRequest httpReq = (HttpServletRequest) req; + HttpServletResponse httpResp = (HttpServletResponse) resp; + + // Get the user details object from the session + SessionUser user = getSessionUser(context, httpReq, httpResp, false); + + if (user == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("There is no user in the session."); + } + // Get the authorization header + + String authHdr = httpReq.getHeader("Authorization"); + + if ( authHdr != null && authHdr.length() > 5 && authHdr.substring(0,5).equalsIgnoreCase("BASIC")) + { + if (logger.isDebugEnabled()) + { + logger.debug("Basic authentication details present in the header."); + } + byte[] encodedString = Base64.decodeBase64(authHdr.substring(5).getBytes()); + + // ALF-13621: Due to browser inconsistencies we have to try a fallback path of encodings + Set attemptedAuths = new HashSet(ENCODINGS.length * 2); + for (String encoding : ENCODINGS) + { + CharsetDecoder decoder = Charset.forName(encoding).newDecoder() + .onMalformedInput(CodingErrorAction.REPORT); + try + { + // Attempt to decode using this charset + String basicAuth = decoder.decode(ByteBuffer.wrap(encodedString)).toString(); + + // It decoded OK but we may already have tried this string. + if (!attemptedAuths.add(basicAuth)) + { + // Already tried - no need to try again + continue; + } + + String username = null; + String password = null; + + // Split the username and password + int pos = basicAuth.indexOf(":"); + if (pos != -1) + { + username = basicAuth.substring(0, pos); + password = basicAuth.substring(pos + 1); + } + else + { + username = basicAuth; + password = ""; + } + + // Go to the repo and authenticate + Authorization auth = new Authorization(username, password); + if (auth.isTicket()) + { + authenticationService.validate(auth.getTicket()); + } + else + { + authenticationService.authenticate(username, password.toCharArray()); + if(authenticationListener != null) + { + authenticationListener.userAuthenticated(new BasicAuthCredentials(username, password)); + } + } + + user = createUserEnvironment(httpReq.getSession(), authenticationService.getCurrentUserName(), authenticationService.getCurrentTicket(), false); + if (logger.isTraceEnabled()) + { + logger.trace("Successfully created user environment, login using basic auth or ROLE_TICKET for user: " + + AuthenticationUtil.maskUsername(user.getUserName())); + } + // Success so break out + break; + } + catch (CharacterCodingException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Didn't decode using " + decoder.getClass().getName(), e); + } + } + catch (AuthenticationException ex) + { + if (logger.isDebugEnabled()) + { + logger.debug("Authentication error ", ex); + } + } + catch (NoSuchPersonException e) + { + if (logger.isDebugEnabled()) + { + logger.debug("There is no such person error ", e); + } + } + } + } + else + { + // Check if the request includes an authentication ticket + + String ticket = req.getParameter(ARG_TICKET); + + if (ticket != null && ticket.length() > 0) + { + // PowerPoint bug fix + if (ticket.endsWith(PPT_EXTN)) + { + ticket = ticket.substring(0, ticket.length() - PPT_EXTN.length()); + } + + if (logger.isTraceEnabled()) + { + logger.trace("Logon via ticket from " + req.getRemoteHost() + + " (" + req.getRemoteAddr() + ":" + req.getRemotePort() + ")" + + " ticket=" + ticket); + } + + // Validate the ticket + authenticationService.validate(ticket); + if(authenticationListener != null) + { + authenticationListener.userAuthenticated(new TicketCredentials(ticket)); + } + + // Need to create the User instance if not already available + + String currentUsername = authenticationService.getCurrentUserName(); + + user = createUserEnvironment(httpReq.getSession(), currentUsername, ticket, false); + if (logger.isTraceEnabled()) + { + logger.trace("Successfully created user environment, login using TICKET for user: " + + AuthenticationUtil.maskUsername(user.getUserName())); + } + } + } + + // Check if the user is authenticated, if not then prompt again + + if (user == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("No user/ticket, force the client to prompt for logon details."); + } + + httpResp.setHeader("WWW-Authenticate", "BASIC realm=\"Alfresco DAV Server\""); + httpResp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + httpResp.flushBuffer(); + return; + } + } + else + { + if(authenticationListener != null) + { + authenticationListener.userAuthenticated(new TicketCredentials(user.getTicket())); + } + if (logger.isTraceEnabled()) + { + logger.trace("User already set to: " + AuthenticationUtil.maskUsername(user.getUserName())); + } + } + + // Chain other filters + + chain.doFilter(req, resp); + } + + /** + * Cleanup filter resources + */ + public void destroy() + { + // Nothing to do + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseAuthenticationFilter#getLogger() + */ + @Override + protected Log getLogger() + { + return logger; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseAuthenticationFilter.java new file mode 100644 index 00000000000..c6eaa1da15d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseAuthenticationFilter.java @@ -0,0 +1,522 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.io.Reader; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.management.subsystems.ActivateableBean; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.external.RemoteUserMapper; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.web.auth.AuthenticationListener; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A base class for authentication filters. Handles management of the session user. + * + * @author dward + */ +public abstract class BaseAuthenticationFilter +{ + /** Indication by an up-stream filter that no authentication checks are required. */ + protected static final String NO_AUTH_REQUIRED = "alfNoAuthRequired"; + + /** The default session attribute used to cache the user. Subclasses may override this with {@link #setUserAttributeName(String)}. */ + public static final String AUTHENTICATION_USER = "_alfDAVAuthTicket"; + + /** The session attribute that indicates external authentication. */ + private static final String LOGIN_EXTERNAL_AUTH = "_alfExternalAuth"; + + /** The name of the ticket argument. */ + protected static final String ARG_TICKET = "ticket"; + + /** The authentication service. */ + protected AuthenticationService authenticationService; + + /** The person service. */ + protected PersonService personService; + + /** The node service. */ + protected NodeService nodeService; + + /** The transaction service. */ + protected TransactionService transactionService; + + /** The authentication component. */ + protected AuthenticationComponent authenticationComponent; + + /** The remote user mapper. */ + protected RemoteUserMapper remoteUserMapper; + + /** The authentication listener. */ + protected AuthenticationListener authenticationListener; + + /** The configured user attribute name. */ + private String userAttributeName = AUTHENTICATION_USER; + + + + /** + * Sets the authentication service. + * + * @param authenticationService + * the authService to set + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * Sets the person service. + * + * @param personService + * the personService to set + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * Sets the node service. + * + * @param nodeService + * the nodeService to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the transaction service. + * + * @param transactionService + * the transactionService to set + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Sets the authentication component. + * + * @param authenticationComponent + * the authentication component + */ + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + /** + * Sets the authentication listener. + * + * @param authenticationListener AuthenticationListener + */ + public void setAuthenticationListener(AuthenticationListener authenticationListener) + { + this.authenticationListener = authenticationListener; + } + + /** + * Sets the remote user mapper. + * + * @param remoteUserMapper + * the remote user mapper + */ + public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper) + { + this.remoteUserMapper = remoteUserMapper; + } + + /** + * Create the user object that will be stored in the session. + * + * @param userName + * String + * @param ticket + * String + * @param personNode + * NodeRef + * @param homeSpaceRef + * NodeRef + * @return SessionUser + */ + protected SessionUser createUserObject(String userName, String ticket, NodeRef personNode, NodeRef homeSpaceRef) + { + return new WebDAVUser(userName, ticket, homeSpaceRef); + } + + /** + * Callback to get the specific impl of the Session User for a filter. + * + * @param servletContext + * the servlet context + * @param httpServletRequest + * the http servlet request + * @param httpServletResponse + * the http servlet response + * @param externalAuth + * has the user been authenticated by SSO? + * @return User from the session + */ + protected SessionUser getSessionUser(ServletContext servletContext, final HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, final boolean externalAuth) + { + String userId = null; + + // If the remote user mapper is configured, we may be able to map in an externally authenticated user + if (remoteUserMapper != null + && (!(remoteUserMapper instanceof ActivateableBean) || ((ActivateableBean) remoteUserMapper).isActive())) + { + userId = remoteUserMapper.getRemoteUser(httpServletRequest); + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Found a remote user: " + AuthenticationUtil.maskUsername(userId)); + } + } + + String sessionAttrib = getUserAttributeName(); + HttpSession session = httpServletRequest.getSession(); + SessionUser sessionUser = (SessionUser) session.getAttribute(sessionAttrib); + if (sessionUser != null) + { + try + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Found a session user: " + AuthenticationUtil.maskUsername(sessionUser.getUserName())); + } + authenticationService.validate(sessionUser.getTicket()); + setExternalAuth(session, externalAuth); + } + catch (AuthenticationException e) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("The ticket may have expired or the person could have been removed, invalidating session.", e); + } + invalidateSession(httpServletRequest); + sessionUser = null; + } + } + + if (userId != null) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("We have a previously-cached user with the wrong identity - replace them."); + } + if (sessionUser != null && !sessionUser.getUserName().equals(userId)) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Removing the session user, invalidating session."); + } + session.removeAttribute(sessionAttrib); + session.invalidate(); + sessionUser = null; + } + + if (sessionUser == null) + { + // If we have been authenticated by other means, just propagate through the user identity + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Propagating through the user identity: " + AuthenticationUtil.maskUsername(userId)); + } + authenticationComponent.setCurrentUser(userId); + session = httpServletRequest.getSession(); + + try + { + sessionUser = createUserEnvironment(session, authenticationService.getCurrentUserName(), authenticationService.getCurrentTicket(), true); + } + catch (Throwable e) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Error during ticket validation and user creation: " + e.getMessage(), e); + } + } + } + } + + return sessionUser; + } + + /** + * Remove the user from the session and expire the session - after failed ticket auth. + * + * @param req HttpServletRequest + */ + protected void invalidateSession(HttpServletRequest req) + { + HttpSession session = req.getSession(false); + if (session != null) + { + setExternalAuth(session, false); + session.removeAttribute(getUserAttributeName()); + session.invalidate(); + } + } + + /** + * Executes a callback in a transaction as the system user + * + * @param callback + * the callback + * @return the return value from the callback + */ + protected T doInSystemTransaction(final RetryingTransactionHelper.RetryingTransactionCallback callback) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public T doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction(callback, transactionService.isReadOnly()); + } + }, AuthenticationUtil.SYSTEM_USER_NAME); + } + + /** + * Return the user object session attribute name. + * + * @return the user object session attribute name + */ + protected final String getUserAttributeName() + { + return userAttributeName; + } + + /** + * Set the user object attribute name. + * + * @param userAttr + * the user object session attribute name + */ + protected final void setUserAttributeName(String userAttr) + { + userAttributeName = userAttr; + } + + /** + * Callback to create the User environment as appropriate for a filter impl. + * + * @param session + * HttpSession + * @param userName + * String + * @param ticket + * the ticket + * @param externalAuth + * has the user been authenticated by SSO? + * @return SessionUser + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws ServletException + * the servlet exception + */ + protected SessionUser createUserEnvironment(HttpSession session, final String userName, final String ticket, boolean externalAuth) + throws IOException, ServletException + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Create the User environment for: " + AuthenticationUtil.maskUsername(userName)); + } + SessionUser user = doInSystemTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public SessionUser execute() throws Throwable + { + // Setup User object and Home space ID etc. + final NodeRef personNodeRef = personService.getPerson(userName); + + String name = (String) nodeService.getProperty(personNodeRef, ContentModel.PROP_USERNAME); + + NodeRef homeSpaceRef = (NodeRef) nodeService.getProperty(personNodeRef, ContentModel.PROP_HOMEFOLDER); + + return createUserObject(name, ticket, personNodeRef, homeSpaceRef); + } + }); + + // Store the user on the session + session.setAttribute(getUserAttributeName(), user); + setExternalAuth(session, externalAuth); + return user; + } + + private void setExternalAuth(HttpSession session, boolean externalAuth) + { + if (externalAuth) + { + session.setAttribute(LOGIN_EXTERNAL_AUTH, Boolean.TRUE); + } + else + { + session.removeAttribute(LOGIN_EXTERNAL_AUTH); + } + } + + /** + * Callback to create the User environment as appropriate for a filter impl + * + * @param session + * HttpSession + * @param userName + * String + * @return SessionUser + * @throws IOException + * @throws ServletException + */ + protected SessionUser createUserEnvironment(final HttpSession session, final String userName) throws IOException, + ServletException + { + return this.transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionHelper.RetryingTransactionCallback() + { + + public SessionUser execute() throws Throwable + { + authenticationComponent.setCurrentUser(userName); + return createUserEnvironment(session, userName, authenticationService.getCurrentTicket(), true); + } + }, transactionService.isReadOnly()); + } + + /** + * Return the logger. + * + * @return Log + */ + protected abstract Log getLogger(); + + /** + * Handles the login form directly, allowing management of the session user. + * + * @param req + * the request + * @param res + * the response + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws ServletException + * on error + */ + protected boolean handleLoginForm(HttpServletRequest req, HttpServletResponse res) throws IOException, + ServletException + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Handling the login form."); + } + // Invalidate current session + HttpSession session = req.getSession(false); + if (session != null) + { + session.invalidate(); + } + StringBuilder out = new StringBuilder(1024); + Reader in = req.getReader(); + char[] buff = new char[1024]; + int charsRead; + while ((charsRead = in.read(buff)) != -1) + { + out.append(buff, 0, charsRead); + } + in.close(); + + try + { + JSONObject json = new JSONObject(out.toString()); + String username = json.getString("username"); + String password = json.getString("password"); + + if (username == null || username.length() == 0) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Username not specified in the login form."); + } + res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Username not specified"); + return false; + } + + if (password == null) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Password not specified in the login form."); + } + res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Password not specified"); + return false; + } + + authenticationService.authenticate(username, password.toCharArray()); + session = req.getSession(); + createUserEnvironment(session, username, authenticationService.getCurrentTicket(), false); + res.setStatus(HttpServletResponse.SC_NO_CONTENT); + return true; + } + catch (AuthenticationException e) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Login failed", e); + } + res.sendError(HttpServletResponse.SC_FORBIDDEN, "Login failed"); + } + catch (JSONException jErr) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Unable to parse JSON POST body", jErr); + } + res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unable to parse JSON POST body: " + jErr.getMessage()); + } + return false; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseKerberosAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseKerberosAuthenticationFilter.java new file mode 100644 index 00000000000..2b0bfd70962 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseKerberosAuthenticationFilter.java @@ -0,0 +1,715 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.Principal; +import java.util.Vector; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.security.sasl.RealmCallback; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.alfresco.jlan.server.auth.kerberos.KerberosDetails; +import org.alfresco.jlan.server.auth.kerberos.SessionSetupPrivilegedAction; +import org.alfresco.jlan.server.auth.spnego.NegTokenInit; +import org.alfresco.jlan.server.auth.spnego.NegTokenTarg; +import org.alfresco.jlan.server.auth.spnego.OID; +import org.alfresco.jlan.server.auth.spnego.SPNEGO; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.auth.KerberosCredentials; +import org.alfresco.repo.web.auth.TicketCredentials; +import org.alfresco.repo.web.auth.WebCredentials; +import org.apache.commons.codec.binary.Base64; +import org.ietf.jgss.Oid; + +/** + * Base class with common code and initialisation for Kerberos authentication filters. + * + * @author gkspencer + */ +public abstract class BaseKerberosAuthenticationFilter extends BaseSSOAuthenticationFilter implements CallbackHandler { + + // Constants + // + // Default login configuration entry name + + private static final String LoginConfigEntry = "AlfrescoHTTP"; + + // Kerberos settings + // + // Account name and password for server ticket + // + // The account name must be built from the HTTP server name, in the format :- + // + // HTTP/@ + + private String m_accountName; + private String m_password; + + // Kerberos realm + + private String m_krbRealm; + + // Login configuration entry name + + private String m_loginEntryName = LoginConfigEntry; + + // Server login context + + private LoginContext m_loginContext; + + // Should we strip the @domain suffix from the Kerberos username? + + private boolean m_stripKerberosUsernameSuffix = true; + + /** + * Sets the HTTP service account password. (the Principal should be configured in java.login.config) + * + * @param password + * the password to set + */ + public void setPassword(String password) + { + this.m_password = password; + } + + /** + * Sets the HTTP service account realm. + * + * @param realm the realm to set + */ + public void setRealm(String realm) + { + m_krbRealm = realm; + } + + /** + * Sets the HTTP service login configuration entry name. The default is "AlfrescoHTTP". + * + * @param jaasConfigEntryName + * the jaasConfigEntryName to set + */ + public void setJaasConfigEntryName(String jaasConfigEntryName) + { + m_loginEntryName = jaasConfigEntryName; + } + + /** + * Indicates whether the @domain suffix should be removed from Kerberos user IDs + * + * @param stripKerberosUsernameSuffix + * true if the @domain suffix should be removed from Kerberos user IDs + */ + public void setStripKerberosUsernameSuffix(boolean stripKerberosUsernameSuffix) + { + m_stripKerberosUsernameSuffix = stripKerberosUsernameSuffix; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseSSOAuthenticationFilter#init() + */ + @Override + protected void init() throws ServletException + { + super.init(); + + if ( m_krbRealm == null) + { + throw new ServletException("Kerberos realm not specified"); + } + + if ( m_password == null) + { + throw new ServletException("HTTP service account password not specified"); + } + + if (m_loginEntryName == null) + { + throw new ServletException("Invalid login entry specified"); + } + + // Get the local host name + String localName = null; + + try + { + localName = InetAddress.getLocalHost().getCanonicalHostName(); + } + catch ( UnknownHostException ex) + { + throw new ServletException( "Failed to get local host name"); + } + + // Create a login context for the HTTP server service + + try + { + // Login the HTTP server service + + m_loginContext = new LoginContext( m_loginEntryName, this); + m_loginContext.login(); + + // DEBUG + + if ( getLogger().isTraceEnabled()) + { + getLogger().trace("HTTP Kerberos login successful"); + } + } + catch ( LoginException ex) + { + getLogger().error("HTTP Kerberos web filter error", ex); + + throw new ServletException("Failed to login HTTP server service"); + } + + // Get the HTTP service account name from the subject + + Subject subj = m_loginContext.getSubject(); + Principal princ = subj.getPrincipals().iterator().next(); + + m_accountName = princ.getName(); + + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Logged on using principal " + AuthenticationUtil.maskUsername(m_accountName)); + } + + // Create the Oid list for the SPNEGO NegTokenInit, include NTLMSSP for fallback + + Vector mechTypes = new Vector(); + + mechTypes.add(OID.KERBEROS5); + mechTypes.add(OID.MSKERBEROS5); + + // Build the SPNEGO NegTokenInit blob + + try + { + // Build the mechListMIC principle + // + // Note: This field is not as specified + + String mecListMIC = null; + + StringBuilder mic = new StringBuilder(); + mic.append( localName); + mic.append("$@"); + mic.append( m_krbRealm); + + mecListMIC = mic.toString(); + + // Build the SPNEGO NegTokenInit that contains the authentication types that the HTTP server accepts + + NegTokenInit negTokenInit = new NegTokenInit(mechTypes, mecListMIC); + + // Encode the NegTokenInit blob + negTokenInit.encode(); + } + catch (IOException ex) + { + getLogger().error("Error creating SPNEGO NegTokenInit blob", ex); + + throw new ServletException("Failed to create SPNEGO NegTokenInit blob"); + } + } + + + public boolean authenticateRequest(ServletContext context, HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException + { + // Check if there is an authorization header with an SPNEGO security blob + + String authHdr = req.getHeader("Authorization"); + boolean reqAuth = false; + + if ( authHdr != null) + { + // Check for a Kerberos/SPNEGO authorization header + + if ( authHdr.startsWith( "Negotiate")) + reqAuth = true; + else if ( authHdr.startsWith( "NTLM")) + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Received NTLM logon from client"); + } + + // Restart the authentication + + restartLoginChallenge(context, req, resp); + return false; + } + else if (isFallbackEnabled()) + { + return performFallbackAuthentication(context, req, resp); + } + } + + // Check if the user is already authenticated + SessionUser user = getSessionUser(context, req, resp, true); + HttpSession httpSess = req.getSession(true); + if (user == null) + { + user = (SessionUser) httpSess.getAttribute("_alfAuthTicket"); + // MNT-13191 Opening /alfresco/webdav from a Kerberos-authenticated IE11 browser causes HTTP error 500 + if (user != null) + { + String userName = user.getUserName(); + AuthenticationUtil.setFullyAuthenticatedUser(userName); + } + } + + // If the user has been validated and we do not require re-authentication then continue to + // the next filter + if ( user != null && reqAuth == false) + { + // Filter validate hook + onValidate( context, req, resp, new TicketCredentials(user.getTicket())); + + if ( getLogger().isTraceEnabled()) + { + getLogger().trace("Authentication not required (user), chaining ..."); + } + // Chain to the next filter + return true; + } + + // Check if the login page is being accessed, do not intercept the login page + if (checkLoginPage(req, resp)) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Login page requested, chaining ..."); + } + + // Chain to the next filter + + return true; + } + + // Check the authorization header + + if ( authHdr == null) { + + // If ticket based logons are allowed, check for a ticket parameter + + if ( allowsTicketLogons()) + { + // Check if a ticket parameter has been specified in the reuqest + + if (checkForTicketParameter(context, req, resp)) + { + // Filter validate hook + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Authenticated with a ticket parameter."); + } + + if (user == null) + { + user = (SessionUser) httpSess.getAttribute(getUserAttributeName()); + } + onValidate( context, req, resp, new TicketCredentials(user.getTicket())); + + // Chain to the next filter + + return true; + } + } + + if (getLogger().isTraceEnabled()) + { + getLogger() + .trace("New Kerberos auth request from " + req.getRemoteHost() + " (" + req.getRemoteAddr() + ":" + req.getRemotePort() + ")"); + } + + // Send back a request for SPNEGO authentication + logonStartAgain(context, req, resp, true); + + return false; + } + else + { + // Decode the received SPNEGO blob and validate + + final byte[] spnegoByts = Base64.decodeBase64( authHdr.substring(10).getBytes()); + + // Check if the client sent an NTLMSSP blob + + if ( isNTLMSSPBlob( spnegoByts, 0)) + { + if ( getLogger().isTraceEnabled()) + { + getLogger().trace("Client sent an NTLMSSP security blob"); + } + + // Restart the authentication + + restartLoginChallenge(context, req, resp); + return false; + } + + // Check the received SPNEGO token type + + int tokType = -1; + + try + { + tokType = SPNEGO.checkTokenType( spnegoByts, 0, spnegoByts.length); + } + catch ( IOException ex) + { + } + + // Check for a NegTokenInit blob + + if ( tokType == SPNEGO.NegTokenInit) + { + // Parse the SPNEGO security blob to get the Kerberos ticket + + NegTokenInit negToken = new NegTokenInit(); + + try + { + // Decode the security blob + + negToken.decode( spnegoByts, 0, spnegoByts.length); + + // Determine the authentication mechanism the client is using and logon + + String oidStr = null; + if ( negToken.numberOfOids() > 0) + oidStr = negToken.getOidAt( 0).toString(); + + if ( oidStr != null && (oidStr.equals( OID.ID_MSKERBEROS5) || oidStr.equals(OID.ID_KERBEROS5))) + { + // Kerberos logon + + try + { + NegTokenTarg negTokenTarg = doKerberosLogon( negToken, req, resp, httpSess); + if ( negTokenTarg != null) + { + // Allow the user to access the requested page + onValidate(context, req, resp, new KerberosCredentials(negToken, negTokenTarg)); + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Authenticated through Kerberos."); + } + return true; + } + else + { + // Send back a request for SPNEGO authentication + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Failed SPNEGO authentication."); + } + restartLoginChallenge(context, req, resp); + return false; + } + } + catch (AuthenticationException ex) + { + // Even though the user successfully authenticated, the ticket may not be granted, e.g. to + // max user limit + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Validate failed.", ex); + } + WebCredentials webCredentials = user == null ? null : new TicketCredentials(user.getTicket()); + onValidateFailed(context, req, resp, httpSess, webCredentials); + return false; + } + } + else + { + // Unsupported mechanism, e.g. NegoEx + + if ( getLogger().isDebugEnabled()) + { + getLogger().debug("Unsupported SPNEGO mechanism " + oidStr); + } + // Try again! + + restartLoginChallenge(context, req, resp); + } + } + catch ( IOException ex) + { + // Log the error + if ( getLogger().isDebugEnabled()) + { + getLogger().debug(ex); + } + } + } + else + { + // Unknown SPNEGO token type + + if ( getLogger().isDebugEnabled()) + { + getLogger().debug("Unknown SPNEGO token type"); + } + // Send back a request for SPNEGO authentication + + restartLoginChallenge(context, req, resp); + } + } + return false; + } + + protected boolean checkLoginPage(HttpServletRequest req, HttpServletResponse resp) + { + return (hasLoginPage() && req.getRequestURI().endsWith(getLoginPage())); + } + + /** + * JAAS callback handler + * + * @param callbacks Callback[] + * @exception IOException + * @exception UnsupportedCallbackException + */ + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException + { + // Process the callback list + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Processing the JAAS callback list of " + callbacks.length + " items."); + } + for (int i = 0; i < callbacks.length; i++) + { + // Request for user name + + if (callbacks[i] instanceof NameCallback) + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Request for user name."); + } + NameCallback cb = (NameCallback) callbacks[i]; + cb.setName(m_accountName); + } + + // Request for password + else if (callbacks[i] instanceof PasswordCallback) + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Request for password."); + } + PasswordCallback cb = (PasswordCallback) callbacks[i]; + cb.setPassword(m_password.toCharArray()); + } + + // Request for realm + + else if (callbacks[i] instanceof RealmCallback) + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Request for realm."); + } + RealmCallback cb = (RealmCallback) callbacks[i]; + cb.setText(m_krbRealm); + } + else + { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + } + + /** + * Perform a Kerberos login and return an SPNEGO response + * + * @param negToken NegTokenInit + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @param httpSess HttpSession + * @return NegTokenTarg + */ + private final NegTokenTarg doKerberosLogon( NegTokenInit negToken, HttpServletRequest req, HttpServletResponse resp, HttpSession httpSess) + { + // Authenticate the user + + KerberosDetails krbDetails = null; + String userName = null; + NegTokenTarg negTokenTarg = null; + + try + { + // Run the session setup as a privileged action + + SessionSetupPrivilegedAction sessSetupAction = new SessionSetupPrivilegedAction( m_accountName, negToken.getMechtoken()); + Object result = Subject.doAs( m_loginContext.getSubject(), sessSetupAction); + + if ( result != null) + { + // Access the Kerberos response + + krbDetails = (KerberosDetails) result; + userName = m_stripKerberosUsernameSuffix ? krbDetails.getUserName() : krbDetails.getSourceName(); + + // Create the NegTokenTarg response blob + + negTokenTarg = new NegTokenTarg( SPNEGO.AcceptCompleted, OID.KERBEROS5, krbDetails.getResponseToken()); + + // Check if the user has been authenticated, if so then setup the user environment + + if ( negTokenTarg != null) + { + // Create and store the user authentication context + + SessionUser user = createUserEnvironment(httpSess, userName); + + // Debug + + if (getLogger().isTraceEnabled()) + { + getLogger().trace("User " + AuthenticationUtil.maskUsername(user.getUserName()) + " logged on via Kerberos"); + } + } + } + else + { + // Debug + + if ( getLogger().isDebugEnabled()) + { + getLogger().debug("No SPNEGO response, Kerberos logon failed"); + } + } + } + catch (AuthenticationException ex) + { + // Pass on validation failures + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Failed to validate user " + AuthenticationUtil.maskUsername(userName), ex); + } + + throw ex; + } + catch (Exception ex) + { + // Log the error + if ( getLogger().isDebugEnabled()) + { + getLogger().debug("Kerberos logon error", ex); + } + } + + // Return the response SPNEGO blob + + return negTokenTarg; + } + + + /** + * Restart the Kerberos logon process + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @throws IOException + */ + public void restartLoginChallenge(ServletContext context, HttpServletRequest req, HttpServletResponse resp) throws IOException + { + HttpSession session = req.getSession(false); + if (session != null) + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Clearing session."); + } + session.invalidate(); + } + logonStartAgain(context, req, resp); + } + + /** + * The logon to start again + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @throws IOException + */ + public void logonStartAgain(ServletContext context, HttpServletRequest req, HttpServletResponse resp) throws IOException + { + logonStartAgain(context, req, resp, false); + } + + /** + * The logon to start again + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @param ignoreFallback ignore fallback + * @throws IOException + */ + private void logonStartAgain(ServletContext context, HttpServletRequest req, HttpServletResponse resp, boolean ignoreFallback) throws IOException + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Issuing login challenge to browser."); + } + // Force the logon to start again + resp.setHeader("WWW-Authenticate", "Negotiate"); + + if (!ignoreFallback && isFallbackEnabled()) + { + includeFallbackAuth(context, req, resp); + } + + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + writeLoginPageLink(context, req, resp); + resp.flushBuffer(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseSSOAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseSSOAuthenticationFilter.java new file mode 100644 index 00000000000..3c4bcdb90f5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/BaseSSOAuthenticationFilter.java @@ -0,0 +1,734 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.filesys.ExtendedServerConfigurationAccessor; +import org.alfresco.jlan.server.auth.ntlm.NTLM; +import org.alfresco.jlan.server.config.SecurityConfigSection; +import org.alfresco.jlan.util.IPAddress; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.management.subsystems.ActivateableBean; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.web.auth.WebCredentials; +import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter; +import org.alfresco.rest.api.PublicApiTenantWebScriptServletRequest; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.RuntimeContainer; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.Match; + +/** + * Base class with common code and initialisation for single signon authentication filters. + * + * @author gkspencer + * @author kroast + */ +public abstract class BaseSSOAuthenticationFilter extends BaseAuthenticationFilter implements DependencyInjectedFilter, AuthenticationDriver, ActivateableBean, InitializingBean +{ + private static final Pattern CMIS_URI_PATTERN = Pattern.compile(".*/cmis/versions/[0-9]+\\.[0-9]+/.*"); + + // Allow an authentication ticket to be passed as part of a request to bypass authentication + + private ExtendedServerConfigurationAccessor serverConfiguration; + + // Various services required by NTLM authenticator + + private String m_loginPage; + + // Indicate whether ticket based logons are supported + + private boolean m_ticketLogons; + + // User object attribute name + + private String m_lastConfiguredServerName; + private String m_lastResolvedServerName; + + private boolean m_isActive = true; + + private AuthenticationDriver fallbackDelegate; + private boolean m_isFallbackEnabled = true; + + protected static final String MIME_HTML_TEXT = "text/html"; + + protected String loginPageLink; + + /** + * @return login page link, which is send back to the client if the login fails in the filter. + * Override to change the default behaviour. + */ + public String getLoginPageLink() + { + if (loginPageLink == null || loginPageLink.isEmpty()) + { + return "/faces" + getLoginPage(); + } + else + { + return loginPageLink; + } + } + + public void setLoginPageLink(String loginPageLink) + { + this.loginPageLink = loginPageLink; + } + + /** + * @param serverConfiguration the serverConfiguration to set + */ + public void setServerConfiguration(ExtendedServerConfigurationAccessor serverConfiguration) + { + this.serverConfiguration = serverConfiguration; + } + + /** + * Activates or deactivates the bean + * + * @param active + * true if the bean is active and initialization should complete + */ + public final void setActive(boolean active) + { + this.m_isActive = active; + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive() + */ + public final boolean isActive() + { + return m_isActive; + } + + /** + * Sets the fallback authentication support for this filter + * + * @param delegate AuthenticationDriver + */ + public final void setFallback(AuthenticationDriver delegate) + { + this.fallbackDelegate = delegate; + } + + /** + * Activates or deactivates the fallback authentication support for this filter + * + * @param fallbackEnabled + */ + public final void setFallbackEnabled(boolean fallbackEnabled) + { + this.m_isFallbackEnabled = fallbackEnabled; + } + + /** + * @return true if fallback authentication enabled + */ + public final boolean isFallbackEnabled() + { + return m_isFallbackEnabled && fallbackDelegate != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public final void afterPropertiesSet() throws ServletException + { + // Don't trigger initialization if this component has been disabled + if (isActive()) + { + init(); + } + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.web.filter.beans.DependencyInjectedFilter#doFilter(javax.servlet.ServletContext, + * javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + public void doFilter(ServletContext context, ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + // Get the publicapi.container bean. + ApplicationContext appContext = WebApplicationContextUtils.getRequiredWebApplicationContext(context); + RuntimeContainer container = (RuntimeContainer) appContext.getBean("publicapi.container"); + + // Get the HTTP request/response + HttpServletRequest req = (HttpServletRequest) request; + + Match match = null; + + try + { + match = container.getRegistry().findWebScript(req.getMethod(), getScriptUrl(req)); + } + catch (NotFoundException notFoundEx) + { + getLogger().debug(req.getMethod() + " " + getScriptUrl(req) + "not found in Public API Container."); + } + + // If a filter up the chain has marked the request as not requiring auth then respect it + if (request.getAttribute(NO_AUTH_REQUIRED) != null) + { + if ( getLogger().isTraceEnabled()) + { + getLogger().trace("Authentication not required (filter), chaining ..."); + } + chain.doFilter(request, response); + } + // check the authentication required - if none then we don't want any of the filters down the chain to require any authentication checks + else if ((match != null) && (match.getWebScript() != null) && (RequiredAuthentication.none == match.getWebScript().getDescription().getRequiredAuthentication())) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Found webscript with no authentication - set NO_AUTH_REQUIRED flag."); + } + + req.setAttribute(NO_AUTH_REQUIRED, Boolean.TRUE); + + chain.doFilter(request, response); + } + else if (authenticateRequest(context, (HttpServletRequest) request, (HttpServletResponse) response)) + { + chain.doFilter(request, response); + } + } + + /** + * Initializes the filter. Only called if the filter is active, as indicated by {@link #isActive()}. Subclasses + * should override. + */ + protected void init() throws ServletException + { + } + + /** + * Callback executed on successful ticket validation during Type3 Message processing. + * + * @param sc + * the servlet context + * @param req + * the request + * @param res + * the response + */ + protected void onValidate(ServletContext sc, HttpServletRequest req, HttpServletResponse res, WebCredentials credentials) + { + authenticationListener.userAuthenticated(credentials); + } + + /** + * Callback executed on failed authentication of a user ticket during Type3 Message processing + * + * @param sc the servlet context + * @param req HttpServletRequest + * @param res HttpServletResponse + * @param session HttpSession + */ + protected void onValidateFailed(ServletContext sc, HttpServletRequest req, HttpServletResponse res, HttpSession session, WebCredentials credentials) + throws IOException + { + authenticationListener.authenticationFailed(credentials); + + // Restart the login challenge process if validation fails + + restartLoginChallenge(sc, req, res); + } + + /** + * Callback executed on completion of NTLM login + * + * @param req HttpServletRequest + * @param res HttpServletResponse + * @return true to continue filter chaining, false otherwise + */ + protected boolean onLoginComplete(ServletContext sc, HttpServletRequest req, HttpServletResponse res, boolean userInit) + throws IOException + { + return true; + } + + /** + * Check if the request has specified a ticket parameter to bypass the standard authentication. + * + * @param servletContext + * the servlet context + * @param req + * the request + * @param resp + * the response + * @return boolean + */ + protected boolean checkForTicketParameter(ServletContext servletContext, HttpServletRequest req, HttpServletResponse resp) + { + // Check if the request includes an authentication ticket + + boolean ticketValid = false; + String ticket = req.getParameter(ARG_TICKET); + + if (ticket != null && ticket.length() != 0) + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace( + "Logon via ticket from " + req.getRemoteHost() + " (" + req.getRemoteAddr() + ":" + req.getRemotePort() + ")" + + " ticket=" + ticket); + } + + UserTransaction tx = null; + try + { + // Get a cached user with a valid ticket + SessionUser user = getSessionUser(servletContext, req, resp, true); + + // If this isn't the same ticket, invalidate the session + if (user != null && !ticket.equals(user.getTicket())) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("The ticket doesn't match, invalidate the session."); + } + invalidateSession(req); + user = null; + } + + // If we don't yet have a valid cached user, validate the ticket and create one + if (user == null) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("There is no valid cached user, validate the ticket and create one."); + } + authenticationService.validate(ticket); + user = createUserEnvironment(req.getSession(), authenticationService.getCurrentUserName(), + authenticationService.getCurrentTicket(), true); + } + + // Indicate the ticket parameter was specified, and valid + + ticketValid = true; + } + catch (AuthenticationException authErr) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Failed to authenticate user ticket: " + authErr.getMessage(), authErr); + } + } + catch (Throwable e) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Error during ticket validation and user creation: " + e.getMessage(), e); + } + } + finally + { + try + { + if (tx != null) + { + tx.rollback(); + } + } + catch (Exception tex) + { + } + } + } + + // Return the ticket parameter status + + return ticketValid; + } + + /** + * Redirect to the login page + * + * @param req HttpServletRequest + * @param res HttpServletResponse + * @exception IOException + */ + protected void redirectToLoginPage(HttpServletRequest req, HttpServletResponse res) + throws IOException + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("redirectToLoginPage..."); + } + if (hasLoginPage()) + res.sendRedirect(req.getContextPath() + "/faces" + getLoginPage()); + } + + /** + * Determine if the login page is available + * + * @return boolean + */ + protected final boolean hasLoginPage() + { + return m_loginPage != null ? true : false; + } + + /** + * Return the login page address + * + * @return String + */ + protected final String getLoginPage() + { + return m_loginPage; + } + + /** + * Set the login page address + * + * @param loginPage String + */ + protected final void setLoginPage( String loginPage) + { + m_loginPage = loginPage; + } + + /** + * Check if ticket based logons are allowed + * + * @return boolean + */ + protected final boolean allowsTicketLogons() + { + return m_ticketLogons; + } + + /** + * Set the ticket based logons allowed flag + * + * @param ticketsAllowed boolean + */ + public final void setTicketLogons( boolean ticketsAllowed) + { + m_ticketLogons = ticketsAllowed; + } + + /** + * Check if a security blob starts with the NTLMSSP signature + * + * @param byts byte[] + * @param offset int + * @return boolean + */ + protected final boolean isNTLMSSPBlob( byte[] byts, int offset) + { + // Check if the blob has the NTLMSSP signature + + boolean isNTLMSSP = false; + + if (( byts.length - offset) >= NTLM.Signature.length) { + + // Check for the NTLMSSP signature + + int idx = 0; + while ( idx < NTLM.Signature.length && byts[offset + idx] == NTLM.Signature[ idx]) + idx++; + + if ( idx == NTLM.Signature.length) + isNTLMSSP = true; + } + + return isNTLMSSP; + } + + /** + * Because the file server configuration may change during the lifetime of this filter, this method checks against + * the last configured server name before returning a cached result + * + * @return resolved local server name + */ + protected synchronized String getServerName() + { + // Get the local server name, try the file server config first + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Searching for local server name."); + } + String srvName = null; + if (serverConfiguration != null) + { + srvName = serverConfiguration.getServerName(); + if (srvName != null && srvName.length() == 0) + { + srvName = null; + } + + } + + if (m_lastResolvedServerName != null + && (m_lastConfiguredServerName == null && srvName == null || m_lastConfiguredServerName.equals(srvName))) + { + return m_lastResolvedServerName; + } + + m_lastResolvedServerName = null; + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Found server name in the file server configuration: " + srvName); + } + m_lastConfiguredServerName = srvName; + if (serverConfiguration != null) + { + if (m_lastConfiguredServerName != null) + { + try + { + InetAddress resolved = InetAddress.getByName(m_lastConfiguredServerName); + if (resolved == null) + { + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Failed to resolve the configured name."); + } + + m_lastResolvedServerName = serverConfiguration.getLocalServerName(true); + } + else + { + m_lastResolvedServerName = m_lastConfiguredServerName; + } + } + catch (UnknownHostException ex) + { + if (getLogger().isWarnEnabled()) + { + getLogger().warn("NTLM filter, error resolving CIFS host name" + m_lastConfiguredServerName); + } + } + } + + // If we still do not have a name use the DNS name of the server, with the domain part removed + + if (m_lastResolvedServerName == null) + { + m_lastResolvedServerName = serverConfiguration.getLocalServerName(true); + + // DEBUG + + if (getLogger().isInfoEnabled()) + { + getLogger().info("NTLM filter using server name " + m_lastResolvedServerName); + } + } + } + else + { + // Get the host name + try + { + // Get the local host name + + m_lastResolvedServerName = InetAddress.getLocalHost().getHostName(); + + if (getLogger().isInfoEnabled()) + { + getLogger().info("Found FQDN " + m_lastResolvedServerName); + } + // Strip any domain name + + int pos = m_lastResolvedServerName.indexOf("."); + if (pos != -1) + { + m_lastResolvedServerName = m_lastResolvedServerName.substring(0, pos); + } + } + catch (UnknownHostException ex) + { + getLogger().error("NTLM filter, error getting local host name", ex); + } + } + + // Check if the server name is valid + + if (m_lastResolvedServerName == null || m_lastResolvedServerName.length() == 0) + { + throw new AlfrescoRuntimeException("Failed to get local server name"); + } + + return m_lastResolvedServerName; + } + + protected SecurityConfigSection getSecurityConfigSection() + { + return serverConfiguration == null ? null : (SecurityConfigSection) serverConfiguration.getConfigSection(SecurityConfigSection.SectionName); + } + + /** + * Writes link to login page and refresh tag which cause user + * to be redirected to the login page. + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @throws IOException + */ + protected void writeLoginPageLink(ServletContext context, HttpServletRequest req, HttpServletResponse resp) throws IOException + { + if ( hasLoginPage()) + { + resp.setContentType(MIME_HTML_TEXT); + + try (PrintWriter out = resp.getWriter()) + { + out.println(""); + // Removed the auto refresh to avoid refresh loop, MNT-16931 + // Removed the link to the login page, MNT-20200 + out.println("

Login failed. Please try again.

"); + out.println(""); + } + } + } + + /** + * Include into response authentication method that is supported by fallback mechanism + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @throws IOException + */ + protected void includeFallbackAuth(ServletContext context, HttpServletRequest req, HttpServletResponse resp) throws IOException + { + fallbackDelegate.restartLoginChallenge(context, req, resp); + } + + /** + * Delegate authentication to the fallback mechanism + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @return boolean + * @throws IOException + * @throws ServletException + */ + protected boolean performFallbackAuthentication(ServletContext context, HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException + { + if (getLogger().isTraceEnabled()) + { + getLogger().trace("Performing fallback authentication..."); + } + + boolean fallbackSuccess = fallbackDelegate.authenticateRequest(context, req, resp); + + if (!fallbackSuccess) + { + restartLoginChallenge(context, req, resp); + + if (getLogger().isDebugEnabled()) + { + getLogger().debug("Fallback authentication failed. Restarting login..."); + } + } + + if (fallbackSuccess && getLogger().isDebugEnabled()) + { + getLogger().debug("Fallback authentication succeeded."); + } + + return fallbackSuccess; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRuntime#getScriptUrl() + */ + private String getScriptUrl(HttpServletRequest req) + { + // NOTE: Don't use req.getPathInfo() - it truncates the path at first semi-colon in Tomcat + final String requestURI = req.getRequestURI(); + final String serviceContextPath = req.getContextPath() + req.getServletPath(); + String pathInfo; + + if (serviceContextPath.length() > requestURI.length()) + { + // NOTE: assume a redirect has taken place e.g. tomcat welcome-page + // NOTE: this is unlikely, and we'll take the hit if the path contains a semi-colon + pathInfo = req.getPathInfo(); + } + // MNT-13057 fix, do not decode CMIS uris. + else if (CMIS_URI_PATTERN.matcher(requestURI).matches()) + { + pathInfo = requestURI.substring(serviceContextPath.length()); + } + else + { + pathInfo = URLDecoder.decode(requestURI.substring(serviceContextPath.length())); + } + + // NOTE: must contain at least root / and single character for tenant name + if (pathInfo.length() < 2 || pathInfo.equals("/")) + { + // url path has no tenant id -> get networks request + pathInfo = PublicApiTenantWebScriptServletRequest.NETWORKS_PATH; + } + else + { + if(!pathInfo.substring(0, 6).toLowerCase().equals("/cmis/") && !pathInfo.equals("/discovery")) + { + // remove tenant + int idx = pathInfo.indexOf('/', 1); + pathInfo = pathInfo.substring(idx == -1 ? pathInfo.length() : idx); + if(pathInfo.equals("") || pathInfo.equals("/")) + { + // url path is just a tenant id -> get network request + pathInfo = PublicApiTenantWebScriptServletRequest.NETWORK_PATH; + } + } + } + + return pathInfo; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/HTTPRequestAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/HTTPRequestAuthenticationFilter.java new file mode 100644 index 00000000000..76432d5486c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/HTTPRequestAuthenticationFilter.java @@ -0,0 +1,316 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.security.PersonService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * WebDAV Authentication Filter Class for SSO linke SiteMinder and IChains + */ +public class HTTPRequestAuthenticationFilter extends BaseAuthenticationFilter implements Filter +{ + // Debug logging + + private static Log logger = LogFactory.getLog(HTTPRequestAuthenticationFilter.class); + + // Servlet context + + private ServletContext m_context; + + private String httpServletRequestAuthHeaderName; + + private AuthenticationComponent m_authComponent; + + // By default match everything if this is not set + private String m_authPatternString = null; + + private Pattern m_authPattern = null; + + /** + * Initialize the filter + * + * @param config + * FitlerConfig + * @exception ServletException + */ + public void init(FilterConfig config) throws ServletException + { + // Save the context + + m_context = config.getServletContext(); + + // Setup the authentication context + + WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(m_context); + + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + setNodeService(serviceRegistry.getNodeService()); + setAuthenticationService(serviceRegistry.getAuthenticationService()); + setTransactionService(serviceRegistry.getTransactionService()); + setPersonService((PersonService) ctx.getBean("PersonService")); // transactional and permission-checked + m_authComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent"); + + httpServletRequestAuthHeaderName = config.getInitParameter("httpServletRequestAuthHeaderName"); + if(httpServletRequestAuthHeaderName == null) + { + httpServletRequestAuthHeaderName = "x-user"; + } + this.m_authPatternString = config.getInitParameter("authPatternString"); + if (this.m_authPatternString != null) + { + try + { + m_authPattern = Pattern.compile(this.m_authPatternString); + } + catch (PatternSyntaxException e) + { + logger.warn("Invalid pattern: " + this.m_authPatternString, e); + m_authPattern = null; + } + } + + } + + /** + * Run the authentication filter + * + * @param req + * ServletRequest + * @param resp + * ServletResponse + * @param chain + * FilterChain + * @exception ServletException + * @exception IOException + */ + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, + ServletException + { + // Assume it's an HTTP request + + final HttpServletRequest httpReq = (HttpServletRequest) req; + HttpServletResponse httpResp = (HttpServletResponse) resp; + + // Get the user details object from the session + + SessionUser user = (SessionUser) httpReq.getSession().getAttribute(AUTHENTICATION_USER); + + if (user == null) + { + // Check for the auth header + + String authHdr = httpReq.getHeader(httpServletRequestAuthHeaderName); + if (logger.isTraceEnabled()) + { + if (authHdr == null) + { + logger.trace("Header not found: " + httpServletRequestAuthHeaderName); + } + else + { + logger.trace("Header is <" + authHdr + ">"); + } + } + + // Throw an error if we have an unknown authentication + + if ((authHdr != null) && (authHdr.length() > 0)) + { + + // Get the user + + final String userName; + if (m_authPattern != null) + { + Matcher matcher = m_authPattern.matcher(authHdr); + if (matcher.matches()) + { + userName = matcher.group(); + if ((userName == null) || (userName.length() < 1)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Extracted null or empty user name from pattern " + m_authPatternString + + " against " + authHdr); + } + reject(httpReq, httpResp); + return; + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("no pattern match for " + m_authPatternString + " against " + authHdr); + } + reject(httpReq, httpResp); + return; + } + } + else + { + userName = authHdr; + } + + if (logger.isTraceEnabled()) + { + logger.trace("User = " + AuthenticationUtil.maskUsername(userName)); + } + + // Get the authorization header + + user = transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionHelper.RetryingTransactionCallback() + { + + public SessionUser execute() throws Throwable + { + try + { + // Authenticate the user + + m_authComponent.clearCurrentSecurityContext(); + m_authComponent.setCurrentUser(userName); + + return createUserEnvironment(httpReq.getSession(), userName, authenticationService + .getCurrentTicket(), true); + } + catch (AuthenticationException ex) + { + if (logger.isDebugEnabled()) + { + logger.debug("Failed", ex); + } + return null; + // Perhaps auto-creation/import is disabled + } + } + }); + + } + else + { + // Check if the request includes an authentication ticket + + String ticket = req.getParameter(ARG_TICKET); + + if (ticket != null && ticket.length() > 0) + { + // Debug + + if (logger.isTraceEnabled()) + { + logger.trace("Logon via ticket from " + req.getRemoteHost() + " (" + req.getRemoteAddr() + ":" + req.getRemotePort() + ")" + + " ticket=" + ticket); + } + + try + { + // Validate the ticket + authenticationService.validate(ticket); + + // Need to create the User instance if not already available + user = createUserEnvironment(httpReq.getSession(), authenticationService.getCurrentUserName(), + ticket, true); + } + catch (AuthenticationException authErr) + { + // Clear the user object to signal authentication failure + if (logger.isDebugEnabled()) + { + logger.debug("Failed", authErr); + } + user = null; + } + } + } + + // Check if the user is authenticated, if not then prompt again + + if (user == null) + { + // No user/ticket, force the client to prompt for logon details + reject(httpReq, httpResp); + return; + } + } + + // Chain other filters + + chain.doFilter(req, resp); + } + + private void reject( HttpServletRequest httpReq, HttpServletResponse httpResp) throws IOException + { + httpResp.setHeader("WWW-Authenticate", "BASIC realm=\"Alfresco DAV Server\""); + httpResp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpResp.flushBuffer(); + return; + } + + /** + * Cleanup filter resources + */ + public void destroy() + { + // Nothing to do + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseAuthenticationFilter#getLogger() + */ + @Override + protected Log getLogger() + { + return logger; + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/HTTPRequestAuthenticationFilterTestFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/HTTPRequestAuthenticationFilterTestFilter.java new file mode 100644 index 00000000000..7d942519ed0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/HTTPRequestAuthenticationFilterTestFilter.java @@ -0,0 +1,139 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +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.HttpServletRequest; + +/** + * This filter simply sets a request header to test the SSO filters + */ +public class HTTPRequestAuthenticationFilterTestFilter implements Filter +{ + + private String httpServletRequestAuthHeaderName; + + private String userName; + + /** + * Initialize the filter + * + * @param config + * FitlerConfig + * @exception ServletException + */ + public void init(FilterConfig config) throws ServletException + { + httpServletRequestAuthHeaderName = config.getInitParameter("httpServletRequestAuthHeaderName"); + if (httpServletRequestAuthHeaderName == null) + { + httpServletRequestAuthHeaderName = "x-user"; + } + + userName = config.getInitParameter("userName"); + if (userName == null) + { + userName = "guest"; + } + } + + /** + * Run the authentication filter + * + * @param req + * ServletRequest + * @param resp + * ServletResponse + * @param chain + * FilterChain + * @exception ServletException + * @exception IOException + */ + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException + { + // Assume it's an HTTP request + + HttpServletRequest httpReq = (HttpServletRequest) req; + chain.doFilter(getProxy(httpReq), resp); + } + + /** + * Cleanup filter resources + */ + public void destroy() + { + // Nothing to do + } + + private HttpServletRequest getProxy(HttpServletRequest req) + { + HttpServletRequest proxy = (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(), new Class[] { HttpServletRequest.class }, + new Handler(req)); + return proxy; + } + + class Handler implements InvocationHandler + { + HttpServletRequest httpReq; + + Handler(HttpServletRequest httpReq) + { + this.httpReq = httpReq; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + if (method.getName().equals("getHeader")) + { + Object arg0 = args[0]; + if (arg0 != null) + { + if (arg0 instanceof String) + { + String headerName = (String) arg0; + if (headerName.equals(httpServletRequestAuthHeaderName)) + { + return userName; + } + } + } + } + return method.invoke(httpReq, args); + } + + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/KerberosAuthenticationFilter.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/KerberosAuthenticationFilter.java new file mode 100644 index 00000000000..6a019a950bc --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/KerberosAuthenticationFilter.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.alfresco.repo.web.auth.WebCredentials; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * WebDAV Kerberos Authentication Filter Class + * + * @author GKSpencer + */ +public class KerberosAuthenticationFilter extends BaseKerberosAuthenticationFilter +{ + // Debug logging + private static Log logger = LogFactory.getLog(KerberosAuthenticationFilter.class); + + @Override + public String getLoginPageLink() + { + return loginPageLink; + } + + /* (non-Javadoc) + * @see org.alfresco.repo.webdav.auth.BaseSSOAuthenticationFilter#onValidateFailed(javax.servlet.ServletContext, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.http.HttpSession) + */ + @Override + protected void onValidateFailed(ServletContext sc, HttpServletRequest req, HttpServletResponse res, HttpSession session, WebCredentials credentials) + throws IOException + { + super.onValidateFailed(sc, req, res, session, credentials); + // Restart the login challenge process if validation fails + restartLoginChallenge(sc, req, res); + } + + /* (non-Javadoc) + * + * @see org.alfresco.repo.webdav.auth.BaseSSOAuthenticationFilter#getLogger() + */ + @Override + protected Log getLogger() + { + return logger; + } + + @Override + protected boolean checkLoginPage(HttpServletRequest req, HttpServletResponse resp) + { + return (req.getRequestURI().endsWith("/jsp/login.jsp")); + } + + /** + * Writes link to login page and refresh tag which cause user + * to be redirected to the login page. + * + * @param context ServletContext + * @param req HttpServletRequest + * @param resp HttpServletResponse + * @throws IOException + */ + @Override + protected void writeLoginPageLink(ServletContext context, HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.setContentType(MIME_HTML_TEXT); + + try (PrintWriter out = resp.getWriter()) + { + out.println(""); + // Removed the auto refresh to avoid refresh loop, MNT-16931 + // Removed the link to the login page, MNT-20200 + out.println("

Login failed. Please try again.

"); + out.println(""); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/SSOFallbackBasicAuthenticationDriver.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/SSOFallbackBasicAuthenticationDriver.java new file mode 100644 index 00000000000..016b1a64633 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/SSOFallbackBasicAuthenticationDriver.java @@ -0,0 +1,210 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav.auth; + +import java.io.IOException; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.SessionUser; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.Authorization; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

+ * Basic HTTP web authentication implementation. Main purpose to use as fallback authentication with SSO filters. + *

+ * + * @author pavel.yurkevich + */ +public class SSOFallbackBasicAuthenticationDriver implements AuthenticationDriver +{ + private Log logger = LogFactory.getLog(SSOFallbackBasicAuthenticationDriver.class); + + private AuthenticationService authenticationService; + private PersonService personService; + private NodeService nodeService; + private TransactionService transactionService; + + private String userAttributeName = AuthenticationDriver.AUTHENTICATION_USER; + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setUserAttributeName(String userAttributeName) + { + this.userAttributeName = userAttributeName; + } + + @Override + public boolean authenticateRequest(ServletContext context, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + String authHdr = request.getHeader("Authorization"); + HttpSession session = request.getSession(false); + SessionUser user = session == null ? null : (SessionUser) session.getAttribute(userAttributeName); + if (user == null) + { + if (authHdr != null && authHdr.length() > 5 && authHdr.substring(0, 5).equalsIgnoreCase("Basic")) + { + String basicAuth = new String(Base64.decodeBase64(authHdr.substring(5).getBytes())); + String username = null; + String password = null; + + int pos = basicAuth.indexOf(":"); + if (pos != -1) + { + username = basicAuth.substring(0, pos); + password = basicAuth.substring(pos + 1); + } + else + { + username = basicAuth; + password = ""; + } + + try + { + if (logger.isTraceEnabled()) + { + logger.trace("Authenticating user '" + AuthenticationUtil.maskUsername(username) + "'"); + } + + Authorization auth = new Authorization(username, password); + if (auth.isTicket()) + { + authenticationService.validate(auth.getTicket()); + } + else + { + authenticationService.authenticate(username, password.toCharArray()); + } + + final RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + @Override + public SessionUser execute() throws Throwable + { + NodeRef personNodeRef = personService.getPerson(authenticationService.getCurrentUserName()); + String username = (String) nodeService.getProperty(personNodeRef, ContentModel.PROP_USERNAME); + NodeRef homeSpaceRef = (NodeRef) nodeService.getProperty(personNodeRef, ContentModel.PROP_HOMEFOLDER); + + return new WebDAVUser(username, authenticationService.getCurrentTicket(), homeSpaceRef); + } + }; + + user = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public SessionUser doWork() throws Exception + { + return transactionService.getRetryingTransactionHelper().doInTransaction(callback, true); + } + }, AuthenticationUtil.SYSTEM_USER_NAME); + + if (logger.isTraceEnabled()) + { + logger.trace("Authenticated user '" + AuthenticationUtil.maskUsername(username) + "'"); + } + + request.getSession().setAttribute(userAttributeName, user); + return true; + } + catch (AuthenticationException ex) + { + // Do nothing, user object will be null + if (logger.isDebugEnabled()) + { + logger.debug(ex.getMessage(), ex); + } + } + } + } + else + { + try + { + authenticationService.validate(user.getTicket()); + return true; + } + catch (AuthenticationException ex) + { + session.invalidate(); + if (logger.isDebugEnabled()) + { + logger.debug(ex.getMessage(), ex); + } + } + } + + return false; + } + + @Override + public void restartLoginChallenge(ServletContext context, HttpServletRequest request, HttpServletResponse response) + throws IOException + { + if (logger.isDebugEnabled()) + { + logger.debug("Including Basic HTTP authentication into response headers..."); + } + + response.addHeader("WWW-Authenticate", "Basic realm=\"Alfresco Server\""); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/SharepointConstants.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/SharepointConstants.java new file mode 100644 index 00000000000..5ac00c44233 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/SharepointConstants.java @@ -0,0 +1,39 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.repo.webdav.auth; + +/** + * A place to put Sharepoint specific authentication constants. + * + * @author dward + */ +public interface SharepointConstants +{ + + /** The session attribute under which sharepoint {@link AuthenticationDriver}s store their user objects. */ + public final static String USER_SESSION_ATTRIBUTE = "_vtiAuthTicket"; + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/auth/WebDAVUser.java b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/WebDAVUser.java new file mode 100644 index 00000000000..2c722e6e784 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/auth/WebDAVUser.java @@ -0,0 +1,142 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.repo.webdav.auth; + +import org.alfresco.repo.SessionUser; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * WebDAV User Class + * + *

Contains the details of an authenticated WebDAV user + * + * @author GKSpencer + */ +public class WebDAVUser implements SessionUser +{ + private static final long serialVersionUID = -6948146071131901345L; + + // User name + + private String m_userName; + + // Authentication ticket + + private String m_ticket; + + // User home node + + private NodeRef m_homeNode; + + /** + * Class constructor + * + * @param user String + * @param ticket String + * @param homeNode NodeRef + */ + public WebDAVUser(String user, String ticket, NodeRef homeNode) + { + m_userName = user; + m_ticket = ticket; + m_homeNode = homeNode; + } + + /** + * Return the user name + * + * @return String + */ + public final String getUserName() + { + return m_userName; + } + + /** + * Return the ticket + * + * @return String + */ + public final String getTicket() + { + return m_ticket; + } + + /** + * Check if the user has a home node + * + * @return boolean + */ + public final boolean hasHomeNode() + { + return m_homeNode != null ? true : false; + } + + /** + * Return the user home node + * + * @return NodeRef + */ + public final NodeRef getHomeNode() + { + return m_homeNode; + } + + /** + * Set the home folder node for this user + * + * @param homeNode NodeRef + */ + protected final void setHomeNode(NodeRef homeNode) + { + m_homeNode = homeNode; + } + + /** + * Return the user details as a string + * + * @return String + */ + public String toString() + { + StringBuilder str = new StringBuilder(); + + str.append("["); + str.append(getUserName()); + str.append(":"); + str.append(getTicket()); + + if ( hasHomeNode()) + { + str.append(",Home="); + str.append(getHomeNode()); + } + str.append("]"); + + return str.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Actions.java b/remote-api/src/main/java/org/alfresco/rest/api/Actions.java new file mode 100644 index 00000000000..e45fa3ce605 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Actions.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api; + + +import org.alfresco.rest.api.model.Action; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.NodeRef; + +public interface Actions +{ + CollectionWithPagingInfo getActionDefinitions(NodeRef nodeRef, Parameters params); + + CollectionWithPagingInfo getActionDefinitions(Parameters params); + + ActionDefinition getActionDefinitionById(String actionDefinitionId); + + enum SortKey + { + NAME, + TITLE + }; + + Action executeAction(Action action, Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Activities.java b/remote-api/src/main/java/org/alfresco/rest/api/Activities.java new file mode 100644 index 00000000000..590f5c236e3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Activities.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.util.Map; + +import org.alfresco.sync.repo.Client; +import org.alfresco.repo.domain.activities.ActivityFeedEntity; +import org.alfresco.rest.api.model.Activity; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.json.JSONException; + +public interface Activities +{ + static final String APP_TOOL = "restapi"; + static final Client RESTAPI_CLIENT = Client.asType(Client.ClientType.restapi); + + public static enum ActivityWho + { + me, others; + }; + + public Map getActivitySummary(ActivityFeedEntity entity) throws JSONException; + public CollectionWithPagingInfo getUserActivities(String personId, final Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Audit.java b/remote-api/src/main/java/org/alfresco/rest/api/Audit.java new file mode 100644 index 00000000000..fc3c3e4dc1e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Audit.java @@ -0,0 +1,121 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.AuditApp; +import org.alfresco.rest.api.model.AuditEntry; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +/** + * Handles audit (applications & entries) + * + * @author janv, anechifor, eknizat + */ +public interface Audit +{ + String VALUES_VALUE = "valuesValue"; + String VALUES_KEY = "valuesKey"; + String CREATED_BY_USER = "createdByUser"; + String CREATED_AT = "createdAt"; + String ID = "id"; + String PARAM_INCLUDE_VALUES = "values"; + String PARAM_INCLUDE_MAX = "max"; + String PARAM_INCLUDE_MIN = "min"; + + /** + * Gets a single audit application by id + * + * @param auditAppId + * @param parameters + * @return an audit app + */ + AuditApp getAuditApp(String auditAppId, Parameters parameters); + + /** + * Lists audit applications + * + * @param paging + * @return Collection of audit apps + */ + CollectionWithPagingInfo getAuditApps(Paging paging); + + /** + * Updates a single audit application by id + * + * @param auditAppId + * @param auditApp + * @param parameters + * @return an audit entry + */ + AuditApp update(String auditAppId, AuditApp auditApp, Parameters parameters); + + /** + * Get a single audit entry by id + * + * @param auditAppId + * @param auditEntryId + * @param parameters + * @return an audit entry + */ + AuditEntry getAuditEntry(String auditAppId, long auditEntryId, Parameters parameters); + + /** + * Lists audit entries + * + * @param auditAppId + * if null then across all audit apps + * @param parameters + * @return Collection of audit entries + */ + CollectionWithPagingInfo listAuditEntries(String auditAppId, Parameters parameters); + + /** + * Delete a single audit entry by id + * + * @param auditAppId + * @param auditEntryId + * @param parameters + */ + void deleteAuditEntry(String auditAppId, long auditEntryId, Parameters parameters); + + /** + * Delete set of audit entities + * + * @param auditAppId + * @param parameters - required - delete is based on "where" query + */ + void deleteAuditEntries(String auditAppId, Parameters parameters); + + /** + * + * @param nodeId + * @param parameters + * @return + */ + CollectionWithPagingInfo listAuditEntriesByNodeId(String nodeId, Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Authentications.java b/remote-api/src/main/java/org/alfresco/rest/api/Authentications.java new file mode 100644 index 00000000000..b21dae68733 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Authentications.java @@ -0,0 +1,45 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.api.model.LoginTicketResponse; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; + +/** + * @author Jamal Kaabi-Mofrad + */ +public interface Authentications +{ + + LoginTicketResponse createTicket(LoginTicket loginRequest, Parameters parameters); + + LoginTicketResponse validateTicket(String me, Parameters parameters, WithResponse withResponse); + + void deleteTicket(String me, Parameters parameters, WithResponse withResponse); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Comments.java b/remote-api/src/main/java/org/alfresco/rest/api/Comments.java new file mode 100644 index 00000000000..fce22fcc7a6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Comments.java @@ -0,0 +1,45 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Comment; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; + +import java.util.List; + +/** + * + * @author steveglover + * @since publicapi1.0 + */ +public interface Comments +{ + public Comment createComment(String nodeId, Comment comment); + public Comment updateComment(String nodeId, Comment comment); + public void deleteComment(String nodeId, String commentNodeId); + public CollectionWithPagingInfo getComments(String nodeId, Paging paging, List include); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/CustomModels.java b/remote-api/src/main/java/org/alfresco/rest/api/CustomModels.java new file mode 100644 index 00000000000..c9ef74f8701 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/CustomModels.java @@ -0,0 +1,227 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api; + +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.rest.api.model.CustomAspect; +import org.alfresco.rest.api.model.CustomModel; +import org.alfresco.rest.api.model.CustomModelConstraint; +import org.alfresco.rest.api.model.CustomModelDownload; +import org.alfresco.rest.api.model.CustomType; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +/** + * @author Jamal Kaabi-Mofrad + */ +public interface CustomModels +{ + /** + * Gets the {@code org.alfresco.rest.api.model.CustomModel} representation for the given model + * + * @param modelName the model name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomModel} object + */ + public CustomModel getCustomModel(String modelName, Parameters parameters); + + /** + * Gets a paged list of all custom models + * + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return a paged list of {@code org.alfresco.rest.api.model.CustomModel} objects + */ + public CollectionWithPagingInfo getCustomModels(Parameters parameters); + + /** + * Creates custom model + * + * @param model the custom model to create + * @return {@code org.alfresco.rest.api.model.CustomModel} object + */ + public CustomModel createCustomModel(CustomModel model); + + /** + * Creates custom model from the imported {@link M2Model}. + * + * @param m2Model the model + * @return {@code org.alfresco.rest.api.model.CustomModel} object + */ + public CustomModel createCustomModel(M2Model m2Model); + + /** + * Updates or activates/deactivates the custom model + * + * @param modelName the model name + * @param model the custom model to update (JSON payload) + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomModel} object + */ + public CustomModel updateCustomModel(String modelName, CustomModel model, Parameters parameters); + + /** + * Deletes the custom model + * + * @param modelName the model name + */ + public void deleteCustomModel(String modelName); + + /** + * Gets the {@code org.alfresco.rest.api.model.CustomType} representation of + * the given model's type + * + * @param modelName the model name + * @param typeName the model's type name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomType} object + */ + public CustomType getCustomType(String modelName, String typeName, Parameters parameters); + + /** + * Gets a paged list of all the given custom model's types + * + * @param modelName the model name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return a paged list of {@code org.alfresco.rest.api.model.CustomType} objects + */ + public CollectionWithPagingInfo getCustomTypes(String modelName, Parameters parameters); + + /** + * Creates custom model's type + * + * @param modelName the model name + * @param type the custom type to create within the given model + * @return {@code org.alfresco.rest.api.model.CustomType} object + */ + public CustomType createCustomType(String modelName, CustomType type); + + /** + * Updates the custom model's type + * + * @param modelName the model name + * @param type the custom model's type to update (JSON payload) + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomType} object + */ + public CustomType updateCustomType(String modelName, CustomType type, Parameters parameters); + + /** + * Deletes the custom model's type + * + * @param modelName the model name + * @param typeName the model's type name + */ + public void deleteCustomType(String modelName, String typeName); + + /** + * Gets the {@code org.alfresco.rest.api.model.CustomAspect} representation of + * the given model's aspect + * + * @param modelName the model name + * @param aspectName the model's aspect name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomAspect} object + */ + public CustomAspect getCustomAspect(String modelName, String aspectName, Parameters parameters); + + /** + * Gets a paged list of all the given custom model's aspects + * + * @param modelName the model name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return a paged list of {@code org.alfresco.rest.api.model.CustomAspect} objects + */ + public CollectionWithPagingInfo getCustomAspects(String modelName, Parameters parameters); + + /** + * Creates custom model's aspect + * + * @param modelName the model name + * @param aspect the custom aspect to create within the given model + * @return {@code org.alfresco.rest.api.model.CustomAspect} object + */ + public CustomAspect createCustomAspect(String modelName, CustomAspect aspect); + + /** + * Updates the custom model's aspect + * + * @param modelName the model name + * @param aspect the custom model's aspect to update (JSON payload) + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomAspect} object + */ + public CustomAspect updateCustomAspect(String modelName, CustomAspect aspect, Parameters parameters); + + /** + * Deletes the custom model's aspect + * + * @param modelName the model name + * @param aspectName the model's aspect name + */ + public void deleteCustomAspect(String modelName, String aspectName); + + /** + * Gets the {@code org.alfresco.rest.api.model.CustomModelConstraint} + * representation of the given model's constraint + * + * @param modelName the model name + * @param constraintName the model's constraint name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code org.alfresco.rest.api.model.CustomModelConstraint} object + */ + public CustomModelConstraint getCustomModelConstraint(String modelName, String constraintName, Parameters parameters); + + /** + * Gets a paged list of all of the given custom model's constraints + * + * @param modelName the model name + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return a paged list of {@code org.alfresco.rest.api.model.CustomModelConstraint} objects + */ + public CollectionWithPagingInfo getCustomModelConstraints(String modelName, Parameters parameters); + + /** + * Creates custom model's constraint + * + * @param modelName the model name + * @param constraint the custom constraint to create within the given model + * @return {@code org.alfresco.rest.api.model.CustomModelConstraint} object + */ + public CustomModelConstraint createCustomModelConstraint(String modelName, CustomModelConstraint constraint); + + /** + * Starts the creation of a downloadable archive file containing the + * custom model file and its associated Share extension module file (if requested). + * + * @param modelName the model name + * @param parameters the {@link Parameters} object to get the parameters + * passed into the request + * @return {@code org.alfresco.rest.api.model.CustomModelDownload} object + * containing the archive node reference + */ + public CustomModelDownload createDownload(String modelName, Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/DeletedNodes.java b/remote-api/src/main/java/org/alfresco/rest/api/DeletedNodes.java new file mode 100644 index 00000000000..1b2a568e4da --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/DeletedNodes.java @@ -0,0 +1,102 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.util.Map; + +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.NodeTargetAssoc; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +/** + * Handles trashcan / deleted nodes + * + * @author Gethin James + */ +public interface DeletedNodes +{ + /** + * Lists deleted nodes using a ArchivedNodesCannedQuery + * @param parameters + * @return Collection of deleted Nodes + */ + CollectionWithPagingInfo listDeleted(Parameters parameters); + + /** + * Gets a single deleted node by id. + * @param originalId + * @param parameters + * @param fullnode Should we return the full representation of the minimal one? + * @param mapUserInfo + * @return a deleted node + */ + Node getDeletedNode(String originalId, Parameters parameters, boolean fullnode, Map mapUserInfo); + + /** + * Restores a deleted node and returns it. + * + * @param archivedId + * @param nodeTargetAssoc + * - optional + * @return the new undeleted node. + */ + Node restoreArchivedNode(String archivedId, NodeTargetAssoc nodeTargetAssoc); + + /** + * Permanently delete the node. + * @param archivedId + */ + void purgeArchivedNode(String archivedId); + + /** + * Download file content (or rendition content) via archived node. + * + * @param archivedId + * @param renditionId + * - optional + * @param parameters + * {@link Parameters} + * @return + */ + BinaryResource getContent(String archivedId, String renditionId, Parameters parameters); + + /** + * @param archivedId + * @param renditionId + * @return + */ + Rendition getRendition(String archivedId, String renditionId, Parameters parameters); + + /** + * @param archivedId + * @return + */ + CollectionWithPagingInfo getRenditions(String archivedId, Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Downloads.java b/remote-api/src/main/java/org/alfresco/rest/api/Downloads.java new file mode 100644 index 00000000000..dea9eb45525 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Downloads.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Download; + +/** + * downloads API + * + * @author cpopa + * + */ +public interface Downloads +{ + /** + * Creates a download:download node. + * + * @param download + * @return information about the newly created download:download node + */ + Download createDownloadNode(Download download); + + /** + * Get status info about a download node. + * + * @param downloadNodeId + * @return status info about a download:download node + */ + Download getDownloadStatus(String downloadNodeId); + + /** + * Stop the zip creation if still in progress + * @param downloadNodeId + */ + void cancel(String downloadNodeId); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Favourites.java b/remote-api/src/main/java/org/alfresco/rest/api/Favourites.java new file mode 100644 index 00000000000..7919b929f42 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Favourites.java @@ -0,0 +1,96 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Favourite; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +/** + * Centralises access to favourites functionality and maps between representations repository and api representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public interface Favourites +{ + String PARAM_INCLUDE_PATH = Nodes.PARAM_INCLUDE_PATH; + + String PARAM_INCLUDE_PROPERTIES = Nodes.PARAM_INCLUDE_PROPERTIES; + + /** + * Add a favourite for user personId + * + * @param personId the personId for which the favourite is to be added + * @param favourite the favourite to add + */ + Favourite addFavourite(String personId, Favourite favourite); + + /** + * Add a favourite for user personId taking parameters into account + * + * @param personId the personId for which the favourite is to be added + * @param favourite the favourite to add + * @param parameters the parameters + */ + Favourite addFavourite(String personId, Favourite favourite, Parameters parameters); + + /** + * Add a favourite for user personId + * + * @param personId the personId for which the favourite is to be removed + * @param id the id of the favourite to remove (id is a uuid) + */ + void removeFavourite(String personId, String id); + + /** + * Get a paged list of favourites for user personId + * + * @param personId the personId for which the favourite is to be removed + * @param parameters Parameters + * @return paged favourites + */ + CollectionWithPagingInfo getFavourites(String personId, final Parameters parameters); + + /** + * Get a specific favourite for user personId + * + * @param personId the personId for which the favourite is to be removed + * @param favouriteId the favourite id + * @return the favourite + */ + Favourite getFavourite(String personId, String favouriteId); + + /** + * Get a specific favourite for user personId taking parameters into account + * + * @param personId the personId for which the favourite is to be removed + * @param favouriteId the favourite id + * @param parameters the parameters + * @return the favourite + */ + Favourite getFavourite(String personId, String favouriteId, Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Groups.java b/remote-api/src/main/java/org/alfresco/rest/api/Groups.java new file mode 100644 index 00000000000..53eb2203875 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Groups.java @@ -0,0 +1,150 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Group; +import org.alfresco.rest.api.model.GroupMember; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +/** + * Groups API + * + * @author cturlica + */ +public interface Groups +{ + String PARAM_ID = "id"; + String PARAM_DISPLAY_NAME = "displayName"; + String PARAM_INCLUDE_PARENT_IDS = "parentIds"; + String PARAM_INCLUDE_ZONES = "zones"; + String PARAM_IS_ROOT = "isRoot"; + String PARAM_CASCADE = "cascade"; + String PARAM_MEMBER_TYPE = "memberType"; + String PARAM_MEMBER_TYPE_GROUP = "GROUP"; + String PARAM_MEMBER_TYPE_PERSON = "PERSON"; + + /** + * Create a group. + * + * @param group the group to create. + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - include param (parentIds, zones) + * @return a {@code org.alfresco.rest.api.model.Group} object + */ + Group create(Group group, Parameters parameters); + + /** + * Update the given group. Not all fields are used, only those as defined in + * the Open API spec. + * + * @param groupId + * the group ID + * @param group + * details to use for the update + * @param parameters + * the {@link Parameters} object to get the parameters passed + * into the request including: - include param (parentIds, zones) + * @return Updated group + */ + Group update(String groupId, Group group, Parameters parameters); + + /** + * Get a group by it's id. + * + * @param groupId the identifier of a group. + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - include param (parentIds, zones) + * @return a {@code org.alfresco.rest.api.model.Group} object + * @throws EntityNotFoundException + */ + Group getGroup(String groupId, Parameters parameters) throws EntityNotFoundException; + + /** + * Gets a list of groups. + * + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - filter, sort & paging params (where, orderBy, skipCount, maxItems) + * - include param (parentIds, zones) + * @return a paged list of {@code org.alfresco.rest.api.model.Group} objects + */ + CollectionWithPagingInfo getGroups(Parameters parameters); + + /** + * Gets the list of groups for which the specified person is a member. + * + * @param personId the person's ID ("-me-" may be used as an alias for the current user.) + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - sort & paging params (orderBy, skipCount, maxItems) + * @return a paged list of {@code org.alfresco.rest.api.model.Group} objects + */ + CollectionWithPagingInfo getGroupsByPersonId(String personId, Parameters parameters); + + /** + * Delete the given group. + * + * @param groupId + * the group ID + * @param parameters + * the {@link Parameters} object to get the parameters passed + * into the request including: - include param (parentIds, zones) + */ + void delete(String groupId, Parameters parameters); + + /** + * Gets a list of groups. + * + * @param groupId the identifier of a group. + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - filter, sort & paging params (where, orderBy, skipCount, maxItems) + * - include param (parentIds, zones) + * @return a paged list of {@code org.alfresco.rest.api.model.GroupMember} objects + */ + CollectionWithPagingInfo getGroupMembers(String groupId, Parameters parameters); + + /** + * Create a group member. + * + * @param groupId the identifier of a group. + * @return a {@code org.alfresco.rest.api.model.GroupMember} object + */ + GroupMember createGroupMember(String groupId, GroupMember groupMember); + + /** + * + * Delete group membership + * + * @param groupId + * @param groupMemberId + */ + void deleteGroupMembership(String groupId, String groupMemberId); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/NetworkWebScriptGet.java b/remote-api/src/main/java/org/alfresco/rest/api/NetworkWebScriptGet.java new file mode 100644 index 00000000000..c1a17aa8ab6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/NetworkWebScriptGet.java @@ -0,0 +1,119 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.rest.api.model.PersonNetwork; +import org.alfresco.rest.api.networks.NetworksEntityResource; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.jacksonextensions.JacksonHelper.Writer; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.alfresco.rest.framework.webscripts.ApiWebScript; +import org.alfresco.rest.framework.webscripts.ResourceWebScriptHelper; +import org.springframework.extensions.webscripts.Format; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +public class NetworkWebScriptGet extends ApiWebScript implements ResponseWriter +{ + private Networks networks; + private ResourceWebScriptHelper helper; + + public void setHelper(ResourceWebScriptHelper helper) + { + this.helper = helper; + } + + public void setNetworks(Networks networks) + { + this.networks = networks; + } + + @Override + public void execute(final Api api, final WebScriptRequest req, final WebScriptResponse res) throws IOException + { + try + { + transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + // apply content type + res.setContentType(Format.JSON.mimetype() + ";charset=UTF-8"); + + assistant.getJsonHelper().withWriter(res.getOutputStream(), new Writer() + { + @Override + public void writeContents(JsonGenerator generator, ObjectMapper objectMapper) + throws JsonGenerationException, JsonMappingException, IOException + { + String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + String networkId = TenantUtil.getCurrentDomain(); + + PersonNetwork networkMembership = networks.getNetwork(personId, networkId); + if(networkMembership != null) + { + // TODO this is not ideal, but the only way to populate the embedded network entities (this would normally be + // done automatically by the api framework). + Object wrapped = helper.processAdditionsToTheResponse(res, Api.ALFRESCO_PUBLIC, NetworksEntityResource.NAME, Params.valueOf(personId, null, req), networkMembership); + + objectMapper.writeValue(generator, wrapped); + } + else + { + throw new EntityNotFoundException(networkId); + } + } + }); + + return null; + } + }, true, true); + } + catch (ApiException | WebScriptException apiException) + { + renderException(apiException, res, assistant); + } + catch (RuntimeException runtimeException) + { + renderException(runtimeException, res, assistant); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Networks.java b/remote-api/src/main/java/org/alfresco/rest/api/Networks.java new file mode 100644 index 00000000000..f6edd4115c4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Networks.java @@ -0,0 +1,39 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Network; +import org.alfresco.rest.api.model.PersonNetwork; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; + +public interface Networks +{ + public Network validateNetwork(String networkId); + public Network getNetwork(String networkId); + public PersonNetwork getNetwork(String personId, String networkId); + public CollectionWithPagingInfo getNetworks(String personId, Paging paging); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/NetworksWebScriptGet.java b/remote-api/src/main/java/org/alfresco/rest/api/NetworksWebScriptGet.java new file mode 100644 index 00000000000..e5f4183060d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/NetworksWebScriptGet.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.rest.api.model.PersonNetwork; +import org.alfresco.rest.api.networks.NetworksEntityResource; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.jacksonextensions.JacksonHelper.Writer; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.alfresco.rest.framework.webscripts.ApiWebScript; +import org.alfresco.rest.framework.webscripts.ResourceWebScriptHelper; +import org.springframework.extensions.webscripts.Format; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * A webscript that returns the authenticated user's network memberships. + * + * @author steveglover + * + */ +public class NetworksWebScriptGet extends ApiWebScript implements RecognizedParamsExtractor, ResponseWriter +{ + private Networks networks; + private ResourceWebScriptHelper helper; + + public void setHelper(ResourceWebScriptHelper helper) + { + this.helper = helper; + } + + public void setNetworks(Networks networks) + { + this.networks = networks; + } + + @Override + public void execute(final Api api, final WebScriptRequest req, final WebScriptResponse res) throws IOException + { + try + { + transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + final Paging paging = findPaging(req); + + // apply content type + res.setContentType(Format.JSON.mimetype() + ";charset=UTF-8"); + + assistant.getJsonHelper().withWriter(res.getOutputStream(), new Writer() + { + @Override + public void writeContents(JsonGenerator generator, ObjectMapper objectMapper) + throws JsonGenerationException, JsonMappingException, IOException + { + List entities = new ArrayList(); + + String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + + CollectionWithPagingInfo networkMemberships = networks.getNetworks(personId, paging); + for (PersonNetwork networkMember : networkMemberships.getCollection()) + { + // TODO this is not ideal, but the only way to populate the embedded network entities (this would normally be + // done automatically by the api framework). + Object wrapped = helper.processAdditionsToTheResponse(res, Api.ALFRESCO_PUBLIC, NetworksEntityResource.NAME, Params.valueOf(personId, null, req), networkMember); + entities.add(wrapped); + } + + objectMapper.writeValue(generator, CollectionWithPagingInfo.asPaged(paging, entities)); + } + }); + + return null; + } + }, true, true); + } + catch (ApiException | WebScriptException apiException) + { + renderException(apiException, res, assistant); + } + catch (RuntimeException runtimeException) + { + renderException(runtimeException, res, assistant); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/NodeDefinitionMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/NodeDefinitionMapper.java new file mode 100644 index 00000000000..918be2531dd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/NodeDefinitionMapper.java @@ -0,0 +1,39 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.NodeDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; +/** + * Maps representations from TypeDefinition to NodeDefinition + * + * @author gfertuso + */ +public interface NodeDefinitionMapper +{ + NodeDefinition fromTypeDefinition(TypeDefinition typeDefinition, MessageLookup messageLookup); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/NodeRatings.java b/remote-api/src/main/java/org/alfresco/rest/api/NodeRatings.java new file mode 100644 index 00000000000..dba18a3cc7a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/NodeRatings.java @@ -0,0 +1,40 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.impl.node.ratings.RatingScheme; +import org.alfresco.rest.api.model.NodeRating; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; + +public interface NodeRatings +{ + public RatingScheme validateRatingScheme(String ratingSchemeId); + public NodeRating getNodeRating(String nodeId, String ratingSchemeId); + public CollectionWithPagingInfo getNodeRatings(String nodeId, Paging paging); + public void addRating(String nodeId, String ratingSchemeId, Object rating); + public void removeRating(String nodeId, String ratingSchemeId); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Nodes.java b/remote-api/src/main/java/org/alfresco/rest/api/Nodes.java new file mode 100644 index 00000000000..5ea28363159 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Nodes.java @@ -0,0 +1,408 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.rest.api.model.AssocChild; +import org.alfresco.rest.api.model.AssocTarget; +import org.alfresco.rest.api.model.Document; +import org.alfresco.rest.api.model.Folder; +import org.alfresco.rest.api.model.LockInfo; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.PathInfo; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * File Folder (Nodes) API + * + * @author janv + * @author Jamal Kaabi-Mofrad + * @author Gethin James + * @author steveglover + */ +public interface Nodes +{ + /** + * Get the node representation for the given node. + * + * @param nodeId String + * @return Node + */ + Node getNode(String nodeId); + + /** + * Get the document representation for the given node. + * + * @param nodeRef NodeRef + * @return Document + */ + Document getDocument(NodeRef nodeRef); + + /** + * Get the folder representation for the given node. + * + * @param nodeRef NodeRef + * @return Folder + */ + Folder getFolder(NodeRef nodeRef); + + /** + * Get the folder or document representation (as appropriate) for the given node. + * + * @param nodeId String nodeId or well-known alias, eg. "-root-" or "-my-" + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - incPrimaryParent + * @return + */ + Node getFolderOrDocument(String nodeId, Parameters parameters); + + Node getFolderOrDocumentFullInfo(NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, Parameters parameters, Map mapUserInfo); + + /** + * Get the folder or document representation (as appropriate) for the given node. + * + * @param nodeRef A real Node + * @param parentNodeRef + * @param nodeTypeQName + * @param includeParam + * @param mapUserInfo + * @return + */ + Node getFolderOrDocument(NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, List includeParam, Map mapUserInfo); + + /** + * Get list of children of a parent folder. + * + * @param parentFolderNodeId String id of parent folder node or well-known alias, eg. "-root-" or "-my-" + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * including: + * - filter, sort & paging params (where, orderBy, skipCount, maxItems) + * - incFiles, incFolders (both true by default) + * @return a paged list of {@code org.alfresco.rest.api.model.Node} objects + */ + CollectionWithPagingInfo listChildren(String parentFolderNodeId, Parameters parameters); + + /** + * Delete the given node. Note: will cascade delete for a folder. + * + * @param nodeId String id of node (folder or document) + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * - permanent (default false) + */ + void deleteNode(String nodeId, Parameters parameters); + + /** + * Create node - folder or (empty) file. + * + * @param parentFolderNodeId + * @param nodeInfo + * @param parameters + * @return + */ + Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters); + + /** + * Move or Copy node + * + * @param sourceNodeId + * @param parentFolderNodeId + * @param name + * @param parameters + * @return + */ + Node moveOrCopyNode(String sourceNodeId, String parentFolderNodeId, String name, Parameters parameters, boolean isCopy); + + /** + * Update node meta-data. + * + * @param nodeId + * @param entity + * @param parameters + * @return + */ + Node updateNode(String nodeId, Node entity, Parameters parameters); + + /** + * Download file content. + * + * @param fileNodeId + * @param parameters + * @param recordActivity true, if an activity post is required. + * @return + */ + BinaryResource getContent(String fileNodeId, Parameters parameters, boolean recordActivity); + + /** + * Download file content. + * + * @param nodeRef the content nodeRef + * @param parameters + * @param recordActivity true, if an activity post is required. + * @return + */ + BinaryResource getContent(NodeRef nodeRef, Parameters parameters, boolean recordActivity); + + /** + * Uploads file content (updates existing node with new content). + * + * Note: may create a new version, depending on versioning behaviour. + * + * @param fileNodeId + * @param contentInfo + * @param stream + * @param parameters + * @return + */ + Node updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters); + + /** + * Uploads file content and meta-data into the repository. + * + * @param parentFolderNodeId String id of parent folder node or well-known alias, eg. "-root-" or "-my-" + * @param formData the {@link FormData} + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code Node} if successful + */ + Node upload(String parentFolderNodeId, FormData formData, Parameters parameters); + + + NodeRef validateNode(StoreRef storeRef, String nodeId); + NodeRef validateNode(String nodeId); + NodeRef validateNode(NodeRef nodeRef); + NodeRef validateOrLookupNode(String nodeId, String path); + + boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes); + + /** + * Determines whether the type of the given nodeRef is a sub-class of another class or not. + * + * @param nodeRef source nodeRef + * @param ofClassQName the class to test against + * @param validateNodeRef whether to validate the given source node or not + * @return true if the type of the given nodeRef is a sub-class of another class, otherwise false + */ + boolean isSubClass(NodeRef nodeRef, QName ofClassQName, boolean validateNodeRef); + + /** + * Helper to create a QName from either a fully qualified or short-name QName string + * + * @param qnameStr Fully qualified or short-name QName string + * @return QName + */ + QName createQName(String qnameStr); + + QName getAssocType(String assocTypeQNameStr); + + QName getAssocType(String assocTypeQNameStr, boolean mandatory); + + /** + * + * @param parentNodeId + * @param entities + * @return + */ + List addChildren(String parentNodeId, List entities); + + /** + * + * @param sourceNodeId + * @param entities + * @return + */ + List addTargets(String sourceNodeId, List entities); + + /** + * Lock a node + * @param nodeId + * @param lockInfo + * @param parameters + * @return + */ + Node lock(String nodeId, LockInfo lockInfo, Parameters parameters); + + /** + * Unlock a node + * @param nodeId + * @param parameters + * @return + */ + Node unlock(String nodeId, Parameters parameters); + + DirectAccessUrl requestContentUrl(String nodeId, DirectAccessUrlRequest directAccessUrlRequest); + + DirectAccessUrl requestContentUrl(NodeRef nodeRef, DirectAccessUrlRequest directAccessUrlRequest); + + /** + * Convert from node properties (map of QName to Serializable) retrieved from + * the respository to a map of String to Object that can be formatted/expressed + * as required by the API JSON response for get nodes, get person etc. + *

+ * Returns null if there are no properties to return, rather than an empty map. + * + * @param nodeProps + * @param selectParam + * @param mapUserInfo + * @param excludedNS + * @param excludedProps + * @return The map of properties, or null if none to return. + */ + Map mapFromNodeProperties(Map nodeProps, List selectParam, Map mapUserInfo, List excludedNS, List excludedProps); + + /** + * Map from the JSON API format of properties (String to Object) to + * the typical node properties map used by the repository (QName to Serializable). + * + * @param props + * @return + */ + Map mapToNodeProperties(Map props); + + /** + * Returns the path to the given nodeRef {@code nodeRefIn} or the archived nodeRef {@code archivedParentAssoc}. + * + * @param nodeRefIn the NodeRef + * @param archivedParentAssoc the ChildAssociationRef of the archived NodeRef + * @return the path to the given node + */ + PathInfo lookupPathInfo(NodeRef nodeRefIn, ChildAssociationRef archivedParentAssoc); + + /** + * Map from a String representation of aspect names to a set + * of QName objects, as used by the repository. + * + * @param aspectNames + * @return + */ + Set mapToNodeAspects(List aspectNames); + + /** + * Map from aspects (Set of QName) retrieved from the repository to a + * map List of String required that can be formatted/expressed as required + * by the API JSON response for get nodes, get person etc. + *

+ * Returns null if there are no aspect names to return, rather than an empty list. + * + * @param nodeAspects + * @param excludedNS + * @param excludedAspects + * @return The list of aspect names, or null if none to return. + */ + List mapFromNodeAspects(Set nodeAspects, List excludedNS, List excludedAspects); + + /** + * Add aspects to the specified NodeRef. Aspects that appear in the exclusions list + * will be ignored. + * + * @param nodeRef + * @param aspectNames + * @param exclusions + */ + void addCustomAspects(NodeRef nodeRef, List aspectNames, List exclusions); + + /** + * Update aspects for the specified NodeRef. An empty list will result in + * aspects being removed. + * + * @param nodeRef + * @param aspectNames + * @param exclusions + */ + void updateCustomAspects(NodeRef nodeRef, List aspectNames, List exclusions); + + void validateAspects(List aspectNames, List excludedNS, List excludedAspects); + + void validateProperties(Map properties, List excludedNS, List excludedProperties); + + + /** + * API Constants - query parameters, etc + */ + + String PATH_ROOT = "-root-"; + String PATH_MY = "-my-"; + String PATH_SHARED = "-shared-"; + + String OP_CREATE = "create"; + String OP_DELETE = "delete"; + String OP_UPDATE = "update"; + String OP_UPDATE_PERMISSIONS = "updatePermissions"; + + String PARAM_RELATIVE_PATH = "relativePath"; + String PARAM_PERMANENT = "permanent"; + + String PARAM_INCLUDE_PROPERTIES = "properties"; + String PARAM_INCLUDE_PATH = "path"; + String PARAM_INCLUDE_ASPECTNAMES = "aspectNames"; + String PARAM_INCLUDE_ISLINK = "isLink"; + String PARAM_INCLUDE_ISLOCKED = "isLocked"; + String PARAM_INCLUDE_ALLOWABLEOPERATIONS = "allowableOperations"; + String PARAM_INCLUDE_PERMISSIONS = "permissions"; + String PARAM_INCLUDE_ISFAVORITE = "isFavorite"; + + String PARAM_INCLUDE_ASSOCIATION = "association"; + String PARAM_INCLUDE_DEFINITION = "definition"; + + String PARAM_ISFOLDER = "isFolder"; + String PARAM_ISFILE = "isFile"; + + String PARAM_INCLUDE_SUBTYPES = "INCLUDESUBTYPES"; + + String PARAM_NAME = "name"; + String PARAM_CREATEDAT = "createdAt"; + String PARAM_MODIFIEDAT = "modifiedAt"; + String PARAM_CREATEBYUSER = "createdByUser"; + String PARAM_MODIFIEDBYUSER = "modifiedByUser"; + String PARAM_MIMETYPE = "mimeType"; + String PARAM_SIZEINBYTES = "sizeInBytes"; + String PARAM_NODETYPE = "nodeType"; + + String PARAM_VERSION_MAJOR = "majorVersion"; // true if major, false if minor + String PARAM_VERSION_COMMENT = "comment"; + + String PARAM_OVERWRITE = "overwrite"; + String PARAM_AUTO_RENAME = "autoRename"; + + String PARAM_ISPRIMARY = "isPrimary"; + String PARAM_ASSOC_TYPE = "assocType"; +} + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/People.java b/remote-api/src/main/java/org/alfresco/rest/api/People.java new file mode 100644 index 00000000000..ea7af6886cc --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/People.java @@ -0,0 +1,133 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.InputStream; +import java.util.List; + +import org.alfresco.rest.api.model.PasswordReset; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.NoSuchPersonException; + +public interface People +{ + String DEFAULT_USER = "-me-"; + String PARAM_INCLUDE_ASPECTNAMES = "aspectNames"; + String PARAM_INCLUDE_PROPERTIES = "properties"; + String PARAM_INCLUDE_CAPABILITIES = "capabilities"; + String PARAM_FIRST_NAME = "firstName"; + String PARAM_LAST_NAME = "lastName"; + String PARAM_ID = "id"; + + String validatePerson(String personId); + String validatePerson(String personId, boolean validateIsCurrentUser); + NodeRef getAvatar(String personId); + + /** + * Get a person. This included a full representation of the person. + * + * @throws NoSuchPersonException if personId does not exist + */ + Person getPerson(final String personId); + + /** + * Get a person, specifying optional includes as required. + * + * @param personId + * @param include + * @return + */ + Person getPerson(String personId, List include); + + /** + * Create a person. + * + * @param person + * @return + */ + Person create(Person person); + + /** + * Update the given person's details. + * + * @param personId The identifier of a person. + * @param person The person details. + * @return The updated person details. + */ + Person update(String personId, Person person); + + /** + * Get people list + * + * @return CollectionWithPagingInfo + */ + CollectionWithPagingInfo getPeople(Parameters parameters); + + /** + * Request password reset (an email will be sent to the registered email of the given {@code userId}). + * The API returns a 202 response for a valid, as well as the invalid (does not exist or disabled) userId + * + * @param userId the user id of the person requesting the password reset + * @param client the client name which is registered to send emails + */ + void requestPasswordReset(String userId, String client); + + /** + * Performs password reset + * + * @param passwordReset the password reset details + */ + void resetPassword(String personId, PasswordReset passwordReset); + + /** + * + * @param personId + * @param parameters + * @return + */ + BinaryResource downloadAvatarContent(String personId, Parameters parameters); + + /** + * + * @param personId + * @param contentInfo + * @param stream + * @param parameters + * @return + */ + Person uploadAvatarContent(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters); + + /** + * + * @param personId + */ + void deleteAvatarContent(String personId); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Preferences.java b/remote-api/src/main/java/org/alfresco/rest/api/Preferences.java new file mode 100644 index 00000000000..c8faeb99fce --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Preferences.java @@ -0,0 +1,36 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Preference; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; + +public interface Preferences +{ + public Preference getPreference(String personId, String preferenceName); + public CollectionWithPagingInfo getPreferences(String personId, Paging paging); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java new file mode 100644 index 00000000000..2eef57809f6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiAuthenticatorFactory.java @@ -0,0 +1,305 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.web.auth.AuthenticationListener; +import org.alfresco.repo.web.auth.TenantAuthentication; +import org.alfresco.repo.web.auth.WebCredentials; +import org.alfresco.repo.web.scripts.TenantWebScriptServletRequest; +import org.alfresco.repo.web.scripts.servlet.RemoteUserAuthenticatorFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Authenticator; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletResponse; + +/** + * HTTP Basic Authentication for Public Api. + * + * Adapted from org.alfresco.module.org_alfresco_module_cloud.webscripts.TenantBasicHTTPAuthenticatorFactory. + * + * @author sglover + */ +public class PublicApiAuthenticatorFactory extends RemoteUserAuthenticatorFactory +{ + private static Log logger = LogFactory.getLog(PublicApiAuthenticatorFactory.class); + + public static final String DEFAULT_AUTHENTICATOR_KEY_HEADER = "X-Alfresco-Authenticator-Key"; + + private String authenticatorKeyHeader = DEFAULT_AUTHENTICATOR_KEY_HEADER; + private RetryingTransactionHelper retryingTransactionHelper; + private TenantAuthentication tenantAuthentication; + private Set validAuthenticatorKeys = Collections.emptySet(); + private Set outboundHeaderNames; + private boolean useBasicAuth = true; + + public void setAuthenticatorKeyHeader(String authenticatorKeyHeader) + { + this.authenticatorKeyHeader = authenticatorKeyHeader; + } + + /** + * Set the headers passed to the gateway for authentication. + * + */ + public void setOutboundHeaders(Set outboundHeaders) + { + if (outboundHeaders != null) + { + Set trimmed = new HashSet(); + for (String value : outboundHeaders) + { + trimmed.add(value.toLowerCase(Locale.ENGLISH).trim()); + } + outboundHeaders = trimmed; + } + + this.outboundHeaderNames = outboundHeaders; + } + + /** + * Whether to suggest that users use Basic auth. If set to true, then a + * 401 (unauthorized) response will contain a WWW-Authentication header + * specifying the scheme "Basic". If this is set to false, then + * the scheme "AlfTicket" will be used. + *

+ * Set this to false to avoid getting Basic auth dialogue popups in browsers + * when using the public API directly, for example. + * + * @see REPO-2575 + * @param useBasicAuth + */ + public void setUseBasicAuth(boolean useBasicAuth) + { + this.useBasicAuth = useBasicAuth; + } + + public void setTenantAuthentication(TenantAuthentication service) + { + this.tenantAuthentication = service; + } + + public void setTransactionHelper(RetryingTransactionHelper service) + { + this.retryingTransactionHelper = service; + } + + public void setValidAuthentictorKeys(Set validKeys) + { + if (validKeys != null) + { + Set trimmedKeys = new HashSet(); + for (String key : validKeys) + { + trimmedKeys.add(key.trim()); + } + validKeys = trimmedKeys; + } + this.validAuthenticatorKeys = validKeys; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.servlet.ServletAuthenticatorFactory#create(org.alfresco.web.scripts.servlet.WebScriptServletRequest, org.alfresco.web.scripts.servlet.WebScriptServletResponse) + */ + public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res) + { + return new PublicApiAuthenticator(req, res, new ProxyListener()); + } + + private Map getOutboundHeaders(TenantWebScriptServletRequest req) + { + Map outboundHeaders = new HashMap(); + for (String headerName : outboundHeaderNames) + { + String[] headerValues = req.getHeaderValues(headerName); + if (headerValues != null && headerValues.length > 0) + { + outboundHeaders.put(headerName, headerValues); + } + } + return outboundHeaders; + } + + /** + * Public api authentication with additional tenant applicability check + */ + public class PublicApiAuthenticator extends RemoteUserAuthenticator + { + // dependencies + private TenantWebScriptServletRequest servletReq; + + // Proxy listener used to receive initial authentication events from the base BasicHttpAuthenticator + private ProxyListener proxyListener; + + /** + * Construct + * + * @param req WebScriptServletRequest + * @param res WebScriptServletResponse + * @param proxyListener ProxyListener + */ + public PublicApiAuthenticator(WebScriptServletRequest req, WebScriptServletResponse res, ProxyListener proxyListener) + { + super(req, res, proxyListener); + if (!(req instanceof TenantWebScriptServletRequest)) + { + throw new WebScriptException("Request is not a tenant aware request"); + } + servletReq = (TenantWebScriptServletRequest)req; + this.proxyListener = proxyListener; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Authenticator#authenticate(org.alfresco.web.scripts.Description.RequiredAuthentication, boolean) + */ + public boolean authenticate(RequiredAuthentication required, boolean isGuest) + { + boolean authorized = false; + try + { + String authenticatorKey = servletReq.getHeader(authenticatorKeyHeader); + String remoteUser = getRemoteUser(); + if (authenticatorKey != null && remoteUser != null) + { + // Trusted auth. Validate key and setup authentication context. + authorized = authenticateViaGateway(required, isGuest, authenticatorKey, remoteUser); + } + else + { + // Fallback to parent authenticator + try + { + authorized = super.authenticate(required, isGuest); + } + catch (AuthenticationException ae) + { + // e.g. guest + if (logger.isDebugEnabled()) + logger.debug("TenantBasicHttpAuthenticator: required="+required+", isGuest="+isGuest+" - "+ae.getMessage()); + } + } + if (authorized) + { + // check tenant validity + final String tenant = servletReq.getTenant(); + final String email = AuthenticationUtil.getFullyAuthenticatedUser(); + try + { + authorized = retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Boolean execute() throws Exception + { + return tenantAuthentication.authenticateTenant(email, tenant); + } + }, true, false); + } + finally + { + if (!authorized) + { + listener.authenticationFailed(new TenantCredentials(tenant, email, proxyListener.getOrignalCredentials())); + AuthenticationUtil.clearCurrentSecurityContext(); + } + else + { + listener.userAuthenticated(new TenantCredentials(tenant, email, proxyListener.getOrignalCredentials())); + } + } + } + return authorized; + } + finally + { + if (!authorized) + { + servletRes.setStatus(401); + String scheme = useBasicAuth ? "Basic" : "AlfTicket"; + String challenge = scheme + " realm=\"Alfresco " + servletReq.getTenant() + " tenant\""; + servletRes.setHeader("WWW-Authenticate", challenge); + } + } + } + + private boolean authenticateViaGateway(RequiredAuthentication required, boolean isGuest, String authenticatorKey, String remoteUser) + { + // Validate the authenticator key, and if valid set the fully authenticated user. + if (validAuthenticatorKeys.contains(authenticatorKey)) + { + AuthenticationUtil.setFullyAuthenticatedUser(remoteUser); + proxyListener.userAuthenticated(new PublicApiCredentials(authenticatorKey, remoteUser, getOutboundHeaders(servletReq))); + return true; + } + else + { + logger.error("Invalid authenticator key:- " + authenticatorKey); + proxyListener.authenticationFailed(new PublicApiCredentials(authenticatorKey, remoteUser, getOutboundHeaders(servletReq))); + return false; + } + } + } + + private class ProxyListener implements AuthenticationListener + { + private WebCredentials originalCredentials; + + @Override + public void userAuthenticated(WebCredentials credentials) + { + this.originalCredentials = credentials; + } + + @Override + public void authenticationFailed(WebCredentials credentials) + { + listener.authenticationFailed(credentials); + } + + @Override + public void authenticationFailed(WebCredentials credentials, Exception ex) + { + listener.authenticationFailed(credentials, ex); + } + + public WebCredentials getOrignalCredentials() + { + return this.originalCredentials; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiCredentials.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiCredentials.java new file mode 100644 index 00000000000..7f3573236ad --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiCredentials.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.util.Map; + +import org.alfresco.repo.web.auth.WebCredentials; + +/** + * {@link WebCredentials} class for holding information related to authentication when using the public API.. + * + * @author Alex Miller + */ +public class PublicApiCredentials implements WebCredentials +{ + private static final long serialVersionUID = 7828112870415043104L; + + private String authenticatorKey; + private Map outboundHeaders; + private String user; + + /** + * @param authenticatorKey The Gateway specific key + * @param user The user name supplied by the gateway + * @param outboundHeaders The headers used by the gateway for authentication. + */ + public PublicApiCredentials(String authenticatorKey, String user, Map outboundHeaders) + { + this.authenticatorKey = authenticatorKey; + this.user = user; + this.outboundHeaders = outboundHeaders; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + + ((this.authenticatorKey == null) ? 0 : this.authenticatorKey.hashCode()); + result = prime * result + + ((this.outboundHeaders == null) ? 0 : this.outboundHeaders.hashCode()); + result = prime * result + ((this.user == null) ? 0 : this.user.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; } + PublicApiCredentials other = (PublicApiCredentials) obj; + if (this.authenticatorKey == null) + { + if (other.authenticatorKey != null) { return false; } + } + else if (!this.authenticatorKey.equals(other.authenticatorKey)) { return false; } + if (this.outboundHeaders == null) + { + if (other.outboundHeaders != null) { return false; } + } + else if (!this.outboundHeaders.equals(other.outboundHeaders)) { return false; } + if (this.user == null) + { + if (other.user != null) { return false; } + } + else if (!this.user.equals(other.user)) { return false; } + return true; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java new file mode 100644 index 00000000000..a198acb9aa0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiDeclarativeRegistry.java @@ -0,0 +1,494 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; + +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.core.ResourceLocator; +import org.alfresco.rest.framework.core.ResourceWithMetadata; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; +import org.alfresco.rest.framework.resource.actions.interfaces.ResourceAction; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.apache.commons.lang3.StringUtils; +import org.springframework.extensions.webscripts.*; +import org.springframework.extensions.webscripts.Description.FormatStyle; +import org.springframework.extensions.webscripts.Description.RequiredAuthentication; +import org.springframework.extensions.webscripts.Description.RequiredTransaction; +import org.springframework.extensions.webscripts.Description.TransactionCapability; +import org.springframework.http.HttpMethod; + +/** + * + * @author steveglover + * @author janv + * @author Jamal Kaabi-Mofrad + * @since PublicApi1.0 + */ +public class PublicApiDeclarativeRegistry extends DeclarativeRegistry +{ + private WebScript getNetworksWebScript; + private WebScript getNetworkWebScript; + private Container container; + + private ResourceLocator locator; + + public void setLocator(ResourceLocator locator) + { + this.locator = locator; + } + + public void setGetNetworksWebScript(WebScript getNetworksWebScript) + { + this.getNetworksWebScript = getNetworksWebScript; + } + + public void setGetNetworkWebScript(WebScript getNetworkWebScript) + { + this.getNetworkWebScript = getNetworkWebScript; + } + + public void setContainer(Container container) + { + super.setContainer(container); + this.container = container; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.Registry#findWebScript(java.lang.String, java.lang.String) + */ + public Match findWebScript(String method, String uri) + { + Match match; + + HttpMethod httpMethod = HttpMethod.valueOf(method); + if (HttpMethod.GET.equals(httpMethod)) + { + if (uri.equals(PublicApiTenantWebScriptServletRequest.NETWORKS_PATH)) + { + Map templateVars = new HashMap<>(); + templateVars.put("apiScope", "public"); + templateVars.put("apiVersion", "1"); + templateVars.put("apiName", "networks"); + match = new Match("", templateVars, "", getNetworksWebScript); + } + else if (uri.equals(PublicApiTenantWebScriptServletRequest.NETWORK_PATH)) + { + Map templateVars = new HashMap<>(); + templateVars.put("apiScope", "public"); + templateVars.put("apiVersion", "1"); + templateVars.put("apiName", "network"); + match = new Match("", templateVars, "", getNetworkWebScript); + } + else + { + match = super.findWebScript(method, uri); + if (match == null) + { + return null; + } + Map templateVars = match.getTemplateVars(); + ResourceWithMetadata rwm = getResourceWithMetadataOrNull(templateVars, httpMethod); + if (rwm != null) + { + Class resAction = null; + + final Map resourceVars = locator.parseTemplateVars(templateVars); + final String entityId = resourceVars.get(ResourceLocator.ENTITY_ID); + final String relationshipId = resourceVars.get(ResourceLocator.RELATIONSHIP_ID); + + switch (rwm.getMetaData().getType()) + { + case ENTITY: + if (StringUtils.isNotBlank(entityId)) + { + if (EntityResourceAction.ReadById.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = EntityResourceAction.ReadById.class; + } + } + else + { + if (EntityResourceAction.Read.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = EntityResourceAction.Read.class; + } + } + break; + case PROPERTY: + if (StringUtils.isNotBlank(entityId)) + { + if (BinaryResourceAction.Read.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = BinaryResourceAction.Read.class; + } + else if (RelationshipResourceBinaryAction.Read.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = RelationshipResourceBinaryAction.Read.class; + } + } + break; + case RELATIONSHIP: + if (StringUtils.isNotBlank(relationshipId)) + { + if (RelationshipResourceAction.ReadById.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = RelationshipResourceAction.ReadById.class; + } + } + else + { + if (RelationshipResourceAction.Read.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = RelationshipResourceAction.Read.class; + } + } + break; + default: + break; + } + + final boolean noAuth = (resAction != null && rwm.getMetaData().isNoAuth(resAction)); + if (noAuth) + { + // override match with noAuth + match = overrideMatch(match); + } + } + } + } + else if (HttpMethod.POST.equals(httpMethod)) + { + match = super.findWebScript(method, uri); + if (match != null) + { + ResourceWithMetadata rwm = getResourceWithMetadataOrNull(match.getTemplateVars(), httpMethod); + if (rwm != null) + { + Class resAction = null; + Boolean noAuth = null; + switch (rwm.getMetaData().getType()) + { + case ENTITY: + if (EntityResourceAction.Create.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = EntityResourceAction.Create.class; + } + else if (EntityResourceAction.CreateWithResponse.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = EntityResourceAction.CreateWithResponse.class; + } + break; + case RELATIONSHIP: + if (RelationshipResourceAction.Create.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = RelationshipResourceAction.Create.class; + } + else if (RelationshipResourceAction.CreateWithResponse.class.isAssignableFrom(rwm.getResource().getClass())) + { + resAction = RelationshipResourceAction.CreateWithResponse.class; + } + break; + case OPERATION: + noAuth = rwm.getMetaData().isNoAuth(null); + break; + default: + break; + } + + if (noAuth == null) + { + noAuth = (resAction != null && rwm.getMetaData().isNoAuth(resAction)); + } + if (noAuth) + { + // override match with noAuth + match = overrideMatch(match); + } + } + } + } + else + { + match = super.findWebScript(method, uri); + } + + if (match == null) + { + throw new NotFoundException(NotFoundException.DEFAULT_MESSAGE_ID, new String[] {uri}); + } + return match; + } + + private ResourceWithMetadata getResourceWithMetadataOrNull(Map templateVars, HttpMethod method) + { + if (templateVars.get("apiName") != null) + { + // NOTE: noAuth currently only exposed for GET or POST + Api api = ApiAssistant.determineApi(templateVars); + + // TODO can we avoid locating resource more than once (or at least provide a common code to determine the GET resourceAction) ? + return locator.locateResource(api, templateVars, method); + } + return null; + } + + private Match overrideMatch(final Match match) + { + // TODO is there a better way (to dynamically override "requiredAuthentication") or handle noAuth check earlier ? + WebScript noAuthWebScriptWrapper = new WebScript() + { + @Override + public void init(Container container, Description description) + { + match.getWebScript().init(container, description); + } + + @Override + public Description getDescription() + { + final Description d = match.getWebScript().getDescription(); + return new Description() + { + @Override + public String getStorePath() + { + return d.getStorePath(); + } + + @Override + public String getScriptPath() + { + return d.getScriptPath(); + } + + @Override + public Path getPackage() + { + return d.getPackage(); + } + + @Override + public String getDescPath() + { + return d.getDescPath(); + } + + @Override + public InputStream getDescDocument() throws IOException + { + return d.getDescDocument(); + } + + @Override + public String getKind() + { + return d.getKind(); + } + + @Override + public Set getFamilys() + { + return d.getFamilys(); + } + + @Override + public RequiredAuthentication getRequiredAuthentication() + { + return RequiredAuthentication.none; + } + + @Override + public String getRunAs() + { + return d.getRunAs(); + } + + @Override + public RequiredTransaction getRequiredTransaction() + { + return d.getRequiredTransaction(); + } + + @Override + public RequiredTransactionParameters getRequiredTransactionParameters() + { + return d.getRequiredTransactionParameters(); + } + + @Override + public RequiredCache getRequiredCache() + { + return d.getRequiredCache(); + } + + @Override + public String getMethod() + { + return d.getMethod(); + } + + @Override + public String[] getURIs() + { + return d.getURIs(); + } + + @Override + public FormatStyle getFormatStyle() + { + return d.getFormatStyle(); + } + + @Override + public String getDefaultFormat() + { + return d.getDefaultFormat(); + } + + @Override + public NegotiatedFormat[] getNegotiatedFormats() + { + return d.getNegotiatedFormats(); + } + + @Override + public Map getExtensions() + { + return d.getExtensions(); + } + + @Override + public Lifecycle getLifecycle() + { + return d.getLifecycle(); + } + + @Override + public boolean getMultipartProcessing() + { + return d.getMultipartProcessing(); + } + + @Override + public void setMultipartProcessing(boolean b) + { + d.setMultipartProcessing(b); + } + + @Override + public ArgumentTypeDescription[] getArguments() + { + return d.getArguments(); + } + + @Override + public TypeDescription[] getRequestTypes() + { + return d.getRequestTypes(); + } + + @Override + public TypeDescription[] getResponseTypes() + { + return d.getResponseTypes(); + } + + @Override + public String getId() + { + return d.getId(); + } + + @Override + public String getShortName() + { + return d.getShortName(); + } + + @Override + public String getDescription() + { + return d.getDescription(); + } + }; + } + + @Override + public ResourceBundle getResources() + { + return match.getWebScript().getResources(); + } + + @Override + public void execute(WebScriptRequest webScriptRequest, WebScriptResponse webScriptResponse) throws IOException + { + match.getWebScript().execute(webScriptRequest, webScriptResponse); + } + + @Override + public void setURLModelFactory(URLModelFactory urlModelFactory) + { + match.getWebScript().setURLModelFactory(urlModelFactory); + } + }; + + // override match with noAuth + return new Match(match.getTemplate(), match.getTemplateVars(), match.getPath(), noAuthWebScriptWrapper); + } + + private void initWebScript(WebScript webScript, String name) + { + DescriptionImpl serviceDesc = new DescriptionImpl(name, name, name, name); + serviceDesc.setRequiredAuthentication(RequiredAuthentication.user); + TransactionParameters transactionParameters = new TransactionParameters(); + transactionParameters.setRequired(RequiredTransaction.required); + transactionParameters.setCapability(TransactionCapability.readonly); + serviceDesc.setRequiredTransactionParameters(transactionParameters); + serviceDesc.setFormatStyle(FormatStyle.argument); + serviceDesc.setDefaultFormat("json"); + serviceDesc.setUris(new String[] { name }); + webScript.init(container, serviceDesc); + } + + public void reset() + { + super.reset(); + initWebScript(getNetworksWebScript, "networks"); + initWebScript(getNetworkWebScript, "network"); + } +} + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiRepositoryContainer.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiRepositoryContainer.java new file mode 100644 index 00000000000..1bd23ddb4ee --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiRepositoryContainer.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.IOException; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.repo.web.scripts.TenantRepositoryContainer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Authenticator; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Repository (server-tier) container for public api + * + * @author steveglover + * @author davidc + */ +public class PublicApiRepositoryContainer extends TenantRepositoryContainer +{ + protected static final Log logger = LogFactory.getLog(PublicApiRepositoryContainer.class); + + @Override + public void executeScript(final WebScriptRequest scriptReq, final WebScriptResponse scriptRes, final Authenticator auth) + throws IOException + { + String tenant = ((PublicApiTenantWebScriptServletRequest)scriptReq).getTenant(); + if (tenant != null) + { + // handle special tenant keys + // -super- => run as system tenant + // -default- => run as user's default tenant + String user = null; + if (tenant.equalsIgnoreCase(TenantUtil.DEFAULT_TENANT)) + { + // switch from default to super tenant, if not authenticated + user = AuthenticationUtil.getFullyAuthenticatedUser(); + if (user == null) + { + tenant = TenantUtil.SYSTEM_TENANT; + } + } + + // run as super tenant + if (tenant.equalsIgnoreCase(TenantUtil.SYSTEM_TENANT)) + { + if (logger.isDebugEnabled()) + { + logger.debug("executeScript (-system-): ["+user+","+tenant+"] "+scriptReq.getServicePath()); + } + + TenantUtil.runAsTenant(new TenantRunAsWork() + { + public Object doWork() throws Exception + { + PublicApiRepositoryContainer.super.executeScript(scriptReq, scriptRes, auth); + return null; + + } + }, TenantService.DEFAULT_DOMAIN); + } + else + { + if (tenant.equalsIgnoreCase(TenantUtil.DEFAULT_TENANT)) + { + tenant = tenantAdminService.getUserDomain(user); + } + + // run as explicit tenant + TenantUtil.runAsTenant(new TenantRunAsWork() + { + public Object doWork() throws Exception + { + PublicApiRepositoryContainer.super.executeScript(scriptReq, scriptRes, auth); + return null; + } + }, tenant); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantAuthentication.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantAuthentication.java new file mode 100644 index 00000000000..cf011421253 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantAuthentication.java @@ -0,0 +1,105 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.tenant.TenantAdminService; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.web.auth.TenantAuthentication; + +/** + * Authenticate current user against specified tenant (Enterprise) + * + * @author steveglover + */ +public class PublicApiTenantAuthentication implements TenantAuthentication +{ + private TenantAdminService tenantAdminService; + + public void setTenantAdminService(TenantAdminService service) + { + this.tenantAdminService = service; + } + + /** + * Determine whether tenant exists and enabled + * + * @param tenant String + * @return true => it exists, no it doesn't + */ + public boolean tenantExists(final String tenant) + { + if (tenant == null || TenantService.DEFAULT_DOMAIN.equalsIgnoreCase(tenant)) + { + return true; + } + + return AuthenticationUtil.runAsSystem(new RunAsWork() + { + public Boolean doWork() throws Exception + { + return tenantAdminService.existsTenant(tenant) && tenantAdminService.isEnabled(); + } + }); + } + + /** + * Authenticate user against network/tenant. + * + * @param username String + * @param networkId String + * @return true => authenticated, false => not authenticated + */ + public boolean authenticateTenant(String username, String networkId) + { + boolean authenticated = false; + + String userNetworkId = tenantAdminService.getUserDomain(username); + if(userNetworkId == null || userNetworkId.equals(TenantService.DEFAULT_DOMAIN)) + { + if(networkId.equalsIgnoreCase(TenantUtil.DEFAULT_TENANT) || networkId.equalsIgnoreCase(TenantUtil.SYSTEM_TENANT)) + { + authenticated = true; + } + } + else + { + if(networkId.equalsIgnoreCase(TenantUtil.DEFAULT_TENANT)) + { + networkId = userNetworkId; + } + + if(userNetworkId.equalsIgnoreCase(networkId)) + { + authenticated = tenantAdminService.isEnabledTenant(networkId); + } + } + + return authenticated; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantWebScriptServletRequest.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantWebScriptServletRequest.java new file mode 100644 index 00000000000..c264769ce7e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantWebScriptServletRequest.java @@ -0,0 +1,83 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.web.scripts.TenantWebScriptServletRequest; +import org.springframework.extensions.config.ServerProperties; +import org.springframework.extensions.webscripts.Match; +import org.springframework.extensions.webscripts.Runtime; + +public class PublicApiTenantWebScriptServletRequest extends TenantWebScriptServletRequest +{ + public static final String NETWORKS_PATH = "networks"; + public static final String NETWORK_PATH = "network"; + + public PublicApiTenantWebScriptServletRequest(Runtime container, HttpServletRequest req, Match serviceMatch, ServerProperties serverProperties) + { + super(container, req, serviceMatch, serverProperties); + } + + @Override + protected void parse() + { + String realPathInfo = getRealPathInfo(); + + if(realPathInfo.equals("") || realPathInfo.equals("/")) + { + // no tenant - "index" request + tenant = TenantUtil.DEFAULT_TENANT; + pathInfo = NETWORKS_PATH; + } + else if (realPathInfo.equals("/discovery")) + { + // The '/discovery' API is special and doesn't need network info, however, + // we set the tenant to default, to satisfy PublicApiTenantAuthentication logic. + tenant = TenantUtil.DEFAULT_TENANT; + pathInfo = realPathInfo; + } + else + { + // optimisation - don't need to lowercase the whole path + if(realPathInfo.substring(0, 5).toLowerCase().equals("/cmis")) + { + // cmis service document, pass through as is and set tenant to "-default-". + tenant = TenantUtil.DEFAULT_TENANT; + pathInfo = realPathInfo; + } + else + { + int idx = realPathInfo.indexOf('/', 1); + + // remove tenant + tenant = realPathInfo.substring(1, idx == -1 ? realPathInfo.length() : idx); + pathInfo = realPathInfo.substring(tenant.length() + 1); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantWebScriptServletRuntime.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantWebScriptServletRuntime.java new file mode 100644 index 00000000000..8df1321ea19 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiTenantWebScriptServletRuntime.java @@ -0,0 +1,154 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.io.IOException; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.web.scripts.TenantWebScriptServletRuntime; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.springframework.extensions.config.ServerProperties; +import org.springframework.extensions.surf.util.URLDecoder; +import org.springframework.extensions.webscripts.*; +import org.springframework.extensions.webscripts.servlet.ServletAuthenticatorFactory; + +public class PublicApiTenantWebScriptServletRuntime extends TenantWebScriptServletRuntime implements ResponseWriter +{ + private static final Pattern CMIS_URI_PATTERN = Pattern.compile(".*/cmis/versions/[0-9]+\\.[0-9]+/.*"); + private ApiAssistant apiAssistant; + + public PublicApiTenantWebScriptServletRuntime(RuntimeContainer container, ServletAuthenticatorFactory authFactory, HttpServletRequest req, + HttpServletResponse res, ServerProperties serverProperties, ApiAssistant apiAssistant) + { + super(container, authFactory, req, res, serverProperties); + this.apiAssistant = apiAssistant; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRuntime#getScriptUrl() + */ + @Override + protected String getScriptUrl() + { + // NOTE: Don't use req.getPathInfo() - it truncates the path at first semi-colon in Tomcat + final String requestURI = req.getRequestURI(); + final String serviceContextPath = req.getContextPath() + req.getServletPath(); + String pathInfo; + + if (serviceContextPath.length() > requestURI.length()) + { + // NOTE: assume a redirect has taken place e.g. tomcat welcome-page + // NOTE: this is unlikely, and we'll take the hit if the path contains a semi-colon + pathInfo = req.getPathInfo(); + } + // MNT-13057 fix, do not decode CMIS uris. + else if (CMIS_URI_PATTERN.matcher(requestURI).matches()) + { + pathInfo = requestURI.substring(serviceContextPath.length()); + } + else + { + pathInfo = URLDecoder.decode(requestURI.substring(serviceContextPath.length())); + } + + // NOTE: must contain at least root / and single character for tenant name + if (pathInfo.length() < 2 || pathInfo.equals("/")) + { + // url path has no tenant id -> get networks request + pathInfo = PublicApiTenantWebScriptServletRequest.NETWORKS_PATH; + } + else + { + if(!pathInfo.substring(0, 6).toLowerCase().equals("/cmis/") && !pathInfo.equals("/discovery")) + { + // remove tenant + int idx = pathInfo.indexOf('/', 1); + pathInfo = pathInfo.substring(idx == -1 ? pathInfo.length() : idx); + if(pathInfo.equals("") || pathInfo.equals("/")) + { + // url path is just a tenant id -> get network request + pathInfo = PublicApiTenantWebScriptServletRequest.NETWORK_PATH; + } + } + } + + return pathInfo; + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptRuntime#createRequest(org.alfresco.web.scripts.WebScriptMatch) + */ + @Override + protected WebScriptRequest createRequest(Match match) + { +// try +// { + // make the request input stream a BufferedInputStream so that the first x bytes can be reused. +// PublicApiHttpServletRequest wrapped = new PublicApiHttpServletRequest(req); + + // TODO: construct org.springframework.extensions.webscripts.servlet.WebScriptServletResponse when + // org.alfresco.web.scripts.WebScriptServletResponse (deprecated) is removed + servletReq = new PublicApiTenantWebScriptServletRequest(this, req, match, serverProperties); + return servletReq; +// } +// catch(IOException e) +// { +// throw new AlfrescoRuntimeException("", e); +// } + } + + /* (non-Javadoc) + * @see org.alfresco.web.scripts.WebScriptContainer#getName() + */ + public String getName() + { + return "PublicApiTenantServletRuntime"; + } + + @Override + protected void renderErrorResponse(Match match, Throwable exception, WebScriptRequest request, WebScriptResponse response) { + + //If its cmis or not an exception then use the default behaviour + if (CMIS_URI_PATTERN.matcher(req.getRequestURI()).matches() || !(exception instanceof Exception)) + { + super.renderErrorResponse(match, exception, request, response); + } + else + { + try { + renderException((Exception)exception, response, apiAssistant); + } catch (IOException e) { + logger.error("Internal error", e); + throw new WebScriptException("Internal error", e); + } + } + + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/PublicApiWebScriptServlet.java b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiWebScriptServlet.java new file mode 100644 index 00000000000..f977d5a0005 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/PublicApiWebScriptServlet.java @@ -0,0 +1,58 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.web.scripts.TenantWebScriptServlet; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.springframework.context.ApplicationContext; +import org.springframework.extensions.webscripts.RuntimeContainer; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRuntime; +import org.springframework.web.context.support.WebApplicationContextUtils; + +public class PublicApiWebScriptServlet extends TenantWebScriptServlet +{ + private static final long serialVersionUID = 726730674397482039L; + private ApiAssistant apiAssistant; + @Override + public void init() throws ServletException + { + super.init(); + + ApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + container = (RuntimeContainer)context.getBean("publicapi.container"); + apiAssistant = (ApiAssistant) context.getBean("apiAssistant"); + } + + protected WebScriptServletRuntime getRuntime(HttpServletRequest req, HttpServletResponse res) + { + WebScriptServletRuntime runtime = new PublicApiTenantWebScriptServletRuntime(container, authenticatorFactory, req, res, serverProperties, apiAssistant); + return runtime; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Queries.java b/remote-api/src/main/java/org/alfresco/rest/api/Queries.java new file mode 100644 index 00000000000..93d5b5f3c1a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Queries.java @@ -0,0 +1,98 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api; + +import org.alfresco.model.ContentModel; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.api.model.Site; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.rest.framework.webscripts.ResourceWebScriptHelper; + +/** + * Queries API + * + * @author janv + * @author Alan Davis + */ +public interface Queries +{ + // General + static String PARAM_TERM = "term"; + static String PARAM_ORDERBY = RecognizedParamsExtractor.PARAM_ORDERBY; + static String PARAM_FIELDS = RecognizedParamsExtractor.PARAM_FILTER_FIELDS; + static String PARAM_INCLUDE = RecognizedParamsExtractor.PARAM_INCLUDE; + + // Node query + static String PARAM_ROOT_NODE_ID = "rootNodeId"; + static String PARAM_NODE_TYPE = "nodeType"; + static String PARAM_NAME = "name"; + static String PARAM_CREATEDAT = "createdAt"; + static String PARAM_MODIFIEDAT = "modifiedAt"; + static int MIN_TERM_LENGTH_NODES = 3; + + // People query + static String PARAM_PERSON_ID = "id"; + static String PARAM_FIRSTNAME = ContentModel.PROP_FIRSTNAME.getLocalName(); + static String PARAM_LASTNAME = ContentModel.PROP_LASTNAME.getLocalName(); + static int MIN_TERM_LENGTH_PEOPLE = 2; + + // Sites query + static String PARAM_SITE_ID = "id"; + static String PARAM_SITE_TITLE = "title"; + static String PARAM_SITE_DESCRIPTION = "description"; + static int MIN_TERM_LENGTH_SITES = 2; + + /** + * Find Nodes + * + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * + * @return the search query results + */ + CollectionWithPagingInfo findNodes(Parameters parameters); + + /** + * Find People + * + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * + * @return the search query results + */ + CollectionWithPagingInfo findPeople(Parameters parameters); + + /** + * Find Sites + * + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * + * @return the search query results + */ + CollectionWithPagingInfo findSites(Parameters parameters); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/QuickShareLinks.java b/remote-api/src/main/java/org/alfresco/rest/api/QuickShareLinks.java new file mode 100644 index 00000000000..1e40851d498 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/QuickShareLinks.java @@ -0,0 +1,142 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.QuickShareLink; +import org.alfresco.rest.api.model.QuickShareLinkEmailRequest; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +import java.util.List; + +/** + * Centralises access to quick share services and maps between representations. + * + * @author janv + * @author Jamal Kaabi-Mofrad + * + * @since publicapi1.0 + */ +public interface QuickShareLinks +{ + /** + * Returns limited metadata regarding the shared (content) link. + * + * Note: does *not* require authenticated access for (public) shared link. + */ + QuickShareLink readById(String sharedId, Parameters parameters); + + /** + * Download file content (or rendition content) via shared link. + * + * Note: does *not* require authenticated access for (public) shared link. + * + * @param sharedId + * @param renditionId - optional + * @param parameters {@link Parameters} + * @return + * @throws EntityNotFoundException + */ + BinaryResource readProperty(String sharedId, String renditionId, Parameters parameters) throws EntityNotFoundException; + + /** + * Gets information about a rendition of a shared link. + * + * @param shareId + * @param renditionId + * @return the {@link Rendition} object + */ + Rendition getRendition(String shareId, String renditionId); + + /** + * List renditions info - note: only returns available (=> created) renditions. + * + * Note: does *not* require authenticated access for (public) shared link. + * + * @param sharedId + * @return + */ + CollectionWithPagingInfo getRenditions(String sharedId); + + /** + * Delete the shared link. + * + * Once deleted, the shared link will no longer exist hence get/download will no longer work (ie. return 404). + * If the link is later re-created then a new unique shared id will be generated. + * + * Requires authenticated access. + * + * @param sharedId String id of the quick share + */ + void delete(String sharedId, Parameters parameters); + + /** + * Create quick share. + * + * Requires authenticated access. + * + * @param nodeIds + * @param parameters + * @return + */ + List create(List nodeIds, Parameters parameters); + + /** + * Notifies users by email that a content has been shared with them. + * + * @param sharedId The string id of the quick share + * @param emailRequest The email details including its template details + * @param parameters The {@link Parameters} object to get the parameters passed into the request + */ + void emailSharedLink(String sharedId, QuickShareLinkEmailRequest emailRequest, Parameters parameters); + + /** + * Find (search) for shared links visible to current user. + * Optionally filter by "sharedByUser/id" (if -me- then filter by current user). + * + * @param parameters + * @return + */ + CollectionWithPagingInfo findLinks(Parameters parameters); + + /** + * API Constants - query parameters, etc + */ + String PARAM_SHAREDBY = "sharedByUser"; + + String PARAM_INCLUDE_ALLOWABLEOPERATIONS = Nodes.PARAM_INCLUDE_ALLOWABLEOPERATIONS; + + String PARAM_INCLUDE_PATH = Nodes.PARAM_INCLUDE_PATH; + + String PARAM_INCLUDE_PROPERTIES = Nodes.PARAM_INCLUDE_PROPERTIES; + + String PARAM_INCLUDE_ISFAVORITE = Nodes.PARAM_INCLUDE_ISFAVORITE; + + String PARAM_INCLUDE_ASPECTNAMES = Nodes.PARAM_INCLUDE_ASPECTNAMES; +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java b/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java new file mode 100644 index 00000000000..ca179312fae --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Renditions.java @@ -0,0 +1,194 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.NodeRef; + +import java.util.List; + +/** + * Renditions API + * + * @author Jamal Kaabi-Mofrad, janv + */ +public interface Renditions +{ + String PARAM_STATUS = "status"; + + String PARAM_ATTACHMENT = "attachment"; + String PARAM_PLACEHOLDER = "placeholder"; + + /** + * Lists all available renditions includes those that have been created and those that are yet to be created. + * + * @param nodeRef the source/live nodeRef + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition results + */ + CollectionWithPagingInfo getRenditions(NodeRef nodeRef, Parameters parameters); + + /** + * Lists all available renditions includes those that have been created and those that are yet to be created. + * + * @param nodeRef the source/live nodeRef + * @param versionId the version id (aka version label) + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition results + */ + CollectionWithPagingInfo getRenditions(NodeRef nodeRef, String versionId, Parameters parameters); + + /** + * Gets information about a rendition of a node in the repository. + * If there is no rendition, then returns the available/registered rendition. + * + * @param nodeRef the source nodeRef, ie. live node + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the {@link Rendition} object + */ + Rendition getRendition(NodeRef nodeRef, String renditionId, Parameters parameters); + + /** + * Gets information about a rendition of a node in the repository. + * If there is no rendition, then returns the available/registered rendition. + * + * @param nodeRef the source nodeRef, ie. live node + * @param versionId the version id (aka version label) + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the {@link Rendition} object + */ + Rendition getRendition(NodeRef nodeRef, String versionId, String renditionId, Parameters parameters); + + /** + * Creates a rendition for the given node asynchronously. + * + * @param nodeRef the source nodeRef, ie. live node + * @param rendition the {@link Rendition} request + * @param parameters the {@link Parameters} object to get the parameters passed into the request + */ + void createRendition(NodeRef nodeRef, Rendition rendition, Parameters parameters); + + /** + * Creates a rendition for the given node - either async r sync + * + * @param nodeRef the source nodeRef, ie. live node + * @param rendition the {@link Rendition} request + * @param executeAsync + * @param parameters + */ + void createRendition(NodeRef nodeRef, Rendition rendition, boolean executeAsync, Parameters parameters); + + /** + * Creates a rendition for the given node - either async r sync + * + * @param nodeRef the source nodeRef, ie. live node + * @param versionId the version id (aka version label) + * @param rendition the {@link Rendition} request + * @param executeAsync + * @param parameters + */ + void createRendition(NodeRef nodeRef, String versionId, Rendition rendition, boolean executeAsync, Parameters parameters); + + /** + * Creates renditions that don't already exist for the given node asynchronously. + * + * @param nodeRef the source nodeRef, ie. live node + * @param renditions the list of {@link Rendition} requests + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @throws NotFoundException if any of the rendition id do not exist. + * @throws ConstraintViolatedException if all of the renditions already exist. + */ + void createRenditions(NodeRef nodeRef, List renditions, Parameters parameters) + throws NotFoundException, ConstraintViolatedException; + + /** + * Creates renditions that don't already exist for the given node asynchronously. + * + * @param nodeRef the source nodeRef, ie. live node + * @param versionId the version id (aka version label) + * @param renditions the list of {@link Rendition} requests + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @throws NotFoundException if any of the rendition id do not exist. + * @throws ConstraintViolatedException if all of the renditions already exist. + */ + void createRenditions(NodeRef nodeRef, String versionId, List renditions, Parameters parameters) + throws NotFoundException, ConstraintViolatedException; + + /** + * Downloads rendition. + * + * @param nodeRef the source nodeRef, ie. live node + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition stream + */ + BinaryResource getContent(NodeRef nodeRef, String renditionId, Parameters parameters); + + /** + * Downloads rendition. + * + * @param nodeRef the source nodeRef, ie. live node + * @param versionId the version id (aka version label) + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition stream + */ + BinaryResource getContent(NodeRef nodeRef, String versionId, String renditionId, Parameters parameters); + + /** + * Downloads rendition. + * + * @param nodeRef the source nodeRef, ie. live node + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition stream + */ + BinaryResource getContentNoValidation(NodeRef nodeRef, String renditionId, Parameters parameters); + + /** + * Downloads rendition. + * + * @param nodeRef the source nodeRef, ie. live node + * @param versionId the version id (aka version label) + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition stream + */ + BinaryResource getContentNoValidation(NodeRef nodeRef, String versionId, String renditionId, Parameters parameters); + + DirectAccessUrl requestContentUrl(String nodeId, String versionId, String renditionId, DirectAccessUrlRequest directAccessUrlRequest); +} + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/SiteMembershipRequests.java b/remote-api/src/main/java/org/alfresco/rest/api/SiteMembershipRequests.java new file mode 100644 index 00000000000..fa6892b1dfd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/SiteMembershipRequests.java @@ -0,0 +1,93 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.SiteMembershipApproval; +import org.alfresco.rest.api.model.SiteMembershipRejection; +import org.alfresco.rest.api.model.SiteMembershipRequest; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +/** + * Public REST API: centralises access to site membership requests and maps between representations. + * + * @author steveglover + * + */ +public interface SiteMembershipRequests +{ + String PARAM_SITE_ID = "siteId"; + String PARAM_PERSON_ID = "personId"; + + /** + * Create a site membership request for the user 'inviteeId' + * @param inviteeId the site inviteee id + * @param siteInvite the site invite + * @return SiteMembershipRequest + */ + SiteMembershipRequest createSiteMembershipRequest(String inviteeId, final SiteMembershipRequest siteInvite); + + /** + * Update the site membership request for inviteeId and site + * @param inviteeId the site inviteee id + * @param siteInvite the site invite + * @return the updated siteMembershipRequest + */ + SiteMembershipRequest updateSiteMembershipRequest(String inviteeId, final SiteMembershipRequest siteInvite); + + /** + * Cancel site membership request for invitee and site. + * + * @param inviteeId the site inviteee id + * @param siteId the site id + */ + void cancelSiteMembershipRequest(String inviteeId, String siteId); + + /** + * Get the site membership request for inviteeId and siteId, if it exists. + * + * @param inviteeId the site inviteee id + * @param siteId the site id + * @return the site membership request + */ + SiteMembershipRequest getSiteMembershipRequest(String inviteeId, String siteId); + + /** + * Get a paged list of site membership requests for inviteeId. + * + * @param inviteeId the site inviteee id + * @param paging paging information + * @return a paged list of site membership requests + */ + CollectionWithPagingInfo getPagedSiteMembershipRequests(String inviteeId, Paging paging); + + CollectionWithPagingInfo getPagedSiteMembershipRequests(final Parameters parameters); + + void approveSiteMembershipRequest(String siteId, String inviteeId, SiteMembershipApproval siteMembershipApproval); + + void rejectSiteMembershipRequest(String siteId, String inviteeId, SiteMembershipRejection siteMembershipRejection); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Sites.java b/remote-api/src/main/java/org/alfresco/rest/api/Sites.java new file mode 100644 index 00000000000..15a4dbd299a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Sites.java @@ -0,0 +1,87 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.query.PagingResults; +import org.alfresco.rest.api.model.*; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; + +public interface Sites +{ + SiteInfo validateSite(String siteShortName); + SiteInfo validateSite(NodeRef nodeRef); + CollectionWithPagingInfo getSiteMembers(String siteShortName, Parameters parameters); + Site getSite(String siteId); + void deleteSite(String siteId, Parameters parameters); + Site createSite(Site site, Parameters parameters); + Site updateSite(String siteId, SiteUpdate site, Parameters parameters); + + /** + * people//sites/ + * + * @param personId String + * @param siteShortName String + * @return MemberOfSite + */ + MemberOfSite getMemberOfSite(String personId, String siteShortName); + SiteMember getSiteMember(String personId, String siteShortName); + SiteMember addSiteMember(String siteShortName, SiteMember siteMember); + void removeSiteMember(String personId, String siteId); + SiteMember updateSiteMember(String siteShortName, SiteMember siteMember); + CollectionWithPagingInfo getSites(String personId, Parameters parameters); + SiteContainer getSiteContainer(String siteShortName, String containerId); + PagingResults getSiteContainers(String siteShortName, Paging paging); + CollectionWithPagingInfo getSites(Parameters parameters); + FavouriteSite getFavouriteSite(String personId, String siteShortName); + void addFavouriteSite(String personId, FavouriteSite favouriteSite); + void removeFavouriteSite(String personId, String siteId); + CollectionWithPagingInfo getFavouriteSites(String personId, Parameters parameters); + + String getSiteRole(String siteId); + String getSiteRole(String siteId, String personId); + + CollectionWithPagingInfo getSiteGroupMemberships(String siteId, Parameters parameters); + SiteGroup addSiteGroupMembership(String siteId, SiteGroup group); + SiteGroup getSiteGroupMembership(String siteId, String groupId); + SiteGroup updateSiteGroupMembership(String siteId, SiteGroup group); + void removeSiteGroupMembership(String groupId, String siteId); + + String PARAM_PERMANENT = "permanent"; + String PARAM_SKIP_ADDTOFAVORITES = "skipAddToFavorites"; + String PARAM_SKIP_SURF_CONFIGURATION = "skipConfiguration"; + + String PARAM_SITE_ID = "id"; + String PARAM_SITE_TITLE = "title"; + String PARAM_SITE_DESCRIPTION = "description"; + + String PARAM_SITE_ROLE = "role"; + String PARAM_VISIBILITY = "visibility"; + String PARAM_PRESET = "preset"; +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Tags.java b/remote-api/src/main/java/org/alfresco/rest/api/Tags.java new file mode 100644 index 00000000000..6c8971a12a8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/Tags.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import java.util.List; + +import org.alfresco.rest.api.model.Tag; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.StoreRef; + +public interface Tags +{ + public List addTags(String nodeId, List tags); + public Tag getTag(StoreRef storeRef, String tagId); + public void deleteTag(String nodeId, String tagId); + public CollectionWithPagingInfo getTags(StoreRef storeRef, Parameters params); + public Tag changeTag(StoreRef storeRef, String tagId, Tag tag); + public CollectionWithPagingInfo getTags(String nodeId, Parameters params); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/TenantCredentials.java b/remote-api/src/main/java/org/alfresco/rest/api/TenantCredentials.java new file mode 100644 index 00000000000..8aa575648a3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/TenantCredentials.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.repo.web.auth.WebCredentials; +import org.alfresco.repo.web.scripts.servlet.BasicHttpAuthenticatorFactory.BasicHttpAuthenticator; + +/** + * {@link WebCredentials} class which wraps the credentials from the {@link BasicHttpAuthenticator} and adds + * additional information related to TenantBased logins. + * + * @author Alex Miller + * @since Cloud Sprint 5 + */ +public class TenantCredentials implements WebCredentials +{ + private static final long serialVersionUID = -3877007259822281712L; + + private String tenant; + private String email; + private WebCredentials originalCredentials; + + public TenantCredentials(String tenant, String email, WebCredentials orignalCredentials) + { + this.tenant = tenant; + this.email = email; + this.originalCredentials = orignalCredentials; + } + + public WebCredentials getOriginalCredentials() + { + return originalCredentials; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.email == null) ? 0 : this.email.hashCode()); + result = prime + * result + + ((this.originalCredentials == null) ? 0 : this.originalCredentials.hashCode()); + result = prime * result + ((this.tenant == null) ? 0 : this.tenant.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; } + TenantCredentials other = (TenantCredentials) obj; + if (this.email == null) + { + if (other.email != null) { return false; } + } + else if (!this.email.equals(other.email)) { return false; } + if (this.originalCredentials == null) + { + if (other.originalCredentials != null) { return false; } + } + else if (!this.originalCredentials.equals(other.originalCredentials)) { return false; } + if (this.tenant == null) + { + if (other.tenant != null) { return false; } + } + else if (!this.tenant.equals(other.tenant)) { return false; } + return true; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionDefinitionsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionDefinitionsEntityResource.java new file mode 100644 index 00000000000..e8ab760b66e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionDefinitionsEntityResource.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.actions; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; + +@EntityResource(name="action-definitions", title = "Actions") +public class ActionDefinitionsEntityResource implements EntityResourceAction.Read, EntityResourceAction.ReadById +{ + private Actions actions; + + public void setActions(Actions actions) + { + this.actions = actions; + } + + @Override + public CollectionWithPagingInfo readAll(Parameters params) + { + return actions.getActionDefinitions(params); + } + + @Override + public ActionDefinition readById(String id, Parameters parameters) throws EntityNotFoundException + { + return actions.getActionDefinitionById(id); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionExecutionsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionExecutionsEntityResource.java new file mode 100644 index 00000000000..30f2594fd15 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionExecutionsEntityResource.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.actions; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.Action; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.springframework.extensions.webscripts.Status; + +@EntityResource(name = "action-executions", title = "Actions") +public class ActionExecutionsEntityResource implements EntityResourceAction.Create +{ + private Actions actions; + + public void setActions(Actions actions) + { + this.actions = actions; + } + + @WebApiDescription(title = "Execute action", successStatus = Status.STATUS_ACCEPTED) + @Override + public List create(List entity, Parameters parameters) + { + if (entity == null || entity.size() != 1) + { + throw new InvalidArgumentException("Please specify one action request only."); + } + + List result = new ArrayList<>(1); + result.add(actions.executeAction(entity.get(0), parameters)); + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/actions/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/actions/package-info.java new file mode 100644 index 00000000000..77b88184974 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/actions/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope= Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.actions; + +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; diff --git a/remote-api/src/main/java/org/alfresco/rest/api/audit/AuditApplicationsAuditEntriesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/audit/AuditApplicationsAuditEntriesRelation.java new file mode 100644 index 00000000000..79c209cb68c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/audit/AuditApplicationsAuditEntriesRelation.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.audit; + +import org.alfresco.rest.api.Audit; +import org.alfresco.rest.api.model.AuditEntry; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Audit Entries (within the context of an Audit Application) + * + * @author anechifor, janv + */ +@RelationshipResource(name = "audit-entries", entityResource = AuditApplicationsEntityResource.class, title = "Audit Application Entries") +public class AuditApplicationsAuditEntriesRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, RelationshipResourceAction.Delete, RelationshipResourceAction.DeleteSet, InitializingBean +{ + private Audit audit; + + public void setAudit(Audit audit) + { + this.audit = audit; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("audit", this.audit); + } + + @WebApiDescription(title = "Returns audit entries for audit app id") + @Override + public CollectionWithPagingInfo readAll(String auditAppId, Parameters parameters) + { + return audit.listAuditEntries(auditAppId, parameters); + } + + @Override + @WebApiDescription(title = "Return audit entry id for audit app id") + public AuditEntry readById(String auditAppId, String auditEntryId, Parameters parameters) throws RelationshipResourceNotFoundException + { + return audit.getAuditEntry(auditAppId, Long.valueOf(auditEntryId), parameters); + } + + @Override + @WebApiDescription(title = "Delete audit entry id for audit app id") + public void delete(String auditAppId, String auditEntryId, Parameters parameters) throws RelationshipResourceNotFoundException + { + audit.deleteAuditEntry(auditAppId, Long.valueOf(auditEntryId), parameters); + } + + @Override + @WebApiDescription(title = "Delete collection/set of audit entries for audit app id - based on params") + public void deleteSet(String auditAppId, Parameters parameters) throws RelationshipResourceNotFoundException + { + audit.deleteAuditEntries(auditAppId, parameters); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/audit/AuditApplicationsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/audit/AuditApplicationsEntityResource.java new file mode 100644 index 00000000000..5ce75ebb074 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/audit/AuditApplicationsEntityResource.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.audit; + +import org.alfresco.rest.api.Audit; +import org.alfresco.rest.api.model.AuditApp; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * An implementation of an Entity Resource for handling audit applications + * + * @author janv, anechifor, eknizat + */ +@EntityResource(name = "audit-applications", title = "Audit Applications") +public class AuditApplicationsEntityResource implements EntityResourceAction.ReadById, EntityResourceAction.Read, + EntityResourceAction.Update, InitializingBean +{ + private Audit audit; + + public void setAudit(Audit audit) + { + this.audit = audit; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("audit", this.audit); + } + + @Override + @WebApiDescription(title = "Returns audit application for audit app id") + public AuditApp readById(String auditAppId, Parameters parameters) throws EntityNotFoundException + { + return audit.getAuditApp(auditAppId, parameters); + } + + @Override + @WebApiDescription(title = "Update audit", description = "Update audit") + public AuditApp update(String auditAppId, AuditApp auditApp, Parameters parameters) + { + return audit.update(auditAppId, auditApp, parameters); + } + + @Override + @WebApiDescription(title = "Get List of audit applications", description = "Get List of Audit Applications") + public CollectionWithPagingInfo readAll(Parameters parameters) + { + return audit.getAuditApps(parameters.getPaging()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/audit/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/audit/package-info.java new file mode 100644 index 00000000000..3f0c9a02fd2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/audit/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.audit; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java new file mode 100644 index 00000000000..46ffe179e72 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/authentications/AuthenticationTicketsEntityResource.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.authentications; + +import org.alfresco.rest.api.Authentications; +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +import java.util.Collections; +import java.util.List; + +/** + * @author Jamal Kaabi-Mofrad + */ +@EntityResource(name = "tickets", title = "Authentication tickets") +public class AuthenticationTicketsEntityResource implements EntityResourceAction.Create, + EntityResourceAction.ReadByIdWithResponse, + EntityResourceAction.DeleteWithResponse, + InitializingBean +{ + // tickets => @EntityResource(name = "tickets" ... + public static final String COLLECTION_RESOURCE_NAME = "tickets"; + + private Authentications authentications; + + public void setAuthentications(Authentications authentications) + { + this.authentications = authentications; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "authentications", authentications); + } + + @WebApiDescription(title = "Login", description = "Login.") + @WebApiNoAuth + @Override + public List create(List entity, Parameters parameters) + { + if (entity == null || entity.size() != 1) + { + throw new InvalidArgumentException("Please specify one login request only."); + } + LoginTicket result = authentications.createTicket(entity.get(0), parameters); + return Collections.singletonList(result); + } + + @WebApiDescription(title = "Validate login ticket", description = "Validate login ticket.") + @Override + public LoginTicket readById(String me, Parameters parameters, WithResponse withResponse) + { + return authentications.validateTicket(me, parameters, withResponse); + } + + @WebApiDescription(title = "Logout", description = "Logout.") + @Override + public void delete(String me, Parameters parameters, WithResponse withResponse) + { + authentications.deleteTicket(me, parameters, withResponse); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/authentications/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/authentications/package-info.java new file mode 100644 index 00000000000..a839c435103 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/authentications/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name = "authentication", scope = Api.SCOPE.PUBLIC, version = 1) +package org.alfresco.rest.api.authentications; + +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; diff --git a/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelAspectsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelAspectsRelation.java new file mode 100644 index 00000000000..ec9eebbf919 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelAspectsRelation.java @@ -0,0 +1,106 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.cmm; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.CustomAspect; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "aspects", entityResource = CustomModelEntityResource.class, title = "Custom Model Aspects") +public class CustomModelAspectsRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Create, + RelationshipResourceAction.Update, + RelationshipResourceAction.Delete, + InitializingBean +{ + + private CustomModels customModels; + + public void setCustomModels(CustomModels customModels) + { + this.customModels = customModels; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "customModels", customModels); + } + + @Override + @WebApiDescription(title = "Returns a paged list of all the custom model's aspects.") + public CollectionWithPagingInfo readAll(String modelName, Parameters parameters) + { + return customModels.getCustomAspects(modelName, parameters); + } + + @Override + @WebApiDescription(title = "Returns custom aspect information for the given 'aspectName' in 'modelName'.") + public CustomAspect readById(String modelName, String aspectName, Parameters parameters) + { + return customModels.getCustomAspect(modelName, aspectName, parameters); + } + + @Override + @WebApiDescription(title = "Removes the custom aspect for the given 'aspectName' in 'modelName'.") + public void delete(String modelName, String aspectName, Parameters parameters) + { + customModels.deleteCustomAspect(modelName, aspectName); + } + + @Override + @WebApiDescription(title = "Updates the custom aspect in the given 'modelName'.") + public CustomAspect update(String modelName, CustomAspect aspect, Parameters parameters) + { + return customModels.updateCustomAspect(modelName, aspect, parameters); + } + + @Override + @WebApiDescription(title = "Creates custom aspects for the model 'modelName'.") + public List create(String modelName, List aspects, Parameters parameters) + { + List result = new ArrayList<>(aspects.size()); + for (CustomAspect aspect : aspects) + { + result.add(customModels.createCustomAspect(modelName, aspect)); + } + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelConstraintRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelConstraintRelation.java new file mode 100644 index 00000000000..4800b5780ca --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelConstraintRelation.java @@ -0,0 +1,90 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.cmm; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.CustomModelConstraint; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "constraints", entityResource = CustomModelEntityResource.class, title = "Custom Model Constraints") +public class CustomModelConstraintRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Create, + InitializingBean +{ + + private CustomModels customModels; + + public void setCustomModels(CustomModels customModels) + { + this.customModels = customModels; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "customModels", customModels); + } + + @Override + @WebApiDescription(title = "Returns a paged list of all the custom model's constraints.") + public CollectionWithPagingInfo readAll(String modelName, Parameters parameters) + { + return customModels.getCustomModelConstraints(modelName, parameters); + } + + @Override + @WebApiDescription(title = "Returns custom constraint information for the given 'constraintName' in 'modelName'.") + public CustomModelConstraint readById(String modelName, String constraintName, Parameters parameters) + { + return customModels.getCustomModelConstraint(modelName, constraintName, parameters); + } + + @Override + @WebApiDescription(title = "Creates custom constraints for the model 'modelName'.") + public List create(String modelName, List constraints, Parameters parameters) + { + List result = new ArrayList<>(constraints.size()); + for (CustomModelConstraint constraint : constraints) + { + result.add(customModels.createCustomModelConstraint(modelName, constraint)); + } + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelDownloadRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelDownloadRelation.java new file mode 100644 index 00000000000..5f7b043d324 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelDownloadRelation.java @@ -0,0 +1,68 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.cmm; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.CustomModelDownload; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "download", entityResource = CustomModelEntityResource.class, title = "Custom Model Download") +public class CustomModelDownloadRelation implements RelationshipResourceAction.Create, InitializingBean +{ + + private CustomModels customModels; + + public void setCustomModels(CustomModels customModels) + { + this.customModels = customModels; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "customModels", customModels); + } + + @Override + @WebApiDescription(title = "Creates download node containing the custom model file and if specified, its associated Share extension module file.") + public List create(String modelName, List download, Parameters parameters) + { + CustomModelDownload result = customModels.createDownload(modelName, parameters); + return Collections.singletonList(result); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelEntityResource.java new file mode 100644 index 00000000000..6eb5eb07378 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelEntityResource.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.cmm; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.CustomModel; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Jamal Kaabi-Mofrad + */ +@EntityResource(name = "cmm", title = "Custom Model Management") +public class CustomModelEntityResource implements EntityResourceAction.Read, + EntityResourceAction.ReadById, + EntityResourceAction.Create, + EntityResourceAction.Update, + EntityResourceAction.Delete, + InitializingBean +{ + + private CustomModels customModels; + + public void setCustomModels(CustomModels customModels) + { + this.customModels = customModels; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "customModels", customModels); + } + + @Override + @WebApiDescription(title="Returns custom model information for the given model name.") + public CustomModel readById(String modelName, Parameters parameters) throws EntityNotFoundException + { + return customModels.getCustomModel(modelName, parameters); + } + + @Override + @WebApiDescription(title="Returns a paged list of all custom models.") + public CollectionWithPagingInfo readAll(Parameters parameters) + { + return customModels.getCustomModels(parameters); + } + + @Override + @WebApiDescription(title="Creates custom model(s).") + public List create(List entity, Parameters parameters) + { + List result = new ArrayList<>(entity.size()); + for (CustomModel cm : entity) + { + result.add(customModels.createCustomModel(cm)); + } + return result; + } + + @Override + @WebApiDescription(title = "Updates or activates/deactivates the custom model.") + public CustomModel update(String modelName, CustomModel entity, Parameters parameters) + { + return customModels.updateCustomModel(modelName, entity, parameters); + } + + @Override + @WebApiDescription(title = "Deletes the custom model.") + public void delete(String modelName, Parameters parameters) + { + customModels.deleteCustomModel(modelName); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelTypesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelTypesRelation.java new file mode 100644 index 00000000000..28d1dd77203 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/cmm/CustomModelTypesRelation.java @@ -0,0 +1,106 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.cmm; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.CustomType; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "types", entityResource = CustomModelEntityResource.class, title = "Custom Model Types") +public class CustomModelTypesRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Create, + RelationshipResourceAction.Update, + RelationshipResourceAction.Delete, + InitializingBean +{ + + private CustomModels customModels; + + public void setCustomModels(CustomModels customModels) + { + this.customModels = customModels; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "customModels", customModels); + } + + @Override + @WebApiDescription(title = "Returns a paged list of all the custom model's types.") + public CollectionWithPagingInfo readAll(String modelName, Parameters parameters) + { + return customModels.getCustomTypes(modelName, parameters); + } + + @Override + @WebApiDescription(title = "Returns custom type information for the given 'typeName' in 'modelName'.") + public CustomType readById(String modelName, String typeName, Parameters parameters) + { + return customModels.getCustomType(modelName, typeName, parameters); + } + + @Override + @WebApiDescription(title = "Removes the custom type for the given 'typeName' in 'modelName'.") + public void delete(String modelName, String typeName, Parameters parameters) + { + customModels.deleteCustomType(modelName, typeName); + } + + @Override + @WebApiDescription(title = "Updates the custom type in the given 'modelName'.") + public CustomType update(String modelName, CustomType type, Parameters parameters) + { + return customModels.updateCustomType(modelName, type, parameters); + } + + @Override + @WebApiDescription(title = "Creates custom types for the model 'modelName'.") + public List create(String modelName, List types, Parameters parameters) + { + List result = new ArrayList<>(types.size()); + for (CustomType type : types) + { + result.add(customModels.createCustomType(modelName, type)); + } + return result; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/cmm/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/cmm/package-info.java new file mode 100644 index 00000000000..efacb45f418 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/cmm/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PRIVATE, version=1) +package org.alfresco.rest.api.cmm; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; diff --git a/remote-api/src/main/java/org/alfresco/rest/api/discovery/DiscoveryApiWebscript.java b/remote-api/src/main/java/org/alfresco/rest/api/discovery/DiscoveryApiWebscript.java new file mode 100644 index 00000000000..c163447fd0b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/discovery/DiscoveryApiWebscript.java @@ -0,0 +1,197 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.discovery; + +import org.alfresco.rest.api.model.DiscoveryDetails; +import org.alfresco.rest.api.model.ModulePackage; +import org.alfresco.rest.api.model.RepositoryInfo; +import org.alfresco.rest.api.model.RepositoryInfo.LicenseInfo; +import org.alfresco.rest.api.model.RepositoryInfo.StatusInfo; +import org.alfresco.rest.api.model.RepositoryInfo.VersionInfo; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.jacksonextensions.JacksonHelper; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.alfresco.service.cmr.admin.RepoAdminService; +import org.alfresco.service.cmr.audit.AuditService; +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.service.cmr.module.ModuleService; +import org.alfresco.service.cmr.quickshare.QuickShareService; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.descriptor.DescriptorService; +import org.alfresco.util.PropertyCheck; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class DiscoveryApiWebscript extends AbstractWebScript implements RecognizedParamsExtractor, ResponseWriter, InitializingBean +{ + private DescriptorService descriptorService; + private RepoAdminService repoAdminService; + private AuditService auditService; + private QuickShareService quickShareService; + private ModuleService moduleService; + private ApiAssistant assistant; + private ThumbnailService thumbnailService; + + private boolean enabled = true; + private final static String DISABLED = "Not Implemented"; + + public void setDescriptorService(DescriptorService descriptorService) + { + this.descriptorService = descriptorService; + } + + public void setRepoAdminService(RepoAdminService repoAdminService) + { + this.repoAdminService = repoAdminService; + } + + public void setAuditService(AuditService auditService) + { + this.auditService = auditService; + } + + public void setQuickShareService(QuickShareService quickShareService) + { + this.quickShareService = quickShareService; + } + + public void setModuleService(ModuleService moduleService) + { + this.moduleService = moduleService; + } + + public void setAssistant(ApiAssistant assistant) + { + this.assistant = assistant; + } + + public void setThumbnailService(ThumbnailService thumbnailService) + { + this.thumbnailService = thumbnailService; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "descriptorService", descriptorService); + PropertyCheck.mandatory(this, "repoAdminService", repoAdminService); + PropertyCheck.mandatory(this, "auditService", auditService); + PropertyCheck.mandatory(this, "quickShareService", quickShareService); + PropertyCheck.mandatory(this, "moduleService", moduleService); + PropertyCheck.mandatory(this, "assistant", assistant); + PropertyCheck.mandatory(this, "thumbnailService", thumbnailService); + } + + @Override + public void execute(WebScriptRequest webScriptRequest, WebScriptResponse webScriptResponse) throws IOException + { + try + { + checkEnabled(); + + DiscoveryDetails discoveryDetails = new DiscoveryDetails(getRepositoryInfo()); + // Write response + setResponse(webScriptResponse, DEFAULT_SUCCESS); + renderJsonResponse(webScriptResponse, discoveryDetails, assistant.getJsonHelper()); + } + catch (Exception exception) + { + renderException(exception, webScriptResponse, assistant); + } + } + + public RepositoryInfo getRepositoryInfo() + { + LicenseInfo licenseInfo = null; + if(descriptorService.getLicenseDescriptor() != null) + { + licenseInfo = new LicenseInfo(descriptorService.getLicenseDescriptor()); + } + Descriptor serverDescriptor = descriptorService.getServerDescriptor(); + return new RepositoryInfo() + .setId(descriptorService.getCurrentRepositoryDescriptor().getId()) + .setEdition(serverDescriptor.getEdition()) + .setVersion(new VersionInfo(serverDescriptor)) + .setLicense(licenseInfo) + .setModules(getModules()) + .setStatus(new StatusInfo() + .setReadOnly(repoAdminService.getUsage().isReadOnly()) + .setAuditEnabled(auditService.isAuditEnabled()) + .setQuickShareEnabled(quickShareService.isQuickShareEnabled()) + .setThumbnailGenerationEnabled(thumbnailService.getThumbnailsEnabled())); + } + + private List getModules() + { + List details = moduleService.getAllModules(); + if (details.isEmpty()) + { + return null; + } + List packages = new ArrayList<>(details.size()); + for (ModuleDetails detail : details) + { + packages.add(ModulePackage.fromModuleDetails(detail)); + } + return packages; + } + + @Override + public void renderJsonResponse(final WebScriptResponse res, final Object toSerialize, final JacksonHelper jsonHelper) throws IOException + { + jsonHelper.withWriter(res.getOutputStream(), (generator, objectMapper) -> { + JSONObject obj = new JSONObject(); + obj.put("entry", toSerialize); + objectMapper.writeValue(generator, obj); + }); + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + private void checkEnabled() + { + if (!enabled) + { + throw new DisabledServiceException(DISABLED); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java new file mode 100644 index 00000000000..7cb2616671a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.downloads; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.Downloads; +import org.alfresco.rest.api.model.Download; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author cpopa + * + */ +@EntityResource(name = "downloads", title = "Downloads") +public class DownloadsEntityResource implements EntityResourceAction.Create, EntityResourceAction.ReadById, EntityResourceAction.Delete, InitializingBean +{ + private Downloads downloads; + + public void setDownloads(Downloads downloads) + { + this.downloads = downloads; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("downloads", this.downloads); + } + + @Override + @WebApiDescription(title = "Create download", description = "Create a download node whose content will be a zip which is being created asynchronously.", successStatus = HttpServletResponse.SC_ACCEPTED) + @WebApiParam(name = "entity", title = "Download request", description = "Download request which contains the node ids for the zip elements.", + kind = ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple = false) + public List create(List entity, Parameters parameters) + { + Download downloadNode = downloads.createDownloadNode(entity.get(0)); + return Collections.singletonList(downloadNode); + } + + @Override + @WebApiDescription(title = "Get download information", description = "Get information about the progress of the zip creation.") + @WebApiParam(name = "nodeId", title = "Download nodeId") + public Download readById(String nodeId, Parameters parameters) throws EntityNotFoundException + { + return downloads.getDownloadStatus(nodeId); + } + + @WebApiDescription(title = "Cancel download", description = "Stop the zip creation if still in progress.", successStatus = HttpServletResponse.SC_ACCEPTED) + @Override + public void delete(String nodeId, Parameters parameters) + { + downloads.cancel(nodeId); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/downloads/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/downloads/package-info.java new file mode 100644 index 00000000000..7583142401a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/downloads/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.downloads; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/groups/GroupMembersRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/groups/GroupMembersRelation.java new file mode 100644 index 00000000000..dfef446cc14 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/groups/GroupMembersRelation.java @@ -0,0 +1,87 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.groups; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.Groups; +import org.alfresco.rest.api.model.GroupMember; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + + * @author cturlica + */ +@RelationshipResource(name = "members", entityResource = GroupsEntityResource.class, title = "Group Members") +public class GroupMembersRelation + implements RelationshipResourceAction.Read, RelationshipResourceAction.Create, RelationshipResourceAction.Delete, InitializingBean +{ + private Groups groups; + + public void setGroups(Groups groups) + { + this.groups = groups; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("groups", this.groups); + } + + @Override + @WebApiDescription(title = "A paged list of all the members of the group 'groupId'.") + public CollectionWithPagingInfo readAll(String groupId, Parameters params) + { + return groups.getGroupMembers(groupId, params); + } + + @Override + @WebApiDescription(title = "Create group membership.") + @WebApiParam(name = "entity", title = "A single group member", description = "A single group member, multiple groups members are not supported.", kind = ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple = false, required = true) + public List create(String groupId, List entity, Parameters params) + { + List result = new ArrayList<>(1); + result.add(groups.createGroupMember(groupId, entity.get(0))); + return result; + } + + @Override + @WebApiDescription(title = "Delete group membership") + public void delete(String entityResourceId, String id, Parameters parameters) + { + groups.deleteGroupMembership(entityResourceId, id); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/groups/GroupsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/groups/GroupsEntityResource.java new file mode 100644 index 00000000000..bd6ecc71de4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/groups/GroupsEntityResource.java @@ -0,0 +1,104 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.groups; + +import org.alfresco.rest.api.Groups; +import org.alfresco.rest.api.model.Group; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of an Entity Resource for a Group + * + * @author cturlica + */ +@EntityResource(name = "groups", title = "Groups") +public class GroupsEntityResource implements EntityResourceAction.Read, EntityResourceAction.ReadById, EntityResourceAction.Create, + EntityResourceAction.Update, EntityResourceAction.Delete, InitializingBean +{ + private Groups groups; + + public void setGroups(Groups groups) + { + this.groups = groups; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("groups", this.groups); + } + + @Override + @WebApiDescription(title = "Get List of Groups", description = "Get List of Groups") + public CollectionWithPagingInfo readAll(Parameters params) + { + return groups.getGroups(params); + } + + @Override + @WebApiDescription(title="Returns group information for group id") + public Group readById(String groupId, Parameters parameters) throws EntityNotFoundException + { + return groups.getGroup(groupId, parameters); + } + + @Override + @WebApiDescription(title="Create group", description="Create group") + @WebApiParam(name="entity", title="A single group", description="A single group, multiple groups are not supported.", + kind= ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple=false, required = true) + public List create(List entity, Parameters parameters) + { + List result = new ArrayList<>(1); + result.add(groups.create(entity.get(0), parameters)); + return result; + } + + @Override + @WebApiDescription(title = "Update group", description = "Update group") + public Group update(String groupId, Group group, Parameters parameters) + { + return groups.update(groupId, group, parameters); + } + + @Override + @WebApiDescription(title = "Delete group", description = "Delete group") + public void delete(String groupId, Parameters parameters) + { + groups.delete(groupId, parameters); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/groups/SiteGroupsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/groups/SiteGroupsRelation.java new file mode 100644 index 00000000000..00b13af7a67 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/groups/SiteGroupsRelation.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.groups; + +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.SiteGroup; +import org.alfresco.rest.api.sites.SiteEntityResource; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +import java.util.List; +import java.util.stream.Collectors; + +@RelationshipResource(name = "group-members", entityResource = SiteEntityResource.class, title = "Site Groups") +public class SiteGroupsRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.Delete, + RelationshipResourceAction.Create, + RelationshipResourceAction.Update, + RelationshipResourceAction.ReadById, + InitializingBean +{ + + private Sites sites; + + public void setSites(Sites sites) + { + this.sites = sites; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("sites", this.sites); + } + + /** + * POST sites//group-members + *

+ * Adds groups to site + *

+ * If group does not exist throws NotFoundException (status 404). + * + * @see RelationshipResourceAction.Create#create(String, List, Parameters) + */ + @Override + @WebApiDescription(title = "Adds groups as a member of site siteId.") + public List create(String siteId, List siteMembers, Parameters parameters) + { + return siteMembers.stream().map((group) -> sites.addSiteGroupMembership(siteId, group)).collect(Collectors.toList()); + } + + /** + * Returns a paged list of all the groups of the site 'siteId'. + *

+ * If siteId does not exist, throws NotFoundException (status 404). + */ + @Override + @WebApiDescription(title = "A paged list of all the groups of the site 'siteId'.") + public CollectionWithPagingInfo readAll(String siteId, Parameters parameters) + { + return sites.getSiteGroupMemberships(siteId, parameters); + } + + /** + * Returns site membership information for groupId in siteId. + *

+ * GET sites//group-members/ + */ + @Override + @WebApiDescription(title = "Returns site membership information for groupId in siteId.") + public SiteGroup readById(String siteId, String groupId, Parameters parameters) + { + return sites.getSiteGroupMembership(siteId, groupId); + } + + /** + * PUT sites//group-members/ + *

+ * Updates the membership of group in the site. + */ + @Override + @WebApiDescription(title = "Updates the membership of groupId in the site.") + public SiteGroup update(String siteId, SiteGroup groupMember, Parameters parameters) + { + return sites.updateSiteGroupMembership(siteId, groupMember); + } + + /** + * DELETE sites//group-members/ + *

+ * Remove a group from site. + */ + @Override + @WebApiDescription(title = "Removes groupId as a member of site siteId.") + public void delete(String siteId, String groupId, Parameters parameters) + { + sites.removeSiteGroupMembership(siteId, groupId); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/groups/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/groups/package-info.java new file mode 100644 index 00000000000..884613cec9d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/groups/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.groups; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java new file mode 100644 index 00000000000..8c89fb7a2e0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java @@ -0,0 +1,409 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.Action; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.json.JSONArray; +import org.json.JSONException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static java.util.Comparator.nullsFirst; + +public class ActionsImpl implements Actions +{ + private ActionService actionService; + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + private NodeService nodeService; + private NamespacePrefixResolver prefixResolver; + + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPrefixResolver(NamespacePrefixResolver prefixResolver) + { + this.prefixResolver = prefixResolver; + } + + @Override + public ActionDefinition getActionDefinitionById(String actionDefinitionId) + { + if (actionDefinitionId == null) + { + throw new IllegalArgumentException("Missing actionDefinitionId"); + } + + // Non-existing actionDefinitionId -> 404 + ActionDefinition result = null; + try + { + result = getActionDefinition(actionService.getActionDefinition(actionDefinitionId)); + } + catch (NoSuchBeanDefinitionException nsbdx) + { + // Intentionally empty. + } + + if (result == null) + { + throw new EntityNotFoundException(actionDefinitionId); + } + + return result; + } + + private ActionDefinition getActionDefinition( + org.alfresco.service.cmr.action.ActionDefinition actionDefinitionId) + { + List paramDefs = + actionDefinitionId. + getParameterDefinitions(). + stream(). + map(this::toModel). + collect(Collectors.toList()); + return new ActionDefinition( + actionDefinitionId.getName(), // ID is a synonym for name. + actionDefinitionId.getName(), + actionDefinitionId.getTitle(), + actionDefinitionId.getDescription(), + toShortQNames(actionDefinitionId.getApplicableTypes()), + actionDefinitionId.getAdhocPropertiesAllowed(), + actionDefinitionId.getTrackStatus(), + paramDefs); + } + + @Override + public CollectionWithPagingInfo getActionDefinitions(NodeRef nodeRef, Parameters params) + { + return actionDefinitions(actionService.getActionDefinitions(nodeRef), params); + } + + @Override + public CollectionWithPagingInfo getActionDefinitions(Parameters params) + { + return actionDefinitions(actionService.getActionDefinitions(), params); + } + + private CollectionWithPagingInfo actionDefinitions( + List actionDefinitions, + Parameters params) + { + List sorting = params.getSorting(); + Actions.SortKey sortKey = SortKey.NAME; // default + Boolean sortAsc = true; // default + if (sorting != null && !sorting.isEmpty()) + { + if (sorting.size() > 1) + { + throw new IllegalArgumentException("Only a single sort field ('name' or 'title') is supported."); + } + sortKey = Actions.SortKey.valueOf(sorting.get(0).column.toUpperCase()); + sortAsc = sorting.get(0).asc; + } + + Comparator comparator; + switch (sortKey) + { + case TITLE: + comparator = comparing(ActionDefinition::getTitle, nullsFirst(naturalOrder())); + break; + case NAME: + comparator = comparing(ActionDefinition::getName, nullsFirst(naturalOrder())); + break; + default: + throw new IllegalArgumentException("Invalid sort key, must be either 'title' or 'name'."); + } + + if (!sortAsc) + { + comparator = comparator.reversed(); + } + + + final int maxItems = params.getPaging().getMaxItems(); + final int skip = params.getPaging().getSkipCount(); + + List sortedPage = actionDefinitions. + stream(). + map(actionDefinition -> { + List paramDefs = + actionDefinition. + getParameterDefinitions(). + stream(). + map(this::toModel). + collect(Collectors.toList()); + return new ActionDefinition( + actionDefinition.getName(), // ID is a synonym for name. + actionDefinition.getName(), + actionDefinition.getTitle(), + actionDefinition.getDescription(), + toShortQNames(actionDefinition.getApplicableTypes()), + actionDefinition.getAdhocPropertiesAllowed(), + actionDefinition.getTrackStatus(), + paramDefs); + }). + sorted(comparator). + skip(skip). + limit(maxItems). + collect(Collectors.toList()); + + boolean hasMoreItems = actionDefinitions.size() > (skip + maxItems); + + return CollectionWithPagingInfo.asPaged( + params.getPaging(), + sortedPage, + hasMoreItems, + actionDefinitions.size()); + } + + @Override + public Action executeAction(Action action, Parameters parameters) + { + + if (action == null) + { + throw new InvalidArgumentException("action is null"); + } + + // Check action definition. + + if (action.getActionDefinitionId() == null || action.getActionDefinitionId().isEmpty()) + { + throw new InvalidArgumentException("action.actionDefinitionId is null or empty"); + } + + org.alfresco.service.cmr.action.ActionDefinition actionDef = null; + try + { + actionDef = actionService.getActionDefinition(action.getActionDefinitionId()); + } + catch (NoSuchBeanDefinitionException nsbdx) + { + // Intentionally empty. + } + + // The null check was intentionally added to catch the case when the bean is + // found but it isn't an instance of ActionExecuter. This is the only case when + // the result of getActionDefinition can be null and not throw the exception. + if (actionDef == null) + { + throw new EntityNotFoundException(action.getActionDefinitionId()); + } + + // targetId is optional, however, currently targetId must be a valid node ID. + NodeRef actionedUponNodeRef = null; + if (action.getTargetId() != null && !action.getTargetId().isEmpty()) + { + // Does it exist in the repo? + actionedUponNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, action.getTargetId()); + + if (!nodeService.exists(actionedUponNodeRef)) + { + throw new EntityNotFoundException(action.getTargetId()); + } + } + + org.alfresco.service.cmr.action.Action cmrAction; + if (action.getParams() != null && !action.getParams().isEmpty()) + { + cmrAction = actionService.createAction(action.getActionDefinitionId(), extractActionParams(actionDef, action.getParams())); + } + else + { + cmrAction = actionService.createAction(action.getActionDefinitionId()); + } + + actionService.executeAction(cmrAction, actionedUponNodeRef, true, true); + + // Create user result. + Action result = new Action(); + result.setId(cmrAction.getId()); + + return result; + } + + private Map extractActionParams(org.alfresco.service.cmr.action.ActionDefinition actionDefinition, Map params) + { + Map parameterValues = new HashMap<>(); + + try + { + for (Map.Entry entry : params.entrySet()) + { + String propertyName = entry.getKey(); + Object propertyValue = entry.getValue(); + + // Get the parameter definition we care about + ParameterDefinition paramDef = actionDefinition.getParameterDefintion(propertyName); + if (paramDef == null && !actionDefinition.getAdhocPropertiesAllowed()) + { + throw new AlfrescoRuntimeException("Invalid parameter " + propertyName + " for action/condition " + actionDefinition.getName()); + } + if (paramDef != null) + { + QName typeQName = paramDef.getType(); + + // Convert the property value + Serializable value = convertValue(typeQName, propertyValue); + parameterValues.put(propertyName, value); + } + else + { + // If there is no parameter definition we can only rely on the .toString() + // representation of the ad-hoc property + parameterValues.put(propertyName, propertyValue.toString()); + } + } + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Could not parse JSON from req.", je); + } + + return parameterValues; + } + + private Serializable convertValue(QName typeQName, Object propertyValue) throws JSONException + { + Serializable value; + + DataTypeDefinition typeDef = dictionaryService.getDataType(typeQName); + if (typeDef == null) + { + throw new AlfrescoRuntimeException("Action property type definition " + typeQName.toPrefixString() + " is unknown."); + } + + if (propertyValue instanceof JSONArray) + { + // Convert property type to java class + String javaClassName = typeDef.getJavaClassName(); + try + { + Class.forName(javaClassName); + } + catch (ClassNotFoundException e) + { + throw new DictionaryException("Java class " + javaClassName + " of property type " + typeDef.getName() + " is invalid", e); + } + + int length = ((JSONArray) propertyValue).length(); + List list = new ArrayList<>(length); + for (int i = 0; i < length; i++) + { + list.add(convertValue(typeQName, ((JSONArray) propertyValue).get(i))); + } + value = (Serializable) list; + } + else + { + if (typeQName.equals(DataTypeDefinition.QNAME) && typeQName.toString().contains(":")) + { + value = QName.createQName(propertyValue.toString(), namespaceService); + } + else + { + value = (Serializable) DefaultTypeConverter.INSTANCE.convert(dictionaryService.getDataType(typeQName), propertyValue); + } + } + + return value; + } + + private List toShortQNames(Set types) + { + return types. + stream(). + map(this::toShortQName). + collect(Collectors.toList()); + } + + private String toShortQName(QName type) + { + return type.toPrefixString(prefixResolver); + } + + private ActionDefinition.ParameterDefinition toModel(ParameterDefinition p) + { + return new ActionDefinition.ParameterDefinition( + p.getName(), + toShortQName(p.getType()), + p.isMultiValued(), + p.isMandatory(), + p.getDisplayLabel(), + p.getParameterConstraintName() + ); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/ActivitiesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/ActivitiesImpl.java new file mode 100644 index 00000000000..8064c1ed300 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/ActivitiesImpl.java @@ -0,0 +1,182 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.domain.activities.ActivityFeedEntity; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.rest.api.Activities; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.impl.activities.ActivitySummaryParser; +import org.alfresco.rest.api.model.Activity; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.JSONException; + +/** + * Centralises access to activities services and maps between representations. + * + * @author steveglover + * + */ +public class ActivitiesImpl implements Activities +{ + private static final String ACTIVITIES_FORMAT = "json"; + + private People people; + private ActivityService activityService; + private ActivitySummaryParser activitySummaryParser; + private TenantService tenantService; + private Sites sites; + + public void setSites(Sites sites) + { + this.sites = sites; + } + + public void setPeople(People people) + { + this.people = people; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setActivitySummaryParser(ActivitySummaryParser activitySummaryParser) + { + this.activitySummaryParser = activitySummaryParser; + } + + public Map getActivitySummary(ActivityFeedEntity entity) throws JSONException + { + Map activitySummary = activitySummaryParser.parse(entity.getActivityType(), entity.getActivitySummary()); + return activitySummary; + } + + private String getSiteId(String siteNetwork) + { + String siteId = siteNetwork; + + int idx = siteNetwork.lastIndexOf(TenantService.SEPARATOR); + if(idx != -1) + { + siteId = siteNetwork.substring(idx + 1); + } + + return siteId; + } + + public CollectionWithPagingInfo getUserActivities(String personId, final Parameters parameters) + { + personId = people.validatePerson(personId); + + Paging paging = parameters.getPaging(); + String siteId = parameters.getParameter("siteId"); + String who = parameters.getParameter("who"); + ActivityWho activityWho = null; + if(who != null) + { + try + { + activityWho = ActivityWho.valueOf(who); + } + catch(IllegalArgumentException e) + { + throw new InvalidArgumentException("Parameter who should be one of " + Arrays.toString(ActivityWho.values())); + } + } + + if(siteId != null && !siteId.equals("")) + { + SiteInfo siteInfo = sites.validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + } + + try + { + PagingResults activities = null; + + if(activityWho == null) + { + activities = activityService.getPagedUserFeedEntries(personId, siteId, false, false, -1, Util.getPagingRequest(paging)); + } + else if(activityWho.equals(ActivityWho.me)) + { + activities = activityService.getPagedUserFeedEntries(personId, siteId, false, true, -1, Util.getPagingRequest(paging)); + } + else if(activityWho.equals(ActivityWho.others)) + { + activities = activityService.getPagedUserFeedEntries(personId, siteId, true, false, -1, Util.getPagingRequest(paging)); + } + else + { + throw new InvalidArgumentException("Who argument is invalid."); + } + + List feedEntities = activities.getPage(); + List ret = new ArrayList(feedEntities.size()); + for(ActivityFeedEntity entity : feedEntities) + { + String feedSiteId = getSiteId(entity.getSiteNetwork()); + String networkId = tenantService.getDomain(entity.getSiteNetwork()); + Activity activity = new Activity(entity.getId(), networkId, feedSiteId, entity.getFeedUserId(), entity.getPostUserId(), + entity.getPostDate(), entity.getActivityType(), getActivitySummary(entity)); + ret.add(activity); + } + + return CollectionWithPagingInfo.asPaged(paging, ret, activities.hasMoreItems(), activities.getTotalResultCount().getFirst()); + } + catch(JSONException e) + { + throw new AlfrescoRuntimeException("", e); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/AuditImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/AuditImpl.java new file mode 100644 index 00000000000..fdb865fc995 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/AuditImpl.java @@ -0,0 +1,885 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +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.TreeSet; +import java.util.stream.Collectors; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Audit; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.model.AuditApp; +import org.alfresco.rest.api.model.AuditEntry; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.cmr.audit.AuditQueryParameters; +import org.alfresco.service.cmr.audit.AuditService; +import org.alfresco.service.cmr.audit.AuditService.AuditApplication; +import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.ISO8601DateFormat; +import org.alfresco.util.ISO9075; +import org.alfresco.util.Pair; + +/** + * Handles audit (applications & entries) + * + * @author janv, anechifor, eknizat + */ +public class AuditImpl implements Audit +{ + + private final static String DISABLED = "Audit is disabled system-wide"; + private final static String DEFAULT_USER = "-me-"; + + // list of equals filter's auditEntry (via where clause) + private final static Set LIST_AUDIT_ENTRY_EQUALS_QUERY_PROPERTIES = new HashSet<>( + Arrays.asList(new String[] { CREATED_BY_USER, VALUES_KEY, VALUES_VALUE })); + + // map of sort parameters for the moment one createdAt + private final static Map SORT_PARAMS_TO_NAMES; + + static + { + Map aMap = new HashMap<>(1); + aMap.put(CREATED_AT, CREATED_AT); + SORT_PARAMS_TO_NAMES = Collections.unmodifiableMap(aMap); + } + + private AuditService auditService; + + private PersonService personService; + + private NodeService nodeService; + + private NamespaceService namespaceService; + + private Nodes nodes; + + private People people; + + public void setPeople(People people) + { + this.people = people; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setAuditService(AuditService auditService) + { + this.auditService = auditService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + private void checkEnabled() + { + if (!auditService.isAuditEnabled()) + { + throw new DisabledServiceException(DISABLED); + } + } + + @Override + public AuditApp getAuditApp(String auditAppId, Parameters parameters) + { + checkEnabled(); + + AuditService.AuditApplication auditApplication = findAuditAppByIdOr404(auditAppId); + + AuditApp auditApp = new AuditApp(auditApplication.getKey().substring(1), auditApplication.getName(), auditApplication.isEnabled()); + + if (!parameters.getInclude().isEmpty()) + { + List filteredParams = parameters.getInclude().stream().filter(p -> (p.contains(PARAM_INCLUDE_MAX) || p.contains(PARAM_INCLUDE_MIN))) + .collect(Collectors.toList()); + + if (!filteredParams.isEmpty()) + { + HashMap result = auditService.getAuditMinMaxByApp(auditApp.getName(), filteredParams); + + if (!result.isEmpty()) + { + // Set the max/min results to audit application + auditApp.setMaxEntryId(result.get("max")); + auditApp.setMinEntryId(result.get("min")); + } + } + } + return auditApp; + } + + private AuditService.AuditApplication findAuditAppByIdOr404(String auditAppId) + { + AuditService.AuditApplication auditApplication = findAuditAppById(auditAppId); + + if (auditApplication == null) + { + throw new EntityNotFoundException(auditAppId); + } + + return auditApplication; + } + + private AuditService.AuditApplication findAuditAppById(String auditAppId) + { + AuditService.AuditApplication auditApp = null; + Map auditApplicationsByName = auditService.getAuditApplications(); + if (auditApplicationsByName != null) + { + for (AuditService.AuditApplication auditApplication : auditApplicationsByName.values()) + { + if (auditApplication.getKey().equals("/" + auditAppId)) + { + auditApp = auditApplication; + } + } + } + return auditApp; + } + + @Override + public CollectionWithPagingInfo getAuditApps(Paging paging) + { + checkEnabled(); + + Map auditApplicationsByName = auditService.getAuditApplications(); + + Set audAppsName = new TreeSet(auditApplicationsByName.keySet()); + Iterator audAppsNameIt = audAppsName.iterator(); + + int skipCount = paging.getSkipCount(); + int maxItems = paging.getMaxItems(); + int totalItems = audAppsName.size(); + int end = skipCount + maxItems; + + if (skipCount >= totalItems) + { + List empty = Collections.emptyList(); + return CollectionWithPagingInfo.asPaged(paging, empty, false, totalItems); + } + + List auditApps = new ArrayList(totalItems); + int count = 0; + for (int i = 0; i < end && audAppsNameIt.hasNext(); i++) + { + String auditAppName = audAppsNameIt.next(); + if (i < skipCount) + { + continue; + } + count++; + AuditApplication auditApplication = auditApplicationsByName.get(auditAppName); + + auditApps.add(new AuditApp(auditApplication.getKey().substring(1), auditApplication.getName(), auditApplication.isEnabled())); + } + + boolean hasMoreItems = (skipCount + count < totalItems); + + return CollectionWithPagingInfo.asPaged(paging, auditApps, hasMoreItems, totalItems); + } + + @Override + public CollectionWithPagingInfo listAuditEntries(String auditAppId, Parameters parameters) + { + checkEnabled(); + + AuditService.AuditApplication auditApplication = findAuditAppByIdOr404(auditAppId); + + // adding orderBy property + Pair sortProp = getAuditEntrySortProp(parameters); + Boolean forward = true; + if ((sortProp != null) && (sortProp.getFirst().equals(CREATED_AT))) + { + forward = sortProp.getSecond(); + } + + // Parse where clause properties. + List entriesAudit = new ArrayList<>(); + Query q = parameters.getQuery(); + // paging + Paging paging = parameters.getPaging(); + int skipCount = paging.getSkipCount(); + int maxItems = paging.getMaxItems(); + int limit = skipCount + maxItems + 1; // to detect hasMoreItems + + if (q != null) + { + // filtering via "where" clause + AuditEntryQueryWalker propertyWalker = new AuditEntryQueryWalker(); + QueryHelper.walk(q, propertyWalker); + entriesAudit = getQueryResultAuditEntries(auditApplication, propertyWalker, parameters.getInclude(), limit, forward); + } + + // clear null elements + entriesAudit.removeAll(Collections.singleton(null)); + int totalItems = entriesAudit.size(); + + if (skipCount >= totalItems) + { + List empty = Collections.emptyList(); + return CollectionWithPagingInfo.asPaged(paging, empty, false, totalItems); + } + else + { + int end = Math.min(limit - 1, totalItems); + boolean hasMoreItems = totalItems > end; + + entriesAudit = entriesAudit.subList(skipCount, end); + return CollectionWithPagingInfo.asPaged(paging, entriesAudit, hasMoreItems, totalItems); + } + } + + /** + * @param parameters + * @return + * @throws InvalidArgumentException + */ + private Pair getAuditEntrySortProp(Parameters parameters) + { + Pair sortProp = null; + List sortCols = parameters.getSorting(); + + if ((sortCols != null) && (sortCols.size() > 0)) + { + if (sortCols.size() > 1) + { + throw new InvalidArgumentException("Multiple sort fields not allowed."); + } + + SortColumn sortCol = sortCols.get(0); + + String sortPropName = SORT_PARAMS_TO_NAMES.get(sortCol.column); + if (sortPropName == null) + { + throw new InvalidArgumentException("Invalid sort field: " + sortCol.column); + } + + sortProp = new Pair<>(sortPropName, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE)); + } + return sortProp; + } + + /** + * @author anechifor + */ + private class AuditEntryQueryWalker extends MapBasedQueryWalker + { + private Long fromTime; + private Long toTime; + + private Long fromId; + private Long toId; + + public AuditEntryQueryWalker() + { + super(LIST_AUDIT_ENTRY_EQUALS_QUERY_PROPERTIES, null); + } + + @Override + public void and() + { + // allow AND, e.g. createdByUser='jbloggs' AND createdAt BETWEEN ('...','...) + } + + @Override + public void between(String propertyName, String firstValue, String secondValue, boolean negated) + { + if (propertyName.equals(CREATED_AT)) + { + fromTime = getTime(firstValue); + toTime = getTime(secondValue) + 1; + } + + if (propertyName.equals(ID)) + { + fromId = Long.valueOf(firstValue); + toId = Long.valueOf(secondValue) + 1; + } + } + + public Long getFromTime() + { + return fromTime; + } + + public Long getToTime() + { + return toTime; + } + + public String getCreatedByUser() + { + String propertyValue = getProperty(CREATED_BY_USER, WhereClauseParser.EQUALS, String.class); + + // Check if '-me-' alias is used and replace it with userId + if ((propertyValue != null) && propertyValue.equalsIgnoreCase(DEFAULT_USER)) + { + propertyValue = AuditImpl.this.people.validatePerson(propertyValue); + } + return propertyValue; + } + + public String getValuesKey() + { + return getProperty(VALUES_KEY, WhereClauseParser.EQUALS, String.class); + } + + public String getValuesValue() + { + return getProperty(VALUES_VALUE, WhereClauseParser.EQUALS, String.class); + } + + public Long getFromId() + { + return fromId; + } + + public Long getToId() + { + return toId; + } + } + + /** + * @param auditAppId + * @param propertyWalker + * @param includeParams + * @param maxItem + * @param forward + * @return + */ + public List getQueryResultAuditEntries(AuditService.AuditApplication auditApplication, AuditEntryQueryWalker propertyWalker, + List includeParam, int maxItem, Boolean forward) + { + final List results = new ArrayList<>(); + + final String auditAppId = auditApplication.getKey().substring(1); + String auditApplicationName = auditApplication.getName(); + + // Execute the query + AuditQueryParameters params = new AuditQueryParameters(); + + // used to orderBY by field createdAt + params.setForward(forward); + + params.setApplicationName(auditApplicationName); + params.setUser(propertyWalker.getCreatedByUser()); + + Long fromId = propertyWalker.getFromId(); + Long toId = propertyWalker.getToId(); + + validateWhereBetween(auditAppId, fromId, toId); + + Long fromTime = propertyWalker.getFromTime(); + Long toTime = propertyWalker.getToTime(); + + validateWhereBetween(auditAppId, fromTime, toTime); + + params.setFromTime(fromTime); + params.setToTime(toTime); + + params.setFromId(fromId); + params.setToId(toId); + + if (propertyWalker.getValuesKey() != null && propertyWalker.getValuesValue() != null) + { + params.addSearchKey(propertyWalker.getValuesKey(), propertyWalker.getValuesValue()); + } + + final Map mapUserInfo = new HashMap<>(10); + + // create the callback for auditQuery method + final AuditQueryCallback callback = new AuditQueryCallback() + { + public boolean valuesRequired() + { + return ((includeParam != null) && (includeParam.contains(PARAM_INCLUDE_VALUES))); + } + + public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) + { + throw new AlfrescoRuntimeException("Failed to retrieve audit data.", error); + } + + public boolean handleAuditEntry(Long entryId, String applicationName, String userName, long time, Map values) + { + UserInfo userInfo = Node.lookupUserInfo(userName, mapUserInfo, personService); + AuditEntry auditEntry = new AuditEntry(entryId, auditAppId, userInfo, new Date(time), values); + results.add(auditEntry); + return true; + } + }; + + auditService.auditQuery(callback, params, maxItem); + return results; + } + + @Override + public AuditApp update(String auditAppId, AuditApp auditApp, Parameters parameters) + { + checkEnabled(); + + AuditService.AuditApplication auditApplication = findAuditAppByIdOr404(auditAppId); + + // Enable/Disable audit application + if (auditApp.getIsEnabled() && !auditApplication.isEnabled()) + { + auditService.enableAudit(auditApplication.getName(), null); + } + else if (!auditApp.getIsEnabled() && auditApplication.isEnabled()) + { + auditService.disableAudit(auditApplication.getName(), null); + } + + return new AuditApp(auditApplication.getKey().substring(1), auditApplication.getName(), auditApp.getIsEnabled()); + } + + @Override + public AuditEntry getAuditEntry(String auditAppId, long auditEntryId, Parameters parameters) + { + checkEnabled(); + + AuditService.AuditApplication auditApplication = findAuditAppByIdOr404(auditAppId); + + // Execute the query + AuditQueryParameters params = new AuditQueryParameters(); + params.setApplicationName(auditApplication.getName()); + params.setFromId(auditEntryId); + params.setToId(auditEntryId + 1); + + List includeParam = new ArrayList<>(); + if (parameters != null) + { + includeParam.addAll(parameters.getInclude()); + } + + // Add values for single get + includeParam.add(PARAM_INCLUDE_VALUES); + + final List results = new ArrayList<>(); + + // create the callback for auditQuery method + final AuditQueryCallback callback = new AuditQueryCallback() + { + public boolean valuesRequired() + { + return ((includeParam != null) && (includeParam.contains(PARAM_INCLUDE_VALUES))); + } + + public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) + { + throw new AlfrescoRuntimeException("Failed to retrieve audit data.", error); + } + + public boolean handleAuditEntry(Long entryId, String applicationName, String userName, long time, Map values) + { + UserInfo userInfo = Node.lookupUserInfo(userName, new HashMap<>(0), personService); + AuditEntry auditEntry = new AuditEntry(entryId, auditAppId, userInfo, new Date(time), values); + results.add(auditEntry); + return true; + } + }; + + auditService.auditQuery(callback, params, 1); + + if (results.size() != 1) + { + throw new EntityNotFoundException("" + auditEntryId); + } + return results.get(0); + } + + @Override + public void deleteAuditEntry(String auditAppId, long auditEntryId, Parameters parameters) + { + checkEnabled(); + + AuditService.AuditApplication auditApplication = findAuditAppByIdOr404(auditAppId); + + int deleted = auditService.clearAuditByIdRange(auditApplication.getName(), auditEntryId, auditEntryId + 1); + if (deleted != 1) + { + throw new EntityNotFoundException("" + auditEntryId); + } + } + + @Override + public void deleteAuditEntries(String auditAppId, Parameters parameters) + { + checkEnabled(); + + AuditService.AuditApplication auditApplication = findAuditAppByIdOr404(auditAppId); + + Query q = parameters.getQuery(); + if ((q == null) || (q.getTree() == null)) + { + throw new InvalidArgumentException("where clause is required to delete audit entries (" + auditAppId + ")"); + } + + // delete via "where" clause + DeleteAuditEntriesQueryWalker walker = new DeleteAuditEntriesQueryWalker(); + QueryHelper.walk(q, walker); + + Long fromId = walker.getFromId(); + Long toId = walker.getToId(); + + validateWhereBetween(auditAppId, fromId, toId); + + Long fromTime = walker.getFromTime(); + Long toTime = walker.getToTime(); + + validateWhereBetween(auditAppId, fromTime, toTime); + + if ((fromId != null) && (fromTime != null)) + { + throw new InvalidArgumentException("where clause is invalid - cannot specify both createdAt & id (" + auditAppId + ")"); + } + + if (fromId != null) + { + auditService.clearAuditByIdRange(auditApplication.getName(), fromId, toId); // ignore + // response + } + else if (fromTime != null) + { + auditService.clearAudit(auditApplication.getName(), fromTime, toTime); // ignore + // response + } + + // return success (even if nothing is deleted) + } + + private static class DeleteAuditEntriesQueryWalker extends MapBasedQueryWalker + { + private Long fromTime; + private Long toTime; + + private Long fromId; + private Long toId; + + public DeleteAuditEntriesQueryWalker() + { + super(null, null); + } + + @Override + public void between(String propertyName, String firstValue, String secondValue, boolean negated) + { + if (propertyName.equals(CREATED_AT)) + { + fromTime = getTime(firstValue); + toTime = getTime(secondValue) + 1; + } + + if (propertyName.equals(ID)) + { + fromId = Long.valueOf(firstValue); + toId = Long.valueOf(secondValue) + 1; + } + } + + public Long getFromTime() + { + return fromTime; + } + + public Long getToTime() + { + return toTime; + } + + public Long getFromId() + { + return fromId; + } + + public Long getToId() + { + return toId; + } + } + + private static long getTime(String iso8601String) + { + return ISO8601DateFormat.parse(iso8601String.replace(" ", "+")).getTime(); + } + + private void validateWhereBetween(String auditAppId, Long from, Long to) + { + if ((from != null) || (to != null)) + { + if ((from == null) || (to == null)) + { + // belts-and-braces + throw new InvalidArgumentException("where BETWEEN is invalid - must contain range (" + auditAppId + ")"); + } + + if (from >= to) + { + throw new InvalidArgumentException("where BETWEEN is invalid - range start greater than end (" + auditAppId + ")"); + } + } + } + + @Override + public CollectionWithPagingInfo listAuditEntriesByNodeId(String nodeId, Parameters parameters) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Void doWork() throws Exception + { + checkEnabled(); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + // note: node read permission is checked later - see nodeService.getPath + NodeRef nodeRef = nodes.validateNode(nodeId); + List entriesAudit = new ArrayList<>(); + + // adding orderBy property + Pair sortProp = getAuditEntrySortProp(parameters); + Boolean forward = true; + if ((sortProp != null) && (sortProp.getFirst().equals(CREATED_AT))) + { + forward = sortProp.getSecond(); + } + + // paging + Paging paging = parameters.getPaging(); + int skipCount = paging.getSkipCount(); + int maxItems = paging.getMaxItems(); + int limit = skipCount + maxItems + 1; // to detect hasMoreItems + + Query q = parameters.getQuery(); + + if (q != null) + { + // filtering via "where" clause + AuditEntriesByNodeIdQueryWalker propertyWalker = new AuditEntriesByNodeIdQueryWalker(); + QueryHelper.walk(q, propertyWalker); + entriesAudit = getQueryResultAuditEntriesByNodeRef(nodeRef, propertyWalker, parameters.getInclude(), forward, limit); + } + + // clear null elements + entriesAudit.removeAll(Collections.singleton(null)); + int totalItems = entriesAudit.size(); + + if (skipCount >= totalItems) + { + List empty = Collections.emptyList(); + return CollectionWithPagingInfo.asPaged(paging, empty, false, totalItems); + } + else + { + int end = Math.min(limit - 1, totalItems); + boolean hasMoreItems = totalItems > end; + + entriesAudit = entriesAudit.subList(skipCount, end); + return CollectionWithPagingInfo.asPaged(paging, entriesAudit, hasMoreItems, totalItems); + } + } + + private List getQueryResultAuditEntriesByNodeRef(NodeRef nodeRef, AuditEntriesByNodeIdQueryWalker propertyWalker, + List includeParam, boolean forward, int limit) + { + final List results = new ArrayList<>(); + + String auditAppId = "alfresco-access"; + String auditApplicationName = AuthenticationUtil.runAs(new RunAsWork() + { + public String doWork() throws Exception + { + return findAuditAppByIdOr404(auditAppId).getName(); + } + }, AuthenticationUtil.getSystemUserName()); + + // create the callback for auditQuery method + final AuditQueryCallback callback = new AuditQueryCallback() + { + public boolean valuesRequired() + { + return ((includeParam != null) && (includeParam.contains(PARAM_INCLUDE_VALUES))); + } + + public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) + { + throw new AlfrescoRuntimeException("Failed to retrieve audit data.", error); + } + + public boolean handleAuditEntry(Long entryId, String applicationName, String userName, long time, Map values) + { + UserInfo userInfo = Node.lookupUserInfo(userName, new HashMap<>(0), personService); + AuditEntry auditEntry = new AuditEntry(entryId, auditAppId, userInfo, new Date(time), values); + results.add(auditEntry); + return true; + } + }; + + // resolve the path of the node - note: this will also check read permission for current user + final String nodePath = ISO9075.decode(nodeService.getPath(nodeRef).toPrefixString(namespaceService)); + Long fromTime = propertyWalker.getFromTime(); + Long toTime = propertyWalker.getToTime(); + validateWhereBetween(nodeRef.getId(), fromTime, toTime); + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + // QueryParameters + AuditQueryParameters pathParams = new AuditQueryParameters(); + // used to orderBY by field createdAt + pathParams.setForward(forward); + pathParams.setUser(propertyWalker.getCreatedByUser()); + pathParams.setFromTime(fromTime); + pathParams.setToTime(toTime); + pathParams.setApplicationName(auditApplicationName); + pathParams.addSearchKey("/"+auditAppId+"/transaction/path", nodePath); + auditService.auditQuery(callback, pathParams, limit); + + AuditQueryParameters copyFromPathParams = new AuditQueryParameters(); + // used to orderBY by field createdAt + copyFromPathParams.setForward(forward); + copyFromPathParams.setUser(propertyWalker.getCreatedByUser()); + copyFromPathParams.setFromTime(fromTime); + copyFromPathParams.setToTime(toTime); + copyFromPathParams.setApplicationName(auditApplicationName); + copyFromPathParams.addSearchKey("/"+auditAppId+"/transaction/copy/from/path", nodePath); + auditService.auditQuery(callback, copyFromPathParams, limit); + + AuditQueryParameters moveFromPathParams = new AuditQueryParameters(); + // used to orderBY by field createdAt + moveFromPathParams.setForward(forward); + moveFromPathParams.setUser(propertyWalker.getCreatedByUser()); + moveFromPathParams.setFromTime(fromTime); + moveFromPathParams.setToTime(toTime); + moveFromPathParams.setApplicationName(auditApplicationName); + moveFromPathParams.addSearchKey("/"+auditAppId+"/transaction/move/from/path", nodePath); + auditService.auditQuery(callback, moveFromPathParams, limit); + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + return results; + } + + private class AuditEntriesByNodeIdQueryWalker extends MapBasedQueryWalker + { + private Long fromTime; + private Long toTime; + + public AuditEntriesByNodeIdQueryWalker() + { + super(new HashSet<>(Arrays.asList(new String[] { CREATED_BY_USER })), null); + } + + @Override + public void and() + { + // allow AND, e.g. createdByUser='jbloggs' AND createdAt BETWEEN ('...','...) + } + + @Override + public void between(String propertyName, String firstValue, String secondValue, boolean negated) + { + if (propertyName.equals(CREATED_AT)) + { + fromTime = getTime(firstValue); + toTime = getTime(secondValue) + 1; + } + + } + + public String getCreatedByUser() + { + String propertyValue = getProperty(CREATED_BY_USER, WhereClauseParser.EQUALS, String.class); + + // Check if '-me-' alias is used and replace it with userId + if ((propertyValue != null) && propertyValue.equalsIgnoreCase(DEFAULT_USER)) + { + propertyValue = AuditImpl.this.people.validatePerson(propertyValue); + } + return propertyValue; + } + + public Long getFromTime() + { + return fromTime; + } + + public Long getToTime() + { + return toTime; + } + + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java new file mode 100644 index 00000000000..ed4f04bc9f6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/AuthenticationsImpl.java @@ -0,0 +1,266 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.impl; + +import org.alfresco.repo.management.subsystems.ActivateableBean; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.Authorization; +import org.alfresco.repo.security.authentication.TicketComponent; +import org.alfresco.repo.security.authentication.external.RemoteUserMapper; +import org.alfresco.repo.web.scripts.BufferedRequest; +import org.alfresco.rest.api.Authentications; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.PublicApiTenantWebScriptServletRequest; +import org.alfresco.rest.api.model.LoginTicket; +import org.alfresco.rest.api.model.LoginTicketResponse; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.lang3.StringUtils; +import org.springframework.extensions.surf.util.Base64; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class AuthenticationsImpl implements Authentications +{ + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String PARAM_ALF_TICKET = "alf_ticket"; + + private AuthenticationService authenticationService; + private TicketComponent ticketComponent; + private RemoteUserMapper remoteUserMapper; + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setTicketComponent(TicketComponent ticketComponent) + { + this.ticketComponent = ticketComponent; + } + + public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper) + { + this.remoteUserMapper = remoteUserMapper; + } + + public void init() + { + PropertyCheck.mandatory(this, "authenticationService", authenticationService); + PropertyCheck.mandatory(this, "ticketComponent", ticketComponent); + } + + @Override + public LoginTicketResponse createTicket(LoginTicket loginRequest, Parameters parameters) + { + validateLoginRequest(loginRequest); + try + { + // get ticket + authenticationService.authenticate(loginRequest.getUserId(), loginRequest.getPassword().toCharArray()); + + LoginTicketResponse response = new LoginTicketResponse(); + response.setUserId(loginRequest.getUserId()); + response.setId(authenticationService.getCurrentTicket()); + + return response; + } + catch (AuthenticationException e) + { + throw new PermissionDeniedException("Login failed"); + } + finally + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + } + + @Override + public LoginTicketResponse validateTicket(String me, Parameters parameters, WithResponse withResponse) + { + if (!People.DEFAULT_USER.equals(me)) + { + throw new InvalidArgumentException("Invalid parameter: " + me); + } + + final String ticket = getTicket(parameters); + try + { + final String ticketUser = ticketComponent.validateTicket(ticket); + + final String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // do not go any further if tickets are different + // or the user is not fully authenticated + if (currentUser == null || !currentUser.equals(ticketUser)) + { + throw new NotFoundException(NotFoundException.DEFAULT_MESSAGE_ID, new String[] { ticket }); + } + } + catch (AuthenticationException e) + { + throw new NotFoundException(NotFoundException.DEFAULT_MESSAGE_ID, new String[] { ticket }); + } + LoginTicketResponse response = new LoginTicketResponse(); + response.setId(ticket); + return response; + } + + @Override + public void deleteTicket(String me, Parameters parameters, WithResponse withResponse) + { + if (!People.DEFAULT_USER.equals(me)) + { + throw new InvalidArgumentException("Invalid parameter: " + me); + } + + final String ticket = getTicket(parameters); + try + { + final String ticketUser = ticketComponent.validateTicket(ticket); + + final String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + // do not go any further if tickets are different + // or the user is not fully authenticated + if (currentUser == null || !currentUser.equals(ticketUser)) + { + throw new NotFoundException(NotFoundException.DEFAULT_MESSAGE_ID, new String[] { ticket }); + } + else + { + // delete the ticket + authenticationService.invalidateTicket(ticket); + } + } + catch (AuthenticationException e) + { + throw new NotFoundException(NotFoundException.DEFAULT_MESSAGE_ID, new String[] { ticket }); + } + } + + protected void validateLoginRequest(LoginTicket loginTicket) + { + if (loginTicket == null || loginTicket.getUserId() == null || loginTicket.getPassword() == null) + { + throw new InvalidArgumentException("Invalid login details."); + } + } + + protected String getTicket(Parameters parameters) + { + // First check the alf_ticket in the URL + final String alfTicket = parameters.getParameter(PARAM_ALF_TICKET); + if (StringUtils.isNotEmpty(alfTicket)) + { + return alfTicket; + } + + // Check the Authorization header + final String authorization = parameters.getRequest().getHeader(AUTHORIZATION_HEADER); + if (StringUtils.isEmpty(authorization)) + { + throw new InvalidArgumentException("Authorization header is required."); + } + + final String[] authorizationParts = authorization.split(" "); + if (authorizationParts[0].equalsIgnoreCase("basic")) + { + final String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); + Authorization authObj = new Authorization(decodedAuthorisation); + if (!authObj.isTicket()) + { + throw new InvalidArgumentException("Ticket base authentication required."); + } + return authObj.getTicket(); + } + else if (authorizationParts[0].equalsIgnoreCase("bearer")) + { + return getTicketFromRemoteUserMapperUserId(parameters); + } + + throw new InvalidArgumentException("Authorization '" + authorizationParts[0] + "' not supported."); + } + + private String getTicketFromRemoteUserMapperUserId(Parameters parameters) + { + // If we got to execute any of the code in this class, it means that, somehow, the user is authenticated; + // If we got this far, in this method, it means that: + // * there is no alf_ticket in the URL; + // * there is an "Authorization" header that says it is "bearer"; + // * the authorization type in the header is not "basic"; therefore the user was not authenticated with basic auth + + // We could end up here authenticated with some other mechanism (kerberos (SSO) or other custom authenticators) + // We need to validate the bearer token so as not to open an exploit where we return the alf_ticket even if + // the value of the bearer access token is not valid; + + // Validate the bearer access token again and + // confirm that the current authenticated user is the same user specified in the bearer token + HttpServletRequest httpServletRequest = extractHttpServletRequestFromParameters(parameters); + if (httpServletRequest != null && isRemoteUserMapperActive()) + { + String remoteUser = remoteUserMapper.getRemoteUser(httpServletRequest); + // We accept that the remoteUserMapper may have not been the IdentityServiceRemoteUserMapper, + // and could have been DefaultRemoteUserMapper (using External authentication), but that is ok + // because the business logic is similar. + if (remoteUser != null) + { + return ticketComponent.getCurrentTicket(remoteUser, false); + } + } + throw new InvalidArgumentException("Can't use Alfresco Identity Services to validate the user in the bearer access token"); + } + + private HttpServletRequest extractHttpServletRequestFromParameters(Parameters parameters) + { + // An alternative solution would be to create some sort of ServletHttpFacade object based on the information present + // in the parameters object. But for that we need to write a lot of code and check that we pass all the data required + // by the keycloak library; + + // Parameters object is clearly not designed to give us access to the HttpServletRequest object, + // but we know that remoteUserMapper.getRemoteUser will use this in a safe way + if (parameters.getRequest() instanceof BufferedRequest && + ((BufferedRequest) parameters.getRequest()).getNext() instanceof PublicApiTenantWebScriptServletRequest) + { + return ((PublicApiTenantWebScriptServletRequest) ((BufferedRequest) parameters.getRequest()).getNext()).getHttpServletRequest(); + } + return null; + } + + private boolean isRemoteUserMapperActive() + { + return remoteUserMapper instanceof ActivateableBean && ((ActivateableBean) remoteUserMapper).isActive(); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/CommentsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/CommentsImpl.java new file mode 100644 index 00000000000..b569a842825 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/CommentsImpl.java @@ -0,0 +1,248 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.forum.CommentService; +import org.alfresco.rest.api.Comments; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.model.Comment; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.TypeConstraint; + +import java.io.Serializable; +import java.util.AbstractList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.alfresco.rest.api.People.PARAM_INCLUDE_ASPECTNAMES; +import static org.alfresco.rest.api.People.PARAM_INCLUDE_PROPERTIES; + +/** + * Centralises access to comment services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class CommentsImpl implements Comments +{ + private static final List INCLUDE_FULL_PERSON = Arrays.asList( + PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_PROPERTIES); + private Nodes nodes; + private NodeService nodeService; + private CommentService commentService; + private ContentService contentService; + private TypeConstraint typeConstraint; + private People people; + + public void setTypeConstraint(TypeConstraint typeConstraint) + { + this.typeConstraint = typeConstraint; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setCommentService(CommentService commentService) + { + this.commentService = commentService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setPeople(People people) + { + this.people = people; + } + + private Comment toComment(NodeRef nodeRef, NodeRef commentNodeRef, List include) + { + Map nodeProps = nodeService.getProperties(commentNodeRef); + + ContentReader reader = contentService.getReader(commentNodeRef, ContentModel.PROP_CONTENT); + if(reader != null) + { + String content = reader.getContentString(); + nodeProps.put(Comment.PROP_COMMENT_CONTENT, content); + nodeProps.remove(ContentModel.PROP_CONTENT); + } + + + Map map = commentService.getCommentPermissions(nodeRef, commentNodeRef); + boolean canEdit = map.get(CommentService.CAN_EDIT); + boolean canDelete = map.get(CommentService.CAN_DELETE); + + Person createdBy; + try { + createdBy = people.getPerson((String) nodeProps.get(ContentModel.PROP_CREATOR), include); + } catch (EntityNotFoundException enfe){ + createdBy = new Person(); + createdBy.setUserName((String) nodeProps.get(ContentModel.PROP_CREATOR)); + } + nodeProps.put(Comment.PROP_COMMENT_CREATED_BY, createdBy); + + Person modifiedBy; + try { + modifiedBy = people.getPerson((String) nodeProps.get(ContentModel.PROP_MODIFIER), include); + } catch (EntityNotFoundException enfe) + { + modifiedBy = new Person(); + modifiedBy.setUserName((String) nodeProps.get(ContentModel.PROP_MODIFIER)); + } + nodeProps.put(Comment.PROP_COMMENT_MODIFIED_BY, modifiedBy); + + Comment comment = new Comment(commentNodeRef.getId(), nodeProps, canEdit, canDelete); + return comment; + } + + public Comment createComment(String nodeId, Comment comment) + { + NodeRef nodeRef = nodes.validateNode(nodeId); + + if(!typeConstraint.matches(nodeRef)) + { + throw new UnsupportedResourceOperationException("Cannot comment on this node"); + } + + try + { + NodeRef commentNode = commentService.createComment(nodeRef, comment.getTitle(), comment.getContent(), false); + return toComment(nodeRef, commentNode, INCLUDE_FULL_PERSON); + } + catch(IllegalArgumentException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } + + public Comment updateComment(String nodeId, Comment comment) + { + try + { + NodeRef nodeRef = nodes.validateNode(nodeId); + String commentNodeId = comment.getId(); + NodeRef commentNodeRef = nodes.validateNode(commentNodeId); + + String title = comment.getTitle(); + String content = comment.getContent(); + + if(content == null) + { + throw new InvalidArgumentException(); + } + + commentService.updateComment(commentNodeRef, title, content); + return toComment(nodeRef, commentNodeRef, INCLUDE_FULL_PERSON); + } + catch(IllegalArgumentException e) + { + throw new ConstraintViolatedException(e.getMessage()); + } + } + + public CollectionWithPagingInfo getComments(String nodeId, Paging paging, List include) + { + final NodeRef nodeRef = nodes.validateNode(nodeId); + + /* MNT-10536 : fix */ + final Set contentAndFolders = + new HashSet<>(Arrays.asList(ContentModel.TYPE_FOLDER, ContentModel.TYPE_CONTENT)); + if (!nodes.nodeMatches(nodeRef, contentAndFolders, null)) + { + throw new InvalidArgumentException("NodeId of folder or content is expected"); + } + + PagingRequest pagingRequest = Util.getPagingRequest(paging); + final PagingResults pagingResults = commentService.listComments(nodeRef, pagingRequest); + + final List page = pagingResults.getPage(); + List comments = new AbstractList<>() + { + @Override + public Comment get(int index) + { + return toComment(nodeRef, page.get(index), include); + } + + @Override + public int size() + { + return page.size(); + } + }; + + return CollectionWithPagingInfo.asPaged(paging, comments, pagingResults.hasMoreItems(), pagingResults.getTotalResultCount().getFirst()); + } + + @Override + public void deleteComment(String nodeId, String commentNodeId) + { + try + { + NodeRef nodeRef = nodes.validateNode(nodeId); + NodeRef commentNodeRef = nodes.validateNode(commentNodeId); + + if (! nodeRef.equals(commentService.getDiscussableAncestor(commentNodeRef))) + { + throw new InvalidArgumentException("Unexpected "+nodeId+","+commentNodeId); + } + + commentService.deleteComment(commentNodeRef); + } + catch(IllegalArgumentException e) + { + throw new ConstraintViolatedException(e.getMessage()); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java new file mode 100644 index 00000000000..f1fa39df0d0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java @@ -0,0 +1,1733 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.dictionary.CompiledModel; +import org.alfresco.repo.dictionary.CustomModelDefinitionImpl; +import org.alfresco.repo.dictionary.Facetable; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2Class; +import org.alfresco.repo.dictionary.M2Constraint; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Namespace; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.dictionary.M2Type; +import org.alfresco.repo.dictionary.ValueDataTypeValidator; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.rest.api.CustomModels; +import org.alfresco.rest.api.model.AbstractClassModel; +import org.alfresco.rest.api.model.AbstractCommonDetails; +import org.alfresco.rest.api.model.CustomAspect; +import org.alfresco.rest.api.model.CustomModel; +import org.alfresco.rest.api.model.CustomModel.ModelStatus; +import org.alfresco.rest.api.model.CustomModelConstraint; +import org.alfresco.rest.api.model.CustomModelDownload; +import org.alfresco.rest.api.model.CustomModelNamedValue; +import org.alfresco.rest.api.model.CustomModelProperty; +import org.alfresco.rest.api.model.CustomType; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.CustomModelDefinition; +import org.alfresco.service.cmr.dictionary.CustomModelException; +import org.alfresco.service.cmr.dictionary.CustomModelService; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.ModelDefinition; +import org.alfresco.service.cmr.dictionary.NamespaceDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.dictionary.CustomModelException.ActiveModelConstraintException; +import org.alfresco.service.cmr.dictionary.CustomModelException.CustomModelConstraintException; +import org.alfresco.service.cmr.dictionary.CustomModelException.InvalidCustomModelException; +import org.alfresco.service.cmr.dictionary.CustomModelException.ModelDoesNotExistException; +import org.alfresco.service.cmr.dictionary.CustomModelException.ModelExistsException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.alfresco.util.collections.CollectionUtils; +import org.alfresco.util.collections.Function; +import org.apache.commons.lang3.StringUtils; +import org.springframework.extensions.surf.util.I18NUtil; + + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomModelsImpl implements CustomModels +{ + // for consistency the patterns are equivalent to the patterns defined in the cmm-misc.lib.js + public static final Pattern NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_\\-]+$"); + public static final Pattern URI_PATTERN = Pattern.compile("^[A-Za-z0-9:/_\\.\\-]+$"); + + public static final String MODEL_NAME_NULL_ERR = "cmm.rest_api.model_name_null"; + public static final String TYPE_NAME_NULL_ERR = "cmm.rest_api.type_name_null"; + public static final String ASPECT_NAME_NULL_ERR = "cmm.rest_api.aspect_name_null"; + public static final String CONSTRAINT_NAME_NULL_ERR = "cmm.rest_api.constraint_name_null"; + + // Services + protected CustomModelService customModelService; + protected DictionaryService dictionaryService; + protected PersonService personService; + protected NodeService nodeService; + protected NamespaceService namespaceService; + protected ValueDataTypeValidator valueDataTypeValidator; + + private static final String DEFAULT_DATA_TYPE = "d:text"; + private static final String BOOLEAN_DATA_TYPE = "d:boolean"; + private static final String SELECT_ALL = "all"; + private static final String SELECT_STATUS = "status"; + private static final String SELECT_PROPS = "props"; + private static final String SELECT_ALL_PROPS = "allProps"; + private static final String PARAM_UPDATE_PROP = "update"; + private static final String PARAM_DELETE_PROP = "delete"; + private static final String PARAM_WITH_EXT_MODULE = "extModule"; + + public void setCustomModelService(CustomModelService customModelService) + { + this.customModelService = customModelService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setValueDataTypeValidator(ValueDataTypeValidator valueDataTypeValidator) + { + this.valueDataTypeValidator = valueDataTypeValidator; + } + + @Override + public CustomModel getCustomModel(String modelName, Parameters parameters) + { + CustomModelDefinition modelDef = getCustomModelImpl(modelName); + + if (hasSelectProperty(parameters, SELECT_ALL)) + { + return new CustomModel(modelDef, + convertToCustomTypes(modelDef.getTypeDefinitions(), false), + convertToCustomAspects(modelDef.getAspectDefinitions(), false), + convertToCustomModelConstraints(modelDef.getModelDefinedConstraints())); + } + + return new CustomModel(modelDef); + } + + private CustomModelDefinition getCustomModelImpl(String modelName) + { + if(modelName == null) + { + throw new InvalidArgumentException(MODEL_NAME_NULL_ERR); + } + + CustomModelDefinition model = null; + try + { + model = customModelService.getCustomModel(modelName); + } + catch (CustomModelException ex) + { + throw new EntityNotFoundException(modelName); + } + + if (model == null) + { + throw new EntityNotFoundException(modelName); + } + + return model; + } + + @Override + public CollectionWithPagingInfo getCustomModels(Parameters parameters) + { + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); + PagingResults results = customModelService.getCustomModels(pagingRequest); + + Integer totalItems = results.getTotalResultCount().getFirst(); + List page = results.getPage(); + + List models = new ArrayList<>(page.size()); + for (CustomModelDefinition modelDefinition : page) + { + models.add(new CustomModel(modelDefinition)); + } + + return CollectionWithPagingInfo.asPaged(paging, models, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue())); + } + + @Override + public CustomModel createCustomModel(CustomModel model) + { + // Check the current user is authorised to create a custom model + validateCurrentUser(); + return createCustomModelImpl(model, true); + } + + private CustomModel createCustomModelImpl(CustomModel model, boolean basicModelOnly) + { + M2Model m2Model = null; + if (basicModelOnly) + { + m2Model = convertToM2Model(model, null, null, null); + } + else + { + m2Model = convertToM2Model(model, model.getTypes(), model.getAspects(), model.getConstraints()); + } + + boolean activate = ModelStatus.ACTIVE.equals(model.getStatus()); + try + { + CustomModelDefinition modelDefinition = customModelService.createCustomModel(m2Model, activate); + return new CustomModel(modelDefinition); + } + catch (ModelExistsException me) + { + throw new ConstraintViolatedException(me.getMessage()); + } + catch (CustomModelConstraintException ncx) + { + throw new ConstraintViolatedException(ncx.getMessage()); + } + catch (InvalidCustomModelException iex) + { + throw new InvalidArgumentException(iex.getMessage()); + } + catch (Exception e) + { + throw new ApiException("cmm.rest_api.model_invalid", e); + } + } + + @Override + public CustomModel updateCustomModel(String modelName, CustomModel model, Parameters parameters) + { + // Check the current user is authorised to update the custom model + validateCurrentUser(); + + // Check to see if the model exists + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + CustomModel existingModel = existingModelDetails.getModel(); + + // The model just needs to be activated/deactivated (in other words, + // the other properties should be untouched) + if (hasSelectProperty(parameters, SELECT_STATUS)) + { + ModelStatus status = model.getStatus(); + if (status == null) + { + throw new InvalidArgumentException("cmm.rest_api.model_status_null"); + } + try + { + if (ModelStatus.ACTIVE.equals(status)) + { + customModelService.activateCustomModel(modelName); + } + else + { + customModelService.deactivateCustomModel(modelName); + } + // update the model's status + existingModel.setStatus(status); + return existingModel; + } + catch (CustomModelConstraintException mce) + { + throw new ConstraintViolatedException(mce.getMessage()); + } + catch (Exception ex) + { + throw new ApiException(ex.getMessage(), ex); + } + } + else + { + if (model.getName() != null && !(existingModel.getName().equals(model.getName()))) + { + throw new InvalidArgumentException("cmm.rest_api.model_name_cannot_update"); + } + + existingModel.setNamespaceUri(model.getNamespaceUri()); + final boolean isNamespacePrefixChanged = !(existingModel.getNamespacePrefix().equals(model.getNamespacePrefix())); + if(isNamespacePrefixChanged) + { + // Change types' and aspects' parents as well as the property constraint's Ref namespace prefix + replacePrefix(existingModelDetails.getTypes(), existingModel.getNamespacePrefix(), model.getNamespacePrefix()); + replacePrefix(existingModelDetails.getAspects(), existingModel.getNamespacePrefix(), model.getNamespacePrefix()); + } + existingModel.setNamespacePrefix(model.getNamespacePrefix()); + existingModel.setAuthor(model.getAuthor()); + existingModel.setDescription(model.getDescription()); + + CustomModelDefinition modelDef = updateModel(existingModelDetails, "cmm.rest_api.model_update_failure"); + return new CustomModel(modelDef); + } + } + + private void replacePrefix(List existingTypesOrAspects, String modelOldNamespacePrefix, String modelNewNamespacePrefix) + { + for(AbstractClassModel classModel : existingTypesOrAspects) + { + // Type/Aspect's parent name + String parentName = classModel.getParentName(); + if(parentName != null) + { + Pair prefixLocalNamePair = splitPrefixedQName(parentName); + // Check to see if the parent name prefix, is the namespace prefix of the model being edited. + // As we don't want to modify the parent name of the imported models. + if(modelOldNamespacePrefix.equals(prefixLocalNamePair.getFirst())) + { + // Change the parent name prefix, to a new model namespace prefix. + String newParentName = constructName(prefixLocalNamePair.getSecond(), modelNewNamespacePrefix); + classModel.setParentName(newParentName); + } + } + + // Change the property constraint ref + List properties = classModel.getProperties(); + for(CustomModelProperty prop : properties) + { + List constraintRefs = prop.getConstraintRefs(); + if(constraintRefs.size() > 0) + { + List modifiedRefs = new ArrayList<>(constraintRefs.size()); + for(String ref : constraintRefs) + { + // We don't need to check if the prefix is equal to the model prefix here, as it was + // done upon adding the constraint refs in the setM2Properties method. + Pair prefixLocalNamePair = splitPrefixedQName(ref); + // Change the constraint ref prefix, to a new model namespace prefix. + String newRef = constructName(prefixLocalNamePair.getSecond(), modelNewNamespacePrefix); + modifiedRefs.add(newRef); + } + prop.setConstraintRefs(modifiedRefs); + } + } + } + } + + @Override + public void deleteCustomModel(String modelName) + { + // Check the current user is authorised to delete the custom model + validateCurrentUser(); + + if(modelName == null) + { + throw new InvalidArgumentException(MODEL_NAME_NULL_ERR); + } + + try + { + customModelService.deleteCustomModel(modelName); + } + catch (ModelDoesNotExistException ee) + { + throw new EntityNotFoundException(modelName); + } + catch (ActiveModelConstraintException ae) + { + throw new ConstraintViolatedException(ae.getMessage()); + } + catch (Exception ex) + { + throw new ApiException(ex.getMessage(), ex); + } + } + + @Override + public CustomType getCustomType(String modelName, String typeName, Parameters parameters) + { + if(typeName == null) + { + throw new InvalidArgumentException(TYPE_NAME_NULL_ERR); + } + + final CustomModelDefinition modelDef = getCustomModelImpl(modelName); + QName typeQname = QName.createQName(modelDef.getName().getNamespaceURI(), typeName); + + TypeDefinition customTypeDef = customModelService.getCustomType(typeQname); + if (customTypeDef == null) + { + throw new EntityNotFoundException(typeName); + } + + // Check if inherited properties have been requested + boolean includeInheritedProps = hasSelectProperty(parameters, SELECT_ALL_PROPS); + return convertToCustomType(customTypeDef, includeInheritedProps); + } + + @Override + public CollectionWithPagingInfo getCustomTypes(String modelName, Parameters parameters) + { + CustomModelDefinition modelDef = getCustomModelImpl(modelName); + Collection typeDefinitions = modelDef.getTypeDefinitions(); + // TODO Should we support paging? + Paging paging = Paging.DEFAULT; + + List customTypes = convertToCustomTypes(typeDefinitions, false); + + return CollectionWithPagingInfo.asPaged(paging, customTypes, false, typeDefinitions.size()); + + } + + @Override + public CustomType createCustomType(String modelName, CustomType type) + { + // Check the current user is authorised to update the custom model + validateCurrentUser(); + + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + + // Validate type's parent + validateTypeAspectParent(type, existingModelDetails.getModel()); + existingModelDetails.getTypes().add(type); + + updateModel(existingModelDetails, "cmm.rest_api.type_create_failure"); + return type; + } + + @Override + public CustomType updateCustomType(String modelName, CustomType type, Parameters parameters) + { + return updateTypeAspect(modelName, type, parameters); + } + + private T updateTypeAspect(String modelName, T classDef, Parameters parameters) + { + // Check the current user is authorised to update the custom model + validateCurrentUser(); + + final boolean isAspect = classDef instanceof CustomAspect; + + String name = classDef.getName(); + if(name == null) + { + String msgId = isAspect ? ASPECT_NAME_NULL_ERR : TYPE_NAME_NULL_ERR; + throw new InvalidArgumentException(msgId); + } + + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + + List allClassDefs = isAspect ? existingModelDetails.getAspects() : existingModelDetails.getTypes(); + + @SuppressWarnings("unchecked") + T existingClassDef = (T) getObjectByName(allClassDefs, name); + if (existingClassDef == null) + { + throw new EntityNotFoundException(name); + } + + if (hasSelectProperty(parameters, SELECT_PROPS)) + { + String errorMsg = null; + String propName = parameters.getParameter(PARAM_DELETE_PROP); + if (propName == null) + { + errorMsg = "cmm.rest_api.property_create_update_failure"; + // Add/Update properties + mergeProperties(existingClassDef, classDef, parameters, existingModelDetails.isActive()); + } + else //Delete property request + { + errorMsg = "cmm.rest_api.property_delete_failure"; + deleteProperty(existingClassDef, propName); + } + + updateModel(existingModelDetails, errorMsg); + } + else + { + existingClassDef.setTitle(classDef.getTitle()); + existingClassDef.setDescription(classDef.getDescription()); + final boolean isParentChanged = !(StringUtils.equals(existingClassDef.getParentName(), classDef.getParentName())); + if (isParentChanged && existingModelDetails.isActive()) + { + String errMsgId = isAspect ? "cmm.rest_api.aspect_parent_cannot_update" : "cmm.rest_api.type_parent_cannot_update"; + throw new ConstraintViolatedException(errMsgId); + } + // Validate type/aspect parent + validateTypeAspectParent(classDef, existingModelDetails.getModel()); + existingClassDef.setParentName(classDef.getParentName()); + + String errMsgId = isAspect ? "cmm.rest_api.aspect_update_failure" : "cmm.rest_api.type_update_failure"; + updateModel(existingModelDetails, errMsgId); + } + return existingClassDef; + } + + @Override + public void deleteCustomType(String modelName, String typeName) + { + // Check the current user is authorised to delete the custom model's type + validateCurrentUser(); + + if(typeName == null) + { + throw new InvalidArgumentException(TYPE_NAME_NULL_ERR); + } + + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + if(existingModelDetails.isActive()) + { + throw new ConstraintViolatedException("cmm.rest_api.type_cannot_delete"); + } + + Map allTypes = transformToMap(existingModelDetails.getTypes(), toNameFunction()); + CustomType typeToBeDeleted = allTypes.get(typeName); + + if(typeToBeDeleted == null) + { + throw new EntityNotFoundException(typeName); + } + + // Validate type's dependency + validateTypeAspectDelete(allTypes.values(), typeToBeDeleted.getPrefixedName()); + + // Remove the validated type + allTypes.remove(typeName); + existingModelDetails.setTypes(new ArrayList<>(allTypes.values())); + + updateModel(existingModelDetails, "cmm.rest_api.type_delete_failure"); + } + + @Override + public CustomAspect getCustomAspect(String modelName, String aspectName, Parameters parameters) + { + if(aspectName == null) + { + throw new InvalidArgumentException(ASPECT_NAME_NULL_ERR); + } + + final CustomModelDefinition modelDef = getCustomModelImpl(modelName); + QName aspectQname = QName.createQName(modelDef.getName().getNamespaceURI(), aspectName); + + AspectDefinition customAspectDef = customModelService.getCustomAspect(aspectQname); + if (customAspectDef == null) + { + throw new EntityNotFoundException(aspectName); + } + + // Check if inherited properties have been requested + boolean includeInheritedProps = hasSelectProperty(parameters, SELECT_ALL_PROPS); + return convertToCustomAspect(customAspectDef, includeInheritedProps); + } + + @Override + public CollectionWithPagingInfo getCustomAspects(String modelName, Parameters parameters) + { + CustomModelDefinition modelDef = getCustomModelImpl(modelName); + Collection aspectDefinitions = modelDef.getAspectDefinitions(); + // TODO Should we support paging? + Paging paging = Paging.DEFAULT; + + List customAspects = convertToCustomAspects(aspectDefinitions, false); + + return CollectionWithPagingInfo.asPaged(paging, customAspects, false, aspectDefinitions.size()); + } + + @Override + public CustomAspect createCustomAspect(String modelName, CustomAspect aspect) + { + // Check the current user is authorised to update the custom model + validateCurrentUser(); + + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + + // Validate aspect's parent + validateTypeAspectParent(aspect, existingModelDetails.getModel()); + existingModelDetails.getAspects().add(aspect); + + updateModel(existingModelDetails, "cmm.rest_api.aspect_create_failure"); + return aspect; + } + + @Override + public CustomAspect updateCustomAspect(String modelName, CustomAspect aspect, Parameters parameters) + { + return updateTypeAspect(modelName, aspect, parameters); + } + + @Override + public void deleteCustomAspect(String modelName, String aspectName) + { + // Check the current user is authorised to delete the custom model's aspect + validateCurrentUser(); + + if(aspectName == null) + { + throw new InvalidArgumentException(ASPECT_NAME_NULL_ERR); + } + + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + if(existingModelDetails.isActive()) + { + throw new ConstraintViolatedException("cmm.rest_api.aspect_cannot_delete"); + } + + Map allAspects = transformToMap(existingModelDetails.getAspects(), toNameFunction()); + CustomAspect aspectToBeDeleted = allAspects.get(aspectName); + + if(aspectToBeDeleted == null) + { + throw new EntityNotFoundException(aspectName); + } + + // Validate aspect's dependency + validateTypeAspectDelete(allAspects.values(), aspectToBeDeleted.getPrefixedName()); + + // Remove the validated aspect + allAspects.remove(aspectName); + existingModelDetails.setAspects(new ArrayList<>(allAspects.values())); + + updateModel(existingModelDetails, "cmm.rest_api.aspect_delete_failure"); + } + + @Override + public CollectionWithPagingInfo getCustomModelConstraints(String modelName, Parameters parameters) + { + CustomModelDefinition modelDef = getCustomModelImpl(modelName); + Collection constraintDefinitions = modelDef.getModelDefinedConstraints(); + // TODO Should we support paging? + Paging paging = Paging.DEFAULT; + + List customModelConstraints = convertToCustomModelConstraints(constraintDefinitions); + + return CollectionWithPagingInfo.asPaged(paging, customModelConstraints, false, constraintDefinitions.size()); + } + + @Override + public CustomModelConstraint getCustomModelConstraint(String modelName, String constraintName, Parameters parameters) + { + if (constraintName == null) + { + throw new InvalidArgumentException(CONSTRAINT_NAME_NULL_ERR); + } + + final CustomModelDefinition modelDef = getCustomModelImpl(modelName); + QName constraintQname = QName.createQName(modelDef.getName().getNamespaceURI(), constraintName); + + ConstraintDefinition constraintDef = customModelService.getCustomConstraint(constraintQname); + if (constraintDef == null) + { + throw new EntityNotFoundException(constraintName); + } + + return new CustomModelConstraint(constraintDef, dictionaryService); + } + + @Override + public CustomModelConstraint createCustomModelConstraint(String modelName, CustomModelConstraint constraint) + { + // Check the current user is authorised to create constraints + validateCurrentUser(); + + ModelDetails existingModelDetails = new ModelDetails(getCustomModelImpl(modelName)); + + existingModelDetails.getModelDefinedConstraints().add(constraint); + + updateModel(existingModelDetails, "cmm.rest_api.constraint_create_failure"); + return constraint; + } + + @Override + public CustomModelDownload createDownload(String modelName, Parameters parameters) + { + // Check the current user is authorised to export the model + validateCurrentUser(); + + if (modelName == null) + { + throw new InvalidArgumentException(MODEL_NAME_NULL_ERR); + } + + String propName = parameters.getParameter(PARAM_WITH_EXT_MODULE); + boolean withForm = Boolean.valueOf(propName); + try + { + NodeRef nodeRef = customModelService.createDownloadNode(modelName, withForm); + return new CustomModelDownload(nodeRef); + } + catch (Exception ex) + { + String errorMsg = "cmm.rest_api.model_download_failure"; + if (ex.getMessage() != null) + { + errorMsg = ex.getMessage(); + } + throw new ApiException(errorMsg, ex); + } + } + + private CustomType convertToCustomType(TypeDefinition typeDefinition, boolean includeInheritedProps) + { + List properties = convertToCustomModelProperty(typeDefinition, includeInheritedProps); + return new CustomType(typeDefinition, dictionaryService, properties); + } + + private List convertToCustomTypes(Collection typeDefinitions, boolean includeInheritedProps) + { + // Convert a collection of TypeDefinitions into a list of CustomTypes + List customTypes = new ArrayList<>(typeDefinitions.size()); + for (TypeDefinition td : typeDefinitions) + { + customTypes.add(convertToCustomType(td, includeInheritedProps)); + } + + return customTypes; + } + + private CustomAspect convertToCustomAspect(AspectDefinition aspectDefinition, boolean includeInheritedProps) + { + List properties = convertToCustomModelProperty(aspectDefinition, includeInheritedProps); + return new CustomAspect(aspectDefinition, dictionaryService, properties); + } + + private List convertToCustomAspects(Collection aspectDefinitions, boolean includeInheritedProps) + { + // Convert a collection of AspectDefinitions into a list of CustomAspect + List customAspects = new ArrayList<>(aspectDefinitions.size()); + for (AspectDefinition ad : aspectDefinitions) + { + customAspects.add(convertToCustomAspect(ad, includeInheritedProps)); + } + + return customAspects; + } + + private List convertToCustomModelProperty(ClassDefinition classDefinition, boolean includeInherited) + { + Collection ownProperties = null; + ClassDefinition parentDef = classDefinition.getParentClassDefinition(); + if (!includeInherited && parentDef != null) + { + // Remove inherited properties + ownProperties = removeRightEntries(classDefinition.getProperties(), parentDef.getProperties()).values(); + } + else + { + ownProperties = classDefinition.getProperties().values(); + } + + List customProperties = new ArrayList<>(ownProperties.size()); + for (PropertyDefinition propDef : ownProperties) + { + customProperties.add(new CustomModelProperty(propDef, dictionaryService)); + } + + return customProperties; + } + + private List convertToCustomModelConstraints(Collection constraintDefinitions) + { + List constraints = new ArrayList<>(constraintDefinitions.size()); + for (ConstraintDefinition definition : constraintDefinitions) + { + constraints.add(new CustomModelConstraint(definition, dictionaryService)); + } + return constraints; + } + + /** + * Converts the given {@code ModelDetails} object into a {@link M2Model} object + * + * @param modelDetails the custom model details + * @return {@link M2Model} object + */ + private M2Model convertToM2Model(ModelDetails modelDetails) + { + return convertToM2Model(modelDetails.getModel(), modelDetails.getTypes(), modelDetails.getAspects(), modelDetails.getModelDefinedConstraints()); + } + + /** + * Converts the given {@code org.alfresco.rest.api.model.CustomModel} + * object, a collection of {@code org.alfresco.rest.api.model.CustomType} + * objects, a collection of + * {@code org.alfresco.rest.api.model.CustomAspect} objects, and a collection of + * {@code org.alfresco.rest.api.model.CustomModelConstraint} objects into a {@link M2Model} object + * + * @param customModel the custom model + * @param types the custom types + * @param aspects the custom aspects + * @param constraints the custom constraints + * @return {@link M2Model} object + */ + private M2Model convertToM2Model(CustomModel customModel, Collection types, Collection aspects, Collection constraints) + { + validateBasicModelInput(customModel); + + Set> namespacesToImport = new LinkedHashSet<>(); + + final String namespacePrefix = customModel.getNamespacePrefix(); + final String namespaceURI = customModel.getNamespaceUri(); + // Construct the model name + final String name = constructName(customModel.getName(), namespacePrefix); + + M2Model model = M2Model.createModel(name); + model.createNamespace(namespaceURI, namespacePrefix); + model.setDescription(customModel.getDescription()); + String author = customModel.getAuthor(); + if (author == null) + { + author = getCurrentUserFullName(); + } + model.setAuthor(author); + + // Types + if(types != null) + { + for(CustomType type : types) + { + validateName(type.getName(), TYPE_NAME_NULL_ERR); + M2Type m2Type = model.createType(constructName(type.getName(), namespacePrefix)); + m2Type.setDescription(type.getDescription()); + m2Type.setTitle(type.getTitle()); + setParentName(m2Type, type.getParentName(), namespacesToImport, namespacePrefix); + setM2Properties(m2Type, type.getProperties(), namespacePrefix, namespacesToImport); + } + } + + // Aspects + if(aspects != null) + { + for(CustomAspect aspect : aspects) + { + validateName(aspect.getName(), ASPECT_NAME_NULL_ERR); + M2Aspect m2Aspect = model.createAspect(constructName(aspect.getName(), namespacePrefix)); + m2Aspect.setDescription(aspect.getDescription()); + m2Aspect.setTitle(aspect.getTitle()); + setParentName(m2Aspect, aspect.getParentName(), namespacesToImport, namespacePrefix); + setM2Properties(m2Aspect, aspect.getProperties(), namespacePrefix, namespacesToImport); + } + } + + // Constraints + if(constraints != null) + { + for (CustomModelConstraint constraint : constraints) + { + validateName(constraint.getName(), CONSTRAINT_NAME_NULL_ERR); + final String constraintName = constructName(constraint.getName(), namespacePrefix); + M2Constraint m2Constraint = model.createConstraint(constraintName, constraint.getType()); + // Set title, desc and parameters + setConstraintOtherData(constraint, m2Constraint, null); + } + } + + // Add imports + for (Pair uriPrefix : namespacesToImport) + { + // Don't import the already defined namespace + if (!namespaceURI.equals(uriPrefix.getFirst())) + { + model.createImport(uriPrefix.getFirst(), uriPrefix.getSecond()); + } + } + + return model; + } + + private void setConstraintOtherData(CustomModelConstraint constraint, M2Constraint m2Constraint, String propDataType) + { + if (m2Constraint.getType() == null) + { + throw new InvalidArgumentException("cmm.rest_api.constraint_type_null"); + } + + ConstraintValidator constraintValidator = ConstraintValidator.findByType(m2Constraint.getType()); + if (propDataType != null) + { + // Check if the constraint can be used with given data type + constraintValidator.validateUsage(prefixedStringToQname(propDataType)); + } + + m2Constraint.setTitle(constraint.getTitle()); + m2Constraint.setDescription(constraint.getDescription()); + for (CustomModelNamedValue parameter : constraint.getParameters()) + { + validateName(parameter.getName(), "cmm.rest_api.constraint_parameter_name_null"); + if (parameter.getListValue() != null) + { + if (propDataType != null && "allowedValues".equals(parameter.getName())) + { + validateListConstraint(parameter.getListValue(), propDataType); + } + m2Constraint.createParameter(parameter.getName(), parameter.getListValue()); + } + else + { + constraintValidator.validate(parameter.getName(), parameter.getSimpleValue()); + m2Constraint.createParameter(parameter.getName(), parameter.getSimpleValue()); + } + } + } + + /* + * List constraint is a special case, so can't use the ConstraintValidator. + */ + private void validateListConstraint(List listValue, String propDataType) + { + for (String value : listValue) + { + try + { + // validate list values + this.valueDataTypeValidator.validateValue(propDataType, value); + } + catch (Exception ex) + { + throw new InvalidArgumentException(ex.getMessage()); + } + } + } + + private void setM2Properties(M2Class m2Class, List properties, String namespacePrefix, + Set> namespacesToImport) + { + if (properties != null) + { + for (CustomModelProperty prop : properties) + { + validateName(prop.getName(), "cmm.rest_api.property_name_null"); + M2Property m2Property = m2Class.createProperty(constructName(prop.getName(), namespacePrefix)); + m2Property.setTitle(prop.getTitle()); + m2Property.setDescription(prop.getDescription()); + m2Property.setMandatory(prop.isMandatory()); + m2Property.setMandatoryEnforced(prop.isMandatoryEnforced()); + m2Property.setMultiValued(prop.isMultiValued()); + + String dataType = prop.getDataType(); + // Default type is d:text + if (StringUtils.isBlank(dataType)) + { + dataType = DEFAULT_DATA_TYPE; + } + else + { + if (!dataType.contains(":")) + { + throw new InvalidArgumentException("cmm.rest_api.property_datatype_invalid", new Object[] { dataType }); + } + } + namespacesToImport.add(resolveToUriAndPrefix(dataType)); + try + { + // validate default values + this.valueDataTypeValidator.validateValue(dataType, prop.getDefaultValue()); + } + catch (Exception ex) + { + throw new InvalidArgumentException(ex.getMessage()); + } + m2Property.setType(dataType); + m2Property.setDefaultValue(prop.getDefaultValue()); + + // Set indexing options + m2Property.setIndexed(prop.isIndexed()); + // SHA-1234 + // This 'if' statement can be removed when we fix the Solr schema + // so it can support boolean data type. + if (!BOOLEAN_DATA_TYPE.equals(dataType)) + { + if (Facetable.TRUE == prop.getFacetable()) + { + m2Property.setFacetable(true); + } + else if (Facetable.FALSE == prop.getFacetable()) + { + m2Property.setFacetable(false); + } + } + m2Property.setIndexTokenisationMode(prop.getIndexTokenisationMode()); + + // Check for constraints + List constraintRefs = prop.getConstraintRefs(); + List constraints = prop.getConstraints(); + if (constraintRefs.size() > 0) + { + for (String ref : constraintRefs) + { + Pair prefixLocalName = splitPrefixedQName(ref); + if (!namespacePrefix.equals(prefixLocalName.getFirst())) + { + throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.constraint_ref_not_defined", ref)); + } + m2Property.addConstraintRef(ref); + } + } + if(constraints.size() > 0) + { + for (CustomModelConstraint modelConstraint : constraints) + { + String constraintName = null; + if (modelConstraint.getName() != null) + { + validateName(modelConstraint.getName(), CONSTRAINT_NAME_NULL_ERR); + constraintName = constructName(modelConstraint.getName(), namespacePrefix); + } + M2Constraint m2Constraint = m2Property.addConstraint(constraintName, modelConstraint.getType()); + // Set title, desc and parameters + setConstraintOtherData(modelConstraint, m2Constraint, dataType); + } + } + } + } + } + + private void validateBasicModelInput(CustomModel customModel) + { + // validate model name + validateName(customModel.getName(), MODEL_NAME_NULL_ERR); + + // validate model namespace prefix + validateName(customModel.getNamespacePrefix(), "cmm.rest_api.model_namespace_prefix_null"); + + // validate model namespace URI + if (customModel.getNamespaceUri() == null) + { + throw new InvalidArgumentException("cmm.rest_api.model_namespace_uri_null"); + } + else + { + Matcher matcher = URI_PATTERN.matcher(customModel.getNamespaceUri()); + if (!matcher.find()) + { + throw new InvalidArgumentException("cmm.rest_api.model_namespace_uri_invalid"); + } + } + } + + private void validateName(String name, String errMsgId) + { + if (name == null) + { + if (errMsgId == null) + { + errMsgId = InvalidArgumentException.DEFAULT_MESSAGE_ID; + } + throw new InvalidArgumentException(errMsgId); + } + else + { + Matcher matcher = NAME_PATTERN.matcher(name); + if (!matcher.find()) + { + throw new InvalidArgumentException("cmm.rest_api.input_validation_err", new Object [] {name}); + } + } + } + + /** + * Checks the current user access rights and throws + * {@link PermissionDeniedException} if the user is not a member of the + * ALFRESCO_MODEL_ADMINISTRATORS group + */ + private void validateCurrentUser() + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!customModelService.isModelAdmin(currentUser)) + { + throw new PermissionDeniedException(); + } + } + + /** + * Gets the fully authenticated user's full name + * + * @return user's full name or the user's id if the full name dose not exit + */ + protected String getCurrentUserFullName() + { + String userName = AuthenticationUtil.getFullyAuthenticatedUser(); + NodeRef personRef = personService.getPerson(userName, false); + + String firstName = (String) nodeService.getProperty(personRef, ContentModel.PROP_FIRSTNAME); + String lastName = (String) nodeService.getProperty(personRef, ContentModel.PROP_LASTNAME); + + String fullName = (firstName != null ? firstName + " " : "") + (lastName != null ? lastName : ""); + + return ((StringUtils.isBlank(fullName) ? userName : fullName)).trim(); + } + + private String constructName(String name, String prefix) + { + return new StringBuilder(100).append(prefix).append(QName.NAMESPACE_PREFIX).append(name).toString(); + } + + /** + * Gets the namespace URI and prefix from the parent's name, provided that the + * given name is of a valid format. The valid format consist of a + * namespace prefix, a colon and a name. E.g. sys:localized + * + * @param parentName the parent name + * @return a pair of namespace URI and prefix object + */ + protected Pair resolveToUriAndPrefix(String parentName) + { + QName qName = prefixedStringToQname(parentName); + Collection prefixes = namespaceService.getPrefixes(qName.getNamespaceURI()); + if (prefixes.size() == 0) + { + throw new InvalidArgumentException("cmm.rest_api.prefix_not_registered", new Object[] { qName.getNamespaceURI() }); + } + String prefix = prefixes.iterator().next(); + return new Pair(qName.getNamespaceURI(), prefix); + } + + /** + * Creates {@link QName} from a valid prefixed string. + */ + private QName prefixedStringToQname(String prefixedQName) + { + try + { + return QName.createQName(prefixedQName, namespaceService); + } + catch (Exception ex) + { + String msg = ex.getMessage(); + if (msg == null) + { + msg = ""; + } + throw new InvalidArgumentException("cmm.rest_api.prefixed_qname_invalid", new Object[] { prefixedQName, msg }); + } + } + + /** + * Validates and sets the type's or aspect's parent name + * + * @param m2Class the {@link M2Type} or {@link M2Aspect} object + * @param parentPrefixedName the parent prefixed name. E.g. prefix:localName + * @param namespacesToImport the {@link Set} of namespace pairs to import + * @param modelNamespacePrefix the model namespace prefix + */ + private void setParentName(M2Class m2Class, String parentPrefixedName, Set> namespacesToImport, String modelNamespacePrefix) + { + if (StringUtils.isBlank(parentPrefixedName)) + { + return; + } + + Pair prefixLocaNamePair = splitPrefixedQName(parentPrefixedName); + if (!modelNamespacePrefix.equals(prefixLocaNamePair.getFirst())) + { + // Add to the list of imports + Pair uriPrefixPair = resolveToUriAndPrefix(parentPrefixedName); + namespacesToImport.add(uriPrefixPair); + } + m2Class.setParentName(parentPrefixedName); + } + + private void validateTypeAspectParent(AbstractClassModel typeAspect, CustomModel existingModel) + { + String parentPrefixedName = typeAspect.getParentName(); + if (StringUtils.isBlank(parentPrefixedName)) + { + return; + } + + Pair prefixLocaNamePair = splitPrefixedQName(parentPrefixedName); + String parentPrefix = prefixLocaNamePair.getFirst(); + String parentLocalName = prefixLocaNamePair.getSecond(); + + // Validate parent prefix and localName + // We know that the values are not null, we just check against the defined RegEx + validateName(parentPrefix, null); + validateName(parentLocalName, null); + + final boolean isAspect = (typeAspect instanceof CustomAspect); + ClassDefinition classDefinition = null; + QName qname = null; + if (existingModel.getNamespacePrefix().equals(parentPrefix)) + { + // Check for types/aspects within the model + qname = QName.createQName(existingModel.getNamespaceUri(), parentLocalName); + classDefinition = (isAspect) ? customModelService.getCustomAspect(qname) : customModelService.getCustomType(qname); + } + else + { + // Make sure the namespace URI and Prefix are registered + Pair uriPrefixPair = resolveToUriAndPrefix(parentPrefixedName); + + qname = QName.createQName(uriPrefixPair.getFirst(), parentLocalName); + classDefinition = (isAspect) ? dictionaryService.getAspect(qname) : dictionaryService.getType(qname); + } + + if (classDefinition == null) + { + String msgId = (isAspect) ? "cmm.rest_api.aspect_parent_not_exist" : "cmm.rest_api.type_parent_not_exist"; + throw new ConstraintViolatedException(I18NUtil.getMessage(msgId, parentPrefixedName)); + } + else + { + checkCircularDependency(classDefinition.getModel(), existingModel, parentPrefixedName); + } + } + + /** + * Validates models circular dependencies + *

E.g. if {@literal B -> A} denotes model B depends on model A, then {@link ConstraintViolatedException} must be thrown for following: + *

  • if {@literal B -> A}, then {@literal A -> B} must throw exception
  • + *
  • if {@literal B -> A} and {@literal C -> B}, then {@literal A -> C} must throw exception
  • + *
  • if {@literal B -> A} and {@literal C -> B} and {@literal D -> C}, then {@literal A -> D} must throw exception
  • + * @param modelDefinition the model which has a reference to the model containing the {@code parentPrefixedName} + * @param existingModel the model being updated + * @param parentPrefixedName the type/aspect parent name + */ + private void checkCircularDependency(ModelDefinition modelDefinition, CustomModel existingModel, String parentPrefixedName) + { + for (NamespaceDefinition importedNamespace : modelDefinition.getImportedNamespaces()) + { + ModelDefinition md = null; + if ((md = customModelService.getCustomModelByUri(importedNamespace.getUri())) != null) + { + if (existingModel.getNamespaceUri().equals(importedNamespace.getUri())) + { + String msg = I18NUtil.getMessage("cmm.rest_api.circular_dependency_err", parentPrefixedName, existingModel.getName()); + throw new ConstraintViolatedException(msg); + } + checkCircularDependency(md, existingModel, parentPrefixedName); + } + } + } + + /** + * Returns the qualified name of the following format + * prefix:localName, as a pair of (prefix, localName) + * + * @param prefixedQName the prefixed name. E.g. prefix:localName + * @return {@link Pair} of (prefix, localName) + */ + private Pair splitPrefixedQName(String prefixedQName) + { + // index 0 => prefix and index 1 => local name + String[] prefixLocalName = QName.splitPrefixedQName(prefixedQName); + + if (NamespaceService.DEFAULT_PREFIX.equals(prefixLocalName[0])) + { + throw new InvalidArgumentException("cmm.rest_api.prefixed_qname_invalid_format", new Object[] { prefixedQName }); + } + + return new Pair(prefixLocalName[0], prefixLocalName[1]); + } + + private CustomModelDefinition updateModel(ModelDetails modelDetails, String errorMsg) + { + M2Model m2Model = convertToM2Model(modelDetails); + try + { + CustomModelDefinition modelDef = customModelService.updateCustomModel(modelDetails.getModel().getName(), m2Model, modelDetails.isActive()); + return modelDef; + } + catch (CustomModelConstraintException mce) + { + throw new ConstraintViolatedException(mce.getMessage()); + } + catch (InvalidCustomModelException iex) + { + throw new InvalidArgumentException(iex.getMessage()); + } + catch (Exception ex) + { + if (ex.getMessage() != null) + { + errorMsg = ex.getMessage(); + } + throw new ApiException(errorMsg, ex); + } + } + + private void mergeProperties(AbstractClassModel existingDetails, AbstractClassModel newDetails, Parameters parameters, boolean isModelActive) + { + validateList(newDetails.getProperties(), "cmm.rest_api.properties_empty_null"); + + // Transform existing properties into a map + Map existingProperties = transformToMap(existingDetails.getProperties(), toNameFunction()); + + // Transform new properties into a map + Map newProperties = transformToMap(newDetails.getProperties(), toNameFunction()); + + String propName = parameters.getParameter(PARAM_UPDATE_PROP); + // (propName == null) => property create request + if (propName == null) + { + // As this is a create request, check for duplicate properties + for (String name : newProperties.keySet()) + { + if (existingProperties.containsKey(name)) + { + throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.property_create_name_already_in_use", name)); + } + } + } + else + {// Update request + CustomModelProperty existingProp = existingProperties.get(propName); + if (existingProp == null) + { + throw new EntityNotFoundException(propName); + } + + CustomModelProperty modifiedProp = newProperties.get(propName); + if (modifiedProp == null) + { + throw new InvalidArgumentException("cmm.rest_api.property_update_prop_not_found", new Object[] { propName }); + } + + existingProp.setTitle(modifiedProp.getTitle()); + existingProp.setDescription(modifiedProp.getDescription()); + existingProp.setDefaultValue(modifiedProp.getDefaultValue()); + existingProp.setConstraintRefs(modifiedProp.getConstraintRefs()); + existingProp.setConstraints(modifiedProp.getConstraints()); + if (isModelActive) + { + validateActivePropertyUpdate(existingProp, modifiedProp); + } + existingProp.setDataType(modifiedProp.getDataType()); + existingProp.setMandatory(modifiedProp.isMandatory()); + existingProp.setMandatoryEnforced(modifiedProp.isMandatoryEnforced()); + existingProp.setMultiValued(modifiedProp.isMultiValued()); + } + // Override properties + existingProperties.putAll(newProperties); + existingDetails.setProperties(new ArrayList<>(existingProperties.values())); + } + + /** + * A helper method to throw a more informative exception (for an active model) rather than depending on the + * {@link org.alfresco.repo.dictionary.ModelValidatorImpl#validateModel} + * generic exception. + */ + private void validateActivePropertyUpdate(CustomModelProperty existingProp, CustomModelProperty newProp) + { + if (!StringUtils.equals(existingProp.getDataType(), newProp.getDataType())) + { + throw new ConstraintViolatedException("cmm.rest_api.property_change_datatype_err"); + } + if (existingProp.isMandatory() != newProp.isMandatory()) + { + throw new ConstraintViolatedException("cmm.rest_api.property_change_mandatory_opt_err"); + } + if (existingProp.isMandatoryEnforced() != newProp.isMandatoryEnforced()) + { + throw new ConstraintViolatedException("cmm.rest_api.property_change_mandatory_enforced_opt_err"); + } + if (existingProp.isMultiValued() != newProp.isMultiValued()) + { + throw new ConstraintViolatedException("cmm.rest_api.property_change_multi_valued_opt_err"); + } + } + + private void deleteProperty(AbstractClassModel existingClassModel, String propertyName) + { + // Transform existing properties into a map + Map existingProperties = transformToMap(existingClassModel.getProperties(), toNameFunction()); + if (!existingProperties.containsKey(propertyName)) + { + throw new EntityNotFoundException(propertyName); + } + existingProperties.remove(propertyName); + existingClassModel.setProperties(new ArrayList<>(existingProperties.values())); + } + + private void validateList(List list, String errorMsg) + { + if (CollectionUtils.isEmpty(list)) + { + throw new InvalidArgumentException(errorMsg); + } + } + + private static Map transformToMap(Collection collection, Function function) + { + Map map = new HashMap<>(collection.size()); + + for (V item : collection) + { + map.put(function.apply(item), item); + } + return map; + } + + private static Map removeRightEntries(Map leftMap, Map rightMap) + { + Map result = new HashMap<>(leftMap); + for (K key : rightMap.keySet()) + { + result.remove(key); + } + return result; + } + + private void validateTypeAspectDelete(Collection list, String classPrefixedName) + { + for(AbstractClassModel acm : list) + { + if(classPrefixedName.equals(acm.getParentName())) + { + throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.aspect_type_cannot_delete", classPrefixedName, acm.getPrefixedName())); + } + } + } + + private boolean hasSelectProperty(Parameters parameters, String param) + { + return parameters.getSelectedProperties().contains(param); + } + + private static Function toNameFunction() + { + return new Function() + { + @Override + public String apply(AbstractCommonDetails details) + { + return details.getName(); + } + }; + } + + private T getObjectByName(Collection collection, String name) + { + for (T details : collection) + { + if (details.getName().equals(name)) + { + return details; + } + } + return null; + } + + public class ModelDetails + { + private CustomModel model; + private boolean active; + private List types; + private List aspects; + private List modelDefinedConstraints; + + public ModelDetails(CustomModelDefinition modelDefinition) + { + this.model = new CustomModel(modelDefinition); + this.active = modelDefinition.isActive(); + this.types = convertToCustomTypes(modelDefinition.getTypeDefinitions(), false); + this.aspects = convertToCustomAspects(modelDefinition.getAspectDefinitions(), false); + this.modelDefinedConstraints = convertToCustomModelConstraints(modelDefinition.getModelDefinedConstraints()); + } + + public CustomModel getModel() + { + return this.model; + } + + public void setModel(CustomModel model) + { + this.model = model; + } + + public List getTypes() + { + return this.types; + } + + public void setTypes(List types) + { + this.types = types; + } + + public List getAspects() + { + return this.aspects; + } + + public void setAspects(List aspects) + { + this.aspects = aspects; + } + + public List getModelDefinedConstraints() + { + return this.modelDefinedConstraints; + } + + public void setModelDefinedConstraints(List modelDefinedConstraints) + { + this.modelDefinedConstraints = modelDefinedConstraints; + } + + public boolean isActive() + { + return this.active; + } + } + + /** + * Constraint validator + * + * @author Jamal Kaabi-Mofrad + */ + public enum ConstraintValidator + { + REGEX + { + @Override + public void validate(String parameterName, String value) + { + if ("expression".equals(parameterName)) + { + try + { + Pattern.compile(value); + } + catch (Exception ex) + { + throw new InvalidArgumentException("cmm.rest_api.regex_constraint_invalid_expression", new Object[] { value }); + } + } + } + }, + MINMAX + { + @Override + public void validate(String parameterName, String value) + { + double parsedValue; + try + { + parsedValue = Double.parseDouble(value); + } + catch (Exception ex) + { + throw new InvalidArgumentException("cmm.rest_api.minmax_constraint_invalid_parameter", new Object[] { value, parameterName }); + } + // SHA-1126. We check for the Double.MIN_VALUE to be consistent with NumericRangeConstraint.minValue + if("maxValue".equalsIgnoreCase(parameterName) && parsedValue < Double.MIN_VALUE) + { + throw new InvalidArgumentException("cmm.rest_api.minmax_constraint_invalid_max_value"); + } + } + + @Override + public void validateUsage(QName propDataType) + { + if (propDataType != null && !(DataTypeDefinition.INT.equals(propDataType) + || DataTypeDefinition.LONG.equals(propDataType) + || DataTypeDefinition.FLOAT.equals(propDataType) + || DataTypeDefinition.DOUBLE.equals(propDataType))) + { + throw new InvalidArgumentException("cmm.rest_api.minmax_constraint_invalid_use"); + } + } + }, + LENGTH + { + @Override + public void validate(String parameterName, String value) + { + try + { + Integer.parseInt(value); + } + catch (Exception ex) + { + throw new InvalidArgumentException("cmm.rest_api.length_constraint_invalid_parameter", new Object[] { value, parameterName }); + } + } + + @Override + public void validateUsage(QName propDataType) + { + if (propDataType != null && !(DataTypeDefinition.TEXT.equals(propDataType) + || DataTypeDefinition.MLTEXT.equals(propDataType) + || DataTypeDefinition.CONTENT.equals(propDataType))) + { + throw new InvalidArgumentException("cmm.rest_api.length_constraint_invalid_use"); + } + } + }, + DUMMY_CONSTRAINT + { + @Override + public void validate(String parameterName, String value) + { + // nothing to do + } + }; + + public abstract void validate(String parameterName, String value); + + public void validateUsage(QName propDataType) + { + return; // nothing to do + } + + public static ConstraintValidator findByType(String constraintType) + { + for (ConstraintValidator c : values()) + { + if (c.name().equals(constraintType)) + { + return c; + } + } + return DUMMY_CONSTRAINT; + } + } + + @Override + public CustomModel createCustomModel(M2Model m2Model) + { + // Check the current user is authorised to import the custom model + validateCurrentUser(); + + validateImportedM2Model(m2Model); + + CompiledModel compiledModel = null; + try + { + compiledModel = customModelService.compileModel(m2Model); + } + catch (CustomModelConstraintException mce) + { + throw new ConstraintViolatedException(mce.getMessage()); + } + catch (InvalidCustomModelException iex) + { + throw new InvalidArgumentException(iex.getMessage()); + } + + ModelDefinition modelDefinition = compiledModel.getModelDefinition(); + CustomModel customModel = new CustomModel(); + customModel.setName(modelDefinition.getName().getLocalName()); + customModel.setAuthor(modelDefinition.getAuthor()); + customModel.setDescription(modelDefinition.getDescription(dictionaryService)); + customModel.setStatus(ModelStatus.DRAFT); + NamespaceDefinition nsd = modelDefinition.getNamespaces().iterator().next(); + customModel.setNamespaceUri(nsd.getUri()); + customModel.setNamespacePrefix(nsd.getPrefix()); + + List customTypes = convertToCustomTypes(compiledModel.getTypes(), false); + List customAspects = convertToCustomAspects(compiledModel.getAspects(), false); + + List constraintDefinitions = CustomModelDefinitionImpl.removeInlineConstraints(compiledModel); + List customModelConstraints = convertToCustomModelConstraints(constraintDefinitions); + + customModel.setTypes(customTypes); + customModel.setAspects(customAspects); + customModel.setConstraints(customModelConstraints); + + return createCustomModelImpl(customModel, false); + } + + private void validateImportedM2Model(M2Model m2Model) + { + List namespaces = m2Model.getNamespaces(); + if (namespaces.size() > 1) + { + throw new ConstraintViolatedException(I18NUtil.getMessage("cmm.rest_api.model.import_namespace_multiple_found", namespaces.size())); + } + else if (namespaces.isEmpty()) + { + throw new ConstraintViolatedException("cmm.rest_api.model.import_namespace_undefined"); + } + + checkUnsupportedModelElements(m2Model.getTypes()); + checkUnsupportedModelElements(m2Model.getAspects()); + } + + private void checkUnsupportedModelElements(Collection m2Classes) + { + for (M2Class cls : m2Classes) + { + if (cls.getAssociations().size() > 0) + { + throw new ConstraintViolatedException("cmm.rest_api.model.import_associations_unsupported"); + } + if (cls.getPropertyOverrides().size() > 0) + { + throw new ConstraintViolatedException("cmm.rest_api.model.import_overrides_unsupported"); + } + if (cls.getMandatoryAspects().size() > 0) + { + throw new ConstraintViolatedException("cmm.rest_api.model.import_mandatory_aspects_unsupported"); + } + if(cls.getArchive() != null) + { + throw new ConstraintViolatedException("cmm.rest_api.model.import_archive_unsupported"); + } + if(cls.getIncludedInSuperTypeQuery() != null) + { + throw new ConstraintViolatedException("cmm.rest_api.model.import_includedInSuperTQ_unsupported"); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/DefaultExceptionHandler.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/DefaultExceptionHandler.java new file mode 100644 index 00000000000..fa30cdf9135 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/DefaultExceptionHandler.java @@ -0,0 +1,50 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; + +/** + * Translates access denied exceptions from the service layer to API not found exception. + * + * @author steveglover + * + */ +public class DefaultExceptionHandler implements ExceptionHandler +{ + @Override + public boolean handle(Throwable t) + { + if(t instanceof AccessDeniedException || t instanceof PermissionDeniedException) + { + // Note: security, no message to indicate why + throw new NotFoundException(); + } + return false; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/DeletedNodesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/DeletedNodesImpl.java new file mode 100644 index 00000000000..2398a370670 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/DeletedNodesImpl.java @@ -0,0 +1,247 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.node.archive.ArchivedNodesCannedQueryBuilder; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.repo.node.archive.RestoreNodeReport; +import org.alfresco.repo.node.integrity.IntegrityException; +import org.alfresco.rest.api.DeletedNodes; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.NodeTargetAssoc; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; + +/** + * Handles trashcan / deleted nodes + * + * @author Gethin James + */ +public class DeletedNodesImpl implements DeletedNodes, RecognizedParamsExtractor +{ + private NodeArchiveService nodeArchiveService; + private PersonService personService; + private NodeService nodeService; + private Nodes nodes; + private Renditions renditions; + + public void setNodeArchiveService(NodeArchiveService nodeArchiveService) + { + this.nodeArchiveService = nodeArchiveService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + /** + * Sets archived information on the Node + * @param aNode + * @param mapUserInfo + */ + private void mapArchiveInfo(Node aNode, Map mapUserInfo) + { + if (mapUserInfo == null) { + mapUserInfo = new HashMap<>(); + } + Map nodeProps = nodeService.getProperties(aNode.getNodeRef()); + aNode.setArchivedAt((Date)nodeProps.get(ContentModel.PROP_ARCHIVED_DATE)); + aNode.setArchivedByUser(aNode.lookupUserInfo((String)nodeProps.get(ContentModel.PROP_ARCHIVED_BY), mapUserInfo, personService)); + + //Don't show parent id + aNode.setParentId(null); + } + + @Override + public CollectionWithPagingInfo listDeleted(Parameters parameters) + { + PagingRequest pagingRequest = Util.getPagingRequest(parameters.getPaging()); + NodeRef archiveStoreRootNodeRef = nodeService.getStoreArchiveNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + + // Create canned query + ArchivedNodesCannedQueryBuilder queryBuilder = new ArchivedNodesCannedQueryBuilder.Builder( + archiveStoreRootNodeRef, pagingRequest).sortOrderAscending(false).build(); + + // Query the DB + PagingResults result = nodeArchiveService.listArchivedNodes(queryBuilder); + + Integer totalItems = result.getTotalResultCount().getFirst(); + + List nodesFound = new ArrayList(result.getPage().size()); + Map mapUserInfo = new HashMap<>(); + for (NodeRef nRef:result.getPage()) + { + Node foundNode = nodes.getFolderOrDocument(nRef, null, null, parameters.getInclude(), mapUserInfo); + mapArchiveInfo(foundNode,mapUserInfo); + nodesFound.add(foundNode); + } + + return CollectionWithPagingInfo.asPaged(parameters.getPaging(), nodesFound, result.hasMoreItems(), (totalItems == null ? null : totalItems.intValue())); + } + + @Override + public Node getDeletedNode(String originalId, Parameters parameters, boolean fullnode, Map mapUserInfo) + { + //First check the node is valid and has been archived. + NodeRef validatedNodeRef = nodes.validateNode(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, originalId); + + //Now get the Node + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, validatedNodeRef.getId()); + NodeRef archivedNodeRef = nodeArchiveService.getArchivedNode(nodeRef); + + Node foundNode = null; + if (fullnode) + { + foundNode = nodes.getFolderOrDocumentFullInfo(archivedNodeRef, null, null, parameters, mapUserInfo); + } + else + { + foundNode = nodes.getFolderOrDocument(archivedNodeRef, null, null, parameters.getInclude(), mapUserInfo); + } + + if (foundNode != null) mapArchiveInfo(foundNode,null); + return foundNode; + } + + @Override + public Node restoreArchivedNode(String archivedId, NodeTargetAssoc nodeTargetAssoc) + { + //First check the node is valid and has been archived. + NodeRef validatedNodeRef = nodes.validateNode(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, archivedId); + + RestoreNodeReport restored = null; + + if (nodeTargetAssoc != null) + { + NodeRef targetNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeTargetAssoc.getTargetParentId()); + QName assocType = nodes.getAssocType(nodeTargetAssoc.getAssocType()); + restored = nodeArchiveService.restoreArchivedNode(validatedNodeRef, targetNodeRef, assocType, null); + } + else + { + restored = nodeArchiveService.restoreArchivedNode(validatedNodeRef); + } + + switch (restored.getStatus()) + { + case SUCCESS: + return nodes.getFolderOrDocumentFullInfo(restored.getRestoredNodeRef(), null, null, null, null); + case FAILURE_PERMISSION: + throw new PermissionDeniedException(); + case FAILURE_INTEGRITY: + throw new IntegrityException("Restore failed due to an integrity error", null); + case FAILURE_DUPLICATE_CHILD_NODE_NAME: + throw new ConstraintViolatedException("Name already exists in target"); + case FAILURE_INVALID_ARCHIVE_NODE: + throw new EntityNotFoundException(archivedId); + case FAILURE_INVALID_PARENT: + throw new NotFoundException("Invalid parent id "+restored.getTargetParentNodeRef()); + default: + throw new ApiException("Unable to restore node "+archivedId); + } + } + + @Override + public void purgeArchivedNode(String archivedId) + { + //First check the node is valid and has been archived. + NodeRef validatedNodeRef = nodes.validateNode(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, archivedId); + nodeArchiveService.purgeArchivedNode(validatedNodeRef); + } + + @Override + public BinaryResource getContent(String archivedId, String renditionId, Parameters parameters) + { + // First check if the archived node is valid + NodeRef validatedNodeRef = nodes.validateNode(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, archivedId); + + if (renditionId != null) + { + return renditions.getContent(validatedNodeRef, renditionId, parameters); + } + else + { + return nodes.getContent(validatedNodeRef, parameters, false); + } + } + + @Override + public Rendition getRendition(String archivedId, String renditionId, Parameters parameters) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, archivedId); + Rendition rendition = renditions.getRendition(nodeRef, renditionId, parameters); + return rendition; + } + + @Override + public CollectionWithPagingInfo getRenditions(String archivedId, Parameters parameters) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, archivedId); + return renditions.getRenditions(nodeRef, parameters); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/DownloadsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/DownloadsImpl.java new file mode 100644 index 00000000000..87b428927b6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/DownloadsImpl.java @@ -0,0 +1,175 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.HashSet; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.download.DownloadModel; +import org.alfresco.rest.api.Downloads; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Download; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.service.cmr.download.DownloadService; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; + +/** + * + * @author cpopa + * + */ +public class DownloadsImpl implements Downloads +{ + private DownloadService downloadService; + private NodeService nodeService; + private Nodes nodes; + private PermissionService permissionService; + public static final String DEFAULT_ARCHIVE_NAME = "archive.zip"; + public static final String DEFAULT_ARCHIVE_EXTENSION = ".zip"; + + public void setDownloadService(DownloadService downloadService) + { + this.downloadService = downloadService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + @Override + public Download createDownloadNode(Download download) + { + checkEmptyNodeIds(download); + + checkDuplicateNodeId(download); + + NodeRef[] zipContentNodeRefs = validateAndGetNodeRefs(download); + + checkNodeIdsReadPermission(zipContentNodeRefs); + + NodeRef zipNodeRef = downloadService.createDownload(zipContentNodeRefs, true); + + String archiveName = zipContentNodeRefs.length > 1 ? + DEFAULT_ARCHIVE_NAME : + nodeService.getProperty(zipContentNodeRefs[0], ContentModel.PROP_NAME) + DEFAULT_ARCHIVE_EXTENSION; + + nodeService.setProperty(zipNodeRef, ContentModel.PROP_NAME, archiveName); + Download downloadInfo = getStatus(zipNodeRef); + return downloadInfo; + } + + @Override + public Download getDownloadStatus(String downloadNodeId) + { + NodeRef downloadNodeRef = nodes.validateNode(downloadNodeId); + + checkIsDownloadNodeType(downloadNodeRef); + + Download downloadInfo = getStatus(downloadNodeRef); + return downloadInfo; + } + + @Override + public void cancel(String downloadNodeId) + { + NodeRef downloadNodeRef = nodes.validateNode(downloadNodeId); + checkIsDownloadNodeType(downloadNodeRef); + + downloadService.cancelDownload(downloadNodeRef); + } + + protected NodeRef[] validateAndGetNodeRefs(Download download) + { + return download.getNodeIds().stream() + .map(nodeRef -> nodes.validateNode(nodeRef)) + .toArray(NodeRef[]::new); + } + + protected void checkNodeIdsReadPermission(NodeRef[] zipContentNodeRefs) + { + for (NodeRef nodeRef : zipContentNodeRefs) + { + if (permissionService.hasReadPermission(nodeRef).equals(AccessStatus.DENIED)){ + throw new PermissionDeniedException(); + } + } + } + + protected void checkDuplicateNodeId(Download download) + { + if(download.getNodeIds().size() != new HashSet(download.getNodeIds()).size()){ + throw new InvalidArgumentException("Cannot specify the same nodeId twice"); + } + } + + protected void checkEmptyNodeIds(Download download) + { + if (download.getNodeIds().size() == 0) + { + throw new InvalidArgumentException("Cannot create an archive with 0 entries."); + } + } + + protected void checkIsDownloadNodeType(NodeRef downloadNodeRef) + { + QName nodeIdType = this.nodeService.getType(downloadNodeRef); + + if(!nodeIdType.equals(DownloadModel.TYPE_DOWNLOAD)){ + throw new InvalidArgumentException("Please specify the nodeId of a download node."); + } + } + + private Download getStatus(NodeRef downloadNodeRef) + { + DownloadStatus status = downloadService.getDownloadStatus(downloadNodeRef); + Download downloadInfo = new Download(); + downloadInfo.setId(downloadNodeRef.getId()); + downloadInfo.setBytesAdded(status.getDone()); + downloadInfo.setFilesAdded(status.getFilesAdded()); + downloadInfo.setStatus(status.getStatus()); + downloadInfo.setTotalFiles(status.getTotalFiles()); + downloadInfo.setTotalBytes(status.getTotal()); + return downloadInfo; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/ExceptionHandler.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/ExceptionHandler.java new file mode 100644 index 00000000000..7ae201bf1d4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/ExceptionHandler.java @@ -0,0 +1,31 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +public interface ExceptionHandler +{ + boolean handle(Throwable t); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/ExceptionInterceptor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/ExceptionInterceptor.java new file mode 100644 index 00000000000..5b2046ce246 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/ExceptionInterceptor.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.List; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +/** + * An interceptor that catches exceptions and handlers them in some way, possibly by re-throwing as a different exception. + * + * @author steveglover + * + */ +public class ExceptionInterceptor implements MethodInterceptor +{ + private List exceptionHandlers; + + public void setExceptionHandlers(List exceptionHandlers) + { + this.exceptionHandlers = exceptionHandlers; + } + + public ExceptionInterceptor() + { + super(); + } + + public Object invoke(MethodInvocation mi) throws Throwable + { + try + { + return mi.proceed(); + } + catch(Throwable t) + { + for(ExceptionHandler handler : exceptionHandlers) + { + if(handler.handle(t)) + { + break; + } + } + throw t; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/ExcludedTypes.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/ExcludedTypes.java new file mode 100644 index 00000000000..27712a75d83 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/ExcludedTypes.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.alfresco.opencmis.dictionary.QNameFilter; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Stores a set of excluded types, by full type name. The localName can be a wildcard (*) to indicate that the + * whole namespace should be excluded. + * + * @author steveglover + * + */ +public class ExcludedTypes +{ + private List expectedTypes; + private QNameFilter excludedTypes; + private NodeService nodeService; + + public void setExpectedTypes(List expectedTypes) + { + if(expectedTypes != null && expectedTypes.size() > 0) + { + this.expectedTypes = new ArrayList(expectedTypes.size()); + + for(String type : expectedTypes) + { + final QName typeDef = QName.createQName(type); + this.expectedTypes.add(typeDef); + } + } + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setExcludedTypes(QNameFilter excludedTypes) + { + this.excludedTypes = excludedTypes; + } + + public boolean isExcluded(final NodeRef nodeRef) + { + boolean excluded = false; + + QName nodeType = nodeService.getType(nodeRef); + if(expectedTypes != null && !expectedTypes.contains(nodeType)) + { + excluded = true; + } + + if(!excluded) + { + // need to run as system - caller may not be able to read the node's aspects + // but we need to know what they are in order to determine exclusion. + excluded = TenantUtil.runAsSystemTenant(new TenantRunAsWork() + { + @Override + public Boolean doWork() throws Exception + { + boolean excluded = false; + + // the node is a content node. Make sure it doesn't have an aspect in the excluded list. + Set aspects = new HashSet(nodeService.getAspects(nodeRef)); + for(QName aspect : aspects) + { + if(excludedTypes.isExcluded(aspect)) + { + excluded = true; + break; + } + } + + return excluded; + } + }, TenantUtil.getCurrentDomain()); + } + + return excluded; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/FavouritesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/FavouritesImpl.java new file mode 100644 index 00000000000..00924291183 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/FavouritesImpl.java @@ -0,0 +1,451 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.favourites.PersonFavourite; +import org.alfresco.repo.site.SiteDoesNotExistException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.rest.api.Favourites; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.Document; +import org.alfresco.rest.api.model.DocumentTarget; +import org.alfresco.rest.api.model.Favourite; +import org.alfresco.rest.api.model.Folder; +import org.alfresco.rest.api.model.FolderTarget; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.PathInfo; +import org.alfresco.rest.api.model.Site; +import org.alfresco.rest.api.model.SiteTarget; +import org.alfresco.rest.api.model.Target; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper.WalkerCallbackAdapter; +import org.alfresco.service.cmr.favourites.FavouritesService; +import org.alfresco.service.cmr.favourites.FavouritesService.SortFields; +import org.alfresco.service.cmr.favourites.FavouritesService.Type; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Public REST API: Centralises access to favourites functionality and maps between representations repository and api representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class FavouritesImpl implements Favourites +{ + private static final Log logger = LogFactory.getLog(FavouritesImpl.class); + + private People people; + private Sites sites; + private Nodes nodes; + private FavouritesService favouritesService; + private SiteService siteService; + private NamespaceService namespaceService; + + // additional exclude properties for favourites as these can be already top-level properties + private static final List EXCLUDED_PROPS = Arrays.asList( + ContentModel.PROP_TITLE, + ContentModel.PROP_DESCRIPTION, + SiteModel.PROP_SITE_VISIBILITY, + SiteModel.PROP_SITE_PRESET + ); + + public void setPeople(People people) + { + this.people = people; + } + + public void setSites(Sites sites) + { + this.sites = sites; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setFavouritesService(FavouritesService favouritesService) + { + this.favouritesService = favouritesService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + private Target getTarget(PersonFavourite personFavourite, Parameters parameters) + { + Target target = null; + NodeRef nodeRef = personFavourite.getNodeRef(); + Type type = personFavourite.getType(); + if(type.equals(Type.FILE)) + { + Document document = nodes.getDocument(nodeRef); + setPathInfo(document, parameters.getInclude()); + target = new DocumentTarget(document); + } + else if(type.equals(Type.FOLDER)) + { + Folder folder = nodes.getFolder(nodeRef); + setPathInfo(folder, parameters.getInclude()); + target = new FolderTarget(folder); + } + else if(type.equals(Type.SITE)) + { + SiteInfo siteInfo = siteService.getSite(nodeRef); + String role = sites.getSiteRole(siteInfo.getShortName()); + Site site = new Site(siteInfo, role); + target = new SiteTarget(site); + } + else + { + throw new AlfrescoRuntimeException("Unexpected favourite target type: " + type); + } + + return target; + } + + private Favourite getFavourite(PersonFavourite personFavourite, Parameters parameters) + { + Favourite fav = new Favourite(); + fav.setTargetGuid(personFavourite.getNodeRef().getId()); + fav.setCreatedAt(personFavourite.getCreatedAt()); + Target target = getTarget(personFavourite, parameters); + fav.setTarget(target); + + // REPO-1147 allow retrieving additional properties + if (parameters.getInclude().contains(PARAM_INCLUDE_PROPERTIES)) + { + List includeProperties = new LinkedList<>(); + includeProperties.add(PARAM_INCLUDE_PROPERTIES); + // get node representation with only properties included + Node node = nodes.getFolderOrDocument(personFavourite.getNodeRef(), null, null, includeProperties, null); + // Create a map from node properties excluding properties already in this Favorite + Map filteredNodeProperties = filterProps(node.getProperties(), EXCLUDED_PROPS); + if(filteredNodeProperties.size() > 0) + { + fav.setProperties(filteredNodeProperties); + } + } + + return fav; + } + + private Map filterProps(Map properties, List toRemove) + { + Map filteredProps = properties == null ? new HashMap<>() : new HashMap<>(properties); + List propsToRemove = toRemove.stream().map(e -> e.toPrefixString(namespaceService)).collect(Collectors.toList()); + filteredProps.keySet().removeAll(propsToRemove); + return filteredProps; + } + + private CollectionWithPagingInfo wrap(Paging paging, PagingResults personFavourites, Parameters parameters) + { + final List page = personFavourites.getPage(); + final List list = new AbstractList() + { + @Override + public Favourite get(int index) + { + PersonFavourite personFavourite = page.get(index); + Favourite fav = getFavourite(personFavourite, parameters); + return fav; + } + + @Override + public int size() + { + return page.size(); + } + }; + Pair pair = personFavourites.getTotalResultCount(); + Integer total = null; + if(pair.getFirst().equals(pair.getSecond())) + { + total = pair.getFirst(); + } + return CollectionWithPagingInfo.asPaged(paging, list, personFavourites.hasMoreItems(), total); + } + + @Override + public Favourite addFavourite(String personId, Favourite favourite) + { + Parameters parameters = getDefaultParameters(personId, null); + return addFavourite(personId, favourite, parameters); + } + + @Override + public Favourite addFavourite(String personId, Favourite favourite, Parameters parameters) + { + Favourite ret = null; + + personId = people.validatePerson(personId, true); + Target target = favourite.getTarget(); + if(target == null) + { + throw new InvalidArgumentException("target is missing"); + } + else if(target instanceof SiteTarget) + { + SiteTarget siteTarget = (SiteTarget)target; + String guid = siteTarget.getSite().getGuid(); + SiteInfo siteInfo = sites.validateSite(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, guid)); + NodeRef siteNodeRef = siteInfo.getNodeRef(); + String siteId = siteInfo.getShortName(); + + try + { + PersonFavourite personFavourite = favouritesService.addFavourite(personId, siteNodeRef); + ret = getFavourite(personFavourite, parameters); + } + catch(SiteDoesNotExistException e) + { + throw new RelationshipResourceNotFoundException(personId, siteId); + } + } + else if(target instanceof DocumentTarget) + { + DocumentTarget documentTarget = (DocumentTarget)target; + NodeRef nodeRef = documentTarget.getFile().getGuid(); + if(!nodes.nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null)) + { + throw new RelationshipResourceNotFoundException(personId, nodeRef.getId()); + } + + PersonFavourite personFavourite = favouritesService.addFavourite(personId, nodeRef); + ret = getFavourite(personFavourite, parameters); + } + else if(target instanceof FolderTarget) + { + FolderTarget folderTarget = (FolderTarget)target; + NodeRef nodeRef = folderTarget.getFolder().getGuid(); + if(!nodes.nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), Collections.singleton(SiteModel.TYPE_SITE))) + { + throw new RelationshipResourceNotFoundException(personId, nodeRef.getId()); + } + + PersonFavourite personFavourite = favouritesService.addFavourite(personId, nodeRef); + ret = getFavourite(personFavourite, parameters); + } + + return ret; + } + + @Override + public void removeFavourite(String personId, String id) + { + personId = people.validatePerson(personId, true); + NodeRef nodeRef = nodes.validateNode(id); + boolean exists = false; + + Type type = favouritesService.getType(nodeRef); + if(type.equals(Type.SITE)) + { + SiteInfo siteInfo = siteService.getSite(nodeRef); + if(siteInfo == null) + { + // shouldn't happen because the type implies it's a site + throw new AlfrescoRuntimeException("Unable to find site with nodeRef " + nodeRef); + } + exists = favouritesService.removeFavourite(personId, siteInfo.getNodeRef()); + } + else if(type.equals(Type.FILE)) + { + exists = favouritesService.removeFavourite(personId, nodeRef); + } + else if(type.equals(Type.FOLDER)) + { + exists = favouritesService.removeFavourite(personId, nodeRef); + } + if(!exists) + { + throw new RelationshipResourceNotFoundException(personId, id); + } + } + + @Override + public Favourite getFavourite(String personId, String favouriteId) + { + Parameters parameters = getDefaultParameters(personId, favouriteId); + return getFavourite(personId, favouriteId, parameters); + } + + @Override + public Favourite getFavourite(String personId, String favouriteId, Parameters parameters) + { + NodeRef nodeRef = nodes.validateNode(favouriteId); + personId = people.validatePerson(personId, true); + + PersonFavourite personFavourite = favouritesService.getFavourite(personId, nodeRef); + if(personFavourite != null) + { + Favourite favourite = getFavourite(personFavourite, parameters); + return favourite; + } + else + { + throw new RelationshipResourceNotFoundException(personId, favouriteId); + } + } + + @Override + public CollectionWithPagingInfo getFavourites(String personId, final Parameters parameters) + { + personId = people.validatePerson(personId, true); + + Paging paging = parameters.getPaging(); + + List> sortProps = getSortProps(parameters); + + final Set filteredByClientQuery = new HashSet(); + Set filterTypes = FavouritesService.Type.ALL_FILTER_TYPES; //Default all + + // filterType is of the form 'target.' + QueryHelper.walk(parameters.getQuery(), new WalkerCallbackAdapter() + { + @Override + public void or() { + //OR is supported but exists() will be called for each EXISTS so we don't + //need to do anything here. If we don't override it then it will be assumed + //that OR in the grammar is not supported. + } + + @Override + public void exists(String filteredByClient, boolean negated) { + if(filteredByClient != null) + { + int idx = filteredByClient.lastIndexOf("/"); + if(idx == -1 || idx == filteredByClient.length()) + { + throw new InvalidArgumentException(); + } + else + { + String filtertype = filteredByClient.substring(idx + 1).toUpperCase(); + filteredByClientQuery.add(Type.valueOf(filtertype)); + } + } + + } + }); + + if (filteredByClientQuery.size() > 0) + { + filterTypes = filteredByClientQuery; + } + + final PagingResults favourites = favouritesService.getPagedFavourites(personId, filterTypes, sortProps, Util.getPagingRequest(paging)); + + return wrap(paging, favourites, parameters); + } + + private void setPathInfo(Node node, List includeParam) + { + if (includeParam.contains(PARAM_INCLUDE_PATH)) + { + PathInfo pathInfo = nodes.lookupPathInfo(node.getNodeRef(), null); + node.setPath(pathInfo); + } + } + + /** + * Returns a {@code {@link Parameters} object where almost all of its values are null. + * the non-null value is the {@literal include} and whatever value is passed for {@code personId} and {@code favouriteId} + */ + private Parameters getDefaultParameters(String personId, String favouriteId) + { + Params.RecognizedParams recognizedParams = new Params.RecognizedParams(null, null, null, null, Collections.emptyList(), null, null, null, + false); + Parameters parameters = Params.valueOf(recognizedParams, personId, favouriteId, null); + return parameters; + } + + private List> getSortProps(Parameters parameters) + { + List> sortProps = new ArrayList<>(); + List sortCols = parameters.getSorting(); + if ((sortCols != null) && (sortCols.size() > 0)) + { + for (SortColumn sortCol : sortCols) + { + SortFields sortField; + try + { + sortField = SortFields.valueOf(sortCol.column); + } + catch (Exception e) + { + throw new InvalidArgumentException("Invalid sort field: " + sortCol.column); + } + sortProps.add(new Pair<>(sortField, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE))); + } + } + else + { + // default sort order + sortProps = FavouritesService.DEFAULT_SORT_PROPS; + } + return sortProps; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/GroupsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/GroupsImpl.java new file mode 100644 index 00000000000..de7dcd7f155 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/GroupsImpl.java @@ -0,0 +1,1070 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import static org.alfresco.repo.security.authentication.AuthenticationUtil.runAsSystem; + +import java.text.Collator; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +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.stream.Collectors; + +import org.alfresco.query.CannedQueryPageDetails; +import org.alfresco.query.EmptyPagingResults; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authority.AuthorityDAO; +import org.alfresco.repo.security.authority.AuthorityException; +import org.alfresco.repo.security.authority.AuthorityInfo; +import org.alfresco.repo.security.authority.UnknownAuthorityException; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Groups; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.model.Group; +import org.alfresco.rest.api.model.GroupMember; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalkerOrSupported; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.util.AlfrescoCollator; +import org.alfresco.util.Pair; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Centralises access to groups services and maps between representations. + * + * @author cturlica + */ +public class GroupsImpl implements Groups +{ + private static final int MAX_ZONES = 1; + private static final String DISPLAY_NAME = "displayName"; + private static final String AUTHORITY_NAME = "authorityName"; + private static final String ERR_MSG_MODIFY_FIXED_AUTHORITY = "Trying to modify a fixed authority"; + + private final static Map SORT_PARAMS_TO_NAMES; + static + { + Map aMap = new HashMap<>(2); + aMap.put(PARAM_ID, AUTHORITY_NAME); + aMap.put(PARAM_DISPLAY_NAME, DISPLAY_NAME); + + SORT_PARAMS_TO_NAMES = Collections.unmodifiableMap(aMap); + } + + // List groups filtering (via where clause) + private final static Set LIST_GROUPS_EQUALS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(new String[] { PARAM_IS_ROOT })); + + private final static Set LIST_GROUP_MEMBERS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(new String[] { PARAM_MEMBER_TYPE })); + + protected AuthorityService authorityService; + private AuthorityDAO authorityDAO; + + protected People people; + + public AuthorityService getAuthorityService() + { + return authorityService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setAuthorityDAO(AuthorityDAO authorityDAO) + { + this.authorityDAO = authorityDAO; + } + + public void setPeople(People people) + { + this.people = people; + } + + public Group create(Group group, Parameters parameters) + { + validateGroup(group, false); + + // Create authority with default zones. + final Set authorityZones = authorityService.getDefaultZones(); + String authorityDisplayName = null; + if(group.getDisplayName() != null && !group.getDisplayName().isEmpty()) + { + authorityDisplayName = group.getDisplayName(); + } + + String authority = authorityService.createAuthority(AuthorityType.GROUP, group.getId(), authorityDisplayName, authorityZones); + + // Set a given child authority to be included by the given parent + // authorities. + if (group.getParentIds() != null && !group.getParentIds().isEmpty()) + { + authorityService.addAuthority(group.getParentIds(), authority); + } + + return getGroup(authority, parameters); + } + + public Group update(String groupId, Group group, Parameters parameters) + { + validateGroupId(groupId, false); + validateGroup(group, true); + + try + { + authorityService.setAuthorityDisplayName(groupId, group.getDisplayName()); + } + catch (AuthorityException ae) + { + handleAuthorityException(ae); + } + + return getGroup(groupId, parameters); + } + + public Group getGroup(String groupId, Parameters parameters) throws EntityNotFoundException + { + AuthorityInfo authorityInfo = getAuthorityInfo(groupId); + + final Set rootAuthorities = getAllRootAuthorities(AuthorityType.GROUP); + final List includeParam = parameters.getInclude(); + + return getGroup(authorityInfo, includeParam, rootAuthorities); + } + + public CollectionWithPagingInfo getGroups(final Parameters parameters) + { + final List includeParam = parameters.getInclude(); + + Paging paging = parameters.getPaging(); + + // Retrieve sort column. This is limited for now to sort column due to + // v0 api implementation. Should be improved in the future. + Pair sortProp = getGroupsSortProp(parameters); + + // Parse where clause properties. + Query q = parameters.getQuery(); + Boolean isRootParam = null; + String zoneFilter = null; + if (q != null) + { + GroupsQueryWalker propertyWalker = new GroupsQueryWalker(); + QueryHelper.walk(q, propertyWalker); + + isRootParam = propertyWalker.getIsRoot(); + List zonesParam = propertyWalker.getZones(); + if (zonesParam != null) + { + validateZonesParam(zonesParam); + zoneFilter = zonesParam.get(0); + } + } + + final AuthorityType authorityType = AuthorityType.GROUP; + final Set rootAuthorities = getAllRootAuthorities(authorityType); + + PagingResults pagingResult; + try + { + pagingResult = getAuthoritiesInfo(authorityType, isRootParam, zoneFilter, rootAuthorities, sortProp, paging); + } + catch (UnknownAuthorityException e) + { + // Non-existent zone + pagingResult = new EmptyPagingResults<>(); + } + + // Create response. + final List page = pagingResult.getPage(); + int totalItems = pagingResult.getTotalResultCount().getFirst(); + List groups = new AbstractList() + { + @Override + public Group get(int index) + { + AuthorityInfo authorityInfo = page.get(index); + return getGroup(authorityInfo, includeParam, rootAuthorities); + } + + @Override + public int size() + { + return page.size(); + } + }; + + return CollectionWithPagingInfo.asPaged(paging, groups, pagingResult.hasMoreItems(), totalItems); + } + + private void validateZonesParam(List zonesParam) + { + if (zonesParam.size() > MAX_ZONES) + { + throw new IllegalArgumentException("A maximum of " + MAX_ZONES + " zones may be specified."); + } + else if (zonesParam.isEmpty()) + { + throw new IllegalArgumentException("Zones filter list cannot be empty."); + } + // Validate each zone name + zonesParam.forEach(zone -> { + if (zone.isEmpty()) + { + throw new IllegalArgumentException("Zone name cannot be empty (i.e. '')"); + } + }); + } + + @Override + public CollectionWithPagingInfo getGroupsByPersonId(String requestedPersonId, Parameters parameters) + { + // Canonicalize the person ID, performing -me- alias substitution. + final String personId = people.validatePerson(requestedPersonId); + + // Non-admins can only access their own data + // TODO: this is also in PeopleImpl.update(personId,personInfo) - refactor? + boolean isAdmin = authorityService.hasAdminAuthority(); + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) + { + // The user is not an admin user and is not attempting to retrieve *their own* details. + throw new PermissionDeniedException(); + } + + Query q = parameters.getQuery(); + Boolean isRootParam = null; + String zoneFilter = null; + if (q != null) + { + GroupsQueryWalker propertyWalker = new GroupsQueryWalker(); + QueryHelper.walk(q, propertyWalker); + + isRootParam = propertyWalker.getIsRoot(); + List zonesParam = propertyWalker.getZones(); + if (zonesParam != null) + { + validateZonesParam(zonesParam); + zoneFilter = zonesParam.get(0); + } + } + + final List includeParam = parameters.getInclude(); + Paging paging = parameters.getPaging(); + + // Retrieve sort column. This is limited for now to sort column due to + // v0 api implementation. Should be improved in the future. + Pair sortProp = getGroupsSortProp(parameters); + + // Get all the authorities for a user, including but not limited to, groups. + Set userAuthorities = runAsSystem( + () -> authorityService.getAuthoritiesForUser(personId)); + + final Set rootAuthorities = getAllRootAuthorities(AuthorityType.GROUP); + + // Filter, transform and sort the list of user authorities into + // a suitable list of AuthorityInfo objects. + final String finalZoneFilter = zoneFilter; + final Boolean finalIsRootParam = isRootParam; + List groupAuthorities = userAuthorities.stream(). + filter(a -> a.startsWith(AuthorityType.GROUP.getPrefixString())). + filter(a -> isRootPredicate(finalIsRootParam, rootAuthorities, a)). + filter(a -> zonePredicate(a, finalZoneFilter)). + map(this::getAuthorityInfo). + sorted(new AuthorityInfoComparator(sortProp.getFirst(), sortProp.getSecond())). + collect(Collectors.toList()); + + PagingResults pagingResult = Util.wrapPagingResults(paging, groupAuthorities); + + // Create response. + final List page = pagingResult.getPage(); + int totalItems = pagingResult.getTotalResultCount().getFirst(); + + // Transform the page of results into Group objects + List groups = page.stream(). + map(authority -> getGroup(authority, includeParam, rootAuthorities)). + collect(Collectors.toList()); + + return CollectionWithPagingInfo.asPaged(paging, groups, pagingResult.hasMoreItems(), totalItems); + } + + private PagingResults getAuthoritiesInfo(AuthorityType authorityType, Boolean isRootParam, String zoneFilter, Set rootAuthorities, Pair sortProp, + Paging paging) + { + PagingResults pagingResult; + + if (isRootParam != null) + { + List groupList; + + if (isRootParam) + { + // Limit the post processing work by using the already loaded + // list of root authorities. + List authorities = rootAuthorities.stream(). + map(this::getAuthorityInfo). + filter(auth -> zonePredicate(auth.getAuthorityName(), zoneFilter)). + collect(Collectors.toList()); + groupList = new ArrayList<>(rootAuthorities.size()); + groupList.addAll(authorities); + + // Post process sorting - this should be moved to service + // layer. It is done here because sorting is not supported at + // service layer. + AuthorityInfoComparator authorityComparator = new AuthorityInfoComparator(sortProp.getFirst(), sortProp.getSecond()); + Collections.sort(groupList, authorityComparator); + } + else + { + PagingRequest pagingNoMaxItems = new PagingRequest(CannedQueryPageDetails.DEFAULT_PAGE_SIZE); + + // Get authorities using canned query but without using + // the requested paginating now because we need to filter out + // the root authorities. + PagingResults nonPagingResult = authorityService.getAuthoritiesInfo(authorityType, zoneFilter, null, sortProp.getFirst(), sortProp.getSecond(), + pagingNoMaxItems); + + // Post process filtering - this should be moved to service + // layer. It is done here because filtering by "isRoot" is not + // supported at service layer. + groupList = nonPagingResult.getPage(); + if (groupList != null) + { + for (Iterator i = groupList.iterator(); i.hasNext();) + { + AuthorityInfo authorityInfo = i.next(); + if (!isRootParam.equals(isRootAuthority(rootAuthorities, authorityInfo.getAuthorityName()))) + { + i.remove(); + } + } + } + } + + // Post process paging - this should be moved to service layer. + pagingResult = Util.wrapPagingResults(paging, groupList); + } + else + { + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + // Get authorities using canned query. + pagingResult = authorityService.getAuthoritiesInfo(authorityType, zoneFilter, null, sortProp.getFirst(), sortProp.getSecond(), pagingRequest); + } + return pagingResult; + } + + /** + * Checks to see if the named group authority should be included in results + * when filtered by zone. + * + * @see #zonePredicate(Set, String) + * @param groupName + * @param zone + * @return true if result should be included. + */ + private boolean zonePredicate(String groupName, String zone) + { + Set zones = null; + if (zone != null) + { + // Don't bother doing this lookup unless the zone filter is non-null. + zones = authorityService.getAuthorityZones(groupName); + } + return zonePredicate(zones, zone); + } + + /** + * Test to see if a result should be included, using the isRoot parameter. + *

    + * If isRootParam is null, then no results will be filtered. Otherwise + * results will be filtered to return only those that are root or non-root, + * depending on the value of isRootParam. + * + * @param isRootParam + * @param rootAuthorities + * @param authority + * @return + */ + private boolean isRootPredicate(Boolean isRootParam, Set rootAuthorities, String authority) + { + if (isRootParam != null) + { + return isRootParam == isRootAuthority(rootAuthorities, authority); + } + return true; + } + + /** + * Checks a list of zones to see if it matches the supplied zone filter + * ({@code requiredZone} parameter). + *

    + * If the requiredZone parameter is null, then the filter will not be applied (returns true.) + *

    + * If the requiredZone parameter is not null (i.e. a filter must be applied) and the + * {@code zones}) list is {@code null} then the predicate will return false. + * + * @param zones + * @param requiredZone + * @return true if result should be included. + */ + private boolean zonePredicate(Set zones, String requiredZone) + { + if (requiredZone != null) + { + return zones != null && zones.contains(requiredZone); + } + return true; + } + + private Set getAllRootAuthorities(AuthorityType authorityType) + { + Set authorities; + try + { + authorities = authorityService.getAllRootAuthorities(authorityType); + } + catch (UnknownAuthorityException e) + { + authorities = Collections.emptySet(); + } + + return authorities; + } + + /** + * Retrieve authority info by name. Node id field isn't used at this time + * and it is set to null. + * + * @param id + * The authority name. + * @return The authority info. + */ + private AuthorityInfo getAuthorityInfo(String id) + { + return getAuthorityInfo(id, false); + } + + /** + * Retrieve authority info by name. Node id field isn't used at this time + * and it is set to null. + * + * @param id + * The authority name. + * @param defaultDisplayNameIfNull + * True if we would like to get a default value (e.g. shortName of the authority) if the authority display name is null. + * @return The authority info. + */ + private AuthorityInfo getAuthorityInfo(String id, boolean defaultDisplayNameIfNull) + { + if (id == null || id.isEmpty()) + { + throw new InvalidArgumentException("id is null or empty"); + } + + // authorityService.authorityExists(ALL_AUTHORITIES) returns false! + if (!id.equals(PermissionService.ALL_AUTHORITIES) && !authorityService.authorityExists(id)) + { + throw new EntityNotFoundException(id); + } + + String authorityDisplayName = getAuthorityDisplayName(id, defaultDisplayNameIfNull); + + return new AuthorityInfo(null, authorityDisplayName, id); + } + + private String getAuthorityDisplayName(String id, boolean defaultDisplayNameIfNull) + { + return defaultDisplayNameIfNull ? authorityService.getAuthorityDisplayName(id) : authorityDAO.getAuthorityDisplayName(id); + } + + private Group getGroup(AuthorityInfo authorityInfo, List includeParam, Set rootAuthorities) + { + if (authorityInfo == null) + { + return null; + } + + Group group = new Group(); + group.setId(authorityInfo.getAuthorityName()); + + // REPO-1743 + String authorityDisplayName = authorityInfo.getAuthorityDisplayName(); + if (authorityDisplayName == null || authorityDisplayName.isEmpty()) + { + authorityDisplayName = authorityService.getAuthorityDisplayName(authorityInfo.getAuthorityName()); + } + + group.setDisplayName(authorityDisplayName); + + group.setIsRoot(isRootAuthority(rootAuthorities, authorityInfo.getAuthorityName())); + + // Optionally include + if (includeParam != null) + { + if (includeParam.contains(PARAM_INCLUDE_PARENT_IDS)) + { + String authority = authorityInfo.getAuthorityName(); + Set containingAuthorities = Collections.emptySet(); + // Workaround for AuthorityDAO.listAuthorities, where although AuthorityType.GUEST + // is a special case, AuthorityType.EVERYONE is not, and an exception is thrown. + if (!authority.equalsIgnoreCase(PermissionService.ALL_AUTHORITIES)) + { + containingAuthorities = authorityService.getContainingAuthorities(AuthorityType.GROUP, authority, true); + } + group.setParentIds(containingAuthorities); + } + + if (includeParam.contains(PARAM_INCLUDE_ZONES)) + { + Set authorityZones = authorityService.getAuthorityZones(authorityInfo.getAuthorityName()); + group.setZones(authorityZones); + } + } + + return group; + } + + private boolean isRootAuthority(Set rootAuthorities, String authorityName) + { + return rootAuthorities.contains(authorityName); + } + + private Pair getGroupsSortProp(Parameters parameters) + { + Pair sortProp; + List sortCols = parameters.getSorting(); + + if ((sortCols != null) && (sortCols.size() > 0)) + { + if (sortCols.size() > 1) + { + throw new InvalidArgumentException("Multiple sort fields not allowed."); + } + + SortColumn sortCol = sortCols.get(0); + + String sortPropName = SORT_PARAMS_TO_NAMES.get(sortCol.column); + if (sortPropName == null) + { + throw new InvalidArgumentException("Invalid sort field: " + sortCol.column); + } + + sortProp = new Pair<>(sortPropName, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE)); + } + else + { + sortProp = getGroupsSortPropDefault(); + } + return sortProp; + } + + /** + *

    + * Returns the default sort order. + *

    + * + * @return The default Pair<QName, Boolean> sort + * property. + */ + private Pair getGroupsSortPropDefault() + { + return new Pair<>(DISPLAY_NAME, Boolean.TRUE); + } + + private static class AuthorityInfoComparator implements Comparator + { + private Map nameCache; + private String sortBy; + private Collator col; + private int orderMultiplier = 1; + + private AuthorityInfoComparator(String sortBy, boolean sortAsc) + { + col = AlfrescoCollator.getInstance(I18NUtil.getLocale()); + this.sortBy = sortBy; + this.nameCache = new HashMap<>(); + + if (!sortAsc) + { + orderMultiplier = -1; + } + } + + @Override + public int compare(AuthorityInfo g1, AuthorityInfo g2) + { + return col.compare(get(g1), get(g2)) * orderMultiplier; + } + + private String get(AuthorityInfo g) + { + String v = nameCache.get(g); + if (v == null) + { + + // Please see GetAuthoritiesCannedQuery.AuthComparator for details. + + // Get the value from the group + if (DISPLAY_NAME.equals(sortBy)) + { + v = g.getAuthorityDisplayName(); + } + else if (AUTHORITY_NAME.equals(sortBy)) + { + v = g.getAuthorityName(); + } + else + { + throw new InvalidArgumentException("Invalid sort field: " + sortBy); + } + + if (v == null) + { + v = g.getAuthorityName(); + } + + // // Lower case it for case insensitive search + // v = v.toLowerCase(); + + // Cache it + nameCache.put(g, v); + } + return v; + } + } + + public void delete(String groupId, Parameters parameters) + { + if (!isGroupAuthority(groupId)) + { + throw new InvalidArgumentException("Invalid group id: " + groupId); + } + + // Get cascade param - default false (if not provided). + boolean cascade = Boolean.valueOf(parameters.getParameter(PARAM_CASCADE)); + + try + { + authorityService.deleteAuthority(groupId, cascade); + } + catch (AuthorityException ae) + { + handleAuthorityException(ae); + } + } + + private void handleAuthorityException(AuthorityException ae) + { + if (ae.getMsgId().equals("Trying to modify a fixed authority")) + { + throw new ConstraintViolatedException(ERR_MSG_MODIFY_FIXED_AUTHORITY); + } + else + { + throw ae; + } + } + + public CollectionWithPagingInfo getGroupMembers(String groupId, final Parameters parameters) + { + validateGroupId(groupId, false); + + // Not allowed to list all members. + if (PermissionService.ALL_AUTHORITIES.equals(groupId)) + { + throw new UnsupportedResourceOperationException(); + } + + Paging paging = parameters.getPaging(); + + // Retrieve sort column. This is limited for now to sort column due to + // v0 api implementation. Should be improved in the future. + Pair sortProp = getGroupsSortProp(parameters); + + AuthorityType authorityType = null; + + // Parse where clause properties. + Query q = parameters.getQuery(); + if (q != null) + { + MapBasedQueryWalkerOrSupported propertyWalker = new MapBasedQueryWalkerOrSupported(LIST_GROUP_MEMBERS_QUERY_PROPERTIES, null); + QueryHelper.walk(q, propertyWalker); + + String memberTypeStr = propertyWalker.getProperty(PARAM_MEMBER_TYPE, WhereClauseParser.EQUALS, String.class); + authorityType = getAuthorityType(memberTypeStr); + } + + PagingResults pagingResult = getAuthoritiesInfo(authorityType, groupId, sortProp, paging); + + // Create response. + final List page = pagingResult.getPage(); + int totalItems = pagingResult.getTotalResultCount().getFirst(); + List groupMembers = new AbstractList() + { + @Override + public GroupMember get(int index) + { + AuthorityInfo authorityInfo = page.get(index); + return getGroupMember(authorityInfo); + } + + @Override + public int size() + { + return page.size(); + } + }; + + return CollectionWithPagingInfo.asPaged(paging, groupMembers, pagingResult.hasMoreItems(), totalItems); + } + + @Override + public GroupMember createGroupMember(String groupId, GroupMember groupMember) + { + validateGroupId(groupId, false); + + // Not allowed to modify a GROUP_EVERYONE member. + if (PermissionService.ALL_AUTHORITIES.equals(groupId)) + { + throw new ConstraintViolatedException(ERR_MSG_MODIFY_FIXED_AUTHORITY); + } + + validateGroupMember(groupMember); + + AuthorityType authorityType = getAuthorityType(groupMember.getMemberType()); + + if (!authorityService.authorityExists(groupMember.getId())) + { + throw new EntityNotFoundException(groupMember.getId()); + } + + AuthorityType existingAuthorityType = AuthorityType.getAuthorityType(groupMember.getId()); + if (existingAuthorityType != authorityType) + { + throw new IllegalArgumentException("Incorrect group member type, " + + (AuthorityType.USER.equals(existingAuthorityType) ? Groups.PARAM_MEMBER_TYPE_PERSON : existingAuthorityType) + " exists with the given id"); + } + + authorityService.addAuthority(groupId, groupMember.getId()); + String authority = authorityService.getName(authorityType, groupMember.getId()); + + return getGroupMember(authority); + } + + public void deleteGroupMembership(String groupId, String groupMemberId) + { + validateGroupId(groupId, false); + + // Not allowed to modify a GROUP_EVERYONE member. + if (PermissionService.ALL_AUTHORITIES.equals(groupId)) + { + throw new ConstraintViolatedException(ERR_MSG_MODIFY_FIXED_AUTHORITY); + } + + validateGroupMemberId(groupMemberId); + + // Verify if groupMemberId is member of groupId + AuthorityType authorityType = AuthorityType.getAuthorityType(groupMemberId); + Set parents = authorityService.getContainingAuthorities(AuthorityType.GROUP, groupMemberId, true); + if (!parents.contains(groupId)) + { + throw new NotFoundException(groupMemberId + " is not member of " + groupId); + } + + authorityService.removeAuthority(groupId, groupMemberId); + } + + private AuthorityType getAuthorityType(String memberType) + { + AuthorityType authorityType = null; + if (memberType != null && !memberType.isEmpty()) + { + switch (memberType) + { + case PARAM_MEMBER_TYPE_GROUP: + authorityType = AuthorityType.GROUP; + break; + case PARAM_MEMBER_TYPE_PERSON: + authorityType = AuthorityType.USER; + break; + default: + throw new InvalidArgumentException("MemberType is invalid (expected eg. GROUP, PERSON)"); + } + } + return authorityType; + } + + private PagingResults getAuthoritiesInfo(AuthorityType authorityType, String groupId, Pair sortProp, Paging paging) + { + Set authorities; + try + { + authorities = authorityService.findAuthorities(authorityType, groupId, true, null, null); + } + catch (UnknownAuthorityException e) + { + authorities = Collections.emptySet(); + } + + List authorityInfoList = new ArrayList<>(authorities.size()); + authorityInfoList.addAll(authorities.stream().map(this::getAuthorityInfo).collect(Collectors.toList())); + + // Post process sorting - this should be moved to service + // layer. It is done here because sorting is not supported at + // service layer. + AuthorityInfoComparator authorityComparator = new AuthorityInfoComparator(sortProp.getFirst(), sortProp.getSecond()); + Collections.sort(authorityInfoList, authorityComparator); + + // Post process paging - this should be moved to service layer. + return Util.wrapPagingResults(paging, authorityInfoList); + } + + private GroupMember getGroupMember(AuthorityInfo authorityInfo) + { + if (authorityInfo == null) + { + return null; + } + + GroupMember groupMember = new GroupMember(); + groupMember.setId(authorityInfo.getAuthorityName()); + + String authorityDisplayName = authorityInfo.getAuthorityDisplayName(); + if (authorityDisplayName == null || authorityDisplayName.isEmpty()) + { + authorityDisplayName = authorityService.getAuthorityDisplayName(authorityInfo.getAuthorityName()); + } + + groupMember.setDisplayName(authorityDisplayName); + + String memberType = null; + AuthorityType authorityType = AuthorityType.getAuthorityType(authorityInfo.getAuthorityName()); + switch (authorityType) + { + case GROUP: + memberType = PARAM_MEMBER_TYPE_GROUP; + break; + case USER: + memberType = PARAM_MEMBER_TYPE_PERSON; + break; + default: + } + groupMember.setMemberType(memberType); + + return groupMember; + } + + private GroupMember getGroupMember(String authorityId) + { + AuthorityInfo authorityInfo = getAuthorityInfo(authorityId); + + return getGroupMember(authorityInfo); + } + + private void validateGroupId(String groupId, boolean inferPrefix) + { + if (groupId == null || groupId.isEmpty()) + { + throw new InvalidArgumentException("groupId is null or empty"); + } + + if (!PermissionService.ALL_AUTHORITIES.equals(groupId) && !groupAuthorityExists(groupId, inferPrefix)) + { + throw new EntityNotFoundException(groupId); + } + } + + private void validateGroupMemberId(String groupMemberId) + { + if (groupMemberId == null || groupMemberId.isEmpty()) + { + throw new InvalidArgumentException("group member id is null or empty"); + } + if (!(personAuthorityExists(groupMemberId) || groupAuthorityExists(groupMemberId, false))) + { + throw new EntityNotFoundException(groupMemberId); + } + } + + private void validateGroup(Group group, boolean isUpdate) + { + if (group == null) + { + throw new InvalidArgumentException("group is null"); + } + + if (!isUpdate) + { + String groupId = group.getId(); + if (groupId == null || groupId.isEmpty()) + { + throw new InvalidArgumentException("groupId is null or empty"); + } + + if (groupAuthorityExists(groupId)) + { + throw new ConstraintViolatedException("Group '" + group.getId() + "' already exists."); + } + } + else + { + if (group.wasSet(Group.ID)) + { + throw new InvalidArgumentException("Group update does not support field: id"); + } + + if (group.wasSet(Group.IS_ROOT)) + { + throw new InvalidArgumentException("Group update does not support field: isRoot"); + } + + if (group.wasSet(Group.PARENT_IDS)) + { + throw new InvalidArgumentException("Group update does not support field: parentIds"); + } + + if (group.wasSet(Group.ZONES)) + { + throw new InvalidArgumentException("Group update does not support field: zones"); + } + } + } + + private void validateGroupMember(GroupMember groupMember) + { + if (groupMember == null) + { + throw new InvalidArgumentException("group member is null"); + } + + if (groupMember.getId() == null || groupMember.getId().isEmpty()) + { + throw new InvalidArgumentException("group member Id is null or empty"); + } + + if (groupMember.getMemberType() == null || groupMember.getMemberType().isEmpty()) + { + throw new InvalidArgumentException("group member type is null or empty"); + } + } + + private boolean groupAuthorityExists(String authorityName) + { + return groupAuthorityExists(authorityName, true); + } + + private boolean groupAuthorityExists(String authorityName, boolean inferPrefix) + { + return authorityExists(AuthorityType.GROUP, authorityName, inferPrefix); + } + + private boolean personAuthorityExists(String authorityName) + { + return authorityExists(AuthorityType.USER, authorityName, false); + } + + private boolean authorityExists(AuthorityType authorityType, String authorityName, boolean inferPrefix) + { + String name = inferPrefix ? authorityService.getName(authorityType, authorityName) : authorityName; + + return (name != null && authorityService.authorityExists(name)); + } + + private boolean isGroupAuthority(String authorityName) + { + AuthorityType authorityType = AuthorityType.getAuthorityType(authorityName); + return AuthorityType.GROUP.equals(authorityType) || AuthorityType.EVERYONE.equals(authorityType); + } + + private static class GroupsQueryWalker extends MapBasedQueryWalker + { + private List zones; + + public GroupsQueryWalker() + { + super(LIST_GROUPS_EQUALS_QUERY_PROPERTIES, null); + } + + @Override + public void and() + { + // allow AND, e.g. isRoot=true AND zones in ('BLAH') + } + + @Override + public void in(String propertyName, boolean negated, String... propertyValues) + { + if (propertyName.equalsIgnoreCase("zones")) + { + zones = Arrays.asList(propertyValues); + } + } + + /** + * The list of zones specified in the where clause. + * + * @return The zones list if specified, or null if not. + */ + public List getZones() + { + return zones; + } + + /** + * Get the value of the isRoot clause, if specified. + * + * @return The isRoot param if specified, null if not. + */ + public Boolean getIsRoot() + { + return getProperty(PARAM_IS_ROOT, WhereClauseParser.EQUALS, Boolean.class); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/LegacyExceptionHandler.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/LegacyExceptionHandler.java new file mode 100644 index 00000000000..32a1b675227 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/LegacyExceptionHandler.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; + +/** + * Translates access denied exceptions from the service layer to API permission denied exception. + * + * @author steveglover + * + */ +public class LegacyExceptionHandler implements ExceptionHandler +{ + @Override + public boolean handle(Throwable t) + { + if(t instanceof AccessDeniedException) + { + // Note: security, no message to indicate why + throw new PermissionDeniedException(); + } + return false; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/NetworksImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/NetworksImpl.java new file mode 100644 index 00000000000..1e48a299a59 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/NetworksImpl.java @@ -0,0 +1,112 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.query.PagingResults; +import org.alfresco.repo.tenant.NetworksService; +import org.alfresco.rest.api.Networks; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.model.Network; +import org.alfresco.rest.api.model.NetworkImpl; +import org.alfresco.rest.api.model.PersonNetwork; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; + +/** + * Centralises access to network services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class NetworksImpl implements Networks +{ + private People people; + private NetworksService networksService; + + public void setPeople(People people) + { + this.people = people; + } + + public void setNetworksService(NetworksService networksService) + { + this.networksService = networksService; + } + + public Network validateNetwork(String networkId) + { + org.alfresco.repo.tenant.Network network = networksService.getNetwork(networkId); + if(network == null) + { + throw new EntityNotFoundException(networkId); + } + Network restNetwork = new NetworkImpl(network); + return restNetwork; + } + + private PersonNetwork getPersonNetwork(org.alfresco.repo.tenant.Network network) + { + Network restNetwork = new NetworkImpl(network); + PersonNetwork personNetwork = new PersonNetwork(network.getIsHomeNetwork(), restNetwork); + return personNetwork; + } + + public Network getNetwork(String networkId) + { + Network network = validateNetwork(networkId); + return network; + } + + public PersonNetwork getNetwork(String personId, String networkId) + { + // check that personId is the current user + personId = people.validatePerson(personId, true); + Network network = validateNetwork(networkId); + + org.alfresco.repo.tenant.Network tenantNetwork = networksService.getNetwork(network.getId()); + PersonNetwork personNetwork = getPersonNetwork(tenantNetwork); + return personNetwork; + } + + public CollectionWithPagingInfo getNetworks(String personId, Paging paging) + { + // check that personId is the current user + personId = people.validatePerson(personId, true); + + PagingResults networks = networksService.getNetworks(Util.getPagingRequest(paging)); + List ret = new ArrayList(networks.getPage().size()); + for(org.alfresco.repo.tenant.Network network : networks.getPage()) + { + PersonNetwork personNetwork = getPersonNetwork(network); + ret.add(personNetwork); + } + return CollectionWithPagingInfo.asPaged(paging, ret, networks.hasMoreItems(), networks.getTotalResultCount().getFirst()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodeDefinitionMapperImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodeDefinitionMapperImpl.java new file mode 100644 index 00000000000..edc477e1268 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodeDefinitionMapperImpl.java @@ -0,0 +1,124 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.rest.api.NodeDefinitionMapper; +import org.alfresco.rest.api.model.NodeDefinitionConstraint; +import org.alfresco.rest.api.model.NodeDefinition; +import org.alfresco.rest.api.model.NodeDefinitionProperty; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +/** + * Maps representations from TypeDefinition to NodeDefinition + * + * @author gfertuso + */ +public class NodeDefinitionMapperImpl implements NodeDefinitionMapper +{ + + private final List EXCLUDED_NS = Arrays.asList(NamespaceService.SYSTEM_MODEL_1_0_URI); + private static final List EXCLUDED_PROPS = Arrays.asList(ContentModel.PROP_CONTENT); + + @Override + public NodeDefinition fromTypeDefinition(TypeDefinition typeDefinition, + MessageLookup messageLookup) + { + + if (typeDefinition == null) + { + throw new AlfrescoRuntimeException("Undefined definition for the node"); + } + NodeDefinition nodeDefinition = new NodeDefinition(); + nodeDefinition.setProperties(getProperties(typeDefinition.getProperties(), messageLookup)); + + return nodeDefinition; + } + + private boolean isPropertyExcluded(QName propertyName) + { + return EXCLUDED_NS.contains(propertyName.getNamespaceURI()) || EXCLUDED_PROPS.contains(propertyName); + } + + private List getProperties(Map propertiesMap, MessageLookup messageLookup) + { + return propertiesMap.values().stream() + .filter(p -> !isPropertyExcluded(p.getName())) + .map(p -> fromPropertyDefinitionToProperty(p , messageLookup)) + .collect(Collectors.toList()); + } + + private NodeDefinitionProperty fromPropertyDefinitionToProperty(PropertyDefinition propertyDefinition, + MessageLookup messageLookup) + { + NodeDefinitionProperty property = new NodeDefinitionProperty(); + property.setId(propertyDefinition.getName().toPrefixString()); + property.setTitle(propertyDefinition.getTitle(messageLookup)); + property.setDescription(propertyDefinition.getDescription(messageLookup)); + property.setDefaultValue(propertyDefinition.getDefaultValue()); + property.setDataType(propertyDefinition.getDataType().getName().toPrefixString()); + property.setIsMultiValued(propertyDefinition.isMultiValued()); + property.setIsMandatory(propertyDefinition.isMandatory()); + property.setIsMandatoryEnforced(propertyDefinition.isMandatoryEnforced()); + property.setIsProtected(propertyDefinition.isProtected()); + property.setConstraints(getConstraints(propertyDefinition.getConstraints(), messageLookup)); + + return property; + } + + private List getConstraints( Collection constraintDefinitions, + MessageLookup messageLookup) + { + + return constraintDefinitions.stream() + .filter(constraint -> constraint.getConstraint() != null) + .map(constraint -> fromConstraintDefinitionToConstraint(constraint, messageLookup)) + .collect(Collectors.toList()); + } + + private NodeDefinitionConstraint fromConstraintDefinitionToConstraint(ConstraintDefinition constraintDefinition, + MessageLookup messageLookup) + { + NodeDefinitionConstraint constraint = new NodeDefinitionConstraint(); + constraint.setId(constraintDefinition.getConstraint().getShortName()); + constraint.setType(constraintDefinition.getConstraint().getType()); + constraint.setTitle(constraintDefinition.getTitle(messageLookup)); + constraint.setDescription(constraintDefinition.getDescription(messageLookup)); + constraint.setParameters(constraintDefinition.getConstraint().getParameters()); + return constraint; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodeRatingsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodeRatingsImpl.java new file mode 100644 index 00000000000..27256ae15ea --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodeRatingsImpl.java @@ -0,0 +1,160 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.alfresco.rest.api.NodeRatings; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.impl.node.ratings.RatingScheme; +import org.alfresco.rest.api.model.NodeRating; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.cmr.rating.RatingService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.TypeConstraint; +import org.alfresco.util.registry.NamedObjectRegistry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Centralises access to node ratings services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class NodeRatingsImpl implements NodeRatings +{ + private static Log logger = LogFactory.getLog(NodeRatingsImpl.class); + + private Nodes nodes; + private RatingService ratingService; + private NamedObjectRegistry nodeRatingSchemeRegistry; + private TypeConstraint typeConstraint; + + public void setTypeConstraint(TypeConstraint typeConstraint) + { + this.typeConstraint = typeConstraint; + } + + public void setRatingService(RatingService ratingService) + { + this.ratingService = ratingService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNodeRatingSchemeRegistry(NamedObjectRegistry nodeRatingSchemeRegistry) + { + this.nodeRatingSchemeRegistry = nodeRatingSchemeRegistry; + } + + public RatingScheme validateRatingScheme(String ratingSchemeId) + { + RatingScheme ratingScheme = nodeRatingSchemeRegistry.getNamedObject(ratingSchemeId); + if(ratingScheme == null) + { + throw new InvalidArgumentException("Invalid ratingSchemeId " + ratingSchemeId); + } + + return ratingScheme; + } + + // TODO deal with fractional ratings - InvalidArgumentException + public NodeRating getNodeRating(String nodeId, String ratingSchemeId) + { + NodeRef nodeRef = nodes.validateNode(nodeId); + RatingScheme ratingScheme = validateRatingScheme(ratingSchemeId); + return ratingScheme.getNodeRating(nodeRef); + } + + public CollectionWithPagingInfo getNodeRatings(String nodeId, Paging paging) + { + NodeRef nodeRef = nodes.validateNode(nodeId); + Set schemes = new TreeSet(ratingService.getRatingSchemes().keySet()); + Iterator it = schemes.iterator(); + + int skipCount = paging.getSkipCount(); + int maxItems = paging.getMaxItems(); + int totalSize = schemes.size(); + int count = 0; + + int end = skipCount + maxItems; + if(end < 0) + { + // overflow + end = Integer.MAX_VALUE; + } + List ratings = new ArrayList(totalSize); + + for(int i = 0; i < end && it.hasNext(); i++) + { + String schemeName = it.next(); + if(i < skipCount) + { + continue; + } + count++; + RatingScheme ratingScheme = validateRatingScheme(schemeName); + NodeRating nodeRating = ratingScheme.getNodeRating(nodeRef); + ratings.add(nodeRating); + } + + boolean hasMoreItems = (skipCount + count < totalSize); + + return CollectionWithPagingInfo.asPaged(paging, ratings, hasMoreItems, totalSize); + } + + public void addRating(String nodeId, String ratingSchemeId, Object rating) + { + NodeRef nodeRef = nodes.validateNode(nodeId); + + RatingScheme ratingScheme = validateRatingScheme(ratingSchemeId); + + if(!typeConstraint.matches(nodeRef)) + { + throw new UnsupportedResourceOperationException("Cannot rate this node"); + } + + ratingScheme.applyRating(nodeRef, rating); + } + + public void removeRating(String nodeId, String ratingSchemeId) + { + RatingScheme ratingScheme = validateRatingScheme(ratingSchemeId); + NodeRef nodeRef = nodes.validateNode(nodeId); + ratingScheme.removeRating(nodeRef); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java new file mode 100644 index 00000000000..600c2763371 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -0,0 +1,3693 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +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.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import org.alfresco.model.ApplicationModel; +import org.alfresco.model.ContentModel; +import org.alfresco.model.QuickShareModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.action.executer.ContentMetadataExtracter; +import org.alfresco.repo.activities.ActivityType; +import org.alfresco.repo.content.ContentLimitViolationException; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.domain.node.AuditablePropertiesEntity; +import org.alfresco.repo.lock.mem.Lifetime; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; +import org.alfresco.repo.node.getchildren.FilterProp; +import org.alfresco.repo.node.getchildren.FilterPropBoolean; +import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; +import org.alfresco.repo.node.integrity.IntegrityException; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.rendition2.RenditionDefinition2; +import org.alfresco.repo.rendition2.RenditionDefinitionRegistry2; +import org.alfresco.repo.rendition2.RenditionService2; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.repo.virtual.store.VirtualStore; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Activities; +import org.alfresco.rest.api.NodeDefinitionMapper; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.QuickShareLinks; +import org.alfresco.rest.api.model.AssocChild; +import org.alfresco.rest.api.model.AssocTarget; +import org.alfresco.rest.api.model.NodeDefinition; +import org.alfresco.rest.api.model.Document; +import org.alfresco.rest.api.model.Folder; +import org.alfresco.rest.api.model.LockInfo; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.NodePermissions; +import org.alfresco.rest.api.model.PathInfo; +import org.alfresco.rest.api.model.PathInfo.ElementInfo; +import org.alfresco.rest.api.model.QuickShareLink; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InsufficientStorageException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException; +import org.alfresco.rest.framework.core.exceptions.UnsupportedMediaTypeException; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.content.ContentInfoImpl; +import org.alfresco.rest.framework.resource.content.NodeBinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.activities.ActivitiesTransactionListener; +import org.alfresco.service.cmr.activities.ActivityInfo; +import org.alfresco.service.cmr.activities.ActivityPoster; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.lock.LockService; +import org.alfresco.service.cmr.lock.NodeLockedException; +import org.alfresco.service.cmr.model.FileExistsException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.model.FileNotFoundException; +import org.alfresco.service.cmr.preference.PreferenceService; +import org.alfresco.service.cmr.repository.AssociationExistsException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentIOException; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.Path.Element; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.alfresco.service.cmr.usage.ContentQuotaException; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.joda.time.DateTime; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * Centralises access to file/folder/node services and maps between representations. + * + * Note: + * This class was originally used for returning some basic node info when listing Favourites. + * + * It has now been re-purposed and extended to implement the new Nodes (RESTful) API for + * managing files & folders, as well as custom node types. + * + * @author steveglover + * @author janv + * @author Jamal Kaabi-Mofrad + * + * @since publicapi1.0 + */ +public class NodesImpl implements Nodes +{ + private static final Log logger = LogFactory.getLog(NodesImpl.class); + + private enum Type + { + // Note: ordered + DOCUMENT, FOLDER + } + + private NodeService nodeService; + private DictionaryService dictionaryService; + private FileFolderService fileFolderService; + private NamespaceService namespaceService; + private PermissionService permissionService; + private MimetypeService mimetypeService; + private ContentService contentService; + private ActionService actionService; + private VersionService versionService; + private PersonService personService; + private OwnableService ownableService; + private AuthorityService authorityService; + private ThumbnailService thumbnailService; + private RenditionService2 renditionService2; + private SiteService siteService; + private ActivityPoster poster; + private RetryingTransactionHelper retryingTransactionHelper; + private LockService lockService; + private VirtualStore smartStore; // note: remove as part of REPO-1173 + private NodeDefinitionMapper nodeDefinitionMapper; + + private enum Activity_Type + { + ADDED, UPDATED, DELETED, DOWNLOADED + } + + private BehaviourFilter behaviourFilter; + + // note: circular - Nodes/QuickShareLinks currently use each other (albeit for different methods) + private QuickShareLinks quickShareLinks; + + private Repository repositoryHelper; + private ServiceRegistry sr; + private Set defaultIgnoreTypesAndAspects; + + // ignore types/aspects + private Set ignoreQNames; + + private ConcurrentHashMap ddCache = new ConcurrentHashMap<>(); + + private Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf + + public void setNonAttachContentTypes(Set nonAttachWhiteList) + { + this.nonAttachContentTypes = nonAttachWhiteList; + } + + public void init() + { + PropertyCheck.mandatory(this, "serviceRegistry", sr); + PropertyCheck.mandatory(this, "behaviourFilter", behaviourFilter); + PropertyCheck.mandatory(this, "repositoryHelper", repositoryHelper); + PropertyCheck.mandatory(this, "quickShareLinks", quickShareLinks); + PropertyCheck.mandatory(this, "poster", poster); + + this.namespaceService = sr.getNamespaceService(); + this.fileFolderService = sr.getFileFolderService(); + this.nodeService = sr.getNodeService(); + this.permissionService = sr.getPermissionService(); + this.dictionaryService = sr.getDictionaryService(); + this.mimetypeService = sr.getMimetypeService(); + this.contentService = sr.getContentService(); + this.actionService = sr.getActionService(); + this.versionService = sr.getVersionService(); + this.personService = sr.getPersonService(); + this.ownableService = sr.getOwnableService(); + this.authorityService = sr.getAuthorityService(); + this.thumbnailService = sr.getThumbnailService(); + this.renditionService2 = sr.getRenditionService2(); + this.siteService = sr.getSiteService(); + this.retryingTransactionHelper = sr.getRetryingTransactionHelper(); + this.lockService = sr.getLockService(); + + if (defaultIgnoreTypesAndAspects != null) + { + ignoreQNames = new HashSet<>(defaultIgnoreTypesAndAspects.size()); + for (String type : defaultIgnoreTypesAndAspects) + { + ignoreQNames.add(createQName(type)); + } + } + } + + public void setServiceRegistry(ServiceRegistry sr) + { + this.sr = sr; + } + + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + public void setRepositoryHelper(Repository repositoryHelper) + { + this.repositoryHelper = repositoryHelper; + } + + public void setQuickShareLinks(QuickShareLinks quickShareLinks) + { + this.quickShareLinks = quickShareLinks; + } + + public void setIgnoreTypes(Set ignoreTypesAndAspects) + { + this.defaultIgnoreTypesAndAspects = ignoreTypesAndAspects; + } + + public void setPoster(ActivityPoster poster) + { + this.poster = poster; + } + + public void setSmartStore(VirtualStore smartStore) + { + this.smartStore = smartStore; + } + + public void setNodeDefinitionMapper(NodeDefinitionMapper nodeDefinitionMapper) + { + this.nodeDefinitionMapper = nodeDefinitionMapper; + } + + // excluded namespaces (aspects, properties, assoc types) + private static final List EXCLUDED_NS = Arrays.asList(NamespaceService.SYSTEM_MODEL_1_0_URI); + + // excluded aspects + private static final List EXCLUDED_ASPECTS = Arrays.asList(); + + // excluded properties + private static final List EXCLUDED_PROPS = Arrays.asList( + // top-level minimal info + ContentModel.PROP_NAME, + ContentModel.PROP_MODIFIER, + ContentModel.PROP_MODIFIED, + ContentModel.PROP_CREATOR, + ContentModel.PROP_CREATED, + ContentModel.PROP_CONTENT, + // other - TBC + ContentModel.PROP_INITIAL_VERSION, + ContentModel.PROP_AUTO_VERSION_PROPS, + ContentModel.PROP_AUTO_VERSION); + + public static final List PROPS_USERLOOKUP = Arrays.asList( + ContentModel.PROP_CREATOR, + ContentModel.PROP_MODIFIER, + ContentModel.PROP_OWNER, + ContentModel.PROP_LOCK_OWNER, + ContentModel.PROP_WORKING_COPY_OWNER); + + public final static Map PARAM_SYNONYMS_QNAME; + static + { + Map aMap = new HashMap<>(9); + + aMap.put(PARAM_ISFOLDER, GetChildrenCannedQuery.SORT_QNAME_NODE_IS_FOLDER); + aMap.put(PARAM_NAME, ContentModel.PROP_NAME); + aMap.put(PARAM_CREATEDAT, ContentModel.PROP_CREATED); + aMap.put(PARAM_MODIFIEDAT, ContentModel.PROP_MODIFIED); + aMap.put(PARAM_CREATEBYUSER, ContentModel.PROP_CREATOR); + aMap.put(PARAM_MODIFIEDBYUSER, ContentModel.PROP_MODIFIER); + aMap.put(PARAM_MIMETYPE, GetChildrenCannedQuery.SORT_QNAME_CONTENT_MIMETYPE); + aMap.put(PARAM_SIZEINBYTES, GetChildrenCannedQuery.SORT_QNAME_CONTENT_SIZE); + aMap.put(PARAM_NODETYPE, GetChildrenCannedQuery.SORT_QNAME_NODE_TYPE); + + PARAM_SYNONYMS_QNAME = Collections.unmodifiableMap(aMap); + } + + // list children filtering (via where clause) + private final static Set LIST_FOLDER_CHILDREN_EQUALS_QUERY_PROPERTIES = + new HashSet<>(Arrays.asList(new String[] {PARAM_ISFOLDER, PARAM_ISFILE, PARAM_NODETYPE, PARAM_ISPRIMARY, PARAM_ASSOC_TYPE})); + + /* + * Validates that node exists. + * + * Note: assumes workspace://SpacesStore + */ + @Override + public NodeRef validateNode(String nodeId) + { + //belts-and-braces + if (nodeId == null) + { + throw new InvalidArgumentException("Missing nodeId"); + } + + return validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + } + + @Override + public NodeRef validateNode(StoreRef storeRef, String nodeId) + { + String versionLabel = null; + + int idx = nodeId.indexOf(";"); + if (idx != -1) + { + versionLabel = nodeId.substring(idx + 1); + nodeId = nodeId.substring(0, idx); + if (versionLabel.equals("pwc")) + { + // TODO correct exception? + throw new EntityNotFoundException(nodeId); + } + } + + NodeRef nodeRef = new NodeRef(storeRef, nodeId); + return validateNode(nodeRef); + } + + @Override + public NodeRef validateNode(NodeRef nodeRef) + { + if (!nodeService.exists(nodeRef)) + { + throw new EntityNotFoundException(nodeRef.getId()); + } + + return nodeRef; + } + + /* + * Check that nodes exists and matches given expected/excluded type(s). + */ + @Override + public boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes) + { + return nodeMatches(nodeRef, expectedTypes, excludedTypes, true); + } + + @Override + public boolean isSubClass(NodeRef nodeRef, QName ofClassQName, boolean validateNodeRef) + { + if (validateNodeRef) + { + nodeRef = validateNode(nodeRef); + } + return isSubClass(getNodeType(nodeRef), ofClassQName); + } + + private boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes, boolean existsCheck) + { + if (existsCheck && (! nodeService.exists(nodeRef))) + { + throw new EntityNotFoundException(nodeRef.getId()); + } + + return typeMatches(getNodeType(nodeRef), expectedTypes, excludedTypes); + } + + private QName getNodeType(NodeRef nodeRef) + { + return nodeService.getType(nodeRef); + } + + private boolean isSubClass(QName className, QName ofClassQName) + { + return dictionaryService.isSubClass(className, ofClassQName); + } + + protected boolean typeMatches(QName type, Set expectedTypes, Set excludedTypes) + { + if (((expectedTypes != null) && (expectedTypes.size() == 1)) && + ((excludedTypes == null) || (excludedTypes.size() == 0))) + { + // use isSubClass if checking against single expected type (and no excluded types) + return isSubClass(type, expectedTypes.iterator().next()); + } + + Set allExpectedTypes = new HashSet<>(); + if (expectedTypes != null) + { + for (QName expectedType : expectedTypes) + { + allExpectedTypes.addAll(dictionaryService.getSubTypes(expectedType, true)); + } + } + + Set allExcludedTypes = new HashSet<>(); + if (excludedTypes != null) + { + for (QName excludedType : excludedTypes) + { + allExcludedTypes.addAll(dictionaryService.getSubTypes(excludedType, true)); + } + } + + boolean inExpected = allExpectedTypes.contains(type); + boolean excluded = allExcludedTypes.contains(type); + return (inExpected && !excluded); + } + + /** + * @deprecated review usage (backward compat') + */ + @Override + public Node getNode(String nodeId) + { + NodeRef nodeRef = validateNode(nodeId); + + return new Node(nodeRef, null, nodeService.getProperties(nodeRef), null, sr); + } + + /** + * @deprecated review usage (backward compat') + */ + public Node getNode(NodeRef nodeRef) + { + return new Node(nodeRef, null, nodeService.getProperties(nodeRef), null, sr); + } + + private Type getType(NodeRef nodeRef) + { + return getType(getNodeType(nodeRef), nodeRef); + } + + private Type getType(QName typeQName, NodeRef nodeRef) + { + // quick check for common types + if (typeQName.equals(ContentModel.TYPE_FOLDER) || typeQName.equals(ApplicationModel.TYPE_FOLDERLINK)) + { + return Type.FOLDER; + } + else if (typeQName.equals(ContentModel.TYPE_CONTENT) || typeQName.equals(ApplicationModel.TYPE_FILELINK)) + { + return Type.DOCUMENT; + } + + // further checks + + if (isSubClass(typeQName, ContentModel.TYPE_LINK)) + { + if (isSubClass(typeQName, ApplicationModel.TYPE_FOLDERLINK)) + { + return Type.FOLDER; + } + else if (isSubClass(typeQName, ApplicationModel.TYPE_FILELINK)) + { + return Type.DOCUMENT; + } + + NodeRef linkNodeRef = (NodeRef)nodeService.getProperty(nodeRef, ContentModel.PROP_LINK_DESTINATION); + if (linkNodeRef != null) + { + try + { + typeQName = getNodeType(linkNodeRef); + // drop-through to check type of destination + // note: edge-case - if link points to another link then we will return null + } + catch (InvalidNodeRefException inre) + { + // ignore + } + } + } + + if (isSubClass(typeQName, ContentModel.TYPE_FOLDER)) + { + if (! isSubClass(typeQName, ContentModel.TYPE_SYSTEM_FOLDER)) + { + return Type.FOLDER; + } + return null; // unknown + } + else if (isSubClass(typeQName, ContentModel.TYPE_CONTENT)) + { + return Type.DOCUMENT; + } + + return null; // unknown + } + + /** + * @deprecated note: currently required for backwards compat' (Favourites API) + */ + @Override + public Document getDocument(NodeRef nodeRef) + { + Type type = getType(nodeRef); + if ((type != null) && type.equals(Type.DOCUMENT)) + { + Map properties = nodeService.getProperties(nodeRef); + + Document doc = new Document(nodeRef, getParentNodeRef(nodeRef), properties, null, sr); + + doc.setVersionLabel((String) properties.get(ContentModel.PROP_VERSION_LABEL)); + ContentData cd = (ContentData) properties.get(ContentModel.PROP_CONTENT); + if (cd != null) + { + doc.setSizeInBytes(BigInteger.valueOf(cd.getSize())); + doc.setMimeType((cd.getMimetype())); + } + + setCommonProps(doc, nodeRef, properties); + return doc; + } + else + { + throw new InvalidArgumentException("Node is not a file: "+nodeRef.getId()); + } + } + + private void setCommonProps(Node node, NodeRef nodeRef, Map properties) + { + node.setGuid(nodeRef); + node.setTitle((String)properties.get(ContentModel.PROP_TITLE)); + node.setDescription((String)properties.get(ContentModel.PROP_DESCRIPTION)); + node.setModifiedBy((String)properties.get(ContentModel.PROP_MODIFIER)); + node.setCreatedBy((String)properties.get(ContentModel.PROP_CREATOR)); + } + + /** + * @deprecated note: currently required for backwards compat' (Favourites API) + */ + @Override + public Folder getFolder(NodeRef nodeRef) + { + Type type = getType(nodeRef); + if ((type != null) && type.equals(Type.FOLDER)) + { + Map properties = nodeService.getProperties(nodeRef); + + Folder folder = new Folder(nodeRef, getParentNodeRef(nodeRef), properties, null, sr); + setCommonProps(folder, nodeRef, properties); + return folder; + } + else + { + throw new InvalidArgumentException("Node is not a folder: "+nodeRef.getId()); + } + } + + private NodeRef getParentNodeRef(NodeRef nodeRef) + { + if (repositoryHelper.getCompanyHome().equals(nodeRef)) + { + return null; // note: does not make sense to return parent above C/H + } + + return nodeService.getPrimaryParent(nodeRef).getParentRef(); + } + + public NodeRef validateOrLookupNode(String nodeId, String path) + { + NodeRef parentNodeRef; + + if ((nodeId == null) || (nodeId.isEmpty())) + { + throw new InvalidArgumentException("Missing nodeId"); + } + + if (nodeId.equals(PATH_ROOT)) + { + parentNodeRef = repositoryHelper.getCompanyHome(); + } + else if (nodeId.equals(PATH_SHARED)) + { + parentNodeRef = repositoryHelper.getSharedHome(); + } + else if (nodeId.equals(PATH_MY)) + { + NodeRef person = repositoryHelper.getPerson(); + if (person == null) + { + throw new InvalidArgumentException("Unexpected - cannot use: " + PATH_MY); + } + parentNodeRef = repositoryHelper.getUserHome(person); + if (parentNodeRef == null) + { + throw new EntityNotFoundException(nodeId); + } + } + else + { + parentNodeRef = validateNode(nodeId); + } + + if (path != null) + { + // check that parent is a folder before resolving relative path + if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false)) + { + throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef.getId()); + } + + // resolve path relative to current nodeId + parentNodeRef = resolveNodeByPath(parentNodeRef, path, true); + } + + return parentNodeRef; + } + + protected NodeRef resolveNodeByPath(final NodeRef parentNodeRef, String path, boolean checkForCompanyHome) + { + final List pathElements = getPathElements(path); + + if (!pathElements.isEmpty() && checkForCompanyHome) + { + /* + if (nodeService.getRootNode(parentNodeRef.getStoreRef()).equals(parentNodeRef)) + { + // special case + NodeRef chNodeRef = repositoryHelper.getCompanyHome(); + String chName = (String) nodeService.getProperty(chNodeRef, ContentModel.PROP_NAME); + if (chName.equals(pathElements.get(0))) + { + pathElements = pathElements.subList(1, pathElements.size()); + parentNodeRef = chNodeRef; + } + } + */ + } + + FileInfo fileInfo = null; + try + { + if (!pathElements.isEmpty()) + { + fileInfo = fileFolderService.resolveNamePath(parentNodeRef, pathElements); + } + else + { + fileInfo = fileFolderService.getFileInfo(parentNodeRef); + if (fileInfo == null) + { + throw new EntityNotFoundException(parentNodeRef.getId()); + } + } + } + catch (FileNotFoundException fnfe) + { + // convert checked exception + throw new NotFoundException("The entity with relativePath: " + path + " was not found."); + } + catch (AccessDeniedException ade) + { + // return 404 instead of 403 (as per security review - uuid vs path) + throw new NotFoundException("The entity with relativePath: " + path + " was not found."); + } + + return fileInfo.getNodeRef(); + } + + private List getPathElements(String path) + { + final List pathElements = new ArrayList<>(); + if (path != null && path.trim().length() > 0) + { + // There is no need to check for leading and trailing "/" + final StringTokenizer tokenizer = new StringTokenizer(path, "/"); + while (tokenizer.hasMoreTokens()) + { + pathElements.add(tokenizer.nextToken().trim()); + } + } + return pathElements; + } + + private NodeRef makeFolders(NodeRef parentNodeRef, List pathElements) + { + NodeRef currentParentRef = parentNodeRef; + // just loop and create if necessary + for (final String element : pathElements) + { + final NodeRef contextNodeRef = currentParentRef; + // does it exist? + // Navigation should not check permissions + NodeRef nodeRef = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + return nodeService.getChildByName(contextNodeRef, ContentModel.ASSOC_CONTAINS, element); + } + }, AuthenticationUtil.getSystemUserName()); + + if (nodeRef == null) + { + try + { + // Checks for create permissions as the fileFolderService is a public service. + FileInfo createdFileInfo = fileFolderService.create(currentParentRef, element, ContentModel.TYPE_FOLDER); + currentParentRef = createdFileInfo.getNodeRef(); + } + catch (AccessDeniedException ade) + { + throw new PermissionDeniedException(ade.getMessage()); + } + catch (FileExistsException fex) + { + // Assume concurrency failure, so retry + throw new ConcurrencyFailureException(fex.getMessage()); + } + } + else if (!isSubClass(nodeRef, ContentModel.TYPE_FOLDER, false)) + { + String parentName = (String) nodeService.getProperty(contextNodeRef, ContentModel.PROP_NAME); + throw new ConstraintViolatedException("Name [" + element + "] already exists in the target parent: " + parentName); + } + else + { + // it exists + currentParentRef = nodeRef; + } + } + return currentParentRef; + } + + @Override + public Node getFolderOrDocument(String nodeId, Parameters parameters) + { + String path = parameters.getParameter(PARAM_RELATIVE_PATH); + NodeRef nodeRef = validateOrLookupNode(nodeId, path); + Node node = getFolderOrDocumentFullInfo(nodeRef, null, null, parameters); + return node; + + } + + private Node getFolderOrDocumentFullInfo(NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, Parameters parameters) + { + return getFolderOrDocumentFullInfo(nodeRef, parentNodeRef, nodeTypeQName, parameters, null); + } + + @Override + public Node getFolderOrDocumentFullInfo(NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, Parameters parameters, Map mapUserInfo) + { + List includeParam = new ArrayList<>(); + if (parameters != null) + { + includeParam.addAll(parameters.getInclude()); + } + + // Add basic info for single get (above & beyond minimal that is used for listing collections) + includeParam.add(PARAM_INCLUDE_ASPECTNAMES); + includeParam.add(PARAM_INCLUDE_PROPERTIES); + + return getFolderOrDocument(nodeRef, parentNodeRef, nodeTypeQName, includeParam, mapUserInfo); + } + + @Override + public Node getFolderOrDocument(final NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, List includeParam, Map mapUserInfo) + { + if (mapUserInfo == null) + { + mapUserInfo = new HashMap<>(2); + } + + if (includeParam == null) + { + includeParam = Collections.emptyList(); + } + + Node node; + Map properties = nodeService.getProperties(nodeRef); + + PathInfo pathInfo = null; + if (includeParam.contains(PARAM_INCLUDE_PATH)) + { + ChildAssociationRef archivedParentAssoc = (ChildAssociationRef) properties.get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + pathInfo = lookupPathInfo(nodeRef, archivedParentAssoc); + } + + if (nodeTypeQName == null) + { + nodeTypeQName = getNodeType(nodeRef); + } + + if (parentNodeRef == null) + { + parentNodeRef = getParentNodeRef(nodeRef); + } + + Type type = getType(nodeTypeQName, nodeRef); + + if (type == null) + { + // not direct folder (or file) ... + // might be sub-type of cm:cmobject (or a cm:link pointing to cm:cmobject or possibly even another cm:link) + node = new Node(nodeRef, parentNodeRef, properties, mapUserInfo, sr); + node.setIsFolder(false); + node.setIsFile(false); + } + else if (type.equals(Type.DOCUMENT)) + { + node = new Document(nodeRef, parentNodeRef, properties, mapUserInfo, sr); + } + else if (type.equals(Type.FOLDER)) + { + node = new Folder(nodeRef, parentNodeRef, properties, mapUserInfo, sr); + } + else + { + throw new RuntimeException("Unexpected - should not reach here: "+type); + } + + if (includeParam.size() > 0) + { + node.setProperties(mapFromNodeProperties(properties, includeParam, mapUserInfo, EXCLUDED_NS, EXCLUDED_PROPS)); + } + + Set aspects = null; + if (includeParam.contains(PARAM_INCLUDE_ASPECTNAMES)) + { + aspects = nodeService.getAspects(nodeRef); + node.setAspectNames(mapFromNodeAspects(aspects, EXCLUDED_NS, EXCLUDED_ASPECTS)); + } + + if (includeParam.contains(PARAM_INCLUDE_ISLINK)) + { + boolean isLink = isSubClass(nodeTypeQName, ContentModel.TYPE_LINK); + node.setIsLink(isLink); + } + + if (includeParam.contains(PARAM_INCLUDE_ISLOCKED)) + { + boolean isLocked = isLocked(nodeRef, aspects); + node.setIsLocked(isLocked); + } + + if (includeParam.contains(PARAM_INCLUDE_ISFAVORITE)) + { + boolean isFavorite = isFavorite(nodeRef); + node.setIsFavorite(isFavorite); + } + + if (includeParam.contains(PARAM_INCLUDE_ALLOWABLEOPERATIONS)) + { + // note: refactor when requirements change + Map mapPermsToOps = new HashMap<>(3); + mapPermsToOps.put(PermissionService.DELETE, OP_DELETE); + mapPermsToOps.put(PermissionService.ADD_CHILDREN, OP_CREATE); + mapPermsToOps.put(PermissionService.WRITE, OP_UPDATE); + mapPermsToOps.put(PermissionService.CHANGE_PERMISSIONS, OP_UPDATE_PERMISSIONS); + + + List allowableOperations = new ArrayList<>(3); + for (Entry kv : mapPermsToOps.entrySet()) + { + String perm = kv.getKey(); + String op = kv.getValue(); + + if (perm.equals(PermissionService.ADD_CHILDREN) && Type.DOCUMENT.equals(type)) + { + // special case: do not return "create" (as an allowable op) for file/content types - note: 'type' can be null + continue; + } + else if (perm.equals(PermissionService.DELETE) && (isSpecialNode(nodeRef, nodeTypeQName))) + { + // special case: do not return "delete" (as an allowable op) for specific system nodes + continue; + } + else if (permissionService.hasPermission(nodeRef, perm) == AccessStatus.ALLOWED) + { + allowableOperations.add(op); + } + } + + node.setAllowableOperations((allowableOperations.size() > 0 )? allowableOperations : null); + } + + if (includeParam.contains(PARAM_INCLUDE_PERMISSIONS)) + { + Boolean inherit = permissionService.getInheritParentPermissions(nodeRef); + + List inheritedPerms = new ArrayList<>(5); + List setDirectlyPerms = new ArrayList<>(5); + Set settablePerms = null; + boolean allowRetrievePermission = true; + + try + { + for (AccessPermission accessPerm : permissionService.getAllSetPermissions(nodeRef)) + { + NodePermissions.NodePermission nodePerm = new NodePermissions.NodePermission(accessPerm.getAuthority(), accessPerm.getPermission(), accessPerm.getAccessStatus().toString()); + if (accessPerm.isSetDirectly()) + { + setDirectlyPerms.add(nodePerm); + } else + { + inheritedPerms.add(nodePerm); + } + } + + settablePerms = permissionService.getSettablePermissions(nodeRef); + } + catch (AccessDeniedException ade) + { + // ignore - ie. denied access to retrieve permissions, eg. non-admin on root (Company Home) + allowRetrievePermission = false; + } + + // If the user does not have read permissions at + // least on a special node then do not include permissions and + // returned only node info that he's allowed to see + if (allowRetrievePermission) + { + NodePermissions nodePerms = new NodePermissions(inherit, inheritedPerms, setDirectlyPerms, settablePerms); + node.setPermissions(nodePerms); + } + } + + if (includeParam.contains(PARAM_INCLUDE_ASSOCIATION)) + { + // Ugh ... can we optimise this and return the actual assoc directly (via FileFolderService/GetChildrenCQ) ? + ChildAssociationRef parentAssocRef = nodeService.getPrimaryParent(nodeRef); + + // note: parentAssocRef.parentRef can be null for -root- node ! + if ((parentAssocRef == null) || (parentAssocRef.getParentRef() == null) || (! parentAssocRef.getParentRef().equals(parentNodeRef))) + { + List parentAssocRefs = nodeService.getParentAssocs(nodeRef); + for (ChildAssociationRef pAssocRef : parentAssocRefs) + { + if (pAssocRef.getParentRef().equals(parentNodeRef)) + { + // for now, assume same parent/child cannot appear more than once (due to unique name) + parentAssocRef = pAssocRef; + break; + } + } + } + + if (parentAssocRef != null) + { + QName assocTypeQName = parentAssocRef.getTypeQName(); + if ((assocTypeQName != null) && (! EXCLUDED_NS.contains(assocTypeQName.getNamespaceURI()))) + { + AssocChild childAssoc = new AssocChild( + assocTypeQName.toPrefixString(namespaceService), + parentAssocRef.isPrimary()); + + node.setAssociation(childAssoc); + } + } + } + + if (includeParam.contains(PARAM_INCLUDE_DEFINITION)) + { + NodeDefinition nodeDefinition = nodeDefinitionMapper.fromTypeDefinition(getTypeDefinition(nodeRef), dictionaryService); + node.setDefinition(nodeDefinition); + } + + node.setNodeType(nodeTypeQName.toPrefixString(namespaceService)); + node.setPath(pathInfo); + + return node; + } + + private TypeDefinition getTypeDefinition(NodeRef nodeRef) + { + QName type = nodeService.getType(nodeRef); + Set aspectNames = nodeService.getAspects(nodeRef); + TypeDefinition typeDefinition = dictionaryService.getAnonymousType(type, aspectNames); + return typeDefinition; + } + + @Override + public PathInfo lookupPathInfo(NodeRef nodeRefIn, ChildAssociationRef archivedParentAssoc) + { + + List pathElements = new ArrayList<>(); + Boolean isComplete = Boolean.TRUE; + final Path nodePath; + final int pathIndex; + + if (archivedParentAssoc != null) + { + if (permissionService.hasPermission(archivedParentAssoc.getParentRef(), PermissionService.READ).equals(AccessStatus.ALLOWED) + && nodeService.exists(archivedParentAssoc.getParentRef())) + { + nodePath = nodeService.getPath(archivedParentAssoc.getParentRef()); + pathIndex = 1;// 1 => we want to include the given node in the path as well. + } + else + { + //We can't return a valid path + return null; + } + } + else + { + nodePath = nodeService.getPath(nodeRefIn); + pathIndex = 2; // 2 => as we don't want to include the given node in the path as well. + } + + for (int i = nodePath.size() - pathIndex; i >= 0; i--) + { + Element element = nodePath.get(i); + if (element instanceof Path.ChildAssocElement) + { + ChildAssociationRef elementRef = ((Path.ChildAssocElement) element).getRef(); + if (elementRef.getParentRef() != null) + { + NodeRef childNodeRef = elementRef.getChildRef(); + if (permissionService.hasPermission(childNodeRef, PermissionService.READ) == AccessStatus.ALLOWED) + { + Serializable nameProp = nodeService.getProperty(childNodeRef, ContentModel.PROP_NAME); + String type = getNodeType(childNodeRef).toPrefixString(namespaceService); + Set aspects = nodeService.getAspects(childNodeRef); + List aspectNames = mapFromNodeAspects(aspects, EXCLUDED_NS, EXCLUDED_ASPECTS); + pathElements.add(0, new ElementInfo(childNodeRef.getId(), nameProp.toString(), type, aspectNames)); + } + else + { + // Just return the pathInfo up to the location where the user has access + isComplete = Boolean.FALSE; + break; + } + } + } + } + + String pathStr = null; + if (pathElements.size() > 0) + { + StringBuilder sb = new StringBuilder(120); + for (PathInfo.ElementInfo e : pathElements) + { + sb.append("/").append(e.getName()); + } + pathStr = sb.toString(); + } + else + { + // There is no path element, so set it to null in order to be + // ignored by Jackson during serialisation + isComplete = null; + } + return new PathInfo(pathStr, isComplete, pathElements); + } + + public Set mapToNodeAspects(List aspectNames) + { + Set nodeAspects = new HashSet<>(aspectNames.size()); + + for (String aspectName : aspectNames) + { + QName aspectQName = createQName(aspectName); + + AspectDefinition ad = dictionaryService.getAspect(aspectQName); + if (ad != null) + { + nodeAspects.add(aspectQName); + } + else + { + throw new InvalidArgumentException("Unknown aspect: " + aspectName); + } + } + + return nodeAspects; + } + + public Map mapToNodeProperties(Map props) + { + Map nodeProps = new HashMap<>(props.size()); + + for (Entry entry : props.entrySet()) + { + String propName = entry.getKey(); + QName propQName = createQName(propName); + + PropertyDefinition pd = dictionaryService.getProperty(propQName); + if (pd != null) + { + Serializable value = null; + if (entry.getValue() != null) + { + if (pd.getDataType().getName().equals(DataTypeDefinition.NODE_REF)) + { + String nodeRefString = (String) entry.getValue(); + if (! NodeRef.isNodeRef(nodeRefString)) + { + value = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeRefString); + } + else + { + value = new NodeRef(nodeRefString); + } + } + else + { + value = (Serializable)entry.getValue(); + } + } + nodeProps.put(propQName, value); + } + else + { + throw new InvalidArgumentException("Unknown property: " + propName); + } + } + return nodeProps; + } + + public Map mapFromNodeProperties(Map nodeProps, List selectParam, Map mapUserInfo, List excludedNS, List excludedProps) + { + List selectedProperties; + + if ((selectParam.size() == 0) || selectParam.contains(PARAM_INCLUDE_PROPERTIES)) + { + // return all properties + selectedProperties = new ArrayList<>(nodeProps.size()); + for (QName propQName : nodeProps.keySet()) + { + if ((! excludedNS.contains(propQName.getNamespaceURI())) && (! excludedProps.contains(propQName))) + { + selectedProperties.add(propQName); + } + } + } + else + { + // return selected properties + selectedProperties = createQNames(selectParam, excludedProps); + } + + Map props = null; + if (!selectedProperties.isEmpty()) + { + props = new HashMap<>(selectedProperties.size()); + + for (QName qName : selectedProperties) + { + Serializable value = nodeProps.get(qName); + if (value != null) + { + if (PROPS_USERLOOKUP.contains(qName)) + { + value = Node.lookupUserInfo((String)value, mapUserInfo, sr.getPersonService()); + } + + // Empty (zero length) string values are considered to be + // null values, and will be represented the same as null + // values (i.e. by non-existence of the property). + if (value != null && value instanceof String && ((String) value).isEmpty()) + { + continue; + } + + props.put(qName.toPrefixString(namespaceService), value); + } + } + if (props.isEmpty()) + { + props = null; // set to null so it doesn't show up as an empty object in the JSON response. + } + } + + return props; + } + + public List mapFromNodeAspects(Set nodeAspects, List excludedNS, List excludedAspects) + { + List aspectNames = new ArrayList<>(nodeAspects.size()); + + for (QName aspectQName : nodeAspects) + { + if ((! excludedNS.contains(aspectQName.getNamespaceURI())) && (! excludedAspects.contains(aspectQName))) + { + aspectNames.add(aspectQName.toPrefixString(namespaceService)); + } + } + + if (aspectNames.size() == 0) + { + aspectNames = null; // no aspects to return + } + + return aspectNames; + } + + @Override + public CollectionWithPagingInfo listChildren(String parentFolderNodeId, Parameters parameters) + { + String path = parameters.getParameter(PARAM_RELATIVE_PATH); + + final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, path); + + final List includeParam = parameters.getInclude(); + + QName assocTypeQNameParam = null; + + Query q = parameters.getQuery(); + + if (q != null) + { + // filtering via "where" clause + MapBasedQueryWalker propertyWalker = createListChildrenQueryWalker(); + QueryHelper.walk(q, propertyWalker); + + String assocTypeQNameStr = propertyWalker.getProperty(PARAM_ASSOC_TYPE, WhereClauseParser.EQUALS, String.class); + if (assocTypeQNameStr != null) + { + assocTypeQNameParam = getAssocType(assocTypeQNameStr); + } + } + + List> sortProps = getListChildrenSortProps(parameters); + List filterProps = getListChildrenFilterProps(parameters); + + Paging paging = parameters.getPaging(); + + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + final PagingResults pagingResults; + + Pair, Set> pair = buildSearchTypesAndIgnoreAspects(parameters); + + Set searchTypeQNames = pair.getFirst(); + Set ignoreAspectQNames = pair.getSecond(); + + Set assocTypeQNames = buildAssocTypes(assocTypeQNameParam); + + // call GetChildrenCannedQuery (via FileFolderService) + if (((filterProps == null) || (filterProps.size() == 0)) && + ((assocTypeQNames == null) || (assocTypeQNames.size() == 0)) && + (smartStore.isVirtual(parentNodeRef)|| (smartStore.canVirtualize(parentNodeRef)))) + { + pagingResults = fileFolderService.list(parentNodeRef, searchTypeQNames, ignoreAspectQNames, sortProps, pagingRequest); + } + else + { + // TODO smart folders (see REPO-1173) + pagingResults = fileFolderService.list(parentNodeRef, assocTypeQNames, searchTypeQNames, ignoreAspectQNames, sortProps, filterProps, pagingRequest); + } + + final Map mapUserInfo = new HashMap<>(10); + + final List page = pagingResults.getPage(); + List nodes = new AbstractList() + { + @Override + public Node get(int index) + { + FileInfo fInfo = page.get(index); + + // minimal info by default (unless "include"d otherwise) + // (pass in null as parentNodeRef to force loading of primary + // parent node as parentId) + Node node = getFolderOrDocument(fInfo.getNodeRef(), null, fInfo.getType(), includeParam, mapUserInfo); + if (node.getPath() != null) + { + calculateRelativePath(parentFolderNodeId, node); + } + return node; + } + + private void calculateRelativePath(String parentFolderNodeId, Node node) + { + NodeRef rootNodeRef = validateOrLookupNode(parentFolderNodeId, null); + try + { + // get the path elements + List pathInfos = fileFolderService.getNameOnlyPath(rootNodeRef, node.getNodeRef()); + + int sizePathInfos = pathInfos.size(); + + if (sizePathInfos > 1) + { + // remove the current child + pathInfos.remove(sizePathInfos - 1); + + // build the path string + StringBuilder sb = new StringBuilder(pathInfos.size() * 20); + for (String fileInfo : pathInfos) + { + sb.append("/"); + sb.append(fileInfo); + } + + node.getPath().setRelativePath(sb.toString()); + } + + } + + catch (FileNotFoundException e) + { + // NOTE: return null as relativePath + } + } + + @Override + public int size() + { + return page.size(); + } + }; + + Node sourceEntity = null; + if (parameters.includeSource()) + { + sourceEntity = getFolderOrDocumentFullInfo(parentNodeRef, null, null, null, mapUserInfo); + } + + return CollectionWithPagingInfo.asPaged(paging, nodes, pagingResults.hasMoreItems(), pagingResults.getTotalResultCount().getFirst(), sourceEntity); + } + + /** + * Create query walker for listChildren. + * + * @return The created {@link MapBasedQueryWalker}. + */ + private MapBasedQueryWalker createListChildrenQueryWalker() + { + return new MapBasedQueryWalker(LIST_FOLDER_CHILDREN_EQUALS_QUERY_PROPERTIES, null); + } + + /** + *

    Returns a List of filter properties specified by request parameters.

    + * + * @param parameters The {@link Parameters} object to get the parameters passed into the request + * including: + * - filter, sort & paging params (where, orderBy, skipCount, maxItems) + * - incFiles, incFolders (both true by default) + * @return The list of {@link FilterProp}. Can be null. + */ + protected List getListChildrenFilterProps(final Parameters parameters) + { + List filterProps = null; + Query q = parameters.getQuery(); + if (q != null) + { + MapBasedQueryWalker propertyWalker = createListChildrenQueryWalker(); + QueryHelper.walk(q, propertyWalker); + + Boolean isPrimary = propertyWalker.getProperty(PARAM_ISPRIMARY, WhereClauseParser.EQUALS, Boolean.class); + + if (isPrimary != null) + { + filterProps = new ArrayList<>(1); + filterProps.add(new FilterPropBoolean(GetChildrenCannedQuery.FILTER_QNAME_NODE_IS_PRIMARY, isPrimary)); + } + } + return filterProps; + } + + /** + *

    Returns a List of sort properties specified by the "sorting" request parameter.

    + * + * @param parameters The {@link Parameters} object to get the parameters passed into the request + * including: + * - filter, sort & paging params (where, orderBy, skipCount, maxItems) + * - incFiles, incFolders (both true by default) + * @return The list of Pair<QName, Boolean> sort properties. If no sort parameters are + * found defaults to {@link #getListChildrenSortPropsDefault() getListChildrenSortPropsDefault}. + */ + protected List> getListChildrenSortProps(final Parameters parameters) + { + List sortCols = parameters.getSorting(); + List> sortProps; + if ((sortCols != null) && (sortCols.size() > 0)) + { + // TODO should we allow isFile in sort (and map to reverse of isFolder) ? + sortProps = new ArrayList<>(sortCols.size()); + for (SortColumn sortCol : sortCols) + { + QName propQname = PARAM_SYNONYMS_QNAME.get(sortCol.column); + if (propQname == null) + { + propQname = createQName(sortCol.column); + } + + if (propQname != null) + { + sortProps.add(new Pair<>(propQname, sortCol.asc)); + } + } + } + else + { + sortProps = getListChildrenSortPropsDefault(); + } + + return sortProps; + } + + /** + *

    + * Returns the default sort order. + *

    + * + * @return The list of Pair<QName, Boolean> sort + * properties. + */ + protected List> getListChildrenSortPropsDefault() + { + List> sortProps = new ArrayList<>( + Arrays.asList(new Pair<>(GetChildrenCannedQuery.SORT_QNAME_NODE_IS_FOLDER, Boolean.FALSE), new Pair<>(ContentModel.PROP_NAME, true))); + return sortProps; + } + + private Pair parseNodeTypeFilter(String nodeTypeStr) + { + boolean filterIncludeSubTypes = false; // default nodeType filtering is without subTypes (unless nodeType value is suffixed with ' INCLUDESUBTYPES') + + int idx = nodeTypeStr.lastIndexOf(' '); + if (idx > 0) + { + String suffix = nodeTypeStr.substring(idx); + if (suffix.equalsIgnoreCase(" "+PARAM_INCLUDE_SUBTYPES)) + { + filterIncludeSubTypes = true; + nodeTypeStr = nodeTypeStr.substring(0, idx); + } + } + + QName filterNodeTypeQName = createQName(nodeTypeStr); + if (dictionaryService.getType(filterNodeTypeQName) == null) + { + throw new InvalidArgumentException("Unknown filter nodeType: "+nodeTypeStr); + } + + return new Pair<>(filterNodeTypeQName, filterIncludeSubTypes); + } + + protected Set buildAssocTypes(QName assocTypeQName) + { + Set assocTypeQNames = null; + if (assocTypeQName != null) + { + assocTypeQNames = Collections.singleton(assocTypeQName); + } + /* + // TODO review - this works, but reduces from ~100 to ~96 (OOTB) + // maybe we could post filter (rather than join) - examples: sys:children, sys:lost_found, sys:archivedLink, sys:archiveUserLink + else + { + Collection qnames = dictionaryService.getAllAssociations(); + assocTypeQNames = new HashSet<>(qnames.size()); + + // remove system assoc types + for (QName qname : qnames) + { + if ((!EXCLUDED_NS.contains(qname.getNamespaceURI()))) + { + assocTypeQNames.add(qname); + } + } + } + */ + return assocTypeQNames; + } + + protected Pair, Set> buildSearchTypesAndIgnoreAspects(QName nodeTypeQName, boolean includeSubTypes, Set ignoreQNameTypes, Boolean includeFiles, Boolean includeFolders) + { + Set searchTypeQNames = new HashSet<>(100); + Set ignoreAspectQNames = null; + + if (nodeTypeQName != null) + { + // Build a list of (sub-)types + if (includeSubTypes) + { + Collection qnames = dictionaryService.getSubTypes(nodeTypeQName, true); + searchTypeQNames.addAll(qnames); + } + searchTypeQNames.add(nodeTypeQName); + + // Remove 'system' folders + if (includeSubTypes) + { + Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_SYSTEM_FOLDER, true); + searchTypeQNames.removeAll(qnames); + } + searchTypeQNames.remove(ContentModel.TYPE_SYSTEM_FOLDER); + } + + if (includeFiles != null) + { + if (includeFiles) + { + if (includeSubTypes) + { + Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true); + searchTypeQNames.addAll(qnames); + } + searchTypeQNames.add(ContentModel.TYPE_CONTENT); + } + else + { + Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true); + searchTypeQNames.removeAll(qnames); + searchTypeQNames.remove(ContentModel.TYPE_CONTENT); + } + } + + if (includeFolders != null) + { + if (includeFolders) + { + if (includeSubTypes) + { + Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_FOLDER, true); + searchTypeQNames.addAll(qnames); + } + searchTypeQNames.add(ContentModel.TYPE_FOLDER); + + // Remove 'system' folders + if (includeSubTypes) + { + Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_SYSTEM_FOLDER, true); + searchTypeQNames.removeAll(qnames); + } + searchTypeQNames.remove(ContentModel.TYPE_SYSTEM_FOLDER); + } + else + { + Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_FOLDER, true); + searchTypeQNames.removeAll(qnames); + searchTypeQNames.remove(ContentModel.TYPE_FOLDER); + } + } + + if (ignoreQNameTypes != null) + { + Set ignoreQNamesNotSearchTypes = new HashSet<>(ignoreQNameTypes); + ignoreQNamesNotSearchTypes.removeAll(searchTypeQNames); + ignoreQNamesNotSearchTypes.remove(ContentModel.TYPE_SYSTEM_FOLDER); + + if (ignoreQNamesNotSearchTypes.size() > 0) + { + ignoreAspectQNames = getAspectsToIgnore(ignoreQNamesNotSearchTypes); + } + + searchTypeQNames.removeAll(ignoreQNameTypes); + } + + return new Pair<>(searchTypeQNames, ignoreAspectQNames); + } + + protected Pair, Set> buildSearchTypesAndIgnoreAspects(final Parameters parameters) + { + // filters + Boolean includeFolders = null; + Boolean includeFiles = null; + QName filterNodeTypeQName = null; + + // note: for files/folders, include subtypes by default (unless filtering by a specific nodeType - see below) + boolean filterIncludeSubTypes = true; + + Query q = parameters.getQuery(); + + if (q != null) + { + // filtering via "where" clause + MapBasedQueryWalker propertyWalker = createListChildrenQueryWalker(); + QueryHelper.walk(q, propertyWalker); + + Boolean isFolder = propertyWalker.getProperty(PARAM_ISFOLDER, WhereClauseParser.EQUALS, Boolean.class); + Boolean isFile = propertyWalker.getProperty(PARAM_ISFILE, WhereClauseParser.EQUALS, Boolean.class); + + if (isFolder != null) + { + includeFolders = isFolder; + } + + if (isFile != null) + { + includeFiles = isFile; + } + + if (Boolean.TRUE.equals(includeFiles) && Boolean.TRUE.equals(includeFolders)) + { + throw new InvalidArgumentException("Invalid filter (isFile=true and isFolder=true) - a node cannot be both a file and a folder"); + } + + String nodeTypeStr = propertyWalker.getProperty(PARAM_NODETYPE, WhereClauseParser.EQUALS, String.class); + if ((nodeTypeStr != null) && (! nodeTypeStr.isEmpty())) + { + if ((isFile != null) || (isFolder != null)) + { + throw new InvalidArgumentException("Invalid filter - nodeType and isFile/isFolder are mutually exclusive"); + } + + Pair pair = parseNodeTypeFilter(nodeTypeStr); + filterNodeTypeQName = pair.getFirst(); + filterIncludeSubTypes = pair.getSecond(); + } + } + + // notes (see also earlier validation checks): + // - no filtering means any types/sub-types (well, apart from hidden &/or default ignored types - eg. systemfolder, fm types) + // - node type filtering is mutually exclusive from isFile/isFolder, can optionally also include sub-types + // - isFile & isFolder cannot both be true + // - (isFile=false) means any other types/sub-types (other than files) + // - (isFolder=false) means any other types/sub-types (other than folders) + // - (isFile=false and isFolder=false) means any other types/sub-types (other than files or folders) + + if (filterNodeTypeQName == null) + { + if ((includeFiles == null) && (includeFolders == null)) + { + // no additional filtering + filterNodeTypeQName = ContentModel.TYPE_CMOBJECT; + } + else if ((includeFiles != null) && (includeFolders != null)) + { + if ((! includeFiles) && (! includeFolders)) + { + // no files or folders + filterNodeTypeQName = ContentModel.TYPE_CMOBJECT; + } + } + else if ((includeFiles != null) && (! includeFiles)) + { + // no files + filterNodeTypeQName = ContentModel.TYPE_CMOBJECT; + } + else if ((includeFolders != null) && (! includeFolders)) + { + // no folders + filterNodeTypeQName = ContentModel.TYPE_CMOBJECT; + } + } + + return buildSearchTypesAndIgnoreAspects(filterNodeTypeQName, filterIncludeSubTypes, ignoreQNames, includeFiles, includeFolders); + } + + private Set getAspectsToIgnore(Set ignoreQNames) + { + Set ignoreQNameAspects = new HashSet<>(ignoreQNames.size()); + for (QName qname : ignoreQNames) + { + if (dictionaryService.getAspect(qname) != null) + { + ignoreQNameAspects.add(qname); + } + } + return ignoreQNameAspects; + } + + @Override + public void deleteNode(String nodeId, Parameters parameters) + { + NodeRef nodeRef = validateOrLookupNode(nodeId, null); + + if (isSpecialNode(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Cannot delete: " + nodeId); + } + + // default false (if not provided) + boolean permanentDelete = Boolean.valueOf(parameters.getParameter(PARAM_PERMANENT)); + + if (permanentDelete == true) + { + boolean isAdmin = authorityService.hasAdminAuthority(); + if (! isAdmin) + { + String owner = ownableService.getOwner(nodeRef); + if (! AuthenticationUtil.getRunAsUser().equals(owner)) + { + // non-owner/non-admin cannot permanently delete (even if they have delete permission) + throw new PermissionDeniedException("Non-owner/non-admin cannot permanently delete: " + nodeId); + } + } + + // Set as temporary to delete node instead of archiving. + nodeService.addAspect(nodeRef, ContentModel.ASPECT_TEMPORARY, null); + } + + final ActivityInfo activityInfo = getActivityInfo(getParentNodeRef(nodeRef), nodeRef); + postActivity(Activity_Type.DELETED, activityInfo, true); + + fileFolderService.delete(nodeRef); + } + + @Override + public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters) + { + if (nodeInfo.getNodeRef() != null) + { + throw new InvalidArgumentException("Unexpected id when trying to create a new node: "+nodeInfo.getNodeRef().getId()); + } + validateAspects(nodeInfo.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); + validateProperties(nodeInfo.getProperties(), EXCLUDED_NS, Arrays.asList()); + + // check that requested parent node exists and it's type is a (sub-)type of folder + NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); + + // node name - mandatory + String nodeName = nodeInfo.getName(); + if ((nodeName == null) || nodeName.isEmpty()) + { + throw new InvalidArgumentException("Node name is expected: "+parentNodeRef.getId()); + } + + // node type - check that requested type is a (sub-) type of cm:object + String nodeType = nodeInfo.getNodeType(); + if ((nodeType == null) || nodeType.isEmpty()) + { + throw new InvalidArgumentException("Node type is expected: "+parentNodeRef.getId()+","+nodeName); + } + + QName nodeTypeQName = createQName(nodeType); + + boolean isContent = isSubClass(nodeTypeQName, ContentModel.TYPE_CONTENT); + if (! isContent) + { + validateCmObject(nodeTypeQName); + } + + /* RA-834: commented-out since not currently applicable for empty file + List thumbnailDefs = null; + String renditionsParam = parameters.getParameter(PARAM_RENDITIONS); + if (renditionsParam != null) + { + if (!isContent) + { + throw new InvalidArgumentException("Renditions ['"+renditionsParam+"'] only apply to content types: "+parentNodeRef.getId()+","+nodeName); + } + + thumbnailDefs = getThumbnailDefs(renditionsParam); + } + */ + + Map props = new HashMap<>(1); + + if (nodeInfo.getProperties() != null) + { + // node properties - set any additional properties + props = mapToNodeProperties(nodeInfo.getProperties()); + } + + // Optionally, lookup by relative path + String relativePath = nodeInfo.getRelativePath(); + parentNodeRef = getOrCreatePath(parentNodeRef, relativePath); + + // Existing file/folder name handling + boolean autoRename = Boolean.valueOf(parameters.getParameter(PARAM_AUTO_RENAME)); + if (autoRename && (isContent || isSubClass(nodeTypeQName, ContentModel.TYPE_FOLDER))) + { + NodeRef existingNode = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, nodeName); + if (existingNode != null) + { + // File already exists, find a unique name + nodeName = findUniqueName(parentNodeRef, nodeName); + } + } + + QName assocTypeQName = ContentModel.ASSOC_CONTAINS; + if ((nodeInfo.getAssociation() != null) && (nodeInfo.getAssociation().getAssocType() != null)) + { + assocTypeQName = getAssocType(nodeInfo.getAssociation().getAssocType()); + } + + Boolean versionMajor = null; + String str = parameters.getParameter(PARAM_VERSION_MAJOR); + if (str != null) + { + versionMajor = Boolean.valueOf(str); + } + String versionComment = parameters.getParameter(PARAM_VERSION_COMMENT); + + // Create the node + NodeRef nodeRef; + + if (isContent) + { + // create empty file node - note: currently will be set to default encoding only (UTF-8) + nodeRef = createNewFile(parentNodeRef, nodeName, nodeTypeQName, null, props, assocTypeQName, parameters, versionMajor, versionComment); + } + else + { + // create non-content node + nodeRef = createNodeImpl(parentNodeRef, nodeName, nodeTypeQName, props, assocTypeQName); + } + + addCustomAspects(nodeRef, nodeInfo.getAspectNames(), EXCLUDED_ASPECTS); + + processNodePermissions(nodeRef, nodeInfo); + + // eg. to create mandatory assoc(s) + + if (nodeInfo.getTargets() != null) + { + addTargets(nodeRef.getId(), nodeInfo.getTargets()); + } + + if (nodeInfo.getSecondaryChildren() != null) + { + addChildren(nodeRef.getId(), nodeInfo.getSecondaryChildren()); + } + + Node newNode = getFolderOrDocument(nodeRef.getId(), parameters); + + /* RA-834: commented-out since not currently applicable for empty file + requestRenditions(thumbnailDefs, newNode); // note: noop for folder + */ + + return newNode; + } + + public void addCustomAspects(NodeRef nodeRef, List aspectNames, List excludedAspects) + { + if (aspectNames == null) + { + return; + } + // node aspects - set any additional aspects + Set aspectQNames = mapToNodeAspects(aspectNames); + for (QName aspectQName : aspectQNames) + { + if (excludedAspects.contains(aspectQName) || aspectQName.equals(ContentModel.ASPECT_AUDITABLE)) + { + continue; // ignore + } + + nodeService.addAspect(nodeRef, aspectQName, null); + } + } + + private NodeRef getOrCreatePath(NodeRef parentNodeRef, String relativePath) + { + if (relativePath != null) + { + List pathElements = getPathElements(relativePath); + + // Checks for the presence of, and creates as necessary, + // the folder structure in the provided path elements list. + if (!pathElements.isEmpty()) + { + parentNodeRef = makeFolders(parentNodeRef, pathElements); + } + } + + return parentNodeRef; + } + + public List addChildren(String parentNodeId, List entities) + { + NodeRef parentNodeRef = validateNode(parentNodeId); + + List result = new ArrayList<>(entities.size()); + + for (AssocChild assoc : entities) + { + String childId = assoc.getChildId(); + if (childId == null) + { + throw new InvalidArgumentException("Missing childId"); + } + + QName assocTypeQName = getAssocType(assoc.getAssocType()); + + try + { + NodeRef childNodeRef = validateNode(childId); + + String nodeName = (String)nodeService.getProperty(childNodeRef, ContentModel.PROP_NAME); + QName assocChildQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(nodeName)); + + nodeService.addChild(parentNodeRef, childNodeRef, assocTypeQName, assocChildQName); + } + catch (AssociationExistsException aee) + { + throw new ConstraintViolatedException(aee.getMessage()); + } + catch (DuplicateChildNodeNameException dcne) + { + throw new ConstraintViolatedException(dcne.getMessage()); + } + + result.add(assoc); + } + + return result; + } + + public List addTargets(String sourceNodeId, List entities) + { + List result = new ArrayList<>(entities.size()); + + NodeRef srcNodeRef = validateNode(sourceNodeId); + + for (AssocTarget assoc : entities) + { + String targetNodeId = assoc.getTargetId(); + if (targetNodeId == null) + { + throw new InvalidArgumentException("Missing targetId"); + } + + String assocTypeStr = assoc.getAssocType(); + QName assocTypeQName = getAssocType(assocTypeStr); + try + { + NodeRef tgtNodeRef = validateNode(targetNodeId); + nodeService.createAssociation(srcNodeRef, tgtNodeRef, assocTypeQName); + } + catch (AssociationExistsException aee) + { + throw new ConstraintViolatedException("Node association '"+assocTypeStr+"' already exists from "+sourceNodeId+" to "+targetNodeId); + } + catch (IllegalArgumentException iae) + { + // note: for now, we assume it is invalid assocType - alternatively, we could attempt to pre-validate via dictionary.getAssociation + throw new InvalidArgumentException(sourceNodeId+","+assocTypeStr+","+targetNodeId); + } + + result.add(assoc); + } + return result; + } + + public QName getAssocType(String assocTypeQNameStr) + { + return getAssocType(assocTypeQNameStr, true); + } + + public QName getAssocType(String assocTypeQNameStr, boolean mandatory) + { + QName assocType = null; + + if ((assocTypeQNameStr != null) && (! assocTypeQNameStr.isEmpty())) + { + assocType = createQName(assocTypeQNameStr); + if (dictionaryService.getAssociation(assocType) == null) + { + throw new InvalidArgumentException("Unknown assocType: " + assocTypeQNameStr); + } + + if (EXCLUDED_NS.contains(assocType.getNamespaceURI())) + { + throw new InvalidArgumentException("Invalid assocType: " + assocTypeQNameStr); + } + } + + if (mandatory && (assocType == null)) + { + throw new InvalidArgumentException("Missing assocType"); + } + + return assocType; + } + + + private NodeRef createNodeImpl(NodeRef parentNodeRef, String nodeName, QName nodeTypeQName, Map props, QName assocTypeQName) + { + NodeRef newNode = null; + if (props == null) + { + props = new HashMap<>(1); + } + props.put(ContentModel.PROP_NAME, nodeName); + + validatePropValues(props); + + QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(nodeName)); + try + { + newNode = nodeService.createNode(parentNodeRef, assocTypeQName, assocQName, nodeTypeQName, props).getChildRef(); + } + catch (DuplicateChildNodeNameException dcne) + { + // duplicate - name clash + throw new ConstraintViolatedException(dcne.getMessage()); + } + + ActivityInfo activityInfo = getActivityInfo(parentNodeRef, newNode); + postActivity(Activity_Type.ADDED, activityInfo, false); + return newNode; + } + + /** + * Posts activities based on the activity_type. + * If the method is called with aSync=true then a TransactionListener is used post the activity + * afterCommit. Otherwise the activity posting is done synchronously. + * @param activity_type + * @param activityInfo + * @param aSync + */ + protected void postActivity(Activity_Type activity_type, ActivityInfo activityInfo, boolean aSync) + { + if (activityInfo == null) return; //Nothing to do. + + String activityType = determineActivityType(activity_type, activityInfo.getFileInfo().isFolder()); + if (activityType != null) + { + if (aSync) + { + ActivitiesTransactionListener txListener = new ActivitiesTransactionListener(activityType, activityInfo, + TenantUtil.getCurrentDomain(), Activities.APP_TOOL, Activities.RESTAPI_CLIENT, + poster, retryingTransactionHelper); + AlfrescoTransactionSupport.bindListener(txListener); + } + else + { + poster.postFileFolderActivity(activityType, null, TenantUtil.getCurrentDomain(), + activityInfo.getSiteId(), activityInfo.getParentNodeRef(), activityInfo.getNodeRef(), + activityInfo.getFileName(), Activities.APP_TOOL, Activities.RESTAPI_CLIENT, + activityInfo.getFileInfo()); + } + } + } + + // note: see also org.alfresco.opencmis.ActivityPosterImpl + protected ActivityInfo getActivityInfo(NodeRef parentNodeRef, NodeRef nodeRef) + { + // runAs system, eg. user may not have permission see one or more parents (irrespective of whether in a site context of not) + SiteInfo siteInfo = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public SiteInfo doWork() throws Exception + { + return siteService.getSite(nodeRef); + } + }, AuthenticationUtil.getSystemUserName()); + + String siteId = (siteInfo != null ? siteInfo.getShortName() : null); + if(siteId != null && !siteId.equals("")) + { + FileInfo fileInfo = fileFolderService.getFileInfo(nodeRef); + if (fileInfo != null) + { + boolean isContent = isSubClass(fileInfo.getType(), ContentModel.TYPE_CONTENT); + + if (fileInfo.isFolder() || isContent) + { + return new ActivityInfo(null, parentNodeRef, siteId, fileInfo); + } + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Non-site activity, so ignored " + nodeRef); + } + } + return null; + } + + protected static String determineActivityType(Activity_Type activity_type, boolean isFolder) + { + switch (activity_type) + { + case DELETED: + return isFolder ? ActivityType.FOLDER_DELETED:ActivityType.FILE_DELETED; + case ADDED: + return isFolder ? ActivityType.FOLDER_ADDED:ActivityType.FILE_ADDED; + case UPDATED: + if (!isFolder) return ActivityType.FILE_UPDATED; + break; + case DOWNLOADED: + if (!isFolder) return ActivityPoster.DOWNLOADED; + break; + } + return null; + } + + // check cm:cmobject (but *not* cm:systemfolder) + private void validateCmObject(QName nodeTypeQName) + { + if (! isSubClass(nodeTypeQName, ContentModel.TYPE_CMOBJECT)) + { + throw new InvalidArgumentException("Invalid type: " + nodeTypeQName + " - expected (sub-)type of cm:cmobject"); + } + + if (isSubClass(nodeTypeQName, ContentModel.TYPE_SYSTEM_FOLDER)) + { + throw new InvalidArgumentException("Invalid type: " + nodeTypeQName + " - cannot be (sub-)type of cm:systemfolder"); + } + } + + // special cases: additional validation of property values (if not done by underlying foundation services) + private void validatePropValues(Map props) + { + String newOwner = (String)props.get(ContentModel.PROP_OWNER); + if (newOwner != null) + { + // validate that user exists + if (! personService.personExists(newOwner)) + { + throw new InvalidArgumentException("Unknown owner: "+newOwner); + } + } + } + + + /** + * Check for special case: additional node validation (pending common lower-level service support) + * for blacklist of system nodes that should not be deleted or locked, eg. Company Home, Sites, Data Dictionary + * + * @param nodeRef + * @param type + * @return + */ + protected boolean isSpecialNode(NodeRef nodeRef, QName type) + { + // Check for Company Home, Sites and Data Dictionary (note: must be tenant-aware) + + if (nodeRef.equals(repositoryHelper.getCompanyHome())) + { + return true; + } + else if (type.equals(SiteModel.TYPE_SITES) || type.equals(SiteModel.TYPE_SITE)) + { + // note: alternatively, we could inject SiteServiceInternal and use getSitesRoot (or indirectly via node locator) + return true; + } + else + { + String tenantDomain = TenantUtil.getCurrentDomain(); + NodeRef ddNodeRef = ddCache.get(tenantDomain); + if (ddNodeRef == null) + { + List ddAssocs = nodeService.getChildAssocs( + repositoryHelper.getCompanyHome(), + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "dictionary")); + if (ddAssocs.size() == 1) + { + ddNodeRef = ddAssocs.get(0).getChildRef(); + ddCache.put(tenantDomain, ddNodeRef); + } + } + + if (nodeRef.equals(ddNodeRef)) + { + return true; + } + } + + return false; + } + + private boolean isLocked(NodeRef nodeRef, Set aspects) + { + boolean locked = false; + if (((aspects != null) && aspects.contains(ContentModel.ASPECT_LOCKABLE)) + || nodeService.hasAspect(nodeRef, ContentModel.ASPECT_LOCKABLE)) + { + locked = lockService.isLocked(nodeRef); + } + + return locked; + } + + @Override + public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + NodeRef nodeRef = updateNodeImpl(nodeId, nodeInfo, parameters); + ActivityInfo activityInfo = getActivityInfo(getParentNodeRef(nodeRef), nodeRef); + postActivity(Activity_Type.UPDATED, activityInfo, false); + + return null; + } + }, false, true); + + return retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Node execute() throws Throwable + { + return getFolderOrDocument(nodeId, parameters); + } + }, false, false); + } + + protected NodeRef updateNodeImpl(String nodeId, Node nodeInfo, Parameters parameters) + { + validateAspects(nodeInfo.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); + validateProperties(nodeInfo.getProperties(), EXCLUDED_NS, Arrays.asList()); + + final NodeRef nodeRef = validateOrLookupNode(nodeId, null); + + QName nodeTypeQName = getNodeType(nodeRef); + + validateCmObject(nodeTypeQName); + + Map props = new HashMap<>(0); + + if (nodeInfo.getProperties() != null) + { + props = mapToNodeProperties(nodeInfo.getProperties()); + } + + String name = nodeInfo.getName(); + if ((name != null) && (! name.isEmpty())) + { + // update node name if needed - note: if the name is different than existing then this is equivalent of a rename (within parent folder) + props.put(ContentModel.PROP_NAME, name); + } + + String nodeType = nodeInfo.getNodeType(); + if ((nodeType != null) && (! nodeType.isEmpty())) + { + // update node type - ensure that we are performing a specialise (we do not support generalise) + QName destNodeTypeQName = createQName(nodeType); + + if ((! destNodeTypeQName.equals(nodeTypeQName)) && + isSubClass(destNodeTypeQName, nodeTypeQName) && + (! isSubClass(destNodeTypeQName, ContentModel.TYPE_SYSTEM_FOLDER))) + { + nodeService.setType(nodeRef, destNodeTypeQName); + } + else if (! destNodeTypeQName.equals(nodeTypeQName)) + { + throw new InvalidArgumentException("Failed to change (specialise) node type - from "+nodeTypeQName+" to "+destNodeTypeQName); + } + } + + NodeRef parentNodeRef = nodeInfo.getParentId(); + if (parentNodeRef != null) + { + NodeRef currentParentNodeRef = getParentNodeRef(nodeRef); + if (currentParentNodeRef == null) + { + // implies root (Company Home) hence return 403 here + throw new PermissionDeniedException(); + } + + if (! currentParentNodeRef.equals(parentNodeRef)) + { + //moveOrCopy(nodeRef, parentNodeRef, name, false); // not currently supported - client should use explicit POST /move operation instead + throw new InvalidArgumentException("Cannot update parentId of "+nodeId+" via PUT /nodes/{nodeId}. Please use explicit POST /nodes/{nodeId}/move operation instead"); + } + } + + List aspectNames = nodeInfo.getAspectNames(); + updateCustomAspects(nodeRef, aspectNames, EXCLUDED_ASPECTS); + + if (props.size() > 0) + { + validatePropValues(props); + + try + { + handleNodeRename(props, nodeRef); + // update node properties - note: null will unset the specified property + nodeService.addProperties(nodeRef, props); + } + catch (DuplicateChildNodeNameException dcne) + { + throw new ConstraintViolatedException(dcne.getMessage()); + } + } + + processNodePermissions(nodeRef, nodeInfo); + + return nodeRef; + } + + private void handleNodeRename(Map props, NodeRef nodeRef) + { + Serializable nameProp = props.get(ContentModel.PROP_NAME); + if ((nameProp != null)) + { + String currentName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + String newName = (String) nameProp; + if (!currentName.equals(newName)) + { + rename(nodeRef, newName); + } + } + } + + protected void processNodePermissions(NodeRef nodeRef, Node nodeInfo) + { + NodePermissions nodePerms = nodeInfo.getPermissions(); + if (nodePerms != null) + { + // Cannot set inherited permissions, only direct (locally set) permissions can be set + if ((nodePerms.getInherited() != null) && (nodePerms.getInherited().size() > 0)) + { + throw new InvalidArgumentException("Cannot set *inherited* permissions on this node"); + } + + // Check inherit from parent value and if it's changed set the new value + if (nodePerms.getIsInheritanceEnabled() != null) + { + if (nodePerms.getIsInheritanceEnabled() != permissionService.getInheritParentPermissions(nodeRef)) + { + permissionService.setInheritParentPermissions(nodeRef, nodePerms.getIsInheritanceEnabled()); + } + } + + // set direct permissions + if ((nodePerms.getLocallySet() != null)) + { + // list of all directly set permissions + Set directPerms = new HashSet<>(5); + for (AccessPermission accessPerm : permissionService.getAllSetPermissions(nodeRef)) + { + if (accessPerm.isSetDirectly()) + { + directPerms.add(accessPerm); + } + } + + // + // replace (or clear) set of direct permissions + // + + // TODO cleanup the way we replace permissions (ie. add, update and delete) + + // check if same permission is sent more than once + if (hasDuplicatePermissions(nodePerms.getLocallySet())) + { + throw new InvalidArgumentException("Duplicate node permissions, there is more than one permission with the same authority and name!"); + } + + for (NodePermissions.NodePermission nodePerm : nodePerms.getLocallySet()) + { + String permName = nodePerm.getName(); + String authorityId = nodePerm.getAuthorityId(); + + AccessStatus accessStatus = AccessStatus.ALLOWED; + if (nodePerm.getAccessStatus() != null) + { + accessStatus = AccessStatus.valueOf(nodePerm.getAccessStatus()); + } + + if (authorityId == null || authorityId.isEmpty()) + { + throw new InvalidArgumentException("Authority Id is expected."); + } + + if (permName == null || permName.isEmpty()) + { + throw new InvalidArgumentException("Permission name is expected."); + } + + if (((!authorityId.equals(PermissionService.ALL_AUTHORITIES) && (!authorityService.authorityExists(authorityId))))) + { + throw new InvalidArgumentException("Cannot set permissions on this node - unknown authority: " + authorityId); + } + + AccessPermission existing = null; + boolean addPerm = true; + boolean updatePerm = false; + + // If the permission already exists but with different access status it will be updated + for (AccessPermission accessPerm : directPerms) + { + if (accessPerm.getAuthority().equals(authorityId) && accessPerm.getPermission().equals(permName)) + { + existing = accessPerm; + addPerm = false; + + if (accessPerm.getAccessStatus() != accessStatus) + { + updatePerm = true; + } + break; + } + } + + if (existing != null) + { + // ignore existing permissions + directPerms.remove(existing); + } + + if (addPerm || updatePerm) + { + try + { + permissionService.setPermission(nodeRef, authorityId, permName, (accessStatus == AccessStatus.ALLOWED)); + } + catch (UnsupportedOperationException e) + { + throw new InvalidArgumentException("Cannot set permissions on this node - unknown access level: " + permName); + } + } + } + + // remove any remaining direct perms + for (AccessPermission accessPerm : directPerms) + { + permissionService.deletePermission(nodeRef, accessPerm.getAuthority(), accessPerm.getPermission()); + } + } + } + } + + @Override + public Node moveOrCopyNode(String sourceNodeId, String targetParentId, String name, Parameters parameters, boolean isCopy) + { + if ((sourceNodeId == null) || (sourceNodeId.isEmpty())) + { + throw new InvalidArgumentException("Missing sourceNodeId"); + } + + if ((targetParentId == null) || (targetParentId.isEmpty())) + { + throw new InvalidArgumentException("Missing targetParentId"); + } + + final NodeRef parentNodeRef = validateOrLookupNode(targetParentId, null); + final NodeRef sourceNodeRef = validateOrLookupNode(sourceNodeId, null); + + FileInfo fi = moveOrCopyImpl(sourceNodeRef, parentNodeRef, name, isCopy); + return getFolderOrDocument(fi.getNodeRef().getId(), parameters); + } + + public void updateCustomAspects(NodeRef nodeRef, List aspectNames, List excludedAspects) + { + if (aspectNames != null) + { + // update aspects - note: can be empty (eg. to remove existing aspects+properties) but not cm:auditable, sys:referencable, sys:localized + + Set aspectQNames = mapToNodeAspects(aspectNames); + + Set existingAspects = nodeService.getAspects(nodeRef); + + Set aspectsToAdd = new HashSet<>(3); + Set aspectsToRemove = new HashSet<>(3); + + for (QName aspectQName : aspectQNames) + { + if (EXCLUDED_NS.contains(aspectQName.getNamespaceURI()) || excludedAspects.contains(aspectQName) || aspectQName.equals(ContentModel.ASPECT_AUDITABLE)) + { + continue; // ignore + } + + if (! existingAspects.contains(aspectQName)) + { + aspectsToAdd.add(aspectQName); + } + } + + for (QName existingAspect : existingAspects) + { + if (EXCLUDED_NS.contains(existingAspect.getNamespaceURI()) || excludedAspects.contains(existingAspect) || existingAspect.equals(ContentModel.ASPECT_AUDITABLE)) + { + continue; // ignore + } + + if (! aspectQNames.contains(existingAspect)) + { + aspectsToRemove.add(existingAspect); + } + } + + // Note: for now, if aspectNames are sent then all that are required should be sent (to avoid properties from other existing aspects being removed) + // TODO: optional PATCH mechanism to add one new new aspect (with some related aspect properties) without affecting existing aspects/properties + for (QName aQName : aspectsToRemove) + { + if (aQName.equals(QuickShareModel.ASPECT_QSHARE)) + { + String qSharedId = (String)nodeService.getProperty(nodeRef, QuickShareModel.PROP_QSHARE_SHAREDID); + if (qSharedId != null) + { + // note: for now, go via QuickShareLinks (rather than QuickShareService) to ensure consistent permission checks + // alternatively we could disallow (or ignore) "qshare:shared" aspect removal + quickShareLinks.delete(qSharedId, null); + } + } + + nodeService.removeAspect(nodeRef, aQName); + } + + for (QName aQName : aspectsToAdd) + { + if (aQName.equals(QuickShareModel.ASPECT_QSHARE)) + { + // note: for now, go via QuickShareLinks (rather than QuickShareService) to ensure consistent permission checks + // alternatively we could disallow (or ignore) "qshare:shared" aspect addition + QuickShareLink qs = new QuickShareLink(); + qs.setNodeId(nodeRef.getId()); + quickShareLinks.create(Collections.singletonList(qs), null); + } + + nodeService.addAspect(nodeRef, aQName, null); + } + } + } + + private void rename(NodeRef nodeRef, String name) + { + try + { + fileFolderService.rename(nodeRef, name); + } + catch (FileNotFoundException fnfe) + { + // convert checked exception + throw new EntityNotFoundException(nodeRef.getId()); + } + catch (FileExistsException fee) + { + // duplicate - name clash + throw new ConstraintViolatedException("Name already exists in target parent: " + name); + } + } + + protected FileInfo moveOrCopyImpl(NodeRef nodeRef, NodeRef parentNodeRef, String name, boolean isCopy) + { + String targetParentId = parentNodeRef.getId(); + + try + { + if (isCopy) + { + // copy + FileInfo newFileInfo = fileFolderService.copy(nodeRef, parentNodeRef, name); + if (newFileInfo.getNodeRef().equals(nodeRef)) + { + // copy did not happen - eg. same parent folder and name (name can be null or same) + throw new FileExistsException(nodeRef, ""); + } + return newFileInfo; + } + else + { + // move + if ((! nodeRef.equals(parentNodeRef)) && isSpecialNode(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Cannot move: "+nodeRef.getId()); + } + + // updating "parentId" means moving primary parent ! + // note: in the future (as and when we support secondary parent/child assocs) we may also + // wish to select which parent to "move from" (in case where the node resides in multiple locations) + return fileFolderService.move(nodeRef, parentNodeRef, name); + } + } + catch (InvalidNodeRefException inre) + { + throw new EntityNotFoundException(targetParentId); + } + catch (FileNotFoundException fnfe) + { + // convert checked exception + throw new EntityNotFoundException(targetParentId); + } + catch (FileExistsException fee) + { + // duplicate - name clash + throw new ConstraintViolatedException("Name already exists in target parent: "+name); + } + catch (FileFolderServiceImpl.InvalidTypeException ite) + { + throw new InvalidArgumentException("Invalid type of target parent: "+targetParentId); + } + } + + @Override + public BinaryResource getContent(String fileNodeId, Parameters parameters, boolean recordActivity) + { + final NodeRef nodeRef = validateNode(fileNodeId); + return getContent(nodeRef, parameters, recordActivity); + } + + @Override + public BinaryResource getContent(NodeRef nodeRef, Parameters parameters, boolean recordActivity) + { + if (!nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) + { + throw new InvalidArgumentException("NodeId of content is expected: " + nodeRef.getId()); + } + + Map nodeProps = nodeService.getProperties(nodeRef); + ContentData cd = (ContentData) nodeProps.get(ContentModel.PROP_CONTENT); + String name = (String) nodeProps.get(ContentModel.PROP_NAME); + + org.alfresco.rest.framework.resource.content.ContentInfo ci = null; + String mimeType = null; + if (cd != null) + { + mimeType = cd.getMimetype(); + ci = new org.alfresco.rest.framework.resource.content.ContentInfoImpl(mimeType, cd.getEncoding(), cd.getSize(), cd.getLocale()); + } + + // By default set attachment header (with filename) unless attachment=false *and* content type is pre-configured as non-attach + boolean attach = true; + String attachment = parameters.getParameter("attachment"); + if (attachment != null) + { + Boolean a = Boolean.valueOf(attachment); + if (!a) + { + if (nonAttachContentTypes.contains(mimeType)) + { + attach = false; + } + else + { + logger.warn("Ignored attachment=false for "+nodeRef.getId()+" since "+mimeType+" is not in the whitelist for non-attach content types"); + } + } + } + String attachFileName = (attach ? name : null); + + if (recordActivity) + { + final ActivityInfo activityInfo = getActivityInfo(getParentNodeRef(nodeRef), nodeRef); + postActivity(Activity_Type.DOWNLOADED, activityInfo, true); + } + + return new NodeBinaryResource(nodeRef, ContentModel.PROP_CONTENT, ci, attachFileName); + } + + @Override + public Node updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) + { + if (contentInfo.getMimeType().toLowerCase().startsWith("multipart")) + { + throw new UnsupportedMediaTypeException("Cannot update using "+contentInfo.getMimeType()); + } + + final NodeRef nodeRef = validateNode(fileNodeId); + + if (! nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) + { + throw new InvalidArgumentException("NodeId of content is expected: " + nodeRef.getId()); + } + + Boolean versionMajor = null; + String str = parameters.getParameter(PARAM_VERSION_MAJOR); + if (str != null) + { + versionMajor = Boolean.valueOf(str); + } + String versionComment = parameters.getParameter(PARAM_VERSION_COMMENT); + + String fileName = parameters.getParameter(PARAM_NAME); + if (fileName != null) + { + // optionally rename, before updating the content + nodeService.setProperty(nodeRef, ContentModel.PROP_NAME, fileName); + } + else + { + fileName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + } + + return updateExistingFile(null, nodeRef, fileName, contentInfo, stream, parameters, versionMajor, versionComment); + } + + private Node updateExistingFile(NodeRef parentNodeRef, NodeRef nodeRef, String fileName, BasicContentInfo contentInfo, InputStream stream, Parameters parameters, Boolean versionMajor, String versionComment) + { + boolean isVersioned = versionService.isVersioned(nodeRef); + + behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + try + { + writeContent(nodeRef, fileName, stream, true); + + if ((isVersioned) || (versionMajor != null) || (versionComment != null) ) + { + VersionType versionType = null; + if (versionMajor != null) + { + versionType = (versionMajor ? VersionType.MAJOR : VersionType.MINOR); + } + else + { + // note: it is possible to have versionable aspect but no versions (=> no version label) + if ((! isVersioned) || (nodeService.getProperty(nodeRef, ContentModel.PROP_VERSION_LABEL) == null)) + { + versionType = VersionType.MAJOR; + } + else + { + versionType = VersionType.MINOR; + } + } + + createVersion(nodeRef, isVersioned, versionType, versionComment); + } + + ActivityInfo activityInfo = getActivityInfo(parentNodeRef, nodeRef); + postActivity(Activity_Type.UPDATED, activityInfo, false); + + extractMetadata(nodeRef); + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + } + + return getFolderOrDocumentFullInfo(nodeRef, null, null, parameters); + } + + private void writeContent(NodeRef nodeRef, String fileName, InputStream stream, boolean guessEncoding) + { + try + { + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + + String mimeType = mimetypeService.guessMimetype(fileName); + if ((mimeType != null) && (!mimeType.equals(MimetypeMap.MIMETYPE_BINARY))) + { + // quick/weak guess based on file extension + writer.setMimetype(mimeType); + } else + { + // stronger guess based on file stream + writer.guessMimetype(fileName); + } + + InputStream is = null; + + if (guessEncoding) + { + is = new BufferedInputStream(stream); + is.mark(1024); + writer.setEncoding(guessEncoding(is, mimeType, false)); + try + { + is.reset(); + } catch (IOException ioe) + { + if (logger.isWarnEnabled()) + { + logger.warn("Failed to reset stream after trying to guess encoding: " + ioe.getMessage()); + } + } + } else + { + is = stream; + } + + writer.putContent(is); + } + catch (ContentQuotaException cqe) + { + throw new InsufficientStorageException(); + } + catch (ContentLimitViolationException clv) + { + throw new RequestEntityTooLargeException(clv.getMessage()); + } + catch (ContentIOException cioe) + { + if (cioe.getCause() instanceof NodeLockedException) + { + throw (NodeLockedException)cioe.getCause(); + } + throw cioe; + } + } + + private String guessEncoding(InputStream in, String mimeType, boolean close) + { + String encoding = "UTF-8"; + try + { + if (in != null) + { + Charset charset = mimetypeService.getContentCharsetFinder().getCharset(in, mimeType); + encoding = charset.name(); + } + } + finally + { + try + { + if (close && (in != null)) + { + in.close(); + } + } + catch (IOException ioe) + { + if (logger.isWarnEnabled()) + { + logger.warn("Failed to close stream after trying to guess encoding: " + ioe.getMessage()); + } + } + } + return encoding; + } + + protected void createVersion(NodeRef nodeRef, boolean isVersioned, VersionType versionType, String reason) + { + if (!isVersioned) + { + nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, null); + } + + Map versionProperties = new HashMap<>(2); + versionProperties.put(VersionModel.PROP_VERSION_TYPE, versionType); + if (reason != null) + { + versionProperties.put(VersionModel.PROP_DESCRIPTION, reason); + } + + versionService.createVersion(nodeRef, versionProperties); + } + + @Override + public Node upload(String parentFolderNodeId, FormData formData, Parameters parameters) + { + if (formData == null || !formData.getIsMultiPart()) + { + throw new InvalidArgumentException("The request content-type is not multipart: "+parentFolderNodeId); + } + + NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); + if (!nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false)) + { + throw new InvalidArgumentException("NodeId of folder is expected: " + parentNodeRef.getId()); + } + + String fileName = null; + Content content = null; + boolean autoRename = false; + QName nodeTypeQName = ContentModel.TYPE_CONTENT; + boolean overwrite = false; // If a fileName clashes for a versionable file + Boolean versionMajor = null; + String versionComment = null; + String relativePath = null; + String renditionNames = null; + + Map qnameStrProps = new HashMap<>(); + Map properties = null; + Map formDataParameters = formData.getParameters(); + + for (FormData.FormField field : formData.getFields()) + { + switch (field.getName().toLowerCase()) + { + case "name": + String str = getStringOrNull(field.getValue()); + if ((str != null) && (! str.isEmpty())) + { + fileName = str; + } + break; + + case "filedata": + if (field.getIsFile()) + { + fileName = (fileName != null ? fileName : field.getFilename()); + content = field.getContent(); + } + break; + + case "autorename": + autoRename = Boolean.valueOf(field.getValue()); + break; + + case "nodetype": + nodeTypeQName = createQName(getStringOrNull(field.getValue())); + if (! isSubClass(nodeTypeQName, ContentModel.TYPE_CONTENT)) + { + throw new InvalidArgumentException("Can only upload type of cm:content: " + nodeTypeQName); + } + break; + + case "overwrite": + overwrite = Boolean.valueOf(field.getValue()); + break; + + case "majorversion": + versionMajor = Boolean.valueOf(field.getValue()); + break; + + case "comment": + versionComment = getStringOrNull(field.getValue()); + break; + + case "relativepath": + relativePath = getStringOrNull(field.getValue()); + break; + + case "renditions": + renditionNames = getStringOrNull(field.getValue()); + break; + + default: + { + final String propName = field.getName(); + if (propName.indexOf(QName.NAMESPACE_PREFIX) > -1 && !qnameStrProps.containsKey(propName)) + { + String[] fieldValue = formDataParameters.get(propName); + if (fieldValue.length > 1) + { + qnameStrProps.put(propName, Arrays.asList(fieldValue)); + } + else + { + qnameStrProps.put(propName, fieldValue[0]); + } + } + } + } + } + + // Ensure mandatory file attributes have been located. Need either + // destination, or site + container or updateNodeRef + if ((fileName == null) || fileName.isEmpty() || (content == null)) + { + throw new InvalidArgumentException("Required parameters are missing"); + } + + if (autoRename && overwrite) + { + throw new InvalidArgumentException("Both 'overwrite' and 'autoRename' should not be true when uploading a file"); + } + + // if requested, make (get or create) path + parentNodeRef = getOrCreatePath(parentNodeRef, relativePath); + final QName assocTypeQName = ContentModel.ASSOC_CONTAINS; + final Set renditions = getRequestedRenditions(renditionNames); + + validateProperties(qnameStrProps, EXCLUDED_NS, Arrays.asList()); + try + { + // Map the given properties, if any. + if (qnameStrProps.size() > 0) + { + properties = mapToNodeProperties(qnameStrProps); + } + + /* + * Existing file handling + */ + NodeRef existingFile = nodeService.getChildByName(parentNodeRef, assocTypeQName, fileName); + if (existingFile != null) + { + // File already exists, decide what to do + if (autoRename) + { + // attempt to find a unique name + fileName = findUniqueName(parentNodeRef, fileName); + + // drop-through ! + } + else if (overwrite && nodeService.hasAspect(existingFile, ContentModel.ASPECT_VERSIONABLE)) + { + // overwrite existing (versionable) file + BasicContentInfo contentInfo = new ContentInfoImpl(content.getMimetype(), content.getEncoding(), -1, null); + return updateExistingFile(parentNodeRef, existingFile, fileName, contentInfo, content.getInputStream(), parameters, versionMajor, versionComment); + } + else + { + // name clash (and no autoRename or overwrite) + throw new ConstraintViolatedException(fileName + " already exists."); + } + } + + // Note: pending REPO-159, we currently auto-enable versioning on new upload (but not when creating empty file) + if (versionMajor == null) + { + versionMajor = true; + } + + // Create a new file. + NodeRef nodeRef = createNewFile(parentNodeRef, fileName, nodeTypeQName, content, properties, assocTypeQName, parameters, versionMajor, versionComment); + + // Create the response + final Node fileNode = getFolderOrDocumentFullInfo(nodeRef, parentNodeRef, nodeTypeQName, parameters); + + checkRenditionNames(renditions); + requestRenditions(renditions, fileNode); + + return fileNode; + + // Do not clean formData temp files to allow for retries. + // Temp files will be deleted later when GC call DiskFileItem#finalize() method or by temp file cleaner. + } + catch (AccessDeniedException ade) + { + throw new PermissionDeniedException(ade.getMessage()); + } + + /* + * NOTE: Do not clean formData temp files to allow for retries. It's + * possible for a temp file to remain if max retry attempts are + * made, but this is rare, so leave to usual temp file cleanup. + */ + } + + private NodeRef createNewFile(NodeRef parentNodeRef, String fileName, QName nodeType, Content content, Map props, QName assocTypeQName, Parameters params, + Boolean versionMajor, String versionComment) + { + NodeRef nodeRef = createNodeImpl(parentNodeRef, fileName, nodeType, props, assocTypeQName); + + if (content == null) + { + // Write "empty" content + writeContent(nodeRef, fileName, new ByteArrayInputStream("".getBytes()), false); + } + else + { + // Write content + writeContent(nodeRef, fileName, content.getInputStream(), true); + } + + if ((versionMajor != null) || (versionComment != null)) + { + behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + try + { + // by default, first version is major, unless specified otherwise + VersionType versionType = VersionType.MAJOR; + if ((versionMajor != null) && (!versionMajor)) + { + versionType = VersionType.MINOR; + } + + createVersion(nodeRef, false, versionType, versionComment); + + extractMetadata(nodeRef); + } finally + { + behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + } + } + + return nodeRef; + } + + private String getStringOrNull(String value) + { + if (StringUtils.isNotEmpty(value)) + { + return value.equalsIgnoreCase("null") ? null : value; + } + return null; + } + + private void checkRenditionNames(Set renditionNames) + { + if (renditionNames != null) + { + if (!renditionService2.isEnabled()) + { + throw new DisabledServiceException("Thumbnail generation has been disabled."); + } + + RenditionDefinitionRegistry2 renditionDefinitionRegistry2 = renditionService2.getRenditionDefinitionRegistry2(); + for (String renditionName : renditionNames) + { + RenditionDefinition2 renditionDefinition = renditionDefinitionRegistry2.getRenditionDefinition(renditionName); + if (renditionDefinition == null) + { + throw new NotFoundException(renditionName + " is not registered."); + } + } + } + } + + static Set getRequestedRenditions(String renditionsParam) + { + if (renditionsParam == null) + { + return null; + } + + String[] renditionNames = renditionsParam.split(","); + + Set renditions = new LinkedHashSet<>(renditionNames.length); + for (String name : renditionNames) + { + name = name.trim(); + if (!name.isEmpty()) + { + renditions.add(name.trim()); + } + } + return renditions; + } + + private void requestRenditions(Set renditionNames, Node fileNode) + { + if (renditionNames != null) + { + NodeRef sourceNodeRef = fileNode.getNodeRef(); + String mimeType = fileNode.getContent().getMimeType(); + long size = fileNode.getContent().getSizeInBytes(); + RenditionDefinitionRegistry2 renditionDefinitionRegistry2 = renditionService2.getRenditionDefinitionRegistry2(); + Set availableRenditions = renditionDefinitionRegistry2.getRenditionNamesFrom(mimeType, size); + + for (String renditionName : renditionNames) + { + // RA-1052 (REPO-47) + try + { + // File may be to big + if (!availableRenditions.contains(renditionName)) + { + throw new InvalidArgumentException("Unable to create thumbnail '" + renditionName + "' for " + + mimeType + " as no transformer is currently available."); + } + + renditionService2.render(sourceNodeRef, renditionName); + } + catch (Exception ex) + { + // Note: The log level is not 'error' as it could easily fill out the log file. + if (logger.isDebugEnabled()) + { + // Don't throw the exception as we don't want the the upload to fail, just log it. + logger.debug("Asynchronous request to create a rendition upon upload failed: " + ex.getMessage()); + } + } + } + } + } + + /** + * Extracts the given node metadata asynchronously. + * + * The overwrite policy controls which if any parts of the document's properties are updated from this. + */ + private void extractMetadata(NodeRef nodeRef) + { + final String actionName = ContentMetadataExtracter.EXECUTOR_NAME; + ActionDefinition actionDef = actionService.getActionDefinition(actionName); + if (actionDef != null) + { + Action action = actionService.createAction(actionName); + actionService.executeAction(action, nodeRef); + } + } + + /** + * Creates a unique file name, if the upload component was configured to + * find a new unique name for clashing filenames. + * + * @param parentNodeRef the parent node + * @param fileName the original fileName + * @return a new file name + */ + private String findUniqueName(NodeRef parentNodeRef, String fileName) + { + int counter = 1; + String tmpFilename; + NodeRef existingFile; + do + { + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex == 0) + { + // File didn't have a proper 'name' instead it + // had just a suffix and started with a ".", create "1.txt" + tmpFilename = counter + fileName; + } + else if (dotIndex > 0) + { + // Filename contained ".", create "fileName-1.txt" + tmpFilename = fileName.substring(0, dotIndex) + "-" + counter + fileName.substring(dotIndex); + } + else + { + // Filename didn't contain a dot at all, create "fileName-1" + tmpFilename = fileName + "-" + counter; + } + existingFile = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, tmpFilename); + counter++; + + } while (existingFile != null); + + return tmpFilename; + } + + /** + * Helper to create a QName from either a fully qualified or short-name QName string + * + * @param qnameStr Fully qualified or short-name QName string + * @return QName + */ + public QName createQName(String qnameStr) + { + try + { + QName qname; + if (qnameStr.indexOf(QName.NAMESPACE_BEGIN) != -1) + { + qname = QName.createQName(qnameStr); + } + else + { + qname = QName.createQName(qnameStr, namespaceService); + } + return qname; + } + catch (Exception ex) + { + String msg = ex.getMessage(); + if (msg == null) + { + msg = ""; + } + throw new InvalidArgumentException(qnameStr + " isn't a valid QName. " + msg); + } + } + + /** + * Helper to create a QName from either a fully qualified or short-name QName string + * + * @param qnameStrList list of fully qualified or short-name QName string + * @param excludedProps + * @return a list of {@code QName} objects + */ + protected List createQNames(List qnameStrList, List excludedProps) + { + String PREFIX = PARAM_INCLUDE_PROPERTIES +"/"; + + List result = new ArrayList<>(qnameStrList.size()); + for (String str : qnameStrList) + { + if (str.startsWith(PREFIX)) + { + str = str.substring(PREFIX.length()); + } + + QName name = createQName(str); + if (!excludedProps.contains(name)) + { + result.add(name); + } + } + return result; + } + + @Override + public Node lock(String nodeId, LockInfo lockInfo, Parameters parameters) + { + NodeRef nodeRef = validateOrLookupNode(nodeId, null); + + if (isSpecialNode(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Current user doesn't have permission to lock node " + nodeId); + } + + if (!nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) + { + throw new InvalidArgumentException("Node of type cm:content or a subtype is expected: " + nodeId); + } + + LockInfo validatedLockInfo = validateLockInformation(lockInfo); + lockService.lock(nodeRef, validatedLockInfo.getMappedType(), validatedLockInfo.getTimeToExpire(), validatedLockInfo.getLifetime()); + + return getFolderOrDocument(nodeId, parameters); + } + + private LockInfo validateLockInformation(LockInfo lockInfo) + { + // Set default values for the lock details. + if (lockInfo.getType() == null) + { + lockInfo.setType(LockInfo.LockType2.ALLOW_OWNER_CHANGES.name()); + } + if (lockInfo.getLifetime() == null) + { + lockInfo.setLifetime(Lifetime.PERSISTENT.name()); + } + if (lockInfo.getTimeToExpire() == null) + { + lockInfo.setTimeToExpire(0); + } + return lockInfo; + } + + @Override + public Node unlock(String nodeId, Parameters parameters) + { + NodeRef nodeRef = validateOrLookupNode(nodeId, null); + + if (isSpecialNode(nodeRef, getNodeType(nodeRef))) + { + throw new PermissionDeniedException("Current user doesn't have permission to unlock node " + nodeId); + } + if (!lockService.isLocked(nodeRef)) + { + throw new IntegrityException("Can't unlock node " + nodeId + " because it isn't locked", null); + } + + lockService.unlock(nodeRef); + return getFolderOrDocument(nodeId, parameters); + } + + @Override + public DirectAccessUrl requestContentUrl(String nodeId, DirectAccessUrlRequest nodeDirectAccess) + { + NodeRef nodeRef = validateOrLookupNode(nodeId, null); + return requestContentUrl(nodeRef, nodeDirectAccess); + } + + private void checkExpiryDate(Date expiryDate) + { + DateTime now = DateTime.now(); + if (now.isAfter(expiryDate.getTime())) + { + throw new InvalidArgumentException("Invalid expiry date. Expiry date can't be in the past."); + } + } + + @Override + public DirectAccessUrl requestContentUrl(NodeRef nodeRef, DirectAccessUrlRequest directAccessUrlRequest) + { + if (nodeRef == null) + { + throw new InvalidArgumentException("Missing nodeRef"); + } + + Date expiresAt = null; + if (directAccessUrlRequest != null) + { + if (directAccessUrlRequest.getExpiresAt() != null && directAccessUrlRequest.getValidFor() != null) + { + throw new InvalidArgumentException("Direct access url can not have both expiresAt and validFor set."); + } + + if (directAccessUrlRequest.getExpiresAt() != null) + { + checkExpiryDate(directAccessUrlRequest.getExpiresAt()); + + expiresAt = directAccessUrlRequest.getExpiresAt(); + } + else if (directAccessUrlRequest.getValidFor() != null) + { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + + expTimeMillis += (directAccessUrlRequest.getValidFor() * 1000); + expiration.setTime(expTimeMillis); + + checkExpiryDate(expiration); + + expiresAt = expiration; + } + } + + DirectAccessUrl directAccessUrl = contentService.getDirectAccessUrl(nodeRef, expiresAt); + + if (directAccessUrl == null) + { + throw new DisabledServiceException("Direct access url isn't available."); + } + + return directAccessUrl; + } + + /** + * Checks if same permission is sent more than once + * @param locallySetPermissions + * @return + */ + private boolean hasDuplicatePermissions(List locallySetPermissions) + { + boolean duplicate = false; + if (locallySetPermissions != null) + { + HashSet temp = new HashSet<>(locallySetPermissions.size()); + for (NodePermissions.NodePermission permission : locallySetPermissions) + { + temp.add(permission); + } + duplicate = (locallySetPermissions.size() != temp.size()); + } + return duplicate; + } + + /** + * + * @param node + */ + private boolean isFavorite(NodeRef node) + { + PreferenceService preferenceService = (PreferenceService) sr.getService(ServiceRegistry.PREFERENCE_SERVICE); + String currentUserName = AuthenticationUtil.getFullyAuthenticatedUser(); + Map preferences = preferenceService.getPreferences(currentUserName); + + for (Serializable nodesFavorites : preferences.values()) + { + if (nodesFavorites instanceof String) + { + StringTokenizer st = new StringTokenizer((String) nodesFavorites, ","); + while (st.hasMoreTokens()) + { + String nodeRefStr = st.nextToken(); + nodeRefStr = nodeRefStr.trim(); + + if (!NodeRef.isNodeRef((String) nodeRefStr)) + { + continue; + } + + NodeRef nodeRef = new NodeRef((String) nodeRefStr); + + if (nodeRef.equals(node)) + { + return true; + } + } + } + } + return false; + } + + public void validateAspects(List aspectNames, List excludedNS, List excludedAspects) + { + if (aspectNames != null && excludedNS != null && excludedAspects != null) + { + Set aspects = mapToNodeAspects(aspectNames); + aspects.forEach(aspect -> { + if (excludedNS != null && excludedNS.contains(aspect.getNamespaceURI())) + { + throw new IllegalArgumentException("NameSpace cannot be used by API: " + aspect.toPrefixString()); + } + if (excludedAspects != null && excludedAspects.contains(aspect)) + { + throw new IllegalArgumentException("Cannot be used by API: " + aspect.toPrefixString()); + } + }); + } + } + + public void validateProperties(Map properties, List excludedNS, List excludedProperties) + { + if (properties != null) + { + Map nodeProps = mapToNodeProperties(properties); + nodeProps.keySet().forEach(property -> { + if ((excludedNS != null && excludedNS.contains(property.getNamespaceURI()))) + { + throw new IllegalArgumentException("NameSpace cannot be used by API: " + property.toPrefixString()); + } + if ((excludedProperties != null && excludedProperties.contains(property)) || AuditablePropertiesEntity.getAuditablePropertyQNames().contains(property)) + { + throw new IllegalArgumentException("Cannot be used by API: " + property.toPrefixString()); + } + }); + } + } + + /** + * @author Jamal Kaabi-Mofrad + */ + /* + private static class ContentInfoWrapper implements BasicContentInfo + { + private String mimeType; + private String encoding; + + public String getEncoding() + { + return encoding; + } + + public String getMimeType() + { + return mimeType; + } + + ContentInfoWrapper(BasicContentInfo basicContentInfo) + { + if (basicContentInfo != null) + { + this.mimeType = basicContentInfo.getMimeType(); + this.encoding = basicContentInfo.getEncoding(); + } + } + + ContentInfoWrapper(ContentInfo contentInfo) + { + if (contentInfo != null) + { + this.mimeType = contentInfo.getMimeType(); + this.encoding = contentInfo.getEncoding(); + } + } + + ContentInfoWrapper(Content content) + { + if (content != null && StringUtils.isNotEmpty(content.getMimetype())) + { + try + { + // TODO I think it makes sense to push contentType parsing into org.springframework.extensions.webscripts.servlet.FormData + MediaType media = MediaType.parseMediaType(content.getMimetype()); + this.mimeType = media.getType() + '/' + media.getSubtype(); + + if (media.getCharSet() != null) + { + this.encoding = media.getCharSet().name(); + } + } + catch (InvalidMediaTypeException ime) + { + throw new InvalidArgumentException(ime.getMessage()); + } + } + } + } + */ + + protected NodeService getNodeService() + { + return nodeService; + } + + protected DictionaryService getDictionaryService() + { + return dictionaryService; + } + + protected FileFolderService getFileFolderService() + { + return fileFolderService; + } + + protected NamespaceService getNamespaceService() + { + return namespaceService; + } + + protected PermissionService getPermissionService() + { + return permissionService; + } + + protected MimetypeService getMimetypeService() + { + return mimetypeService; + } + + protected ContentService getContentService() + { + return contentService; + } + + protected ActionService getActionService() + { + return actionService; + } + + protected VersionService getVersionService() + { + return versionService; + } + + protected PersonService getPersonService() + { + return personService; + } + + protected OwnableService getOwnableService() + { + return ownableService; + } + + protected AuthorityService getAuthorityService() + { + return authorityService; + } + + @Deprecated + protected ThumbnailService getThumbnailService() + { + return thumbnailService; + } + + protected SiteService getSiteService() + { + return siteService; + } + + protected ActivityPoster getPoster() + { + return poster; + } + + protected RetryingTransactionHelper getRetryingTransactionHelper() + { + return retryingTransactionHelper; + } + + protected LockService getLockService() + { + return lockService; + } + + protected VirtualStore getSmartStore() + { + return smartStore; + } + + protected QuickShareLinks getQuickShareLinks() + { + return quickShareLinks; + } + + protected Repository getRepositoryHelper() + { + return repositoryHelper; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java new file mode 100644 index 00000000000..9d8ea818cc1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java @@ -0,0 +1,959 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.authentication.ResetPasswordService; +import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordDetails; +import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowException; +import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowInvalidUserException; +import org.alfresco.repo.security.sync.UserRegistrySynchronizer; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.PasswordReset; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.alfresco.service.cmr.usage.ContentUsageService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Centralises access to people services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class PeopleImpl implements People +{ + private static final Log LOGGER = LogFactory.getLog(PeopleImpl.class); + + private static final List EXCLUDED_NS = Arrays.asList( + NamespaceService.SYSTEM_MODEL_1_0_URI, + "http://www.alfresco.org/model/user/1.0", + NamespaceService.CONTENT_MODEL_1_0_URI); + private static final List EXCLUDED_ASPECTS = Arrays.asList(); + private static final List EXCLUDED_PROPS = Arrays.asList(); + private static final int USERNAME_MAXLENGTH = 100; + private static final String[] RESERVED_AUTHORITY_PREFIXES = + { + PermissionService.GROUP_PREFIX, + PermissionService.ROLE_PREFIX + }; + + protected Nodes nodes; + protected Sites sites; + protected SiteService siteService; + protected NodeService nodeService; + protected PersonService personService; + protected AuthenticationService authenticationService; + protected AuthorityService authorityService; + protected ContentUsageService contentUsageService; + protected ContentService contentService; + protected ThumbnailService thumbnailService; + protected ResetPasswordService resetPasswordService; + protected UserRegistrySynchronizer userRegistrySynchronizer; + protected Renditions renditions; + + + private final static Map sort_params_to_qnames; + static + { + Map aMap = new HashMap<>(3); + aMap.put(PARAM_FIRST_NAME, ContentModel.PROP_FIRSTNAME); + aMap.put(PARAM_LAST_NAME, ContentModel.PROP_LASTNAME); + aMap.put(PARAM_ID, ContentModel.PROP_USERNAME); + sort_params_to_qnames = Collections.unmodifiableMap(aMap); + } + + public void setSites(Sites sites) + { + this.sites = sites; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setContentUsageService(ContentUsageService contentUsageService) + { + this.contentUsageService = contentUsageService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setThumbnailService(ThumbnailService thumbnailService) + { + this.thumbnailService = thumbnailService; + } + + public void setResetPasswordService(ResetPasswordService resetPasswordService) + { + this.resetPasswordService = resetPasswordService; + } + + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + public void setUserRegistrySynchronizer(UserRegistrySynchronizer userRegistrySynchronizer) + { + this.userRegistrySynchronizer = userRegistrySynchronizer; + } + + /** + * Validate, perform -me- substitution and canonicalize the person ID. + * + * @param personId + * @return The validated and processed ID. + */ + @Override + public String validatePerson(String personId) + { + return validatePerson(personId, false); + } + + @Override + public String validatePerson(final String requestedPersonId, boolean validateIsCurrentUser) + { + String personId = requestedPersonId; + if(personId == null) + { + throw new InvalidArgumentException("personId is null."); + } + + if(personId.equalsIgnoreCase(DEFAULT_USER)) + { + personId = AuthenticationUtil.getFullyAuthenticatedUser(); + } + + personId = personService.getUserIdentifier(personId); + if(personId == null) + { + // Could not find canonical user ID by case-sensitive ID. + throw new EntityNotFoundException(requestedPersonId); + } + + if(validateIsCurrentUser) + { + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + if(!currentUserId.equalsIgnoreCase(personId)) + { + throw new EntityNotFoundException(personId); + } + } + + return personId; + } + + protected void processPersonProperties(String userName, final Map nodeProps) + { + if(!contentUsageService.getEnabled()) + { + // quota used will always be 0 in this case so remove from the person properties + nodeProps.remove(ContentModel.PROP_SIZE_QUOTA); + nodeProps.remove(ContentModel.PROP_SIZE_CURRENT); + } + + // The person description is located in a separate content file located at cm:persondescription + // "Inline" this data, by removing the cm:persondescription property and adding a temporary property + // (Person.PROP_PERSON_DESCRIPTION) containing the actual content as a string. + final ContentData personDescription = (ContentData)nodeProps.get(ContentModel.PROP_PERSONDESC); + if(personDescription != null) + { + nodeProps.remove(ContentModel.PROP_PERSONDESC); + + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + ContentReader reader = contentService.getRawReader(personDescription.getContentUrl()); + if(reader != null && reader.exists()) + { + String description = reader.getContentString(); + nodeProps.put(Person.PROP_PERSON_DESCRIPTION, description); + } + + return null; + } + }); + } + } + + public boolean hasAvatar(NodeRef personNodeRef) + { + return (getAvatarOriginal(personNodeRef) != null); + } + + @Override + public NodeRef getAvatar(String personId) + { + NodeRef avatar = null; + personId = validatePerson(personId); + NodeRef personNode = personService.getPerson(personId); + if(personNode != null) + { + NodeRef avatarOrig = getAvatarOriginal(personNode); + avatar = thumbnailService.getThumbnailByName(avatarOrig, ContentModel.PROP_CONTENT, "avatar"); + } + + if (avatar == null) + { + throw new EntityNotFoundException(personId); + } + + return avatar; + } + + private NodeRef getAvatarOriginal(NodeRef personNode) + { + NodeRef avatarOrigNodeRef = null; + List avatarChildAssocs = nodeService.getChildAssocs(personNode, Collections.singleton(ContentModel.ASSOC_PREFERENCE_IMAGE)); + if (avatarChildAssocs.size() > 0) + { + ChildAssociationRef ref = avatarChildAssocs.get(0); + avatarOrigNodeRef = ref.getChildRef(); + } + else + { + // TODO do we still need this ? - backward compatible with JSF web-client avatar + List avatorAssocs = nodeService.getTargetAssocs(personNode, ContentModel.ASSOC_AVATAR); + if (avatorAssocs.size() > 0) + { + AssociationRef ref = avatorAssocs.get(0); + avatarOrigNodeRef = ref.getTargetRef(); + } + } + return avatarOrigNodeRef; + } + + @Override + public BinaryResource downloadAvatarContent(String personId, Parameters parameters) + { + personId = validatePerson(personId); + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarNodeRef = getAvatarOriginal(personNode); + + return renditions.getContentNoValidation(avatarNodeRef, "avatar", parameters); + } + + @Override + public Person uploadAvatarContent(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) + { + if (!thumbnailService.getThumbnailsEnabled()) + { + throw new DisabledServiceException("Thumbnail generation has been disabled."); + } + + personId = validatePerson(personId); + checkCurrentUserOrAdmin(personId); + + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); + + if (avatarOrigNodeRef != null) + { + deleteAvatar(avatarOrigNodeRef); + } + + QName origAvatarQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "origAvatar"); + nodeService.addAspect(personNode, ContentModel.ASPECT_PREFERENCES, null); + ChildAssociationRef assoc = nodeService.createNode(personNode, ContentModel.ASSOC_PREFERENCE_IMAGE, origAvatarQName, + ContentModel.TYPE_CONTENT); + NodeRef avatar = assoc.getChildRef(); + String avatarOriginalNodeId = avatar.getId(); + + // TODO do we still need this ? - backward compatible with JSF web-client avatar + nodeService.createAssociation(personNode, avatar, ContentModel.ASSOC_AVATAR); + + Node n = nodes.updateContent(avatarOriginalNodeId, contentInfo, stream, parameters); + String mimeType = n.getContent().getMimeType(); + + if (mimeType.indexOf("image/") != 0) + { + throw new InvalidArgumentException( + "Uploaded content must be an image (content type determined to be '"+mimeType+"')"); + } + + // create thumbnail synchronously + Rendition avatarR = new Rendition(); + avatarR.setId("avatar"); + renditions.createRendition(avatar, avatarR, false, parameters); + + List include = Arrays.asList( + PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_PROPERTIES); + + return getPersonWithProperties(personId, include); + } + + @Override + public void deleteAvatarContent(String personId) + { + personId = validatePerson(personId); + checkCurrentUserOrAdmin(personId); + + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); + if (avatarOrigNodeRef != null) + { + deleteAvatar(avatarOrigNodeRef); + } + } + + private void deleteAvatar(NodeRef avatarOrigNodeRef) + { + // Set as temporary to permanently delete node (instead of archiving) + nodeService.addAspect(avatarOrigNodeRef, ContentModel.ASPECT_TEMPORARY, null); + + nodeService.deleteNode(avatarOrigNodeRef); + } + + + /** + * Get a full representation of a person. + * + * @throws NoSuchPersonException + * if personId does not exist + */ + @Override + public Person getPerson(String personId) + { + personId = validatePerson(personId); + List include = Arrays.asList( + PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_PROPERTIES, + PARAM_INCLUDE_CAPABILITIES); + Person person = getPersonWithProperties(personId, include); + + return person; + } + + public Person getPerson(String personId, List include) + { + personId = validatePerson(personId); + Person person = getPersonWithProperties(personId, include); + + return person; + } + + @Override + public CollectionWithPagingInfo getPeople(final Parameters parameters) + { + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + List> sortProps = getSortProps(parameters); + + // For now the results are not filtered + // please see REPO-555 + final PagingResults pagingResult = personService.getPeople(null, null, sortProps, pagingRequest); + + final List page = pagingResult.getPage(); + int totalItems = pagingResult.getTotalResultCount().getFirst(); + final String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + List people = new AbstractList() + { + @Override + public Person get(int index) + { + PersonService.PersonInfo personInfo = page.get(index); + Person person = getPersonWithProperties(personInfo.getUserName(), parameters.getInclude()); + return person; + } + + @Override + public int size() + { + return page.size(); + } + }; + + return CollectionWithPagingInfo.asPaged(paging, people, pagingResult.hasMoreItems(), totalItems); + } + + private List> getSortProps(Parameters parameters) + { + List> sortProps = new ArrayList<>(); + List sortCols = parameters.getSorting(); + if ((sortCols != null) && (sortCols.size() > 0)) + { + for (SortColumn sortCol : sortCols) + { + QName sortPropQName = sort_params_to_qnames.get(sortCol.column); + if (sortPropQName == null) + { + throw new InvalidArgumentException("Invalid sort field: " + sortCol.column); + } + sortProps.add(new Pair<>(sortPropQName, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE))); + } + } + else + { + // default sort order + sortProps.add(new Pair<>(ContentModel.PROP_USERNAME, Boolean.TRUE)); + } + return sortProps; + } + + private Person getPersonWithProperties(String personId, List include) + { + Person person = null; + NodeRef personNode = personService.getPerson(personId, false); + if (personNode != null) + { + Map nodeProps = nodeService.getProperties(personNode); + processPersonProperties(personId, nodeProps); + // TODO this needs to be run as admin but should we do this here? + final String pId = personId; + Boolean enabled = AuthenticationUtil.runAsSystem(new RunAsWork() + { + public Boolean doWork() throws Exception + { + return authenticationService.getAuthenticationEnabled(pId); + } + }); + person = new Person(personNode, nodeProps, enabled); + + // Remove the temporary property used to help inline the person description content property. + // It may be accessed from the person object (person.getDescription()). + nodeProps.remove(Person.PROP_PERSON_DESCRIPTION); + + // Expose properties + if (include.contains(PARAM_INCLUDE_PROPERTIES)) + { + // Note that custProps may be null. + Map custProps = nodes.mapFromNodeProperties(nodeProps, new ArrayList<>(), new HashMap<>(), EXCLUDED_NS, EXCLUDED_PROPS); + person.setProperties(custProps); + } + if (include.contains(PARAM_INCLUDE_ASPECTNAMES)) + { + // Expose aspect names + Set aspects = nodeService.getAspects(personNode); + person.setAspectNames(nodes.mapFromNodeAspects(aspects, EXCLUDED_NS, EXCLUDED_ASPECTS)); + } + if (include.contains(PARAM_INCLUDE_CAPABILITIES)) + { + // Expose capabilities + Map capabilities = new HashMap<>(3); + capabilities.put("isAdmin", isAdminAuthority(personId)); + capabilities.put("isGuest", isGuestAuthority(personId)); + capabilities.put("isMutable", isMutableAuthority(personId)); + person.setCapabilities(capabilities); + } + + // get avatar information + if (hasAvatar(personNode)) + { + try + { + NodeRef avatar = getAvatar(personId); + person.setAvatarId(avatar); + } + catch (EntityNotFoundException e) + { + // shouldn't happen, but ok + } + } + } + else + { + throw new EntityNotFoundException(personId); + } + + return person; + } + + @Override + public Person create(Person person) + { + validateCreatePersonData(person); + + if (! isAdminAuthority()) + { + // note: do an explict check for admin here (since personExists does not throw 403 unlike createPerson, + // hence next block would cause 409 to be returned) + throw new PermissionDeniedException(); + } + + // Unfortunately PersonService.createPerson(...) only throws an AlfrescoRuntimeException + // rather than a more specific exception and does not use a message ID either, so there's + // no sensible way to know that it was thrown due to the user already existing - hence this check here. + if (personService.personExists(person.getUserName())) + { + throw new ConstraintViolatedException("Person '" + person.getUserName() + "' already exists."); + } + + // set enabled default value true + if (person.isEnabled() == null) + { + person.setEnabled(true); + } + + Map props = person.toProperties(); + + MutableAuthenticationService mas = (MutableAuthenticationService) authenticationService; + mas.createAuthentication(person.getUserName(), person.getPassword().toCharArray()); + mas.setAuthenticationEnabled(person.getUserName(), person.isEnabled()); + + // Add custom properties + if (person.getProperties() != null) + { + Map customProps = person.getProperties(); + props.putAll(nodes.mapToNodeProperties(customProps)); + } + + NodeRef nodeRef = personService.createPerson(props); + + // Add custom aspects + nodes.addCustomAspects(nodeRef, person.getAspectNames(), EXCLUDED_ASPECTS); + + // Write the contents of PersonUpdate.getDescription() text to a content file + // and store the content URL in ContentModel.PROP_PERSONDESC + if (person.getDescription() != null) + { + savePersonDescription(person.getDescription(), nodeRef); + } + + // Return a fresh retrieval + return getPerson(person.getUserName()); + } + + /** + * Write the description to a content file and store the content URL in + * ContentModel.PROP_PERSONDESC + * + * @param description + * @param nodeRef + */ + private void savePersonDescription(final String description, final NodeRef nodeRef) + { + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + if (description != null) + { + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_PERSONDESC, true); + writer.putContent(description); + } + else + { + nodeService.setProperty(nodeRef, ContentModel.PROP_PERSONDESC, null); + } + return null; + } + }); + } + + private void validateCreatePersonData(Person person) + { + // Mandatory field checks first + checkRequiredField("id", person.getUserName()); + checkRequiredField("firstName", person.getFirstName()); + checkRequiredField("email", person.getEmail()); + checkRequiredField("password", person.getPassword()); + + validateUsername(person.getUserName()); + nodes.validateAspects(person.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); + nodes.validateProperties(person.getProperties(), EXCLUDED_NS, EXCLUDED_PROPS); + } + + private void validateUsername(String username) + { + if (username.length() > 100) + { + throw new InvalidArgumentException("Username exceeds max length of " + USERNAME_MAXLENGTH + " characters."); + } + + for (String prefix : RESERVED_AUTHORITY_PREFIXES) + { + if (username.toUpperCase().startsWith(prefix)) + { + throw new IllegalArgumentException("Username cannot start with the reserved prefix '"+prefix+"'."); + } + } + } + + private void checkRequiredField(String fieldName, Object fieldValue) + { + if (fieldValue == null) + { + throw new InvalidArgumentException("Field '"+fieldName+"' is null, but is required."); + } + + // belts-and-braces - note: should not see empty string (since converted to null via custom json deserializer) + if ((fieldValue instanceof String) && ((String)fieldValue).isEmpty()) + { + throw new InvalidArgumentException("Field '"+fieldName+"' is empty, but is required."); + } + } + + @Override + public Person update(String personId, final Person person) + { + // Validate, perform -me- substitution and canonicalize the person ID. + personId = validatePerson(personId); + validateUpdatePersonData(person); + + // Check if user updating *their own* details or is an admin + boolean isAdmin = checkCurrentUserOrAdmin(personId); + + final String personIdToUpdate = validatePerson(personId); + final Map properties = person.toProperties(); + + // if requested, update password + updatePassword(isAdmin, personIdToUpdate, person); + + if (person.isEnabled() != null) + { + if (isAdminAuthority(personIdToUpdate)) + { + throw new PermissionDeniedException("Admin authority cannot be disabled."); + } + + // note: if current user is not an admin then permission denied exception is thrown + MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; + mutableAuthenticationService.setAuthenticationEnabled(personIdToUpdate, person.isEnabled()); + } + + NodeRef personNodeRef = personService.getPerson(personIdToUpdate, false); + if (person.wasSet(Person.PROP_PERSON_DESCRIPTION)) + { + // Remove person description from saved properties + properties.remove(ContentModel.PROP_PERSONDESC); + + // Custom save for person description. + savePersonDescription(person.getDescription(), personNodeRef); + } + + // Update custom aspects - do this *before* adding custom properties. The + // addition of custom properties may result in the auto-addition of aspects + // and we don't want to remove them during the update of explicitly specified aspects. + nodes.updateCustomAspects(personNodeRef, person.getAspectNames(), EXCLUDED_ASPECTS); + + // Add custom properties + if (person.getProperties() != null) + { + Map customProps = person.getProperties(); + properties.putAll(nodes.mapToNodeProperties(customProps)); + } + + // MNT-21150 LDAP synced attributes can be changed using REST API + Set immutableProperties = userRegistrySynchronizer.getPersonMappedProperties(personIdToUpdate); + + immutableProperties.forEach(immutableProperty -> { + if (properties.containsKey(immutableProperty)) + { + properties.remove(immutableProperty); + } + }); + + // The person service only allows admin users to set the properties by default. + if(!properties.isEmpty()) + { + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override public Void doWork() throws Exception + { + personService.setPersonProperties(personIdToUpdate, properties, false); + return null; + } + }); + } + + return getPerson(personId); + } + + private boolean checkCurrentUserOrAdmin(String personId) + { + boolean isAdmin = isAdminAuthority(); + + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) + { + throw new PermissionDeniedException(); + } + + return isAdmin; + } + + private void validateUpdatePersonData(Person person) + { + nodes.validateAspects(person.getAspectNames(), EXCLUDED_NS, EXCLUDED_ASPECTS); + nodes.validateProperties(person.getProperties(), EXCLUDED_NS, EXCLUDED_PROPS); + + if (person.wasSet(ContentModel.PROP_FIRSTNAME)) + { + checkRequiredField("firstName", person.getFirstName()); + } + + if (person.wasSet(ContentModel.PROP_EMAIL)) + { + checkRequiredField("email", person.getEmail()); + } + + if (person.wasSet(ContentModel.PROP_ENABLED) && (person.isEnabled() == null)) + { + throw new IllegalArgumentException("'enabled' field cannot be empty."); + } + + if (person.wasSet(ContentModel.PROP_EMAIL_FEED_DISABLED) && (person.isEmailNotificationsEnabled() == null)) + { + throw new IllegalArgumentException("'emailNotificationsEnabled' field cannot be empty."); + } + } + + private void updatePassword(boolean isAdmin, String personIdToUpdate, Person person) + { + MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; + + boolean isOldPassword = person.wasSet(Person.PROP_PERSON_OLDPASSWORD); + boolean isPassword = person.wasSet(Person.PROP_PERSON_PASSWORD); + + if (isPassword || isOldPassword) + { + if (isOldPassword && ((person.getOldPassword() == null) || (person.getOldPassword().isEmpty()))) + { + throw new IllegalArgumentException("'oldPassword' field cannot be empty."); + } + + if (!isPassword || (person.getPassword() == null) || (person.getPassword().isEmpty())) + { + throw new IllegalArgumentException("password' field cannot be empty."); + } + + char[] newPassword = person.getPassword().toCharArray(); + + if (!isAdmin) + { + // Non-admin users can update their own password, but must provide their current password. + if (!isOldPassword) + { + throw new IllegalArgumentException("To change password, both 'oldPassword' and 'password' fields are required."); + } + + char[] oldPassword = person.getOldPassword().toCharArray(); + try + { + mutableAuthenticationService.updateAuthentication(personIdToUpdate, oldPassword, newPassword); + } + catch (AuthenticationException e) + { + throw new PermissionDeniedException("Incorrect password."); + } + } + else + { + // An admin user can update without knowing the original pass - but must know their own! + // note: is it reasonable to silently ignore oldPassword if supplied ? + + mutableAuthenticationService.setAuthentication(personIdToUpdate, newPassword); + } + } + } + + private boolean isAdminAuthority() + { + return authorityService.hasAdminAuthority(); + } + + private boolean isAdminAuthority(String authorityName) + { + return authorityService.isAdminAuthority(authorityName); + } + + @Override + public void requestPasswordReset(String userId, String client) + { + // Validate the userId and the client + checkRequiredField("userId", userId); + checkRequiredField("client", client); + + // This is an un-authenticated API call so we wrap it to run as System + AuthenticationUtil.runAsSystem(() -> { + try + { + resetPasswordService.requestReset(userId, client); + } + catch (ResetPasswordWorkflowInvalidUserException ex) + { + // we don't throw an exception. + // For security reason (prevent the attackers to determine that userId exists in the system or not), + // the endpoint returns a 202 response if the userId does not exist or + // if the user is disabled by an Administrator. + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Invalid user. " + ex.getMessage()); + } + } + + return null; + }); + } + + @Override + public void resetPassword(String personId, final PasswordReset passwordReset) + { + checkResetPasswordData(passwordReset); + checkRequiredField("personId", personId); + + ResetPasswordDetails resetDetails = new ResetPasswordDetails() + .setUserId(personId) + .setPassword(passwordReset.getPassword()) + .setWorkflowId(passwordReset.getId()) + .setWorkflowKey(passwordReset.getKey()); + try + { + // This is an un-authenticated API call so we wrap it to run as System + AuthenticationUtil.runAsSystem(() -> { + resetPasswordService.initiateResetPassword(resetDetails); + + return null; + }); + + } + catch (ResetPasswordWorkflowException ex) + { + // we don't throw an exception. + // For security reason, the endpoint returns a 202 response + // See APPSREPO-35 acceptance criteria + if (LOGGER.isWarnEnabled()) + { + LOGGER.warn(ex.getMessage()); + } + } + } + + private void checkResetPasswordData(PasswordReset data) + { + checkRequiredField("password", data.getPassword()); + checkRequiredField("id", data.getId()); + checkRequiredField("key", data.getKey()); + } + + private boolean isGuestAuthority(String authorityName) + { + return authorityService.isGuestAuthority(authorityName); + } + + private boolean isMutableAuthority(String authorityName) + { + MutableAuthenticationService mutableAuthenticationService = (MutableAuthenticationService) authenticationService; + // Check whether the account is mutable according to the authentication service + if (!mutableAuthenticationService.isAuthenticationMutable(authorityName)) + { + return false; + } + // Only allow non-admin users to mutate their own accounts + String currentUser = mutableAuthenticationService.getCurrentUserName(); + if (currentUser.equals(authorityName) || authorityService.isAdminAuthority(currentUser)) + { + return true; + } + return false; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/PreferencesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/PreferencesImpl.java new file mode 100644 index 00000000000..abdf9ee8c68 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/PreferencesImpl.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.query.PagingResults; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Preferences; +import org.alfresco.rest.api.model.Preference; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.cmr.preference.PreferenceService; +import org.alfresco.util.Pair; + +/** + * Centralises access to preference services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class PreferencesImpl implements Preferences +{ + private People people; + private PreferenceService preferenceService; + + public void setPeople(People people) + { + this.people = people; + } + + public void setPreferenceService(PreferenceService preferenceService) + { + this.preferenceService = preferenceService; + } + + public Preference getPreference(String personId, String preferenceName) + { + personId = people.validatePerson(personId); + Serializable preferenceValue = preferenceService.getPreference(personId, preferenceName); + if(preferenceValue != null) + { + return new Preference(preferenceName, preferenceValue); + } + else + { + throw new RelationshipResourceNotFoundException(personId, preferenceName); + } + } + + public CollectionWithPagingInfo getPreferences(String personId, Paging paging) + { + personId = people.validatePerson(personId); + + PagingResults> preferences = preferenceService.getPagedPreferences(personId, null, Util.getPagingRequest(paging)); + List ret = new ArrayList(preferences.getPage().size()); + for(Pair prefEntity : preferences.getPage()) + { + Preference pref = new Preference(prefEntity.getFirst(), prefEntity.getSecond()); + ret.add(pref); + } + + return CollectionWithPagingInfo.asPaged(paging, ret, preferences.hasMoreItems(), preferences.getTotalResultCount().getFirst()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/QueriesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/QueriesImpl.java new file mode 100644 index 00000000000..c86c064e8d9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/QueriesImpl.java @@ -0,0 +1,651 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.impl; + +import static org.alfresco.rest.api.impl.QueriesImpl.AbstractQuery.Sort.IN_QUERY_SORT; +import static org.alfresco.rest.api.impl.QueriesImpl.AbstractQuery.Sort.POST_QUERY_SORT; + +import java.io.Serializable; +import java.text.Collator; +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.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Queries; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.api.model.Site; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.PermissionEvaluationMode; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.AlfrescoCollator; +import org.alfresco.util.ISO9075; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.SearchLanguageConversion; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Queries implementation + * + * @author janv + * @author Alan Davis + */ +public class QueriesImpl implements Queries, InitializingBean +{ + private final static Map NODE_SORT_PARAMS_TO_QNAMES = sortParamsToQNames( + PARAM_NAME, ContentModel.PROP_NAME, + PARAM_CREATEDAT, ContentModel.PROP_CREATED, + PARAM_MODIFIEDAT, ContentModel.PROP_MODIFIED); + + private final static Map PEOPLE_SORT_PARAMS_TO_QNAMES = sortParamsToQNames( + PARAM_PERSON_ID, ContentModel.PROP_USERNAME, + ContentModel.PROP_FIRSTNAME, + ContentModel.PROP_LASTNAME); + + private final static Map SITE_SORT_PARAMS_TO_QNAMES = sortParamsToQNames( + PARAM_SITE_ID, ContentModel.PROP_NAME, + PARAM_SITE_TITLE, ContentModel.PROP_TITLE, + PARAM_SITE_DESCRIPTION, ContentModel.PROP_DESCRIPTION); + + /** + * Helper method to build a map of sort parameter names to QNames. This method iterates through + * the parameters. If a parameter is a String it is assumed to be a sort parameter name and will + * be followed by a QName to which it maps. If however it is a QName the local name of the OName + * is used as the sort parameter name. + * @param parameters to build up the map. + * @return the map + */ + private static Map sortParamsToQNames(Object... parameters) + { + Map map = new HashMap<>(); + for (int i=0; i findNodes(Parameters parameters) + { + SearchService searchService = sr.getSearchService(); + return new AbstractQuery(nodeService, searchService) + { + private final Map mapUserInfo = new HashMap<>(10); + + @Override + protected void buildQuery(StringBuilder query, String term, SearchParameters sp, String queryTemplateName) + { + sp.addQueryTemplate(queryTemplateName, "%(cm:name cm:title cm:description TEXT TAG)"); + + String rootNodeId = parameters.getParameter(PARAM_ROOT_NODE_ID); + if (rootNodeId != null) + { + NodeRef nodeRef = nodes.validateOrLookupNode(rootNodeId, null); + query.append("PATH:\"").append(getQNamePath(nodeRef.getId())).append("//*\" AND ("); + } + if (term != null) + { + query.append("\""); + query.append(term); + query.append("\""); + } + if (rootNodeId != null) + { + query.append(")"); + } + + String nodeTypeStr = parameters.getParameter(PARAM_NODE_TYPE); + if (nodeTypeStr != null) + { + QName filterNodeTypeQName = nodes.createQName(nodeTypeStr); + if (dictionaryService.getType(filterNodeTypeQName) == null) + { + throw new InvalidArgumentException("Unknown filter nodeType: "+nodeTypeStr); + } + + query.append(" AND (+TYPE:\"").append(nodeTypeStr).append(("\")")); + query.append(" AND -ASPECT:\"sys:hidden\" AND -cm:creator:system AND -QNAME:comment\\-* "); + } + else + { + query.append(" AND (+TYPE:\"cm:content\" OR +TYPE:\"cm:folder\")"); + query.append(" AND -TYPE:\"cm:thumbnail\" AND -TYPE:\"cm:failedThumbnail\" AND -TYPE:\"cm:rating\" AND -TYPE:\"fm:post\""); + query.append(" AND -TYPE:\"st:site\" AND -ASPECT:\"st:siteContainer\""); + query.append(" AND -ASPECT:\"sys:hidden\" AND -cm:creator:system AND -QNAME:comment\\-* "); + } + } + + private String getQNamePath(String nodeId) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + + Map cache = new HashMap<>(); + StringBuilder buf = new StringBuilder(128); + Path path = null; + try + { + path = nodeService.getPath(nodeRef); + } + catch (InvalidNodeRefException inre) + { + throw new EntityNotFoundException(nodeId); + } + + for (Path.Element e : path) + { + if (e instanceof Path.ChildAssocElement) + { + QName qname = ((Path.ChildAssocElement) e).getRef().getQName(); + if (qname != null) + { + String prefix = cache.get(qname.getNamespaceURI()); + if (prefix == null) + { + // first request for this namespace prefix, get and cache result + Collection prefixes = namespaceService.getPrefixes(qname.getNamespaceURI()); + prefix = prefixes.size() != 0 ? prefixes.iterator().next() : ""; + cache.put(qname.getNamespaceURI(), prefix); + } + buf.append('/').append(prefix).append(':').append(ISO9075.encode(qname.getLocalName())); + } + } + else + { + buf.append('/').append(e.toString()); + } + } + return buf.toString(); + } + + @Override + protected List newList(int capacity) + { + return new ArrayList(capacity); + } + + @Override + protected Node convert(NodeRef nodeRef, List includeParam) + { + return nodes.getFolderOrDocument(nodeRef, null, null, includeParam, mapUserInfo); + } + + @Override + protected String escapeTerm(String term) + { + term = term.trim(); + term = SearchLanguageConversion.escapeLuceneQuery(term); + return term; + } + + }.find(parameters, PARAM_TERM, MIN_TERM_LENGTH_NODES, "keywords", + IN_QUERY_SORT, NODE_SORT_PARAMS_TO_QNAMES, + new SortColumn(PARAM_MODIFIEDAT, false)); + } + + @Override + public CollectionWithPagingInfo findPeople(Parameters parameters) + { + SearchService searchService = sr.getSearchService(); + return new AbstractQuery(nodeService, searchService) + { + @Override + protected void buildQuery(StringBuilder query, String term, SearchParameters sp, String queryTemplateName) + { + sp.addQueryTemplate(queryTemplateName, "|%firstName OR |%lastName OR |%userName"); + sp.setExcludeTenantFilter(false); + sp.setPermissionEvaluation(PermissionEvaluationMode.EAGER); + + query.append("TYPE:\"").append(ContentModel.TYPE_PERSON).append("\" AND (\"*"); + query.append(term); + query.append("*\")"); + } + + @Override + protected List newList(int capacity) + { + return new ArrayList(capacity); + } + + @Override + protected Person convert(NodeRef nodeRef, List includeParam) + { + String personId = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_USERNAME); + Person person = people.getPerson(personId); + return person; + } + + // TODO Do the sort in the query on day. A comment in the code for the V0 API used for live people + // search says adding sort values for this query don't work - tried it and they really don't. + }.find(parameters, PARAM_TERM, MIN_TERM_LENGTH_PEOPLE, "_PERSON", + POST_QUERY_SORT, PEOPLE_SORT_PARAMS_TO_QNAMES, + new SortColumn(PARAM_FIRSTNAME, true), new SortColumn(PARAM_LASTNAME, true)); + } + + @Override + public CollectionWithPagingInfo findSites(Parameters parameters) + { + SearchService searchService = sr.getSearchService(); + return new AbstractQuery(nodeService, searchService) + { + @Override + protected void buildQuery(StringBuilder query, String term, SearchParameters sp, String queryTemplateName) + { + sp.addQueryTemplate(queryTemplateName, "%(cm:name cm:title cm:description)"); + sp.setExcludeTenantFilter(false); + sp.setPermissionEvaluation(PermissionEvaluationMode.EAGER); + + query.append("TYPE:\"").append(SiteModel.TYPE_SITE).append("\" AND (\"*"); + query.append(term); + query.append("*\")"); + } + + @Override + protected List newList(int capacity) + { + return new ArrayList<>(capacity); + } + + @Override + protected Site convert(NodeRef nodeRef, List includeParam) + { + return getSite(siteService.getSite(nodeRef), true); + } + + // note: see also Sites.getSite + private Site getSite(SiteInfo siteInfo, boolean includeRole) + { + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + String siteId = siteInfo.getShortName(); + String role = null; + if(includeRole) + { + role = sites.getSiteRole(siteId); + } + return new Site(siteInfo, role); + } + }.find(parameters, PARAM_TERM, MIN_TERM_LENGTH_SITES, "_SITE", POST_QUERY_SORT, SITE_SORT_PARAMS_TO_QNAMES, new SortColumn(PARAM_SITE_TITLE, true)); + } + + public abstract static class AbstractQuery + { + public enum Sort + { + IN_QUERY_SORT, POST_QUERY_SORT + } + + private final NodeService nodeService; + private final SearchService searchService; + + public AbstractQuery(NodeService nodeService, SearchService searchService) + { + this.nodeService = nodeService; + this.searchService = searchService; + } + + public CollectionWithPagingInfo find(Parameters parameters, + String termName, int minTermLength, String queryTemplateName, + Sort sort, Map sortParamsToQNames, SortColumn... defaultSort) + { + SearchParameters sp = new SearchParameters(); + sp.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO); + sp.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + sp.setDefaultFieldName(queryTemplateName); + + String term = getTerm(parameters, termName, minTermLength); + + StringBuilder query = new StringBuilder(); + buildQuery(query, term, sp, queryTemplateName); + sp.setQuery(query.toString()); + + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + List defaultSortCols = (defaultSort != null ? Arrays.asList(defaultSort) : Collections.emptyList()); + if (sort == IN_QUERY_SORT) + { + addSortOrder(parameters, sortParamsToQNames, defaultSortCols, sp); + + sp.setSkipCount(pagingRequest.getSkipCount()); + sp.setMaxItems(pagingRequest.getMaxItems()); + } + + ResultSet queryResults = null; + List collection = null; + try + { + queryResults = searchService.query(sp); + + List nodeRefs = queryResults.getNodeRefs(); + + if (sort == POST_QUERY_SORT) + { + nodeRefs = postQuerySort(parameters, sortParamsToQNames, defaultSortCols, nodeRefs); + } + + collection = newList(nodeRefs.size()); + List includeParam = parameters.getInclude(); + + for (NodeRef nodeRef : nodeRefs) + { + T t = convert(nodeRef, includeParam); + collection.add(t); + } + + if (sort == POST_QUERY_SORT) + { + return listPage(collection, paging); + } + else + { + return CollectionWithPagingInfo.asPaged(paging, collection, queryResults.hasMore(), Long.valueOf(queryResults.getNumberFound()).intValue()); + } + } + finally + { + if (queryResults != null) + { + queryResults.close(); + } + } + } + + /** + * Builds up the query and is expected to call {@link SearchParameters#setDefaultFieldName(String)} + * and {@link SearchParameters#addQueryTemplate(String, String)} + * @param query StringBuilder into which the query should be built. + * @param term to be searched for + * @param sp SearchParameters + * @param queryTemplateName + */ + protected abstract void buildQuery(StringBuilder query, String term, SearchParameters sp, String queryTemplateName); + + /** + * Returns a list of the correct type. + * @param capacity of the list + * @return a new list. + */ + protected abstract List newList(int capacity); + + /** + * Converts a nodeRef into the an object of the required type. + * @param nodeRef to be converted + * @param includeParam additional fields to be included + * @return the object + */ + protected abstract T convert(NodeRef nodeRef, List includeParam); + + protected String getTerm(Parameters parameters, String termName, int minTermLength) + { + String term = parameters.getParameter(termName); + if (term == null) + { + throw new InvalidArgumentException("Query '"+termName+"' not specified"); + } + + term = escapeTerm(term); + + int cnt = 0; + for (int i = 0; i < term.length(); i++) + { + char c = term.charAt(i); + if (Character.isLetterOrDigit(c)) + { + cnt++; + if (cnt == minTermLength) + { + break; + } + } + } + + if (cnt < minTermLength) + { + throw new InvalidArgumentException("Query '"+termName+"' is too short. Must have at least "+minTermLength+" alphanumeric chars"); + } + + return term; + } + + /** + * Trim and escape the term - override if needed + * + * @param term + * @return + */ + protected String escapeTerm(String term) + { + term = term.trim(); + term = term.replace("\"", ""); + term = SearchLanguageConversion.escapeLuceneQuery(term); + return term; + } + + /** + * Adds sort order to the SearchParameters. + */ + protected void addSortOrder(Parameters parameters, Map sortParamsToQNames, + List defaultSortCols, SearchParameters sp) + { + List sortCols = getSorting(parameters, defaultSortCols); + for (SortColumn sortCol : sortCols) + { + QName sortPropQName = sortParamsToQNames.get(sortCol.column); + if (sortPropQName == null) + { + throw new InvalidArgumentException("Invalid sort field: "+sortCol.column); + } + sp.addSort("@" + sortPropQName, sortCol.asc); + } + } + + private List getSorting(Parameters parameters, List defaultSortCols) + { + List sortCols = parameters.getSorting(); + if (sortCols == null || sortCols.size() == 0) + { + sortCols = defaultSortCols == null ? Collections.emptyList() : defaultSortCols; + } + return sortCols; + } + + protected List postQuerySort(Parameters parameters, Map sortParamsToQNames, + List defaultSortCols, List nodeRefs) + { + final List sortCols = getSorting(parameters, defaultSortCols); + int sortColCount = sortCols.size(); + if (sortColCount > 0) + { + // make copy of nodeRefs because it can be unmodifiable list. + nodeRefs = new ArrayList(nodeRefs); + + List sortPropQNames = new ArrayList<>(sortColCount); + for (SortColumn sortCol : sortCols) + { + QName sortPropQName = sortParamsToQNames.get(sortCol.column); + if (sortPropQName == null) + { + throw new InvalidArgumentException("Invalid sort field: "+sortCol.column); + } + sortPropQNames.add(sortPropQName); + } + + final Collator col = AlfrescoCollator.getInstance(I18NUtil.getLocale()); + Collections.sort(nodeRefs, new Comparator() + { + @Override + public int compare(NodeRef n1, NodeRef n2) + { + int result = 0; + for (int i=0; i CollectionWithPagingInfo listPage(List result, Paging paging) + { + // return 'page' of results (based on full result set) + int skipCount = paging.getSkipCount(); + int pageSize = paging.getMaxItems(); + int pageEnd = skipCount + pageSize; + + final List page = new ArrayList<>(pageSize); + if (result == null) + { + result = Collections.emptyList(); + } + + Iterator it = result.iterator(); + for (int counter = 0; counter < pageEnd && it.hasNext(); counter++) + { + T element = it.next(); + if (counter < skipCount) + { + continue; + } + if (counter > pageEnd - 1) + { + break; + } + page.add(element); + } + + int totalCount = result.size(); + boolean hasMoreItems = ((skipCount + page.size()) < totalCount); + + return CollectionWithPagingInfo.asPaged(paging, page, hasMoreItems, totalCount); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java new file mode 100644 index 00000000000..94ebee970dc --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java @@ -0,0 +1,701 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.QuickShareModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.quickshare.QuickShareLinkExpiryActionException; +import org.alfresco.repo.quickshare.QuickShareServiceImpl.QuickShareEmailRequest; +import org.alfresco.repo.search.QueryParameterDefImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.QuickShareLinks; +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.model.ContentInfo; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.QuickShareLink; +import org.alfresco.rest.api.model.QuickShareLinkEmailRequest; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.quickshare.InvalidSharedIdException; +import org.alfresco.service.cmr.quickshare.QuickShareDTO; +import org.alfresco.service.cmr.quickshare.QuickShareService; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.QueryParameterDefinition; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.SearchLanguageConversion; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.surf.util.I18NUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Centralises access to shared link (public "quick share") services and maps between representations. + * + * @author janv + * @author Jamal Kaabi-Mofrad + * + * @since publicapi1.0 + */ +public class QuickShareLinksImpl implements QuickShareLinks, RecognizedParamsExtractor, InitializingBean +{ + private static final Log logger = LogFactory.getLog(QuickShareLinksImpl.class); + + private final static String DISABLED = "QuickShare is disabled system-wide"; + private boolean enabled = true; + + private ServiceRegistry sr; + private QuickShareService quickShareService; + private Nodes nodes; + private Renditions renditions; + + private NodeService nodeService; + private PersonService personService; + private MimetypeService mimeTypeService; + private SearchService searchService; + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + + // additional exclude properties for share links as these are already top-level properties + private static final List EXCLUDED_PROPS = Arrays.asList( + QuickShareModel.PROP_QSHARE_SHAREDBY, + QuickShareModel.PROP_QSHARE_SHAREDID); + + + public void setServiceRegistry(ServiceRegistry sr) + { + this.sr = sr; + } + + + public void setQuickShareService(QuickShareService quickShareService) + { + this.quickShareService = quickShareService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("sr", this.sr); + ParameterCheck.mandatory("quickShareService", this.quickShareService); + ParameterCheck.mandatory("nodes", this.nodes); + + this.nodeService = sr.getNodeService(); + this.personService = sr.getPersonService(); + this.mimeTypeService = sr.getMimetypeService(); + this.searchService = sr.getSearchService(); + this.dictionaryService = sr.getDictionaryService(); + this.namespaceService = sr.getNamespaceService(); + } + + /** + * Returns limited metadata regarding the shared (content) link. + *

    + * Note: does *not* require authenticated access for (public) shared link. + */ + public QuickShareLink readById(final String sharedId, final Parameters parameters) + { + checkEnabled(); + + try + { + Pair pair = quickShareService.getTenantNodeRefFromSharedId(sharedId); + String networkTenantDomain = pair.getFirst(); + + return TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork() + { + public QuickShareLink doWork() throws Exception + { + // note: assume noAuth here (rather than rely on getRunAsUser which will be null in non-MT) + return getQuickShareInfo(sharedId, true, parameters.getInclude()); + } + }, networkTenantDomain); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: " + sharedId); + throw new EntityNotFoundException(sharedId); + } + } + + /** + * Download content via shared link. + *

    + * Note: does *not* require authenticated access for (public) shared link. + * + * @param sharedId + * @param renditionId - optional + * @param parameters {@link Parameters} + * @return + * @throws EntityNotFoundException + */ + public BinaryResource readProperty(String sharedId, final String renditionId, final Parameters parameters) throws EntityNotFoundException + { + checkEnabled(); + checkValidShareId(sharedId); + + try + { + Pair pair = quickShareService.getTenantNodeRefFromSharedId(sharedId); + + String networkTenantDomain = pair.getFirst(); + final NodeRef nodeRef = pair.getSecond(); + + return TenantUtil.runAsSystemTenant(() -> { + // belt-and-braces (similar to QuickShareContentGet) + if (!nodeService.hasAspect(nodeRef, QuickShareModel.ASPECT_QSHARE)) + { + throw new InvalidNodeRefException(nodeRef); + } + + if (renditionId != null) + { + return renditions.getContent(nodeRef, renditionId, parameters); + } + else + { + return nodes.getContent(nodeRef, parameters, false); + } + }, networkTenantDomain); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: " + sharedId); + throw new EntityNotFoundException(sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new EntityNotFoundException(sharedId); + } + } + + /** + * Delete the shared link. + *

    + * Once deleted, the shared link will no longer exist hence get/download will no longer work (ie. return 404). + * If the link is later re-created then a new unique shared id will be generated. + *

    + * Requires authenticated access. + * + * @param sharedId String id of the quick share + */ + public void delete(String sharedId, Parameters parameters) + { + checkEnabled(); + checkValidShareId(sharedId); + + try + { + NodeRef nodeRef = quickShareService.getTenantNodeRefFromSharedId(sharedId).getSecond(); + + String sharedByUserId = (String)nodeService.getProperty(nodeRef, QuickShareModel.PROP_QSHARE_SHAREDBY); + if (!quickShareService.canDeleteSharedLink(nodeRef, sharedByUserId)) + { + throw new PermissionDeniedException("Can't perform unshare action: " + sharedId); + } + + quickShareService.unshareContent(sharedId); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: " + sharedId); + throw new EntityNotFoundException(sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new EntityNotFoundException(sharedId); + } + } + + /** + * Create quick share. + *

    + * Requires authenticated access. + * + * @param nodeIds + * @param parameters + * @return + */ + public List create(List nodeIds, Parameters parameters) + { + checkEnabled(); + + List result = new ArrayList<>(nodeIds.size()); + + List includeParam = parameters != null ? parameters.getInclude() : Collections. emptyList(); + + for (QuickShareLink qs : nodeIds) + { + String nodeId = qs.getNodeId(); + + if (nodeId == null) + { + throw new InvalidArgumentException("A valid nodeId must be specified !"); + } + + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + + try + { + // Note: will throw InvalidNodeRefException (=> 404) if node does not exist + String sharedId = (String) nodeService.getProperty(nodeRef, QuickShareModel.PROP_QSHARE_SHAREDID); + if (sharedId != null) + { + throw new ConstraintViolatedException("sharedId already exists: "+nodeId+" ["+sharedId+"]"); + } + + // Note: will throw AccessDeniedException (=> 403) via QuickShareService (when NodeService tries to getAspects) + // Note: since we already check node exists above, we can assume that InvalidNodeRefException (=> 404) here means not content (see type check) + try + { + QuickShareDTO qsDto = quickShareService.shareContent(nodeRef, qs.getExpiresAt()); + result.add(getQuickShareInfo(qsDto.getId(), false, includeParam)); + } + catch (InvalidNodeRefException inre) + { + throw new InvalidArgumentException("Unable to create shared link to non-file content: " + nodeId); + } + catch (QuickShareLinkExpiryActionException ex) + { + throw new InvalidArgumentException(ex.getMessage()); + } + } + catch (AccessDeniedException ade) + { + throw new PermissionDeniedException("Unable to create shared link to node that does not exist: " + nodeId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to create shared link: [" + nodeRef + "]"); + throw new EntityNotFoundException(nodeId); + } + } + + return result; + } + + @Override + public void emailSharedLink(String sharedId, QuickShareLinkEmailRequest emailRequest, Parameters parameters) + { + checkEnabled(); + checkValidShareId(sharedId); + validateEmailRequest(emailRequest); + + try + { + NodeRef nodeRef = quickShareService.getTenantNodeRefFromSharedId(sharedId).getSecond(); + String sharedNodeName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); + QuickShareEmailRequest request = new QuickShareEmailRequest(); + request.setSharedNodeName(sharedNodeName); + request.setClient(emailRequest.getClient()); + request.setSharedId(sharedId); + request.setSenderMessage(emailRequest.getMessage()); + request.setLocale(I18NUtil.parseLocale(emailRequest.getLocale())); + request.setToEmails(emailRequest.getRecipientEmails()); + quickShareService.sendEmailNotification(request); + } + catch (InvalidSharedIdException ex) + { + throw new EntityNotFoundException(sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new EntityNotFoundException(sharedId); + } + } + + private Parameters getParamsWithCreatedStatus() + { + String filterStatusCreated = "(" + Renditions.PARAM_STATUS + "='" + Rendition.RenditionStatus.CREATED + "')"; + Query whereQuery = getWhereClause(filterStatusCreated); + Params.RecognizedParams recParams = new Params.RecognizedParams(null, null, null, null, null, null, whereQuery, null, false); + Parameters params = Params.valueOf(recParams, null, null, null); + return params; + } + + @Override + public Rendition getRendition(String sharedId, String renditionId) + { + checkEnabled(); + checkValidShareId(sharedId); + + try + { + Pair pair = quickShareService.getTenantNodeRefFromSharedId(sharedId); + + String networkTenantDomain = pair.getFirst(); + final NodeRef nodeRef = pair.getSecond(); + + return TenantUtil.runAsSystemTenant(() -> + { + Parameters params = getParamsWithCreatedStatus(); + return renditions.getRendition(nodeRef, renditionId, params); + + }, networkTenantDomain); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: " + sharedId); + throw new EntityNotFoundException(sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new EntityNotFoundException(sharedId); + } + } + + @Override + public CollectionWithPagingInfo getRenditions(String sharedId) + { + checkEnabled(); + checkValidShareId(sharedId); + + try + { + Pair pair = quickShareService.getTenantNodeRefFromSharedId(sharedId); + + String networkTenantDomain = pair.getFirst(); + final NodeRef nodeRef = pair.getSecond(); + + return TenantUtil.runAsSystemTenant(() -> + { + Parameters params = getParamsWithCreatedStatus(); + return renditions.getRenditions(nodeRef, params); + + }, networkTenantDomain); + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: " + sharedId); + throw new EntityNotFoundException(sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new EntityNotFoundException(sharedId); + } + } + + // Helper find (search) method + + private final static Set FIND_SHARED_LINKS_QUERY_PROPERTIES = + new HashSet<>(Arrays.asList(new String[] {PARAM_SHAREDBY})); + + public CollectionWithPagingInfo findLinks(Parameters parameters) + { + checkEnabled(); + + String queryString = + "ASPECT:\"" + QuickShareModel.ASPECT_QSHARE.toString() + "\""; + + SearchParameters sp = new SearchParameters(); + + // note: REST API query parameter (ie. where clause filter) - not to be confused with search query ;-) + Query q = parameters.getQuery(); + if (q != null) + { + // filtering via "where" clause + MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(FIND_SHARED_LINKS_QUERY_PROPERTIES, null); + QueryHelper.walk(q, propertyWalker); + + String sharedByUserId = propertyWalker.getProperty(PARAM_SHAREDBY, WhereClauseParser.EQUALS, String.class); + + if (sharedByUserId != null) + { + if (People.DEFAULT_USER.equalsIgnoreCase(sharedByUserId)) + { + sharedByUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + } + + QueryParameterDefinition qpd = new QueryParameterDefImpl(QuickShareModel.PROP_QSHARE_SHAREDBY, dictionaryService.getDataType(DataTypeDefinition.TEXT), + true, sharedByUserId); + sp.addQueryParameterDefinition(qpd); + + String qsharedBy = QuickShareModel.PROP_QSHARE_SHAREDBY.toPrefixString(namespaceService); + queryString += " +@"+SearchLanguageConversion.escapeLuceneQuery(qsharedBy)+":\"${"+qsharedBy+"}\""; + } + } + + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(queryString); + sp.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + sp.setSkipCount(pagingRequest.getSkipCount()); + sp.setMaxItems(pagingRequest.getMaxItems()); + + sp.addSort("@" + ContentModel.PROP_MODIFIED, false); + + ResultSet results = searchService.query(sp); + + List qsLinks = new ArrayList<>(results.length()); + + List includeParam = parameters.getInclude(); + + for (ResultSetRow row : results) + { + NodeRef nodeRef = row.getNodeRef(); + qsLinks.add(getQuickShareInfo(nodeRef, false, includeParam)); + } + + results.close(); + + return CollectionWithPagingInfo.asPaged(paging, qsLinks, results.hasMore(), Long.valueOf(results.getNumberFound()).intValue()); + } + + private QuickShareLink getQuickShareInfo(String sharedId, boolean noAuth, List includeParam) + { + checkValidShareId(sharedId); + + Map map = (Map) quickShareService.getMetaData(sharedId).get("item"); + NodeRef nodeRef = new NodeRef((String) map.get("nodeRef")); + + return getQuickShareInfo(nodeRef, map, noAuth, includeParam); + } + + private QuickShareLink getQuickShareInfo(NodeRef nodeRef, boolean noAuth, List includeParam) + { + Map map = (Map) quickShareService.getMetaData(nodeRef).get("item"); + return getQuickShareInfo(nodeRef, map , noAuth, includeParam); + } + + private QuickShareLink getQuickShareInfo(NodeRef nodeRef, Map map, boolean noAuth, List includeParam) + { + String sharedId = (String)map.get("sharedId"); + + try + { + Map nodeProps = nodeService.getProperties(nodeRef); + ContentData cd = (ContentData)nodeProps.get(ContentModel.PROP_CONTENT); + + String mimeType = cd.getMimetype(); + String mimeTypeName = mimeTypeService.getDisplaysByMimetype().get(mimeType); + ContentInfo contentInfo = new ContentInfo(mimeType, mimeTypeName, cd.getSize(), cd.getEncoding()); + + Map mapUserInfo = new HashMap<>(2); + + // note: if noAuth mode then don't return userids (to limit disclosure and be consistent with v0 internal) + boolean displayNameOnly = noAuth; + + UserInfo modifiedByUser = Node.lookupUserInfo((String)nodeProps.get(ContentModel.PROP_MODIFIER), mapUserInfo, personService, displayNameOnly); + + // TODO review - should we return sharedByUser for authenticated users only ?? (not exposed by V0 but needed for "find") + String sharedByUserId = (String)nodeProps.get(QuickShareModel.PROP_QSHARE_SHAREDBY); + UserInfo sharedByUser = Node.lookupUserInfo(sharedByUserId, mapUserInfo, personService, displayNameOnly); + + QuickShareLink qs = new QuickShareLink(sharedId, nodeRef.getId()); + qs.setName((String) map.get("name")); + qs.setTitle((String) map.get("title")); + qs.setDescription((String) map.get("description")); + qs.setContent(contentInfo); + qs.setModifiedAt((Date) map.get("modified")); + qs.setModifiedByUser(modifiedByUser); + qs.setSharedByUser(sharedByUser); + qs.setExpiresAt((Date) map.get("expiryDate")); + + // note: if noAuth mode then do not return allowable operations (eg. but can be optionally returned when finding shared links) + if (!noAuth) + { + // Cached document + Node doc = null; + + if (includeParam.contains(PARAM_INCLUDE_ALLOWABLEOPERATIONS)) + { + if (quickShareService.canDeleteSharedLink(nodeRef, sharedByUserId)) + { + // the allowable operations for the shared link + qs.setAllowableOperations(Collections.singletonList(Nodes.OP_DELETE)); + } + doc = getNode(nodeRef, includeParam, mapUserInfo); + List allowableOps = doc.getAllowableOperations(); + // the allowable operations for the actual file being shared + qs.setAllowableOperationsOnTarget(allowableOps); + } + + // in noAuth mode we don't return the path info + if (includeParam.contains(PARAM_INCLUDE_PATH)) + { + qs.setPath(nodes.lookupPathInfo(nodeRef, null)); + } + + if (includeParam.contains(PARAM_INCLUDE_PROPERTIES)) + { + if (doc == null) + { + doc = getNode(nodeRef, includeParam, mapUserInfo); + } + // Create a map from node properties excluding properties already in this QuickShareLink + Map filteredNodeProperties = filterProps(doc.getProperties(), EXCLUDED_PROPS); + qs.setProperties(filteredNodeProperties); + } + + if (includeParam.contains(PARAM_INCLUDE_ISFAVORITE)) + { + if (doc == null) + { + doc = getNode(nodeRef, includeParam, mapUserInfo); + } + qs.setIsFavorite(doc.getIsFavorite()); + } + + if (includeParam.contains(PARAM_INCLUDE_ASPECTNAMES)) + { + if (doc == null) + { + doc = getNode(nodeRef, includeParam, mapUserInfo); + } + qs.setAspectNames(doc.getAspectNames()); + } + } + + return qs; + } + catch (InvalidSharedIdException ex) + { + logger.warn("Unable to find: " + sharedId); + throw new EntityNotFoundException(sharedId); + } + catch (InvalidNodeRefException inre) + { + logger.warn("Unable to find: " + sharedId + " [" + inre.getNodeRef() + "]"); + throw new EntityNotFoundException(sharedId); + } + } + + private Map filterProps(Map properties, List toRemove) + { + Map filteredProps = properties == null ? new HashMap<>() : new HashMap<>(properties); + List propsToRemove = toRemove.stream().map(e -> e.toPrefixString(namespaceService)).collect(Collectors.toList()); + filteredProps.keySet().removeAll(propsToRemove); + return filteredProps; + } + + private Node getNode(NodeRef nodeRef, List includeParam, Map mapUserInfo) + { + List modifiedIncludeParams = new LinkedList<>(includeParam); + // Remove path as this information is already retrieved elsewhere + modifiedIncludeParams.remove(PARAM_INCLUDE_PATH); + return nodes.getFolderOrDocument(nodeRef, null, null, modifiedIncludeParams, mapUserInfo); + } + + private void checkEnabled() + { + if (!enabled) + { + throw new DisabledServiceException(DISABLED); + } + } + + private void checkValidShareId(String sharedId) + { + if (sharedId==null) + { + throw new InvalidArgumentException("A valid sharedId must be specified !"); + } + } + + private void validateEmailRequest(QuickShareLinkEmailRequest emailRequest) + { + if (StringUtils.isEmpty(emailRequest.getClient())) + { + throw new InvalidArgumentException("A valid client must be specified."); + } + if (emailRequest.getRecipientEmails() == null || emailRequest.getRecipientEmails().isEmpty()) + { + throw new InvalidArgumentException("A valid recipientEmail must be specified."); + } + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java new file mode 100644 index 00000000000..8e65103edc7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java @@ -0,0 +1,735 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software LimitedP + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.impl; + +import org.alfresco.heartbeat.RenditionsDataCollector; +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.rendition2.RenditionDefinition2; +import org.alfresco.repo.rendition2.RenditionDefinitionRegistry2; +import org.alfresco.repo.rendition2.RenditionService2; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.thumbnail.script.ScriptThumbnailService; +import org.alfresco.repo.version.common.VersionUtil; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.model.ContentInfo; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.api.model.Rendition.RenditionStatus; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.StaleEntityException; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.content.CacheDirective; +import org.alfresco.rest.framework.resource.content.ContentInfoImpl; +import org.alfresco.rest.framework.resource.content.FileBinaryResource; +import org.alfresco.rest.framework.resource.content.NodeBinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionDoesNotExistException; +import org.alfresco.service.cmr.version.VersionHistory; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; + +import java.io.File; +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeMap; + +/** + * @author Jamal Kaabi-Mofrad, janv + */ +public class RenditionsImpl implements Renditions, ResourceLoaderAware +{ + private static final Log logger = LogFactory.getLog(RenditionsImpl.class); + + private static final Set RENDITION_STATUS_COLLECTION_EQUALS_QUERY_PROPERTIES = Collections.singleton(PARAM_STATUS); + + private Nodes nodes; + private NodeService nodeService; + private ScriptThumbnailService scriptThumbnailService; + private MimetypeService mimetypeService; + private ServiceRegistry serviceRegistry; + private ResourceLoader resourceLoader; + private TenantService tenantService; + private RenditionService2 renditionService2; + private RenditionsDataCollector renditionsDataCollector; + private VersionService versionService; + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setScriptThumbnailService(ScriptThumbnailService scriptThumbnailService) + { + this.scriptThumbnailService = scriptThumbnailService; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) + { + this.resourceLoader = resourceLoader; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + public void setRenditionService2(RenditionService2 renditionService2) + { + this.renditionService2 = renditionService2; + } + + public void setRenditionsDataCollector(RenditionsDataCollector renditionsDataCollector) + { + this.renditionsDataCollector = renditionsDataCollector; + } + + public void init() + { + PropertyCheck.mandatory(this, "nodes", nodes); + PropertyCheck.mandatory(this, "scriptThumbnailService", scriptThumbnailService); + PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry); + PropertyCheck.mandatory(this, "tenantService", tenantService); + PropertyCheck.mandatory(this, "renditionService2", renditionService2); + PropertyCheck.mandatory(this, "renditionsDataCollector", renditionsDataCollector); + + this.nodeService = serviceRegistry.getNodeService(); + this.versionService = serviceRegistry.getVersionService(); + this.mimetypeService = serviceRegistry.getMimetypeService(); + } + + @Override + public CollectionWithPagingInfo getRenditions(NodeRef nodeRef, Parameters parameters) + { + return getRenditions(nodeRef, null, parameters); + } + + @Override + public CollectionWithPagingInfo getRenditions(NodeRef nodeRef, String versionLabelId, Parameters parameters) + { + final NodeRef validatedNodeRef = validateNode(nodeRef.getStoreRef(), nodeRef.getId(), versionLabelId, parameters); + ContentData contentData = getContentData(validatedNodeRef, true); + String sourceMimetype = contentData.getMimetype(); + + boolean includeCreated = true; + boolean includeNotCreated = true; + String status = getStatus(parameters); + if (status != null) + { + includeCreated = RenditionStatus.CREATED.equals(RenditionStatus.valueOf(status)); + includeNotCreated = !includeCreated; + } + + // List all available rendition definitions + long size = contentData.getSize(); + RenditionDefinitionRegistry2 renditionDefinitionRegistry2 = renditionService2.getRenditionDefinitionRegistry2(); + Set renditionNames = renditionDefinitionRegistry2.getRenditionNamesFrom(sourceMimetype, size); + + Map apiRenditions = new TreeMap<>(); + if (includeNotCreated) + { + for (String renditionName : renditionNames) + { + apiRenditions.put(renditionName, toApiRendition(renditionName)); + } + } + + List nodeRefRenditions = renditionService2.getRenditions(validatedNodeRef); + if (!nodeRefRenditions.isEmpty()) + { + for (ChildAssociationRef childAssociationRef : nodeRefRenditions) + { + NodeRef renditionNodeRef = childAssociationRef.getChildRef(); + Rendition apiRendition = toApiRendition(renditionNodeRef); + String renditionName = apiRendition.getId(); + if (renditionNames.contains(renditionName)) + { + if (includeCreated) + { + // Replace/append any thumbnail definitions with created rendition info + apiRenditions.put(renditionName, apiRendition); + } + else + { + // Remove any thumbnail definitions that has been created from the list, + // as the filter requires only the Not_Created renditions + apiRenditions.remove(renditionName); + } + } + else + { + if (logger.isTraceEnabled()) + { + logger.trace("Skip unknown rendition [" + renditionName + ", " + renditionNodeRef + "]"); + } + } + } + } + + // Wrap paging info, as the core service doesn't support paging + Paging paging = parameters.getPaging(); + PagingResults results = Util.wrapPagingResults(paging, apiRenditions.values()); + + return CollectionWithPagingInfo.asPaged(paging, results.getPage(), results.hasMoreItems(), results.getTotalResultCount().getFirst()); + } + + @Override + public Rendition getRendition(NodeRef nodeRef, String renditionId, Parameters parameters) + { + return getRendition(nodeRef, null, renditionId, parameters); + } + + @Override + public Rendition getRendition(NodeRef nodeRef, String versionLabelId, String renditionId, Parameters parameters) + { + final NodeRef validatedNodeRef = validateNode(nodeRef.getStoreRef(), nodeRef.getId(), versionLabelId, parameters); + NodeRef renditionNodeRef = getRenditionByName(validatedNodeRef, renditionId, parameters); + boolean includeNotCreated = true; + String status = getStatus(parameters); + if (status != null) + { + includeNotCreated = !RenditionStatus.CREATED.equals(RenditionStatus.valueOf(status)); + } + + // if there is no rendition, then try to find the available/registered rendition (yet to be created). + if (renditionNodeRef == null && includeNotCreated) + { + ContentData contentData = getContentData(validatedNodeRef, true); + String sourceMimetype = contentData.getMimetype(); + long size = contentData.getSize(); + RenditionDefinitionRegistry2 renditionDefinitionRegistry2 = renditionService2.getRenditionDefinitionRegistry2(); + RenditionDefinition2 renditionDefinition = renditionDefinitionRegistry2.getRenditionDefinition(renditionId); + if (renditionDefinition == null) + { + throw new NotFoundException(renditionId + " is not registered."); + } + else + { + Set renditionNames = renditionDefinitionRegistry2.getRenditionNamesFrom(sourceMimetype, size); + boolean found = false; + for (String renditionName : renditionNames) + { + // Check the registered renditionId is applicable for the node's mimeType + if (renditionId.equals(renditionName)) + { + found = true; + break; + } + } + if (!found) + { + throw new NotFoundException(renditionId + " is not applicable for the node's mimeType " + sourceMimetype); + } + } + return toApiRendition(renditionId); + } + + if (renditionNodeRef == null) + { + throw new NotFoundException("The rendition with id: " + renditionId + " was not found."); + } + + return toApiRendition(renditionNodeRef); + } + + @Override + public void createRendition(NodeRef nodeRef, Rendition rendition, Parameters parameters) + { + createRendition(nodeRef, rendition, true, parameters); + } + + @Override + public void createRendition(NodeRef nodeRef, Rendition rendition, boolean executeAsync, Parameters parameters) + { + createRendition(nodeRef, null, rendition, executeAsync, parameters); + } + + @Override + public void createRendition(NodeRef nodeRef, String versionLabelId, Rendition rendition, boolean executeAsync, Parameters parameters) + { + // If thumbnail generation has been configured off, then don't bother. + if (!renditionService2.isEnabled()) + { + throw new DisabledServiceException("Rendition generation has been disabled."); + } + + final NodeRef sourceNodeRef = validateNode(nodeRef.getStoreRef(), nodeRef.getId(), versionLabelId, parameters); + final NodeRef renditionNodeRef = getRenditionByName(sourceNodeRef, rendition.getId(), parameters); + if (renditionNodeRef != null) + { + throw new ConstraintViolatedException(rendition.getId() + " rendition already exists."); // 409 + } + + try + { + renditionService2.render(sourceNodeRef, rendition.getId()); + } + catch (IllegalArgumentException e) + { + throw new NotFoundException(rendition.getId() + " is not registered."); // 404 + } + catch (UnsupportedOperationException e) + { + throw new IllegalArgumentException((e.getMessage())); // 400 + } + catch (IllegalStateException e) + { + throw new StaleEntityException(e.getMessage()); // 409 + } + } + + @Override + public void createRenditions(NodeRef nodeRef, List renditions, Parameters parameters) + throws NotFoundException, ConstraintViolatedException + { + createRenditions(nodeRef, null, renditions, parameters); + } + + @Override + public void createRenditions(NodeRef nodeRef, String versionLabelId, List renditions, Parameters parameters) + throws NotFoundException, ConstraintViolatedException + { + if (renditions.isEmpty()) + { + return; + } + + if (!renditionService2.isEnabled()) + { + throw new DisabledServiceException("Rendition generation has been disabled."); + } + + final NodeRef sourceNodeRef = validateNode(nodeRef.getStoreRef(), nodeRef.getId(), versionLabelId, parameters); + RenditionDefinitionRegistry2 renditionDefinitionRegistry2 = renditionService2.getRenditionDefinitionRegistry2(); + + // So that POST /nodes/{nodeId}/renditions can specify rendition names as a comma separated list just like + // POST /nodes/{nodId}/children can specify a comma separated list, the following code checks to see if the + // supplied Rendition names are actually comma separated lists. The following example shows it is possible to + // use both approaches. + // [ + // { "id": "doclib" }, + // { "id": "avatar,avatar32" } + // ] + Set renditionNames = new HashSet<>(); + for (Rendition rendition : renditions) + { + String name = getName(rendition); + Set requestedRenditions = NodesImpl.getRequestedRenditions(name); + if (requestedRenditions == null) + { + renditionNames.add(null); + } + else + { + renditionNames.addAll(requestedRenditions); + } + } + + StringJoiner renditionNamesAlreadyExist = new StringJoiner(","); + StringJoiner renditionNamesNotRegistered = new StringJoiner(","); + List renditionNamesToCreate = new ArrayList<>(); + for (String renditionName : renditionNames) + { + if (renditionName == null) + { + throw new IllegalArgumentException(("Null rendition name supplied")); // 400 + } + + RenditionDefinition2 renditionDefinition = renditionDefinitionRegistry2.getRenditionDefinition(renditionName); + if (renditionDefinition == null) + { + renditionNamesNotRegistered.add(renditionName); + } + + final NodeRef renditionNodeRef = getRenditionByName(sourceNodeRef, renditionName, parameters); + if (renditionNodeRef == null) + { + renditionNamesToCreate.add(renditionName); + } + else + { + renditionNamesAlreadyExist.add(renditionName); + } + } + + if (renditionNamesNotRegistered.length() != 0) + { + throw new NotFoundException("Renditions not registered: " + renditionNamesNotRegistered); // 404 + } + + if (renditionNamesToCreate.size() == 0) + { + throw new ConstraintViolatedException("All renditions requested already exist: " + renditionNamesAlreadyExist); // 409 + } + + for (String renditionName : renditionNamesToCreate) + { + try + { + renditionService2.render(sourceNodeRef, renditionName); + } + catch (UnsupportedOperationException e) + { + throw new IllegalArgumentException((e.getMessage())); // 400 + } + catch (IllegalStateException e) + { + throw new StaleEntityException(e.getMessage()); // 409 + } + } + + } + + private String getName(Rendition rendition) + { + String renditionName = rendition.getId(); + if (renditionName != null) + { + renditionName = renditionName.trim(); + if (renditionName.isEmpty()) + { + renditionName = null; + } + } + return renditionName; + } + + @Override + public BinaryResource getContent(NodeRef nodeRef, String renditionId, Parameters parameters) + { + return getContent(nodeRef, null, renditionId, parameters); + } + + @Override + public BinaryResource getContent(NodeRef nodeRef, String versionLabelId, String renditionId, Parameters parameters) + { + final NodeRef validatedNodeRef = validateNode(nodeRef.getStoreRef(), nodeRef.getId(), versionLabelId, parameters); + return getContentImpl(validatedNodeRef, renditionId, parameters); + } + + @Override + public BinaryResource getContentNoValidation(NodeRef nodeRef, String renditionId, Parameters parameters) + { + return getContentNoValidation(nodeRef, null, renditionId, parameters); + } + + @Override + public BinaryResource getContentNoValidation(NodeRef nodeRef, String versionLabelId, String renditionId, Parameters parameters) + { + nodeRef = findVersionIfApplicable(nodeRef, versionLabelId); + return getContentImpl(nodeRef, renditionId, parameters); + } + + private BinaryResource getContentImpl(NodeRef nodeRef, String renditionId, Parameters parameters) + { + NodeRef renditionNodeRef = getRenditionByName(nodeRef, renditionId, parameters); + + // By default set attachment header (with rendition Id) unless attachment=false + boolean attach = true; + String attachment = parameters.getParameter(PARAM_ATTACHMENT); + if (attachment != null) + { + attach = Boolean.valueOf(attachment); + } + final String attachFileName = (attach ? renditionId : null); + + if (renditionNodeRef == null) + { + boolean isPlaceholder = Boolean.valueOf(parameters.getParameter(PARAM_PLACEHOLDER)); + if (!isPlaceholder) + { + throw new NotFoundException("Thumbnail was not found for [" + renditionId + ']'); + } + String sourceNodeMimeType = null; + try + { + sourceNodeMimeType = (nodeRef != null ? getMimeType(nodeRef) : null); + } + catch (InvalidArgumentException e) + { + // No content for node, e.g. ASSOC_AVATAR rather than ASSOC_PREFERENCE_IMAGE + } + // resource based on the content's mimeType and rendition id + String phPath = scriptThumbnailService.getMimeAwarePlaceHolderResourcePath(renditionId, sourceNodeMimeType); + if (phPath == null) + { + // 404 since no thumbnail was found + throw new NotFoundException("Thumbnail was not found and no placeholder resource available for [" + renditionId + ']'); + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Retrieving content from resource path [" + phPath + ']'); + } + // get extension of resource + String ext = ""; + int extIndex = phPath.lastIndexOf('.'); + if (extIndex != -1) + { + ext = phPath.substring(extIndex); + } + + try + { + final String resourcePath = "classpath:" + phPath; + InputStream inputStream = resourceLoader.getResource(resourcePath).getInputStream(); + // create temporary file + File file = TempFileProvider.createTempFile(inputStream, "RenditionsApi-", ext); + return new FileBinaryResource(file, attachFileName); + } + catch (Exception ex) + { + if (logger.isErrorEnabled()) + { + logger.error("Couldn't load the placeholder." + ex.getMessage()); + } + throw new ApiException("Couldn't load the placeholder."); + } + } + } + + Map nodeProps = nodeService.getProperties(renditionNodeRef); + ContentData contentData = (ContentData) nodeProps.get(ContentModel.PROP_CONTENT); + Date modified = (Date) nodeProps.get(ContentModel.PROP_MODIFIED); + + org.alfresco.rest.framework.resource.content.ContentInfo contentInfo = null; + if (contentData != null) + { + contentInfo = new ContentInfoImpl(contentData.getMimetype(), contentData.getEncoding(), contentData.getSize(), contentData.getLocale()); + } + // add cache settings + CacheDirective cacheDirective = new CacheDirective.Builder() + .setNeverCache(false) + .setMustRevalidate(false) + .setLastModified(modified) + .setETag(modified != null ? Long.toString(modified.getTime()) : null) + .setMaxAge(Long.valueOf(31536000))// one year (in seconds) + .build(); + + return new NodeBinaryResource(renditionNodeRef, ContentModel.PROP_CONTENT, contentInfo, attachFileName, cacheDirective); + } + + protected NodeRef getRenditionByName(NodeRef nodeRef, String renditionId, Parameters parameters) + { + if (nodeRef != null) + { + if (StringUtils.isEmpty(renditionId)) + { + throw new InvalidArgumentException("renditionId can't be null or empty."); + } + + ChildAssociationRef nodeRefRendition = renditionService2.getRenditionByName(nodeRef, renditionId); + if (nodeRefRendition != null) + { + ContentData contentData = getContentData(nodeRefRendition.getChildRef(), false); + if (contentData != null) + { + return tenantService.getName(nodeRef, nodeRefRendition.getChildRef()); + } + } + } + + return null; + } + + protected Rendition toApiRendition(NodeRef renditionNodeRef) + { + Rendition apiRendition = new Rendition(); + + String renditionName = (String) nodeService.getProperty(renditionNodeRef, ContentModel.PROP_NAME); + apiRendition.setId(renditionName); + + ContentData contentData = getContentData(renditionNodeRef, false); + ContentInfo contentInfo = null; + if (contentData != null) + { + contentInfo = new ContentInfo(contentData.getMimetype(), + getMimeTypeDisplayName(contentData.getMimetype()), + contentData.getSize(), + contentData.getEncoding()); + } + apiRendition.setContent(contentInfo); + apiRendition.setStatus(RenditionStatus.CREATED); + + return apiRendition; + } + + protected Rendition toApiRendition(String renditionName) + { + RenditionDefinitionRegistry2 renditionDefinitionRegistry2 = renditionService2.getRenditionDefinitionRegistry2(); + RenditionDefinition2 renditionDefinition = renditionDefinitionRegistry2.getRenditionDefinition(renditionName); + ContentInfo contentInfo = new ContentInfo(renditionDefinition.getTargetMimetype(), + getMimeTypeDisplayName(renditionDefinition.getTargetMimetype()), null, null); + + Rendition apiRendition = new Rendition(); + apiRendition.setId(renditionName); + apiRendition.setContent(contentInfo); + apiRendition.setStatus(RenditionStatus.NOT_CREATED); + + return apiRendition; + } + + private NodeRef validateNode(StoreRef storeRef, final String nodeId, String versionLabelId, Parameters parameters) + { + if (nodeId == null) + { + throw new InvalidArgumentException("Missing nodeId"); + } + + NodeRef nodeRef = nodes.validateNode(storeRef, nodeId); + // check if the node represents a file + isContentFile(nodeRef); + + nodeRef = findVersionIfApplicable(nodeRef, versionLabelId); + + return nodeRef; + } + + private NodeRef findVersionIfApplicable(NodeRef nodeRef, String versionLabelId) + { + if (versionLabelId != null) + { + nodeRef = nodes.validateOrLookupNode(nodeRef.getId(), null); + VersionHistory vh = versionService.getVersionHistory(nodeRef); + if (vh != null) + { + try + { + Version v = vh.getVersion(versionLabelId); + nodeRef = VersionUtil.convertNodeRef(v.getFrozenStateNodeRef()); + } + catch (VersionDoesNotExistException vdne) + { + throw new NotFoundException("Couldn't find version: [" + nodeRef.getId() + ", " + versionLabelId + "]"); + } + } + } + + return nodeRef; + } + + private void isContentFile(NodeRef nodeRef) + { + if (!nodes.isSubClass(nodeRef, ContentModel.PROP_CONTENT, false)) + { + throw new InvalidArgumentException("Node id '" + nodeRef.getId() + "' does not represent a file."); + } + } + + private String getMimeTypeDisplayName(String mimeType) + { + return mimetypeService.getDisplaysByMimetype().get(mimeType); + } + + private ContentData getContentData(NodeRef nodeRef, boolean validate) + { + ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); + if (validate && !ContentData.hasContent(contentData)) + { + throw new InvalidArgumentException("Node id '" + nodeRef.getId() + "' has no content."); + } + return contentData; + } + + private String getMimeType(NodeRef nodeRef) + { + ContentData contentData = getContentData(nodeRef, true); + return contentData.getMimetype(); + } + + private String getStatus(Parameters parameters) + { + Query query = parameters.getQuery(); + String status = null; + if (query != null) + { + // Filtering via "where" clause + MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(RENDITION_STATUS_COLLECTION_EQUALS_QUERY_PROPERTIES, null); + QueryHelper.walk(query, propertyWalker); + + status = propertyWalker.getProperty(PARAM_STATUS, WhereClauseParser.EQUALS); + } + return status; + } + + @Override + public DirectAccessUrl requestContentUrl(String nodeId, String versionId, String renditionId, DirectAccessUrlRequest directAccessUrlRequest) + { + final NodeRef validatedNodeRef = validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId, versionId, null); + NodeRef renditionNodeRef = getRenditionByName(validatedNodeRef, renditionId, null); + + if (renditionNodeRef == null) + { + throw new NotFoundException("The rendition with id: " + renditionId + " was not found."); + } + + return nodes.requestContentUrl(renditionNodeRef, directAccessUrlRequest); + } +} + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteImportPackageHandler.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteImportPackageHandler.java new file mode 100644 index 00000000000..97a0d69d7e7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteImportPackageHandler.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.service.cmr.view.ImportPackageHandler; +import org.alfresco.service.cmr.view.ImporterException; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; + +// note: based on HomeSiteImportPackageHandler in Cloud/Thor module +public class SiteImportPackageHandler implements ImportPackageHandler +{ + private final static String SITEID_PLACEHOLDER = "${siteId}"; + + private SiteSurfConfig config; + private String siteId; + + public SiteImportPackageHandler(SiteSurfConfig config, String siteId) + { + this.config = config; + this.siteId = siteId; + } + + @Override + public void startImport() + { + } + + @Override + public Reader getDataStream() + { + return new StringReader(config.getImportView()); + } + + @Override + public InputStream importStream(String contentPath) + { + String content = config.getImportContent(contentPath); + if (content == null) + { + return null; + } + + String siteContent = content.replace(SITEID_PLACEHOLDER, siteId); + try + { + return new ByteArrayInputStream(siteContent.getBytes("UTF-8")); + } + catch(UnsupportedEncodingException e) + { + throw new ImporterException("Failed to read content " + contentPath, e); + } + } + + @Override + public void endImport() + { + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteMembershipRequestsExceptionHandler.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteMembershipRequestsExceptionHandler.java new file mode 100644 index 00000000000..43b212b929c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteMembershipRequestsExceptionHandler.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.cmr.invitation.InvitationExceptionForbidden; + +public class SiteMembershipRequestsExceptionHandler extends DefaultExceptionHandler +{ + @Override + public boolean handle(Throwable t) + { + if (t instanceof AccessDeniedException || t instanceof InvitationExceptionForbidden) + { + // Note: security, no message to indicate why + throw new NotFoundException(); + } + return false; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteMembershipRequestsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteMembershipRequestsImpl.java new file mode 100644 index 00000000000..6cc008c7316 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteMembershipRequestsImpl.java @@ -0,0 +1,692 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.invitation.InvitationSearchCriteriaImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.authority.UnknownAuthorityException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.repo.tenant.NetworksService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.SiteMembershipRequests; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.api.model.SiteMembershipApproval; +import org.alfresco.rest.api.model.SiteMembershipRejection; +import org.alfresco.rest.api.model.SiteMembershipRequest; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.cmr.invitation.Invitation; +import org.alfresco.service.cmr.invitation.Invitation.ResourceType; +import org.alfresco.service.cmr.invitation.InvitationExceptionForbidden; +import org.alfresco.service.cmr.invitation.InvitationExceptionNotFound; +import org.alfresco.service.cmr.invitation.InvitationSearchCriteria.InvitationType; +import org.alfresco.service.cmr.invitation.InvitationService; +import org.alfresco.service.cmr.invitation.ModeratedInvitation; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Public REST API: centralises access to site membership requests and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class SiteMembershipRequestsImpl implements SiteMembershipRequests +{ + private static final Log logger = LogFactory.getLog(SiteMembershipRequestsImpl.class); + + // Default role to assign to the site membership request + public final static String DEFAULT_ROLE = SiteModel.SITE_CONSUMER; + + // List site memberships filtering (via where clause) + private final static Set LIST_SITE_MEMBERSHIPS_EQUALS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(new String[] { PARAM_SITE_ID, PARAM_PERSON_ID })); + + private People people; + private Sites sites; + private SiteService siteService; + private NodeService nodeService; + private InvitationService invitationService; + private NetworksService networksService; + + public void setNetworksService(NetworksService networksService) + { + this.networksService = networksService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPeople(People people) + { + this.people = people; + } + + public void setSites(Sites sites) + { + this.sites = sites; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setInvitationService(InvitationService invitationService) + { + this.invitationService = invitationService; + } + + private Invitation getSiteInvitation(String inviteeId, String siteId) + { + // Is there an outstanding site invite request for the invitee? + InvitationSearchCriteriaImpl criteria = new InvitationSearchCriteriaImpl(); + criteria.setInvitationType(InvitationType.MODERATED); + criteria.setInvitee(inviteeId); + criteria.setResourceName(siteId); + criteria.setResourceType(ResourceType.WEB_SITE); + List invitations = invitationService.searchInvitation(criteria); + if(invitations.size() > 1) + { + // TODO exception + throw new AlfrescoRuntimeException("There should be only one outstanding site invitation"); + } + return (invitations.size() == 0 ? null : invitations.get(0)); + } + + private List getSiteInvitations(String inviteeId) + { + InvitationSearchCriteriaImpl criteria = new InvitationSearchCriteriaImpl(); + criteria.setInvitationType(InvitationType.MODERATED); + criteria.setInvitee(inviteeId); + criteria.setResourceType(ResourceType.WEB_SITE); + List invitations = invitationService.searchInvitation(criteria); + return invitations; + } + + private SiteMembershipRequest inviteToModeratedSite(final String message, final String inviteeId, final String siteId, + final String inviteeRole) + { + ModeratedInvitation invitation = invitationService.inviteModerated(message, inviteeId, ResourceType.WEB_SITE, siteId, inviteeRole); + + SiteMembershipRequest ret = new SiteMembershipRequest(); + ret.setId(siteId); + ret.setMessage(message); + ret.setCreatedAt(invitation.getCreatedAt()); + return ret; + } + + private SiteMembershipRequest inviteToSite(String siteId, String inviteeId, String inviteeRole, String message) + { + siteService.setMembership(siteId, inviteeId, inviteeRole); + SiteMembershipRequest ret = new SiteMembershipRequest(); + ret.setId(siteId); + ret.setMessage(message); + Date createdAt = new Date(); + ret.setCreatedAt(createdAt); + return ret; + } + + private SiteMembershipRequest inviteToPublicSite(final SiteInfo siteInfo, final String message, final String inviteeId, + final String inviteeRole) + { + SiteMembershipRequest siteMembershipRequest = null; + + final String siteId = siteInfo.getShortName(); + NodeRef siteNodeRef = siteInfo.getNodeRef(); + String siteCreator = (String)nodeService.getProperty(siteNodeRef, ContentModel.PROP_CREATOR); + + final String siteNetwork = networksService.getUserDefaultNetwork(siteCreator); + if(StringUtils.isNotEmpty(siteNetwork)) + { + // MT + siteMembershipRequest = TenantUtil.runAsUserTenant(new TenantRunAsWork() + { + @Override + public SiteMembershipRequest doWork() throws Exception + { + return inviteToSite(siteId, inviteeId, inviteeRole, message); + } + }, siteCreator, siteNetwork); + } + else + { + siteMembershipRequest = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public SiteMembershipRequest doWork() throws Exception + { + return inviteToSite(siteId, inviteeId, inviteeRole, message); + } + }, siteCreator); + } + + return siteMembershipRequest; + } + + @Override + public SiteMembershipRequest createSiteMembershipRequest(String inviteeId, final SiteMembershipRequest siteInvite) + { + SiteMembershipRequest request = null; + + inviteeId = people.validatePerson(inviteeId, true); + + // Note that the order of error checking is important. The server first needs to check for the status 404 + // conditions before checking for status 400 conditions. Otherwise the server is open to a probing attack. + String siteId = siteInvite.getId(); + final SiteInfo siteInfo = sites.validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + final SiteVisibility siteVisibility = siteInfo.getVisibility(); + + if(siteVisibility.equals(SiteVisibility.PRIVATE)) + { + // note: security, no indication that this is a private site + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + + // Is the invitee already a member of the site? + boolean isMember = siteService.isMember(siteId, inviteeId); + if(isMember) + { + // yes + throw new InvalidArgumentException(inviteeId + " is already a member of site " + siteId); + } + + // Is there an outstanding site invite request for the (invitee, site)? + Invitation invitation = getSiteInvitation(inviteeId, siteId); + if(invitation != null) + { + // yes + throw new InvalidArgumentException(inviteeId + " is already invited to site " + siteId); + } + + final String inviteeRole = DEFAULT_ROLE; + String message = siteInvite.getMessage(); + if(message == null) + { + // the invitation service ignores null messages so convert to an empty message. + message = ""; + } + + if(siteVisibility.equals(SiteVisibility.MODERATED)) + { + request = inviteToModeratedSite(message, inviteeId, siteId, inviteeRole); + } + else if(siteVisibility.equals(SiteVisibility.PUBLIC)) + { + request = inviteToPublicSite(siteInfo, message, inviteeId, inviteeRole); + } + else + { + // note: security, no indication that this is a private site + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + + return request; + } + + @Override + public SiteMembershipRequest updateSiteMembershipRequest(String inviteeId, final SiteMembershipRequest siteInvite) + { + SiteMembershipRequest updatedSiteInvite = null; + + inviteeId = people.validatePerson(inviteeId, true); + + String siteId = siteInvite.getId(); + SiteInfo siteInfo = sites.validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + String message = siteInvite.getMessage(); + if(message == null) + { + // the invitation service ignores null messages so convert to an empty message. + message = ""; + } + + try + { + ModeratedInvitation updatedInvitation = invitationService.updateModeratedInvitation(inviteeId, siteId, message); + if(updatedInvitation == null) + { + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + updatedSiteInvite = getSiteMembershipRequest(updatedInvitation); + } + catch(InvitationExceptionNotFound e) + { + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + + if(updatedSiteInvite == null) + { + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + + return updatedSiteInvite; + } + + @Override + public void cancelSiteMembershipRequest(String inviteeId, String siteId) + { + inviteeId = people.validatePerson(inviteeId); + SiteInfo siteInfo = sites.validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + Invitation invitation = getSiteInvitation(inviteeId, siteId); + if(invitation == null) + { + // no such invitation + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + invitationService.cancel(invitation.getInviteId()); + } + + public SiteMembershipRequest getSiteMembershipRequest(String inviteeId, final String siteId) + { + inviteeId = people.validatePerson(inviteeId); + + SiteInfo siteInfo = AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public SiteInfo doWork() throws Exception + { + SiteInfo siteInfo = sites.validateSite(siteId); + return siteInfo; + } + }); + + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + + if(siteInfo.getVisibility().equals(SiteVisibility.MODERATED)) + { + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + String normalizedSiteId = siteInfo.getShortName(); + + Invitation invitation = getSiteInvitation(inviteeId, normalizedSiteId); + if(invitation == null) + { + // no such invitation + throw new RelationshipResourceNotFoundException(inviteeId, normalizedSiteId); + } + if(invitation instanceof ModeratedInvitation) + { + ModeratedInvitation moderatedInvitation = (ModeratedInvitation)invitation; + SiteMembershipRequest siteInvite = getSiteMembershipRequest(moderatedInvitation); + return siteInvite; + } + else + { + throw new InvalidArgumentException("Expected moderated invitation"); + } + } + else + { + // non-moderated sites cannot appear in a site membership request, so throw an exception + throw new RelationshipResourceNotFoundException(inviteeId, siteId); + } + } + + private SiteMembershipRequest getSiteMembershipRequest(ModeratedInvitation moderatedInvitation) + { + return getSiteMembershipRequest(moderatedInvitation, false); + } + + private SiteMembershipRequest getSiteMembershipRequest(ModeratedInvitation moderatedInvitation, boolean includePersonDetails) + { + SiteMembershipRequest siteMembershipRequest = null; + + ResourceType resourceType = moderatedInvitation.getResourceType(); + if(resourceType.equals(ResourceType.WEB_SITE)) + { + final String siteId = moderatedInvitation.getResourceName(); + + SiteInfo siteInfo = AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public SiteInfo doWork() throws Exception + { + SiteInfo siteInfo = sites.validateSite(siteId); + return siteInfo; + } + }); + + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + + if(siteInfo.getVisibility().equals(SiteVisibility.MODERATED)) + { + // return a site membership request only if this is a moderated site + siteMembershipRequest = new SiteMembershipRequest(); + String title = siteInfo.getTitle(); + siteMembershipRequest.setTitle(title); + siteMembershipRequest.setId(siteId); + siteMembershipRequest.setMessage(moderatedInvitation.getInviteeComments()); + siteMembershipRequest.setCreatedAt(moderatedInvitation.getCreatedAt()); + siteMembershipRequest.setModifiedAt(moderatedInvitation.getModifiedAt()); + + if (includePersonDetails) + { + Person person = people.getPerson(moderatedInvitation.getInviteeUserName()); + siteMembershipRequest.setPerson(person); + } + } + } + else + { + logger.warn("Unexpected resource type " + resourceType + " for site membership request"); + } + + return siteMembershipRequest; + } + + private List toSiteMembershipRequests(List invitations) + { + return toSiteMembershipRequests(invitations, false); + } + + private List toSiteMembershipRequests(List invitations, boolean includePersonDetails) + { + List siteMembershipRequests = new ArrayList(invitations.size()); + for(Invitation invitation : invitations) + { + if(invitation instanceof ModeratedInvitation) + { + ModeratedInvitation moderatedInvitation = (ModeratedInvitation)invitation; + SiteMembershipRequest siteMembershipRequest = getSiteMembershipRequest(moderatedInvitation, includePersonDetails); + if(siteMembershipRequest != null) + { + // note: siteMembershipRequest may be null if the site is now no longer a moderated site + // or if the invitation is malformed and does not refer to a site. + siteMembershipRequests.add(siteMembershipRequest); + } + } + else + { + // just ignore, shouldn't happen because getSiteInvitations filters by ModeratedInvitation + } + } + + return siteMembershipRequests; + } + + private CollectionWithPagingInfo createPagedSiteMembershipRequests(List siteMembershipRequests, Paging paging) + { + int skipCount = paging.getSkipCount(); + int maxItems = paging.getMaxItems(); + int max = skipCount + maxItems + 1; // to detect hasMoreItems + + // unfortunately, need to sort in memory because there's no way to get site + // membership requests sorted by title from + // the workflow apis + Collections.sort(siteMembershipRequests); + + int totalItems = siteMembershipRequests.size(); + + if (skipCount >= totalItems) + { + List empty = Collections.emptyList(); + return CollectionWithPagingInfo.asPaged(paging, empty, false, totalItems); + } + else + { + int end = Math.min(max - 1, totalItems); + boolean hasMoreItems = totalItems > end; + + siteMembershipRequests = siteMembershipRequests.subList(skipCount, end); + return CollectionWithPagingInfo.asPaged(paging, siteMembershipRequests, hasMoreItems, totalItems); + } + } + + @Override + public CollectionWithPagingInfo getPagedSiteMembershipRequests(String personId, Paging paging) + { + personId = people.validatePerson(personId, true); + + List invitations = getSiteInvitations(personId); + return createPagedSiteMembershipRequests(toSiteMembershipRequests(invitations), paging); + } + + @Override + public CollectionWithPagingInfo getPagedSiteMembershipRequests(final Parameters parameters) + { + InvitationSearchCriteriaImpl criteria = new InvitationSearchCriteriaImpl(); + criteria.setInvitationType(InvitationType.MODERATED); + criteria.setResourceType(ResourceType.WEB_SITE); + + // Parse where clause properties. + Query q = parameters.getQuery(); + if (q != null) + { + // Filtering via "where" clause. + MapBasedQueryWalker propertyWalker = createSiteMembershipRequestQueryWalker(); + QueryHelper.walk(q, propertyWalker); + + String siteId = propertyWalker.getProperty(PARAM_SITE_ID, WhereClauseParser.EQUALS, String.class); + + if (siteId != null && !siteId.isEmpty()) + { + criteria.setResourceName(siteId); + } + + String personId = propertyWalker.getProperty(PARAM_PERSON_ID, WhereClauseParser.EQUALS, String.class); + + if (personId != null && !personId.isEmpty()) + { + criteria.setInvitee(personId); + } + } + + List invitations = invitationService.searchInvitation(criteria); + return createPagedSiteMembershipRequests(toSiteMembershipRequests(invitations, true), parameters.getPaging()); + } + + @Override + public void approveSiteMembershipRequest(String siteId, String inviteeId, SiteMembershipApproval siteMembershipApproval) + { + SiteInfo siteInfo = sites.validateSite(siteId); + if (siteInfo == null) + { + throw new EntityNotFoundException(siteId); + } + + // Set the site id to the short name (to deal with case sensitivity issues with + // using the siteId from the url) + siteId = siteInfo.getShortName(); + + // Validate invitation. + Invitation invitation = getSiteInvitation(inviteeId, siteId); + if (invitation == null || !(invitation instanceof ModeratedInvitation)) + { + throw new RelationshipResourceNotFoundException(siteId, inviteeId); + } + + ModeratedInvitation moderatedInvitation = (ModeratedInvitation) invitation; + ResourceType resourceType = moderatedInvitation.getResourceType(); + + if (!resourceType.equals(ResourceType.WEB_SITE) || !SiteVisibility.MODERATED.equals(siteInfo.getVisibility())) + { + // note: security, no indication that this has a different visibility + throw new RelationshipResourceNotFoundException(siteId, inviteeId); + } + + try + { + invitationService.approve(invitation.getInviteId(), ""); + } + catch (InvitationExceptionForbidden ex) + { + throw new PermissionDeniedException(); + } + + // Workflow doesn't allow changing the role, so a new update may be required if + // approval role differs from default one. + if (siteMembershipApproval != null && !(siteMembershipApproval.getRole() == null || siteMembershipApproval.getRole().isEmpty())) + { + String role = siteMembershipApproval.getRole(); + + // Check if role chosen by moderator differs from the invite role. + if (!moderatedInvitation.getRoleName().equals(role)) + { + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + + // Update invitation with new role. + try + { + addSiteMembership(invitation.getInviteeUserName(), siteId, role, currentUserId); + } + catch (UnknownAuthorityException e) + { + logger.debug("addSiteMember: UnknownAuthorityException " + siteId + " person " + invitation.getInviteId() + " role " + role); + throw new InvalidArgumentException("Unknown role '" + role + "'"); + } + } + } + } + + @Override + public void rejectSiteMembershipRequest(String siteId, String inviteeId, SiteMembershipRejection siteMembershipRejection) + { + SiteInfo siteInfo = sites.validateSite(siteId); + if (siteInfo == null) + { + throw new EntityNotFoundException(siteId); + } + + // set the site id to the short name (to deal with case sensitivity issues with + // using the siteId from the url) + siteId = siteInfo.getShortName(); + + // Validate invitation. + Invitation invitation = getSiteInvitation(inviteeId, siteId); + if (invitation == null || !(invitation instanceof ModeratedInvitation)) + { + throw new RelationshipResourceNotFoundException(siteId, inviteeId); + } + + ModeratedInvitation moderatedInvitation = (ModeratedInvitation) invitation; + ResourceType resourceType = moderatedInvitation.getResourceType(); + + if (!resourceType.equals(ResourceType.WEB_SITE) || !SiteVisibility.MODERATED.equals(siteInfo.getVisibility())) + { + // note: security, no indication that this has a different visibility + throw new RelationshipResourceNotFoundException(siteId, inviteeId); + } + + String reason = null; + if (siteMembershipRejection != null && !(siteMembershipRejection.getComment() == null || siteMembershipRejection.getComment().isEmpty())) + { + reason = siteMembershipRejection.getComment(); + } + + try + { + invitationService.reject(invitation.getInviteId(), reason); + } + catch (InvitationExceptionForbidden ex) + { + throw new PermissionDeniedException(); + } + } + + /** + * Create query walker for listChildren. + * + * @return The created {@link MapBasedQueryWalker}. + */ + private MapBasedQueryWalker createSiteMembershipRequestQueryWalker() + { + return new MapBasedQueryWalker(LIST_SITE_MEMBERSHIPS_EQUALS_QUERY_PROPERTIES, null); + } + + private void addSiteMembership(final String invitee, final String siteName, final String role, final String runAsUser) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Void doWork() throws Exception + { + siteService.setMembership(siteName, invitee, role); + return null; + } + + }, runAsUser); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteSurfConfig.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteSurfConfig.java new file mode 100644 index 00000000000..e7e957030fb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/SiteSurfConfig.java @@ -0,0 +1,132 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.extensions.webscripts.ClassPathStoreResourceResolver; +import org.springframework.util.FileCopyUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +// note: based on HomeSiteSurfConfig in Cloud/Thor module +public class SiteSurfConfig implements ApplicationContextAware, InitializingBean +{ + private ApplicationContext applicationContext; + private String configPath; + private String packageName = "surf-config"; + private String importView; + private Map importContent; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + public void setConfigPath(String configPath) + { + if (!configPath.endsWith("/")) + { + configPath = configPath + "/"; + } + this.configPath = configPath; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + @Override + public void afterPropertiesSet() throws Exception + { + if (this.configPath == null) + { + setConfigPath("alfresco/bootstrap/site"); + } + importView = loadImportView(); + importContent = loadContent(); + } + + public String getImportView() + { + return importView; + } + + public String getImportContent(String contentPath) + { + return importContent.get(contentPath); + } + + private String loadImportView() throws IOException + { + ClassPathResource importViewResource = new ClassPathResource(configPath + packageName + ".xml"); + if (!importViewResource.exists()) + { + throw new AlfrescoRuntimeException("Cannot find site config " + importViewResource.getPath()); + } + return convert(importViewResource.getInputStream()); + } + + private Map loadContent() throws IOException + { + ClassPathStoreResourceResolver resourceResolver = new ClassPathStoreResourceResolver(applicationContext); + Resource[] contentResources = resourceResolver.getResources("classpath*:" + configPath + packageName + "/*.*"); + Map content = new HashMap(); + for (Resource contentResource : contentResources) + { + String fileName = contentResource.getFilename(); + // ignore hidden directories / files + if (fileName.startsWith(".")) + { + continue; + } + String key = packageName + "/" + fileName; + String value = convert(contentResource.getInputStream()); + content.put(key, value); + } + return content; + } + + private String convert(InputStream contentInputStream) throws IOException + { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + FileCopyUtils.copy(contentInputStream, os); + byte[] bytes = os.toByteArray(); + String content = new String(bytes, "UTF-8"); + return content; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/SitesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/SitesImpl.java new file mode 100644 index 00000000000..26c038914d0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/SitesImpl.java @@ -0,0 +1,1445 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.io.Serializable; +import java.text.Collator; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.query.CannedQuerySortDetails.SortOrder; +import org.alfresco.query.PageDetails; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.repo.node.getchildren.FilterProp; +import org.alfresco.repo.node.getchildren.FilterPropBoolean; +import org.alfresco.repo.node.getchildren.FilterPropString; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authority.UnknownAuthorityException; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.site.*; +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.*; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalkerOrSupported; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.favourites.FavouritesService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.preference.PreferenceService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteMemberInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.service.cmr.view.ImportPackageHandler; +import org.alfresco.service.cmr.view.ImporterBinding; +import org.alfresco.service.cmr.view.ImporterContentCache; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Centralises access to site services and maps between representations. + * + * @author steveglover + * @author janv + * @since publicapi1.0 + */ +public class SitesImpl implements Sites +{ + private static final Log logger = LogFactory.getLog(SitesImpl.class); + + private static final String FAVOURITE_SITES_PREFIX = "org.alfresco.share.sites.favourites."; + private static final int FAVOURITE_SITES_PREFIX_LENGTH = FAVOURITE_SITES_PREFIX.length(); + + // based on Share create site + private static final int SITE_MAXLEN_ID = 72; + private static final int SITE_MAXLEN_TITLE = 256; + private static final int SITE_MAXLEN_DESCRIPTION = 512; + + private static final String SITE_ID_VALID_CHARS_PARTIAL_REGEX = "A-Za-z0-9\\-"; + + private static final String DEFAULT_SITE_PRESET = "site-dashboard"; + private static final String PARAM_IS_MEMBER_OF_GROUP = "isMemberOfGroup"; + + private final static Map SORT_PARAMS_TO_QNAMES; + static + { + Map aMap = new HashMap<>(3); + aMap.put(PARAM_SITE_TITLE, ContentModel.PROP_TITLE); + aMap.put(PARAM_SITE_ID, ContentModel.PROP_NAME); + aMap.put(PARAM_SITE_DESCRIPTION, ContentModel.PROP_DESCRIPTION); + SORT_PARAMS_TO_QNAMES = Collections.unmodifiableMap(aMap); + } + + private final static Map SORT_SITE_MEMBERSHIP; + static + { + Map aMap = new HashMap<>(3); + aMap.put(PARAM_SITE_TITLE, SiteService.SortFields.SiteTitle); + aMap.put(SiteService.SortFields.SiteTitle.toString(), SiteService.SortFields.SiteTitle); // for backwards compat' + aMap.put(PARAM_SITE_ID, SiteService.SortFields.SiteShortName); + aMap.put(SiteService.SortFields.SiteShortName.toString(), SiteService.SortFields.SiteShortName); // for backwards compat' + aMap.put(PARAM_SITE_ROLE, SiteService.SortFields.Role); + aMap.put(SiteService.SortFields.Role.toString(), SiteService.SortFields.Role); // for backwards compat' + SORT_SITE_MEMBERSHIP = Collections.unmodifiableMap(aMap); + } + + // list children filtering (via where clause) + private final static Set LIST_SITES_EQUALS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(PARAM_VISIBILITY, PARAM_PRESET)); + + protected Nodes nodes; + protected People people; + protected NodeService nodeService; + protected DictionaryService dictionaryService; + protected SiteService siteService; + protected FavouritesService favouritesService; + protected PreferenceService preferenceService; + protected ImporterService importerService; + protected SiteSurfConfig siteSurfConfig; + protected PermissionService permissionService; + protected SiteServiceImpl siteServiceImpl; + protected AuthorityService authorityService; + + public void setPreferenceService(PreferenceService preferenceService) + { + this.preferenceService = preferenceService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setFavouritesService(FavouritesService favouritesService) + { + this.favouritesService = favouritesService; + } + + public void setPeople(People people) + { + this.people = people; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setImporterService(ImporterService importerService) + { + this.importerService = importerService; + } + + public void setSiteSurfConfig(SiteSurfConfig siteSurfConfig) + { + this.siteSurfConfig = siteSurfConfig; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public void setSiteServiceImpl(SiteServiceImpl siteServiceImpl) + { + this.siteServiceImpl = siteServiceImpl; + } + + public AuthorityService getAuthorityService() + { + return authorityService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public SiteInfo validateSite(NodeRef guid) + { + SiteInfo siteInfo = null; + + if(guid == null) + { + throw new InvalidArgumentException("guid is null"); + } + nodes.validateNode(guid); + QName type = nodeService.getType(guid); + boolean isSiteNodeRef = dictionaryService.isSubClass(type, SiteModel.TYPE_SITE); + if(isSiteNodeRef) + { + siteInfo = siteService.getSite(guid); + if(siteInfo == null) + { + // not a site + throw new InvalidArgumentException(guid.getId() + " is not a site"); + } + } + else + { + // site does not exist + throw new EntityNotFoundException(guid.getId()); + } + + return siteInfo; + } + + public SiteInfo validateSite(String siteId) + { + if(siteId == null) + { + throw new InvalidArgumentException("siteId is null"); + } + SiteInfo siteInfo = siteService.getSite(siteId); + return siteInfo; + } + + public CollectionWithPagingInfo getSiteMembers(String siteId, Parameters parameters) + { + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); + pagingRequest.setRequestTotalCountMax(100); + + MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(new HashSet<>(Collections.singletonList(PARAM_IS_MEMBER_OF_GROUP)), null);; + QueryHelper.walk(parameters.getQuery(), propertyWalker); + + Boolean expandGroups = propertyWalker.getProperty(PARAM_IS_MEMBER_OF_GROUP, WhereClauseParser.EQUALS, Boolean.class); + + if (expandGroups == null) { + expandGroups = true; + } + + final List> sort = new ArrayList>(); + sort.add(new Pair(SiteService.SortFields.LastName, Boolean.TRUE)); + sort.add(new Pair(SiteService.SortFields.FirstName, Boolean.TRUE)); + sort.add(new Pair(SiteService.SortFields.Role, Boolean.TRUE)); + sort.add(new Pair(SiteService.SortFields.Username, Boolean.TRUE)); + PagingResults pagedResults = siteService.listMembersPaged(siteId, expandGroups, sort, pagingRequest); + + List ret = pagedResults.getPage() + .stream() + .map((siteMembership) -> new SiteMember(siteMembership.getPersonId(), siteMembership.getRole(), siteMembership.isMemberOfGroup())) + .collect(Collectors.toList()); + + return CollectionWithPagingInfo.asPaged(paging, ret, pagedResults.hasMoreItems(), pagedResults.getTotalResultCount().getFirst()); + } + + public String getSiteRole(String siteId) + { + String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + return getSiteRole(siteId, personId); + } + + public String getSiteRole(String siteId, String personId) + { + return siteService.getMembersRole(siteId, personId); + } + + public Site getSite(String siteId) + { + return getSite(siteId, true); + } + + public Site getSite(String siteId, boolean includeRole) + { + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + return getSite(siteInfo, includeRole); + } + + private Site getSite(SiteInfo siteInfo, boolean includeRole) + { + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + String siteId = siteInfo.getShortName(); + String role = null; + if(includeRole) + { + role = getSiteRole(siteId); + } + return new Site(siteInfo, role); + } + + /** + * people//sites/ + * + * @param siteId String + * @param personId String + * @return MemberOfSite + */ + public MemberOfSite getMemberOfSite(String personId, String siteId) + { + MemberOfSite siteMember = null; + + personId = people.validatePerson(personId); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(personId, siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + String roleStr = siteService.getMembersRole(siteInfo.getShortName(), personId); + if(roleStr != null) + { + Site site = new Site(siteInfo, roleStr); + siteMember = new MemberOfSite(site.getId(), siteInfo.getNodeRef(), roleStr); + } + else + { + throw new RelationshipResourceNotFoundException(personId, siteId); + } + + return siteMember; + } + + public SiteMember getSiteMember(String personId, String siteId) + { + SiteMember siteMember = null; + + personId = people.validatePerson(personId); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + logger.debug("Site does not exist: "+siteId); + throw new RelationshipResourceNotFoundException(personId, siteId); + } + siteId = siteInfo.getShortName(); + + logger.debug("Getting member role for "+siteId+ " person "+personId); + String role = siteService.getMembersRole(siteId, personId); + if(role != null) + { + siteMember = new SiteMember(personId, role); + } + else + { + logger.debug("Getting member role but role is null"); + throw new RelationshipResourceNotFoundException(personId, siteId); + } + + return siteMember; + } + + public SiteMember addSiteMember(String siteId, SiteMember siteMember) + { + String personId = people.validatePerson(siteMember.getPersonId()); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + logger.debug("addSiteMember: site does not exist "+siteId+ " person "+personId); + throw new EntityNotFoundException(siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + String role = siteMember.getRole(); + if(role == null) + { + logger.debug("addSiteMember: Must provide a role "+siteMember); + throw new InvalidArgumentException("Must provide a role"); + } + + if(siteService.isMember(siteId, personId)) + { + logger.debug("addSiteMember: "+ personId + " is already a member of site " + siteId); + throw new ConstraintViolatedException(personId + " is already a member of site " + siteId); + } + + if(!siteService.canAddMember(siteId, personId, role)) + { + logger.debug("addSiteMember: PermissionDeniedException "+siteId+ " person "+personId+ " role "+role); + throw new PermissionDeniedException(); + } + + try + { + siteService.setMembership(siteId, personId, role); + } + catch (UnknownAuthorityException e) + { + logger.debug("addSiteMember: UnknownAuthorityException "+siteId+ " person "+personId+ " role "+role); + throw new InvalidArgumentException("Unknown role '" + role + "'"); + } + return siteMember; + } + + public void removeSiteMember(String personId, String siteId) + { + personId = people.validatePerson(personId); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(personId, siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + boolean isMember = siteService.isMember(siteId, personId); + if(!isMember) + { + throw new InvalidArgumentException(); + } + String role = siteService.getMembersRole(siteId, personId); + if(role != null) + { + if(role.equals(SiteModel.SITE_MANAGER)) + { + int numAuthorities = siteService.countAuthoritiesWithRole(siteId, SiteModel.SITE_MANAGER); + if(numAuthorities <= 1) + { + throw new InvalidArgumentException("Can't remove last manager of site " + siteId); + } + siteService.removeMembership(siteId, personId); + } + else + { + siteService.removeMembership(siteId, personId); + } + } + else + { + throw new AlfrescoRuntimeException("Unable to determine role of site member"); + } + } + + public SiteMember updateSiteMember(String siteId, SiteMember siteMember) + { + String siteMemberId = siteMember.getPersonId(); + if(siteMemberId == null) + { + throw new InvalidArgumentException("Member id is null"); + } + siteMemberId = people.validatePerson(siteMemberId); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + siteId = siteInfo.getShortName(); + String siteRole = siteMember.getRole(); + if(siteRole == null) + { + throw new InvalidArgumentException("Must provide a role"); + } + + /* MNT-10551 : fix */ + if (!siteService.isMember(siteId, siteMember.getPersonId())) + { + throw new InvalidArgumentException("User is not a member of the site"); + } + + try + { + siteService.setMembership(siteId, siteMember.getPersonId(), siteRole); + } + catch (UnknownAuthorityException e) + { + throw new InvalidArgumentException("Unknown role '" + siteRole + "'"); + } + return siteMember; + } + + public CollectionWithPagingInfo getSites(String personId, Parameters parameters) + { + Paging paging = parameters.getPaging(); + + personId = people.validatePerson(personId); + + PagingRequest pagingRequest = Util.getPagingRequest(paging); + + // get the sorting options + List> sortPairs = new ArrayList<>(parameters.getSorting().size()); + + List sortCols = parameters.getSorting(); + if ((sortCols != null) && (sortCols.size() > 0)) + { + for (SortColumn sortCol : sortCols) + { + SiteService.SortFields sortProp = SORT_SITE_MEMBERSHIP.get(sortCol.column); + if (sortProp == null) + { + throw new InvalidArgumentException("Invalid sort field: "+sortCol.column); + } + sortPairs.add(new Pair<>(sortProp, (sortCol.asc ? SortOrder.ASCENDING : SortOrder.DESCENDING))); + } + } + else + { + // default sort order + sortPairs.add(new Pair( + SiteService.SortFields.SiteTitle, + SortOrder.ASCENDING )); + } + + // get the unsorted list of site memberships + List siteMembers = siteService.listSiteMemberships(personId, 0); + + // sort the list of site memberships + int totalSize = siteMembers.size(); + final List sortedSiteMembers = new ArrayList<>(siteMembers); + Collections.sort(sortedSiteMembers, new SiteMembershipComparator( + sortPairs, + SiteMembershipComparator.Type.SITES)); + + PageDetails pageDetails = PageDetails.getPageDetails(pagingRequest, totalSize); + List ret = new ArrayList<>(totalSize); + + List filterProps = getFilterPropListOfSites(parameters); + + int counter; + int totalItems = 0; + Iterator it = sortedSiteMembers.iterator(); + for(counter = 0; it.hasNext();) + { + SiteMembership siteMember = it.next(); + + if (filterProps != null && !includeFilter(siteMember, filterProps)) + { + continue; + } + + if(counter < pageDetails.getSkipCount()) + { + totalItems++; + counter++; + continue; + } + + if (counter <= pageDetails.getEnd() - 1) + { + SiteInfo siteInfo = siteMember.getSiteInfo(); + MemberOfSite memberOfSite = new MemberOfSite(siteInfo.getShortName(), siteInfo.getNodeRef(), siteMember.getRole()); + ret.add(memberOfSite); + + counter++; + } + + totalItems++; + } + return CollectionWithPagingInfo.asPaged(paging, ret, counter < totalItems, totalItems); + + } + + public SiteContainer getSiteContainer(String siteId, String containerId) + { + // check site and container node validity + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(siteId, containerId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + NodeRef containerNodeRef = siteService.getContainer(siteId, containerId); + if(containerNodeRef == null) + { + throw new RelationshipResourceNotFoundException(siteId, containerId); + } + + // check that the containerId is actually a container for the specified site + SiteInfo testSiteInfo = siteService.getSite(containerNodeRef); + if(testSiteInfo == null) + { + throw new RelationshipResourceNotFoundException(siteId, containerId); + } + else + { + if(!testSiteInfo.getShortName().equals(siteId)) + { + throw new RelationshipResourceNotFoundException(siteId, containerId); + } + } + + String folderId = (String)nodeService.getProperty(containerNodeRef, SiteModel.PROP_COMPONENT_ID); + + SiteContainer siteContainer = new SiteContainer(folderId, containerNodeRef); + return siteContainer; + } + + public PagingResults getSiteContainers(String siteId, Paging paging) + { + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + + final PagingResults pagingResults = siteService.listContainers(siteInfo.getShortName(), Util.getPagingRequest(paging)); + List containerFileInfos = pagingResults.getPage(); + final List siteContainers = new ArrayList(containerFileInfos.size()); + for(FileInfo containerFileInfo : containerFileInfos) + { + NodeRef nodeRef = containerFileInfo.getNodeRef(); + String containerId = (String)nodeService.getProperty(nodeRef, SiteModel.PROP_COMPONENT_ID); + SiteContainer siteContainer = new SiteContainer(containerId, nodeRef); + siteContainers.add(siteContainer); + } + + return new PagingResults() + { + @Override + public List getPage() + { + return siteContainers; + } + + @Override + public boolean hasMoreItems() + { + return pagingResults.hasMoreItems(); + } + + @Override + public Pair getTotalResultCount() + { + return pagingResults.getTotalResultCount(); + } + + @Override + public String getQueryExecutionId() + { + return null; + } + }; + } + + public CollectionWithPagingInfo getSites(final Parameters parameters) + { + final BeanPropertiesFilter filter = parameters.getFilter(); + + Paging paging = parameters.getPaging(); + PagingRequest pagingRequest = Util.getPagingRequest(paging); +// pagingRequest.setRequestTotalCountMax(requestTotalCountMax) + + List> sortProps = new ArrayList>(); + List sortCols = parameters.getSorting(); + if ((sortCols != null) && (sortCols.size() > 0)) + { + for (SortColumn sortCol : sortCols) + { + QName sortPropQName = SORT_PARAMS_TO_QNAMES.get(sortCol.column); + if (sortPropQName == null) + { + throw new InvalidArgumentException("Invalid sort field: "+sortCol.column); + } + sortProps.add(new Pair<>(sortPropQName, (sortCol.asc ? Boolean.TRUE : Boolean.FALSE))); + } + } + else + { + // default sort order + sortProps.add(new Pair<>(ContentModel.PROP_TITLE, Boolean.TRUE)); + } + + List filterProps = getFilterPropListOfSites(parameters); + + final PagingResults pagingResult = siteService.listSites(filterProps, sortProps, pagingRequest); + final List sites = pagingResult.getPage(); + int totalItems = pagingResult.getTotalResultCount().getFirst(); + final String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + List page = new AbstractList() + { + @Override + public Site get(int index) + { + SiteInfo siteInfo = sites.get(index); + + String role = null; + if(filter.isAllowed(Site.ROLE)) + { + role = siteService.getMembersRole(siteInfo.getShortName(), personId); + } + return new Site(siteInfo, role); + } + + @Override + public int size() + { + return sites.size(); + } + }; + + return CollectionWithPagingInfo.asPaged(paging, page, pagingResult.hasMoreItems(), totalItems); + } + + private SiteVisibility getSiteVisibilityFromParam(String siteVisibilityStr) + { + SiteVisibility visibility; + try + { + // Create the enum value from the string + visibility = SiteVisibility.valueOf(siteVisibilityStr); + } + catch (IllegalArgumentException e) + { + throw new InvalidArgumentException("Site visibility is invalid (expected eg. PUBLIC, PRIVATE, MODERATED)"); + } + + return visibility; + } + + private List getFilterPropListOfSites(final Parameters parameters) + { + List filterProps = new ArrayList(); + Query q = parameters.getQuery(); + if (q != null) + { + MapBasedQueryWalkerOrSupported propertyWalker = new MapBasedQueryWalkerOrSupported(LIST_SITES_EQUALS_QUERY_PROPERTIES, null); + QueryHelper.walk(q, propertyWalker); + + String siteVisibilityStr = propertyWalker.getProperty(PARAM_VISIBILITY, WhereClauseParser.EQUALS, String.class); + if (siteVisibilityStr != null && !siteVisibilityStr.isEmpty()) + { + SiteVisibility siteVisibility = getSiteVisibilityFromParam(siteVisibilityStr); + filterProps.add(new FilterPropString(SiteModel.PROP_SITE_VISIBILITY, siteVisibility.name(), FilterPropString.FilterTypeString.EQUALS)); + } + + String sitePreset = propertyWalker.getProperty(PARAM_PRESET, WhereClauseParser.EQUALS, String.class); + if (sitePreset != null && !sitePreset.isEmpty()) + { + filterProps.add(new FilterPropString(SiteModel.PROP_SITE_PRESET, sitePreset, FilterPropString.FilterTypeString.EQUALS)); + } + } + + // expected null or non-empty list + return filterProps.isEmpty() ? null : filterProps; + } + + private boolean includeFilter(SiteMembership siteMembership, List filterProps) + { + Map propVals = new HashMap<>(); + propVals.put(SiteModel.PROP_SITE_VISIBILITY, siteMembership.getSiteInfo().getVisibility().name()); + propVals.put(SiteModel.PROP_SITE_PRESET, siteMembership.getSiteInfo().getSitePreset()); + return includeFilter(propVals, filterProps); + } + + // note: currently inclusive and OR-based + private boolean includeFilter(Map propVals, List filterProps) + { + for (FilterProp filterProp : filterProps) + { + Serializable propVal = propVals.get(filterProp.getPropName()); + if (propVal != null) + { + if ((filterProp instanceof FilterPropString) && (propVal instanceof String)) + { + String val = (String)propVal; + String filter = (String)filterProp.getPropVal(); + + switch ((FilterPropString.FilterTypeString)filterProp.getFilterType()) + { + case STARTSWITH: + if (val.startsWith(filter)) + { + return true; + } + break; + case STARTSWITH_IGNORECASE: + if (val.toLowerCase().startsWith(filter.toLowerCase())) + { + return true; + } + break; + case EQUALS: + if (val.equals(filter)) + { + return true; + } + break; + case EQUALS_IGNORECASE: + if (val.equalsIgnoreCase(filter)) + { + return true; + } + break; + case ENDSWITH: + if (val.endsWith(filter)) + { + return true; + } + break; + case ENDSWITH_IGNORECASE: + if (val.toLowerCase().endsWith(filter.toLowerCase())) + { + return true; + } + break; + case MATCHES: + if (val.matches(filter)) + { + return true; + } + break; + case MATCHES_IGNORECASE: + if (val.toLowerCase().matches(filter.toLowerCase())) + { + return true; + } + break; + default: + } + } + } + + if ((filterProp instanceof FilterPropBoolean) && (propVal instanceof Boolean)) + { + Boolean val = (Boolean)propVal; + Boolean filter = (Boolean)filterProp.getPropVal(); + + return (val == filter); + } + } + + return false; + } + + public FavouriteSite getFavouriteSite(String personId, String siteId) + { + personId = people.validatePerson(personId); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(personId, siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + NodeRef nodeRef = siteInfo.getNodeRef(); + + if(favouritesService.isFavourite(personId, nodeRef)) + { + String role = getSiteRole(siteId, personId); + return new FavouriteSite(siteInfo, role); + } + else + { + throw new RelationshipResourceNotFoundException(personId, siteId); + } + } + + public void addFavouriteSite(String personId, FavouriteSite favouriteSite) + { + personId = people.validatePerson(personId); + String siteId = favouriteSite.getId(); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + // set the site id to the short name (to deal with case sensitivity issues with using the siteId from the url) + siteId = siteInfo.getShortName(); + + StringBuilder prefKey = new StringBuilder(FAVOURITE_SITES_PREFIX); + prefKey.append(siteId); + String value = (String)preferenceService.getPreference(personId, prefKey.toString()); + boolean isFavouriteSite = (value != null && value.equalsIgnoreCase("true")); + + if(isFavouriteSite) + { + throw new ConstraintViolatedException("Site " + siteId + " is already a favourite site"); + } + + prefKey = new StringBuilder(FAVOURITE_SITES_PREFIX); + prefKey.append(siteId); + + Map preferences = new HashMap(1); + preferences.put(prefKey.toString(), Boolean.TRUE); + preferenceService.setPreferences(personId, preferences); + } + + public void removeFavouriteSite(String personId, String siteId) + { + personId = people.validatePerson(personId); + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + // site does not exist + throw new RelationshipResourceNotFoundException(personId, siteId); + } + siteId = siteInfo.getShortName(); + + StringBuilder prefKey = new StringBuilder(FAVOURITE_SITES_PREFIX); + prefKey.append(siteId); + String value = (String)preferenceService.getPreference(personId, prefKey.toString()); + boolean isFavouriteSite = (value != null && value.equalsIgnoreCase("true")); + + if(!isFavouriteSite) + { + throw new NotFoundException("Site " + siteId + " is not a favourite site"); + } + + preferenceService.clearPreferences(personId, prefKey.toString()); + } + + private PagingResults getFavouriteSites(String userName, PagingRequest pagingRequest) + { + final Collator collator = Collator.getInstance(); + + final Set sortedFavouriteSites = new TreeSet(new Comparator() + { + @Override + public int compare(SiteInfo o1, SiteInfo o2) + { + return collator.compare(o1.getTitle(), o2.getTitle()); + } + }); + + Map prefs = preferenceService.getPreferences(userName, FAVOURITE_SITES_PREFIX); + for(Entry entry : prefs.entrySet()) + { + boolean isFavourite = false; + Serializable s = entry.getValue(); + if(s instanceof Boolean) + { + isFavourite = (Boolean)s; + } + if(isFavourite) + { + String siteShortName = entry.getKey().substring(FAVOURITE_SITES_PREFIX_LENGTH).replace(".favourited", ""); + SiteInfo siteInfo = siteService.getSite(siteShortName); + if(siteInfo != null) + { + sortedFavouriteSites.add(siteInfo); + } + } + } + + int totalSize = sortedFavouriteSites.size(); + final PageDetails pageDetails = PageDetails.getPageDetails(pagingRequest, totalSize); + + final List page = new ArrayList(pageDetails.getPageSize()); + Iterator it = sortedFavouriteSites.iterator(); + for(int counter = 0; counter < pageDetails.getEnd() && it.hasNext(); counter++) + { + SiteInfo favouriteSite = it.next(); + + if(counter < pageDetails.getSkipCount()) + { + continue; + } + + if(counter > pageDetails.getEnd() - 1) + { + break; + } + + page.add(favouriteSite); + } + + return new PagingResults() + { + @Override + public List getPage() + { + return page; + } + + @Override + public boolean hasMoreItems() + { + return pageDetails.hasMoreItems(); + } + + @Override + public Pair getTotalResultCount() + { + Integer total = Integer.valueOf(sortedFavouriteSites.size()); + return new Pair(total, total); + } + + @Override + public String getQueryExecutionId() + { + return null; + } + }; + } + + public CollectionWithPagingInfo getFavouriteSites(String personId, Parameters parameters) + { + personId = people.validatePerson(personId); + + Paging paging = parameters.getPaging(); + BeanPropertiesFilter filter = parameters.getFilter(); + + PagingResults favouriteSites = getFavouriteSites(personId, Util.getPagingRequest(paging)); + List favourites = new ArrayList(favouriteSites.getPage().size()); + for(SiteInfo favouriteSite : favouriteSites.getPage()) + { + String role = null; + if(filter.isAllowed(Site.ROLE)) + { + role = getSiteRole(favouriteSite.getShortName(), personId); + } + FavouriteSite favourite = new FavouriteSite(favouriteSite, role); + favourites.add(favourite); + } + + return CollectionWithPagingInfo.asPaged(paging, favourites, favouriteSites.hasMoreItems(), favouriteSites.getTotalResultCount().getFirst()); + } + + public void deleteSite(String siteId, Parameters parameters) + { + boolean isSiteAdmin = siteService.isSiteAdmin(AuthenticationUtil.getFullyAuthenticatedUser()); + SiteInfo siteInfo = validateSite(siteId); + if (siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + siteId = siteInfo.getShortName(); + + NodeRef siteNodeRef = siteInfo.getNodeRef(); + + // belt-and-braces - double-check before purge/delete (rather than + // rollback) + if ((isSiteAdmin == false) && (permissionService.hasPermission(siteNodeRef, PermissionService.DELETE) != AccessStatus.ALLOWED)) + { + throw new AccessDeniedException("Cannot delete site: " + siteId); + } + + // default false (if not provided) + boolean permanentDelete = Boolean.valueOf(parameters.getParameter(PARAM_PERMANENT)); + + if (permanentDelete == true) + { + // Set as temporary to delete node instead of archiving. + nodeService.addAspect(siteNodeRef, ContentModel.ASPECT_TEMPORARY, null); + + // bypassing trashcan means that purge behaviour will not fire, so + // explicitly force cleanup here + siteServiceImpl.beforePurgeNode(siteNodeRef); + } + + siteService.deleteSite(siteId); + } + + /** + * Uses site service for creating site info + * + * Extracted this call in a separate method because it might be needed to + * call different site service method when creating site info (e.g. + * siteService.createSite(String, String, String, String, SiteVisibility, QName)) + * + * @param site + * @return + */ + protected SiteInfo createSite(Site site) + { + if (site.getPreset() != null) + { + throw new InvalidArgumentException("Site preset should not be set"); + } + return siteService.createSite(DEFAULT_SITE_PRESET, site.getId(), site.getTitle(), site.getDescription(), site.getVisibility()); + } + + /** + * Create default/fixed preset (Share) site - with DocLib container/component + * + * @param site + * @return + */ + public Site createSite(Site site, Parameters parameters) + { + // note: if site id is null then will be generated from the site title + site = validateSite(site); + + SiteInfo siteInfo = null; + try + { + siteInfo = createSite(site); + } + catch (SiteServiceException sse) + { + if (sse.getMsgId().equals("site_service.unable_to_create")) + { + throw new ConstraintViolatedException(sse.getMessage()); + } + else + { + throw sse; + } + } + + String siteId = siteInfo.getShortName(); + NodeRef siteNodeRef = siteInfo.getNodeRef(); + + // default false (if not provided) + boolean skipShareSurfConfig = Boolean.valueOf(parameters.getParameter(PARAM_SKIP_SURF_CONFIGURATION)); + if (skipShareSurfConfig == false) + { + // import default/fixed preset Share surf config + importSite(siteId, siteNodeRef); + } + + // pre-create doclib + siteService.createContainer(siteId, SiteService.DOCUMENT_LIBRARY, ContentModel.TYPE_FOLDER, null); + + // default false (if not provided) + boolean skipAddToFavorites = Boolean.valueOf(parameters.getParameter(PARAM_SKIP_ADDTOFAVORITES)); + if (skipAddToFavorites == false) + { + String personId = AuthenticationUtil.getFullyAuthenticatedUser(); + favouritesService.addFavourite(personId, siteNodeRef); // ignore result + } + + return getSite(siteInfo, true); + } + + @Override + public Site updateSite(String siteId, SiteUpdate update, Parameters parameters) + { + if (logger.isDebugEnabled()) + { + logger.debug("Updating site, ID: "+siteId+", site data: "+update+", parameters: "+parameters); + } + + // Get the site by ID (aka short name) + SiteInfo siteInfo = validateSite(siteId); + if (siteInfo == null) + { + // site does not exist + throw new EntityNotFoundException(siteId); + } + + // Bind any provided values to the site info, allowing for "partial" updates. + if (update.wasSet(Site.TITLE)) + { + siteInfo.setTitle(update.getTitle()); + } + if (update.wasSet(Site.DESCRIPTION)) + { + siteInfo.setDescription(update.getDescription()); + } + if (update.wasSet(Site.VISIBILITY)) + { + siteInfo.setVisibility(update.getVisibility()); + } + + // Validate the new details + validateSite(new Site(siteInfo, null)); + + // Perform the actual update. + siteService.updateSite(siteInfo); + + return getSite(siteId); + } + + protected Site validateSite(Site site) + { + // site title - mandatory + String siteTitle = site.getTitle(); + if ((siteTitle == null) || siteTitle.isEmpty()) + { + throw new InvalidArgumentException("Site title is expected: "+siteTitle); + } + else if (siteTitle.length() > SITE_MAXLEN_TITLE) + { + throw new InvalidArgumentException("Site title exceeds max length of "+SITE_MAXLEN_TITLE+" characters"); + } + + SiteVisibility siteVisibility = site.getVisibility(); + if (siteVisibility == null) + { + throw new InvalidArgumentException("Site visibility is expected: "+siteTitle+" (eg. PUBLIC, PRIVATE, MODERATED)"); + } + + String siteId = site.getId(); + if (siteId == null) + { + // generate a site id from title (similar to Share create site dialog) + siteId = siteTitle. + trim(). // trim leading & trailing whitespace + replaceAll("[^"+SITE_ID_VALID_CHARS_PARTIAL_REGEX+" ]",""). // remove special characters (except spaces) + replaceAll(" +", " "). // collapse multiple spaces to single space + replace(" ","-"). // replaces spaces with dashs + toLowerCase(); // lowercase :-) + } + else + { + if (! siteId.matches("^["+SITE_ID_VALID_CHARS_PARTIAL_REGEX+"]+")) + { + throw new InvalidArgumentException("Invalid site id - should consist of alphanumeric/dash characters"); + } + } + + if (siteId.length() > SITE_MAXLEN_ID) + { + throw new InvalidArgumentException("Site id exceeds max length of " + SITE_MAXLEN_ID + " characters"); + } + + site.setId(siteId); + + String siteDescription = site.getDescription(); + + if (siteDescription == null) + { + // workaround: to avoid Share error (eg. in My Sites dashlet / freemarker template) + site.setDescription(""); + } + + if ((siteDescription != null) && (siteDescription.length() > SITE_MAXLEN_DESCRIPTION)) + { + throw new InvalidArgumentException("Site description exceeds max length of "+SITE_MAXLEN_DESCRIPTION+" characters"); + } + + return site; + } + + private void importSite(final String siteId, final NodeRef siteNodeRef) + { + ImportPackageHandler acpHandler = new SiteImportPackageHandler(siteSurfConfig, siteId); + Location location = new Location(siteNodeRef); + ImporterBinding binding = new ImporterBinding() + { + @Override + public String getValue(String key) + { + if (key.equals("siteId")) + { + return siteId; + } + return null; + } + + @Override + public UUID_BINDING getUUIDBinding() + { + return UUID_BINDING.CREATE_NEW; + } + + @Override + public QName[] getExcludedClasses() + { + return null; + } + + @Override + public boolean allowReferenceWithinTransaction() + { + return false; + } + + @Override + public ImporterContentCache getImportConentCache() + { + return null; + } + }; + importerService.importView(acpHandler, location, binding, null); + } + + @Override + public CollectionWithPagingInfo getSiteGroupMemberships(String siteId, Parameters parameters) + { + validateSite(siteId); + + PagingRequest pagingRequest = Util.getPagingRequest(parameters.getPaging()); + pagingRequest.setRequestTotalCountMax(100); + PagingResults pagedResults = siteService.listGroupMembersPaged(siteId, new ArrayList<>(), pagingRequest); + List groups = pagedResults.getPage().stream().map(siteMembership -> new SiteGroup(siteMembership.getId(), siteMembership.getRole())).collect(Collectors.toList()); + return CollectionWithPagingInfo.asPaged(parameters.getPaging(), groups, pagedResults.hasMoreItems(), pagedResults.getTotalResultCount().getFirst()); + } + + @Override + public SiteGroup addSiteGroupMembership(String siteId, SiteGroup group) + { + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + logger.debug("Site does not exist: " + siteId); + throw new EntityNotFoundException(siteId); + } + + validateGroup(group.getId()); + + SiteMemberInfo groupInfo = siteService.getMembersRoleInfo(siteId, group.getId()); + if(groupInfo != null) + { + logger.debug("addSiteGroupMembership: "+ group.getId() + " is already a member of site " + siteId); + throw new ConstraintViolatedException(group.getId() + " is already a member of site " + siteId); + } + if(group.getRole() == null) + { + logger.debug("Getting member role but role is null"); + throw new RelationshipResourceNotFoundException(group.getId(), siteId); + } + + siteService.setMembership(siteId, group.getId(), group.getRole()); + return group; + } + + @Override + public SiteGroup getSiteGroupMembership(String siteId, String groupId) + { + SiteMemberInfo groupInfo = isMemberOfSite(siteId, groupId); + return new SiteGroup(groupId, groupInfo.getMemberRole()); + } + + @Override + public SiteGroup updateSiteGroupMembership(String siteId, SiteGroup group) + { + isMemberOfSite(siteId, group.getId()); + siteService.setMembership(siteId, group.getId(), group.getRole()); + return group; + } + + @Override + public void removeSiteGroupMembership(String siteId, String groupId) + { + isMemberOfSite(siteId, groupId); + String role = this.siteService.getMembersRole(siteId, groupId); + if(role != null) + { + if(role.equals(SiteModel.SITE_MANAGER)) + { + int numAuthorities = this.siteService.countAuthoritiesWithRole(siteId, SiteModel.SITE_MANAGER); + if(numAuthorities <= 1) + { + throw new InvalidArgumentException("Can't remove last manager of site " + siteId); + } + this.siteService.removeMembership(siteId, groupId); + } + else + { + this.siteService.removeMembership(siteId, groupId); + } + } + else + { + throw new AlfrescoRuntimeException("Unable to determine role of site member"); + } + + } + + private SiteMemberInfo isMemberOfSite(String siteId, String id) + { + + SiteInfo siteInfo = validateSite(siteId); + if(siteInfo == null) + { + logger.debug("Site does not exist: " + siteId); + throw new EntityNotFoundException(siteId); + } + + validateGroup(id); + + SiteMemberInfo memberInfo = this.siteService.getMembersRoleInfo(siteId, id); + if(memberInfo == null) + { + logger.debug("Given authority is not a member of the site"); + throw new InvalidArgumentException("Given authority is not a member of the site"); + } + if(memberInfo.getMemberRole() == null) + { + logger.debug("Getting authority role but role is null"); + throw new RelationshipResourceNotFoundException(memberInfo.getMemberName(), siteId); + } + return memberInfo; + } + + private void validateGroup(String groupId) throws EntityNotFoundException + { + String authorityName = authorityService.getName(AuthorityType.GROUP, groupId); + if(authorityName == null) + { + logger.debug("AuthorityName does not exist: " + groupId); + throw new EntityNotFoundException(groupId); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java new file mode 100644 index 00000000000..529714e41ac --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/TagsImpl.java @@ -0,0 +1,234 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.query.PagingResults; +import org.alfresco.repo.tagging.NonExistentTagException; +import org.alfresco.repo.tagging.TagExistsException; +import org.alfresco.repo.tagging.TaggingException; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.Tags; +import org.alfresco.rest.api.model.Tag; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.tagging.TaggingService; +import org.alfresco.util.Pair; +import org.alfresco.util.TypeConstraint; + +/** + * Centralises access to tag services and maps between representations. + * + * @author steveglover + * @since publicapi1.0 + */ +public class TagsImpl implements Tags +{ + private static final Object PARAM_INCLUDE_COUNT = "count"; + private Nodes nodes; + private TaggingService taggingService; + private TypeConstraint typeConstraint; + + public void setTypeConstraint(TypeConstraint typeConstraint) + { + this.typeConstraint = typeConstraint; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setTaggingService(TaggingService taggingService) + { + this.taggingService = taggingService; + } + + public List addTags(String nodeId, final List tags) + { + NodeRef nodeRef = nodes.validateNode(nodeId); + if(!typeConstraint.matches(nodeRef)) + { + throw new UnsupportedResourceOperationException("Cannot tag this node"); + } + + List tagValues = new AbstractList() + { + @Override + public String get(int arg0) + { + String tag = tags.get(arg0).getTag(); + return tag; + } + + @Override + public int size() + { + return tags.size(); + } + }; + try + { + List> tagNodeRefs = taggingService.addTags(nodeRef, tagValues); + List ret = new ArrayList(tags.size()); + for(Pair pair : tagNodeRefs) + { + ret.add(new Tag(pair.getSecond(), pair.getFirst())); + } + return ret; + } + catch(IllegalArgumentException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } + + public void deleteTag(String nodeId, String tagId) + { + NodeRef nodeRef = nodes.validateNode(nodeId); + getTag(tagId); + NodeRef existingTagNodeRef = validateTag(tagId); + String tagValue = taggingService.getTagName(existingTagNodeRef); + taggingService.removeTag(nodeRef, tagValue); + } + + public CollectionWithPagingInfo getTags(StoreRef storeRef, Parameters params) + { + Paging paging = params.getPaging(); + PagingResults> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging)); + taggingService.getPagedTags(storeRef, 0, paging.getMaxItems()); + Integer totalItems = results.getTotalResultCount().getFirst(); + List> page = results.getPage(); + List tags = new ArrayList(page.size()); + List> tagsByCount = null; + Map tagsByCountMap = new HashMap(); + + if (params.getInclude().contains(PARAM_INCLUDE_COUNT)) + { + tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef); + if (tagsByCount != null) + { + for (Pair tagByCountElem : tagsByCount) + { + tagsByCountMap.put(tagByCountElem.getFirst(), tagByCountElem.getSecond()); + } + } + } + for (Pair pair : page) + { + Tag selectedTag = new Tag(pair.getFirst(), pair.getSecond()); + selectedTag.setCount(tagsByCountMap.get(selectedTag.getTag())); + tags.add(selectedTag); + } + + return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue())); + } + + public NodeRef validateTag(String tagId) + { + NodeRef tagNodeRef = nodes.validateNode(tagId); + if(tagNodeRef == null) + { + throw new EntityNotFoundException(tagId); + } + return tagNodeRef; + } + + public NodeRef validateTag(StoreRef storeRef, String tagId) + { + NodeRef tagNodeRef = nodes.validateNode(storeRef, tagId); + if(tagNodeRef == null) + { + throw new EntityNotFoundException(tagId); + } + return tagNodeRef; + } + + public Tag changeTag(StoreRef storeRef, String tagId, Tag tag) + { + try + { + NodeRef existingTagNodeRef = validateTag(storeRef, tagId); + String existingTagName = taggingService.getTagName(existingTagNodeRef); + String newTagName = tag.getTag(); + NodeRef newTagNodeRef = taggingService.changeTag(storeRef, existingTagName, newTagName); + return new Tag(newTagNodeRef, newTagName); + } + catch(NonExistentTagException e) + { + throw new NotFoundException(e.getMessage()); + } + catch(TagExistsException e) + { + throw new ConstraintViolatedException(e.getMessage()); + } + catch(TaggingException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } + + public Tag getTag(String tagId) + { + return getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, tagId); + } + + public Tag getTag(StoreRef storeRef, String tagId) + { + NodeRef tagNodeRef = validateTag(storeRef, tagId); + String tagValue = taggingService.getTagName(tagNodeRef); + return new Tag(tagNodeRef, tagValue); + } + + public CollectionWithPagingInfo getTags(String nodeId, Parameters params) + { + NodeRef nodeRef = validateTag(nodeId); + + PagingResults> results = taggingService.getTags(nodeRef, Util.getPagingRequest(params.getPaging())); + Integer totalItems = results.getTotalResultCount().getFirst(); + List> page = results.getPage(); + List tags = new ArrayList(page.size()); + for(Pair pair : page) + { + tags.add(new Tag(pair.getFirst(), pair.getSecond())); + } + + return CollectionWithPagingInfo.asPaged(params.getPaging(), tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue())); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/Util.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/Util.java new file mode 100644 index 00000000000..b6612a37a76 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/Util.java @@ -0,0 +1,115 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl; + +import org.alfresco.query.CannedQueryPageDetails; +import org.alfresco.query.PageDetails; +import org.alfresco.query.PagingRequest; +import org.alfresco.query.PagingResults; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.util.Pair; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * @author steveglover + * @author Jamal Kaabi-Mofrad + */ +public class Util +{ + public static PagingRequest getPagingRequest(Paging paging) + { + PagingRequest pagingRequest = new PagingRequest(paging.getSkipCount(), paging.getMaxItems()); + pagingRequest.setRequestTotalCountMax(CannedQueryPageDetails.DEFAULT_PAGE_SIZE); + return pagingRequest; + } + + public static PagingResults wrapPagingResults(Paging paging, Collection result) + { + if (paging == null) + { + throw new InvalidArgumentException("paging object can't be null."); + } + if (result == null) + { + result = Collections.emptyList(); + } + + PagingRequest pagingRequest = getPagingRequest(paging); + + final int totalSize = result.size(); + final PageDetails pageDetails = PageDetails.getPageDetails(pagingRequest, totalSize); + + final List page = new ArrayList<>(pageDetails.getPageSize()); + Iterator it = result.iterator(); + for (int counter = 0, end = pageDetails.getEnd(); counter < end && it.hasNext(); counter++) + { + T element = it.next(); + if (counter < pageDetails.getSkipCount()) + { + continue; + } + if (counter > end - 1) + { + break; + } + page.add(element); + } + + return new PagingResults() + { + @Override + public List getPage() + { + return page; + } + + @Override + public boolean hasMoreItems() + { + return pageDetails.hasMoreItems(); + } + + @Override + public Pair getTotalResultCount() + { + Integer total = totalSize; + return new Pair<>(total, total); + } + + @Override + public String getQueryExecutionId() + { + return null; + } + }; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/AbstractActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/AbstractActivitySummaryProcessor.java new file mode 100644 index 00000000000..f5e1fb117fe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/AbstractActivitySummaryProcessor.java @@ -0,0 +1,144 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; + +public abstract class AbstractActivitySummaryProcessor extends AbstractLifecycleBean implements ActivitySummaryProcessor +{ + protected static Log logger = LogFactory.getLog(ActivitySummaryProcessor.class); + + protected ActivitySummaryProcessorRegistry registry; + private List eventTypes; + + public void setEventTypes(List eventTypes) + { + this.eventTypes = eventTypes; + } + + public void setRegistry(ActivitySummaryProcessorRegistry registry) + { + this.registry = registry; + } + + public void setCustomRenditions(List eventTypes) + { + this.eventTypes = eventTypes; + } + + /* + * (non-Javadoc) + * @see org.springframework.extensions.surf.util.AbstractLifecycleBean#onBootstrap(org.springframework.context.ApplicationEvent) + */ + protected void onBootstrap(ApplicationEvent event) + { + register(); + } + + protected void onShutdown(ApplicationEvent event) + { + + } + + @Override + public Map process(Map entries) + { + List changes = new LinkedList(); + Map ret = new HashMap(entries.size()); + for(Map.Entry entry : entries.entrySet()) + { + String key = entry.getKey(); + Object value = entry.getValue(); + Change change = processEntry(key, value); + if(change != null) + { + changes.add(change); + } + } + + for(Change change : changes) + { + if(change != null) + { + change.process(entries); + } + } + + return ret; + } + + protected abstract Change processEntry(String key, Object value); + + protected void register() + { + for(String eventType : eventTypes) + { + registry.register(eventType, this); + } + } + + public static class ChangeKey implements Change + { + private String oldKey; + private String newKey; + + public ChangeKey(String oldKey, String newKey) { + super(); + this.oldKey = oldKey; + this.newKey = newKey; + } + + public void process(Map entries) + { + Object value = entries.remove(oldKey); + entries.put(newKey, value); + } + } + + public static class RemoveKey implements Change + { + private String key; + + public RemoveKey(String key) { + super(); + this.key = key; + } + + public void process(Map entries) + { + entries.remove(key); + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryParser.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryParser.java new file mode 100644 index 00000000000..2415c0b087e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryParser.java @@ -0,0 +1,193 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.registry.NamedObjectRegistry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.springframework.extensions.surf.util.ISO8601DateFormat; + +/* + * Adapted from JSONtoFmModel + */ +public class ActivitySummaryParser implements ActivitySummaryProcessorRegistry +{ + private final Log logger = LogFactory.getLog(ActivitySummaryParser.class); + + // note: current format is dependent on ISO8601DateFormat.parser, eg. YYYY-MM-DDThh:mm:ss.sssTZD + private static String REGEXP_ISO8061 = "^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(.([0-9]){3})?(Z|[\\+\\-]([0-9]{2}):([0-9]{2}))$"; + private static Pattern matcherISO8601 = Pattern.compile(REGEXP_ISO8061); + private static final Pattern nodeRefPattern = Pattern.compile("^[a-zA-Z]+://.+/.+"); + + private NamedObjectRegistry processors; + + private boolean autoConvertISO8601 = true; + + public ActivitySummaryParser() + { + } + + public void setProcessors(NamedObjectRegistry processors) + { + this.processors = processors; + } + + public void register(String activityType, ActivitySummaryProcessor processor) + { + ActivitySummaryProcessor existingProcessor = processors.getNamedObject(activityType); + if(existingProcessor != null) + { + logger.warn("Activity summary processor " + existingProcessor + " is being overridden by " + processor + " for activity type " + activityType); + } + + processors.register(activityType, processor); + } + + private void processActivitySummary(String activityType, Map entries) + { + ActivitySummaryProcessor processor = processors.getNamedObject(activityType); + if(processor == null) + { + processor = new BaseActivitySummaryProcessor(); + } + + processor.process(entries); + } + + public Map parse(String activityType, String activitySummary) throws JSONException + { + JSONObject json = (JSONObject)JSONValue.parse(activitySummary); + Map map = convertJSONObjectToMap(json); + processActivitySummary(activityType, map); + return map; + } + + /** + * Determine if passed string conforms to the pattern of a node reference + * + * @param nodeRef the node reference as a string + * @return true => it matches the pattern of a node reference + */ + private boolean isNodeRef(String nodeRef) + { + Matcher matcher = nodeRefPattern.matcher(nodeRef); + return matcher.matches(); + } + + Map convertJSONObjectToMap(JSONObject jo) throws JSONException + { + Map model = new HashMap(); + + for(Object key : jo.keySet()) + { + Object value = jo.get(key); + if (value instanceof JSONObject) + { + model.put((String)key, convertJSONObjectToMap((JSONObject)value)); + } + else if (value instanceof JSONArray) + { + model.put((String)key, convertJSONArrayToList((JSONArray)value)); + } + else if (value == null) + { + model.put((String)key, null); + } + else + { + if ((value instanceof String) && autoConvertISO8601 && (matcherISO8601.matcher((String)value).matches())) + { + value = ISO8601DateFormat.parse((String)value); + } + + if ((value instanceof String) && isNodeRef((String)value)) + { + try + { + value = new NodeRef((String)value); + } + catch(AlfrescoRuntimeException e) + { + // cannot convert to a nodeRef, just keep as a string + logger.warn("Cannot convert activity summary NodeRef string " + value + " to a NodeRef"); + } + } + + model.put((String)key, value); + } + } + + return model; + } + + List convertJSONArrayToList(JSONArray ja) throws JSONException + { + List model = new ArrayList(); + + for (int i = 0; i < ja.size(); i++) + { + Object o = ja.get(i); + + if (o instanceof JSONArray) + { + model.add(convertJSONArrayToList((JSONArray)o)); + } + else if (o instanceof JSONObject) + { + model.add(convertJSONObjectToMap((JSONObject)o)); + } + else if (o == null) + { + model.add(null); + } + else + { + if ((o instanceof String) && autoConvertISO8601 && (matcherISO8601.matcher((String)o).matches())) + { + o = ISO8601DateFormat.parse((String)o); + } + + model.add(o); + } + } + + return model; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryProcessor.java new file mode 100644 index 00000000000..2f45f1d75be --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryProcessor.java @@ -0,0 +1,38 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +import java.util.Map; + +public interface ActivitySummaryProcessor +{ + public interface Change + { + void process(Map entries); + } + + public Map process(Map entries); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryProcessorRegistry.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryProcessorRegistry.java new file mode 100644 index 00000000000..4feeeb33c23 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/ActivitySummaryProcessorRegistry.java @@ -0,0 +1,37 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +/** + * A registry for activity summary parsers/post processors. + * + * @author steveglover + * + */ +public interface ActivitySummaryProcessorRegistry +{ + public void register(String activityType, ActivitySummaryProcessor processor); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/BaseActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/BaseActivitySummaryProcessor.java new file mode 100644 index 00000000000..0ac4fcdbef2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/BaseActivitySummaryProcessor.java @@ -0,0 +1,121 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.rest.api.impl.activities.AbstractActivitySummaryProcessor.RemoveKey; +import org.alfresco.service.cmr.repository.NodeRef; + +public class BaseActivitySummaryProcessor extends AbstractActivitySummaryProcessor +{ + @Override + protected Change processEntry(String key, Object value) + { + Change change = null; + + if(key.equals("page")) + { + change = new ChangePageValue(key); + } + + if(key.equals("tenantDomain")) + { + change = new RemoveKey(key); + } + + if(key.equals("nodeRef")) + { + change = new ChangeKey(key, "objectId"); + } + + if(key.equals("parentNodeRef")) + { + change = new ChangeKey(key, "parentObjectId"); + } + + // remove null or empty properties + if(value == null || value.equals("")) + { + change = new RemoveKey(key); + } + + return change; + } + + public static class ChangePageValue implements Change + { + private String key; + private static final String regex = Pattern.quote("document-details?nodeRef=") + "(.*)"; + private static final Pattern pattern = Pattern.compile(regex); + + public ChangePageValue(String key) { + super(); + this.key = key; + } + + /* + * Extract and output the node id from input that looks like this: document-details?nodeRef=workspace%3A%2F%2FSpacesStore%2Fd4c1a75e-a17e-4033-94f4-988cca39a357 (non-Javadoc) + * + * @see org.alfresco.rest.api.impl.activities.ActivitySummaryProcessor.Change#process(java.util.Map) + */ + public void process(Map entries) + { + String value = (String)entries.remove(key); + try + { + value = URLDecoder.decode(value, "UTF-8"); + Matcher matcher = pattern.matcher(value); + if(matcher.matches()) + { + String nodeRefStr = matcher.group(1); + boolean isNodeRef = NodeRef.isNodeRef(nodeRefStr); + if(isNodeRef) + { + NodeRef nodeRef = new NodeRef(nodeRefStr); + entries.put("objectId", nodeRef.getId()); + } + else + { + logger.warn("Activity page url contains an invalid NodeRef " + value); + } + } + else + { + logger.warn("Failed to match activity page url for objectId extraction " + value); + } + } + catch (UnsupportedEncodingException e) + { + logger.warn("Unable to decode activity page url " + value); + } + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/CommentsActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/CommentsActivitySummaryProcessor.java new file mode 100644 index 00000000000..2ca0571098c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/CommentsActivitySummaryProcessor.java @@ -0,0 +1,32 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +public class CommentsActivitySummaryProcessor extends BaseActivitySummaryProcessor +{ + + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/DiscussionsActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/DiscussionsActivitySummaryProcessor.java new file mode 100644 index 00000000000..2164d80f525 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/DiscussionsActivitySummaryProcessor.java @@ -0,0 +1,42 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +public class DiscussionsActivitySummaryProcessor extends BaseActivitySummaryProcessor +{ + @Override + protected Change processEntry(String key, Object value) + { + Change change = null; + + if(key.equals("params")) + { + change = new RemoveKey(key); + } + + return change; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/DocumentLibraryActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/DocumentLibraryActivitySummaryProcessor.java new file mode 100644 index 00000000000..e00e0865ff5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/DocumentLibraryActivitySummaryProcessor.java @@ -0,0 +1,31 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +public class DocumentLibraryActivitySummaryProcessor extends BaseActivitySummaryProcessor +{ + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/SiteActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/SiteActivitySummaryProcessor.java new file mode 100644 index 00000000000..a731290018b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/SiteActivitySummaryProcessor.java @@ -0,0 +1,43 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +public class SiteActivitySummaryProcessor extends BaseActivitySummaryProcessor +{ + @Override + protected Change processEntry(String key, Object value) + { + Change change = super.processEntry(key, value); + + if(key.equals("memberUserName")) + { + change = new ChangeKey(key, "memberPersonId"); + } + + return change; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/SubscriptionsActivitySummaryProcessor.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/SubscriptionsActivitySummaryProcessor.java new file mode 100644 index 00000000000..216696a945b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/activities/SubscriptionsActivitySummaryProcessor.java @@ -0,0 +1,51 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.activities; + +public class SubscriptionsActivitySummaryProcessor extends BaseActivitySummaryProcessor +{ + //{"lastName":"Glover","userFirstName":"David","followerLastName":"Glover","userUserName":"david.bowie@alfresco.com","followerFirstName":"Steve", + // "userLastName":"Bowie","followerUserName":"steven.glover@alfresco.com","firstName":"Steve","tenantDomain":"alfresco.com"} + + @Override + protected Change processEntry(String key, Object value) + { + Change change = super.processEntry(key, value); + + if(key.equals("userUserName")) + { + change = new ChangeKey(key, "userPersonId"); + } + + if(key.equals("followerUserName")) + { + change = new ChangeKey(key, "followerPersonId"); + } + + return change; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/AbstractRatingScheme.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/AbstractRatingScheme.java new file mode 100644 index 00000000000..47773b6ad0f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/AbstractRatingScheme.java @@ -0,0 +1,236 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.node.ratings; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; + +import org.alfresco.model.ContentModel; +import org.alfresco.sync.repo.Client; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.rest.api.model.DocumentRatingSummary; +import org.alfresco.rest.api.model.NodeRating; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.rating.Rating; +import org.alfresco.service.cmr.rating.RatingService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.util.registry.NamedObjectRegistry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.InitializingBean; + +/** + * Manages the mapping between the rest api's representation of a node rating and the repository's + * representation of a node rating. + * + * @author steveglover + * + */ +public abstract class AbstractRatingScheme implements RatingScheme, InitializingBean +{ + private static Log logger = LogFactory.getLog(RatingScheme.class); + + protected String ratingSchemeName; // for interaction with the RatingService + protected String ratingSchemeId; // exposed through the api + protected RatingService ratingService; + protected NodeService nodeService; + protected DictionaryService dictionaryService; + protected ActivityService activityService; + protected SiteService siteService; + + protected NamedObjectRegistry nodeRatingSchemeRegistry; + + public AbstractRatingScheme(String ratingSchemeId, String ratingSchemeName) + { + super(); + this.ratingSchemeName = ratingSchemeName; + this.ratingSchemeId = ratingSchemeId; + } + + public String getRatingSchemeId() + { + return ratingSchemeId; + } + + public String getRatingServiceName() + { + return ratingSchemeName; + } + + public void setNodeRatingSchemeRegistry(NamedObjectRegistry nodeRatingSchemeRegistry) + { + this.nodeRatingSchemeRegistry = nodeRatingSchemeRegistry; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setRatingService(RatingService ratingService) + { + this.ratingService = ratingService; + } + + protected org.alfresco.service.cmr.rating.RatingScheme getRepoRatingScheme() + { + return ratingService.getRatingScheme(ratingSchemeName); + } + + protected abstract DocumentRatingSummary getDocumentRatingSummary(NodeRef nodeRef); + protected abstract Object getApiRating(Float rating); + + @Override + public void afterPropertiesSet() throws Exception + { + nodeRatingSchemeRegistry.register(ratingSchemeId, this); + nodeRatingSchemeRegistry.register(ratingSchemeName, this); + } + + public void validateRating(Float rating) + { + org.alfresco.service.cmr.rating.RatingScheme ratingScheme = getRepoRatingScheme(); + Float minRating = ratingScheme.getMinRating(); + Float maxRating = ratingScheme.getMaxRating(); + if(rating < minRating || rating > maxRating) + { + throw new InvalidArgumentException("Rating is out of bounds."); + } + } + + public NodeRating getNodeRating(NodeRef nodeRef) + { + Rating ratingByCurrentUser = ratingService.getRatingByCurrentUser(nodeRef, ratingSchemeName); + Float rating = null; + Date appliedAt = null; + + if(ratingByCurrentUser != null) + { + rating = ratingByCurrentUser.getScore(); + appliedAt = ratingByCurrentUser.getAppliedAt(); + } + + Object myRating = null; + if(rating != null) + { + validateRating(rating); + myRating = getApiRating(rating); + } + + DocumentRatingSummary documentRatingSummary = getDocumentRatingSummary(nodeRef); + + NodeRating nodeRating = new NodeRating(ratingSchemeId, myRating, appliedAt, documentRatingSummary); + return nodeRating; + } + + private String getSiteId(final NodeRef nodeRef) + { + // may not be able to read site data so run as system + String siteId = AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public String doWork() throws Exception + { + String siteId = null; + SiteInfo siteInfo = siteService.getSite(nodeRef); + if(siteInfo != null) + { + siteId = siteInfo.getShortName(); + } + return siteId; + } + }); + + return siteId; + } + + @SuppressWarnings("unchecked") + private JSONObject getActivityData(final NodeRef nodeRef, String siteId) + { + JSONObject activityData = null; + + if(siteId != null) + { + // may not be able to read these nodes, but we need to for the activity processing so run as system + activityData = AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public JSONObject doWork() throws Exception + { + JSONObject activityData = new JSONObject(); + activityData.put("title", nodeService.getProperty(nodeRef, ContentModel.PROP_NAME)); + try + { + StringBuilder sb = new StringBuilder("document-details?nodeRef="); + sb.append(URLEncoder.encode(nodeRef.toString(), "UTF-8")); + activityData.put("page", sb.toString()); + } + catch (UnsupportedEncodingException e) + { + logger.warn("Unable to urlencode page for create nodeRating activity"); + } + + return activityData; + } + }); + } + + return activityData; + } + + protected void postActivity(final NodeRef nodeRef, final String activityType) + { + String siteId = getSiteId(nodeRef); + JSONObject activityData = getActivityData(nodeRef, siteId); + if(activityData != null) + { + activityService.postActivity(activityType, siteId, "nodeRatings", activityData.toString(), Client.asType(Client.ClientType.restapi)); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/FiveStarRatingScheme.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/FiveStarRatingScheme.java new file mode 100644 index 00000000000..e217451262f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/FiveStarRatingScheme.java @@ -0,0 +1,104 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.node.ratings; + +import org.alfresco.rest.api.model.DocumentRatingSummary; +import org.alfresco.rest.api.model.FiveStarRatingSummary; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.cmr.rating.RatingServiceException; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * The rest apis representation of the 'fiveStar' rating scheme. + * + * @author steveglover + * + */ +public class FiveStarRatingScheme extends AbstractRatingScheme +{ + public FiveStarRatingScheme() + { + super("fiveStar", "fiveStarRatingScheme"); + } + + public Float getRatingServiceRating(Object rating) + { + Float ratingToApply = null; + + if(rating instanceof Integer) + { + ratingToApply = ((Integer)rating).floatValue(); + } + else + { + throw new InvalidArgumentException("Rating should be non-null and an integer for 'fiveStar' rating scheme."); + } + + validateRating(ratingToApply); + + return ratingToApply; + } + + public Object getApiRating(Float rating) + { + Object apiRating = Integer.valueOf(rating.intValue()); + return apiRating; + } + + public DocumentRatingSummary getDocumentRatingSummary(NodeRef nodeRef) + { + return new FiveStarRatingSummary(ratingService.getRatingsCount(nodeRef, ratingSchemeName), + ratingService.getTotalRating(nodeRef, ratingSchemeName), + ratingService.getAverageRating(nodeRef, ratingSchemeName)); + } + + @Override + public void applyRating(NodeRef nodeRef, Object rating) + { + try + { + Float ratingServiceRating = getRatingServiceRating(rating); + ratingService.applyRating(nodeRef, ratingServiceRating, getRatingServiceName()); + } + catch(RatingServiceException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } + + @Override + public void removeRating(NodeRef nodeRef) + { + try + { + ratingService.removeRatingByCurrentUser(nodeRef, getRatingServiceName()); + } + catch(RatingServiceException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/LikesRatingScheme.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/LikesRatingScheme.java new file mode 100644 index 00000000000..be182fd3314 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/LikesRatingScheme.java @@ -0,0 +1,125 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.node.ratings; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.activities.ActivityType; +import org.alfresco.rest.api.model.DocumentRatingSummary; +import org.alfresco.rest.api.model.LikesRatingSummary; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.cmr.rating.RatingServiceException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * The rest api representation of the 'likes' rating scheme. + * + * @author steveglover + * + */ +public class LikesRatingScheme extends AbstractRatingScheme +{ + public LikesRatingScheme() + { + super("likes", "likesRatingScheme"); + } + + public Float getRatingServiceRating(Object rating) + { + Float ratingToApply = null; + + if(rating instanceof Boolean) + { + boolean liked = ((Boolean)rating).booleanValue(); + ratingToApply = Float.valueOf((liked ? 1.0f : 0.0f)); + } + else + { + throw new InvalidArgumentException("Rating should be non-null and a boolean for 'likes' rating scheme."); + } + + return ratingToApply; + } + + @Override + public void applyRating(NodeRef nodeRef, Object rating) + { + try + { + Float ratingServiceRating = getRatingServiceRating(rating); + ratingService.applyRating(nodeRef, ratingServiceRating, getRatingServiceName()); + + QName nodeType = nodeService.getType(nodeRef); + boolean isContainer = dictionaryService.isSubClass(nodeType, ContentModel.TYPE_FOLDER) && + !dictionaryService.isSubClass(nodeType, ContentModel.TYPE_SYSTEM_FOLDER); + postActivity(nodeRef, isContainer ? ActivityType.FOLDER_LIKED : ActivityType.FILE_LIKED); + } + catch(RatingServiceException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } + + @Override + public void removeRating(NodeRef nodeRef) + { + try + { + ratingService.removeRatingByCurrentUser(nodeRef, getRatingServiceName()); + } + catch(RatingServiceException e) + { + throw new InvalidArgumentException(e.getMessage()); + } + } + + @Override + protected Object getApiRating(Float rating) + { + Object apiRating = null; + if(rating == 1.0f) + { + apiRating = true; + } + else if(rating == 0.0f) + { + apiRating = false; + } + else + { + throw new InvalidArgumentException("Rating is invalid."); + } + + return apiRating; + } + + @Override + protected DocumentRatingSummary getDocumentRatingSummary(NodeRef nodeRef) + { + return new LikesRatingSummary(ratingService.getRatingsCount(nodeRef, ratingSchemeName)); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/RatingScheme.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/RatingScheme.java new file mode 100644 index 00000000000..39613899c4a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/node/ratings/RatingScheme.java @@ -0,0 +1,36 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.impl.node.ratings; + +import org.alfresco.rest.api.model.NodeRating; +import org.alfresco.service.cmr.repository.NodeRef; + +public interface RatingScheme +{ + public void applyRating(NodeRef nodeRef, Object rating); + public void removeRating(NodeRef nodeRef); + public NodeRating getNodeRating(NodeRef nodeRef); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/lookups/MimeTypePropertyLookup.java b/remote-api/src/main/java/org/alfresco/rest/api/lookups/MimeTypePropertyLookup.java new file mode 100644 index 00000000000..b289a545c0d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/lookups/MimeTypePropertyLookup.java @@ -0,0 +1,71 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.lookups; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Looks up mimetype values + * Pass in a mimetype value and a display string is returned. + * @author Gethin James + */ +public class MimeTypePropertyLookup implements PropertyLookup +{ + private Set supported = new HashSet<>(); + private ServiceRegistry serviceRegistry; + + @Override + public String lookup(String propertyValue) + { + Map mimetypes = serviceRegistry.getMimetypeService().getDisplaysByMimetype(); + return mimetypes.get(propertyValue); + } + + @Override + public Set supports() + { + return supported; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public void setSupported(List supported) + { + this.supported.add(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "content.mimetype").toString()); + this.supported.addAll(supported); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/lookups/PersonPropertyLookup.java b/remote-api/src/main/java/org/alfresco/rest/api/lookups/PersonPropertyLookup.java new file mode 100644 index 00000000000..041d8bb8305 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/lookups/PersonPropertyLookup.java @@ -0,0 +1,76 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.lookups; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.transaction.TransactionalResourceHelper; +import org.alfresco.rest.api.impl.NodesImpl; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.service.ServiceRegistry; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Looks up information about a person. + * Pass in a userId and the display name is returned. + * + * @author Gethin James + */ +public class PersonPropertyLookup implements PropertyLookup +{ + private Set supported = new HashSet<>(); + private ServiceRegistry serviceRegistry; + + @Override + public String lookup(String propertyValue) + { + Map mapUserInfo = TransactionalResourceHelper.getMap("PERSON_PROPERTY_LOOKUP_USER_INFO_CACHE"); + UserInfo user = Node.lookupUserInfo(propertyValue, mapUserInfo, serviceRegistry.getPersonService()); + if (user != null) return user.getDisplayName(); + return null; + } + + @Override + public Set supports() + { + return supported; + } + + public void setSupported(List supported) + { + NodesImpl.PROPS_USERLOOKUP.forEach(entry -> this.supported.add(entry.toString())); + this.supported.addAll(supported); + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/lookups/PropertyLookup.java b/remote-api/src/main/java/org/alfresco/rest/api/lookups/PropertyLookup.java new file mode 100644 index 00000000000..19313384753 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/lookups/PropertyLookup.java @@ -0,0 +1,52 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.lookups; + + +import java.util.List; +import java.util.Set; + +/** + * Looks up property values for the api + * + * @author Gethin James + */ +public interface PropertyLookup +{ + /** + * The list of property keys that are supported by this class + * @return Set property keys + */ + public Set supports(); + + /** + * Lookup the property value with a new value. + * @param propertyValue + * @return a new value or null if the property value isn't found. + */ + public T lookup(String propertyValue); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/lookups/PropertyLookupRegistry.java b/remote-api/src/main/java/org/alfresco/rest/api/lookups/PropertyLookupRegistry.java new file mode 100644 index 00000000000..50dc9565461 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/lookups/PropertyLookupRegistry.java @@ -0,0 +1,88 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.lookups; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Acts as a central point for property lookups + * + * @author Gethin James + */ +public class PropertyLookupRegistry +{ + private static Log logger = LogFactory.getLog(PropertyLookupRegistry.class); + Map propertyLookups = new HashMap<>(); + + /** + * Set the supported property lookup classes. + * @param lookups + */ + public void setLookups(List lookups) + { + lookups.forEach(entry -> + { + entry.supports().forEach( propKey -> propertyLookups.put((String) propKey, entry) ); + }); + } + + /** + * The list of property keys that are supported by this class + * @return Set property keys + */ + public Set supports() + { + return propertyLookups.keySet(); + } + + /** + * Looks up the property value using a PropertyLookup + * @param propertyName the property name/type + * @param propertyValue the value to lookup + * @return Object to be serialized as json + */ + public Object lookup(String propertyName, String propertyValue) + { + PropertyLookup lookup = propertyLookups.get(propertyName); + if (lookup != null) + { + return lookup.lookup(propertyValue); + } + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/AbstractClassModel.java b/remote-api/src/main/java/org/alfresco/rest/api/model/AbstractClassModel.java new file mode 100644 index 00000000000..9146ef9c302 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/AbstractClassModel.java @@ -0,0 +1,80 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.alfresco.service.namespace.QName; + +/** + * @author Jamal Kaabi-Mofrad + */ +public abstract class AbstractClassModel extends AbstractCommonDetails +{ + /* package */String parentName; + /* package */List properties = Collections.emptyList(); + + public String getParentName() + { + return this.parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public List getProperties() + { + return this.properties; + } + + public void setProperties(List properties) + { + this.properties = properties; + } + + /* package */ List setList(List sourceList) + { + if (sourceList == null) + { + return Collections. emptyList(); + } + return new ArrayList<>(sourceList); + } + + /* package */String getParentNameAsString(QName parentQName) + { + if (parentQName != null) + { + return parentQName.toPrefixString(); + } + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/AbstractCommonDetails.java b/remote-api/src/main/java/org/alfresco/rest/api/model/AbstractCommonDetails.java new file mode 100644 index 00000000000..33f8d6b555c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/AbstractCommonDetails.java @@ -0,0 +1,118 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +/** + * @author Jamal Kaabi-Mofrad + */ +public abstract class AbstractCommonDetails implements Comparable +{ + /* package */String name; + /* package */String prefixedName; + /* package */String title; + /* package */String description; + + public String getName() + { + return this.name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPrefixedName() + { + return this.prefixedName; + } + + public String getTitle() + { + return this.title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getDescription() + { + return this.description; + } + + public void setDescription(String description) + { + this.description = description; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (!(obj instanceof CustomModelConstraint)) + { + return false; + } + CustomModelConstraint other = (CustomModelConstraint) obj; + if (this.name == null) + { + if (other.name != null) + { + return false; + } + } + else if (!this.name.equals(other.name)) + { + return false; + } + return true; + } + + @Override + public int compareTo(AbstractCommonDetails other) + { + return this.name.compareTo(other.getName()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Action.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Action.java new file mode 100644 index 00000000000..99a5da3625d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Action.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Map; + +public class Action +{ + private String id; + private String actionDefinitionId; + private String targetId; + Map params; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getActionDefinitionId() + { + return actionDefinitionId; + } + + public void setActionDefinitionId(String actionDefinitionId) + { + this.actionDefinitionId = actionDefinitionId; + } + + public String getTargetId() + { + return targetId; + } + + public void setTargetId(String targetId) + { + this.targetId = targetId; + } + + public Map getParams() + { + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/ActionDefinition.java b/remote-api/src/main/java/org/alfresco/rest/api/model/ActionDefinition.java new file mode 100644 index 00000000000..c9a9cdf137e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/ActionDefinition.java @@ -0,0 +1,171 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.List; + +public class ActionDefinition +{ + private String id; + private String name; + private String title; + private String description; + private List applicableTypes; + private boolean adhocPropertiesAllowed; + private boolean trackStatus; + private List parameterDefinitions; + + /** + * For Jackson deserialisation. + */ + public ActionDefinition() + { + } + + public ActionDefinition(String id, + String name, + String title, + String description, + List applicableTypes, + boolean adhocPropertiesAllowed, + boolean trackStatus, + List parameterDefinitions) + { + this.id = id; + this.name = name; + this.title = title; + this.description = description; + this.applicableTypes = applicableTypes; + this.adhocPropertiesAllowed = adhocPropertiesAllowed; + this.trackStatus = trackStatus; + this.parameterDefinitions = parameterDefinitions; + } + + /** + * Will be used as a synonym for name. + */ + public String getId() + { + return id; + } + + public String getName() + { + return name; + } + + public String getTitle() + { + return title; + } + + public String getDescription() + { + return description; + } + + public List getApplicableTypes() + { + return applicableTypes; + } + + public boolean isAdhocPropertiesAllowed() + { + return adhocPropertiesAllowed; + } + + public boolean isTrackStatus() + { + return trackStatus; + } + + public List getParameterDefinitions() + { + return parameterDefinitions; + } + + public static class ParameterDefinition + { + private String name; + private String type; + private boolean multiValued; + private boolean mandatory; + private String displayLabel; + private String parameterConstraintName; + + /** + * For Jackson deserialisation. + */ + public ParameterDefinition() + { + } + + public ParameterDefinition(String name, + String type, + boolean multiValued, + boolean mandatory, + String displayLabel, + String parameterConstraintName) + { + this.name = name; + this.type = type; + this.multiValued = multiValued; + this.mandatory = mandatory; + this.displayLabel = displayLabel; + this.parameterConstraintName = parameterConstraintName; + } + + public String getName() + { + return name; + } + + public String getType() + { + return type; + } + + public boolean isMultiValued() + { + return multiValued; + } + + public boolean isMandatory() + { + return mandatory; + } + + public String getDisplayLabel() + { + return displayLabel; + } + + public String getParameterConstraintName() + { + return parameterConstraintName; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Activity.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Activity.java new file mode 100644 index 00000000000..535128f0d8f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Activity.java @@ -0,0 +1,165 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; +import java.util.Map; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents an activity feed entry. + * + * @author steveglover + * + */ +public class Activity implements Comparable +{ + private Long id; + private String networkId; + private String siteId; + private String feedPersonId; + private String postPersonId; + private Date postedAt; + private String activityType; + private Map activitySummary; + + public Activity() + { + } + + public Activity(Long id, String networkId, String siteId, String feedPersonId, + String postPersonId, Date postedAt, String activityType, Map activitySummary) + { + super(); + this.id = id; + this.networkId = networkId; + this.siteId = siteId; + this.feedPersonId = feedPersonId; + this.postPersonId = postPersonId; + this.postedAt = postedAt; + this.activityType = activityType; + this.activitySummary = activitySummary; + } + + @UniqueId + public Long getId() + { + return id; + } + + public String getNetworkId() + { + return networkId; + } + + public String getSiteId() + { + return siteId; + } + + public String getFeedPersonId() + { + return feedPersonId; + } + + public String getPostPersonId() + { + return postPersonId; + } + + public String getActivityType() + { + return activityType; + } + + public Date getPostedAt() + { + return postedAt; + } + + public Map getActivitySummary() + { + return activitySummary; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.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; + } + + Activity other = (Activity) obj; + return(id.equals(other.id)); + } + + @Override + public int compareTo(Activity activity) + { + long otherId = activity.getId(); + long diff = id - otherId; + if(diff == 0) + { + return 0; + } + else + { + return diff < 0 ? -1 : 1; + } + } + + @Override + public String toString() + { + return "Activity [id=" + id + ", siteId=" + siteId + + ", feedPersonId=" + feedPersonId + ", postPersonId=" + postPersonId + + ", postedAt=" + postedAt + + ", activityType=" + activityType + ", activitySummary=" + + activitySummary + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Assoc.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Assoc.java new file mode 100644 index 00000000000..e60d48214a8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Assoc.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + + +/** + * @author janv + */ +public class Assoc +{ + private String prefixAssocTypeQName; + + public Assoc() + { + } + + public Assoc(String prefixAssocTypeQName) + { + this.prefixAssocTypeQName = prefixAssocTypeQName; + } + + public String getAssocType() + { + return prefixAssocTypeQName; + } + + public void setAssocType(String prefixAssocTypeQName) + { + this.prefixAssocTypeQName = prefixAssocTypeQName; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/AssocChild.java b/remote-api/src/main/java/org/alfresco/rest/api/model/AssocChild.java new file mode 100644 index 00000000000..84a1500939f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/AssocChild.java @@ -0,0 +1,73 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * @author janv + */ +public class AssocChild extends Assoc +{ + private String childId; + private Boolean isPrimary; + + public AssocChild() + { + } + + public AssocChild(String prefixAssocTypeQName, boolean isPrimary) + { + super(prefixAssocTypeQName); + + this.isPrimary = isPrimary; + } + + public AssocChild(String childId, String prefixAssocTypeQName) + { + super(prefixAssocTypeQName); + + this.childId = childId; + } + + public Boolean getIsPrimary() + { + return isPrimary; + } + + public void setIsPrimary(Boolean isPrimary) + { + this.isPrimary = isPrimary; + } + + public String getChildId() + { + return childId; + } + + public void setChildId(String childId) + { + this.childId = childId; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/AssocTarget.java b/remote-api/src/main/java/org/alfresco/rest/api/model/AssocTarget.java new file mode 100644 index 00000000000..fa4a232852a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/AssocTarget.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * @author janv + */ +public class AssocTarget extends Assoc +{ + private String targetId; + + public AssocTarget() + { + } + + public AssocTarget(String prefixAssocTypeQName) + { + super(prefixAssocTypeQName); + } + + public AssocTarget(String targetId, String prefixAssocTypeQName) + { + super(prefixAssocTypeQName); + + this.targetId = targetId; + } + + public String getTargetId() + { + return targetId; + } + + public void setTargetId(String targetId) + { + this.targetId = targetId; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/AuditApp.java b/remote-api/src/main/java/org/alfresco/rest/api/model/AuditApp.java new file mode 100644 index 00000000000..13467bb9b3d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/AuditApp.java @@ -0,0 +1,112 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A representation of a Audit App + * + * @author janv + * + */ +public class AuditApp +{ + private String id; + private String name; + private Boolean isEnabled; + private Long maxEntryId; + private Long minEntryId; + + public Long getMaxEntryId() + { + return maxEntryId; + } + + public void setMaxEntryId(Long maxEntryId) + { + this.maxEntryId = maxEntryId; + } + + public Long getMinEntryId() + { + return minEntryId; + } + + public void setMinEntryId(Long minEntryId) + { + this.minEntryId = minEntryId; + } + + public AuditApp() + { + } + + public AuditApp(String id, String name, boolean isEnabled) + { + this.id = id; + this.name = name; + this.isEnabled = isEnabled; + } + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + @JsonProperty(value="isEnabled") + public Boolean getIsEnabled() + { + return isEnabled; + } + + @JsonProperty(value="isEnabled") + public void setIsEnabled(Boolean isEnabled) + { + this.isEnabled = isEnabled; + } + + @Override + public String toString() + { + return "AuditApp [id=" + id + ", name= " + name + ", isEnabled=" + isEnabled + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/AuditEntry.java b/remote-api/src/main/java/org/alfresco/rest/api/model/AuditEntry.java new file mode 100644 index 00000000000..b37734c4da4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/AuditEntry.java @@ -0,0 +1,105 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +public class AuditEntry +{ + + private Long id; + private String auditApplicationId; + protected UserInfo createdByUser; + protected Date createdAt; + protected Map values; + + public AuditEntry() + { + + } + + public AuditEntry(Long id, String auditApplicationId, UserInfo createdByUser, Date createdAt, Map values2) + { + this.id = id; + this.auditApplicationId = auditApplicationId; + this.createdByUser = createdByUser; + this.createdAt = createdAt; + this.values = values2; + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getAuditApplicationId() + { + return auditApplicationId; + } + + public void setAuditApplicationId(String auditApplicationId) + { + this.auditApplicationId = auditApplicationId; + } + + public UserInfo getCreatedByUser() + { + return createdByUser; + } + + public void setCreatedByUser(UserInfo createdByUser) + { + this.createdByUser = createdByUser; + } + + public Date getCreatedAt() + { + return createdAt; + } + + public void setCreatedAt(Date createdAt) + { + this.createdAt = createdAt; + } + + public Map getValues() + { + return values; + } + + public void setValues(Map values) + { + this.values = values; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Client.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Client.java new file mode 100644 index 00000000000..bf6dec8dda4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Client.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +/** + * Representation of a client app. + * + * @author Jamal Kaabi-Mofrad + * @since 5.2.1 + */ +public class Client +{ + /** + * client app name. Used to lookup the client + * that is registered to send emails so that + * client's specific configuration could be used. + */ + protected String client; + + public String getClient() + { + return client; + } + + public Client setClient(String client) + { + this.client = client; + return this; + } + + @Override + public String toString() + { + return "Client [ client=" + client + ']'; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Comment.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Comment.java new file mode 100644 index 00000000000..4b48ced11c4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Comment.java @@ -0,0 +1,178 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.rest.api.people.PeopleEntityResource; +import org.alfresco.rest.framework.resource.EmbeddedEntityResource; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.namespace.QName; + +/** + * A representation of a Comment in the system. + * + * @author Gethin James + * @author steveglover + * + */ +public class Comment +{ + public static final QName PROP_COMMENT_CONTENT = QName.createQName("RestApi", "commentContent"); + public static final QName PROP_COMMENT_CREATED_BY = QName.createQName("RestApi", "createdBy"); + public static final QName PROP_COMMENT_MODIFIED_BY = QName.createQName("RestApi", "modifiedBy"); + + private String id; + private String title; + private String content; + private Date createdAt; + private Person createdBy; + private Date modifiedAt; + private Person modifiedBy; + private Boolean edited; + + // permissions + private boolean canEdit; + private boolean canDelete; + + public Comment() + { + super(); + } + + public Comment(String id, Map nodeProps, boolean canEdit, boolean canDelete) + { + if(id == null) + { + throw new IllegalArgumentException(); + } + + this.id = id; + mapProperties(nodeProps); + this.canEdit = canEdit; + this.canDelete = canDelete; + } + + @UniqueId + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public boolean getCanEdit() + { + return canEdit; + } + + public boolean getCanDelete() + { + return canDelete; + } + + public String getTitle() + { + return this.title; + } + + public String getContent() + { + return this.content; + } + + public void setContent(String content) + { + this.content = content; + } + + public Date getCreatedAt() + { + return createdAt; + } + + public Person getCreatedBy() + { + return createdBy; + } + + public Person getModifiedBy() + { + return modifiedBy; + } + + public Date getModifiedAt() + { + return modifiedAt; + } + + public Boolean getEdited() + { + return edited; + } + + protected void mapProperties(Map nodeProps) + { + String propTitle = (String) nodeProps.get(ContentModel.PROP_TITLE); + if (propTitle != null) + { + title = propTitle; + } + + this.modifiedAt = (Date)nodeProps.get(ContentModel.PROP_MODIFIED); + this.createdAt = (Date)nodeProps.get(ContentModel.PROP_CREATED); + if(modifiedAt != null && createdAt != null) + { + long diff = modifiedAt.getTime() - createdAt.getTime(); + this.edited = Boolean.valueOf(diff >= 100); // logic is consistent with existing (Javascript) comments implementation + } + + this.content = (String)nodeProps.get(PROP_COMMENT_CONTENT); + nodeProps.remove(PROP_COMMENT_CONTENT); + + this.createdBy = (Person) nodeProps.get(PROP_COMMENT_CREATED_BY); + nodeProps.remove(PROP_COMMENT_CREATED_BY); + + this.modifiedBy = (Person) nodeProps.get(PROP_COMMENT_MODIFIED_BY); + nodeProps.remove(PROP_COMMENT_MODIFIED_BY); + } + + @Override + public String toString() + { + return "Comment [id=" + id + ", title=" + title + + ", content=" + content + ", createdAt=" + createdAt + + ", createdBy=" + createdBy + ", modifiedAt=" + modifiedAt + + ", modifiedBy=" + modifiedBy + ", edited=" + edited + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Company.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Company.java new file mode 100644 index 00000000000..f3a8a2dfb01 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Company.java @@ -0,0 +1,176 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.namespace.QName; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a company + * + * @author steveglover + * + */ +public class Company +{ + private String organization; + private String address1; + private String address2; + private String address3; + private String postcode; + private String telephone; + private String fax; + private String email; + + private Map setFields = new HashMap<>(7); + + /** + * Default constructor, required for deserialising from JSON. + */ + public Company() + { + } + + public Company(String organization, String address1, String address2, String address3, + String postcode, String telephone, String fax, String email) + { + super(); + setOrganization(organization); + setAddress1(address1); + setAddress2(address2); + setAddress3(address3); + setPostcode(postcode); + setTelephone(telephone); + setFax(fax); + setEmail(email); + } + + public String getOrganization() + { + return organization; + } + + public String getAddress1() + { + return address1; + } + + public String getAddress2() + { + return address2; + } + + public String getAddress3() + { + return address3; + } + + public String getPostcode() + { + return postcode; + } + + public String getTelephone() + { + return telephone; + } + + public String getFax() + { + return fax; + } + + public String getEmail() + { + return email; + } + + public void setOrganization(String organization) + { + this.organization = organization; + setFields.put(ContentModel.PROP_ORGANIZATION, true); + } + + public void setAddress1(String address1) + { + this.address1 = address1; + setFields.put(ContentModel.PROP_COMPANYADDRESS1, true); + } + + public void setAddress2(String address2) + { + this.address2 = address2; + setFields.put(ContentModel.PROP_COMPANYADDRESS2, true); + } + + public void setAddress3(String address3) + { + this.address3 = address3; + setFields.put(ContentModel.PROP_COMPANYADDRESS3, true); + } + + public void setPostcode(String postcode) + { + this.postcode = postcode; + setFields.put(ContentModel.PROP_COMPANYPOSTCODE, true); + } + + public void setTelephone(String telephone) + { + this.telephone = telephone; + setFields.put(ContentModel.PROP_COMPANYTELEPHONE, true); + } + + public void setFax(String fax) + { + this.fax = fax; + setFields.put(ContentModel.PROP_COMPANYFAX, true); + } + + public void setEmail(String email) + { + this.email = email; + setFields.put(ContentModel.PROP_COMPANYEMAIL, true); + } + + public boolean wasSet(QName fieldName) + { + Boolean b = setFields.get(fieldName); + return (b != null ? b : false); + } + + @Override + public String toString() + { + return "Company [address1=" + address1 + ", address2=" + address2 + + ", address3=" + address3 + ", postcode=" + postcode + + ", telephone=" + telephone + ", fax=" + fax + ", email=" + + email + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java new file mode 100644 index 00000000000..348b164cb41 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/ContentInfo.java @@ -0,0 +1,79 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * Representation of content info + * + * @author janv + * + */ +public class ContentInfo +{ + private String mimeType; + private String mimeTypeName; + private Long sizeInBytes; + private String encoding; + + public ContentInfo() + { + } + + public ContentInfo(String mimeType, String mimeTypeName, Long sizeInBytes, String encoding) + { + this.mimeType = mimeType; + this.mimeTypeName = mimeTypeName; + this.sizeInBytes = sizeInBytes; + this.encoding = encoding; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getMimeTypeName() { + return mimeTypeName; + } + + public Long getSizeInBytes() { + return sizeInBytes; + } + + public String getEncoding() { + return encoding; + } + + @Override + public String toString() + { + return "ContentInfo [mimeType=" + mimeType + ", mimeTypeName=" + mimeTypeName + + ", encoding=" + encoding + ", sizeInBytes=" + sizeInBytes + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomAspect.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomAspect.java new file mode 100644 index 00000000000..6715f109d1c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomAspect.java @@ -0,0 +1,67 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomAspect extends AbstractClassModel +{ + + public CustomAspect() + { + } + + public CustomAspect(AspectDefinition aspectDefinition, MessageLookup messageLookup, List properties) + { + this.name = aspectDefinition.getName().getLocalName(); + this.prefixedName = aspectDefinition.getName().toPrefixString(); + this.title = aspectDefinition.getTitle(messageLookup); + this.description = aspectDefinition.getDescription(messageLookup); + this.parentName = getParentNameAsString(aspectDefinition.getParentName()); + this.properties = setList(properties); + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(512); + builder.append("CustomAspect [name=").append(this.name) + .append(", prefixedName=").append(this.prefixedName) + .append(", title=").append(this.title) + .append(", description=").append(this.description) + .append(", parentName=").append(parentName) + .append(", properties=").append(properties) + .append(']'); + return builder.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModel.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModel.java new file mode 100644 index 00000000000..64c128bc455 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModel.java @@ -0,0 +1,224 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +import org.alfresco.service.cmr.dictionary.CustomModelDefinition; +import org.alfresco.service.cmr.dictionary.NamespaceDefinition; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomModel implements Comparable +{ + public static enum ModelStatus + { + ACTIVE, DRAFT + } + + private String name; + private String author; + private String description; + private ModelStatus status; + private String namespaceUri; + private String namespacePrefix; + private List types; + private List aspects; + private List constraints; + + public CustomModel() + { + } + + public CustomModel(CustomModelDefinition modelDefinition) + { + this(modelDefinition, null, null, null); + } + + public CustomModel(CustomModelDefinition modelDefinition, List types, List aspects, List constraints) + { + this.name = modelDefinition.getName().getLocalName(); + this.author = modelDefinition.getAuthor(); + this.description = modelDefinition.getDescription(); + this.status = modelDefinition.isActive() ? ModelStatus.ACTIVE : ModelStatus.DRAFT; + // we don't need to check for NoSuchElementException, as we don't allow + // the model to be saved without a valid namespace + NamespaceDefinition nsd = modelDefinition.getNamespaces().iterator().next(); + this.namespaceUri = nsd.getUri(); + this.namespacePrefix = nsd.getPrefix(); + this.types = types; + this.aspects = aspects; + this.constraints = constraints; + } + + public String getName() + { + return this.name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getAuthor() + { + return this.author; + } + + public void setAuthor(String author) + { + this.author = author; + } + + public String getDescription() + { + return this.description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public ModelStatus getStatus() + { + return this.status; + } + + public void setStatus(ModelStatus status) + { + this.status = status; + } + + public String getNamespaceUri() + { + return this.namespaceUri; + } + + public void setNamespaceUri(String namespaceUri) + { + this.namespaceUri = namespaceUri; + } + + public String getNamespacePrefix() + { + return this.namespacePrefix; + } + + public void setNamespacePrefix(String namespacePrefix) + { + this.namespacePrefix = namespacePrefix; + } + + public List getTypes() + { + return this.types; + } + + public void setTypes(List types) + { + this.types = types; + } + + public List getAspects() + { + return this.aspects; + } + + public void setAspects(List aspects) + { + this.aspects = aspects; + } + + public List getConstraints() + { + return this.constraints; + } + + public void setConstraints(List constraints) + { + this.constraints = constraints; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (!(obj instanceof CustomModel)) + { + return false; + } + CustomModel other = (CustomModel) obj; + if (this.name == null) + { + if (other.name != null) + { + return false; + } + } + else if (!this.name.equals(other.name)) + { + return false; + } + return true; + } + + @Override + public int compareTo(CustomModel customModel) + { + return this.name.compareTo(customModel.getName()); + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(512); + builder.append("CustomModel [name=").append(this.name).append(", author=").append(this.author) + .append(", description=").append(this.description).append(", status=").append(this.status) + .append(", namespaceUri=").append(this.namespaceUri).append(", namespacePrefix=") + .append(this.namespacePrefix).append(']'); + return builder.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelConstraint.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelConstraint.java new file mode 100644 index 00000000000..30beb8f1291 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelConstraint.java @@ -0,0 +1,104 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomModelConstraint extends AbstractCommonDetails +{ + private String type; + private List parameters = Collections.emptyList(); + + public CustomModelConstraint() + { + } + + public CustomModelConstraint(ConstraintDefinition constraintDefinition, MessageLookup messageLookup) + { + this.name = constraintDefinition.getName().getLocalName(); + this.prefixedName = constraintDefinition.getConstraint().getShortName(); + this.type = constraintDefinition.getConstraint().getType(); + this.title = constraintDefinition.getTitle(messageLookup); + this.description = constraintDefinition.getDescription(messageLookup); + this.parameters = convertToNamedValue(constraintDefinition.getConstraint().getParameters()); + } + + private List convertToNamedValue(Map params) + { + List list = new ArrayList<>(params.size()); + for (Entry en : params.entrySet()) + { + list.add(new CustomModelNamedValue(en.getKey(), en.getValue())); + } + + return list; + } + + public String getType() + { + return this.type; + } + + public void setType(String type) + { + this.type = type; + } + + public List getParameters() + { + return this.parameters; + } + + public void setParameters(List parameters) + { + this.parameters = parameters; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(300); + builder.append("CustomModelConstraint [name=").append(this.name) + .append(", prefixedName=").append(this.prefixedName) + .append(", type=").append(this.type) + .append(", title=").append(this.title) + .append(", description=").append(this.description) + .append(", parameters=").append(this.parameters) + .append(']'); + return builder.toString(); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelDownload.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelDownload.java new file mode 100644 index 00000000000..01b1c623643 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelDownload.java @@ -0,0 +1,101 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomModelDownload implements Comparable +{ + private String nodeRef; + + public CustomModelDownload() + { + } + + public CustomModelDownload(NodeRef nodeRef) + { + this.nodeRef = nodeRef.toString(); + } + + public String getNodeRef() + { + return this.nodeRef; + } + + public void setNodeRef(String nodeRef) + { + this.nodeRef = nodeRef; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.nodeRef == null) ? 0 : this.nodeRef.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (!(obj instanceof CustomModelDownload)) + { + return false; + } + CustomModelDownload other = (CustomModelDownload) obj; + if (this.nodeRef == null) + { + if (other.nodeRef != null) + { + return false; + } + } + else if (!this.nodeRef.equals(other.nodeRef)) + { + return false; + } + return true; + } + + @Override + public int compareTo(CustomModelDownload other) + { + return this.nodeRef.toString().compareTo(other.getNodeRef().toString()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelNamedValue.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelNamedValue.java new file mode 100644 index 00000000000..97e91573ce6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelNamedValue.java @@ -0,0 +1,166 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; + +/** + * + * + * @author Jamal Kaabi-Mofrad + */ +public class CustomModelNamedValue implements Comparable +{ + private String name; + private String simpleValue = null; + private List listValue = null; + + public CustomModelNamedValue() + { + } + + public CustomModelNamedValue(String name, Object value) + { + this.name = name; + if (value instanceof List) + { + List values = (List) value; + listValue = new ArrayList<>(values.size()); + for(Object val : values) + { + listValue.add(convertToString(val)); + } + } + else + { + simpleValue = convertToString(value); + } + } + + private String convertToString(Object value) + { + try + { + return DefaultTypeConverter.INSTANCE.convert(String.class, value); + } + catch (TypeConversionException e) + { + throw new InvalidArgumentException("Cannot convert to string '" + value + "'."); + } + } + + public String getName() + { + return this.name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getSimpleValue() + { + return this.simpleValue; + } + + public void setSimpleValue(String simpleValue) + { + this.simpleValue = simpleValue; + } + + public List getListValue() + { + return this.listValue; + } + + public void setListValue(List listValue) + { + this.listValue = listValue; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (!(obj instanceof CustomModelNamedValue)) + { + return false; + } + CustomModelNamedValue other = (CustomModelNamedValue) obj; + if (this.name == null) + { + if (other.name != null) + { + return false; + } + } + else if (!this.name.equals(other.name)) + { + return false; + } + return true; + } + + @Override + public int compareTo(CustomModelNamedValue other) + { + return name.compareTo(other.getName()); + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(120); + builder.append("CustomModelNamedValue [name=").append(this.name) + .append(", simpleValue=").append(this.simpleValue) + .append(", listValue=").append(this.listValue) + .append(']'); + return builder.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelProperty.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelProperty.java new file mode 100644 index 00000000000..1a4450a7825 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomModelProperty.java @@ -0,0 +1,213 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.alfresco.repo.dictionary.Facetable; +import org.alfresco.repo.dictionary.IndexTokenisationMode; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomModelProperty extends AbstractCommonDetails +{ + private String dataType; + private boolean isMandatory; + private boolean isMandatoryEnforced; + private boolean isMultiValued; + private String defaultValue; + private boolean isIndexed = true; + private Facetable facetable = Facetable.UNSET; + private IndexTokenisationMode indexTokenisationMode; + private List constraintRefs = Collections.emptyList(); + private List constraints = Collections.emptyList(); + + public CustomModelProperty() + { + } + + public CustomModelProperty(PropertyDefinition propertyDefinition, MessageLookup messageLookup) + { + this.name = propertyDefinition.getName().getLocalName(); + this.prefixedName = propertyDefinition.getName().toPrefixString(); + this.title = propertyDefinition.getTitle(messageLookup); + this.dataType = propertyDefinition.getDataType().getName().toPrefixString(); + this.description = propertyDefinition.getDescription(messageLookup); + this.isMandatory = propertyDefinition.isMandatory(); + this.isMandatoryEnforced = propertyDefinition.isMandatoryEnforced(); + this.isMultiValued = propertyDefinition.isMultiValued(); + this.defaultValue = propertyDefinition.getDefaultValue(); + this.isIndexed = propertyDefinition.isIndexed(); + this.facetable = propertyDefinition.getFacetable(); + this.indexTokenisationMode = propertyDefinition.getIndexTokenisationMode(); + List constraintDefs = propertyDefinition.getConstraints(); + if (constraintDefs.size() > 0) + { + this.constraintRefs = new ArrayList<>(); + this.constraints = new ArrayList<>(); + for (ConstraintDefinition cd : constraintDefs) + { + if (cd.getRef() != null) + { + constraintRefs.add(cd.getRef().toPrefixString()); + } + else + { + constraints.add(new CustomModelConstraint(cd, messageLookup)); + } + } + } + } + + public String getDataType() + { + return this.dataType; + } + + public void setDataType(String dataType) + { + this.dataType = dataType; + } + + public boolean isMandatory() + { + return this.isMandatory; + } + + public void setMandatory(boolean isMandatory) + { + this.isMandatory = isMandatory; + } + + public boolean isMandatoryEnforced() + { + return this.isMandatoryEnforced; + } + + public void setMandatoryEnforced(boolean isMandatoryEnforced) + { + this.isMandatoryEnforced = isMandatoryEnforced; + } + + public boolean isMultiValued() + { + return this.isMultiValued; + } + + public void setMultiValued(boolean isMultiValued) + { + this.isMultiValued = isMultiValued; + } + + public String getDefaultValue() + { + return this.defaultValue; + } + + public void setDefaultValue(String defaultValue) + { + this.defaultValue = defaultValue; + } + + public boolean isIndexed() + { + return this.isIndexed; + } + + public void setIndexed(boolean isIndexed) + { + this.isIndexed = isIndexed; + } + + public Facetable getFacetable() + { + return this.facetable; + } + + public void setFacetable(Facetable facetable) + { + this.facetable = facetable; + } + + public IndexTokenisationMode getIndexTokenisationMode() + { + return this.indexTokenisationMode; + } + + public void setIndexTokenisationMode(IndexTokenisationMode indexTokenisationMode) + { + this.indexTokenisationMode = indexTokenisationMode; + } + + public List getConstraintRefs() + { + return this.constraintRefs; + } + + public void setConstraintRefs(List constraintRefs) + { + this.constraintRefs = constraintRefs; + } + + public List getConstraints() + { + return this.constraints; + } + + public void setConstraints(List constraints) + { + this.constraints = constraints; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(612); + builder.append("CustomModelProperty [name=").append(this.name) + .append(", prefixedName=").append(this.prefixedName) + .append(", title=").append(this.title) + .append(", description=").append(this.description) + .append(", dataType=").append(this.dataType) + .append(", isMandatory=").append(this.isMandatory) + .append(", isMandatoryEnforced=").append(this.isMandatoryEnforced) + .append(", isMultiValued=").append(this.isMultiValued) + .append(", defaultValue=").append(this.defaultValue) + .append(", isIndexed=").append(this.isIndexed) + .append(", facetable=").append(this.facetable) + .append(", indexTokenisationMode=").append(this.indexTokenisationMode) + .append(", constraintRefs=").append(this.constraintRefs) + .append(", constraints=").append(this.constraints) + .append(']'); + return builder.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/CustomType.java b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomType.java new file mode 100644 index 00000000000..bfaf5dc585d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/CustomType.java @@ -0,0 +1,67 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.i18n.MessageLookup; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class CustomType extends AbstractClassModel +{ + + public CustomType() + { + } + + public CustomType(TypeDefinition typeDefinition, MessageLookup messageLookup, List properties) + { + this.name = typeDefinition.getName().getLocalName(); + this.prefixedName = typeDefinition.getName().toPrefixString(); + this.title = typeDefinition.getTitle(messageLookup); + this.description = typeDefinition.getDescription(messageLookup); + this.parentName = getParentNameAsString(typeDefinition.getParentName()); + this.properties = setList(properties); + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(512); + builder.append("CustomType [name=").append(this.name) + .append(", prefixedName=").append(this.prefixedName) + .append(", title=").append(this.title) + .append(", description=").append(this.description) + .append(", parentName=").append(parentName) + .append(", properties=").append(properties) + .append(']'); + return builder.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/DirectAccessUrlRequest.java b/remote-api/src/main/java/org/alfresco/rest/api/model/DirectAccessUrlRequest.java new file mode 100644 index 00000000000..ff25083d7a5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/DirectAccessUrlRequest.java @@ -0,0 +1,58 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; + +public class DirectAccessUrlRequest +{ + private Date expiresAt; + private Long validFor; + + public DirectAccessUrlRequest() + { + } + + public Date getExpiresAt() + { + return expiresAt; + } + + public void setExpiresAt(Date expiresAt) + { + this.expiresAt = expiresAt; + } + + public Long getValidFor() + { + return validFor; + } + + public void setValidFor(Long validFor) + { + this.validFor = validFor; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/DiscoveryDetails.java b/remote-api/src/main/java/org/alfresco/rest/api/model/DiscoveryDetails.java new file mode 100644 index 00000000000..e88b8f872ce --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/DiscoveryDetails.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * Representation of the discovery information + * + * @author Jamal Kaabi-Mofrad + */ +public class DiscoveryDetails +{ + private RepositoryInfo repository; + + public DiscoveryDetails() + { + } + + public DiscoveryDetails(RepositoryInfo repository) + { + this.repository = repository; + } + + public RepositoryInfo getRepository() + { + return repository; + } + + public DiscoveryDetails setRepository(RepositoryInfo repository) + { + this.repository = repository; + return this; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(); + sb.append("DiscoveryDetails [repository=").append(repository) + .append(']'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Document.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Document.java new file mode 100644 index 00000000000..98eeb52276a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Document.java @@ -0,0 +1,143 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.alfresco.model.ContentModel; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Representation of a document node. + * + * @author steveglover + * @author janv + * + */ +public class Document extends Node +{ + public Document() { + super(); + } + + public Document(NodeRef nodeRef, NodeRef parentNodeRef, Map nodeProps, Map mapUserInfo, ServiceRegistry sr) + { + super(nodeRef, parentNodeRef, nodeProps, mapUserInfo, sr); + + Serializable val = nodeProps.get(ContentModel.PROP_CONTENT); + + if ((val != null) && (val instanceof ContentData)) { + ContentData cd = (ContentData)val; + String mimeType = cd.getMimetype(); + String mimeTypeName = sr.getMimetypeService().getDisplaysByMimetype().get(mimeType); + contentInfo = new ContentInfo(mimeType, mimeTypeName, cd.getSize(), cd.getEncoding()); + } + + setIsFolder(false); + setIsFile(true); + } + + @Override + public ContentInfo getContent() + { + return contentInfo; + } + + @Override + public void setContent(ContentInfo contentInfoIn) + { + contentInfo = contentInfoIn; + } + + @Override + public String toString() + { + return "Document [contentInfo=" + contentInfo.toString() + ", nodeRef=" + + nodeRef + ", name=" + name + ", createdAt=" + createdAt + + ", modifiedAt=" + modifiedAt + ", createdBy=" + createdBy + + ", modifiedBy=" + modifiedBy + "]"; + } + + // TODO for backwards compat' - set explicitly when needed (ie. favourites) + private String mimeType; + private BigInteger sizeInBytes; + private String versionLabel; + + /** + * @deprecated + */ + public String getMimeType() + { + return mimeType; + } + + /** + * @deprecated + */ + public BigInteger getSizeInBytes() + { + return sizeInBytes; + } + + /** + * @deprecated + */ + public String getVersionLabel() + { + return versionLabel; + } + + /** + * @deprecated + */ + public void setMimeType(String mimeType) + { + this.mimeType = mimeType; + } + + /** + * @deprecated + */ + public void setSizeInBytes(BigInteger sizeInBytes) + { + this.sizeInBytes = sizeInBytes; + } + + /** + * @deprecated + */ + public void setVersionLabel(String versionLabel) + { + this.versionLabel = versionLabel; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/DocumentRatingSummary.java b/remote-api/src/main/java/org/alfresco/rest/api/model/DocumentRatingSummary.java new file mode 100644 index 00000000000..09ee217bfce --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/DocumentRatingSummary.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +public interface DocumentRatingSummary +{ +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/DocumentTarget.java b/remote-api/src/main/java/org/alfresco/rest/api/model/DocumentTarget.java new file mode 100644 index 00000000000..261c5b8e41d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/DocumentTarget.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * A document target favourite. + * + * @author steveglover + * + */ +public class DocumentTarget extends Target +{ + private Document file; + + public DocumentTarget() + { + super(); + } + + public DocumentTarget(Document file) + { + super(); + this.file = file; + } + + public void setDocument(Document file) + { + this.file = file; + } + + public Document getFile() + { + return file; + } + + @Override + public String toString() + { + return "DocumentTarget [file=" + file + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Download.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Download.java new file mode 100644 index 00000000000..498ccaac61d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Download.java @@ -0,0 +1,132 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +import org.alfresco.service.cmr.download.DownloadStatus; + +/** + * Represents a download entity + * + */ +public class Download +{ + private String id; + private List nodeIds; + private DownloadStatus.Status status; + private long bytesAdded; + private long totalBytes; + private long filesAdded; + private long totalFiles; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public List getNodeIds() + { + return nodeIds; + } + + public void setNodeIds(List nodeIds) + { + this.nodeIds = nodeIds; + } + + public DownloadStatus.Status getStatus() + { + return status; + } + + public void setStatus(DownloadStatus.Status status) + { + this.status = status; + } + + public long getBytesAdded() + { + return bytesAdded; + } + + public void setBytesAdded(long bytesAdded) + { + this.bytesAdded = bytesAdded; + } + + public long getTotalBytes() + { + return totalBytes; + } + + public void setTotalBytes(long totalBytes) + { + this.totalBytes = totalBytes; + } + + public long getFilesAdded() + { + return filesAdded; + } + + public void setFilesAdded(long filesAdded) + { + this.filesAdded = filesAdded; + } + + public long getTotalFiles() + { + return totalFiles; + } + + public void setTotalFiles(long totalFiles) + { + this.totalFiles = totalFiles; + } + + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(150); + builder.append("Download [id=").append(id) + .append(", nodeIds=").append(nodeIds) + .append(", status=").append(status) + .append(", bytesAdded=").append(bytesAdded) + .append(", totalBytes=").append(totalBytes) + .append(", filesAdded=").append(filesAdded) + .append(", totalFiles=").append(totalFiles) + .append("]"); + return builder.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Favourite.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Favourite.java new file mode 100644 index 00000000000..a148f4fc051 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Favourite.java @@ -0,0 +1,93 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; +import java.util.Map; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Representation of a favourite (document, folder, site, ...). + * + * @author steveglover + * + */ +public class Favourite +{ + private String targetGuid; + private Date createdAt; + private Target target; + private Map properties; + + public Date getCreatedAt() + { + return createdAt; + } + + public void setCreatedAt(Date createdAt) + { + this.createdAt = createdAt; + } + + @UniqueId(name="targetGuid") + public String getTargetGuid() + { + return targetGuid; + } + + public void setTargetGuid(String targetGuid) + { + this.targetGuid = targetGuid; + } + + public Target getTarget() + { + return target; + } + + public void setTarget(Target target) + { + this.target = target; + } + + public Map getProperties() + { + return properties; + } + + public void setProperties(Map properties) + { + this.properties = properties; + } + + @Override + public String toString() + { + return "Favourite [targetGuid=" + targetGuid + + ", createdAt=" + createdAt + ", target=" + target + ", properties=" + properties + "]"; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/FavouriteSite.java b/remote-api/src/main/java/org/alfresco/rest/api/model/FavouriteSite.java new file mode 100644 index 00000000000..2a0d43c6682 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/FavouriteSite.java @@ -0,0 +1,56 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * Represents a user's favourite site. + * + * Represented by a separate class in order to allow other attributes to be added. + * + * @author steveglover + * + */ +public class FavouriteSite extends Site +{ + public FavouriteSite() + { + } + + public FavouriteSite(SiteInfo siteInfo, String role) + { + super(siteInfo, role); + } + + @Override + public String toString() + { + return "FavouriteSite [id=" + id + ", guid=" + guid + ", title=" + + title + ", description=" + description + ", visibility=" + + visibility + ", role=" + role + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/FiveStarRatingSummary.java b/remote-api/src/main/java/org/alfresco/rest/api/model/FiveStarRatingSummary.java new file mode 100644 index 00000000000..c1663c3be69 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/FiveStarRatingSummary.java @@ -0,0 +1,57 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +public class FiveStarRatingSummary implements DocumentRatingSummary +{ + private Integer numberOfRatings; + private Float average; + + public FiveStarRatingSummary(Integer numberOfRatings, Float ratingTotal, Float average) + { + super(); + this.numberOfRatings = numberOfRatings; + this.average = (average == -1 ? null : average); + } + + public Integer getNumberOfRatings() + { + return numberOfRatings; + } + + public Float getAverage() + { + return average; + } + + @Override + public String toString() + { + return "FiveStarRatingSummary [numberOfRatings=" + numberOfRatings + + ", average=" + average + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Folder.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Folder.java new file mode 100644 index 00000000000..82f6b203b3d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Folder.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.io.Serializable; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Representation of a folder node. + * + * @author steveglover + * @author janv + */ +public class Folder extends Node +{ + public Folder() + { + super(); + } + + public Folder(NodeRef nodeRef, NodeRef parentNodeRef, Map nodeProps, Map mapUserInfo, ServiceRegistry sr) + { + super(nodeRef, parentNodeRef, nodeProps, mapUserInfo, sr); + + setIsFolder(true); + setIsFile(false); + } + + @Override + public ContentInfo getContent() + { + return null; + } + + @Override + public void setContent(ContentInfo contentInfo) + { + } + + @Override + public String toString() + { + return "Folder [nodeRef=" + nodeRef + ", name=" + name + ", title=" + + title + ", description=" + description + ", createdAt=" + + createdAt + ", modifiedAt=" + modifiedAt + ", createdBy=" + + createdBy + ", modifiedBy=" + modifiedBy + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/FolderTarget.java b/remote-api/src/main/java/org/alfresco/rest/api/model/FolderTarget.java new file mode 100644 index 00000000000..6a61696388f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/FolderTarget.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * A folder target favourite. + * + * @author steveglover + * + */ +public class FolderTarget extends Target +{ + private Folder folder; + + public FolderTarget() + { + super(); + } + + public FolderTarget(Folder folder) + { + super(); + this.folder = folder; + } + + public void setFolder(Folder folder) + { + this.folder = folder; + } + + public Folder getFolder() + { + return folder; + } + + @Override + public String toString() + { + return "FolderTarget [folder=" + folder + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Group.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Group.java new file mode 100644 index 00000000000..6ef826229cf --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Group.java @@ -0,0 +1,165 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a group. + * + * @author cturlica + * + */ +public class Group implements Comparable +{ + + protected String id; // group id (aka authority name) + protected String displayName; + protected Boolean isRoot; + protected Set parentIds; + protected Set zones; + + private Map setFields = new HashMap<>(7); + + public static final String ID = "id"; + public static final String DISPLAY_NAME = "displayName"; + public static final String IS_ROOT = "isRoot"; + public static final String PARENT_IDS = "parentIds"; + public static final String ZONES = "zones"; + + public Group() + { + } + + @UniqueId + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + setFields.put(ID, true); + } + + public String getDisplayName() + { + return displayName; + } + + public void setDisplayName(String displayName) + { + this.displayName = displayName; + setFields.put(DISPLAY_NAME, true); + } + + public Boolean getIsRoot() + { + return isRoot; + } + + public void setIsRoot(Boolean isRoot) + { + this.isRoot = isRoot; + setFields.put(IS_ROOT, true); + } + + public Set getParentIds() + { + return parentIds; + } + + public void setParentIds(Set parentIds) + { + this.parentIds = parentIds; + setFields.put(PARENT_IDS, true); + } + + public Set getZones() + { + return zones; + } + + public void setZones(Set zones) + { + this.zones = zones; + setFields.put(ZONES, true); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + Group other = (Group) obj; + return id.equals(other.id); + } + + @Override + public int compareTo(Group group) + { + return id.compareTo(group.getId()); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public String toString() + { + return "Group [id=" + id + ", displayName=" + displayName + ", isRoot=" + isRoot + "]"; + } + + public boolean wasSet(String fieldName) + { + Boolean b = setFields.get(fieldName); + return (b != null ? b : false); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/GroupMember.java b/remote-api/src/main/java/org/alfresco/rest/api/model/GroupMember.java new file mode 100644 index 00000000000..b3931d5591d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/GroupMember.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a group member. + * + * @author cturlica + * + */ +public class GroupMember implements Comparable +{ + + private String id; // group id (aka authority name) + private String displayName; + private String memberType; + + @UniqueId + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getDisplayName() + { + return displayName; + } + + public void setDisplayName(String displayName) + { + this.displayName = displayName; + } + + public String getMemberType() + { + return memberType; + } + + public void setMemberType(String memberType) + { + this.memberType = memberType; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + GroupMember other = (GroupMember) obj; + return id.equals(other.id); + } + + @Override + public int compareTo(GroupMember group) + { + return id.compareTo(group.getId()); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public String toString() + { + return "GroupMember [id=" + id + ", displayName=" + displayName + ", memberType=" + memberType + "]"; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/LikesRatingSummary.java b/remote-api/src/main/java/org/alfresco/rest/api/model/LikesRatingSummary.java new file mode 100644 index 00000000000..efecd1cb4a3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/LikesRatingSummary.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +public class LikesRatingSummary implements DocumentRatingSummary +{ + private Integer numberOfRatings; + + public LikesRatingSummary(Integer numberOfRatings) + { + super(); + this.numberOfRatings = numberOfRatings; + } + + public Integer getNumberOfRatings() + { + return numberOfRatings; + } + + @Override + public String toString() + { + return "LikesRatingSummary [numberOfRatings=" + numberOfRatings + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/LockInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/LockInfo.java new file mode 100644 index 00000000000..46c4c0b3be7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/LockInfo.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.alfresco.repo.lock.mem.Lifetime; +import org.alfresco.service.cmr.lock.LockType; + +/** + * Representation of a lock info + * + * @author Ancuta Morarasu + */ +@JsonIgnoreProperties({"mappedType"}) +public class LockInfo +{ + private Integer timeToExpire; + private LockType2 type; + private Lifetime lifetime; + + /** + * Lock Type enum that maps to the current values in {@link org.alfresco.service.cmr.lock.LockType}. + * These values describe better the meanings of the lock types. + */ + @SuppressWarnings("deprecation") + public static enum LockType2 + { + FULL(LockType.READ_ONLY_LOCK), + ALLOW_OWNER_CHANGES(LockType.WRITE_LOCK); + // ALLOW_ADD_CHILDREN(LockType.NODE_LOCK); // removed for now, as per REPO-1187 + + private LockType type; + + private LockType2(LockType type) + { + this.type = type; + } + + public LockType getType() + { + return type; + } + } + + public LockInfo() {} + + public void setTimeToExpire(Integer timeToExpire) + { + this.timeToExpire = timeToExpire; + } + + public Integer getTimeToExpire() + { + return timeToExpire; + } + + public LockType getMappedType() + { + return type != null ? type.getType() : null; + } + + public LockType2 getType() + { + return type; + } + + public void setType(String type) + { + this.type = LockType2.valueOf(type); + } + + public Lifetime getLifetime() + { + return lifetime; + } + + public void setLifetime(String lifetimeStr) + { + this.lifetime = Lifetime.valueOf(lifetimeStr); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("LockInfo{"); + sb.append(", timeToExpire='").append(timeToExpire).append('\''); + sb.append(", type='").append(type).append('\''); + sb.append(", lifetime='").append(lifetime).append('\''); + sb.append('}'); + return sb.toString(); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/LoginTicket.java b/remote-api/src/main/java/org/alfresco/rest/api/model/LoginTicket.java new file mode 100644 index 00000000000..c747c5333d2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/LoginTicket.java @@ -0,0 +1,77 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class LoginTicket +{ + protected String userId; + protected String password; + protected String id; + + public String getUserId() + { + return userId; + } + + public void setUserId(String userId) + { + this.userId = userId; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(150); + sb.append("LoginTicket [userId=").append(userId) + .append(", password=").append(password) + .append(", id=").append(id) + .append(']'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/LoginTicketResponse.java b/remote-api/src/main/java/org/alfresco/rest/api/model/LoginTicketResponse.java new file mode 100644 index 00000000000..31eecf1934b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/LoginTicketResponse.java @@ -0,0 +1,62 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class LoginTicketResponse extends LoginTicket +{ + + public LoginTicketResponse() + { + this.password = null; + } + + @Override + public String getPassword() + { + return null; + } + + @Override + public void setPassword(String password) + { + // intentionally empty + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(150); + sb.append("LoginTicketResponse [userId=").append(userId) + .append(", password=").append(password) + .append(", id=").append(id) + .append(']'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/MemberOfSite.java b/remote-api/src/main/java/org/alfresco/rest/api/model/MemberOfSite.java new file mode 100644 index 00000000000..0204a435c40 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/MemberOfSite.java @@ -0,0 +1,173 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.api.sites.SiteEntityResource; +import org.alfresco.rest.framework.resource.EmbeddedEntityResource; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; + +/** + * Represents membership of a site. + * + * @author steveglover + * + */ +public class MemberOfSite implements Comparable +{ + private String role; + private String siteShortName; + private NodeRef guid; + + public MemberOfSite() + { + } + + public MemberOfSite(String siteShortName, NodeRef siteGuid, String role) + { + super(); + if(siteShortName == null) + { + throw new IllegalArgumentException(); + } + if(role == null) + { + throw new IllegalArgumentException(); + } + if(siteGuid == null) + { + throw new IllegalArgumentException(); + } + this.role = role; + this.siteShortName = siteShortName; + this.guid = siteGuid; + } + + public static MemberOfSite getMemberOfSite(SiteInfo siteInfo, String siteRole) + { + MemberOfSite memberOfSite = new MemberOfSite(siteInfo.getShortName(), siteInfo.getNodeRef(), siteRole); + return memberOfSite; + } + + @JsonProperty("id") + @UniqueId + @EmbeddedEntityResource(propertyName = "site", entityResource = SiteEntityResource.class) + public String getSiteShortName() + { + return siteShortName; + } + + public NodeRef getGuid() + { + return guid; + } + + public void setGuid(NodeRef guid) + { + this.guid = guid; + } + + public String getRole() + { + return role; + } + + public void setRole(String role) + { + if(role == null) + { + throw new IllegalArgumentException(); + } + this.role = role; + } + + public void setSiteShortName(String siteShortName) + { + if(siteShortName == null) + { + throw new IllegalArgumentException(); + } + this.siteShortName = siteShortName; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((role == null) ? 0 : role.hashCode()); + result = prime * result + + ((siteShortName == null) ? 0 : siteShortName.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; + } + + MemberOfSite other = (MemberOfSite) obj; + if (role != other.role) + { + return false; + } + + return siteShortName.equals(other.siteShortName); + } + + @Override + public int compareTo(MemberOfSite o) + { + int i = siteShortName.compareTo(o.getSiteShortName()); + if(i == 0) + { + i = role.compareTo(o.getRole()); + } + return i; + } + + @Override + public String toString() + { + return "MemberOfSite [role=" + role + ", siteShortName=" + + siteShortName + ", siteGuid=" + guid + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/ModulePackage.java b/remote-api/src/main/java/org/alfresco/rest/api/model/ModulePackage.java new file mode 100644 index 00000000000..f7e171842be --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/ModulePackage.java @@ -0,0 +1,139 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.repo.module.ModuleVersionNumber; +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.service.cmr.module.ModuleInstallState; +import org.alfresco.util.VersionNumber; +import org.apache.maven.artifact.versioning.ArtifactVersion; + +import java.util.Date; + +/** + * POJO representation of Module Details for Serialization as JSON. + * @author Gethin James. + */ +public class ModulePackage +{ + private String id; + private String title; + private String description; + private String version; + private Date installDate; + private ModuleInstallState installState; + private String versionMin; + private String versionMax; + + public ModulePackage() + { + } + + private ModulePackage(ModuleDetails moduleDetails) + { + this.id = moduleDetails.getId(); + this.title = moduleDetails.getTitle(); + this.description = moduleDetails.getDescription(); + this.version = moduleDetails.getModuleVersionNumber().toString(); + this.installDate = moduleDetails.getInstallDate(); + this.installState = moduleDetails.getInstallState(); + this.versionMin = moduleDetails.getRepoVersionMin().toString(); + this.versionMax = moduleDetails.getRepoVersionMax().toString(); + } + + public static ModulePackage fromModuleDetails(ModuleDetails moduleDetails) + { + try + { + return new ModulePackage(moduleDetails); + } + catch (NullPointerException npe) + { + //Something went wrong with the definition of the Module. + //These are just POJO properties, I am unable to represent + //the data so will return null + return null; + } + } + + public String getId() + { + return id; + } + + public String getTitle() + { + return title; + } + + public String getDescription() + { + return description; + } + + public String getVersion() + { + return version; + } + + public Date getInstallDate() + { + return installDate; + } + + public ModuleInstallState getInstallState() + { + return installState; + } + + public String getVersionMin() + { + return versionMin; + } + + public String getVersionMax() + { + return versionMax; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("ModulePackage{"); + sb.append("id='").append(id).append('\''); + sb.append(", title='").append(title).append('\''); + sb.append(", description='").append(description).append('\''); + sb.append(", version=").append(version); + sb.append(", installDate=").append(installDate); + sb.append(", installState=").append(installState); + sb.append(", versionMin='").append(versionMin).append('\''); + sb.append(", versionMax='").append(versionMax).append('\''); + sb.append('}'); + return sb.toString(); + } +} + + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Network.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Network.java new file mode 100644 index 00000000000..67db80fd668 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Network.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; +import java.util.List; + +/** + * Represents a cloud network (account). + * + * @author steveglover + * + */ +public interface Network +{ + public String getId(); + + /** + * Gets the date the account was created + * + * @return The account creation date + */ + public Date getCreatedAt(); + public List getQuotas(); + + /** + * Gets whether an account is enabled or not. + * + * @return true = account is enabled, false = account is disabled + */ + public Boolean getIsEnabled(); + + /** + * Gets the subscription level. + * @return String + */ + public String getSubscriptionLevel(); + + public Boolean getPaidNetwork(); +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NetworkImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NetworkImpl.java new file mode 100644 index 00000000000..6fe06c00628 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NetworkImpl.java @@ -0,0 +1,164 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a cloud network (account). + * + * @author steveglover + * + */ +public class NetworkImpl implements Comparable, Network +{ + private String id; + private Date createdAt; + private List quotas = new LinkedList(); + private Boolean isEnabled; + private String subscriptionLevel; + private Boolean paidNetwork; + + public NetworkImpl(org.alfresco.repo.tenant.Network network) + { + this.id = network.getTenantDomain(); + this.createdAt = network.getCreatedAt(); + this.isEnabled = network.isEnabled(); + this.paidNetwork = network.getPaidNetwork(); + this.subscriptionLevel = network.getSubscriptionLevel(); + } + + public NetworkImpl(String id, Date createdAt, Boolean isEnabled, String subscriptionLevel, Boolean paidNetwork) + { + this.id = id; + this.createdAt = createdAt; + this.isEnabled = isEnabled; + this.subscriptionLevel = subscriptionLevel; + this.paidNetwork = paidNetwork; + } + + /** + * Get the account name + * + * @return The name of the account + */ + @UniqueId + public String getId() + { + return id; + } + + /** + * Gets the date the account was created + * + * @return The account creation date + */ + public Date getCreatedAt() + { + return createdAt; + } + + public List getQuotas() + { + return quotas; + } + + /** + * Gets whether an account is enabled or not. + * + * @return true = account is enabled, false = account is disabled + */ + public Boolean getIsEnabled() + { + return isEnabled; + } + + /** + * Gets the subscription level. + * + * @return ths subscription level + */ + public String getSubscriptionLevel() + { + return subscriptionLevel; + } + + public Boolean getPaidNetwork() + { + return paidNetwork; + } + + @Override + public int compareTo(NetworkImpl network) + { + return id.compareTo(network.getId()); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.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; + } + + Network other = (Network) obj; + return(id.equals(other.getId())); + } + + @Override + public String toString() + { + return "Network [id=" + id + + ", createdAt=" + createdAt + ", quotas=" + quotas + + ", isEnabled=" + isEnabled + ", subscriptionLevel=" + + subscriptionLevel + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Node.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Node.java new file mode 100644 index 00000000000..9f3827d8f47 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Node.java @@ -0,0 +1,689 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.rest.api.search.model.SearchEntry; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.NoSuchPersonException; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; +import org.apache.chemistry.opencmis.commons.data.PropertyData; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Concrete class carrying general information for alf_node data + * + * @author steveglover + * @author Gethin James + * @author janv + */ +public class Node implements Comparable +{ + private static final Log logger = LogFactory.getLog(Node.class); + + protected NodeRef nodeRef; + protected String name; + + protected Date createdAt; + protected Date modifiedAt; + protected UserInfo createdByUser; + protected UserInfo modifiedByUser; + + // Archived info - specifically for archive (deleted) node - see Trashcan API + protected Date archivedAt; + protected UserInfo archivedByUser; + + // Version info - specifically for version node - see Version History API + protected String versionLabel; + protected String versionComment; + protected String nodeId; //This is the frozen node id NOT the current node id + + protected Boolean isFolder; + protected Boolean isFile; + protected Boolean isLink; + protected Boolean isLocked; + + protected NodeRef parentNodeRef; + protected PathInfo pathInfo; + protected String prefixTypeQName; + + // please note: these are currently only used (optionally) for node create request + protected String relativePath; + protected List secondaryChildren; + protected List targets; + + + protected List aspectNames; + protected Map properties; + + protected List allowableOperations; + protected NodePermissions nodePermissions; + protected NodeDefinition definition; + + //optional SearchEntry (only ever returned from a search) + protected SearchEntry search = null; + protected String location; + protected Boolean isFavorite; + + public Node(NodeRef nodeRef, NodeRef parentNodeRef, Map nodeProps, Map mapUserInfo, ServiceRegistry sr) + { + if(nodeRef == null) + { + throw new IllegalArgumentException(); + } + + this.nodeRef = nodeRef; + this.parentNodeRef = parentNodeRef; + + mapMinimalInfo(nodeProps, mapUserInfo, sr); + } + + protected Object getValue(Map> props, String name) + { + PropertyData prop = props.get(name); + Object value = (prop != null ? prop.getFirstValue() : null); + return value; + } + + public Node() + { + } + + protected void mapMinimalInfo(Map nodeProps, Map mapUserInfo, ServiceRegistry sr) + { + PersonService personService = sr.getPersonService(); + + this.name = (String)nodeProps.get(ContentModel.PROP_NAME); + + if (mapUserInfo == null) { + // minor: save one lookup if creator & modifier are the same + mapUserInfo = new HashMap<>(2); + } + + this.createdAt = (Date)nodeProps.get(ContentModel.PROP_CREATED); + this.createdByUser = lookupUserInfo((String)nodeProps.get(ContentModel.PROP_CREATOR), mapUserInfo, personService); + + this.modifiedAt = (Date)nodeProps.get(ContentModel.PROP_MODIFIED); + this.modifiedByUser = lookupUserInfo((String)nodeProps.get(ContentModel.PROP_MODIFIER), mapUserInfo, personService); + } + + public static UserInfo lookupUserInfo(String userName, Map mapUserInfo, PersonService personService) + { + return lookupUserInfo(userName, mapUserInfo, personService, false); + } + + public static UserInfo lookupUserInfo(String userName, Map mapUserInfo, PersonService personService, boolean displayNameOnly) + { + UserInfo userInfo = mapUserInfo.get(userName); + if ((userInfo == null) && (userName != null)) + { + String sysUserName = AuthenticationUtil.getSystemUserName(); + if (userName.equals(sysUserName) || (AuthenticationUtil.isMtEnabled() && userName.startsWith(sysUserName + "@"))) + { + userInfo = new UserInfo((displayNameOnly ? null : userName), userName, ""); + } + else + { + PersonService.PersonInfo pInfo = null; + try + { + NodeRef pNodeRef = personService.getPersonOrNull(userName); + if (pNodeRef != null) + { + pInfo = personService.getPerson(pNodeRef); + } + } + catch (NoSuchPersonException nspe) + { + // drop-through + } + catch (AccessDeniedException ade) + { + // SFS-610 + // drop-through + } + + if (pInfo != null) + { + userInfo = new UserInfo((displayNameOnly ? null : userName), pInfo.getFirstName(), pInfo.getLastName()); + } + else + { + logger.warn("Unknown person: "+userName); + userInfo = new UserInfo((displayNameOnly ? null : userName), userName, ""); + } + } + + mapUserInfo.put(userName, userInfo); + } + return userInfo; + } + + // note: nodeRef maps to json "id" (when serializing/deserializng) + + @JsonProperty("id") + @UniqueId + public NodeRef getNodeRef() + { + return nodeRef; + } + + public void setNodeRef(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + + public Date getCreatedAt() + { + return this.createdAt; + } + + public void setCreated(Date createdAt) + { + this.createdAt = createdAt; + } + + public Date getModifiedAt() + { + return modifiedAt; + } + + public UserInfo getModifiedByUser() { + return modifiedByUser; + } + + public UserInfo getCreatedByUser() { + return createdByUser; + } + + public void setCreatedByUser(UserInfo createdByUser) + { + this.createdByUser = createdByUser; + } + + public String getName() + { + return this.name; + } + + public void setName(String name) + { + this.name = name; + } + + public PathInfo getPath() + { + return pathInfo; + } + + public void setPath(PathInfo pathInfo) + { + this.pathInfo = pathInfo; + } + + public String getNodeType() + { + return prefixTypeQName; + } + + public void setNodeType(String prefixType) + { + this.prefixTypeQName = prefixType; + } + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map props) { + this.properties = props; + } + + public List getAspectNames() { + return aspectNames; + } + + public void setAspectNames(List aspectNames) { + this.aspectNames = aspectNames; + } + + public NodeRef getParentId() + { + return parentNodeRef; + } + + public void setParentId(NodeRef parentNodeRef) + { + this.parentNodeRef = parentNodeRef; + } + + public Boolean getIsFolder() + { + return isFolder; + } + + public void setIsFolder(Boolean isFolder) + { + this.isFolder = isFolder; + } + + public Boolean getIsFile() + { + return isFile; + } + + public void setIsFile(Boolean isFile) + { + this.isFile = isFile; + } + + public Boolean getIsLink() + { + return isLink; + } + + public void setIsLink(Boolean isLink) + { + this.isLink = isLink; + } + + public Boolean getIsLocked() + { + return isLocked; + } + + public void setIsLocked(Boolean isLocked) + { + this.isLocked = isLocked; + } + + public Boolean getIsFavorite() + { + return isFavorite; + } + + public void setIsFavorite(Boolean isFavorite) + { + this.isFavorite = isFavorite; + } + + public List getAllowableOperations() + { + return allowableOperations; + } + + public void setAllowableOperations(List allowableOperations) + { + this.allowableOperations = allowableOperations; + } + + public NodePermissions getPermissions() + { + return nodePermissions; + } + + public void setPermissions(NodePermissions nodePermissions) + { + this.nodePermissions = nodePermissions; + } + + public List getTargets() + { + return targets; + } + + public void setTargets(List targets) + { + this.targets = targets; + } + + public Date getArchivedAt() + { + return archivedAt; + } + + public void setArchivedAt(Date archivedAt) + { + this.archivedAt = archivedAt; + } + + public UserInfo getArchivedByUser() + { + return archivedByUser; + } + + public void setArchivedByUser(UserInfo archivedByUser) + { + this.archivedByUser = archivedByUser; + } + + public String getVersionLabel() + { + return versionLabel; + } + + public void setVersionLabel(String versionLabel) + { + this.versionLabel = versionLabel; + } + + public String getVersionComment() + { + return versionComment; + } + + public void setVersionComment(String versionComment) + { + this.versionComment = versionComment; + } + + public String getLocation() + { + return location; + } + + public void setLocation(String location) + { + this.location = location; + } + + public String getNodeId() + { + return nodeId; + } + + public void setNodeId(String nodeId) + { + this.nodeId = nodeId; + } + + public boolean equals(Object other) + { + if(this == other) + { + return true; + } + + if(!(other instanceof Node)) + { + return false; + } + + Node node = (Node)other; + return EqualsHelper.nullSafeEquals(getNodeRef(), node.getNodeRef()); + } + + public String getRelativePath() + { + return relativePath; + } + + public void setRelativePath(String relativePath) + { + this.relativePath = relativePath; + } + + public List getSecondaryChildren() + { + return secondaryChildren; + } + + public void setSecondaryChildren(List secondaryChildren) + { + this.secondaryChildren = secondaryChildren; + } + + public NodeDefinition getDefinition() + { + return definition; + } + + public void setDefinition(NodeDefinition definition) + { + this.definition = definition; + } + + @Override + public int compareTo(Node node) + { + return getNodeRef().toString().compareTo(node.getNodeRef().toString()); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Node [id=").append(getNodeRef().getId()); + sb.append(", parentId=").append(getParentId()); + sb.append(", type=").append(getNodeType()); + sb.append(", name=").append(getName()); + sb.append(", isFolder=").append(getIsFolder()); + sb.append(", isFile=").append(getIsFile()); + sb.append(", modifiedAt=").append(getModifiedAt()); + sb.append(", modifiedByUser=").append(getModifiedByUser()); + sb.append(", createdAt=").append(getCreatedAt()); + sb.append(", createdByUser=").append(getCreatedByUser()); + if (getArchivedAt() != null) + { + sb.append(", archivedAt=").append(getArchivedAt()); + } + if (getArchivedByUser() != null) + { + sb.append(", archivedByUser=").append(getArchivedByUser()); + } + if (getVersionLabel() != null) + { + sb.append(", versionLabel=").append(getVersionLabel()); + } + if (getVersionComment() != null) + { + sb.append(", versionComment=").append(getVersionComment()); + } + if (getLocation() != null) + { + sb.append(", location=").append(getLocation()); + } + if (getNodeId() != null) + { + sb.append(", nodeId=").append(getNodeId()); + } + if (getIsLink() != null) + { + sb.append(", isLink=").append(getIsLink()); // note: symbolic link (not shared link) + } + if (getPath() != null) + { + sb.append(", path=").append(getPath()); + } + if (getContent() != null) + { + sb.append(", content=").append(getContent()); + } + if (getAspectNames() != null) + { + sb.append(", aspectNames=").append(getAspectNames()); + } + if (getProperties() != null) + { + //sb.append(", properties=").append(getProperties()); + } + if (getRelativePath() != null) + { + sb.append(", relativePath=").append(getRelativePath()); + } + if (getAllowableOperations() != null) + { + sb.append(", allowableOperations=").append(getAllowableOperations()); + } + if (getSearch() != null) + { + sb.append(", search=").append(getSearch()); + } + sb.append("]"); + return sb.toString(); + } + + // here to allow POST /nodes/{id}/children when creating empty file with specified content.mimeType + // also allows list of results to be returned as "nodes" + + protected ContentInfo contentInfo; + + public void setContent(ContentInfo contentInfo) + { + this.contentInfo = contentInfo; + } + + public ContentInfo getContent() + { + return this.contentInfo; + } + + // when appropriate, can be used to show association (in the context of a listing), for example + // GET /nodes/parentId/children, /nodes/parentId/secondary-children, /nodes/childId/parents + // GET /nodes/sourceId/targets, /nodes/targetId/sources + protected Assoc association; + + public Assoc getAssociation() + { + return association; + } + + public void setAssociation(Assoc association) + { + this.association = association; + } + + public SearchEntry getSearch() + { + return search; + } + + public void setSearch(SearchEntry search) + { + this.search = search; + } + // TODO for backwards compat' - set explicitly when needed (ie. favourites) (note: we could choose to have separate old Node/NodeImpl etc) + + protected String title; + protected NodeRef guid; + protected String description; + protected String createdBy; + protected String modifiedBy; + + /** + * @deprecated + */ + public NodeRef getGuid() { + return guid; + } + + /** + * @deprecated + */ + public void setGuid(NodeRef guid) + { + this.guid = guid; + } + + /** + * @deprecated + */ + public String getTitle() + { + return title; + } + + /** + * @deprecated + */ + public void setTitle(String title) + { + this.title = title; + } + + /** + * @deprecated + */ + public String getDescription() + { + return description; + } + + /** + * @deprecated + */ + public void setDescription(String description) + { + this.description = description; + } + + /** + * @deprecated + */ + public String getCreatedBy() + { + return this.createdBy; + } + + /** + * @deprecated + */ + public void setCreatedBy(String createdBy) + { + this.createdBy = createdBy; + } + + /** + * @deprecated + */ + public String getModifiedBy() + { + return modifiedBy; + } + + /** + * @deprecated + */ + public void setModifiedBy(String modifiedBy) + { + this.modifiedBy = modifiedBy; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinition.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinition.java new file mode 100644 index 00000000000..227a1506235 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinition.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.List; +/** + * Represents the node definition. + * + * @author gfertuso + */ +public class NodeDefinition +{ + + List properties; + + public List getProperties() + { + return properties; + } + + public void setProperties(List properties) + { + this.properties = properties; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinitionConstraint.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinitionConstraint.java new file mode 100644 index 00000000000..8a7cbcb6fc6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinitionConstraint.java @@ -0,0 +1,92 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Map; +/** + * Represents a constraint of a node definition property. + * + * @author gfertuso + */ +public class NodeDefinitionConstraint +{ + private String id; + private String type; + private String title; + private String description; + private Map parameters; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public Map getParameters() + { + return parameters; + } + + public void setParameters(Map parameters) + { + this.parameters = parameters; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinitionProperty.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinitionProperty.java new file mode 100644 index 00000000000..c07d02c066e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeDefinitionProperty.java @@ -0,0 +1,147 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.List; +/** + * Represents a property of the node definition. + * + * @author gfertuso + */ +public class NodeDefinitionProperty +{ + private String id; + private String title; + private String description; + private String defaultValue; + private String dataType; + private Boolean isMultiValued; + private Boolean isMandatory; + private Boolean isMandatoryEnforced; + private Boolean isProtected; + private List constraints; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public String getDefaultValue() + { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) + { + this.defaultValue = defaultValue; + } + + public String getDataType() + { + return dataType; + } + + public void setDataType(String dataType) + { + this.dataType = dataType; + } + + public boolean getIsProtected() + { + return isProtected; + } + + public void setIsProtected(boolean isProtected) + { + this.isProtected = isProtected; + } + + public List getConstraints() + { + return constraints; + } + + public void setConstraints(List constraints) + { + this.constraints = constraints; + } + + public boolean getIsMultiValued() + { + return isMultiValued; + } + + public void setIsMultiValued(boolean isMultiValued) + { + this.isMultiValued = isMultiValued; + } + + public boolean getIsMandatory() + { + return isMandatory; + } + + public void setIsMandatory(boolean isMandatory) + { + this.isMandatory = isMandatory; + } + + public boolean getIsMandatoryEnforced() + { + return isMandatoryEnforced; + } + + public void setIsMandatoryEnforced(boolean isMandatoryEnforced) + { + this.isMandatoryEnforced = isMandatoryEnforced; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodePermissions.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodePermissions.java new file mode 100644 index 00000000000..afbc50f3297 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodePermissions.java @@ -0,0 +1,171 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.List; +import java.util.Set; + +import org.alfresco.service.cmr.security.AccessStatus; + + +/** + * Representation of Node Permissions + * + * @author janv + */ +public class NodePermissions +{ + private Boolean inherit; + private List inherited; + private List locallySet; + private Set settable; + + public NodePermissions() + { + } + + public NodePermissions(Boolean inherit, + List inherited, + List locallySet, + Set settable) + { + this.inherit = inherit; + this.inherited = inherited; + this.locallySet = locallySet; + this.settable = settable; + } + + public Boolean getIsInheritanceEnabled() + { + return inherit; + } + + public void setIsInheritanceEnabled(boolean inherit) + { + this.inherit = inherit; + } + + public List getInherited() + { + return inherited; + } + + public List getLocallySet() + { + return locallySet; + } + + public void setLocallySet(List directPermissions) + { + this.locallySet = directPermissions; + } + + public Set getSettable() + { + return settable; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(120); + sb.append("PathInfo [isInheritanceEnabled=").append(inherit) + .append(", inherited=").append(getInherited()) + .append(", locallySet=").append(getLocallySet()) + .append(", settable=").append(getSettable()) + .append(']'); + return sb.toString(); + } + + public static class NodePermission + { + + private String authorityId; + private String name; + private String accessStatus; + + public NodePermission() + { + } + + public NodePermission(String authorityId, String name, String accessStatus) + { + this.authorityId = authorityId; + this.name = name; + this.accessStatus = accessStatus != null ? accessStatus : AccessStatus.ALLOWED.toString(); + } + + public String getName() + { + return name; + } + + public String getAuthorityId() + { + return authorityId; + } + + public String getAccessStatus() + { + return accessStatus; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(250); + sb.append("NodePermission [authorityId=").append(authorityId) + .append(", name=").append(name) + .append(", accessStatus=").append(accessStatus) + .append(']'); + return sb.toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + NodePermission that = (NodePermission) o; + + if (authorityId != null ? !authorityId.equals(that.authorityId) : that.authorityId != null) + return false; + return name != null ? name.equals(that.name) : that.name == null; + + } + + @Override + public int hashCode() + { + int result = authorityId != null ? authorityId.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodeRating.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeRating.java new file mode 100644 index 00000000000..bf451a25757 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeRating.java @@ -0,0 +1,147 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a node rating. + * + * @author steveglover + * + */ +public class NodeRating implements Comparable +{ + private String ratingSchemeId; + private Object myRating; + private Date ratedAt; + private DocumentRatingSummary documentRatingSummary; + + public NodeRating() + { + } + + public NodeRating(String ratingSchemeId, Object myRating, Date ratedAt, DocumentRatingSummary documentRatingSummary) + { + if(ratingSchemeId == null) + { + throw new IllegalArgumentException(); + } + + this.ratingSchemeId = ratingSchemeId; + this.documentRatingSummary = documentRatingSummary; + this.myRating = myRating; + this.ratedAt = ratedAt; + } + + @JsonProperty("id") + @UniqueId + public String getScheme() + { + return ratingSchemeId; + } + + public void setScheme(String ratingSchemeId) + { + if(ratingSchemeId == null) + { + throw new IllegalArgumentException(); + } + this.ratingSchemeId = ratingSchemeId; + } + + public Date getRatedAt() + { + return ratedAt; + } + + public Object getMyRating() + { + return myRating; + } + + public void setMyRating(Object myRating) + { + this.myRating = myRating; + } + + public DocumentRatingSummary getAggregate() + { + return documentRatingSummary; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((ratingSchemeId == null) ? 0 : ratingSchemeId.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; + NodeRating other = (NodeRating) obj; + if (ratingSchemeId == null) { + if (other.getScheme() != null) + return false; + } else if (!ratingSchemeId.equals(other.getScheme())) + return false; + return true; + } + + @Override + public int compareTo(NodeRating other) + { + if(other != null) + { + int ret = (ratingSchemeId.compareTo(other.getScheme())); + return ret; + } + else + { + throw new IllegalArgumentException(); + } + } + + @Override + public String toString() + { + return "NodeRating [scheme=" + ratingSchemeId + ", myRating=" + myRating + + ", ratedAt=" + ratedAt + + ", documentRatingSummary=" + documentRatingSummary + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodeTarget.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeTarget.java new file mode 100644 index 00000000000..bd2b6ea7bc2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeTarget.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * A target object. + * + * @author Gethin James + */ +public class NodeTarget +{ + String targetParentId; + String name; + + public NodeTarget() + { + } + + public String getTargetParentId() + { + return targetParentId; + } + + public void setTargetParentId(String targetParentId) + { + this.targetParentId = targetParentId; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("NodeTarget{"); + sb.append("targetParentId=").append(targetParentId); + sb.append(", name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/NodeTargetAssoc.java b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeTargetAssoc.java new file mode 100644 index 00000000000..017b0b1fc5a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/NodeTargetAssoc.java @@ -0,0 +1,52 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +public class NodeTargetAssoc extends Assoc +{ + private String targetParentId; + + public NodeTargetAssoc() + { + } + + public NodeTargetAssoc(String targetParentId, String prefixAssocTypeQName) + { + super(prefixAssocTypeQName); + + this.targetParentId = targetParentId; + } + + public String getTargetParentId() + { + return targetParentId; + } + + public void setTargetParentId(String targetParentId) + { + this.targetParentId = targetParentId; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/PasswordReset.java b/remote-api/src/main/java/org/alfresco/rest/api/model/PasswordReset.java new file mode 100644 index 00000000000..48c987759fd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/PasswordReset.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +/** + * Representation of a password reset. + * + * @author Jamal Kaabi-Mofrad + * @since 5.2.1 + */ +public class PasswordReset +{ + /** new password */ + private String password; + /** workflow Id */ + private String id; + /** workflow Key */ + private String key; + + public PasswordReset() + { + } + + public String getPassword() + { + return password; + } + + public PasswordReset setPassword(String password) + { + this.password = password; + return this; + } + + public String getId() + { + return id; + } + + public PasswordReset setId(String id) + { + this.id = id; + return this; + } + + public String getKey() + { + return key; + } + + public PasswordReset setKey(String key) + { + this.key = key; + return this; + } + + @Override + public String toString() + { + // we don't return the password for the obvious reason + final StringBuilder sb = new StringBuilder(100); + sb.append("PasswordReset [id=").append(id) + .append(", key=").append(key) + .append(']'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/PathInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/PathInfo.java new file mode 100644 index 00000000000..ac4ad610842 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/PathInfo.java @@ -0,0 +1,142 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.List; + + +/** + * Representation of a path info + * + * @author janv + */ +public class PathInfo +{ + private String name; + private Boolean isComplete; + private String relativePath; + private List elements; + + public PathInfo() + { + } + + public PathInfo(String name, Boolean isComplete, List elements) + { + this.name = name; + this.isComplete = isComplete; + this.elements = elements; + } + + public String getName() + { + return name; + } + + public Boolean getIsComplete() + { + return isComplete; + } + + public List getElements() + { + return elements; + } + + public String getRelativePath() + { + return relativePath; + } + + public void setRelativePath(String relativePath) + { + this.relativePath = relativePath; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(120); + sb.append("PathInfo [name=").append(name) + .append(", isComplete=").append(isComplete) + .append(", elements=").append(elements) + .append(']'); + return sb.toString(); + } + + public static class ElementInfo + { + + private String id; + private String name; + private String nodeType; + private List aspectNames; + + public ElementInfo() + { + } + + public ElementInfo(String id, String name, String nodeType, List aspectNames) + { + this.id = id; + this.name = name; + this.nodeType = nodeType; + this.aspectNames = aspectNames; + } + + public String getName() + { + return name; + } + + public String getId() + { + return id; + } + + public String getNodeType() + { + return nodeType; + } + + public List getAspectNames() + { + return aspectNames; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(250); + sb.append("PathElement [id=").append(id) + .append(", name=").append(name) + .append(", nodeType=").append(nodeType) + .append(", aspectNames=").append(aspectNames) + .append(']'); + return sb.toString(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Person.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Person.java new file mode 100644 index 00000000000..d1bd4dbf0c6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Person.java @@ -0,0 +1,618 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.model.ContentModel; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a person (aka user) within the system. + * + * @author steveglover + * + */ +public class Person implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String userName; + protected Boolean enabled; + protected NodeRef avatarId; + protected String firstName; + protected String lastName; + protected String displayName; + protected String jobTitle; + protected String location; + protected String telephone; + protected String mobile; + protected String email; + protected String skypeId; + protected String instantMessageId; + protected String userStatus; + protected Date statusUpdatedAt; + protected String googleId; + protected Long quota; + protected Long quotaUsed; + protected Boolean emailNotificationsEnabled; + protected String description; + protected transient Company company; + protected String password; + protected String oldPassword; + protected transient Map properties; + protected transient List aspectNames; + protected Map capabilities; + + private Map setFields = new HashMap<>(7); + + public static final QName PROP_PERSON_DESCRIPTION = QName.createQName("RestApi", "description"); + public static final QName PROP_PERSON_COMPANY = QName.createQName("RestApi", "company"); + public static final QName PROP_PERSON_AVATAR_ID = QName.createQName("RestApi", "avatarId"); + public static final QName PROP_PERSON_OLDPASSWORD = QName.createQName("RestApi", "oldPassword"); + public static final QName PROP_PERSON_PASSWORD = QName.createQName("RestApi", "password"); + + public Person() + { + } + + public Person( + String userName, + Boolean enabled, + NodeRef avatarId, + String firstName, + String lastName, + String jobTitle, + String location, + String telephone, + String mobile, + String email, + String skypeId, + String instantMessageId, + String userStatus, + Date statusUpdatedAt, + String googleId, + Long quota, + Long quotaUsed, + Boolean emailNotificationsEnabled, + String description, + Company company) + { + setUserName(userName); + setEnabled(enabled); + setAvatarId(avatarId); + setFirstName(firstName); + setLastName(lastName); + setJobTitle(jobTitle); + setLocation(location); + setTelephone(telephone); + setMobile(mobile); + if (email != null) + { + setEmail(email); + } + setSkypeId(skypeId); + setInstantMessageId(instantMessageId); + setUserStatus(userStatus); + setGoogleId(googleId); + setEmailNotificationsEnabled(emailNotificationsEnabled); + setDescription(description); + setCompany(company); + + // system-maintained / derived + setStatusUpdatedAt(statusUpdatedAt); + setQuota(quota); + updateDisplayName(); + if (quotaUsed != null) + { + setQuotaUsed(quotaUsed); + } + } + + public Person(NodeRef nodeRef, Map nodeProps, boolean enabled) + { + mapProperties(nodeProps); + this.enabled = enabled; + updateDisplayName(); + } + + protected void mapProperties(Map nodeProps) + { + nodeProps.remove(ContentModel.PROP_CONTENT); + + setUserName((String) nodeProps.get(ContentModel.PROP_USERNAME)); + setFirstName((String) nodeProps.get(ContentModel.PROP_FIRSTNAME)); + setLastName((String) nodeProps.get(ContentModel.PROP_LASTNAME)); + setJobTitle((String) nodeProps.get(ContentModel.PROP_JOBTITLE)); + + setLocation((String) nodeProps.get(ContentModel.PROP_LOCATION)); + setTelephone((String) nodeProps.get(ContentModel.PROP_TELEPHONE)); + setMobile((String) nodeProps.get(ContentModel.PROP_MOBILE)); + setEmail((String) nodeProps.get(ContentModel.PROP_EMAIL)); + + String organization = (String) nodeProps.get(ContentModel.PROP_ORGANIZATION); + String companyAddress1 = (String) nodeProps.get(ContentModel.PROP_COMPANYADDRESS1); + String companyAddress2 = (String) nodeProps.get(ContentModel.PROP_COMPANYADDRESS2); + String companyAddress3 = (String) nodeProps.get(ContentModel.PROP_COMPANYADDRESS3); + String companyPostcode = (String) nodeProps.get(ContentModel.PROP_COMPANYPOSTCODE); + String companyTelephone = (String) nodeProps.get(ContentModel.PROP_COMPANYTELEPHONE); + String companyFax = (String) nodeProps.get(ContentModel.PROP_COMPANYFAX); + String companyEmail = (String) nodeProps.get(ContentModel.PROP_COMPANYEMAIL); + setCompany(new Company(organization, companyAddress1, companyAddress2, companyAddress3, companyPostcode, companyTelephone, companyFax, companyEmail)); + + setSkypeId((String) nodeProps.get(ContentModel.PROP_SKYPE)); + setInstantMessageId((String) nodeProps.get(ContentModel.PROP_INSTANTMSG)); + setUserStatus((String) nodeProps.get(ContentModel.PROP_USER_STATUS)); + setGoogleId((String) nodeProps.get(ContentModel.PROP_GOOGLEUSERNAME)); + Boolean bool = (Boolean)nodeProps.get(ContentModel.PROP_EMAIL_FEED_DISABLED); + setEmailNotificationsEnabled(bool == null ? Boolean.TRUE : !bool.booleanValue()); + setDescription((String)nodeProps.get(PROP_PERSON_DESCRIPTION)); + + // system-maintained / derived + setStatusUpdatedAt((Date) nodeProps.get(ContentModel.PROP_USER_STATUS_TIME)); + setQuota((Long) nodeProps.get(ContentModel.PROP_SIZE_QUOTA)); + setQuotaUsed((Long) nodeProps.get(ContentModel.PROP_SIZE_CURRENT)); + } + + public Company getCompany() + { + return company; + } + + public void setCompany(Company company) + { + this.company = company; + setFields.put(PROP_PERSON_COMPANY, true); + } + + public String getInstantMessageId() + { + return instantMessageId; + } + + public void setInstantMessageId(String instantMessageId) + { + this.instantMessageId = instantMessageId; + setFields.put(ContentModel.PROP_INSTANTMSG, true); + } + + public String getGoogleId() + { + return googleId; + } + + public void setGoogleId(String googleId) + { + this.googleId = googleId; + setFields.put(ContentModel.PROP_GOOGLEUSERNAME, true); + } + + public Long getQuota() + { + return quota; + } + + // TEMP: for tracking (pending REPO-110) + protected void setQuota(Long quota) + { + this.quota = quota; + setFields.put(ContentModel.PROP_SIZE_QUOTA, true); + } + + public Long getQuotaUsed() + { + return quotaUsed; + } + + // TEMP: for tracking (pending REPO-110) + protected void setQuotaUsed(Long quotaUsed) + { + this.quotaUsed = quotaUsed; + setFields.put(ContentModel.PROP_SIZE_CURRENT, true); + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + setFields.put(PROP_PERSON_DESCRIPTION, true); + } + + @JsonProperty("id") + @UniqueId + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + setFields.put(ContentModel.PROP_USERNAME, true); + } + + public Boolean isEnabled() + { + return enabled; + } + + public void setEnabled(Boolean enabled) + { + this.enabled = enabled; + setFields.put(ContentModel.PROP_ENABLED, true); + } + + public void setAvatarId(NodeRef avatarId) + { + this.avatarId = avatarId; + setFields.put(PROP_PERSON_AVATAR_ID, true); + } + + public void setPassword(String password) + { + this.password = password; + setFields.put(PROP_PERSON_PASSWORD, true); + } + + public void setOldPassword(String oldPassword) + { + this.oldPassword = oldPassword; + setFields.put(PROP_PERSON_OLDPASSWORD, true); + } + + public NodeRef getAvatarId() + { + return avatarId; + } + + public String getFirstName() + { + return firstName; + } + + public void setFirstName(String firstName) + { + this.firstName = firstName; + setFields.put(ContentModel.PROP_FIRSTNAME, true); + updateDisplayName(); + } + + public void setLastName(String lastName) + { + this.lastName = lastName; + setFields.put(ContentModel.PROP_LASTNAME, true); + updateDisplayName(); + } + + public String getLastName() + { + return lastName; + } + + public String getJobTitle() + { + return jobTitle; + } + + public void setJobTitle(String jobTitle) + { + this.jobTitle = jobTitle; + setFields.put(ContentModel.PROP_JOBTITLE, true); + } + + public String getLocation() + { + return location; + } + + public void setLocation(String location) + { + this.location = location; + setFields.put(ContentModel.PROP_LOCATION, true); + } + + public String getTelephone() + { + return telephone; + } + + public void setTelephone(String telephone) + { + this.telephone = telephone; + setFields.put(ContentModel.PROP_TELEPHONE, true); + } + + public String getMobile() + { + return mobile; + } + + public void setMobile(String mobile) + { + this.mobile = mobile; + setFields.put(ContentModel.PROP_MOBILE, true); + } + + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + setFields.put(ContentModel.PROP_EMAIL, true); + } + + public String getSkypeId() + { + return skypeId; + } + + public void setSkypeId(String skypeId) + { + this.skypeId = skypeId; + setFields.put(ContentModel.PROP_SKYPE, true); + } + + public String getUserStatus() + { + return userStatus; + } + + public void setUserStatus(String userStatus) + { + this.userStatus = userStatus; + setFields.put(ContentModel.PROP_USER_STATUS, true); + } + + public Date getStatusUpdatedAt() + { + return statusUpdatedAt; + } + + // TEMP: for tracking (pending REPO-110) + protected void setStatusUpdatedAt(Date statusUpdatedAt) + { + this.statusUpdatedAt = statusUpdatedAt; + setFields.put(ContentModel.PROP_USER_STATUS_TIME, true); + } + + public Boolean isEmailNotificationsEnabled() + { + return emailNotificationsEnabled; + } + + public void setEmailNotificationsEnabled(Boolean emailNotificationsEnabled) + { + this.emailNotificationsEnabled = emailNotificationsEnabled; + setFields.put(ContentModel.PROP_EMAIL_FEED_DISABLED, true); + } + + public String getPassword() + { + return this.password; + } + + public String getOldPassword() + { + return oldPassword; + } + + public Map getProperties() + { + return properties; + } + + public void setProperties(Map properties) + { + this.properties = properties; + } + + public List getAspectNames() + { + return aspectNames; + } + + public void setAspectNames(List aspectNames) + { + this.aspectNames = aspectNames; + } + + public Map getCapabilities() + { + return capabilities; + } + + public void setCapabilities(Map capabilities) + { + this.capabilities = capabilities; + } + + public boolean wasSet(QName fieldName) + { + Boolean b = setFields.get(fieldName); + return (b != null ? b : false); + } + + public String getDisplayName() + { + return displayName; + } + + @Override + public String toString() + { + return "Person [userName=" + userName + ", enabled=" + enabled + + ", avatarId=" + avatarId + ", firstName=" + firstName + + ", lastName=" + lastName + ", jobTitle=" + jobTitle + + ", location=" + location + + ", telephone=" + telephone + ", mobile=" + mobile + + ", email=" + email + ", skypeId=" + skypeId + + ", instantMessageId=" + instantMessageId + ", userStatus=" + + userStatus + ", statusUpdatedAt=" + statusUpdatedAt + + ", googleId=" + googleId + ", quota=" + quota + + ", quotaUsed=" + quotaUsed + ", emailNotificationsEnabled=" + + emailNotificationsEnabled + ", description=" + description + + ", company=" + company + "]"; + } + + public Map toProperties() + { + Map props = new HashMap<>(); + populateProps(props); + return props; + } + + private void addToMap(Map properties, QName name, Serializable value) + { + if (name != null) + { + Boolean b = setFields.get(name); + if (Boolean.TRUE.equals(b)) + { + properties.put(name, value); + } + } + } + + private void populateProps(Map properties) + { + addToMap(properties, ContentModel.PROP_USERNAME, getUserName()); + addToMap(properties, ContentModel.PROP_FIRSTNAME, getFirstName()); + addToMap(properties, ContentModel.PROP_LASTNAME, getLastName()); + addToMap(properties, ContentModel.PROP_JOBTITLE, getJobTitle()); + addToMap(properties, ContentModel.PROP_LOCATION, getLocation()); + addToMap(properties, ContentModel.PROP_TELEPHONE, getTelephone()); + addToMap(properties, ContentModel.PROP_MOBILE, getMobile()); + if (wasSet(ContentModel.PROP_EMAIL)) + { + addToMap(properties, ContentModel.PROP_EMAIL, getEmail()); + } + + if (wasSet(PROP_PERSON_COMPANY)) + { + Company company = getCompany(); + + int setCount = 0; + if (company != null) + { + if (company.wasSet(ContentModel.PROP_ORGANIZATION)) + { + setCount++; + properties.put(ContentModel.PROP_ORGANIZATION, company.getOrganization()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYADDRESS1)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYADDRESS1, company.getAddress1()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYADDRESS2)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYADDRESS2, company.getAddress2()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYADDRESS3)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYADDRESS3, company.getAddress3()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYPOSTCODE)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYPOSTCODE, company.getPostcode()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYTELEPHONE)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYTELEPHONE, company.getTelephone()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYFAX)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYFAX, company.getFax()); + } + + if (company.wasSet(ContentModel.PROP_COMPANYEMAIL)) + { + setCount++; + properties.put(ContentModel.PROP_COMPANYEMAIL, company.getEmail()); + } + } + + if (setCount == 0) + { + // company was null or {} (no individual properties set) + properties.put(ContentModel.PROP_ORGANIZATION, null); + properties.put(ContentModel.PROP_COMPANYADDRESS1, null); + properties.put(ContentModel.PROP_COMPANYADDRESS2, null); + properties.put(ContentModel.PROP_COMPANYADDRESS3, null); + properties.put(ContentModel.PROP_COMPANYPOSTCODE, null); + properties.put(ContentModel.PROP_COMPANYTELEPHONE, null); + properties.put(ContentModel.PROP_COMPANYFAX, null); + properties.put(ContentModel.PROP_COMPANYEMAIL, null); + } + } + +// addToMap(properties, ContentModel.ASSOC_AVATAR, getAvatarId()); + addToMap(properties, ContentModel.PROP_SKYPE, getSkypeId()); + addToMap(properties, ContentModel.PROP_INSTANTMSG, getInstantMessageId()); + addToMap(properties, ContentModel.PROP_USER_STATUS, getUserStatus()); + addToMap(properties, ContentModel.PROP_USER_STATUS_TIME, getStatusUpdatedAt()); + addToMap(properties, ContentModel.PROP_GOOGLEUSERNAME, getGoogleId()); + addToMap(properties, ContentModel.PROP_SIZE_QUOTA, getQuota()); + if (wasSet(ContentModel.PROP_SIZE_CURRENT)) + { + addToMap(properties, ContentModel.PROP_SIZE_CURRENT, getQuotaUsed()); + } + addToMap(properties, ContentModel.PROP_PERSONDESC, getDescription()); + addToMap(properties, ContentModel.PROP_ENABLED, isEnabled()); + + Boolean isEmailNotificationsEnabled = isEmailNotificationsEnabled(); + addToMap(properties, ContentModel.PROP_EMAIL_FEED_DISABLED, (isEmailNotificationsEnabled == null ? null : !isEmailNotificationsEnabled.booleanValue())); + } + + private void updateDisplayName() + { + displayName = ((firstName != null ? firstName + " " : "") + (lastName != null ? lastName : "")).trim(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/PersonNetwork.java b/remote-api/src/main/java/org/alfresco/rest/api/model/PersonNetwork.java new file mode 100644 index 00000000000..79e8bfe1eae --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/PersonNetwork.java @@ -0,0 +1,132 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; +import java.util.List; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents network membership. + * + * @author steveglover + * + */ +public class PersonNetwork implements Network, Comparable +{ + private Boolean homeNetwork; + private Network network; + + public PersonNetwork() + { + } + + public PersonNetwork(Boolean homeNetwork, Network network) + { + super(); + this.homeNetwork = homeNetwork; + this.network = network; + } + + @UniqueId + public String getId() + { + return network.getId(); + } + + public Date getCreatedAt() + { + return network.getCreatedAt(); + } + + public List getQuotas() + { + return network.getQuotas(); + } + + public Boolean getIsEnabled() + { + return network.getIsEnabled(); + } + + public String getSubscriptionLevel() + { + return network.getSubscriptionLevel(); + } + + public Boolean getPaidNetwork() + { + return network.getPaidNetwork(); + } + + @Override + public int compareTo(PersonNetwork member) + { + int ret = getId().compareTo(member.getId()); + return ret; + } + + public Boolean getHomeNetwork() + { + return homeNetwork; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((network == null) ? 0 : network.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; + PersonNetwork other = (PersonNetwork) obj; + if (network == null) + { + if (other.network != null) + return false; + } + else if (!network.equals(other.network)) + return false; + return true; + } + + @Override + public String toString() + { + return "PersonNetwork [homeNetwork=" + homeNetwork + ", network=" + + network + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Preference.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Preference.java new file mode 100644 index 00000000000..c2817791a65 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Preference.java @@ -0,0 +1,126 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; + +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a user preference. + * + * @author steveglover + * + */ +public class Preference implements Comparable +{ + private String name; + private Serializable value; + + public Preference() + { + } + + public Preference(String name, Serializable value) + { + if(name == null) + { + throw new IllegalArgumentException(); + } + this.name = name; + this.value = value; + } + + @JsonProperty("id") + @UniqueId + public String getName() + { + return name; + } + + public void setName(String name) + { + if(name == null) + { + throw new IllegalArgumentException(); + } + this.name = name; + } + + public Serializable getValue() + { + return value; + } + + public void setValue(Serializable value) + { + this.value = value; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.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; + } + + Preference other = (Preference) obj; + return name.equals(other.name); + } + + @Override + public int compareTo(Preference preference) + { + return name.compareTo(preference.getName()); + } + + @Override + public String toString() + { + return "Preference [name=" + name + ", value=" + value + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Probe.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Probe.java new file mode 100644 index 00000000000..d9ab9e3f890 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Probe.java @@ -0,0 +1,47 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * Representation of a probe result. + * + * Probes simply return a status code of 200 for okay and some other code if not. They should provide very limited + * information to the caller for security reasons as they generally provide unauthenticated access. + */ +public class Probe +{ + final String message; + + public Probe(String message) + { + this.message = message; + } + + public String getMessage() + { + return message; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/QuickShareLink.java b/remote-api/src/main/java/org/alfresco/rest/api/model/QuickShareLink.java new file mode 100644 index 00000000000..c9b281fe683 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/QuickShareLink.java @@ -0,0 +1,271 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Representation of quick share link + * + * The shared link id provides a short id that can be part of a short app url that is easy to + * copy/paste/send (via email or other). + * + * As of now, these shared links are public in that they provide unauthenticated access to the + * node's content and limited metadata info, such as file name and last modifier/modification. + * + * In the future, the QuickShareService *could* be enhanced to provide additional features, + * such as link expiry &/or "password" protection, etc. + * + * @author janv + * + */ +public class QuickShareLink +{ + // unique short id (ie. shorter than a guid, 22 vs 36 chars) + private String sharedId; + + private Date expiresAt; + + private String nodeId; + private String name; + private String title; + private String description; + private PathInfo path; + + private ContentInfo content; + + private Date modifiedAt; + private UserInfo modifiedByUser; + private UserInfo sharedByUser; + + private List allowableOperations; + + private List allowableOperationsOnTarget; + private Map properties; + private List aspectNames; + private Boolean isFavorite; + + public QuickShareLink() + { + } + + public QuickShareLink(String sharedId, String nodeId) + { + this.sharedId = sharedId; + this.nodeId = nodeId; + } + + public List getAspectNames() + { + return aspectNames; + } + + public void setAspectNames(List aspectNames) + { + this.aspectNames = aspectNames; + } + + public Map getProperties() + { + return properties; + } + + public void setProperties(Map properties) + { + this.properties = properties; + } + + public Boolean getIsFavorite() + { + return isFavorite; + } + + public void setIsFavorite(Boolean isFavorite) + { + this.isFavorite = isFavorite; + } + + public String getId() { + return sharedId; + } + + public void setId(String sharedId) { + this.sharedId = sharedId; + } + + public Date getExpiresAt() + { + return expiresAt; + } + + public void setExpiresAt(Date expiresAt) + { + this.expiresAt = expiresAt; + } + + public String getNodeId() { + return nodeId; + } + + public void setNodeId(String nodeId) { + this.nodeId = nodeId; + } + + public ContentInfo getContent() + { + return content; + } + + public void setContent(ContentInfo content) + { + this.content = content; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public PathInfo getPath() + { + return path; + } + + public void setPath(PathInfo pathInfo) + { + this.path = pathInfo; + } + + public Date getModifiedAt() + { + return modifiedAt; + } + + public void setModifiedAt(Date modifiedAt) + { + this.modifiedAt = modifiedAt; + } + + public UserInfo getModifiedByUser() + { + return modifiedByUser; + } + + public void setModifiedByUser(UserInfo modifiedByUser) + { + this.modifiedByUser = modifiedByUser; + } + + public UserInfo getSharedByUser() + { + return sharedByUser; + } + + public void setSharedByUser(UserInfo sharedByUser) + { + this.sharedByUser = sharedByUser; + } + + /** + * Retrieve the allowable operations for the shared link. + * + * @return List of operation labels, e.g. "delete" + */ + public List getAllowableOperations() + { + return allowableOperations; + } + + public void setAllowableOperations(List allowableOperations) + { + this.allowableOperations = allowableOperations; + } + + /** + * Retrieve the allowable operations for the actual file being shared. + * + * @return List of operation labels, e.g. "delete" + */ + public List getAllowableOperationsOnTarget() + { + return allowableOperationsOnTarget; + } + + public void setAllowableOperationsOnTarget(List allowableOperationsOnTarget) + { + this.allowableOperationsOnTarget = allowableOperationsOnTarget; + } + + // eg. for debug logging etc + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("QuickShareLink [id=").append(getId()); + sb.append(", nodeId=").append(getNodeId()); + sb.append(", name=").append(getName()); + sb.append(", title=").append(getTitle()); + sb.append(", description=").append(getDescription()); + sb.append(", path=").append(getPath()); + sb.append(", modifiedAt=").append(getModifiedAt()); + sb.append(", modifiedByUser=").append(getModifiedByUser()); + sb.append(", sharedByUser=").append(getSharedByUser()); + sb.append(", content=").append(getContent()); + sb.append(", allowableOperations=").append(getAllowableOperations()); + sb.append(", allowableOperationsOnTarget=").append(getAllowableOperationsOnTarget()); + sb.append(", expiresAt=").append(getExpiresAt()); + sb.append(", properties=").append(getProperties()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/QuickShareLinkEmailRequest.java b/remote-api/src/main/java/org/alfresco/rest/api/model/QuickShareLinkEmailRequest.java new file mode 100644 index 00000000000..1c3f6422bfd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/QuickShareLinkEmailRequest.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +/** + * Representation of an email request for the quick share link + * + * @author Jamal Kaabi-Mofrad + */ +public class QuickShareLinkEmailRequest +{ + private String client; + private String message; + private String locale; + private List recipientEmails; + + public String getClient() + { + return client; + } + + public QuickShareLinkEmailRequest setClient(String client) + { + this.client = client; + return this; + } + + public String getMessage() + { + return message; + } + + public void setMessage(String message) + { + this.message = message; + } + + public String getLocale() + { + return locale; + } + + public void setLocale(String locale) + { + this.locale = locale; + } + + public List getRecipientEmails() + { + return recipientEmails; + } + + public void setRecipientEmails(List recipientEmails) + { + this.recipientEmails = recipientEmails; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(250); + sb.append("QuickShareLinkEmailRequest [client=").append(client) + .append(", message=").append(message) + .append(", locale=").append(locale) + .append(", recipientEmails=").append(recipientEmails) + .append(']'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Quota.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Quota.java new file mode 100644 index 00000000000..30d9d87cedf --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Quota.java @@ -0,0 +1,93 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a network quota. + * + * @author steveglover + * + */ +public class Quota +{ + private String name; + private Long limit; + private Long usage; + + public Quota() + { + } + + public Quota(String name, Long limit, Long usage) + { + this.name = name; + this.limit = limit; + this.usage = usage; + } + + @JsonProperty("id") + @UniqueId + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public Long getLimit() + { + return limit; + } + + public void setLimit(Long limit) + { + this.limit = limit; + } + + public Long getUsage() + { + return usage; + } + + public void setUsage(Long usage) + { + this.usage = usage; + } + + @Override + public String toString() + { + return "Quota [name=" + name + ", limit=" + limit + ", usage=" + usage + + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Rendition.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Rendition.java new file mode 100644 index 00000000000..47e24d03afe --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Rendition.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.model; + +/** + * Representation of a rendition. + * + * @author Jamal Kaabi-Mofrad + */ +public class Rendition +{ + public enum RenditionStatus + { + CREATED, + NOT_CREATED + } + + private String id; + private RenditionStatus status; + private ContentInfo contentInfo; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public RenditionStatus getStatus() + { + return status; + } + + public void setStatus(RenditionStatus status) + { + this.status = status; + } + + public ContentInfo getContent() + { + return contentInfo; + } + + public void setContent(ContentInfo contentInfo) + { + this.contentInfo = contentInfo; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(150); + sb.append("Rendition [id=").append(id) + .append(", status=").append(status) + .append(", contentInfo=").append(contentInfo) + .append(']'); + return sb.toString(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java new file mode 100644 index 00000000000..f65aa8c5c7a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java @@ -0,0 +1,480 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.license.LicenseDescriptor; +import org.apache.commons.lang3.StringUtils; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Representation of the repository information. + * + * @author Jamal Kaabi-Mofrad + */ +public class RepositoryInfo +{ + private String id; + private String edition; + private VersionInfo version; + private LicenseInfo license; + private StatusInfo status; + private List modules; + + public String getId() + { + return id; + } + + public RepositoryInfo setId(String id) + { + this.id = id; + return this; + } + + public String getEdition() + { + return edition; + } + + public RepositoryInfo setEdition(String edition) + { + this.edition = edition; + return this; + } + + public VersionInfo getVersion() + { + return version; + } + + public RepositoryInfo setVersion(VersionInfo version) + { + this.version = version; + return this; + } + + public LicenseInfo getLicense() + { + return license; + } + + public RepositoryInfo setLicense(LicenseInfo license) + { + this.license = license; + return this; + } + + public StatusInfo getStatus() + { + return status; + } + + public RepositoryInfo setStatus(StatusInfo status) + { + this.status = status; + return this; + } + + public List getModules() + { + return modules; + } + + public RepositoryInfo setModules(List modules) + { + this.modules = modules; + return this; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(450); + sb.append("RepositoryInfo [edition=").append(edition) + .append(", version=").append(version) + .append(", license=").append(license) + .append(", status=").append(status) + .append(", modules=").append(modules) + .append(']'); + return sb.toString(); + } + + /** + * Representation of the repository version information. + * + * @author Jamal Kaabi-Mofrad + */ + public static class VersionInfo + { + private static final Pattern HOTFIX_PATTERN = Pattern.compile("^[0-9]+$"); + private String major; + private String minor; + private String patch; + private String hotfix; + private int schema; + private String label; + private String display; + + // Default constructor required for test purposes + public VersionInfo() + { + } + + public VersionInfo(Descriptor descriptor) + { + this.major = descriptor.getVersionMajor(); + this.minor = descriptor.getVersionMinor(); + this.patch = descriptor.getVersionRevision(); + this.hotfix = getHotfix(descriptor.getVersionLabel()); + this.schema = descriptor.getSchema(); + this.label = descriptor.getVersionBuild(); + this.display = getVersionDisplay(); + } + + public String getMajor() + { + return major; + } + + public String getMinor() + { + return minor; + } + + public String getPatch() + { + return patch; + } + + public String getHotfix() + { + return hotfix; + } + + public int getSchema() + { + return schema; + } + + public String getLabel() + { + return label; + } + + public String getDisplay() + { + return display; + } + + private String getHotfix(String versionLabel) + { + /* + * if the label starts with a dot, then digit(s), or just digit(s), we return the number only. + * for anything else zero will be returned. + */ + if (StringUtils.isNotEmpty(versionLabel)) + { + if (versionLabel.startsWith(".")) + { + versionLabel = versionLabel.substring(1); + } + Matcher matcher = HOTFIX_PATTERN.matcher(versionLabel); + if (matcher.find()) + { + return versionLabel; + } + } + return Integer.toString(0); + } + + private String getVersionDisplay() + { + StringBuilder version = new StringBuilder(major); + version.append('.') + .append(minor) + .append('.') + .append(patch) + .append('.') + .append(getHotfix()); + + if (StringUtils.isNotEmpty(label)) + { + version.append(" (").append(label).append(") "); + } + version.append("schema ").append(schema); + + // Display example: "5.2.0.1 (r123456-b0) schema 10001" + return version.toString(); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(250); + sb.append("VersionInfo [major=").append(major) + .append(", minor=").append(minor) + .append(", patch=").append(patch) + .append(", hotfix=").append(hotfix) + .append(", schema=").append(schema) + .append(", label=").append(label) + .append(", display=").append(display) + .append(']'); + return sb.toString(); + } + } + + /** + * Representation of the license information. + * + * @author Jamal Kaabi-Mofrad + */ + public static class LicenseInfo + { + private Date issuedAt; + private Date expiresAt; + private Integer remainingDays; + private String holder; + private String mode; + private LicenseEntitlement entitlements; + + // Default constructor required for test purposes + public LicenseInfo() + { + } + + public LicenseInfo(LicenseDescriptor licenseDescriptor) + { + this.issuedAt = licenseDescriptor.getIssued(); + this.expiresAt = licenseDescriptor.getValidUntil(); + this.remainingDays = licenseDescriptor.getRemainingDays(); + this.holder = licenseDescriptor.getHolderOrganisation(); + this.mode = licenseDescriptor.getLicenseMode().name(); + this.entitlements = new LicenseEntitlement() + .setMaxDocs(licenseDescriptor.getMaxDocs()) + .setMaxUsers(licenseDescriptor.getMaxUsers()) + .setClusterEnabled(licenseDescriptor.isClusterEnabled()) + .setCryptodocEnabled(licenseDescriptor.isCryptodocEnabled()); + } + + public Date getIssuedAt() + { + return issuedAt; + } + + public Date getExpiresAt() + { + return expiresAt; + } + + public Integer getRemainingDays() + { + return remainingDays; + } + + public String getHolder() + { + return holder; + } + + public String getMode() + { + return mode; + } + + public LicenseEntitlement getEntitlements() + { + return entitlements; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(250); + sb.append("LicenseInfo [issuedAt=").append(issuedAt) + .append(", expiresAt=").append(expiresAt) + .append(", remainingDays=").append(remainingDays) + .append(", holder=").append(holder) + .append(", mode=").append(mode) + .append(", entitlements=").append(entitlements) + .append(']'); + return sb.toString(); + } + } + + /** + * Representation of the license's entitlement. + * + * @author Jamal Kaabi-Mofrad + */ + public static class LicenseEntitlement + { + private Long maxUsers; + private Long maxDocs; + private boolean isClusterEnabled; + private boolean isCryptodocEnabled; + + public LicenseEntitlement() + { + } + + public Long getMaxUsers() + { + return maxUsers; + } + + public LicenseEntitlement setMaxUsers(Long maxUsers) + { + this.maxUsers = maxUsers; + return this; + } + + public Long getMaxDocs() + { + return maxDocs; + } + + public LicenseEntitlement setMaxDocs(Long maxDocs) + { + this.maxDocs = maxDocs; + return this; + } + + public boolean getIsClusterEnabled() + { + return isClusterEnabled; + } + + public LicenseEntitlement setClusterEnabled(boolean clusterEnabled) + { + isClusterEnabled = clusterEnabled; + return this; + } + + public boolean getIsCryptodocEnabled() + { + return isCryptodocEnabled; + } + + public LicenseEntitlement setCryptodocEnabled(boolean cryptodocEnabled) + { + isCryptodocEnabled = cryptodocEnabled; + return this; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(100); + sb.append("LicenseEntitlement [maxUsers=").append(maxUsers) + .append(", maxDocs=").append(maxDocs) + .append(", isClusterEnabled=").append(isClusterEnabled) + .append(", isCryptodocEnabled=").append(isCryptodocEnabled) + .append(']'); + return sb.toString(); + } + } + + /** + * Representation of the repository status information. + * + * @author Jamal Kaabi-Mofrad + */ + public static class StatusInfo + { + private boolean isReadOnly; + private boolean isAuditEnabled; + private boolean isQuickShareEnabled; + private boolean isThumbnailGenerationEnabled; + + public StatusInfo() + { + } + + public boolean getIsReadOnly() + { + return isReadOnly; + } + + public StatusInfo setReadOnly(boolean readOnly) + { + isReadOnly = readOnly; + return this; + } + + public boolean getIsAuditEnabled() + { + return isAuditEnabled; + } + + public StatusInfo setAuditEnabled(boolean auditEnabled) + { + isAuditEnabled = auditEnabled; + return this; + } + + public boolean getIsQuickShareEnabled() + { + return isQuickShareEnabled; + } + + public StatusInfo setQuickShareEnabled(boolean quickShareEnabled) + { + isQuickShareEnabled = quickShareEnabled; + return this; + } + + public boolean getIsThumbnailGenerationEnabled() + { + return isThumbnailGenerationEnabled; + } + + public StatusInfo setThumbnailGenerationEnabled(boolean isThumbnailGenerationEnabled) + { + this.isThumbnailGenerationEnabled = isThumbnailGenerationEnabled; + return this; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(250); + sb.append("StatusInfo [isReadOnly=").append(isReadOnly) + .append(", isAuditEnabled=").append(isAuditEnabled) + .append(", isQuickShareEnabled=").append(isQuickShareEnabled) + .append(", isThumbnailGenerationEnabled=").append(isThumbnailGenerationEnabled) + .append(']'); + return sb.toString(); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Site.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Site.java new file mode 100644 index 00000000000..4860e26df7f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Site.java @@ -0,0 +1,209 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteVisibility; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a site. + * + * @author steveglover + * + */ +public class Site implements Comparable +{ + protected String id; // site id (aka short name) + protected String guid; // site nodeId + protected String title; + protected String description; + protected SiteVisibility visibility; + protected String preset; + protected String role; + + private Map setFields = new HashMap<>(7); + + public static final String ID = "id"; + public static final String GUID = "guid"; + public static final String TITLE = "title"; + public static final String DESCRIPTION = "description"; + public static final String VISIBILITY = "visibility"; + public static final String PRESET = "preset"; + public static final String ROLE = "role"; + + public Site() + { + } + + public Site(SiteInfo siteInfo, String role) + { + if (siteInfo == null) + { + throw new IllegalArgumentException("Must provide siteInfo"); + } + setId(siteInfo.getShortName()); + setGuid(siteInfo.getNodeRef().getId()); + setTitle(siteInfo.getTitle()); + setDescription(siteInfo.getDescription()); + setVisibility(siteInfo.getVisibility()); + setPreset(siteInfo.getSitePreset()); + setRole(role); + } + + @UniqueId + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + setFields.put(ID, true); + } + + public String getGuid() + { + return guid; + } + + public void setGuid(String guid) + { + this.guid = guid; + setFields.put(GUID, true); + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + setFields.put(TITLE, true); + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + setFields.put(DESCRIPTION, true); + } + + public SiteVisibility getVisibility() + { + return visibility; + } + + public void setVisibility(SiteVisibility visibility) + { + this.visibility = visibility; + setFields.put(VISIBILITY, true); + } + + public String getPreset() + { + return preset; + } + + public void setPreset(String preset) + { + this.preset = preset; + setFields.put(PRESET, true); + } + + public String getRole() + { + return role; + } + + public void setRole(String role) + { + this.role = role; + setFields.put(ROLE, true); + } + + public boolean wasSet(String fieldName) + { + Boolean b = setFields.get(fieldName); + return (b != null ? b : false); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + Site other = (Site) obj; + return id.equals(other.id); + } + + @Override + public int compareTo(Site site) + { + return id.compareTo(site.getId()); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public String toString() + { + return "Site [id=" + id + ", guid=" + guid + ", title=" + title + + ", description=" + description + ", visibility=" + visibility + + "]"; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteContainer.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteContainer.java new file mode 100644 index 00000000000..c3d8874aac9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteContainer.java @@ -0,0 +1,119 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Represents a site container. + * + * @author steveglover + * + */ +public class SiteContainer implements Comparable +{ + private String folderId; + private NodeRef nodeRef; + + public SiteContainer() + { + } + + public SiteContainer(String folderId, NodeRef nodeRef) + { + super(); + if(folderId == null) + { + throw new IllegalArgumentException(); + } + if(nodeRef == null) + { + throw new IllegalArgumentException(); + } + this.folderId = folderId; + this.nodeRef = nodeRef; + } + + public String getFolderId() + { + return folderId; + } + + @JsonProperty("id") + @UniqueId + public NodeRef getNodeRef() + { + return nodeRef; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + + ((folderId == null) ? 0 : folderId.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; + } + + SiteContainer other = (SiteContainer) obj; + return nodeRef.equals(other.getNodeRef()); + } + + @Override + public int compareTo(SiteContainer other) + { + return folderId.compareTo(other.getFolderId()); + } + + @Override + public String toString() + { + return "SiteContainer [folderId=" + folderId + ", nodeRef=" + + nodeRef + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteGroup.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteGroup.java new file mode 100644 index 00000000000..45b33381aa9 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteGroup.java @@ -0,0 +1,143 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.api.groups.GroupsEntityResource; +import org.alfresco.rest.framework.resource.EmbeddedEntityResource; +import org.alfresco.rest.framework.resource.UniqueId; + +public class SiteGroup implements Comparable +{ + private String role; + private String id; // group id (aka authority name) + + public SiteGroup() {} + + public SiteGroup(String id, String role) + { + if (id == null) + { + throw new IllegalArgumentException(); + } + if (role == null) + { + throw new IllegalArgumentException(); + } + this.role = role; + this.id = id; + } + + public static SiteGroup getMemberOfSite(String id, String role) + { + return new SiteGroup(id, role); + } + + @JsonProperty("id") + @UniqueId + @EmbeddedEntityResource(propertyName = "group", entityResource = GroupsEntityResource.class) + public String getId() + { + return id; + } + + public String getRole() + { + return role; + } + + public void setRole(String role) + { + if (role == null) + { + throw new IllegalArgumentException(); + } + this.role = role; + } + + public void setId(String id) + { + if (id == null) + { + throw new IllegalArgumentException(); + } + this.id = id; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((role == null) ? 0 : role.hashCode()); + result = prime * result + ((id == null) ? 0 : id.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; + } + + SiteGroup other = (SiteGroup) obj; + if (role != other.role) + { + return false; + } + + return id.equals(other.id); + } + + @Override + public int compareTo(SiteGroup o) + { + int i = id.compareTo(o.getId()); + if (i == 0) + { + i = role.compareTo(o.getRole()); + } + return i; + } + + @Override + public String toString() + { + return "SiteGroup [role='" + role + '\'' + ", id='" + id + '\'' + ", role='" + role + '\'' + "]"; + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMember.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMember.java new file mode 100644 index 00000000000..f51757cba2b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMember.java @@ -0,0 +1,170 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.api.people.PeopleEntityResource; +import org.alfresco.rest.framework.resource.EmbeddedEntityResource; +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents site membership. + * + * @author steveglover + * + */ +public class SiteMember +{ + private String personId; + private String role; + private boolean isMemberOfGroup; + + public SiteMember() + { + } + + public SiteMember(String personId, String role) + { + super(); + if(personId == null) + { + throw new IllegalArgumentException(); + } + if(role == null) + { + throw new IllegalArgumentException(); + } + this.personId = personId; + this.role = role; + } + + public SiteMember(String personId, String role, boolean isMemberOfGroup) + { + super(); + if(personId == null) + { + throw new IllegalArgumentException(); + } + if(role == null) + { + throw new IllegalArgumentException(); + } + this.personId = personId; + this.role = role; + this.isMemberOfGroup = isMemberOfGroup; + } + + @JsonProperty("id") + @UniqueId + @EmbeddedEntityResource(propertyName = "person", entityResource = PeopleEntityResource.class) + public String getPersonId() + { + return personId; + } + + public String getRole() + { + return role; + } + + public void setRole(String role) + { + if(role == null) + { + throw new IllegalArgumentException(); + } + this.role = role; + } + + public void setPersonId(String personId) + { + if(personId == null) + { + throw new IllegalArgumentException(); + } + this.personId = personId; + } + + public void setIsMemberOfGroup(boolean isMemberOfGroup) + { + this.isMemberOfGroup = isMemberOfGroup; + } + + public boolean getIsMemberOfGroup() + { + return isMemberOfGroup; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + + ((personId == null) ? 0 : personId.hashCode()); + result = prime * result + ((role == null) ? 0 : role.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; + } + + SiteMember other = (SiteMember) obj; + if (!personId.equals(other.personId)) + { + return false; + } + + if (isMemberOfGroup != other.isMemberOfGroup) + { + return false; + } + + return(role == other.role); + } + + @Override + public String toString() + { + return "SiteMember [personId=" + personId + ", isMemberOfGroup=" + isMemberOfGroup + ", role=" + role + "]"; + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipApproval.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipApproval.java new file mode 100644 index 00000000000..02d21c22643 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipApproval.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +public class SiteMembershipApproval +{ + + private String role; + + public String getRole() + { + return role; + } + + public void setRole(String role) + { + this.role = role; + } + + @Override + public String toString() + { + return "SiteMembershipApproval [role=" + role + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipRejection.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipRejection.java new file mode 100644 index 00000000000..447fb6afcd5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipRejection.java @@ -0,0 +1,49 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +public class SiteMembershipRejection +{ + + private String comment; + + public String getComment() + { + return comment; + } + + public void setComment(String comment) + { + this.comment = comment; + } + + @Override + public String toString() + { + return "SiteMembershipRejection [comment=" + comment + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipRequest.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipRequest.java new file mode 100644 index 00000000000..e946016e0d2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteMembershipRequest.java @@ -0,0 +1,195 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.text.Collator; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.alfresco.rest.api.sites.SiteEntityResource; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.EmbeddedEntityResource; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.util.Pair; + +/** + * Representation of a site membership request for a specific user. + * + * Ordering is by id (site id). + * + * @author steveglover + * + */ +public class SiteMembershipRequest implements Comparable +{ + private static Collator collator = Collator.getInstance(); + + private String id; // site id + private String message; + private Date createdAt; + private Date modifiedAt; + private String title; // for sorting only + private Person person; + + public static Pair splitId(String id) + { + int idx = id.indexOf(":"); + if(idx != -1) + { + String workflowId = id.substring(0, idx); + String key = id.substring(idx + 1); + Pair ret = new Pair(workflowId, key); + return ret; + } + else + { + throw new InvalidArgumentException("Site invite id is invalid: " + id); + } + } + + public SiteMembershipRequest() + { + } + + @EmbeddedEntityResource(propertyName = "site", entityResource = SiteEntityResource.class) + @UniqueId + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public void setTitle(String title) + { + this.title = title; + } + + @JsonIgnore + public String getTitle() + { + return title; + } + + public Date getCreatedAt() + { + return createdAt; + } + + public void setCreatedAt(Date createdAt) + { + this.createdAt = createdAt; + } + + public Date getModifiedAt() + { + return modifiedAt; + } + + public void setModifiedAt(Date modifiedAt) + { + this.modifiedAt = modifiedAt; + } + + public String getMessage() + { + return message; + } + + public void setMessage(String message) + { + this.message = message; + } + + public Person getPerson() + { + return person; + } + + public void setPerson(Person person) + { + this.person = person; + } + + @Override + public String toString() + { + return "SiteMembershipRequest [id=" + id + ", message=" + message + ", createdAt=" + createdAt + + ", modifiedAt=" + modifiedAt + "]"; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.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; + SiteMembershipRequest other = (SiteMembershipRequest) obj; + if (id == null) + { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public int compareTo(SiteMembershipRequest o) + { + int ret = 0; + + if(title == null && o.getTitle() != null) + { + ret = -1; + } + else if(title != null && o.getTitle() == null) + { + ret = 1; + } + else + { + ret = collator.compare(title, o.getTitle()); + } + + return ret; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteTarget.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteTarget.java new file mode 100644 index 00000000000..74a234ef9a4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteTarget.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + + +/** + * A site target favourite. + * + * @author steveglover + * + */ +public class SiteTarget extends Target +{ + private Site site; + + public SiteTarget() + { + super(); + } + + public SiteTarget(Site site) + { + super(); + this.site = site; + } + + public void setSite(Site site) + { + this.site = site; + } + + public Site getSite() + { + return site; + } + + @Override + public String toString() + { + return "SiteTarget [site=" + site + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/SiteUpdate.java b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteUpdate.java new file mode 100644 index 00000000000..1d5a48e6d67 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/SiteUpdate.java @@ -0,0 +1,135 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import org.alfresco.service.cmr.site.SiteVisibility; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * Class representing a site update API operation. + * + * @author Matt Ward + * @since 5.2 + */ +public class SiteUpdate implements Serializable +{ + private static final long serialVersionUID = 1L; + private String title; + private String description; + private SiteVisibility visibility; + + private Map setFields = new HashMap<>(3); + + public static final String TITLE = "title"; + public static final String DESCRIPTION = "description"; + public static final String VISIBILITY = "visibility"; + + public SiteUpdate() + { + } + + public SiteUpdate(String title, String description, SiteVisibility visibility) + { + setTitle(title); + setDescription(description); + setVisibility(visibility); + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + setFields.put(TITLE, true); + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + setFields.put(DESCRIPTION, true); + } + + public SiteVisibility getVisibility() + { + return visibility; + } + + public void setVisibility(SiteVisibility visibility) + { + this.visibility = visibility; + setFields.put(VISIBILITY, true); + } + + public boolean wasSet(String fieldName) + { + Boolean b = setFields.get(fieldName); + return (b != null ? b : false); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SiteUpdate that = (SiteUpdate) o; + + if (title != null ? !title.equals(that.title) : that.title != null) return false; + if (description != null ? !description.equals(that.description) : that.description != null) return false; + return visibility == that.visibility; + + } + + @Override + public int hashCode() + { + int result = title != null ? title.hashCode() : 0; + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (visibility != null ? visibility.hashCode() : 0); + return result; + } + + @Override + public String toString() + { + return "SiteUpdate{" + + "title='" + title + '\'' + + ", description='" + description + '\'' + + ", visibility=" + visibility + + '}'; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Tag.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Tag.java new file mode 100644 index 00000000000..1fb4c16088b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Tag.java @@ -0,0 +1,140 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Represents a node tag. + * + * @author steveglover + * + */ +public class Tag implements Comparable +{ + private NodeRef nodeRef; + private String tag; + private Integer count; + + public Tag() + { + } + + public Tag(NodeRef nodeRef, String tag) + { + this.nodeRef = nodeRef; + this.tag = tag; + } + + @JsonProperty("id") + @UniqueId + public NodeRef getNodeRef() + { + return nodeRef; + } + + public void setNodeRef(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + + public String getTag() + { + return tag; + } + + public void setTag(String tag) + { + this.tag = tag; + } + + public Integer getCount() + { + + return count; + } + + public void setCount(Integer count) + { + this.count = count; + } + + /* + * Note that comparison of tags is based on their string value. This should still + * be consistent with equals since tags that are equal implies NodeRefs that are equal. + * + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(Tag o) + { + int ret = getTag().compareTo(o.getTag()); + return ret; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((nodeRef == null) ? 0 : nodeRef.hashCode()); + return result; + } + + /* + * Tags are equal if they have the same NodeRef + * + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Tag other = (Tag) obj; + if (nodeRef == null) { + if (other.nodeRef != null) + return false; + } else if (!nodeRef.equals(other.nodeRef)) + return false; + return true; + } + + @Override + public String toString() + { + return "Tag [nodeRef=" + nodeRef + ", tag=" + tag + "]"; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/Target.java b/remote-api/src/main/java/org/alfresco/rest/api/model/Target.java new file mode 100644 index 00000000000..d01bd6dc9da --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/Target.java @@ -0,0 +1,36 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * A favourite target entity. + * + * @author steveglover + * + */ +public abstract class Target +{ +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/UserInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/UserInfo.java new file mode 100644 index 00000000000..b17c529f664 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/UserInfo.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import java.io.Serializable; + +/** + * Representation of a user info + * + * @author janv + */ +public class UserInfo implements Serializable +{ + private String id; + private String displayName; + + public UserInfo() + { + } + + public UserInfo(String id, String firstName, String lastName) + { + this.id = id; + this.displayName = ((firstName != null ? firstName + " " : "") + (lastName != null ? lastName : "")).trim(); + } + + public String getDisplayName() + { + return displayName; + } + + public String getId() + { + return id; + } + + @Override + public String toString() + { + return "User [id=" + id + ", displayName=" + displayName + "]"; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/UserRating.java b/remote-api/src/main/java/org/alfresco/rest/api/model/UserRating.java new file mode 100644 index 00000000000..c569bd596c2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/UserRating.java @@ -0,0 +1,69 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.rest.framework.resource.UniqueId; + +/** + * Represents a user rating for a node. + * + * @author steveglover + * + */ +public class UserRating +{ + private Float userRating; + private String personId; + + public UserRating(String personId, Float userRating) + { + super(); + this.userRating = userRating; + this.personId = personId; + } + + public Float getUserRating() + { + return userRating; + } + + @JsonProperty("id") + @UniqueId + public String getPersonId() + { + return personId; + } + + @Override + public String toString() + { + return "UserRating [userRating=" + userRating + ", personId=" + + personId + "]"; + } + + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/VersionOptions.java b/remote-api/src/main/java/org/alfresco/rest/api/model/VersionOptions.java new file mode 100644 index 00000000000..c81846d2d17 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/VersionOptions.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.model; + +/** + * Version Options + * + * Initially, used by version revert. Should also be consistent with version options used with Node Create & Update. + * + * @author janv + */ +public class VersionOptions +{ + private Boolean majorVersion; + private String comment; + + public VersionOptions() + { + } + + public String getComment() + { + return comment; + } + + public void setComment(String comment) + { + this.comment = comment; + } + + public Boolean getMajorVersion() + { + return majorVersion; + } + + public void setMajorVersion(Boolean majorVersion) + { + this.majorVersion = majorVersion; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/modules/ModulePackagesEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/modules/ModulePackagesEntityResource.java new file mode 100644 index 00000000000..9bfdd99d439 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/modules/ModulePackagesEntityResource.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.modules; + +import org.alfresco.rest.api.model.CustomModel; +import org.alfresco.rest.api.model.ModulePackage; +import org.alfresco.rest.api.model.Site; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.service.cmr.module.ModuleService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Returns Alfresco Module Information. + */ +@EntityResource(name="modulepackages", title = "Installed Modules Packages") +public class ModulePackagesEntityResource implements EntityResourceAction.Read, + EntityResourceAction.ReadById +{ + @Autowired + ModuleService moduleService; + + @Override + @WebApiDescription(title="Returns ModulePackage information for the given module.") + public ModulePackage readById(String modelName, Parameters parameters) throws EntityNotFoundException + { + ModuleDetails moduleDetails = moduleService.getModule(modelName); + if(moduleDetails == null) + { + // module does not exist + throw new EntityNotFoundException(modelName); + } + return ModulePackage.fromModuleDetails(moduleDetails); + } + + @Override + @WebApiDescription(title="Returns a paged list of all Modules.") + public CollectionWithPagingInfo readAll(Parameters parameters) + { + List details = moduleService.getAllModules(); + if (details!= null && details.size()>0) + { + List packages = new ArrayList<>(details.size()); + for (ModuleDetails detail : details) + { + packages.add(ModulePackage.fromModuleDetails(detail)); + } + return CollectionWithPagingInfo.asPaged(parameters.getPaging(), packages); + } + + return CollectionWithPagingInfo.asPaged(parameters.getPaging(), Collections.EMPTY_LIST); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/modules/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/modules/package-info.java new file mode 100644 index 00000000000..7973a5f90eb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/modules/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PRIVATE, version=1) +package org.alfresco.rest.api.modules; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; diff --git a/remote-api/src/main/java/org/alfresco/rest/api/networks/NetworksEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/networks/NetworksEntityResource.java new file mode 100644 index 00000000000..6110b17eaf4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/networks/NetworksEntityResource.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.networks; + +import org.alfresco.rest.api.Networks; +import org.alfresco.rest.api.model.Network; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +@EntityResource(name="networks", title = "Networks entity") +public class NetworksEntityResource implements EntityResourceAction.ReadById, InitializingBean +{ + public static final String NAME = "networks"; + + private Networks networks; + + public void setNetworks(Networks networks) + { + this.networks = networks; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("networks", this.networks); + } + + @Override + @WebApiDescription(title = "Get Network Information", description = "Get information for the network with id 'networkId'") + @WebApiParam(name = "networkId", title = "The network name") + public Network readById(final String networkId, Parameters parameters) + { + return networks.getNetwork(networkId); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/networks/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/networks/package-info.java new file mode 100644 index 00000000000..927d87bcbaf --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/networks/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.networks; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/AbstractNodeRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/AbstractNodeRelation.java new file mode 100644 index 00000000000..e8d08b7ead5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/AbstractNodeRelation.java @@ -0,0 +1,230 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Assoc; +import org.alfresco.rest.api.model.AssocChild; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author janv + */ +public class AbstractNodeRelation implements InitializingBean +{ + // excluded namespaces (assoc types) + protected static final List EXCLUDED_NS = Arrays.asList(NamespaceService.SYSTEM_MODEL_1_0_URI); + + private final static Set WHERE_PARAMS_ASSOC_TYPE = + new HashSet<>(Arrays.asList(new String[] {Nodes.PARAM_ASSOC_TYPE})); + + protected ServiceRegistry sr; + protected NodeService nodeService; + protected NamespaceService namespaceService; + protected DictionaryService dictionaryService; + protected Nodes nodes; + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setServiceRegistry(ServiceRegistry sr) + { + this.sr = sr; + } + + @Override + public void afterPropertiesSet() + { + PropertyCheck.mandatory(this, "serviceRegistry", sr); + ParameterCheck.mandatory("nodes", this.nodes); + + this.nodeService = sr.getNodeService(); + this.namespaceService = sr.getNamespaceService(); + this.dictionaryService = sr.getDictionaryService(); + } + + protected QNamePattern getAssocTypeFromWhereElseAll(Parameters parameters) + { + QNamePattern assocTypeQNamePattern = RegexQNamePattern.MATCH_ALL; + + Query q = parameters.getQuery(); + if (q != null) + { + MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(WHERE_PARAMS_ASSOC_TYPE, null); + QueryHelper.walk(q, propertyWalker); + + String assocTypeQNameStr = propertyWalker.getProperty(Nodes.PARAM_ASSOC_TYPE, WhereClauseParser.EQUALS, String.class); + if (assocTypeQNameStr != null) + { + assocTypeQNamePattern = nodes.getAssocType(assocTypeQNameStr); + } + } + + return assocTypeQNamePattern; + } + + protected CollectionWithPagingInfo listNodePeerAssocs(List assocRefs, Parameters parameters, boolean returnTarget) + { + Map qnameMap = new HashMap<>(3); + + Map mapUserInfo = new HashMap<>(10); + + List includeParam = parameters.getInclude(); + + List collection = new ArrayList(assocRefs.size()); + for (AssociationRef assocRef : assocRefs) + { + // minimal info by default (unless "include"d otherwise) + NodeRef nodeRef = (returnTarget ? assocRef.getTargetRef() : assocRef.getSourceRef()); + + Node node = nodes.getFolderOrDocument(nodeRef, null, null, includeParam, mapUserInfo); + + QName assocTypeQName = assocRef.getTypeQName(); + + if (! EXCLUDED_NS.contains(assocTypeQName.getNamespaceURI())) + { + String assocType = qnameMap.get(assocTypeQName); + if (assocType == null) + { + assocType = assocTypeQName.toPrefixString(namespaceService); + qnameMap.put(assocTypeQName, assocType); + } + + node.setAssociation(new Assoc(assocType)); + + collection.add(node); + } + } + + return listPage(collection, parameters.getPaging()); + } + + protected CollectionWithPagingInfo listNodeChildAssocs(List childAssocRefs, Parameters parameters, Boolean isPrimary, boolean returnChild) + { + Map qnameMap = new HashMap<>(3); + + Map mapUserInfo = new HashMap<>(10); + + List includeParam = parameters.getInclude(); + + List collection = new ArrayList(childAssocRefs.size()); + for (ChildAssociationRef childAssocRef : childAssocRefs) + { + if (isPrimary == null || (isPrimary == childAssocRef.isPrimary())) + { + // minimal info by default (unless "include"d otherwise) + NodeRef nodeRef = (returnChild ? childAssocRef.getChildRef() : childAssocRef.getParentRef()); + + Node node = nodes.getFolderOrDocument(nodeRef, null, null, includeParam, mapUserInfo); + + QName assocTypeQName = childAssocRef.getTypeQName(); + + if (!EXCLUDED_NS.contains(assocTypeQName.getNamespaceURI())) + { + String assocType = qnameMap.get(assocTypeQName); + if (assocType == null) + { + assocType = assocTypeQName.toPrefixString(namespaceService); + qnameMap.put(assocTypeQName, assocType); + } + + node.setAssociation(new AssocChild(assocType, childAssocRef.isPrimary())); + + collection.add(node); + } + } + } + + return listPage(collection, parameters.getPaging()); + } + + protected CollectionWithPagingInfo listPage(List result, Paging paging) + { + // return 'page' of results (note: depends on in-built/natural sort order of results) + int skipCount = paging.getSkipCount(); + int pageSize = paging.getMaxItems(); + int pageEnd = skipCount + pageSize; + + final List page = new ArrayList<>(pageSize); + if (result == null) + { + result = Collections.emptyList(); + } + + Iterator it = result.iterator(); + for (int counter = 0; counter < pageEnd && it.hasNext(); counter++) + { + Object element = it.next(); + if (counter < skipCount) + { + continue; + } + if (counter > pageEnd - 1) + { + break; + } + page.add(element); + } + + int totalCount = result.size(); + boolean hasMoreItems = ((skipCount + page.size()) < totalCount); + + return CollectionWithPagingInfo.asPaged(paging, page, hasMoreItems, totalCount); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeActionDefinitionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeActionDefinitionsRelation.java new file mode 100644 index 00000000000..1c0d6db6809 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeActionDefinitionsRelation.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.ParameterCheck; + +import java.util.List; +import java.util.stream.Collectors; + +@RelationshipResource(name = "action-definitions", entityResource = NodesEntityResource.class, title = "Node action definitions") +public class NodeActionDefinitionsRelation extends AbstractNodeRelation + implements RelationshipResourceAction.Read +{ + private Actions actions; + + @Override + public void afterPropertiesSet() + { + super.afterPropertiesSet(); + ParameterCheck.mandatory("actions", actions); + } + + public void setActions(Actions actions) + { + this.actions = actions; + } + + @Override + public CollectionWithPagingInfo readAll(String entityResourceId, Parameters params) + { + NodeRef parentNodeRef = nodes.validateOrLookupNode(entityResourceId, null); + return actions.getActionDefinitions(parentNodeRef, params); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeAuditEntriesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeAuditEntriesRelation.java new file mode 100644 index 00000000000..13a0a078943 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeAuditEntriesRelation.java @@ -0,0 +1,73 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.Audit; +import org.alfresco.rest.api.model.AuditEntry; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartRelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Node AuditEntries + * + * -list audit-entries + * + * @author anechifor + * + */ +@RelationshipResource(name = "audit-entries", entityResource = NodesEntityResource.class, title = "Audit Entries") +public class NodeAuditEntriesRelation implements RelationshipResourceAction.Read, InitializingBean +{ + + private Audit audit; + + public void setAudit(Audit audit) + { + this.audit = audit; + } + + @Override + public void afterPropertiesSet() throws Exception + { + ParameterCheck.mandatory("audit", this.audit); + + } + + @WebApiDescription(title = "Returns audit entries for node id") + @Override + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + return audit.listAuditEntriesByNodeId(nodeId, parameters); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java new file mode 100644 index 00000000000..25f5128298d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java @@ -0,0 +1,131 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartRelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.servlet.FormData; + +/** + * Node Children + * + * - list folder children + * - create folder &/or empty file + * - create (ie. upload) file with content + * + * @author janv + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "children", entityResource = NodesEntityResource.class, title = "Folder children") +public class NodeChildrenRelation implements + RelationshipResourceAction.Read, + RelationshipResourceAction.Create, + MultiPartRelationshipResourceAction.Create, InitializingBean +{ + private Nodes nodes; + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("nodes", this.nodes); + } + + /** + * List folder children - returns a filtered/sorted/paged list of nodes that are immediate children of the parent folder + * + * @param parentFolderNodeId String id of parent folder - will also accept well-known alias, eg. -root- or -my- or -shared- + * + * Optional query parameters: + * + * - include + * - fields + * - where + * - orderBy + * - skipCount + * - maxItems + * - ... etc + * + * Please refer to OpenAPI spec for more details ! + * + * If parentFolderNodeId does not exist, EntityNotFoundException (status 404). + * If parentFolderNodeId does not represent a folder, InvalidArgumentException (status 400). + */ + @Override + @WebApiDescription(title = "Return a paged list of nodes for the document/folder identified by parentFolderNodeId") + public CollectionWithPagingInfo readAll(String parentFolderNodeId, Parameters parameters) + { + return nodes.listChildren(parentFolderNodeId, parameters); + } + + /** + * Create one or more nodes (folder or empty file) below parent folder. + * + * Note: for parent folder nodeId, can also use well-known alias, eg. -root- or -my- or -shared- + * + * If parentFolderNodeId does not exist, EntityNotFoundException (status 404). + * If parentFolderNodeId does not represent a folder, InvalidArgumentException (status 400). + */ + @Override + @WebApiDescription(title="Create one (or more) nodes as children of folder identified by parentFolderNodeId") + public List create(String parentFolderNodeId, List nodeInfos, Parameters parameters) + { + List result = new ArrayList<>(nodeInfos.size()); + + for (Node nodeInfo : nodeInfos) + { + result.add(nodes.createNode(parentFolderNodeId, nodeInfo, parameters)); + } + + return result; + } + + @Override + @WebApiDescription(title = "Upload file content and meta-data into the repository.") + @WebApiParam(name = "formData", title = "A single form data", description = "A single form data which holds FormFields.") + public Node create(String parentFolderNodeId, FormData formData, Parameters parameters, WithResponse withResponse) + { + return nodes.upload(parentFolderNodeId, formData, parameters); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeCommentsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeCommentsRelation.java new file mode 100644 index 00000000000..923032fb2a1 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeCommentsRelation.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.Comments; +import org.alfresco.rest.api.model.Comment; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.WebApiParameters; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author Gethin James + * @author Steve Glover + */ +@RelationshipResource(name = "comments", entityResource = NodesEntityResource.class, title = "Document or folder comments") +public class NodeCommentsRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create, RelationshipResourceAction.Update, + RelationshipResourceAction.Delete, InitializingBean +{ + private Comments comments; + + public void setComments(Comments comments) + { + this.comments = comments; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("comments", this.comments); + } + + /** + * Create a comment for the node given by nodeId. + * + * THOR-1153: "F314: Add a comment to a folder or document" + * + */ + @Override + @WebApiDescription(title="Creates comments for the node 'nodeId'.") + public List create(String nodeId, List entity, Parameters parameters) + { + List result = new ArrayList(entity.size()); + for (Comment comment : entity) + { + result.add(comments.createComment(nodeId, comment)); + } + return result; + } + + /** + * + * Returns a paged list of comments for the document/folder identified by nodeId, sorted chronologically with the newest first. + * + * THOR-1152: “F313: For a folder or document, get the list of associated comments” + * + * If nodeId does not exist, EntityNotFoundException (status 404). + * If nodeId does not represent a document or folder, InvalidArgumentException (status 400). + */ + @Override + @WebApiDescription(title = "Returns a paged list of comments for the document/folder identified by nodeId, sorted chronologically with the newest first.") + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + return comments.getComments(nodeId, parameters.getPaging(), parameters.getInclude()); + } + + @Override + @WebApiDescription(title = "Updates the comment with the given id.") + public Comment update(String nodeId, Comment entity, Parameters parameters) + { + return comments.updateComment(nodeId, entity); + } + + @Override + @WebApiDescription(title = "Delete the comment with the given commentNodeId.") + @WebApiParameters({ + @WebApiParam(name="nodeId", title="The unique id of the parent Node being addressed", description="A single node id"), + @WebApiParam(name="commentNodeId", title="The unique id of the comment Node being addressed", description="A single node id")}) + public void delete(String nodeId, String commentNodeId, Parameters parameters) + { + comments.deleteComment(nodeId, commentNodeId); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeParentsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeParentsRelation.java new file mode 100644 index 00000000000..d2440a2266d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeParentsRelation.java @@ -0,0 +1,103 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.antlr.WhereClauseParser; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.resource.parameters.where.Query; +import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; +import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Node Parents + * + * @author janv + */ +@RelationshipResource(name = "parents", entityResource = NodesEntityResource.class, title = "Node Parents") +public class NodeParentsRelation extends AbstractNodeRelation implements RelationshipResourceAction.Read +{ + private final static Set WHERE_PARAMS_PARENTS = + new HashSet<>(Arrays.asList(new String[] {Nodes.PARAM_ASSOC_TYPE, Nodes.PARAM_ISPRIMARY})); + + /** + * List child node's parent(s) based on (parent ->) child associations. + * Returns primary parent & also secondary parents, if any. + * + * @param childNodeId String id of child node + */ + @Override + @WebApiDescription(title = "Return a list of parent nodes based on child assocs") + public CollectionWithPagingInfo readAll(String childNodeId, Parameters parameters) + { + NodeRef childNodeRef = nodes.validateOrLookupNode(childNodeId, null); + + QNamePattern assocTypeQNameParam = RegexQNamePattern.MATCH_ALL; + + Boolean isPrimary = null; + + Query q = parameters.getQuery(); + if (q != null) + { + MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(WHERE_PARAMS_PARENTS, null); + QueryHelper.walk(q, propertyWalker); + + isPrimary = propertyWalker.getProperty(Nodes.PARAM_ISPRIMARY, WhereClauseParser.EQUALS, Boolean.class); + + String assocTypeQNameStr = propertyWalker.getProperty(Nodes.PARAM_ASSOC_TYPE, WhereClauseParser.EQUALS, String.class); + if (assocTypeQNameStr != null) + { + assocTypeQNameParam = nodes.getAssocType(assocTypeQNameStr); + } + } + + List childAssocRefs = null; + if (assocTypeQNameParam.equals(RegexQNamePattern.MATCH_ALL)) + { + childAssocRefs = nodeService.getParentAssocs(childNodeRef); + } + else + { + childAssocRefs = nodeService.getParentAssocs(childNodeRef, assocTypeQNameParam, RegexQNamePattern.MATCH_ALL); + } + + return listNodeChildAssocs(childAssocRefs, parameters, isPrimary, false); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRatingsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRatingsRelation.java new file mode 100644 index 00000000000..3381d6d7d29 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRatingsRelation.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.rest.api.NodeRatings; +import org.alfresco.rest.api.model.NodeRating; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.WebApiParameters; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +@RelationshipResource(name = "ratings", entityResource = NodesEntityResource.class, title = "Document or folder ratings") +public class NodeRatingsRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, RelationshipResourceAction.Delete, +RelationshipResourceAction.Create, InitializingBean +{ + private NodeRatings nodeRatings; + + public void setNodeRatings(NodeRatings nodeRatings) + { + this.nodeRatings = nodeRatings; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("nodeRatings", this.nodeRatings); + } + + @Override + @WebApiDescription(title="A paged list of ratings for node 'nodeId'.") + @WebApiParam(name="nodeId", title="The unique id of the Node being addressed", description="A single node id") + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + return nodeRatings.getNodeRatings(nodeId, parameters.getPaging()); + } + + /** + * Create a rating for the node with id 'nodeId'. + * + */ + @Override + @WebApiDescription(title="Rate a node for 'nodeId'.") + @WebApiParam(name="ratingEntity", title="A single rating", description="A single node rating, multiple ratings are not supported.", + kind=ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple=false, required = true) + public List create(String nodeId, List ratingEntity, Parameters parameters) + { + //There will always be 1 value because allowMultiple=false + NodeRating rating = ratingEntity.get(0); + String ratingSchemeId = rating.getScheme(); + nodeRatings.addRating(nodeId, ratingSchemeId, rating.getMyRating()); + return Collections.singletonList(nodeRatings.getNodeRating(nodeId, ratingSchemeId)); + } + + /** + * Returns the rating with id 'schemeName' for node with id 'nodeId'. + * + */ + @Override + @WebApiDescription(title="Get the rating with id 'ratingSchemeId' for node 'nodeId'.") + @WebApiParameters({ + @WebApiParam(name="nodeId", title="The unique id of the Node being addressed", description="A single node id"), + @WebApiParam(name="ratingSchemeId", title="The rating scheme type", description="Possible values are likesRatingScheme.")}) + public NodeRating readById(String nodeId, String ratingSchemeId, Parameters parameters) + { + return nodeRatings.getNodeRating(nodeId, ratingSchemeId); + } + + @Override + @WebApiDescription(title="Deletes a node rating") + public void delete(String nodeId, String ratingSchemeId, Parameters parameters) + { + nodeRatings.removeRating(nodeId, ratingSchemeId); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java new file mode 100644 index 00000000000..d406fdfff84 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRenditionsRelation.java @@ -0,0 +1,121 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.Status; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * Node renditions. + * + * @author Jamal Kaabi-Mofrad + */ +@RelationshipResource(name = "renditions", entityResource = NodesEntityResource.class, title = "Node renditions") +public class NodeRenditionsRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Create, + RelationshipResourceBinaryAction.Read, + InitializingBean +{ + + private Renditions renditions; + + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "renditions", renditions); + } + + @Override + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + return renditions.getRenditions(nodeRef, parameters); + } + + @Override + public Rendition readById(String nodeId, String renditionId, Parameters parameters) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + return renditions.getRendition(nodeRef, renditionId, parameters); + } + + @WebApiDescription(title = "Create rendition", successStatus = Status.STATUS_ACCEPTED) + @Override + public List create(String nodeId, List entity, Parameters parameters) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + renditions.createRenditions(nodeRef, entity, parameters); + return null; + } + + @WebApiDescription(title = "Download rendition", description = "Download rendition") + @BinaryProperties({ "content" }) + @Override + public BinaryResource readProperty(String nodeId, String renditionId, Parameters parameters) + { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + return renditions.getContent(nodeRef, renditionId, parameters); + } + + @Operation("request-content-url") + @WebApiParam(name = "directAccessUrlRequest", title = "Direct access url request", description = "Direct access url request", kind = ResourceParameter.KIND.HTTP_BODY_OBJECT) + @WebApiDescription(title = "Request content url", + description="Generates a direct access url.", + successStatus = HttpServletResponse.SC_OK) + public DirectAccessUrl requestContentUrl(String nodeId, String renditionId, DirectAccessUrlRequest directAccessUrlRequest, Parameters parameters, WithResponse withResponse) + { + return renditions.requestContentUrl(nodeId, null, renditionId, directAccessUrlRequest); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeSecondaryChildrenRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeSecondaryChildrenRelation.java new file mode 100644 index 00000000000..aa17a20561c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeSecondaryChildrenRelation.java @@ -0,0 +1,154 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.AssocChild; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; + +import java.util.List; + +/** + * Node Secondary Children + * + * Manage secondary child associations (sometimes also known as multi-filing) + * + * Please note, if you wish to manage primary child associations then please refer to other endpoints, for example: + * + * - to create primary child, use POST /nodes/{parentId}/children + * - to delete node (ie. primary child), use DELETE /nodes/{nodeId} + * - to move a node from one primary parent to another, use POST /nodes/{nodeId}/copy + * + * @author janv + */ +@RelationshipResource(name = "secondary-children", entityResource = NodesEntityResource.class, title = "Node Secondary Children") +public class NodeSecondaryChildrenRelation extends AbstractNodeRelation implements + RelationshipResourceAction.Read, + RelationshipResourceAction.Create, + RelationshipResourceAction.Delete +{ + /** + * List secondary children only + * + * @param parentNodeId String id of parent node + */ + @Override + @WebApiDescription(title = "Return a paged list of secondary child nodes based on child assocs") + public CollectionWithPagingInfo readAll(String parentNodeId, Parameters parameters) + { + NodeRef parentNodeRef = nodes.validateOrLookupNode(parentNodeId, null); + + QNamePattern assocTypeQNameParam = getAssocTypeFromWhereElseAll(parameters); + + List childAssocRefs = null; + if (assocTypeQNameParam.equals(RegexQNamePattern.MATCH_ALL)) + { + childAssocRefs = nodeService.getChildAssocs(parentNodeRef); + } + else + { + childAssocRefs = nodeService.getChildAssocs(parentNodeRef, assocTypeQNameParam, RegexQNamePattern.MATCH_ALL); + } + + return listNodeChildAssocs(childAssocRefs, parameters, false, true); + } + + @Override + @WebApiDescription(title="Add secondary child assoc") + public List create(String parentNodeId, List entities, Parameters parameters) + { + return nodes.addChildren(parentNodeId, entities); + } + + @Override + @WebApiDescription(title = "Remove secondary child assoc(s)") + public void delete(String parentNodeId, String childNodeId, Parameters parameters) + { + NodeRef parentNodeRef = nodes.validateNode(parentNodeId); + NodeRef childNodeRef = nodes.validateNode(childNodeId); + + String assocTypeStr = parameters.getParameter(Nodes.PARAM_ASSOC_TYPE); + QName assocTypeQName = nodes.getAssocType(assocTypeStr, false); + + List assocRefs = nodeService.getChildAssocs(parentNodeRef); + + boolean found = false; + + for (ChildAssociationRef assocRef : assocRefs) + { + if (! assocRef.getChildRef().equals(childNodeRef)) + { + continue; + } + + if (assocTypeQName != null) + { + if (assocTypeQName.equals(assocRef.getTypeQName())) + { + if (assocRef.isPrimary()) + { + throw new InvalidArgumentException("Cannot use secondary-children to delete primary assoc: " + +parentNodeId+","+assocTypeStr+","+childNodeId); + } + + boolean existed = nodeService.removeSecondaryChildAssociation(assocRef); + if (existed) + { + found = true; + } + } + } + else + { + if (! assocRef.isPrimary()) + { + boolean existed = nodeService.removeSecondaryChildAssociation(assocRef); + if (existed) + { + found = true; + } + } + } + } + + if (! found) + { + throw new EntityNotFoundException(parentNodeId+","+assocTypeStr+","+childNodeId); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeSourcesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeSourcesRelation.java new file mode 100644 index 00000000000..743716fef3d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeSourcesRelation.java @@ -0,0 +1,65 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QNamePattern; + +import java.util.List; + +/** + * Node Sources - list node (peer) associations from target to sources + * + * @author janv + */ +@RelationshipResource(name = "sources", entityResource = NodesEntityResource.class, title = "Node Sources") +public class NodeSourcesRelation extends AbstractNodeRelation implements RelationshipResourceAction.Read +{ + /** + * List sources + * + * @param targetNodeId String id of target node + */ + @Override + @WebApiDescription(title = "Return a paged list of sources nodes based on (peer) assocs") + public CollectionWithPagingInfo readAll(String targetNodeId, Parameters parameters) + { + NodeRef targetNodeRef = nodes.validateOrLookupNode(targetNodeId, null); + + QNamePattern assocTypeQNameParam = getAssocTypeFromWhereElseAll(parameters); + + List assocRefs = nodeService.getSourceAssocs(targetNodeRef, assocTypeQNameParam); + + return listNodePeerAssocs(assocRefs, parameters, false); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeTagsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeTagsRelation.java new file mode 100644 index 00000000000..9e9d9ea3c6e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeTagsRelation.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import java.util.List; + +import org.alfresco.rest.api.Tags; +import org.alfresco.rest.api.model.Tag; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +@RelationshipResource(name = "tags", entityResource = NodesEntityResource.class, title = "Document or folder tags") +public class NodeTagsRelation implements RelationshipResourceAction.Create, RelationshipResourceAction.Delete, RelationshipResourceAction.Read, InitializingBean +{ + private Tags tags; + + public void setTags(Tags tags) + { + this.tags = tags; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("tags", this.tags); + } + + /** + * Add the tag to the node with id 'nodeId'. + * + */ + @Override + @WebApiDescription(title="Adds one or more tags to the node with id 'nodeId'.") + public List create(String nodeId, List tagsToCreate, Parameters parameters) + { + return tags.addTags(nodeId, tagsToCreate); + } + + @Override + @WebApiDescription(title="Remove the tag from the node with id 'nodeId'.") + public void delete(String nodeId, String tagId, Parameters parameters) + { + tags.deleteTag(nodeId, tagId); + } + + @Override + @WebApiDescription(title="A paged list of tags on the node 'nodeId'.") + public CollectionWithPagingInfo readAll(String nodeId, Parameters params) + { + return tags.getTags(nodeId, params); + } + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeTargetsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeTargetsRelation.java new file mode 100644 index 00000000000..f4ac2c04fee --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeTargetsRelation.java @@ -0,0 +1,115 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.AssocTarget; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.service.namespace.RegexQNamePattern; + +import java.util.List; + +/** + * Node Targets + * + * - list node (peer) associations - from source to target + * + * @author janv + */ +@RelationshipResource(name = "targets", entityResource = NodesEntityResource.class, title = "Node Targets") +public class NodeTargetsRelation extends AbstractNodeRelation implements + RelationshipResourceAction.Read, + RelationshipResourceAction.Create, + RelationshipResourceAction.Delete +{ + /** + * List targets + * + * @param sourceNodeId String id of source node + */ + @Override + @WebApiDescription(title = "Return a paged list of target nodes based on (peer) assocs") + public CollectionWithPagingInfo readAll(String sourceNodeId, Parameters parameters) + { + NodeRef sourceNodeRef = nodes.validateOrLookupNode(sourceNodeId, null); + + QNamePattern assocTypeQNameParam = getAssocTypeFromWhereElseAll(parameters); + + List assocRefs = nodeService.getTargetAssocs(sourceNodeRef, assocTypeQNameParam); + + return listNodePeerAssocs(assocRefs, parameters, true); + } + + @Override + @WebApiDescription(title="Add node assoc") + public List create(String sourceNodeId, List entities, Parameters parameters) + { + return nodes.addTargets(sourceNodeId, entities); + } + + @Override + @WebApiDescription(title = "Remove node assoc(s)") + public void delete(String sourceNodeId, String targetNodeId, Parameters parameters) + { + NodeRef srcNodeRef = nodes.validateNode(sourceNodeId); + NodeRef tgtNodeRef = nodes.validateNode(targetNodeId); + + String assocTypeStr = parameters.getParameter(Nodes.PARAM_ASSOC_TYPE); + QNamePattern assocTypeQName = nodes.getAssocType(assocTypeStr, false); + + if (assocTypeQName == null) + { + assocTypeQName = RegexQNamePattern.MATCH_ALL; + } + + boolean found = false; + + List assocRefs = nodeService.getTargetAssocs(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, sourceNodeId), assocTypeQName); + for (AssociationRef assocRef : assocRefs) + { + if (assocRef.getTargetRef().equals(tgtNodeRef)) + { + nodeService.removeAssociation(srcNodeRef, tgtNodeRef, assocRef.getTypeQName()); + found = true; + } + } + + if (! found) + { + throw new EntityNotFoundException(sourceNodeId+","+assocTypeStr+","+targetNodeId); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeVersionRenditionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeVersionRenditionsRelation.java new file mode 100644 index 00000000000..24ed699e3c6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeVersionRenditionsRelation.java @@ -0,0 +1,136 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.nodes; + +import org.alfresco.rest.api.Renditions; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.Status; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * + * Node version renditions + * + * - GET /nodes/{nodeId}/versions/{versionId}/renditions + * - POST /nodes/{nodeId}/versions/{versionId}/renditions + * - GET /nodes/{nodeId}/versions/{versionId}/renditions/{renditionId} + * - GET /nodes/{nodeId}/versions/{versionId}/renditions/{renditionId}/content + * + * @author janv + */ +@RelationshipResource(name = "renditions", entityResource = NodeVersionsRelation.class, title = "Node version renditions") +public class NodeVersionRenditionsRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Create, + RelationshipResourceBinaryAction.Read, + InitializingBean +{ + private Renditions renditions; + + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "renditions", renditions); + } + + @Override + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + String versionId = parameters.getRelationshipId(); + + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + return renditions.getRenditions(nodeRef, versionId, parameters); + } + + @Override + public Rendition readById(String nodeId, String versionId, Parameters parameters) + { + String renditionId = parameters.getRelationship2Id(); + + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + return renditions.getRendition(nodeRef, versionId, renditionId, parameters); + } + + @WebApiDescription(title = "Create rendition", successStatus = Status.STATUS_ACCEPTED) + @Override + public List create(String nodeId, List entity, Parameters parameters) + { + String versionId = parameters.getRelationshipId(); + + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + renditions.createRenditions(nodeRef, versionId, entity, parameters); + return null; + } + + @WebApiDescription(title = "Download rendition", description = "Download rendition") + @BinaryProperties({ "content" }) + @Override + public BinaryResource readProperty(String nodeId, String versionId, Parameters parameters) + { + String renditionId = parameters.getRelationship2Id(); + + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); + return renditions.getContent(nodeRef, versionId, renditionId, parameters); + } + + @Operation("request-content-url") + @WebApiParam(name = "directAccessUrlRequest", title = "Direct access url request", description = "Direct access url request", kind = ResourceParameter.KIND.HTTP_BODY_OBJECT) + @WebApiDescription(title = "Request content url", + description="Generates a direct access url.", + successStatus = HttpServletResponse.SC_OK) + public DirectAccessUrl requestContentUrl(String nodeId, String versionId, DirectAccessUrlRequest directAccessUrlRequest, Parameters parameters, WithResponse withResponse) + { + String renditionId = parameters.getRelationship2Id(); + + return renditions.requestContentUrl(nodeId, versionId, renditionId, directAccessUrlRequest); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeVersionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeVersionsRelation.java new file mode 100644 index 00000000000..73f5c62ff5f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeVersionsRelation.java @@ -0,0 +1,312 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.integrity.IntegrityException; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.version.Version2Model; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.api.model.VersionOptions; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.service.cmr.coci.CheckOutCheckInService; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionHistory; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +import javax.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Node Versions - version history + * + * @author janv + */ +@RelationshipResource(name = "versions", entityResource = NodesEntityResource.class, title = "Node Versions") +public class NodeVersionsRelation extends AbstractNodeRelation implements + RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceBinaryAction.Read, + RelationshipResourceAction.Delete, + InitializingBean +{ + protected VersionService versionService; + protected BehaviourFilter behaviourFilter; + + @Override + public void afterPropertiesSet() + { + PropertyCheck.mandatory(this, "serviceRegistry", sr); + ParameterCheck.mandatory("nodes", this.nodes); + + this.versionService = sr.getVersionService(); + } + + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + /** + * List version history + * + * @param nodeId String id of (live) node + */ + @Override + @WebApiDescription(title = "Return version history as a paged list of version node infos") + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + NodeRef nodeRef = nodes.validateOrLookupNode(nodeId, null); + + VersionHistory vh = versionService.getVersionHistory(nodeRef); + + Map mapUserInfo = new HashMap<>(10); + List includeParam = parameters.getInclude(); + + List collection = null; + if (vh != null) + { + collection = new ArrayList<>(vh.getAllVersions().size()); + for (Version v : vh.getAllVersions()) + { + Node node = nodes.getFolderOrDocument(v.getFrozenStateNodeRef(), null, null, includeParam, mapUserInfo); + mapVersionInfo(v, node); + collection.add(node); + } + } + + return listPage(collection, parameters.getPaging()); + } + + private void mapVersionInfo(Version v, Node aNode) + { + mapVersionInfo(v, aNode, new NodeRef("", "", v.getVersionLabel())); + } + + public void mapVersionInfo(Version v, Node aNode, NodeRef nodeRef) + { + aNode.setNodeRef(nodeRef); + aNode.setVersionComment(v.getDescription()); + + Map props = aNode.getProperties(); + if (props != null) + { + // special case (as per Version2Service) + props.put("cm:"+Version2Model.PROP_VERSION_TYPE, v.getVersionProperty(Version2Model.PROP_VERSION_TYPE)); + } + + //Don't show parentId, createdAt, createdByUser + aNode.setParentId(null); + aNode.setCreated(null); + aNode.setCreatedByUser(null); + } + + @Override + @WebApiDescription(title="Get version node info", description = "Return metadata for a specific version node") + public Node readById(String nodeId, String versionId, Parameters parameters) + { + Version v = findVersion(nodeId, versionId); + + if (v != null) + { + Node node = nodes.getFolderOrDocumentFullInfo(v.getFrozenStateNodeRef(), null, null, parameters, null); + mapVersionInfo(v, node); + return node; + } + + throw new EntityNotFoundException(nodeId+"-"+versionId); + } + + @WebApiDescription(title = "Download version content", description = "Download version content") + @BinaryProperties({ "content" }) + @Override + public BinaryResource readProperty(String nodeId, String versionId, Parameters parameters) + { + Version v = findVersion(nodeId, versionId); + + if (v != null) + { + NodeRef versionNodeRef = v.getFrozenStateNodeRef(); + return nodes.getContent(versionNodeRef, parameters, true); // TODO should we record version downloads ? + } + + throw new EntityNotFoundException(nodeId+"-"+versionId); + } + + @Operation("revert") + @WebApiDescription(title = "Revert Version", + description="Reverts (ie. promotes) specified version to become a new, most recent, version", + successStatus = HttpServletResponse.SC_OK) + public Node revertById(String nodeId, String versionId, VersionOptions versionOptions, Parameters parameters, WithResponse withResponse) + { + Version v = findVersion(nodeId, versionId); + + if (v != null) + { + CheckOutCheckInService cociService = sr.getCheckOutCheckInService(); + + NodeRef nodeRef = v.getVersionedNodeRef(); + + String versionComment = versionOptions.getComment(); + + VersionType versionType = VersionType.MINOR; + Boolean versionMajor = versionOptions.getMajorVersion(); + if ((versionMajor != null) && (versionMajor)) + { + versionType = VersionType.MAJOR; + } + + Map versionProperties = new HashMap<>(2); + versionProperties.put(VersionModel.PROP_VERSION_TYPE, versionType); + if (versionComment != null) + { + versionProperties.put(VersionModel.PROP_DESCRIPTION, versionComment); + } + + //cancel editing if we want to revert + if (sr.getNodeService().hasAspect(nodeRef, ContentModel.ASPECT_WORKING_COPY)) + { + nodeRef = cociService.cancelCheckout(nodeRef); + } + + // TODO review default for deep and/or whether we should make it an option + versionService.revert(nodeRef, v, false); + + // Checkout/Checkin the node - to store the new version in version history + NodeRef wcNodeRef = cociService.checkout(nodeRef); + cociService.checkin(wcNodeRef, versionProperties); + + // get latest version + v = versionService.getVersionHistory(nodeRef).getHeadVersion(); + + Node node = nodes.getFolderOrDocumentFullInfo(v.getFrozenStateNodeRef(), null, null, parameters, null); + mapVersionInfo(v, node); + return node; + } + + throw new EntityNotFoundException(nodeId+"-"+versionId); + } + + @Override + @WebApiDescription(title = "Delete version") + public void delete(String nodeId, String versionId, Parameters parameters) + { + Version v = findVersion(nodeId, versionId); + + // live (aka versioned) nodeRef + NodeRef nodeRef = v.getVersionedNodeRef(); + + if (sr.getPermissionService().hasPermission(nodeRef, PermissionService.DELETE) != AccessStatus.ALLOWED) + { + throw new PermissionDeniedException("Cannot delete version"); + } + + versionService.deleteVersion(nodeRef, v); + + Map props = sr.getNodeService().getProperties(nodeRef); + if (props.get(ContentModel.PROP_VERSION_LABEL) == null) + { + // attempt to delete last version - we do not yet support this (see REPO-835 & REPO-834) + // note: alternatively, the client can remove the "cm:versionable" aspect (if permissions allow) to clear the version history and disable versioning + throw new IntegrityException("Cannot delete last version (did you mean to disable versioning instead ?) ["+nodeId+","+versionId+"]", null); + + /* + if (props.get(ContentModel.PROP_VERSION_TYPE) != null) + { + // minor fix up to versionable aspect - ie. remove versionType + behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + try + { + sr.getNodeService().removeProperty(nodeRef, ContentModel.PROP_VERSION_TYPE); + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); + behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); + } + } + */ + } + } + + @Operation("request-content-url") + @WebApiParam(name = "directAccessUrlRequest", title = "Direct access url request", description = "Direct access url request", kind = ResourceParameter.KIND.HTTP_BODY_OBJECT) + @WebApiDescription(title = "Request content url", + description="Generates a direct access url.", + successStatus = HttpServletResponse.SC_OK) + public DirectAccessUrl requestContentUrl(String nodeId, String versionId, DirectAccessUrlRequest directAccessUrlRequest, Parameters parameters, WithResponse withResponse) + { + Version version = findVersion(nodeId, versionId); + + if (version == null) + { + throw new EntityNotFoundException(nodeId + "-" + versionId); + } + + return nodes.requestContentUrl(version.getFrozenStateNodeRef(), directAccessUrlRequest); + } + + public Version findVersion(String nodeId, String versionLabelId) + { + NodeRef nodeRef = nodes.validateOrLookupNode(nodeId, null); + VersionHistory vh = versionService.getVersionHistory(nodeRef); + if (vh != null) + { + return vh.getVersion(versionLabelId); + } + return null; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodesEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodesEntityResource.java new file mode 100644 index 00000000000..48a5a7c75db --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodesEntityResource.java @@ -0,0 +1,206 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.nodes; + +import java.io.InputStream; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.LockInfo; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.DirectAccessUrlRequest; +import org.alfresco.rest.api.model.NodeTarget; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.service.cmr.repository.DirectAccessUrl; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * An implementation of an Entity Resource for a Node (file or folder) + * + * @author sglover + * @author Gethin James + * @author janv + */ +@EntityResource(name="nodes", title = "Nodes") +public class NodesEntityResource implements + EntityResourceAction.ReadById, EntityResourceAction.Delete, EntityResourceAction.Update, + BinaryResourceAction.Read, BinaryResourceAction.Update, InitializingBean +{ + private Nodes nodes; + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("nodes", this.nodes); + } + + /** + * Returns information regarding the node 'nodeId' - folder or document + * + * @param nodeId String id of node (folder or document) - will also accept well-known aliases, eg. "-root-", "-my-", "-shared-" + * + * Optional parameters: + * - path + */ + @WebApiDescription(title = "Get Node Information", description = "Get information for the node with id 'nodeId'") + @WebApiParam(name = "nodeId", title = "The node id") + public Node readById(String nodeId, Parameters parameters) + { + return nodes.getFolderOrDocument(nodeId, parameters); + } + + /** + * Download content + * + * @param fileNodeId + * @param parameters {@link Parameters} + * @return + * @throws EntityNotFoundException + */ + @Override + @WebApiDescription(title = "Download content", description = "Download content") + @BinaryProperties({"content"}) + public BinaryResource readProperty(String fileNodeId, Parameters parameters) throws EntityNotFoundException + { + return nodes.getContent(fileNodeId, parameters, true); + } + + /** + * Upload new version of content + * + * This allow binary content update of an existing file/content node. + * + * Note: alternatively, can upload via POST (multipart/form-data) with existing file name and form "overwrite=true". + * + * @param fileNodeId + * @param contentInfo Basic information about the content stream + * @param stream An inputstream + * @param parameters + * @return + */ + @Override + @WebApiDescription(title = "Upload content", description = "Upload content") + @BinaryProperties({"content"}) + public Node updateProperty(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) + { + return nodes.updateContent(fileNodeId, contentInfo, stream, parameters); + } + + /** + * Update info on the node 'nodeId' - folder or document + * + * Can update name (which is a "rename" and hence must be unique within the current parent folder) + * or update other properties. + * + * @param nodeId String nodeId of node (folder or document) + * @param nodeInfo node entity with info to update (eg. name, properties ...) + * @param parameters + * @return + */ + @Override + @WebApiDescription(title="Updates a node (file or folder) with id 'nodeId'") + public Node update(String nodeId, Node nodeInfo, Parameters parameters) + { + return nodes.updateNode(nodeId, nodeInfo, parameters); + } + + /** + * Delete the given node. Note: will cascade delete for a folder. + * + * @param nodeId String id of node (folder or document) + */ + @Override + @WebApiDescription(title = "Delete Node", description="Delete the file or folder with id 'nodeId'. Folder will cascade delete") + public void delete(String nodeId, Parameters parameters) + { + nodes.deleteNode(nodeId, parameters); + } + + @Operation("copy") + @WebApiDescription(title = "Copy Node", description="Copy one or more nodes (files or folders) to a new target folder, with option to rename.") + public Node copyById(String nodeId, NodeTarget target, Parameters parameters, WithResponse withResponse) + { + return nodes.moveOrCopyNode(nodeId, target.getTargetParentId(), target.getName(), parameters, true); + } + + @Operation("move") + @WebApiDescription(title = "Move Node", + description="Moves one or more nodes (files or folders) to a new target folder, with option to rename.", + successStatus = HttpServletResponse.SC_OK) + public Node moveById(String nodeId, NodeTarget target, Parameters parameters, WithResponse withResponse) + { + return nodes.moveOrCopyNode(nodeId, target.getTargetParentId(), target.getName(), parameters, false); + } + + @Operation("lock") + @WebApiDescription(title = "Lock Node", + description="Places a lock on a node.", + successStatus = HttpServletResponse.SC_OK) + public Node lock(String nodeId, LockInfo lockInfo, Parameters parameters, WithResponse withResponse) + { + return nodes.lock(nodeId, lockInfo, parameters); + } + + @Operation("unlock") + @WebApiDescription(title = "Unlock Node", + description="Removes a lock on a node.", + successStatus = HttpServletResponse.SC_OK) + public Node unlock(String nodeId, Void ignore, Parameters parameters, WithResponse withResponse) + { + return nodes.unlock(nodeId, parameters); + } + + @Operation("request-content-url") + @WebApiParam(name = "directAccessUrlRequest", title = "Direct access url request", description = "Direct access url request", kind = ResourceParameter.KIND.HTTP_BODY_OBJECT) + @WebApiDescription(title = "Request content url", + description="Generates a direct access url.", + successStatus = HttpServletResponse.SC_OK) + public DirectAccessUrl requestContentUrl(String nodeId, DirectAccessUrlRequest directAccessUrlRequest, Parameters parameters, WithResponse withResponse) + { + return nodes.requestContentUrl(nodeId, directAccessUrlRequest); + } + +} + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/nodes/package-info.java new file mode 100644 index 00000000000..722feda5748 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/nodes/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.nodes; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java new file mode 100644 index 00000000000..114098121fa --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java @@ -0,0 +1,232 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.rest.api.People; +import org.alfresco.rest.api.model.Client; +import org.alfresco.rest.api.model.PasswordReset; +import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * An implementation of an Entity Resource for a Person + * + * @author sglover + * @author Gethin James + */ +@EntityResource(name="people", title = "People") +public class PeopleEntityResource implements EntityResourceAction.ReadById, EntityResourceAction.Create, + EntityResourceAction.Update,EntityResourceAction.Read, + + BinaryResourceAction.Read, BinaryResourceAction.Update, BinaryResourceAction.Delete, InitializingBean +{ + private static Log logger = LogFactory.getLog(PeopleEntityResource.class); + + private People people; + + public void setPeople(People people) + { + this.people = people; + } + + @Override + public void afterPropertiesSet() + { + PropertyCheck.mandatory(this, "people", people); + } + + /** + * Get a person by userName. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction.ReadById#readById(String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Get Person Information", description = "Get information for the person with id 'personId'") + @WebApiParam(name = "personId", title = "The person's username") + public Person readById(String personId, Parameters parameters) + { + Person person = people.getPerson(personId); + return person; + } + + @Override + @WebApiDescription(title="Create person", description="Create a person") + @WebApiParam(name="persons", title="A single person", description="A single person, multiple people are not supported.", + kind= ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple=false, required = true) + public List create(List persons, Parameters parameters) + { + Person p = persons.get(0); + + validateDerivedFieldsExistence(p); + + List result = new ArrayList<>(1); + result.add(people.create(p)); + return result; + } + + @Override + @WebApiDescription(title="Update person", description="Update the given person's details") + public Person update(String personId, Person person, Parameters parameters) + { + if (person.wasSet(ContentModel.PROP_USERNAME)) + { + // REPO-1537 + throw new InvalidArgumentException("Unsupported field: id"); + } + + validateDerivedFieldsExistence(person); + + return people.update(personId, person); + } + + /** + * Explicitly test for the presence of system-maintained (derived) fields that are settable on Person (see also REPO-110). + * + * @param person + */ + private void validateDerivedFieldsExistence(Person person) + { + if (person.wasSet(ContentModel.PROP_USER_STATUS_TIME)) + { + throw new InvalidArgumentException("Unsupported field: statusUpdatedAt"); + } + + if (person.wasSet(Person.PROP_PERSON_AVATAR_ID)) + { + throw new InvalidArgumentException("Unsupported field: avatarId"); + } + + if (person.wasSet(ContentModel.PROP_SIZE_QUOTA)) + { + throw new InvalidArgumentException("Unsupported field: quota"); + } + + if (person.wasSet(ContentModel.PROP_SIZE_CURRENT)) + { + throw new InvalidArgumentException("Unsupported field: quotaUsed"); + } + } + + @Override + @WebApiDescription(title = "Get List of People", description = "Get List of People") + public CollectionWithPagingInfo readAll(Parameters params) + { + return people.getPeople(params); + } + + @Operation("request-password-reset") + @WebApiDescription(title = "Request Password Reset", description = "Request password reset", + successStatus = HttpServletResponse.SC_ACCEPTED) + @WebApiNoAuth + public void requestPasswordReset(String personId, Client client, Parameters parameters, WithResponse withResponse) + { + people.requestPasswordReset(personId, client.getClient()); + } + + @Operation("reset-password") + @WebApiDescription(title = "Reset Password", description = "Performs password reset", successStatus = HttpServletResponse.SC_ACCEPTED) + @WebApiNoAuth + public void resetPassword(String personId, PasswordReset passwordReset, Parameters parameters, WithResponse withResponse) + { + people.resetPassword(personId, passwordReset); + } + + /** + * Download avatar image content + * + * @param personId + * @param parameters {@link Parameters} + * @return + * @throws EntityNotFoundException + */ + @Override + @WebApiDescription(title = "Download avatar", description = "Download avatar") + @BinaryProperties({"avatar"}) + public BinaryResource readProperty(String personId, Parameters parameters) throws EntityNotFoundException + { + return people.downloadAvatarContent(personId, parameters); + } + + /** + * Upload avatar image content + * + * @param personId + * @param contentInfo Basic information about the content stream + * @param stream An inputstream + * @param parameters + * @return + */ + @Override + @WebApiDescription(title = "Upload avatar", description = "Upload avatar") + @BinaryProperties({"avatar"}) + public Person updateProperty(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) + { + return people.uploadAvatarContent(personId, contentInfo, stream, parameters); + } + + /** + * Delete avatar image content + * + * @param personId + * @param parameters + * @return + */ + @Override + @WebApiDescription(title = "Delete avatar image", description = "Delete avatar image") + @BinaryProperties({ "avatar" }) + public void deleteProperty(String personId, Parameters parameters) + { + people.deleteAvatarContent(personId); + } + + +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonActivitiesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonActivitiesRelation.java new file mode 100644 index 00000000000..bf4d59d7055 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonActivitiesRelation.java @@ -0,0 +1,87 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import org.alfresco.rest.api.Activities; +import org.alfresco.rest.api.model.Activity; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.WebApiParameters; +import org.alfresco.rest.framework.core.ResourceParameter.KIND; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author steveglover + * + */ +@RelationshipResource(name = "activities", entityResource = PeopleEntityResource.class, title = "Person Activities") +public class PersonActivitiesRelation implements RelationshipResourceAction.Read, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonActivitiesRelation.class); + + private Activities activities; + + public void setActivities(Activities activities) + { + this.activities = activities; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("activities", this.activities); + } + + /* + * List the user's activities, excluding those of other users. + * + * /people/[id]/activities + * + * ordered by postDate + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Get#get(java.io.Serializable) + */ + @Override + @WebApiDescription(title = "List the user's activities, excluding those of other users.") + @WebApiParameters({ + @WebApiParam(name = "who", title = "Who", + description="Filter to include the user's activities only ('me'), other user's activities only ('others'), or all activities (don't include the parameter).", kind=KIND.QUERY_STRING), + @WebApiParam(name = "siteId", title="siteId", description = "Include only activity feed entries relating to this site.", kind=KIND.QUERY_STRING) + }) + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return activities.getUserActivities(personId, parameters); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonFavouriteSitesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonFavouriteSitesRelation.java new file mode 100644 index 00000000000..9358390c213 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonFavouriteSitesRelation.java @@ -0,0 +1,113 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import java.util.List; + +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.FavouriteSite; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author steveglover + * + */ +@RelationshipResource(name = "favorite-sites", entityResource = PeopleEntityResource.class, title = "Person Favorite Sites") +public class PersonFavouriteSitesRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, +RelationshipResourceAction.Create, RelationshipResourceAction.Delete, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonFavouriteSitesRelation.class); + + private Sites sites; + + public void setSites(Sites sites) + { + this.sites = sites; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("sites", this.sites); + } + + /** + * List the user's favourite sites. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Read#readAll(java.lang.String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Get Person Favorite Sites", description = "Get a paged list of the person's favorite sites") + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return sites.getFavouriteSites(personId, parameters); + } + + /** + * List the favourite site information for a specific site. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.ReadById#readById(String, String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Get Person Favorite Site", description = "Get information on a person's specific favorite site") + public FavouriteSite readById(String personId, String siteShortName, Parameters parameters) + { + return sites.getFavouriteSite(personId, siteShortName); + } + + /** + * Adds the given site as a favourite site for the user. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Create#create(String, java.util.List, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Add Person Favorite Site", description = "Favorite a site") + public List create(String personId, List entity, Parameters parameters) + { + for (FavouriteSite favSite : entity) + { + sites.addFavouriteSite(personId, favSite); + } + return entity; + } + + @Override + @WebApiDescription(title = "Remove Person Favorite Site", description = "Un-favorite a site") + public void delete(String personId, String siteId, Parameters parameters) + { + sites.removeFavouriteSite(personId, siteId); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonFavouritesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonFavouritesRelation.java new file mode 100644 index 00000000000..5124a765a23 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonFavouritesRelation.java @@ -0,0 +1,105 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.Favourites; +import org.alfresco.rest.api.model.Favourite; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +@RelationshipResource(name = "favorites", entityResource = PeopleEntityResource.class, title = "Person Favorites") +public class PersonFavouritesRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, +RelationshipResourceAction.Create, RelationshipResourceAction.Delete, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonFavouritesRelation.class); + + private Favourites favourites; + + public void setFavourites(Favourites favourites) + { + this.favourites = favourites; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("favourites", this.favourites); + } + + /** + * List the user's favourites. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Read#readAll(java.lang.String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Get Person Favorites", description = "Get a paged list of the person's favorites") + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return favourites.getFavourites(personId, parameters); + } + + /** + * Adds the given site as a favourite site for the user. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Create#create(String, java.util.List, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Add Person Favorite", description = "Favorite something") + public List create(String personId, List entity, Parameters parameters) + { + List ret = new ArrayList(entity.size()); + for(Favourite favourite : entity) + { + ret.add(favourites.addFavourite(personId, favourite, parameters)); + } + return ret; + } + + @Override + @WebApiDescription(title = "Remove Person Favorite", description = "Un-favorite something") + public void delete(String personId, String id, Parameters parameters) + { + favourites.removeFavourite(personId, id); + } + + @Override + public Favourite readById(String personId, String favouriteId, Parameters parameters) + throws RelationshipResourceNotFoundException + { + return favourites.getFavourite(personId, favouriteId, parameters); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonGroupsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonGroupsRelation.java new file mode 100644 index 00000000000..e330d9e7c03 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonGroupsRelation.java @@ -0,0 +1,68 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import org.alfresco.rest.api.Groups; +import org.alfresco.rest.api.model.Group; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Provide access to the groups of which a person is a member. + *

    + * For example, list groups for person jbloggs. + * + * @author Matt Ward + */ +@RelationshipResource( + name = "groups", + entityResource = PeopleEntityResource.class, + title = "Person Groups") +public class PersonGroupsRelation implements RelationshipResourceAction.Read, InitializingBean +{ + private Groups groups; + + public void setGroups(Groups groups) + { + this.groups = groups; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("groups", groups); + } + + @Override + public CollectionWithPagingInfo readAll(String personId, Parameters params) + { + return groups.getGroupsByPersonId(personId, params); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonNetworksRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonNetworksRelation.java new file mode 100644 index 00000000000..71c66f11ec2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonNetworksRelation.java @@ -0,0 +1,77 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import org.alfresco.rest.api.Networks; +import org.alfresco.rest.api.model.PersonNetwork; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author steveglover + * + */ +@RelationshipResource(name = "networks", entityResource = PeopleEntityResource.class, title = "Person Networks") +public class PersonNetworksRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonNetworksRelation.class); + + private Networks networks; + + public void setNetworks(Networks networks) + { + this.networks = networks; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("networks", this.networks); + } + + @Override + @WebApiDescription(title = "A paged list of the person's network memberships.") + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return networks.getNetworks(personId, parameters.getPaging()); + } + + @Override + @WebApiDescription(title = "Network membership for person 'personId' in network 'networkId'.") + public PersonNetwork readById(String personId, String networkId, Parameters parameters) + { + return networks.getNetwork(personId, networkId); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonPreferencesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonPreferencesRelation.java new file mode 100644 index 00000000000..b5ca948cf29 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonPreferencesRelation.java @@ -0,0 +1,93 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import org.alfresco.rest.api.Preferences; +import org.alfresco.rest.api.model.Preference; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +@RelationshipResource(name = "preferences", entityResource = PeopleEntityResource.class, title = "Person Preferences") +public class PersonPreferencesRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonPreferencesRelation.class); + + private Preferences preferences; + + public void setPreferences(Preferences preferences) + { + this.preferences = preferences; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("preferences", this.preferences); + } + + /** + * Returns a paged list of preferences for the user personId. + * + * If personId does not exist, NotFoundException (status 404). + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Read#readAll(java.lang.String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "A paged list of the persons preferences.") + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return preferences.getPreferences(personId, parameters.getPaging()); + } + + /** + * Returns information regarding the preference 'preferenceName' for user personId. + * + * If personId does not exist, NotFoundException (status 404). + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.ReadById#readById(String, String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "Preference value for preference 'preferenceName' for person 'personId'.") + public Preference readById(String personId, String preferenceName, Parameters parameters) + { + // fix for REPO-855 + String url = parameters.getRequest().getURL(); + if(url.matches(".*workspace://SpacesStore/.*")){ + preferenceName += url.substring(url.indexOf("//SpacesStore/")); + } + // + + return preferences.getPreference(personId, preferenceName); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonSiteMembershipRequestsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonSiteMembershipRequestsRelation.java new file mode 100644 index 00000000000..3d7c018e265 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonSiteMembershipRequestsRelation.java @@ -0,0 +1,102 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.rest.api.SiteMembershipRequests; +import org.alfresco.rest.api.model.SiteMembershipRequest; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +@RelationshipResource(name = "site-membership-requests", entityResource = PeopleEntityResource.class, title = "Site Membership Requests") +public class PersonSiteMembershipRequestsRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Delete, RelationshipResourceAction.Update, +RelationshipResourceAction.Create, RelationshipResourceAction.ReadById, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonSiteMembershipRequestsRelation.class); + + private SiteMembershipRequests siteMembershipRequests; + + public void setSiteMembershipRequests(SiteMembershipRequests siteMembershipRequests) + { + this.siteMembershipRequests = siteMembershipRequests; + } + + @Override + public void afterPropertiesSet() throws Exception + { + } + + @Override + @WebApiDescription(title = "The site membership request for personId and siteId, if it exists.") + public SiteMembershipRequest readById(String personId, String siteId, Parameters parameters) throws RelationshipResourceNotFoundException + { + SiteMembershipRequest siteInvite = siteMembershipRequests.getSiteMembershipRequest(personId, siteId); + return siteInvite; + } + + @Override + @WebApiDescription(title = "Create a site membership request for personId and siteIds. The personId will be invited to the site as a SiteConsumer.") + public List create(String personId, List invites, Parameters parameters) + { + List result = new ArrayList(invites.size()); + for(SiteMembershipRequest invite : invites) + { + SiteMembershipRequest siteInvite = siteMembershipRequests.createSiteMembershipRequest(personId, invite); + result.add(siteInvite); + } + return result; + } + + @Override + @WebApiDescription(title = "Remove an existing site membership request for personId and siteId, if it exists.") + public void delete(String personId, String siteId, Parameters parameters) + { + siteMembershipRequests.cancelSiteMembershipRequest(personId, siteId); + } + + @Override + @WebApiDescription(title = "A paged list of site membership requests for personId.") + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return siteMembershipRequests.getPagedSiteMembershipRequests(personId, parameters.getPaging()); + } + + @Override + @WebApiDescription(title = "Update the comment for the site membership request for personId and siteId.") + public SiteMembershipRequest update(String personId, SiteMembershipRequest siteInvite, Parameters parameters) + { + return siteMembershipRequests.updateSiteMembershipRequest(personId, siteInvite); + } +} \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/PersonSitesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonSitesRelation.java new file mode 100644 index 00000000000..705bf2728cd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/PersonSitesRelation.java @@ -0,0 +1,96 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.people; + +import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.MemberOfSite; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +@RelationshipResource(name = "sites", entityResource = PeopleEntityResource.class, title = "Person Sites") +public class PersonSitesRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, + RelationshipResourceAction.Delete, InitializingBean +{ + private static final Log logger = LogFactory.getLog(PersonSitesRelation.class); + + private Sites sites; + + public void setSites(Sites sites) + { + this.sites = sites; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("sites", this.sites); + } + + /** + * + * List all the sites that the specified user has a explicit membership of. + * + * THOR-1151: “F312: For a user, get the list of sites they are a member of” + * + * If personId does not exist, NotFoundException (status 404). + * + * @param personId the id (email) of the person + * + * (non-Javadoc) + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.Read#readAll(String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title = "A paged list of the person's site memberships.") + public CollectionWithPagingInfo readAll(String personId, Parameters parameters) + { + return sites.getSites(personId, parameters); + } + + /** + * Returns site membership information for personId in siteId. + * + * @see org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction.ReadById#readById(String, String, org.alfresco.rest.framework.resource.parameters.Parameters) + */ + @Override + @WebApiDescription(title="Site membership information for 'personId' in 'siteId'.") + public MemberOfSite readById(String personId, String siteId, Parameters parameters) + { + return sites.getMemberOfSite(personId, siteId); + } + + @Override + public void delete(String personId, String siteId, Parameters parameters) + { + sites.removeSiteMember(personId, siteId); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/people/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/people/package-info.java new file mode 100644 index 00000000000..448cad15ee8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/people/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.people; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/probes/ProbeEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/probes/ProbeEntityResource.java new file mode 100644 index 00000000000..d470efad266 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/probes/ProbeEntityResource.java @@ -0,0 +1,209 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.probes; + +import org.alfresco.rest.api.discovery.DiscoveryApiWebscript; +import org.alfresco.rest.api.model.Probe; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.ServiceUnavailableException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * An implementation of an Entity Resource for Probes. + */ +@EntityResource(name="probes", title = "Probes") +public class ProbeEntityResource implements EntityResourceAction.ReadById +{ + public static final String LIVE = "-live-"; + public static final String READY = "-ready-"; + + public static final long CHECK_PERIOD = 10 * 1000; // Maximum of only one checkResult every 10 seconds. + + protected static Log logger = LogFactory.getLog(ProbeEntityResource.class);; + + private long nextCheckTime = 0; + private Boolean checkResult; + private Boolean checking = false; + private boolean readySent; + + private DiscoveryApiWebscript discovery; + + public DiscoveryApiWebscript setDiscovery(DiscoveryApiWebscript discovery) + { + DiscoveryApiWebscript result = this.discovery; + this.discovery = discovery; + return result; + } + + /** + * Returns a status code of 200 for okay. The probe contains little information for security reasons. + * + * Note: does *not* require authenticated access, so limits the amount of work performed to avoid a DDOS. + */ + @Override + @WebApiDescription(title="Get probe status", description = "Returns 200 if valid") + @WebApiParam(name = "probeName", title = "The probe's name") + @WebApiNoAuth + public Probe readById(String name, Parameters parameters) + { + boolean isLiveProbe = LIVE.equalsIgnoreCase(name); + if (!isLiveProbe && !READY.equalsIgnoreCase(name)) + { + throw new InvalidArgumentException("Bad probe name"); + } + + String message = doCheckOrNothing(isLiveProbe); + return new Probe(message); + } + + // We don't want to be doing checks all the time or holding monitors for a long time to avoid a DDOS. + public String doCheckOrNothing(boolean isLiveProbe) + { + boolean doCheck = false; + long now = 0; + boolean result; + String message = "No test"; + boolean logInfo = false; + synchronized(checking) + { + // Initially ready needs to be false so we don't get requests and live true so the pod is not killed. + if (checkResult == null) + { + result = isLiveProbe; + } + else + { + result = checkResult; + } + + if (checking) // Is another thread is checking? + { + if (!readySent && result && !isLiveProbe) + { + readySent = true; + logInfo = true; + } + } + else // This thread will do a check + { + now = System.currentTimeMillis(); + if (checkResult == null || nextCheckTime <= now) + { + doCheck = true; + checking = true; + } + } + } + + if (doCheck) + { + try + { + message = "Tested"; + doCheck(isLiveProbe); + result = true; + } + catch (Exception e) + { + result = false; + } + finally + { + synchronized (checking) + { + checking = false; + checkResult = result; + setNextCheckTime(now); + if (result && !readySent && !isLiveProbe) // Are we initially ready + { + readySent = true; + logInfo = true; + } + else if (!result && (isLiveProbe || readySent)) // Are we sick + { + logInfo = true; + } + } + } + } + + message = getMessage(isLiveProbe, result, message); + + if (logInfo) + { + logger.info(message); + } + else + { + logger.debug(message); + } + + if (result) + { + return message; + } + throw new ServiceUnavailableException(message); + } + + private String getMessage(boolean isLiveProbe, boolean result, String message) + { + return (isLiveProbe ? "liveProbe" : "readyProbe")+": "+ + (result ? "Success" : "Failure") + + " - "+message; + } + + private void doCheck(boolean isLiveProbe) + { + discovery.getRepositoryInfo(); + } + + private void setNextCheckTime(long now) + { + long oldValue = nextCheckTime; + if (nextCheckTime == 0) + { + nextCheckTime = (now / 60000) * 60000; + } + + do + { + nextCheckTime += CHECK_PERIOD; + } + while (nextCheckTime <= now); + + if (logger.isTraceEnabled()) + { + logger.trace("nextCheckTime: " + nextCheckTime + " (+" + ((nextCheckTime - oldValue) / 1000) + " secs)"); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/probes/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/probes/package-info.java new file mode 100644 index 00000000000..45a640dee5c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/probes/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.probes; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/queries/QueriesEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/queries/QueriesEntityResource.java new file mode 100644 index 00000000000..f99d8be261c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/queries/QueriesEntityResource.java @@ -0,0 +1,83 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.queries; + +import org.alfresco.rest.api.Queries; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * An implementation of an Entity Resource for Queries. + * + * @author janv + * @author Alan Davis + */ +@EntityResource(name="queries", title = "Queries") +public class QueriesEntityResource implements + EntityResourceAction.ReadById>, + InitializingBean +{ + private final static String QUERY_NODES = "nodes"; + private final static String QUERY_PEOPLE = "people"; + private final static String QUERY_SITES = "sites"; + + private Queries queries; + + public void setQueries(Queries queries) + { + this.queries = queries; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("queries", this.queries); + } + + // hmm - a little unorthodox + @Override + @WebApiDescription(title="Find results", description = "Find & list search results for given query id") + public CollectionWithPagingInfo readById(String queryId, Parameters parameters) + { + switch (queryId) + { + case QUERY_NODES: + return queries.findNodes(parameters); + case QUERY_PEOPLE: + return queries.findPeople(parameters); + case QUERY_SITES: + return queries.findSites(parameters); + default: + throw new NotFoundException(queryId); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/queries/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/queries/package-info.java new file mode 100644 index 00000000000..56e11cb500a --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/queries/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.queries; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java new file mode 100644 index 00000000000..83c23497261 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkEntityResource.java @@ -0,0 +1,159 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.quicksharelinks; + +import org.alfresco.rest.api.QuickShareLinks; +import org.alfresco.rest.api.model.QuickShareLink; +import org.alfresco.rest.api.model.QuickShareLinkEmailRequest; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.Operation; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.rest.framework.webscripts.WithResponse; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.Status; + +import java.util.List; + +/** + * An implementation of an Entity Resource for Shared Links. + * + * @author janv + * @author Jamal Kaabi-Mofrad + */ +@EntityResource(name="shared-links", title = "Shared Links") +public class QuickShareLinkEntityResource implements EntityResourceAction.ReadById, + BinaryResourceAction.Read, + EntityResourceAction.Delete, + EntityResourceAction.Create, + EntityResourceAction.Read, + InitializingBean +{ + private QuickShareLinks quickShareLinks; + + public void setQuickShareLinks(QuickShareLinks quickShareLinks) + { + this.quickShareLinks = quickShareLinks; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("quickShareLinks", this.quickShareLinks); + } + + /** + * Returns limited metadata regarding the shared (content) link. + * + * Note: does *not* require authenticated access for (public) shared link. + */ + @Override + @WebApiDescription(title="Get shared link info", description = "Return limited metadata for shared link") + @WebApiNoAuth + public QuickShareLink readById(String sharedId, Parameters parameters) + { + return quickShareLinks.readById(sharedId, parameters); + } + + /** + * Download content via shared link. + * + * Note: does *not* require authenticated access for (public) shared link. + * + * @param sharedId + * @param parameters {@link Parameters} + * @return + * @throws EntityNotFoundException + */ + @Override + @WebApiDescription(title = "Download shared link content", description = "Download content for shared link") + @WebApiNoAuth + @BinaryProperties({"content"}) + public BinaryResource readProperty(String sharedId, Parameters parameters) throws EntityNotFoundException + { + return quickShareLinks.readProperty(sharedId, null, parameters); + } + + /** + * Delete the shared link. + * + * Once deleted, the shared link will no longer exist hence get/download will no longer work (ie. return 404). + * If the link is later re-created then a new unique shared id will be generated. + * + * Requires authenticated access. + * + * @param sharedId String id of the quick share + */ + @Override + @WebApiDescription(title = "Delete shared link", description="Delete the shared link") + public void delete(String sharedId, Parameters parameters) + { + quickShareLinks.delete(sharedId, parameters); + } + + /** + * Create quick share. + * + * Requires authenticated access. + * + * @param nodeIds + * @param parameters + * @return + */ + @Override + @WebApiDescription(title="Create shared link", description="Create a new unique system-generated shared (link) id") + public List create(List nodeIds, Parameters parameters) + { + return quickShareLinks.create(nodeIds, parameters); + } + + @Operation("email") + @WebApiDescription(title = "Email shared link", successStatus = Status.STATUS_ACCEPTED) + public void email(String sharedId, QuickShareLinkEmailRequest emailRequest, Parameters parameters, WithResponse response) + { + quickShareLinks.emailSharedLink(sharedId, emailRequest, parameters); + } + + /** + * Find shared links + * + */ + @Override + @WebApiDescription(title="Find shared links", description = "Find ('search') & return result set of shared links") + public CollectionWithPagingInfo readAll(Parameters parameters) + { + return quickShareLinks.findLinks(parameters); + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkRenditionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkRenditionsRelation.java new file mode 100644 index 00000000000..e63bfa73da7 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/QuickShareLinkRenditionsRelation.java @@ -0,0 +1,92 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.quicksharelinks; + +import org.alfresco.rest.api.QuickShareLinks; +import org.alfresco.rest.api.model.Rendition; +import org.alfresco.rest.framework.BinaryProperties; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiNoAuth; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceBinaryAction; +import org.alfresco.rest.framework.resource.content.BinaryResource; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Enable rendition(s) to be download via Shared Link + * + * @author janv + */ +@RelationshipResource(name = "renditions", entityResource = QuickShareLinkEntityResource.class, title = "Node renditions via shared link") +public class QuickShareLinkRenditionsRelation implements + RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceBinaryAction.Read, + InitializingBean +{ + private QuickShareLinks quickShareLinks; + + public void setQuickShareLinks(QuickShareLinks quickShareLinks) + { + this.quickShareLinks = quickShareLinks; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("quickShareLinks", this.quickShareLinks); + } + + @WebApiDescription(title = "Download shared link rendition", description = "Download rendition for shared link") + @WebApiNoAuth + @BinaryProperties({"content"}) + @Override + public BinaryResource readProperty(String sharedId, String renditionId, Parameters parameters) + { + return quickShareLinks.readProperty(sharedId, renditionId, parameters); + } + + @WebApiDescription(title = "List renditions", description = "List available (created) renditions") + @WebApiNoAuth + @Override + public CollectionWithPagingInfo readAll(String sharedId, Parameters parameters) + { + return quickShareLinks.getRenditions(sharedId); + } + + @WebApiDescription(title = "Retrieve rendition information", description = "Retrieve (created) rendition information") + @WebApiNoAuth + @Override + public Rendition readById(String entityResourceId, String id, Parameters parameters) + { + return quickShareLinks.getRendition(entityResourceId, id); + } +} + diff --git a/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/package-info.java new file mode 100644 index 00000000000..195a9dda164 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/quicksharelinks/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.quicksharelinks; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java b/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java new file mode 100644 index 00000000000..44000aaf5fb --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/SearchApiWebscript.java @@ -0,0 +1,168 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search; + +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.search.context.SearchRequestContext; +import org.alfresco.rest.api.search.impl.ResultMapper; +import org.alfresco.rest.api.search.impl.SearchMapper; +import org.alfresco.rest.api.search.model.SearchQuery; +import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.rest.framework.tools.RequestReader; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.alfresco.rest.framework.webscripts.ResourceWebScriptHelper; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of the {{baseUrl}}/{{networkId}}/public/search/versions/1/search endpoint + * + * @author Gethin James + */ +public class SearchApiWebscript extends AbstractWebScript implements RecognizedParamsExtractor, RequestReader, ResponseWriter, + InitializingBean +{ + private ServiceRegistry serviceRegistry; + private SearchService searchService; + private SearchMapper searchMapper; + private ResultMapper resultMapper; + protected ApiAssistant assistant; + protected ResourceWebScriptHelper helper; + + @Override + public void afterPropertiesSet() + { + PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry); + this.searchService = serviceRegistry.getSearchService(); + ParameterCheck.mandatory("assistant", this.assistant); + ParameterCheck.mandatory("searchMapper", this.searchMapper); + ParameterCheck.mandatory("resultMapper", this.resultMapper); + } + + @Override + public void execute(WebScriptRequest webScriptRequest, WebScriptResponse webScriptResponse) throws IOException + { + + try { + //Turn JSON into a Java object respresentation + SearchQuery searchQuery = extractJsonContent(webScriptRequest, assistant.getJsonHelper(), SearchQuery.class); + + //Parse the parameters + Params params = getParams(webScriptRequest, searchQuery.getFields(), searchQuery.getInclude(), searchQuery.getPaging()); + + //Make a copy of the request + SearchRequestContext searchRequestContext = SearchRequestContext.from(searchQuery); + + //Turn the SearchQuery json into the Java SearchParameters object + SearchParameters searchParams = searchMapper.toSearchParameters(params, searchQuery, searchRequestContext); + + //Call searchService + ResultSet results = searchService.query(searchParams); + + //Turn solr results into JSON + CollectionWithPagingInfo resultJson = resultMapper.toCollectionWithPagingInfo(params, searchRequestContext, searchQuery, results); + //Post-process the request and pass in params, eg. params.getFilter() + Object toRender = helper.processAdditionsToTheResponse(null, null, null, params, resultJson); + + //Write response + setResponse(webScriptResponse, DEFAULT_SUCCESS); + renderJsonResponse(webScriptResponse, toRender, assistant.getJsonHelper()); + + } catch (Exception exception) { + renderException(exception,webScriptResponse,assistant); + } + } + + /** + * Gets the Params object, parameters come from the SearchQuery json not the request + * @param webScriptRequest + * @param searchQuery + * @return Params + */ + protected Params getParams(WebScriptRequest webScriptRequest, List fields, List include, Paging paging) + { + if (paging == null) + { + paging = Paging.DEFAULT; + } + BeanPropertiesFilter filter = null; + if (fields != null && !fields.isEmpty()) + { + List selectList = new ArrayList<>(fields.size()); + selectList.addAll(fields); + + if (include != null && !include.isEmpty()) + { + selectList.addAll(include); + } + + filter = getFilter("", selectList); + } + + Params.RecognizedParams recognizedParams = new Params.RecognizedParams(null, paging, filter, null, include, null, null, null, false); + return Params.valueOf(null, recognizedParams, null, webScriptRequest); + } + + public void setSearchMapper(SearchMapper searchMapper) + { + this.searchMapper = searchMapper; + } + + public void setResultMapper(ResultMapper resultMapper) + { + this.resultMapper = resultMapper; + } + + public void setAssistant(ApiAssistant assistant) { + this.assistant = assistant; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) { + this.serviceRegistry = serviceRegistry; + } + + public void setHelper(ResourceWebScriptHelper helper) + { + this.helper = helper; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/SearchSQLApiWebscript.java b/remote-api/src/main/java/org/alfresco/rest/api/search/SearchSQLApiWebscript.java new file mode 100644 index 00000000000..b784d6c052b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/SearchSQLApiWebscript.java @@ -0,0 +1,181 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search; +import java.io.IOException; +import java.util.Locale; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.search.impl.lucene.LuceneQueryParserException; +import org.alfresco.repo.search.impl.solr.SolrSQLJSONResultSet; +import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSet; +import org.alfresco.rest.api.search.impl.ResultMapper; +import org.alfresco.rest.api.search.impl.SearchMapper; +import org.alfresco.rest.api.search.model.SearchSQLQuery; +import org.alfresco.rest.api.search.model.TupleList; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.tools.ApiAssistant; +import org.alfresco.rest.framework.tools.RecognizedParamsExtractor; +import org.alfresco.rest.framework.tools.RequestReader; +import org.alfresco.rest.framework.tools.ResponseWriter; +import org.alfresco.rest.framework.webscripts.ResourceWebScriptHelper; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.util.ParameterCheck; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + + +/** + * Search SQL API end point. + * An implementation of the {{baseUrl}}/{{networkId}}/public/search/versions/1/sql + * @author Michael Suzuki + * + */ +public class SearchSQLApiWebscript extends AbstractWebScript implements RecognizedParamsExtractor, + RequestReader, + ResponseWriter, + InitializingBean +{ + private ServiceRegistry serviceRegistry; + private SearchService searchService; + private SearchMapper searchMapper; + private ResultMapper resultMapper; + protected ApiAssistant assistant; + protected ResourceWebScriptHelper helper; + + @Override + public void execute(WebScriptRequest webScriptRequest, WebScriptResponse res) throws IOException + { + try + { + //Turn JSON into a Java object representation + SearchSQLQuery searchQuery = extractJsonContent(webScriptRequest, assistant.getJsonHelper(), SearchSQLQuery.class); + SearchParameters sparams = buildSearchParameters(searchQuery); + + ResultSet results = searchService.query(sparams); + FilteringResultSet frs = (FilteringResultSet) results; + SolrSQLJSONResultSet ssjr = (SolrSQLJSONResultSet) frs.getUnFilteredResultSet(); + //When solr format is requested pass the solr output directly. + if(searchQuery.getFormat().equalsIgnoreCase("solr")) + { + res.getWriter().write(ssjr.getSolrResponse()); + } + else + { + CollectionWithPagingInfo nodes = resultMapper.toCollectionWithPagingInfo(ssjr.getDocs(), searchQuery); + renderJsonResponse(res, nodes, assistant.getJsonHelper()); + } + setResponse(res, DEFAULT_SUCCESS); + } + catch (Exception exception) + { + if (exception instanceof LuceneQueryParserException) + { + renderException(exception,res,assistant); + } + else + { + renderException(new WebScriptException(400, exception.getMessage()), res, assistant); + } + } + } + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry); + this.searchService = serviceRegistry.getSearchService(); + ParameterCheck.mandatory("assistant", this.assistant); + ParameterCheck.mandatory("searchMapper", this.searchMapper); + ParameterCheck.mandatory("resultMapper", this.resultMapper); + } + + public SearchParameters buildSearchParameters(SearchSQLQuery searchQuery) + { + SearchParameters sparams = new SearchParameters(); + sparams.setLanguage(SearchService.LANGUAGE_SOLR_SQL); + sparams.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + if(StringUtils.isEmpty(searchQuery.getStmt())) + { + throw new AlfrescoRuntimeException("Required stmt parameter is missing."); + } + if(searchQuery.getFormat().equalsIgnoreCase("solr")) + { + sparams.addExtraParameter("format", "solr"); + } + if(!StringUtils.isEmpty(searchQuery.getTimezone())) + { + sparams.setTimezone(searchQuery.getTimezone()); + } + sparams.setQuery(searchQuery.getStmt()); + searchQuery.getLocales().forEach(action->{ + Locale locale = new Locale(action); + sparams.addLocale(locale); + }); + searchQuery.getFilterQueries().forEach(sparams::addFilterQuery); + + sparams.setIncludeMetadata(searchQuery.isIncludeMetadata()); + return sparams; + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setSearchMapper(SearchMapper searchMapper) + { + this.searchMapper = searchMapper; + } + + public void setResultMapper(ResultMapper resultMapper) + { + this.resultMapper = resultMapper; + } + + public void setAssistant(ApiAssistant assistant) + { + this.assistant = assistant; + } + + public void setHelper(ResourceWebScriptHelper helper) + { + this.helper = helper; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/context/FacetFieldContext.java b/remote-api/src/main/java/org/alfresco/rest/api/search/context/FacetFieldContext.java new file mode 100644 index 00000000000..82c3f3fac8b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/context/FacetFieldContext.java @@ -0,0 +1,137 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search.context; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.GenericBucket; + +/** + * The results of a Field Faceting + */ +public class FacetFieldContext +{ + private final String label; + private final List buckets; + + public FacetFieldContext(String label, List buckets) + { + this.label = label; + this.buckets = buckets; + } + + public String getLabel() + { + return label; + } + + public List getBuckets() + { + return buckets; + } + + public static class Bucket + { + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String label; + private final String filterQuery; + private final int count; + private final Object display; + + public Bucket(String label, String filterQuery, int count, Object display) + { + this.label = label; + this.filterQuery = filterQuery; + this.count = count; + this.display = display; + } + + public String getFilterQuery() + { + return filterQuery; + } + + public Object getDisplay() + { + return display; + } + + public String getLabel() + { + return label; + } + + public int getCount() + { + return count; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Bucket bucket = (Bucket) o; + + if (count != bucket.count) + return false; + if (label != null ? !label.equals(bucket.label) : bucket.label != null) + return false; + if (filterQuery != null ? !filterQuery.equals(bucket.filterQuery) : bucket.filterQuery != null) + return false; + if (display != null ? !display.equals(bucket.display) : bucket.display != null) + return false; + + return true; + } + + @Override + public int hashCode() + { + int result = label != null ? label.hashCode() : 0; + result = 31 * result + (filterQuery != null ? filterQuery.hashCode() : 0); + result = 31 * result + count; + result = 31 * result + (display != null ? display.hashCode() : 0); + return result; + } + + @Override + public String toString() + { + return "Bucket{" + + "label='" + label + '\'' + + ", filterQuery='" + filterQuery + '\'' + + ", count=" + count + + ", display=" + display + + '}'; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/context/FacetQueryContext.java b/remote-api/src/main/java/org/alfresco/rest/api/search/context/FacetQueryContext.java new file mode 100644 index 00000000000..528f61b55fd --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/context/FacetQueryContext.java @@ -0,0 +1,97 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search.context; + +/** + * The results of a facet query + */ +public class FacetQueryContext +{ + private final String label; + private final String filterQuery; + private final int count; + + public FacetQueryContext(String label, String filterQuery, int count) + { + this.label = label; + this.filterQuery = filterQuery; + this.count = count; + } + + public String getFilterQuery() + { + return filterQuery; + } + + public String getLabel() + { + return label; + } + + public int getCount() + { + return count; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + FacetQueryContext that = (FacetQueryContext) o; + + if (count != that.count) + return false; + if (label != null ? !label.equals(that.label) : that.label != null) + return false; + if (filterQuery != null ? !filterQuery.equals(that.filterQuery) : that.filterQuery != null) + return false; + + return true; + } + + @Override + public int hashCode() + { + int result = label != null ? label.hashCode() : 0; + result = 31 * result + (filterQuery != null ? filterQuery.hashCode() : 0); + result = 31 * result + count; + return result; + } + + @Override + public String toString() + { + return "FacetQueryContext{" + + "label='" + label + '\'' + + ", filterQuery='" + filterQuery + '\'' + + ", count=" + count + + '}'; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/context/SearchContext.java b/remote-api/src/main/java/org/alfresco/rest/api/search/context/SearchContext.java new file mode 100644 index 00000000000..f5bb7d0122d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/context/SearchContext.java @@ -0,0 +1,107 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search.context; + +import java.util.List; + +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.GenericFacetResponse; +import org.alfresco.rest.api.search.model.SearchQuery; + +/** + * The contextual results of a Search + */ +public class SearchContext +{ + private final Consistency consistency; + private final List facetQueries; + private final SpellCheckContext spellCheck; + private final List facetsFields; + private final List facets; + private final SearchQuery request; + + public SearchContext(long lastTxId, List facets, List facetResults, List facetsFields, SpellCheckContext spellCheck, + SearchQuery request) + { + this.spellCheck = spellCheck; + this.request = request; + if (lastTxId > 0) + { + consistency = new Consistency(lastTxId); + } + else + { + consistency = null; + } + this.facetQueries = facetResults; + this.facetsFields = facetsFields; + this.facets = facets; + } + + public Consistency getConsistency() + { + return consistency; + } + + public List getFacetQueries() + { + return facetQueries; + } + + public SpellCheckContext getSpellCheck() + { + return spellCheck; + } + + public List getFacets() + { + return facets; + } + + public List getFacetsFields() + { + return facetsFields; + } + + public SearchQuery getRequest() + { + return request; + } + + public class Consistency + { + private final long lastTxId; + + public Consistency(long lastTxId) + { + this.lastTxId = lastTxId; + } + + public long getlastTxId() + { + return lastTxId; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/context/SearchRequestContext.java b/remote-api/src/main/java/org/alfresco/rest/api/search/context/SearchRequestContext.java new file mode 100644 index 00000000000..4706b22915c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/context/SearchRequestContext.java @@ -0,0 +1,80 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search.context; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.rest.api.search.model.Query; +import org.alfresco.rest.api.search.model.SearchQuery; +import org.alfresco.service.cmr.search.FacetFormat; + +/** + * This is a snapshot of the SearchQuery before the request is made. + * It isn't a complete copy of SearchQuery but only has fields that are useful when building + * the response + */ +public class SearchRequestContext +{ + + private final Query query; + private final boolean includeRequest; + private final Map pivotKeys; + private final Set stores; + + private SearchRequestContext(Query query, boolean includeRequest) + { + this.query = query; + this.includeRequest = includeRequest; + this.pivotKeys = new HashMap<>(); + this.stores = new HashSet<>(); + } + + public static final SearchRequestContext from(SearchQuery searchQuery) + { + return new SearchRequestContext(searchQuery.getQuery(), searchQuery.includeRequest()); + } + + public Query getQuery() + { + return query; + } + public boolean includeRequest() + { + return includeRequest; + } + + public Map getPivotKeys() + { + return pivotKeys; + } + public Set getStores() + { + return stores; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/context/SpellCheckContext.java b/remote-api/src/main/java/org/alfresco/rest/api/search/context/SpellCheckContext.java new file mode 100644 index 00000000000..2ecde4907ff --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/context/SpellCheckContext.java @@ -0,0 +1,53 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ +package org.alfresco.rest.api.search.context; + +import java.util.List; + +/** + * The results of a SpellCheck + */ +public class SpellCheckContext +{ + private final String type; + private final List suggestions; + + public SpellCheckContext(String type, List suggestions) + { + this.type = type; + this.suggestions = suggestions; + } + + public String getType() + { + return type; + } + + public List getSuggestions() + { + return suggestions; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/impl/ResultMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/search/impl/ResultMapper.java new file mode 100644 index 00000000000..6ebba523bd4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/impl/ResultMapper.java @@ -0,0 +1,672 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.search.impl; + +import static org.alfresco.rest.api.search.impl.StoreMapper.DELETED; +import static org.alfresco.rest.api.search.impl.StoreMapper.HISTORY; +import static org.alfresco.rest.api.search.impl.StoreMapper.LIVE_NODES; +import static org.alfresco.rest.api.search.impl.StoreMapper.VERSIONS; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.alfresco.repo.search.impl.lucene.SolrJSONResultSet; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.GenericBucket; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.GenericFacetResponse; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.GenericFacetResponse.FACET_TYPE; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.Metric; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.Metric.METRIC_TYPE; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.RangeResultMapper; +import org.alfresco.repo.search.impl.solr.facet.facetsresponse.SimpleMetric; +import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSet; +import org.alfresco.repo.version.Version2Model; +import org.alfresco.rest.api.DeletedNodes; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.lookups.PropertyLookupRegistry; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.api.nodes.NodeVersionsRelation; +import org.alfresco.rest.api.search.context.FacetFieldContext; +import org.alfresco.rest.api.search.context.FacetFieldContext.Bucket; +import org.alfresco.rest.api.search.context.FacetQueryContext; +import org.alfresco.rest.api.search.context.SearchContext; +import org.alfresco.rest.api.search.context.SearchRequestContext; +import org.alfresco.rest.api.search.context.SpellCheckContext; +import org.alfresco.rest.api.search.model.FacetField; +import org.alfresco.rest.api.search.model.FacetQuery; +import org.alfresco.rest.api.search.model.HighlightEntry; +import org.alfresco.rest.api.search.model.SearchEntry; +import org.alfresco.rest.api.search.model.SearchQuery; +import org.alfresco.rest.api.search.model.SearchSQLQuery; +import org.alfresco.rest.api.search.model.TupleEntry; +import org.alfresco.rest.api.search.model.TupleList; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.search.FacetFormat; +import org.alfresco.service.cmr.search.Interval; +import org.alfresco.service.cmr.search.IntervalSet; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SpellCheckResult; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Maps from a ResultSet to a json public api representation. + * + * @author Gethin James + */ +public class ResultMapper +{ + private ServiceRegistry serviceRegistry; + private Nodes nodes; + private NodeVersionsRelation nodeVersions; + private PropertyLookupRegistry propertyLookup; + private StoreMapper storeMapper; + private DeletedNodes deletedNodes; + private static Log logger = LogFactory.getLog(ResultMapper.class); + + public ResultMapper() + { + } + + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + public void setNodeVersions(NodeVersionsRelation nodeVersions) + { + this.nodeVersions = nodeVersions; + } + + public void setDeletedNodes(DeletedNodes deletedNodes) + { + this.deletedNodes = deletedNodes; + } + + public void setStoreMapper(StoreMapper storeMapper) + { + this.storeMapper = storeMapper; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setPropertyLookup(PropertyLookupRegistry propertyLookup) + { + this.propertyLookup = propertyLookup; + } + + /** + * Turns the results into a CollectionWithPagingInfo + * @param params + * @param searchQuery + *@param results @return CollectionWithPagingInfo + */ + public CollectionWithPagingInfo toCollectionWithPagingInfo(Params params, SearchRequestContext searchRequestContext, SearchQuery searchQuery, ResultSet results) + { + SearchContext context = null; + Integer total = null; + List noderesults = new ArrayList(); + Map mapUserInfo = new HashMap<>(10); + Map>>> hightLighting = results.getHighlighting(); + int notFound = 0; + boolean isHistory = searchRequestContext.getStores().contains(StoreMapper.HISTORY); + + for (ResultSetRow row:results) + { + Node aNode = getNode(row, params, mapUserInfo, isHistory); + + if (aNode != null) + { + float f = row.getScore(); + List highlightEntries = null; + List>> high = hightLighting.get(row.getNodeRef()); + + if (high != null && !high.isEmpty()) + { + highlightEntries = new ArrayList(high.size()); + for (Pair> highlight:high) + { + highlightEntries.add(new HighlightEntry(highlight.getFirst(), highlight.getSecond())); + } + } + aNode.setSearch(new SearchEntry(f, highlightEntries)); + noderesults.add(aNode); + } + else + { + logger.debug("Unknown noderef returned from search results "+row.getNodeRef()); + notFound++; + } + } + + SolrJSONResultSet solrResultSet = findSolrResultSet(results); + + if (solrResultSet != null) + { + //We used Solr for this query + context = toSearchContext(solrResultSet, searchRequestContext, searchQuery, notFound); + } + + total = setTotal(results); + + return CollectionWithPagingInfo.asPaged(params.getPaging(), noderesults, results.hasMore(), total, null, context); + } + + /** + * Builds a node representation based on a ResultSetRow; + * @param searchRequestContext + * @param aRow + * @param params + * @param mapUserInfo + * @param isHistory + * @return Node + */ + public Node getNode(ResultSetRow aRow, Params params, Map mapUserInfo, boolean isHistory) + { + String nodeStore = storeMapper.getStore(aRow.getNodeRef()); + if (isHistory) nodeStore = HISTORY; + Node aNode = null; + + switch (nodeStore) + { + case LIVE_NODES: + aNode = nodes.getFolderOrDocument(aRow.getNodeRef(), null, null, params.getInclude(), mapUserInfo); + break; + case HISTORY: + aNode = nodes.getFolderOrDocument(aRow.getNodeRef(), null, null, params.getInclude(), mapUserInfo); + break; + case VERSIONS: + Map properties = serviceRegistry.getNodeService().getProperties(aRow.getNodeRef()); + NodeRef frozenNodeRef = ((NodeRef) properties.get(Version2Model.PROP_QNAME_FROZEN_NODE_REF)); + String versionLabelId = (String) properties.get(Version2Model.PROP_QNAME_VERSION_LABEL); + Version v = null; + try + { + if (frozenNodeRef != null && versionLabelId != null) + { + v = nodeVersions.findVersion(frozenNodeRef.getId(),versionLabelId); + aNode = nodes.getFolderOrDocument(v.getFrozenStateNodeRef(), null, null, params.getInclude(), mapUserInfo); + } + } + catch (EntityNotFoundException|InvalidNodeRefException e) + { + //Solr says there is a node but we can't find it + logger.debug("Failed to find a versioned node with id of "+frozenNodeRef + + " this is probably because the original node has been deleted."); + } + + if (v != null && aNode != null) + { + nodeVersions.mapVersionInfo(v, aNode, aRow.getNodeRef()); + aNode.setNodeId(frozenNodeRef.getId()); + aNode.setVersionLabel(versionLabelId); + } + break; + case DELETED: + try + { + aNode = deletedNodes.getDeletedNode(aRow.getNodeRef().getId(), params, false, mapUserInfo); + } + catch (EntityNotFoundException enfe) + { + //Solr says there is a deleted node but we can't find it, we want the rest of the search to return so lets ignore it. + logger.debug("Failed to find a deleted node with id of "+aRow.getNodeRef().getId()); + } + break; + } + if (aNode != null) + { + aNode.setLocation(nodeStore); + } + return aNode; + } + + /** + * Sets the total number found. + * @param results + * @return An integer total + */ + public Integer setTotal(ResultSet results) + { + Long totalItems = results.getNumberFound(); + Integer total = totalItems.intValue(); + return total; + } + + /** + * Uses the results from Solr to set the Search Context + * @param SolrJSONResultSet + * @param searchQuery + * @return SearchContext + */ + public SearchContext toSearchContext(SolrJSONResultSet solrResultSet, SearchRequestContext searchRequestContext, SearchQuery searchQuery, int notFound) + { + SearchContext context = null; + Map facetQueries = solrResultSet.getFacetQueries(); + List facets = new ArrayList<>(); + List facetResults = null; + SpellCheckContext spellCheckContext = null; + List ffcs = new ArrayList(); + + if (searchQuery == null) + { + throw new IllegalArgumentException("searchQuery can't be null"); + } + + //Facet queries + if(facetQueries!= null && !facetQueries.isEmpty()) + { + //If group by field populated in query facet return bucketing into facet field. + List facetQueryForFields = getFacetBucketsFromFacetQueries(facetQueries,searchQuery); + if(hasGroup(searchQuery) || FacetFormat.V2 == searchQuery.getFacetFormat()) + { + facets.addAll(facetQueryForFields); + } + else + { + // Return the old way facet query with no bucketing. + facetResults = new ArrayList<>(facetQueries.size()); + for (Entry fq:facetQueries.entrySet()) + { + String filterQuery = null; + if (searchQuery.getFacetQueries() != null) + { + Optional found = searchQuery.getFacetQueries().stream().filter(facetQuery -> fq.getKey().equals(facetQuery.getLabel())).findFirst(); + filterQuery = found.isPresent()? found.get().getQuery():fq.getKey(); + } + facetResults.add(new FacetQueryContext(fq.getKey(), filterQuery, fq.getValue())); + } + } + } + + //Field Facets + Map>> facetFields = solrResultSet.getFieldFacets(); + if(FacetFormat.V2 == searchQuery.getFacetFormat()) + { + facets.addAll(getFacetBucketsForFacetFieldsAsFacets(facetFields, searchQuery)); + } + else + { + ffcs.addAll(getFacetBucketsForFacetFields(facetFields, searchQuery)); + } + + Map>> facetInterval = solrResultSet.getFacetIntervals(); + facets.addAll(getGenericFacetsForIntervals(facetInterval, searchQuery)); + + Map>> facetRanges = solrResultSet.getFacetRanges(); + facets.addAll(RangeResultMapper.getGenericFacetsForRanges(facetRanges, searchQuery.getFacetRanges())); + + List stats = getFieldStats(searchRequestContext, solrResultSet.getStats()); + List pimped = getPivots(searchRequestContext, solrResultSet.getPivotFacets(), stats); + facets.addAll(pimped); + facets.addAll(stats); + + //Spelling + SpellCheckResult spell = solrResultSet.getSpellCheckResult(); + if (spell != null && spell.getResultName() != null && !spell.getResults().isEmpty()) + { + spellCheckContext = new SpellCheckContext(spell.getResultName(),spell.getResults()); + } + + //Put it all together + context = new SearchContext(solrResultSet.getLastIndexedTxId(), facets, facetResults, ffcs, spellCheckContext, searchRequestContext.includeRequest()?searchQuery:null); + return isNullContext(context)?null:context; + } + public static boolean hasGroup(SearchQuery searchQuery) + { + if(searchQuery != null && searchQuery.getFacetQueries() != null) + { + return searchQuery.getFacetQueries().stream().anyMatch(facetQ -> facetQ.getGroup() != null); + } + return false; + } + /** + * Builds a facet field from facet queries. + * @param facetQueries + * @return + */ + protected List getFacetBucketsFromFacetQueries(Map facetQueries, SearchQuery searchQuery) + { + List facetResults = new ArrayList(); + Map> groups = new HashMap<>(); + + for (Entry fq:facetQueries.entrySet()) + { + String group = null; + String filterQuery = null; + if (searchQuery != null && searchQuery.getFacetQueries() != null) + { + Optional found = searchQuery.getFacetQueries().stream().filter(facetQuery -> fq.getKey().equals(facetQuery.getLabel())).findFirst(); + filterQuery = found.isPresent()? found.get().getQuery():fq.getKey(); + if(found.isPresent() && found.get().getGroup() != null) + { + group = found.get().getGroup(); + } + } +// if(group != null && !group.isEmpty() || FacetFormat.V2 == searchQuery.getFacetFormat()) +// { + if(groups.containsKey(group)) + { + Set metrics = new HashSet<>(1); + metrics.add(new SimpleMetric(METRIC_TYPE.count, fq.getValue())); + groups.get(group).add(new GenericBucket(fq.getKey(), filterQuery, null,metrics, null)); + } + else + { + List l = new ArrayList(); + Set metrics = new HashSet<>(1); + metrics.add(new SimpleMetric(METRIC_TYPE.count, fq.getValue())); + l.add(new GenericBucket(fq.getKey(),filterQuery, null, metrics, null)); + groups.put(group, l); + } + } +// } + if(!groups.isEmpty()) + { + groups.forEach((a,v) -> facetResults.add(new GenericFacetResponse(FACET_TYPE.query, a, v))); + } + return facetResults; + } + + protected List getFieldStats(SearchRequestContext searchRequestContext, Map> stats) + { + if(stats != null && !stats.isEmpty()) + { + return stats.entrySet().stream().map(statsFieldEntry -> { + return new GenericFacetResponse(FACET_TYPE.stats, statsFieldEntry.getKey(), + Arrays.asList(new GenericBucket(null,null, null, + statsFieldEntry.getValue(), null)) ); + } + ).collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + + protected List getPivots(SearchRequestContext searchRequest, List pivots, + List stats) + { + if(pivots != null && !pivots.isEmpty()) + { + Map pivotKeys = searchRequest.getPivotKeys(); + + return pivots.stream().map(aFacet -> { + + String pivotLabel = pivotKeys.containsKey(aFacet.getLabel())?pivotKeys.get(aFacet.getLabel()):aFacet.getLabel(); + + //can reference, facetfield, the last one can be rangefacet, facetquery or stats + List bucks = new ArrayList<>(); + Optional foundStat = stats.stream().filter( + aStat -> aStat.getLabel().equals(pivotLabel)).findFirst(); + if (foundStat.isPresent()) + { + bucks.add(foundStat.get().getBuckets().get(0)); + stats.remove(foundStat.get()); + } + bucks.addAll(aFacet.getBuckets().stream().map(genericBucket -> { + Object display = propertyLookup.lookup(aFacet.getLabel(), genericBucket.getLabel()); + return new GenericBucket(genericBucket.getLabel(), genericBucket.getFilterQuery(), + display,genericBucket.getMetrics(), getPivots(searchRequest, genericBucket.getFacets(), stats)); + }).collect(Collectors.toList())); + + return new GenericFacetResponse(aFacet.getType(), pivotLabel, bucks); + }).collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + protected List getFacetBucketsForFacetFieldsAsFacets(Map>> facetFields, SearchQuery searchQuery) + { + if (facetFields != null && !facetFields.isEmpty()) + { + List ffcs = new ArrayList<>(facetFields.size()); + for (Entry>> facet:facetFields.entrySet()) + { + if (facet.getValue() != null && !facet.getValue().isEmpty()) + { + List buckets = new ArrayList<>(facet.getValue().size()); + for (Pair buck:facet.getValue()) + { + Object display = null; + String filterQuery = null; + if (searchQuery != null + && searchQuery.getFacetFields() != null + && searchQuery.getFacetFields().getFacets() != null + && !searchQuery.getFacetFields().getFacets().isEmpty()) + { + Optional found = searchQuery.getFacetFields().getFacets().stream().filter( + queryable -> facet.getKey().equals(queryable.getLabel()!=null?queryable.getLabel():queryable.getField())).findFirst(); + if (found.isPresent()) + { + display = propertyLookup.lookup(found.get().getField(), buck.getFirst()); + String fq = found.get().toFilterQuery(buck.getFirst()); + if (fq != null) + { + filterQuery = fq; + } + } + } + GenericBucket bucket = new GenericBucket(buck.getFirst(), filterQuery, display, new HashSet(Arrays.asList(new SimpleMetric(METRIC_TYPE.count,String.valueOf(buck.getSecond())))), null, null); + buckets.add(bucket); + } + ffcs.add(new GenericFacetResponse(FACET_TYPE.field,facet.getKey(), buckets)); + } + } + return ffcs; + } + return Collections.emptyList(); + } + protected List getFacetBucketsForFacetFields(Map>> facetFields, SearchQuery searchQuery) + { + if (facetFields != null && !facetFields.isEmpty()) + { + List ffcs = new ArrayList<>(facetFields.size()); + for (Entry>> facet:facetFields.entrySet()) + { + if (facet.getValue() != null && !facet.getValue().isEmpty()) + { + List buckets = new ArrayList<>(facet.getValue().size()); + for (Pair buck:facet.getValue()) + { + Object display = null; + String filterQuery = null; + if (searchQuery != null + && searchQuery.getFacetFields() != null + && searchQuery.getFacetFields().getFacets() != null + && !searchQuery.getFacetFields().getFacets().isEmpty()) + { + Optional found = searchQuery.getFacetFields().getFacets().stream().filter( + queryable -> facet.getKey().equals(queryable.getLabel()!=null?queryable.getLabel():queryable.getField())).findFirst(); + if (found.isPresent()) + { + display = propertyLookup.lookup(found.get().getField(), buck.getFirst()); + String fq = found.get().toFilterQuery(buck.getFirst()); + if (fq != null) + { + filterQuery = fq; + } + } + } + buckets.add(new Bucket(buck.getFirst(), filterQuery,buck.getSecond(),display)); + } + ffcs.add(new FacetFieldContext(facet.getKey(), buckets)); + } + } + + return ffcs; + } + return Collections.emptyList(); + } + /** + * Returns generic faceting responses for Intervals + * @param facetFields + * @param searchQuery + * @return GenericFacetResponse + */ + protected static List getGenericFacetsForIntervals(Map>> facetFields, SearchQuery searchQuery) + { + if (facetFields != null && !facetFields.isEmpty()) + { + List ffcs = new ArrayList<>(facetFields.size()); + for (Entry>> facet:facetFields.entrySet()) + { + if (facet.getValue() != null && !facet.getValue().isEmpty()) + { + List buckets = new ArrayList<>(facet.getValue().size()); + for (Pair buck:facet.getValue()) + { + String filterQuery = null; + Map bucketInfo = new HashMap<>(); + + if (searchQuery != null + && searchQuery.getFacetIntervals() != null + && searchQuery.getFacetIntervals().getIntervals() != null + && !searchQuery.getFacetIntervals().getIntervals().isEmpty()) + { + Optional found = searchQuery.getFacetIntervals().getIntervals().stream().filter( + interval -> facet.getKey().equals(interval.getLabel()!=null?interval.getLabel():interval.getField())).findFirst(); + if (found.isPresent()) + { + if (found.get().getSets() != null) + { + Optional foundSet = found.get().getSets().stream().filter(aSet -> buck.getFirst().equals(aSet.getLabel())).findFirst(); + if (foundSet.isPresent()) + { + filterQuery = found.get().getField() + ":" + foundSet.get().toAFTSQuery(); + bucketInfo.put(GenericFacetResponse.START, foundSet.get().getStart()); + bucketInfo.put(GenericFacetResponse.END, foundSet.get().getEnd()); + bucketInfo.put(GenericFacetResponse.START_INC, String.valueOf(foundSet.get().isStartInclusive())); + bucketInfo.put(GenericFacetResponse.END_INC, String.valueOf(foundSet.get().isEndInclusive())); + } + } + } + } + GenericBucket bucket = new GenericBucket(buck.getFirst(), filterQuery, null , new HashSet(Arrays.asList(new SimpleMetric(METRIC_TYPE.count,String.valueOf(buck.getSecond())))), null, bucketInfo); + buckets.add(bucket); + } + ffcs.add(new GenericFacetResponse(FACET_TYPE.interval, facet.getKey(), buckets)); + } + } + + return ffcs; + } + return Collections.emptyList(); + } + + /** + * Is the context null? + * @param context + * @return true if its null + */ + public boolean isNullContext(SearchContext context) + { + return (context.getFacetQueries() == null + && context.getConsistency() == null + && context.getSpellCheck() == null + && context.getFacetsFields() == null + && context.getFacets() == null); + } + + /** + * Gets SolrJSONResultSet class if there is one. + * @param results + * @return + */ + protected SolrJSONResultSet findSolrResultSet(ResultSet results) + { + ResultSet theResultSet = results; + + if (results instanceof FilteringResultSet) + { + theResultSet = ((FilteringResultSet) results).getUnFilteredResultSet(); + } + + if (theResultSet instanceof SolrJSONResultSet) + { + return (SolrJSONResultSet) theResultSet; + } + + return null; + } + public CollectionWithPagingInfo toCollectionWithPagingInfo(JSONArray docs, SearchSQLQuery searchQuery) throws JSONException + { + if(docs == null ) + { + throw new RuntimeException("Solr response is required instead of JSONArray docs was null" ); + } + if(searchQuery == null ) + { + throw new RuntimeException("SearchSQLQuery is required" ); + } + List entries = new ArrayList(); + for(int i = 0; i < docs.length() -1; i++) + { + List row = new ArrayList(); + JSONObject docObj = (JSONObject) docs.get(i); + docObj.keys().forEachRemaining(action -> { + try + { + String value = docObj.get(action.toString()).toString(); + row.add(new TupleEntry(action.toString(), value)); + } + catch (JSONException e) + { + throw new RuntimeException("Unable to parse SQL response. " + e); + } + }); + entries.add(new TupleList(row)); + } + Paging paging = Paging.valueOf(0, searchQuery.getItemLimit()); + return CollectionWithPagingInfo.asPaged(paging, entries); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/search/impl/SearchMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/search/impl/SearchMapper.java new file mode 100644 index 00000000000..2df9fe4e0a5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/search/impl/SearchMapper.java @@ -0,0 +1,833 @@ +/*- + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * 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 . + * #L% + */ + +package org.alfresco.rest.api.search.impl; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_ALLOWABLEOPERATIONS; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_ASPECTNAMES; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_ASSOCIATION; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_ISLINK; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_ISLOCKED; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_PATH; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_PERMISSIONS; +import static org.alfresco.rest.api.Nodes.PARAM_INCLUDE_PROPERTIES; +import static org.alfresco.service.cmr.search.SearchService.LANGUAGE_CMIS_ALFRESCO; +import static org.alfresco.service.cmr.search.SearchService.LANGUAGE_FTS_ALFRESCO; +import static org.alfresco.service.cmr.search.SearchService.LANGUAGE_LUCENE; +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.search.impl.lucene.LuceneQueryLanguageSPI; +import org.alfresco.rest.api.search.context.SearchRequestContext; +import org.alfresco.rest.api.search.model.Default; +import org.alfresco.rest.api.search.model.FacetField; +import org.alfresco.rest.api.search.model.FacetFields; +import org.alfresco.rest.api.search.model.FacetQuery; +import org.alfresco.rest.api.search.model.FilterQuery; +import org.alfresco.rest.api.search.model.Limits; +import org.alfresco.rest.api.search.model.Localization; +import org.alfresco.rest.api.search.model.Pivot; +import org.alfresco.rest.api.search.model.Query; +import org.alfresco.rest.api.search.model.Scope; +import org.alfresco.rest.api.search.model.SearchQuery; +import org.alfresco.rest.api.search.model.SortDef; +import org.alfresco.rest.api.search.model.Spelling; +import org.alfresco.rest.api.search.model.Template; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.rest.framework.resource.parameters.Params; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.GeneralHighlightParameters; +import org.alfresco.service.cmr.search.Interval; +import org.alfresco.service.cmr.search.IntervalParameters; +import org.alfresco.service.cmr.search.IntervalSet; +import org.alfresco.service.cmr.search.LimitBy; +import org.alfresco.service.cmr.search.RangeParameters; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchParameters.FieldFacet; +import org.alfresco.service.cmr.search.SearchParameters.FieldFacetMethod; +import org.alfresco.service.cmr.search.SearchParameters.FieldFacetSort; +import org.alfresco.service.cmr.search.SearchParameters.Operator; +import org.alfresco.service.cmr.search.SearchParameters.SortDefinition; +import org.alfresco.service.cmr.search.SearchParameters.SortDefinition.SortType; +import org.alfresco.service.cmr.search.StatsRequestParameters; +import org.alfresco.util.ParameterCheck; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TimeZone; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +/** + * Maps from a json request and a solr SearchParameters object. + * + * @author Gethin James + */ +public class SearchMapper +{ + public static final List PERMITTED_INCLUDES + = Arrays.asList(PARAM_INCLUDE_ALLOWABLEOPERATIONS,PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_ISLINK, PARAM_INCLUDE_PATH, PARAM_INCLUDE_PROPERTIES, + PARAM_INCLUDE_ASSOCIATION, PARAM_INCLUDE_ISLOCKED, PARAM_INCLUDE_PERMISSIONS); + + public static final String CMIS = "cmis"; + public static final String LUCENE = "lucene"; + public static final String AFTS = "afts"; + private StoreMapper storeMapper; + + /** + * Turn the SearchQuery params serialized by Jackson into the Java SearchParameters object + * @param params + * @return SearchParameters + */ + public SearchParameters toSearchParameters(Params params, SearchQuery searchQuery, SearchRequestContext searchRequestContext) + { + ParameterCheck.mandatory("query", searchQuery.getQuery()); + + SearchParameters sp = new SearchParameters(); + setDefaults(sp); + fromLocalization(sp, searchQuery.getLocalization()); + fromQuery(sp, searchQuery.getQuery()); + fromPaging(sp, params.getPaging()); + fromSort(sp, searchQuery.getSort()); + fromTemplate(sp, searchQuery.getTemplates()); + validateInclude(searchQuery.getInclude()); + fromDefault(sp, searchQuery.getDefaults()); + fromFilterQuery(sp, searchQuery.getFilterQueries()); + fromFacetQuery(sp, searchQuery.getFacetQueries()); + fromPivot(sp, searchQuery.getStats(), searchQuery.getFacetFields(), searchQuery.getFacetRanges(), searchQuery.getPivots(), searchRequestContext); + fromStats(sp, searchQuery.getStats()); + fromFacetFields(sp, searchQuery.getFacetFields()); + fromSpellCheck(sp, searchQuery.getSpellcheck()); + fromHighlight(sp, searchQuery.getHighlight()); + fromFacetIntervals(sp, searchQuery.getFacetIntervals()); + fromRange(sp, searchQuery.getFacetRanges()); + fromScope(sp, searchQuery.getScope(), searchRequestContext); + fromLimits(sp, searchQuery.getLimits()); + return sp; + } + + /** + * Sets the API defaults + * @param sp + */ + public void setDefaults(SearchParameters sp) + { + //Hardcode workspace store + sp.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + } + + /** + * SearchParameters from the Query object + * @param sp SearchParameters + * @param q Query + */ + public void fromQuery(SearchParameters sp, Query q) + { + ParameterCheck.mandatoryString("query", q.getQuery()); + String lang = q.getLanguage()==null?AFTS:q.getLanguage(); + + switch (lang.toLowerCase()) + { + case AFTS: + sp.setLanguage(LANGUAGE_FTS_ALFRESCO); + break; + case LUCENE: + sp.setLanguage(LANGUAGE_LUCENE); + break; + case CMIS: + sp.setLanguage(LANGUAGE_CMIS_ALFRESCO); + break; + default: + throw new InvalidArgumentException(InvalidArgumentException.DEFAULT_MESSAGE_ID, + new Object[] { ": language allowed values: afts,lucene,cmis" }); + } + sp.setQuery(q.getQuery()); + sp.setSearchTerm(q.getUserQuery()); + } + + /** + * SearchParameters from the Paging object + * @param sp SearchParameters + * @param paging Paging + */ + public void fromPaging(SearchParameters sp, Paging paging) + { + if (paging != null) + { + sp.setLimitBy(LimitBy.FINAL_SIZE); + sp.setLimit(paging.getMaxItems()); + sp.setSkipCount(paging.getSkipCount()); + } + } + + /** + * SearchParameters from List + * @param sp SearchParameters + * @param sort List + */ + public void fromSort(SearchParameters sp, List sort) + { + if (sort != null && !sort.isEmpty()) + { + if (LANGUAGE_CMIS_ALFRESCO.equals(sp.getLanguage())) + { + throw new InvalidArgumentException(InvalidArgumentException.DEFAULT_MESSAGE_ID, + new Object[] { ": sort {} not allowed with cmis language" }); + } + for (SortDef sortDef:sort) + { + + try + { + SortType sortType = SortType.valueOf(sortDef.getType()); + String field = sortDef.getField(); + sp.addSort(new SortDefinition(sortType, field, sortDef.isAscending())); + } + catch (IllegalArgumentException e) + { + throw new InvalidArgumentException(InvalidArgumentException.DEFAULT_MESSAGE_ID, new Object[] { sortDef.getType() }); + } + } + } + } + + /** + * SearchParameters from List