From d53327d21ad4b8a333a6bafa0352bd9cd3239b35 Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Thu, 30 Nov 2023 21:14:42 +0900 Subject: [PATCH 1/2] refactor(java): sling-org-apache-sling-servlets-post --- .../Dockerfile | 28 + .../src/CODE_OF_CONDUCT.md | 22 + .../src/CONTRIBUTING.md | 24 + .../src/Jenkinsfile | 20 + .../src/LICENSE | 202 +++++ .../src/Protocols.md | 113 +++ .../src/README.md | 9 + .../src/bnd.bnd | 22 + .../src/developer-tests/README.md | 12 + .../src/developer-tests/testFileUploads.sh | 49 ++ .../src/pom.xml | 205 +++++ .../servlets/post/AbstractPostOperation.java | 635 ++++++++++++++ .../servlets/post/AbstractPostResponse.java | 388 +++++++++ .../post/AbstractSlingPostOperation.java | 83 ++ .../sling/servlets/post/HtmlResponse.java | 175 ++++ .../sling/servlets/post/JSONResponse.java | 218 +++++ .../sling/servlets/post/Modification.java | 142 ++++ .../sling/servlets/post/ModificationType.java | 78 ++ .../servlets/post/NodeNameGenerator.java | 39 + .../sling/servlets/post/PostOperation.java | 85 ++ .../sling/servlets/post/PostResponse.java | 216 +++++ .../servlets/post/PostResponseCreator.java | 28 + .../post/PostResponseWithErrorHandling.java | 61 ++ .../servlets/post/SlingPostConstants.java | 592 +++++++++++++ .../servlets/post/SlingPostOperation.java | 82 ++ .../servlets/post/SlingPostProcessor.java | 49 ++ .../post/VersioningConfiguration.java | 65 ++ .../post/impl/PostOperationProxyProvider.java | 254 ++++++ .../impl/PostResponseWithErrorHandling.java | 66 ++ .../servlets/post/impl/SlingPostServlet.java | 792 ++++++++++++++++++ .../servlets/post/impl/helper/Chunk.java | 76 ++ .../post/impl/helper/ChunkCleanUpTask.java | 190 +++++ .../servlets/post/impl/helper/DateParser.java | 163 ++++ .../impl/helper/DefaultNodeNameGenerator.java | 140 ++++ .../impl/helper/HtmlPostResponseProxy.java | 183 ++++ .../post/impl/helper/HtmlResponseProxy.java | 172 ++++ .../servlets/post/impl/helper/JCRSupport.java | 222 +++++ .../post/impl/helper/JCRSupportImpl.java | 559 ++++++++++++ .../post/impl/helper/MediaRangeList.java | 335 ++++++++ .../post/impl/helper/NodeNameFilter.java | 58 ++ .../post/impl/helper/ReferenceParser.java | 97 +++ .../post/impl/helper/RequestProperty.java | 319 +++++++ .../helper/ResourceIteratorInputStream.java | 71 ++ .../impl/helper/SlingFileUploadHandler.java | 594 +++++++++++++ .../helper/SlingPropertyValueHandler.java | 557 ++++++++++++ .../post/impl/helper/StreamedChunk.java | 419 +++++++++ .../operations/AbstractCopyMoveOperation.java | 166 ++++ .../operations/AbstractCreateOperation.java | 716 ++++++++++++++++ .../operations/AbstractPostOperation.java | 402 +++++++++ .../impl/operations/CheckinOperation.java | 82 ++ .../impl/operations/CheckoutOperation.java | 81 ++ .../post/impl/operations/CopyOperation.java | 75 ++ .../post/impl/operations/DeleteOperation.java | 114 +++ .../post/impl/operations/ImportOperation.java | 274 ++++++ .../post/impl/operations/ModifyOperation.java | 377 +++++++++ .../post/impl/operations/MoveOperation.java | 87 ++ .../post/impl/operations/NopOperation.java | 55 ++ .../impl/operations/RestoreOperation.java | 96 +++ .../operations/StreamedUploadOperation.java | 204 +++++ .../sling/servlets/post/package-info.java | 24 + .../resources/SLING-INF/nodetypes/chunk.cnd | 39 + .../servlets/post/HtmlNoGoBackResponse.html | 42 + .../sling/servlets/post/HtmlResponse.html | 43 + .../src/src/main/resources/system/sling.js | 444 ++++++++++ .../post/AbstractPostOperationTest.java | 124 +++ .../sling/servlets/post/HtmlResponseTest.java | 69 ++ .../sling/servlets/post/JsonResponseTest.java | 183 ++++ .../post/impl/NodeNameFilterTest.java | 54 ++ .../post/impl/RequestPropertyTest.java | 288 +++++++ .../post/impl/SlingPostServletTest.java | 196 +++++ .../impl/helper/HtmlResponseProxyTest.java | 116 +++ .../post/impl/helper/MediaRangeListTest.java | 92 ++ .../helper/MockSlingHttpServlet3Request.java | 148 ++++ .../helper/MockSlingHttpServlet3Response.java | 51 ++ .../ResourceIteratorInputStreamTest.java | 78 ++ ...SlingPropertyValueHandlerAutotypeTest.java | 43 + .../helper/SlingPropertyValueHandlerTest.java | 140 ++++ .../post/impl/helper/StreamedChunkTest.java | 61 ++ .../AbstractCreateOperationTest.java | 89 ++ .../impl/operations/CopyOperationTest.java | 78 ++ .../post/impl/operations/MockPart.java | 97 +++ .../impl/operations/MockRealResource.java | 145 ++++ .../post/impl/operations/MockValueMap.java | 38 + .../StreamingUploadOperationTest.java | 681 +++++++++++++++ .../Dockerfile | 18 + .../buggy.java | 173 ++++ .../metadata.json | 21 + .../npe.json | 7 + .../Dockerfile | 18 + .../buggy.java | 336 ++++++++ .../metadata.json | 21 + .../npe.json | 7 + .../Dockerfile | 18 + .../buggy.java | 336 ++++++++ .../metadata.json | 21 + .../npe.json | 7 + .../Dockerfile | 18 + .../buggy.java | 323 +++++++ .../metadata.json | 21 + .../npe.json | 7 + 100 files changed, 16353 insertions(+) create mode 100644 Java-base/sling-org-apache-sling-servlets-post/Dockerfile create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/CODE_OF_CONDUCT.md create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/CONTRIBUTING.md create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/Jenkinsfile create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/LICENSE create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/Protocols.md create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/README.md create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/bnd.bnd create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/README.md create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/testFileUploads.sh create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/pom.xml create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostResponse.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/HtmlResponse.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/JSONResponse.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/Modification.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/ModificationType.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/NodeNameGenerator.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponse.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseCreator.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseWithErrorHandling.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostProcessor.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/VersioningConfiguration.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostOperationProxyProvider.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostResponseWithErrorHandling.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/Chunk.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ChunkCleanUpTask.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DefaultNodeNameGenerator.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlPostResponseProxy.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxy.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupport.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupportImpl.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ReferenceParser.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStream.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/StreamedChunk.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractPostOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckinOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckoutOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ImportOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/NopOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/RestoreOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/StreamedUploadOperation.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/package-info.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/SLING-INF/nodetypes/chunk.cnd create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlNoGoBackResponse.html create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlResponse.html create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/system/sling.js create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/AbstractPostOperationTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/HtmlResponseTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/JsonResponseTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/RequestPropertyTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxyTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MediaRangeListTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Request.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Response.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStreamTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerAutotypeTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/StreamedChunkTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperationTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/CopyOperationTest.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockPart.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockRealResource.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockValueMap.java create mode 100644 Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/StreamingUploadOperationTest.java create mode 100644 Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/Dockerfile create mode 100644 Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/buggy.java create mode 100644 Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json create mode 100644 Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/npe.json create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/Dockerfile create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/buggy.java create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/npe.json create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/Dockerfile create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/buggy.java create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json create mode 100644 Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/npe.json create mode 100644 Java/sling-org-apache-sling-servlets-post-RequestProperty_152/Dockerfile create mode 100644 Java/sling-org-apache-sling-servlets-post-RequestProperty_152/buggy.java create mode 100644 Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json create mode 100644 Java/sling-org-apache-sling-servlets-post-RequestProperty_152/npe.json diff --git a/Java-base/sling-org-apache-sling-servlets-post/Dockerfile b/Java-base/sling-org-apache-sling-servlets-post/Dockerfile new file mode 100644 index 000000000..e208c4890 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:22.04 + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -y software-properties-common \ + && add-apt-repository ppa:deadsnakes/ppa \ + && apt-get update \ + && apt-get install -y \ + build-essential \ + git \ + vim \ + jq \ + && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/list/* + +RUN apt-get -y install sudo \ + openjdk-8-jdk \ + maven + +RUN bash -c "echo 2 | update-alternatives --config java" + +COPY src /workspace +WORKDIR /workspace + +RUN mvn install -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false + +RUN mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 + +ENV TZ=Asia/Seoul diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/CODE_OF_CONDUCT.md b/Java-base/sling-org-apache-sling-servlets-post/src/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..0fa18e593 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/CODE_OF_CONDUCT.md @@ -0,0 +1,22 @@ + +Apache Software Foundation Code of Conduct +==== + +Being an Apache project, Apache Sling adheres to the Apache Software Foundation's [Code of Conduct](https://www.apache.org/foundation/policies/conduct.html). diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/CONTRIBUTING.md b/Java-base/sling-org-apache-sling-servlets-post/src/CONTRIBUTING.md new file mode 100644 index 000000000..ac82a1abe --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/CONTRIBUTING.md @@ -0,0 +1,24 @@ + +Contributing +==== + +Thanks for choosing to contribute! + +You will find all the necessary details about how you can do this at https://sling.apache.org/contributing.html. diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/Jenkinsfile b/Java-base/sling-org-apache-sling-servlets-post/src/Jenkinsfile new file mode 100644 index 000000000..f5825190c --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/Jenkinsfile @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +slingOsgiBundleBuild() diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/LICENSE b/Java-base/sling-org-apache-sling-servlets-post/src/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/Protocols.md b/Java-base/sling-org-apache-sling-servlets-post/src/Protocols.md new file mode 100644 index 000000000..01a91a95d --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/Protocols.md @@ -0,0 +1,113 @@ +# Protocols + +Many of these protocols are documented elsewhere, however this document is with the source code and so hopefully uptodate. + +There is also published documentation at the following locations which may lag the code and this document. This document wont +reproduce the information in the following locations, but will note differences when they are discovered. + +https://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html +https://cwiki.apache.org/confluence/display/SLING/Chunked+File+Upload+Support + +# Request Processing. + +In general a request is processed by the Sling Engine encapsulating the data in Sling API interfaces. See the above locations +for more information on how that works. + +# Uploads + +The default Sling Post servlets support both streamed and non streamed uploads. Non streamed uploads are processed by the +Sling Engine request processing mechanisms before the information is passed to the the Servlet. While this is more flexible +as the client developer does not have to think about the order of the parameters, it is less efficient as file bodies must +be read completely before processing. This leads to more IO. Streaming uploads on the other hand read the request stream +as it is send from the client and process it as it is read. This avoids some of the additional IO, but requires more attention +to detain by a client developer as the parts of the post must arrive in teh correct order. Streaming is the preference method +of upload where large files are involved. + +# Non streamed uploads. + +## Whole body uploads. + +See https://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html +no known variance from the published documentation. + +## Sling Chunked body uploads. + +Note "Chunked" in this context is a special Sling protocol represented in request parameters intended to allow the client +to upload chunks of a file to Sling. The protocol protects agains multiple clients perfoming an upload on the same resource, +but does not support multiple clients performing chunked uploads at the same time. Multiple requests are used, multiple chunks cannont +generally be sent in a single request. +See https://cwiki.apache.org/confluence/display/SLING/Chunked+File+Upload+Support. + +Chunked uploads are supported by writing the data to special sub nodes of the nt:resource node and when the chunks are complete the subnodes are +read back in sequence creating the final binary. If chunks are send in the wrong order, the chunk is rejected. + +The definition of the chunknode differs from the published documentation. + + // node type to store chunk + // offset: offset of chunk in file + // jcr:data: binary of chunk + [sling:chunk] > nt:hierarchyNode + primaryitem jcr:data + - sling:offset (long) mandatory + - jcr:data (binary) mandatory + + //----------------------------------------------------------------------------- + // Mixin node type to identify that a node has chunks + // sling:fileLength : length of complete file + // sling:length : cumulative length of all uploaded chunks + [sling:chunks] + mixin + - sling:fileLength (long) + - sling:length (long) + + * (sling:chunk) multiple + + + +# Streamed uploads + +Streamed uploads were implemented under the following issues. + +https://issues.apache.org/jira/browse/SLING-5948 +https://issues.apache.org/jira/browse/SLING-6017 +https://issues.apache.org/jira/browse/SLING-6027 + +For streaming to work the Sling Engine must not read the parts of the request. This is achieved by appending a request parameter +to the URL "uploadmode=stream" or adding a Header "Sling-uploadmode: stream". When that is done the Sling Engine does not +read the request, but provides a Iterator in a request attribute (request-parts-iterator) that will provide each part as it +is sent from the client. In this mode the SlingDefaultPost Servlet invokes the StreamedUploadOperation which iterates through the Parts. + +The StreamedUploadOperation does not support the standard Sling Post protocol. Any node that need to be created with non standard properties +or structure should be created in a seperate POST. If the node PrimaryType needs to be non standard, that POST operation should be before +any upload is performed. + +## Full body streamed uploads. + +Full body streamed uploads are performed by a normal file upload. The name of the part is used as the File name. The location of the POST is the parent +resource which should already exist. If the name of the part is "*" then the supplied name of the file upload is used as the name of the resource. This is +the same as non streamed full body uploads. The behaviour is specific to Sling and may not be the same as adopeted by other systems. + +## Chunked streamed uploads. + +Chunked streamed upload are also supported with some subtle variations. Since the length of a body part cant be known until it has been fully streamed, a streaming +chunked upload must specify the length of the body part prior to sending the body part. This can be achieved by in one of 3 ways. Sending a @PartLength form field prior +to the part to indicate the length of the Part. Setting a Content-Length header on the part itself, or using a Content-Range header on the part itself. If all of these are missing, +processing assumes that the body part is the last body part in the file and truncates the file to that length, processing all parts so far. +Unlike the non streamed chunked upload protocol, the streamed upload protocol can accept multiple body parts in 1 request. Each body part +must be immediately preceded by the correct request parameters. (ie new @Offset if using Form fields). + +### Via request parameters. + +The standard Sling Chunked Upload protocol uses request parameters of the form @Length indicating the final length +of the file and @Offset indicating the offset of the next body part and @PartLength to indicate the +length of the part. @PartLength is in addition to the nonstreamed protocol. is the name of the body part +that follows. If any of these parameters are present in the request when a part is encountered, chunked processing is performed. +The chunk processing will detect when all the chunks have been send and perform final processing. + +### Via Content-Range headers + +Content Range headers are a part of the Http 1.1 Standard documented at https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html section 14.16. +If Content Range headers are provided on each Body part they are used in prefence to the propriatary Sling Chunk Upload protocol. This approach +is used by other resuable and chunked upload apis notably the Google Drive API. If content range headers are used, each part is self contained +and no other request parameters or headers are are required. There is an expectation that a content range header will use the form +"Content-Range: bytes 1234-2345/6789" specifying the full lenght and not the form "Content-Range: bytes 1234-2345/*" which implies a length of 2345. + diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/README.md b/Java-base/sling-org-apache-sling-servlets-post/src/README.md new file mode 100644 index 000000000..fdd49c097 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/README.md @@ -0,0 +1,9 @@ +[](https://sling.apache.org) + + [![Build Status](https://builds.apache.org/buildStatus/icon?job=Sling/sling-org-apache-sling-servlets-post/master)](https://builds.apache.org/job/Sling/job/sling-org-apache-sling-servlets-post/job/master) [![Test Status](https://img.shields.io/jenkins/t/https/builds.apache.org/job/Sling/job/sling-org-apache-sling-servlets-post/job/master.svg)](https://builds.apache.org/job/Sling/job/sling-org-apache-sling-servlets-post/job/master/test_results_analyzer/) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.sling/org.apache.sling.servlets.post/badge.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.sling%22%20a%3A%22org.apache.sling.servlets.post%22) [![JavaDocs](https://www.javadoc.io/badge/org.apache.sling/org.apache.sling.servlets.post.svg)](https://www.javadoc.io/doc/org.apache.sling/org.apache.sling.servlets.post) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![servlets](https://sling.apache.org/badges/group-servlets.svg)](https://github.com/apache/sling-aggregator/blob/master/docs/groups/servlets.md) + +# Apache Sling Default POST Servlets + +Provides default POST servlets. + +This module is part of the [Apache Sling](https://sling.apache.org) project. You can read more about this module on our [documentation site](https://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html). diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/bnd.bnd b/Java-base/sling-org-apache-sling-servlets-post/src/bnd.bnd new file mode 100644 index 000000000..5dc7f5b56 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/bnd.bnd @@ -0,0 +1,22 @@ +Import-Package: javax.jcr;resolution:=optional,\ + javax.jcr.nodetype;resolution:=optional,\ + javax.jcr.version;resolution:=optional,\ + org.apache.sling.jcr.contentloader;resolution:=optional,\ + * + +Sling-Bundle-Resources: /system/sling.js + +Sling-Namespaces: sling=http://sling.apache.org/jcr/sling/1.0 + +Sling-Nodetypes: SLING-INF/nodetypes/chunk.cnd + +-exportcontents: ${packages;VERSIONED} + +-includeresource:\ + @jackrabbit-jcr-commons-*.jar!/org/apache/jackrabbit/util/ISO8601.class,\ + @jackrabbit-jcr-commons-*.jar!/org/apache/jackrabbit/util/Text.class,\ + @org.apache.sling.jcr.contentparser-*.jar!/org/apache/sling/jcr/contentparser/impl/JsonTicksConverter.class + +-removeheaders:\ + Include-Resource,\ + Private-Package \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/README.md b/Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/README.md new file mode 100644 index 000000000..bd318a54b --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/README.md @@ -0,0 +1,12 @@ +# Developer test scripts. + +Testing POST behaviour can involve a lot of curl commands or browser work, expecially with file uploads. +This folder contains test scripts that verify the behaviour of this bundle in a running Sling instance. +They are not intended as a replacement for unit tests or integration tests, but rather a quick way to allow +a developer to verify behaviour and be in control of the test being run. + +## testFileUploads.sh +run in parent as sh developer-tests/testFileUploads.sh and it will upload that file using non streamed, + streamed, and streaming chunked protocols to the Sling server on localhost:8080 as admin:admin. It will also download + the file and diff it against the local copy to ensure no changes. Chunks are 20kb each, so a large file will generate a + large number of small chunks. Dont try and test a GB file without editing the script to increase the chunk size. \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/testFileUploads.sh b/Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/testFileUploads.sh new file mode 100644 index 000000000..0179a5ee2 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/developer-tests/testFileUploads.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -e +mkdir -p target +cd target +echo Creating file via normal non streamed upload +cp $1 P1060839.jpg +curl -v -F P1060839.jpg=@P1060839.jpg http://admin:admin@localhost:8080/content/test +curl http://admin:admin@localhost:8080/content/test/P1060839.jpg > P1060839_up.jpg +echo Checking normal upload. +diff P1060839.jpg P1060839_up.jpg + +echo Creating file via normal non streamed upload +cp P1060839.jpg SP1060839.jpg +curl -v -H "Sling-uploadmode: stream" -F key1=value1 -F *=@SP1060839.jpg -F PSP1060839.jpg=@SP1060839.jpg -F key2=admin2 http://admin:admin@localhost:8080/content/test +curl http://admin:admin@localhost:8080/content/test/PSP1060839.jpg > PSP1060839_up.jpg +curl http://admin:admin@localhost:8080/content/test/SP1060839.jpg > SP1060839_up.jpg +diff P1060839.jpg PSP1060839_up.jpg +diff P1060839.jpg SP1060839_up.jpg + +echo Checking chunked upload in 50k blocks. +rm -f P1060839_chunk* +split -b 20k P1060839.jpg P1060839_chunk +offset=0 +length=`wc -c P1060839.jpg | sed "s/ *\([0-9]*\) .*/\1/"` +for i in P1060839_chunk*; do + size=`wc -c $i | sed "s/ *\([0-9]*\) .*/\1/"` + curl -v -H "Sling-uploadmode: stream" -F CP1060839.jpg@Length=$length -F CP1060839.jpg@PartLength=$size -F CP1060839.jpg@Offset=$offset -F CP1060839.jpg=@$i http://admin:admin@localhost:8080/content/test + let offset=offset+size +done +curl http://admin:admin@localhost:8080/content/test/CP1060839.jpg > CP1060839_up.jpg +diff -u P1060839.jpg CP1060839_up.jpg diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/pom.xml b/Java-base/sling-org-apache-sling-servlets-post/src/pom.xml new file mode 100644 index 000000000..96f8c237e --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/pom.xml @@ -0,0 +1,205 @@ + + + + 4.0.0 + + org.apache.sling + sling-bundle-parent + 35 + + + + org.apache.sling.servlets.post + jar + 2.3.37-SNAPSHOT + + Apache Sling Default POST Servlets + + Provides default POST servlets. + + + + scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-servlets-post.git + scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-servlets-post.git + https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-servlets-post.git + HEAD + + + + 12314180 + + + + + + biz.aQute.bnd + bnd-maven-plugin + + + biz.aQute.bnd + bnd-baseline-maven-plugin + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + true + true + + + org.apache.jackrabbit.util + org.apache.sling.servlets.post.impl.jackrabbit + + + org.apache.sling.jcr.contentparser + org.apache.sling.servlets.post.impl.contentparser + + + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/org/apache/sling/servlets/post/HtmlResponse.html + src/main/resources/org/apache/sling/servlets/post/HtmlNoGoBackResponse.html + developer-tests/README.md + Protocols.md + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.sling.servlets.post.impl + + + + + + + + org.osgi + org.osgi.annotation.versioning + provided + + + org.osgi + osgi.core + provided + + + org.osgi + org.osgi.service.component.annotations + provided + + + org.osgi + org.osgi.service.metatype.annotations + provided + + + javax.servlet + javax.servlet-api + + + javax.jcr + jcr + + + org.slf4j + slf4j-api + + + org.apache.sling + org.apache.sling.api + 2.9.0 + provided + + + commons-io + commons-io + 2.4 + provided + + + org.apache.sling + org.apache.sling.jcr.contentloader + 2.1.10 + provided + + + org.apache.jackrabbit + jackrabbit-jcr-commons + 1.6.0 + provided + + + org.apache.geronimo.specs + geronimo-json_1.0_spec + 1.0-alpha-1 + provided + + + org.apache.sling + org.apache.sling.jcr.contentparser + 1.2.4 + provided + + + org.apache.sling + org.apache.sling.commons.johnzon + 1.0.0 + test + + + org.apache.sling + org.apache.sling.commons.testing + 2.0.10 + test + + + junit-addons + junit-addons + 1.4 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + + + diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostOperation.java new file mode 100644 index 000000000..135006ae6 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostOperation.java @@ -0,0 +1,635 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.wrappers.SlingRequestPaths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The AbstractPostOperation class is a base implementation of the + * {@link PostOperation} service interface providing actual implementations with + * useful tooling and common functionality like preparing the change logs or + * saving or refreshing the JCR Session. + * + * @deprecated (SLING-6722): this class mixes Sling and JCR APIs which is not + * optimal as nowadays we favor the Sling APIs. There's no intention to remove + * it however, if you're using JCR APIs anyways in your project it's fine to + * use it. Theres no public replacement for it as I write this. + */ +@Deprecated +public abstract class AbstractPostOperation implements PostOperation { + + /** + * default log + */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * Prepares and finalizes the actual operation. Preparation encompasses + * getting the absolute path of the item to operate on by calling the + * {@link #getItemPath(SlingHttpServletRequest)} method and setting the + * location and parent location on the response. After the operation has + * been done in the {@link #doRun(SlingHttpServletRequest, PostResponse, List)} + * method the session is saved if there are unsaved modifications. In case + * of errorrs, the unsaved changes in the session are rolled back. + * + * @param request the request to operate on + * @param response The PostResponse to record execution + * progress. + * @param processors The array of processors + */ + @Override + public void run(final SlingHttpServletRequest request, + final PostResponse response, + final SlingPostProcessor[] processors) { + final Session session = request.getResourceResolver().adaptTo(Session.class); + + final VersioningConfiguration versionableConfiguration = getVersioningConfiguration(request); + + try { + // calculate the paths + String path = getItemPath(request); + path = removeAndValidateWorkspace(path, session); + response.setPath(path); + + // location + response.setLocation(externalizePath(request, path)); + + // parent location + path = ResourceUtil.getParent(path); + if (path != null) { + response.setParentLocation(externalizePath(request, path)); + } + + final List changes = new ArrayList<>(); + + doRun(request, response, changes); + + // invoke processors + if (processors != null) { + for (SlingPostProcessor processor : processors) { + processor.process(request, changes); + } + } + + // check modifications for remaining postfix and store the base path + final Map modificationSourcesContainingPostfix = new HashMap<>(); + final Set allModificationSources = new HashSet<>(changes.size()); + for (final Modification modification : changes) { + final String source = modification.getSource(); + if (source != null) { + allModificationSources.add(source); + final int atIndex = source.indexOf('@'); + if (atIndex > 0) { + modificationSourcesContainingPostfix.put(source.substring(0, atIndex), source); + } + } + } + + // fail if any of the base paths (before the postfix) which had a postfix are contained in the modification set + if (modificationSourcesContainingPostfix.size() > 0) { + for (final Map.Entry sourceToCheck : modificationSourcesContainingPostfix.entrySet()) { + if (allModificationSources.contains(sourceToCheck.getKey())) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Postfix-containing path " + sourceToCheck.getValue() + + " contained in the modification list. Check configuration."); + return; + } + } + } + + final Set nodesToCheckin = new LinkedHashSet<>(); + + // set changes on html response + for(Modification change : changes) { + switch ( change.getType() ) { + case MODIFY : response.onModified(change.getSource()); break; + case DELETE : response.onDeleted(change.getSource()); break; + case MOVE : response.onMoved(change.getSource(), change.getDestination()); break; + case COPY : response.onCopied(change.getSource(), change.getDestination()); break; + case CREATE : + response.onCreated(change.getSource()); + if (versionableConfiguration.isCheckinOnNewVersionableNode()) { + nodesToCheckin.add(change.getSource()); + } + break; + case ORDER : response.onChange("ordered", change.getSource(), change.getDestination()); break; + case CHECKOUT : + response.onChange("checkout", change.getSource()); + nodesToCheckin.add(change.getSource()); + break; + case CHECKIN : + response.onChange("checkin", change.getSource()); + nodesToCheckin.remove(change.getSource()); + break; + } + } + + if (isSessionSaveRequired(session, request)) { + request.getResourceResolver().commit(); + } + + if (!isSkipCheckin(request)) { + // now do the checkins + for(String checkinPath : nodesToCheckin) { + if (checkin(request.getResourceResolver(), checkinPath)) { + response.onChange("checkin", checkinPath); + } + } + } + + } catch (Exception e) { + + log.error("Exception during response processing.", e); + response.setError(e); + + } finally { + try { + if (isSessionSaveRequired(session, request)) { + request.getResourceResolver().revert(); + } + } catch (RepositoryException e) { + log.warn("RepositoryException in finally block: {}", + e.getMessage(), e); + } + } + + } + + /** + * Actually performs the desired operation filling progress into the + * changes list and preparing and further information in the + * response. + *

+ * The response comes prepared with the path, location and + * parent location set. Other properties are expected to be set by this + * implementation. + * + * @param request The SlingHttpServletRequest providing the + * input, mostly in terms of request parameters, to the + * operation. + * @param response The {@link PostResponse} to fill with response + * information + * @param changes A container to add {@link Modification} instances + * representing the operations done. + * @throws RepositoryException Maybe thrown if any error occurrs while + * accessing the repository. + */ + protected abstract void doRun(SlingHttpServletRequest request, + PostResponse response, + List changes) throws RepositoryException; + + /** + * Get the versioning configuration. + * @param request The http request + * @return The versioning configuration + */ + protected VersioningConfiguration getVersioningConfiguration(SlingHttpServletRequest request) { + VersioningConfiguration versionableConfiguration = + (VersioningConfiguration) request.getAttribute(VersioningConfiguration.class.getName()); + return versionableConfiguration != null ? versionableConfiguration : new VersioningConfiguration(); + } + + /** + * Check if checkin should be skipped + * @param request The http request + * @return {@code true} if checkin should be skipped + */ + protected boolean isSkipCheckin(SlingHttpServletRequest request) { + return !getVersioningConfiguration(request).isAutoCheckin(); + } + + /** + * Check whether changes should be written back + * @param request The http request + * @return {@code true} If session handling should be skipped + */ + protected boolean isSkipSessionHandling(SlingHttpServletRequest request) { + return Boolean.parseBoolean((String) request.getAttribute(SlingPostConstants.ATTR_SKIP_SESSION_HANDLING)) == true; + } + + /** + * Check whether commit to the resource resolver should be called. + * @param session The JCR session + * @param request The http request + * @return {@code true} if a save is required. + * @throws RepositoryException + */ + protected boolean isSessionSaveRequired(Session session, SlingHttpServletRequest request) + throws RepositoryException { + return !isSkipSessionHandling(request) && request.getResourceResolver().hasChanges(); + } + + /** + * Remove the workspace name, if any, from the start of the path and validate that the + * session's workspace name matches the path workspace name. + * @param path The path + * @param session The JCR session + * @return The path without the workspace + * @throws RepositoryException + */ + protected String removeAndValidateWorkspace(String path, Session session) throws RepositoryException { + final int wsSepPos = path.indexOf(":/"); + if (wsSepPos != -1) { + final String workspaceName = path.substring(0, wsSepPos); + if (!workspaceName.equals(session.getWorkspace().getName())) { + throw new RepositoryException("Incorrect workspace. Expecting " + workspaceName + ". Received " + + session.getWorkspace().getName()); + } + return path.substring(wsSepPos + 1); + } + return path; + } + + + /** + * Returns the path of the resource of the request as the item path. + *

+ * This method may be overwritten by extension if the operation has + * different requirements on path processing. + * @param request The http request + * @return The item path + */ + protected String getItemPath(SlingHttpServletRequest request) { + return request.getResource().getPath(); + } + + /** + * Returns an iterator on Resource instances addressed in the + * {@link SlingPostConstants#RP_APPLY_TO} request parameter. If the request + * parameter is not set, null is returned. If the parameter + * is set with valid resources an empty iterator is returned. Any resources + * addressed in the {@link SlingPostConstants#RP_APPLY_TO} parameter is + * ignored. + * + * @param request The SlingHttpServletRequest object used to + * get the {@link SlingPostConstants#RP_APPLY_TO} parameter. + * @return The iterator of resources listed in the parameter or + * null if the parameter is not set in the request. + */ + protected Iterator getApplyToResources( + SlingHttpServletRequest request) { + + final String[] applyTo = request.getParameterValues(SlingPostConstants.RP_APPLY_TO); + if (applyTo == null) { + return null; + } + + return new ApplyToIterator(request, applyTo); + } + + /** + * Returns an external form of the given path prepending the context path + * and appending a display extension. + * + * @param request The http request + * @param path the path to externalize + * @return the url + */ + protected final String externalizePath(SlingHttpServletRequest request, + String path) { + StringBuilder ret = new StringBuilder(); + ret.append(SlingRequestPaths.getContextPath(request)); + ret.append(request.getResourceResolver().map(path)); + + // append optional extension + String ext = request.getParameter(SlingPostConstants.RP_DISPLAY_EXTENSION); + if (ext != null && ext.length() > 0) { + if (ext.charAt(0) != '.') { + ret.append('.'); + } + ret.append(ext); + } + + return ret.toString(); + } + + /** + * Resolves the given path with respect to the current root path. + * + * @param absPath The absolute base path + * @param relPath the path to resolve + * @return the given path if it starts with a '/'; a resolved path + * otherwise. + */ + protected final String resolvePath(String absPath, String relPath) { + if (relPath.startsWith("/")) { + return relPath; + } + return absPath + "/" + relPath; + } + + /** + * Returns true if any of the request parameters starts with + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}. + * In this case only parameters starting with either of the prefixes + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}, + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT ../} + * and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE /} are + * considered as providing content to be stored. Otherwise all parameters + * not starting with the command prefix : are considered as + * parameters to be stored. + * + * @param request The http request + * @return If a prefix is required. + */ + protected final boolean requireItemPathPrefix( + SlingHttpServletRequest request) { + + boolean requirePrefix = false; + + Enumeration names = request.getParameterNames(); + while (names.hasMoreElements() && !requirePrefix) { + String name = (String) names.nextElement(); + requirePrefix = name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT); + } + + return requirePrefix; + } + + /** + * Returns true if the name starts with either + * of the prefixes + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}, + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT ../} + * and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE /}. + * + * @param name The name + * @return {@code true} if the name has a prefix + */ + protected boolean hasItemPathPrefix(String name) { + return name.startsWith(SlingPostConstants.ITEM_PREFIX_ABSOLUTE) + || name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT) + || name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_PARENT); + } + + /** + * Orders the given node according to the specified command. The following + * syntax is supported: <xmp> | first | before all child nodes | before A | + * before child node A | after A | after child node A | last | after all + * nodes | N | at a specific position, N being an integer </xmp> + * + * @param request The http request + * @param item node to order + * @param changes The list of modifications + * @throws RepositoryException if an error occurs + */ + protected void orderNode(SlingHttpServletRequest request, Item item, + List changes) throws RepositoryException { + + String command = request.getParameter(SlingPostConstants.RP_ORDER); + if (command == null || command.length() == 0) { + // nothing to do + return; + } + + if (!item.isNode()) { + return; + } + + Node parent = item.getParent(); + + String next = null; + if (command.equals(SlingPostConstants.ORDER_FIRST)) { + + next = parent.getNodes().nextNode().getName(); + + } else if (command.equals(SlingPostConstants.ORDER_LAST)) { + + next = ""; + + } else if (command.startsWith(SlingPostConstants.ORDER_BEFORE)) { + + next = command.substring(SlingPostConstants.ORDER_BEFORE.length()); + + } else if (command.startsWith(SlingPostConstants.ORDER_AFTER)) { + + String name = command.substring(SlingPostConstants.ORDER_AFTER.length()); + NodeIterator iter = parent.getNodes(); + while (iter.hasNext()) { + Node n = iter.nextNode(); + if (n.getName().equals(name)) { + if (iter.hasNext()) { + next = iter.nextNode().getName(); + } else { + next = ""; + } + } + } + + } else { + // check for integer + try { + // 01234 + // abcde move a -> 2 (above 3) + // bcade move a -> 1 (above 1) + // bacde + int newPos = Integer.parseInt(command); + next = ""; + NodeIterator iter = parent.getNodes(); + while (iter.hasNext() && newPos >= 0) { + Node n = iter.nextNode(); + if (n.getName().equals(item.getName())) { + // if old node is found before index, need to + // inc index + newPos++; + } + if (newPos == 0) { + next = n.getName(); + break; + } + newPos--; + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "provided node ordering command is invalid: " + command); + } + } + + if (next != null) { + if (next.equals("")) { + next = null; + } + parent.orderBefore(item.getName(), next); + changes.add(Modification.onOrder(item.getPath(), next)); + if (log.isDebugEnabled()) { + log.debug("Node {} moved '{}'", item.getPath(), command); + } + } else { + throw new IllegalArgumentException( + "provided node ordering command is invalid: " + command); + } + } + + protected Node findVersionableAncestor(Node node) throws RepositoryException { + if (isVersionable(node)) { + return node; + } + try { + node = node.getParent(); + return findVersionableAncestor(node); + } catch (ItemNotFoundException e) { + // top-level + return null; + } + } + + protected boolean isVersionable(Node node) throws RepositoryException { + return node.isNodeType("mix:versionable"); + } + + protected void checkoutIfNecessary(Node node, List changes, + VersioningConfiguration versioningConfiguration) throws RepositoryException { + if (versioningConfiguration.isAutoCheckout()) { + Node versionableNode = findVersionableAncestor(node); + if (versionableNode != null) { + if (!versionableNode.isCheckedOut()) { + versionableNode.checkout(); + changes.add(Modification.onCheckout(versionableNode.getPath())); + } + } + } + } + + private boolean checkin(final ResourceResolver resolver, final String path) throws RepositoryException { + final Resource rsrc = resolver.getResource(path); + final Node node = (rsrc == null ? null : rsrc.adaptTo(Node.class)); + if (node != null) { + if (node.isCheckedOut() && isVersionable(node)) { + node.checkin(); + return true; + } + } + return false; + } + + private static class ApplyToIterator implements Iterator { + + private final ResourceResolver resolver; + private final Resource baseResource; + private final String[] paths; + + private int pathIndex; + + private Resource nextResource; + + private Iterator resourceIterator = null; + + ApplyToIterator(SlingHttpServletRequest request, String[] paths) { + this.resolver = request.getResourceResolver(); + this.baseResource = request.getResource(); + this.paths = paths; + this.pathIndex = 0; + + nextResource = seek(); + } + + @Override + public boolean hasNext() { + return nextResource != null; + } + + @Override + public Resource next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + Resource result = nextResource; + nextResource = seek(); + + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private Resource seek() { + if (resourceIterator != null) { + if (resourceIterator.hasNext()) { + //return the next resource in the iterator + Resource res = resourceIterator.next(); + return res; + } + resourceIterator = null; + } + while (pathIndex < paths.length) { + String path = paths[pathIndex]; + pathIndex++; + + //SLING-2415 - support wildcard as the last segment of the applyTo path + if (path.endsWith("*")) { + if (path.length() == 1) { + resourceIterator = baseResource.listChildren(); + } else if (path.endsWith("/*")) { + path = path.substring(0, path.length() - 2); + if (path.length() == 0) { + resourceIterator = baseResource.listChildren(); + } else { + Resource res = resolver.getResource(baseResource, path); + if (res != null) { + resourceIterator = res.listChildren(); + } + } + } + if (resourceIterator != null) { + //return the first resource in the iterator + if (resourceIterator.hasNext()) { + Resource res = resourceIterator.next(); + return res; + } + resourceIterator = null; + } + } else { + Resource res = resolver.getResource(baseResource, path); + if (res != null) { + return res; + } + } + } + + // no more elements in the array + return null; + } + } +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostResponse.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostResponse.java new file mode 100644 index 000000000..57e633607 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractPostResponse.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +/** + * The AbstractPostResponse class provides a basic implementation + * of the {@link PostResponse} interface maintaining properties to be + * prepared for sending the response in an internal map. + */ +public abstract class AbstractPostResponse implements PostResponse { + + /** + * Name of the title property set by {@link #setTitle(String)} + */ + public static final String PN_TITLE = "title"; + + /** + * Name of the status code property set by {@link #setStatus(int, String)} + */ + public static final String PN_STATUS_CODE = "status.code"; + + /** + * Name of the status message property set by {@link #setStatus(int, String)} + */ + public static final String PN_STATUS_MESSAGE = "status.message"; + + /** + * Name of the location property set by {@link #setLocation(String)} + */ + public static final String PN_LOCATION = "location"; + + /** + * Name of the parent location property set by {@link #setParentLocation(String)} + */ + public static final String PN_PARENT_LOCATION = "parentLocation"; + + /** + * Name of the path property set by {@link #setPath(String)} + */ + public static final String PN_PATH = "path"; + + /** + * Name of the referer property set by {@link #setReferer(String)} + */ + public static final String PN_REFERER = "referer"; + + /** + * Name of the create status property set by {@link #setCreateRequest(boolean)} + */ + public static final String PN_IS_CREATED = "isCreate"; + + /** + * Name of the error property set by {@link #setError(Throwable)} + */ + public static final String PN_ERROR = "error"; + + /** + * Properties of the response + */ + private final Map properties = new HashMap(); + + // ---------- Settings for the response ------------------------------------ + + /** + * Returns the referer as from the 'referer' request header. + */ + public String getReferer() { + return getProperty(PN_REFERER, String.class); + } + + /** + * Sets the referer property + */ + public void setReferer(String referer) { + setProperty(PN_REFERER, referer); + } + + /** + * Returns the absolute path of the item upon which the request operated. + *

+ * If the {@link #setPath(String)} method has not been called yet, this + * method returns null. + */ + public String getPath() { + return getProperty(PN_PATH, String.class); + } + + /** + * Sets the absolute path of the item upon which the request operated. + */ + public void setPath(String path) { + setProperty(PN_PATH, path); + } + + /** + * Returns true if this was a create request. + *

+ * Before calling the {@link #setCreateRequest(boolean)} method, this method + * always returns false. + */ + public boolean isCreateRequest() { + final Boolean isCreateRequest = getProperty(PN_IS_CREATED, + Boolean.class); + return (isCreateRequest != null) + ? isCreateRequest.booleanValue() + : false; + } + + /** + * Sets whether the request was a create request or not. + */ + public void setCreateRequest(boolean isCreateRequest) { + setProperty(PN_IS_CREATED, isCreateRequest); + } + + /** + * Returns the location of the modification. this is the externalized form + * of the current path. + * + * @return the location of the modification. + */ + public String getLocation() { + return getProperty(PN_LOCATION, String.class); + } + + public void setLocation(String location) { + setProperty(PN_LOCATION, location); + } + + /** + * Returns the parent location of the modification. this is the externalized + * form of the parent node of the current path. + * + * @return the location of the modification. + */ + public String getParentLocation() { + return getProperty(PN_PARENT_LOCATION, String.class); + } + + public void setParentLocation(String parentLocation) { + setProperty(PN_PARENT_LOCATION, parentLocation); + } + + /** + * Sets the title of the response message + * + * @param title the title + */ + public void setTitle(String title) { + setProperty(PN_TITLE, title); + } + + /** + * sets the response status code properties + * + * @param code the code + * @param message the message + */ + public void setStatus(int code, String message) { + setProperty(PN_STATUS_CODE, code); + setProperty(PN_STATUS_MESSAGE, message); + } + + /** + * Returns the status code of this instance. If the status code has never + * been set by calling the {@link #setStatus(int, String)} method, the + * status code is determined by checking if there was an error. If there was + * an error, the response is assumed to be unsuccessful and 500 is returned. + * If there is no error, the response is assumed to be successful and 200 is + * returned. + */ + public int getStatusCode() { + Integer status = getProperty(PN_STATUS_CODE, Integer.class); + if (status == null) { + if (getError() != null) { + // if there was an error + status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } else { + status = HttpServletResponse.SC_OK; + } + } + return status; + } + + public String getStatusMessage() { + return getProperty(PN_STATUS_MESSAGE, String.class); + } + + /** + * Returns any recorded error or null + * + * @return an error or null + */ + public Throwable getError() { + return getProperty(PN_ERROR, Throwable.class); + } + + public void setError(Throwable error) { + setProperty(PN_ERROR, error); + } + + /** + * Returns true if no {@link #getError() error} is set and if + * the {@link #getStatusCode() status code} is one of the 2xx codes. + */ + public boolean isSuccessful() { + return getError() == null && (getStatusCode() / 100) == 2; + } + + // ---------- ChangeLog ---------------------------------------------------- + + /** + * Records a 'modified' change + * + * @param path path of the item that was modified + */ + public void onModified(String path) { + onChange("modified", path); + } + + /** + * Records a 'created' change + * + * @param path path of the item that was created + */ + public void onCreated(String path) { + onChange("created", path); + } + + /** + * Records a 'deleted' change + * + * @param path path of the item that was deleted + */ + public void onDeleted(String path) { + if (path != null) { + onChange("deleted", path); + } + } + + /** + * Records a 'moved' change. + *

+ * Note: the moved change only records the basic move command. the implied + * changes on the moved properties and sub nodes are not recorded. + * + * @param srcPath source path of the node that was moved + * @param dstPath destination path of the node that was moved. + */ + public void onMoved(String srcPath, String dstPath) { + onChange("moved", srcPath, dstPath); + } + + /** + * Records a 'copied' change. + *

+ * Note: the copy change only records the basic copy command. the implied + * changes on the copied properties and sub nodes are not recorded. + * + * @param srcPath source path of the node that was copied + * @param dstPath destination path of the node that was copied. + */ + public void onCopied(String srcPath, String dstPath) { + onChange("copied", srcPath, dstPath); + } + + + /** + * prepares the response properties + */ + private void prepare(final HttpServletResponse response, final boolean setStatus) { + String path = getPath(); + if (getProperty(PN_STATUS_CODE) == null) { + if (getError() != null) { + setStatus(500, getError().toString()); + setTitle("Error while processing " + path); + } else { + if (isCreateRequest()) { + setStatus(201, "Created"); + setTitle("Content created " + path); + } else { + setStatus(200, "OK"); + setTitle("Content modified " + path); + } + } + } + + String referer = getReferer(); + if (referer == null) { + referer = ""; + } + setReferer(referer); + + if (setStatus) { + Object status = getProperty(PN_STATUS_CODE); + if (status instanceof Number) { + int statusCode = ((Number) status).intValue(); + response.setStatus(statusCode); + + // special treatment of 201/CREATED and 3xx: Requires Location + if (statusCode == HttpServletResponse.SC_CREATED || statusCode / 100 == 3) { + response.setHeader("Location", getLocation()); + } + } + } + + } + + /** + * Sets a generic response property with the given + * + * @param name name of the property + * @param value value of the property + */ + protected void setProperty(String name, Object value) { + properties.put(name, value); + } + + /** + * Returns the generic response property with the given name and type or + * null if no such property exists or the property is not of + * the requested type. + */ + @SuppressWarnings("unchecked") + protected Type getProperty(String name, Class type) { + Object value = getProperty(name); + if (type.isInstance(value)) { + return (Type) value; + } + + return null; + } + + /** + * Returns the generic response property with the given name and type or + * null if no such property exists. + */ + protected Object getProperty(String name) { + return properties.get(name); + } + + protected boolean isSafeReferer(){ + String referer = getReferer(); + if (referer.startsWith("http://") || referer.startsWith("https://")) { + return true; + } else { + return false; + } + } + + protected abstract void doSend(HttpServletResponse response) throws IOException; + + /** + * Writes the response to the given writer and replaces all ${var} patterns + * by the value of the respective property. if the property is not defined + * the pattern is not modified. + * + * @param response to send to + * @param setStatus whether to set the status code on the response + * @throws IOException if an i/o exception occurs + */ + public final void send(HttpServletResponse response, boolean setStatus) + throws IOException { + prepare(response, setStatus); + doSend(response); + } + +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java new file mode 100644 index 000000000..242710def --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post; + +import java.util.List; + +import javax.jcr.RepositoryException; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.servlets.HtmlResponse; +import org.apache.sling.servlets.post.impl.helper.HtmlPostResponseProxy; +import org.apache.sling.servlets.post.impl.helper.HtmlResponseProxy; + +/** + * The AbstractSlingPostOperation is the abstract base class + * implementation of the {@link SlingPostOperation} interface extending the new + * {@link AbstractPostOperation}. + *

+ * This class exists for backwards compatibility. Existing implementations are + * advised to migrate to the new {@link AbstractPostOperation}. + * + * @deprecated as of 2.0.8 (Bundle version 2.2.0) and replaced by + * {@link AbstractPostOperation}. + */ +public abstract class AbstractSlingPostOperation extends AbstractPostOperation + implements SlingPostOperation { + + /** + * + * @param request + * @param response + * @param changes + * @throws RepositoryException + */ + protected abstract void doRun(SlingHttpServletRequest request, + HtmlResponse response, List changes) + throws RepositoryException; + + /** + * Implementation of the + * {@link AbstractPostOperation#doRun(SlingHttpServletRequest, PostResponse, List)} + * method calling our own + * {@link #run(SlingHttpServletRequest, HtmlResponse, SlingPostProcessor[])} + * meethod with a proxy for the Sling API HtmlResponse. + */ + protected void doRun(SlingHttpServletRequest request, + PostResponse response, List changes) + throws RepositoryException { + final HtmlResponse htmlResponseProxy = (response instanceof HtmlPostResponseProxy) + ? ((HtmlPostResponseProxy) response).getHtmlResponse() + : new HtmlResponseProxy(response); + doRun(request, htmlResponseProxy, changes); + } + + /** + * Implementation of the + * {@link SlingPostOperation#run(SlingHttpServletRequest, HtmlResponse, SlingPostProcessor[])} + * API method calling the + * {@link PostOperation#run(SlingHttpServletRequest, PostResponse, SlingPostProcessor[])} + * with a proxy around the Sling API HtmlResponse provided. + */ + public void run(SlingHttpServletRequest request, HtmlResponse response, + SlingPostProcessor[] processors) { + final PostResponse postResponseProxy = new HtmlPostResponseProxy( + response); + run(request, postResponseProxy, processors); + } + +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/HtmlResponse.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/HtmlResponse.java new file mode 100644 index 000000000..9fd204f97 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/HtmlResponse.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Writer; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.request.ResponseUtil; + +/** + * The HtmlResponse is an {@link AbstractPostResponse} preparing + * the response in HTML (actually XHTML) such that it can be interpreted + * as a plain response in a browser or as XML response in an Ajax request. + */ +public class HtmlResponse extends AbstractPostResponse { + + /** + * Name of the property into which the change log is gathered to be + * sent back by the {@link #doSend(HttpServletResponse)} method. This + * property is only sent before replacing all variables in the HTML + * response remplate. + */ + private static final String PN_CHANGE_LOG = "changeLog"; + + /** + * name of the html template + */ + private static final String TEMPLATE_NAME = "HtmlResponse.html"; + + /** + * name of the html safe referer template + */ + private static final String NO_GO_BACK_TEMPLATE_NAME = "HtmlNoGoBackResponse.html"; + + /** + * list of changes + */ + private final StringBuilder changes = new StringBuilder(); + + /** + * Records a generic change of the given type. + *

+ * The change is added to the internal list of changes with the syntax of a + * method call, where the type is the method name and the + * arguments are the string arguments to the method enclosed in + * double quotes. For example, the the call + * + *

+     * onChange("sameple", "arg1", "arg2");
+     * 
+ * + * is aded as + * + *
+     * sample("arg1", "arg2")
+     * 
+ * + * to the internal list of changes. + * + * @param type The type of the modification + * @param arguments The arguments to the modifications + */ + public void onChange(String type, String... arguments) { + changes.append(type); + String delim = "("; + for (String a : arguments) { + changes.append(delim); + changes.append('\"'); + changes.append(a); + changes.append('\"'); + delim = ", "; + } + changes.append(");
"); + } + + // ---------- Response Generation ------------------------------------------ + + /** + * Writes the response to the given writer and replaces all ${var} patterns + * by the value of the respective property. if the property is not defined + * the pattern is not modified. + * + * @param response to send to + * @throws IOException if an i/o exception occurs + */ + @Override + protected void doSend(HttpServletResponse response) + throws IOException { + + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + // get changelog + changes.insert(0, "
");
+        changes.append("
"); + if (getError() == null) { + setProperty(PN_CHANGE_LOG, changes.toString()); + } else { + setProperty(PN_CHANGE_LOG, ""); + } + + Writer out = response.getWriter(); + + String templateName; + if(isSafeReferer()) { + templateName = TEMPLATE_NAME; + } else { + templateName = NO_GO_BACK_TEMPLATE_NAME; + } + + InputStream template = getClass().getResourceAsStream(templateName); + Reader in = new BufferedReader(new InputStreamReader(template)); + StringBuilder varBuffer = new StringBuilder(); + int state = 0; + int read; + while ((read = in.read()) >= 0) { + char c = (char) read; + switch (state) { + // initial + case 0: + if (c == '$') { + state = 1; + } else { + out.write(c); + } + break; + // $ read + case 1: + if (c == '{') { + state = 2; + } else { + state = 0; + out.write('$'); + out.write(c); + } + break; + // { read + case 2: + if (c == '}') { + state = 0; + Object prop = getProperty(varBuffer.toString()); + if (prop != null) { + out.write(ResponseUtil.escapeXml(prop.toString())); + } + varBuffer.setLength(0); + } else { + varBuffer.append(c); + } + } + } + in.close(); + out.flush(); + } + +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/JSONResponse.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/JSONResponse.java new file mode 100644 index 000000000..b4e7e4515 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/JSONResponse.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + + +import org.apache.sling.jcr.contentparser.impl.JsonTicksConverter; + +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonStructure; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.StringReader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The JSONResponse is an {@link AbstractPostResponse} preparing + * the response in JSON. + */ +public class JSONResponse extends AbstractPostResponse { + + public static final String RESPONSE_CONTENT_TYPE = "application/json"; + + // package private because it is used by the unit test + static final String PROP_TYPE = "type"; + + // package private because it is used by the unit test + static final String PROP_ARGUMENT = "argument"; + + // package private because it is used by the unit test + static final String RESPONSE_CHARSET = "UTF-8"; + + private static final String PROP_CHANGES = "changes"; + + private Map json = new HashMap<>(); + + private Map jsonCached = new HashMap<>(); + + private List> changes = new ArrayList<>(); + + private Throwable error; + + public void onChange(String type, String... arguments) { + Map change = new HashMap<>(); + change.put(PROP_TYPE, type); + + if (arguments.length > 1) { + change.put(PROP_ARGUMENT, Arrays.asList(arguments)); + } + else if (arguments.length == 1) { + change.put(PROP_ARGUMENT, arguments[0]); + } + changes.add(change); + } + + @Override + public void setError(Throwable error) { + this.error = error; + } + + @Override + public Throwable getError() { + return this.error; + } + + /** + * This method accepts values that correspond to json primitives or otherwise assumes that the toString() of the value + * can be parsed as json. If neither is the case it will throw an Exception. + * + * Assuming the above holds, it will put the value as json directly into the json value part of the response. + * + * @param name name of the property + * @param value value of the property - either of type {String, Boolean, Number, null} + * or the toString() is parseable as json + * @throws JSONResponseException if the value is not usable + */ + @Override + public void setProperty(String name, Object value) { + if (value instanceof String || value instanceof Boolean || value instanceof Number || value == null) { + json.put(name, value); + } + else { + try { + String valueString = JsonTicksConverter.tickToDoubleQuote(value.toString()); + jsonCached.put(name, Json.createReader(new StringReader(valueString)).read()); + json.put(name, value); + } catch (Exception ex) { + throw new JSONResponseException(ex); + } + } + } + + @Override + public Object getProperty(String name) { + return PROP_CHANGES.equals(name) ? getJson().getJsonArray(PROP_CHANGES) : + "error".equals(name) && this.error != null ? getJson().get("error") : json.get(name); + } + + @SuppressWarnings({ "ThrowableResultOfMethodCallIgnored" }) + @Override + protected void doSend(HttpServletResponse response) throws IOException { + + response.setContentType(RESPONSE_CONTENT_TYPE); + response.setCharacterEncoding(RESPONSE_CHARSET); + + Json.createGenerator(response.getWriter()).write(getJson()).close(); + } + + JsonObject getJson() { + JsonObjectBuilder jsonBuilder = Json.createObjectBuilder(); + for (Map.Entry entry : json.entrySet()) { + Object value = entry.getValue(); + if (value instanceof String) { + jsonBuilder.add(entry.getKey(), (String) entry.getValue()); + } + else if (value instanceof Boolean) { + jsonBuilder.add(entry.getKey(), (Boolean) value); + } + else if (value instanceof BigInteger) { + jsonBuilder.add(entry.getKey(), (BigInteger) value); + } + else if (value instanceof BigDecimal) { + jsonBuilder.add(entry.getKey(), (BigDecimal) value); + } + else if (value instanceof Byte) { + jsonBuilder.add(entry.getKey(), (Byte) value); + } + else if (value instanceof Short) { + jsonBuilder.add(entry.getKey(), (Short) value); + } + else if (value instanceof Integer) { + jsonBuilder.add(entry.getKey(), (Integer) value); + } + else if (value instanceof Long) { + jsonBuilder.add(entry.getKey(), (Long) value); + } + else if (value instanceof Double) { + jsonBuilder.add(entry.getKey(), (Double) value); + } + else if (value instanceof Float) { + jsonBuilder.add(entry.getKey(), (Float) value); + } + else if (value == null) { + jsonBuilder.addNull(entry.getKey()); + } + else { + jsonBuilder.add(entry.getKey(), jsonCached.get(entry.getKey())); + } + } + if (this.error != null) { + jsonBuilder + .add("error", Json.createObjectBuilder() + .add("class", error.getClass().getName()) + .add("message", error.getMessage())); + } + JsonArrayBuilder changesBuilder = Json.createArrayBuilder(); + if (this.error == null) { + for (Map entry : changes) { + JsonObjectBuilder entryBuilder = Json.createObjectBuilder(); + entryBuilder.add(PROP_TYPE, (String) entry.get(PROP_TYPE)); + + Object arguments = entry.get(PROP_ARGUMENT); + + if (arguments != null) { + if (arguments instanceof List) { + JsonArrayBuilder argumentsBuilder = Json.createArrayBuilder(); + + for (String argument : ((List) arguments)) { + if(argument != null) { + argumentsBuilder.add(argument); + } + } + + entryBuilder.add(PROP_ARGUMENT, argumentsBuilder); + } else { + entryBuilder.add(PROP_ARGUMENT, (String) arguments); + } + } + changesBuilder.add(entryBuilder); + } + } + jsonBuilder.add(PROP_CHANGES, changesBuilder); + return jsonBuilder.build(); + } + + public class JSONResponseException extends RuntimeException { + public JSONResponseException(String message, Throwable exception) { + super(message, exception); + } + public JSONResponseException(Throwable e) { + super("Error building JSON response", e); + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/Modification.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/Modification.java new file mode 100644 index 000000000..7c014716d --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/Modification.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +public class Modification { + + private final ModificationType type; + + private final String source; + + private final String destination; + + public Modification(final ModificationType type, final String source, + final String destination) { + this.type = type; + this.source = source; + this.destination = destination; + } + + public ModificationType getType() { + return type; + } + + public String getSource() { + return source; + } + + public String getDestination() { + return destination; + } + + /** + * Records a 'modified' change + * + * @param path path of the item that was modified + */ + public static Modification onModified(String path) { + return onChange(ModificationType.MODIFY, path); + } + + /** + * Records a 'created' change + * + * @param path path of the item that was created + */ + public static Modification onCreated(String path) { + return onChange(ModificationType.CREATE, path); + } + + /** + * Records a 'deleted' change + * + * @param path path of the item that was deleted + */ + public static Modification onDeleted(String path) { + return onChange(ModificationType.DELETE, path); + } + + /** + * Records a 'moved' change. + *

+ * Note: the moved change only records the basic move command. the implied + * changes on the moved properties and sub nodes are not recorded. + * + * @param srcPath source path of the node that was moved + * @param dstPath destination path of the node that was moved. + */ + public static Modification onMoved(String srcPath, String dstPath) { + return onChange(ModificationType.MOVE, srcPath, dstPath); + } + + /** + * Records a 'copied' change. + *

+ * Note: the copy change only records the basic copy command. the implied + * changes on the copied properties and sub nodes are not recorded. + * + * @param srcPath source path of the node that was copied + * @param dstPath destination path of the node that was copied. + */ + public static Modification onCopied(String srcPath, String dstPath) { + return onChange(ModificationType.COPY, srcPath, dstPath); + } + + /** + * Records a 'order' change. + * + * @param orderedPath Path of the node that was reordered + * @param beforeSibling Name of the sibling node before which the source node has + * been inserted. + */ + public static Modification onOrder(String orderedPath, String beforeSibling) { + return onChange(ModificationType.ORDER, orderedPath, beforeSibling); + } + + protected static Modification onChange(ModificationType type, String source) { + return onChange(type, source, null); + } + + protected static Modification onChange(ModificationType type, + final String source, final String dest) { + return new Modification(type, source, dest); + } + + public static Modification onCheckin(String path) { + return onChange(ModificationType.CHECKIN, path, null); + } + + public static Modification onCheckout(String path) { + return onChange(ModificationType.CHECKOUT, path, null); + } + + public static Modification onRestore(String path, String version) { + return onChange(ModificationType.RESTORE, path, version); + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Modification[type=").append(type).append(", source=").append(source); + if (destination != null) { + builder.append(", dest=").append(destination); + } + builder.append("]"); + return builder.toString(); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/ModificationType.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/ModificationType.java new file mode 100644 index 000000000..1781a8fc0 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/ModificationType.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +public enum ModificationType { + + /** + * Content has been created or updated. The source path provides the path of + * the modified Item. + */ + MODIFY, + + /** + * An Item has been deleted. The source path provides the path of the + * deleted Item. + */ + DELETE, + + /** + * An Item has been moved to a new location. The source provides the + * original path of the Item, the destination provides the new path of the + * Item. + */ + MOVE, + + /** + * An Item has been copied to a new location. The source path provides the + * path of the copied Item, the destination path provides the path of the + * new Item. + */ + COPY, + + /** + * A Node has been created. The source path provides the path of the newly + * created Node. + */ + CREATE, + + /** + * A child Node has been reordered. The source path provides the path of the + * node, which has been reordered. The destination path provides the name of + * the sibbling node before which the source Node has been ordered. which + * the + */ + ORDER, + + /** + * A Node has been checked out. The source path provides the path of the node. + */ + CHECKOUT, + + /** + * A Node has been checked in. The source path provides the path of the node. + */ + CHECKIN, + + /** + * A Node has been restored to a given version. The soruce path provides the + * path of the node and the destination describes the target version. + */ + RESTORE +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/NodeNameGenerator.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/NodeNameGenerator.java new file mode 100644 index 000000000..02ed40c3f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/NodeNameGenerator.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post; + +import org.apache.sling.api.SlingHttpServletRequest; + +/** + * Service interface which allows for custom node name generation for * resources. + * + */ +public interface NodeNameGenerator { + + /** + * Get the to-be-created node name from the request. + * + * @param request request + * @param parentPath the path to the new node's parent + * @param requirePrefix if true, ignore parameters which do not being with ./ + * @param defaultNodeNameGenerator the default node name generator + * + * @return the node name to be created or null if other NodeNameGenerators should be consulted + */ + public String getNodeName(SlingHttpServletRequest request, String parentPath, boolean requirePrefix, + NodeNameGenerator defaultNodeNameGenerator); +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostOperation.java new file mode 100644 index 000000000..366a29886 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostOperation.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +import org.apache.sling.api.SlingHttpServletRequest; + +/** + * The PostOperation interface defines the service API to be + * implemented by service providers extending the Sling POST servlet. Service + * providers may register OSGi services of this type to be used by the Sling + * default POST servlet to handle specific operations. + *

+ * The PostOperation service must be registered with a + * {@link #PROP_OPERATION_NAME} registration property giving the name(s) of the + * operations supported by the service. The names will be used to find the + * actual operation from the {@link SlingPostConstants#RP_OPERATION + * :operation} request parameter. + *

+ * The Sling POST servlet itself provides various operations (see the + * OPERATION_ constants in the {@link SlingPostConstants} + * interface.These names should not be used by SlingPostOperation + * service providers. + *

+ * This interface replaces the old {@link SlingPostOperation} service interface + * adding support for extensible responses by means of the {@link PostResponse} + * interface as well as operation postprocessing. + *

+ * Implementors of this interface are advised to extend the + * {@link AbstractPostOperation} class to benefit from various precossings + * implemented by that abstract class. + */ +public interface PostOperation { + + /** + * The name of the Sling POST operation service. + */ + public static final String SERVICE_NAME = "org.apache.sling.servlets.post.PostOperation"; + + /** + * The name of the service registration property indicating the name(s) of + * the operation provided by the operation implementation. The value of this + * service property must be a single String or an array or + * java.util.Collection of Strings. If multiple strings are + * defined, the service is registered for all operation names. + */ + public static final String PROP_OPERATION_NAME = "sling.post.operation"; + + /** + * Executes the operation provided by this service implementation. This + * method is called by the Sling POST servlet. + * + * @param request The SlingHttpServletRequest object providing + * the request input for the operation. + * @param response The HtmlResponse into which the operation + * steps should be recorded. + * @param processors The {@link SlingPostProcessor} services to be called + * after applying the operation. This may be null if + * there are none. + * @throws org.apache.sling.api.resource.ResourceNotFoundException May be + * thrown if the operation requires an existing request + * resource. If this exception is thrown the Sling POST servlet + * sends back a 404/NOT FOUND response to the + * client. + * @throws org.apache.sling.api.SlingException May be thrown if an error + * occurrs running the operation. + */ + void run(SlingHttpServletRequest request, PostResponse response, + SlingPostProcessor[] processors); +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponse.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponse.java new file mode 100644 index 000000000..5c8d37022 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponse.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +/** + * The PostResponse interface defines the API of a response + * container which can (and should) be used by {@link PostOperation} services to + * prepare responses to be sent back to the client. + *

+ * This bundle provides a preconfigured {@link HtmlResponse} and a + * {@link JSONResponse} implementation of this interface. Clients may extend the + * {@link AbstractPostResponse} class to provide their own response + * implementations. + */ +public interface PostResponse { + + /** + * Sets the referer property + */ + public void setReferer(String referer); + + /** + * Returns the referer previously set by {@link #setReferer(String)} + */ + public String getReferer(); + + /** + * Sets the absolute path of the item upon which the request operated. + */ + public void setPath(String path); + + /** + * Returns the absolute path of the item upon which the request operated. + *

+ * If the {@link #setPath(String)} method has not been called yet, this + * method returns null. + */ + public String getPath(); + + /** + * Sets whether the request was a create request or not. + */ + public void setCreateRequest(boolean isCreateRequest); + + /** + * Returns true if this was a create request. + *

+ * Before calling the {@link #setCreateRequest(boolean)} method, this method + * always returns false. + */ + public boolean isCreateRequest(); + + /** + * Sets the location of this modification. This is the externalized form of + * the {@link #getPath() current path}. + * + * @param location + */ + public void setLocation(String location); + + /** + * Returns the location of the modification. + *

+ * If the {@link #setLocation(String)} method has not been called yet, this + * method returns null. + */ + public String getLocation(); + + /** + * Sets the parent location of the modification. This is the externalized + * form of the parent node of the {@link #getPath() current path}. + */ + public void setParentLocation(String parentLocation); + + /** + * Returns the parent location of the modification. + *

+ * If the {@link #setParentLocation(String)} method has not been called yet, + * this method returns null. + */ + public String getParentLocation(); + + /** + * Sets the title of the response message + * + * @param title the title + */ + public void setTitle(String title); + + /** + * Sets the response status code properties + * + * @param code the code + * @param message the message + */ + public void setStatus(int code, String message); + + /** + * Returns the status code of this instance. If the status code has never + * been set by calling the {@link #setStatus(int, String)} method, the + * status code is determined by checking if there was an error. If there was + * an error, the response is assumed to be unsuccessful and 500 is returned. + * If there is no error, the response is assumed to be successful and 200 is + * returned. + */ + public int getStatusCode(); + + /** + * Returns the status message or null if no has been set with + * the {@link #setStatus(int, String)} method. + */ + public String getStatusMessage(); + + /** + * Sets the recorded error causing the operation to fail. + */ + public void setError(Throwable error); + + /** + * Returns any recorded error or null + * + * @return an error or null + */ + public Throwable getError(); + + /** + * Returns true if no {@link #getError() error} is set and if + * the {@link #getStatusCode() status code} is one of the 2xx codes. + */ + public boolean isSuccessful(); + + // ---------- ChangeLog ---------------------------------------------------- + + /** + * Records a 'created' change + * + * @param path path of the item that was created + */ + public void onCreated(String path); + + /** + * Records a 'modified' change + * + * @param path path of the item that was modified + */ + public void onModified(String path); + + /** + * Records a 'deleted' change + * + * @param path path of the item that was deleted + */ + public void onDeleted(String path); + + /** + * Records a 'moved' change. + *

+ * Note: the moved change only records the basic move command. the implied + * changes on the moved properties and sub nodes are not recorded. + * + * @param srcPath source path of the node that was moved + * @param dstPath destination path of the node that was moved. + */ + public void onMoved(String srcPath, String dstPath); + + /** + * Records a 'copied' change. + *

+ * Note: the copy change only records the basic copy command. the implied + * changes on the copied properties and sub nodes are not recorded. + * + * @param srcPath source path of the node that was copied + * @param dstPath destination path of the node that was copied. + */ + public void onCopied(String srcPath, String dstPath); + + /** + * Records a generic change of the given type with arguments. + * + * @param type The type of the modification + * @param arguments The arguments to the modifications + */ + void onChange(String type, String... arguments); + + /** + * Writes the response back over the provided HTTP channel. The actual + * format of the response is implementation dependent. + * + * @param response to send to + * @param setStatus whether to set the status code on the response + * @throws IOException if an i/o exception occurs + */ + void send(HttpServletResponse response, boolean setStatus) + throws IOException; + +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseCreator.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseCreator.java new file mode 100644 index 000000000..84ccc96aa --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseCreator.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post; + +import org.apache.sling.api.SlingHttpServletRequest; + +/** + * Service interface which allows for alternate implementations of the + * PostResponse interface to be created as needed. + * + */ +public interface PostResponseCreator { + PostResponse createPostResponse(SlingHttpServletRequest req); +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseWithErrorHandling.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseWithErrorHandling.java new file mode 100644 index 000000000..6900ae00a --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/PostResponseWithErrorHandling.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; + +/** + * @deprecated + */ +@Deprecated +public class PostResponseWithErrorHandling implements PostResponseCreator{ + + @Override + public PostResponse createPostResponse(SlingHttpServletRequest request) { + if (isSendError(request)) { + return new HtmlResponse() { + + @Override + protected void doSend(HttpServletResponse response) throws IOException { + if (!this.isSuccessful()) { + response.sendError(this.getStatusCode(), this.getError().toString()); + return; + }else{ + super.doSend(response); + } + } + }; + }else{ + return null; + } + } + + protected boolean isSendError(SlingHttpServletRequest request){ + boolean sendError=false; + String sendErrorParam=request.getParameter(SlingPostConstants.RP_SEND_ERROR); + if (sendErrorParam!=null && "true".equalsIgnoreCase(sendErrorParam)){ + sendError=true; + } + return sendError; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java new file mode 100644 index 000000000..2b65cbf62 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java @@ -0,0 +1,592 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +/** + * The SlingPostConstants interface provides constants for well + * known parameters of the core SlingPostServlet. Extensions of the servlet + * through implementations of the {@link SlingPostOperation} interface may + * extend this constants. + */ +public interface SlingPostConstants { + + /** + * Prefix for parameter names which control this POST (RP_ stands for + * "request param") (value is ":"). This prefix must be used on all request + * parameters which have significance to POST request processing. Such + * parameters will not be used to denote properties to be written to the + * repository. + */ + public static final String RP_PREFIX = ":"; + + /** + * The name of the parameter containing the operation to execute (value is + * ":operation"). If this parameter is missing or empty, the request is + * assumed to be a request to create new content or to modify existing + * content. + */ + public static final String RP_OPERATION = RP_PREFIX + "operation"; + + /** + * The suffix to the resource path used to indicate to automatically + * generate the name of the new item to create during a content creation + * request (value is "/"). + */ + public static final String DEFAULT_CREATE_SUFFIX = "/"; + + /** + * An alternative suffix to the resource path used to indicate to + * automatically generate the name of the new item to create during a + * content creation request (value is "/*"). + */ + public static final String STAR_CREATE_SUFFIX = "/*"; + + /** + * Name of the predefined modify operation (value is "modify"). + *

+ * The modify operation uses the remaining request parameters to indicate + * nodes and properties to create. + *

+ * The modify operation is actually chosen by the Sling POST Servlet if the + * request has no {@link #RP_OPERATION} request parameter. + * + * @since 2.0.6 (Bundle version 2.0.6) + */ + public static final String OPERATION_MODIFY = "modify"; + + /** + * Name of the predefined delete operation (value is "delete"). + *

+ * The delete operation requires no further request parameters and just + * deletes the content addressed by the request. + *

+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that + * parameter are deleted instead of the request resource. + */ + public static final String OPERATION_DELETE = "delete"; + + /** + * Name of the predefined copy operation (value is "copy"). + *

+ * The copy operation requires the {@link #RP_DEST} request parameter + * denoting the path to copy the content to. In addition the + * {@link #RP_ORDER} parameter may be defined to specificy to relative node + * order of the destination node. Finally the {@link #RP_REPLACE} parameter + * may be set to indicate whether an existing item at the destination should + * be replaced or not. + *

+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that + * parameter are copied instead of the request resource. + */ + public static final String OPERATION_COPY = "copy"; + + /** + * Name of the predefined move operation (value is "move") + *

+ * The move operation requires the {@link #RP_DEST} request parameter + * denoting the path to move the content to. In addition the + * {@link #RP_ORDER} parameter may be defined to specificy to relative node + * order of the destination node. Finally the {@link #RP_REPLACE} parameter + * may be set to indicate whether an existing item at the destination should + * be replaced or not. + *

+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that + * parameter are moved instead of the request resource. + */ + public static final String OPERATION_MOVE = "move"; + + /** + * Name of the predefined null operation (value is "nop"). + *

+ * The null operation is a pseudo operation, which has no effects + * whatsoever except setting the response status. The null operation may + * be accompanied with the {@link #RP_NOP_STATUS} parameter to indicate + * the actual response status to set and the {@link #RP_STATUS} parameter + * to indicate how to send the actual response status. + */ + public static final String OPERATION_NOP = "nop"; + + /** + * Name of the predefined checkin operation (value is "checkin"). + *

+ * The checkin operation requires no further request parameters and just + * checks in the content addressed by the request. + *

+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that + * parameter are checked in instead of the request resource. + */ + public static final String OPERATION_CHECKIN = "checkin"; + + /** + * Name of the predefined checkout operation (value is "checkout"). + *

+ * The checkout operation requires no further request parameters and just + * checks out the content addressed by the request. + *

+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that + * parameter are checked out instead of the request resource. + */ + public static final String OPERATION_CHECKOUT = "checkout"; + + /** + * Name of the predefined restore operation (value is "restore"). + *

+ * The restore operation requires the {@link #RP_VERSION} request parameter + * denoting the name or the label of the version to be restored. + *

+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that + * parameter are restored instead of the request resource. The + * {@link #RP_REMOVE_EXISTING} parameter may be set to true to force + * the operation even if there is a collision. + */ + public static final String OPERATION_RESTORE = "restore"; + + /** + * Name of the predefined import operation (value is "import"). + * + *

+ * The import operation requires either the {@link #RP_CONTENT} and {@link #RP_CONTENT_TYPE} + * request parameters or the {@link #RP_CONTENT_FILE} request parameter. + * Finally the {@link #RP_REPLACE} parameter may be set to indicate whether + * an existing item at the destination should be overwritten or not. + */ + public static final String OPERATION_IMPORT = "import"; + + /** + * Name of the request parameter used to indicate the resource to apply the + * operation to (value is ":applyTo"). + *

+ * This property is used by certain opertaions - namely + * {@link #OPERATION_COPY}, {@link #OPERATION_DELETE} and + * {@link #OPERATION_MOVE} - to apply the operation to multiple resources + * instead of the request resource. + */ + public static final String RP_APPLY_TO = RP_PREFIX + "applyTo"; + + /** + * Name of the request parameter used to indicate the destination for the + * copy and move operations (value is ":dest"). This request parameter is + * required by the copy and move operations. + */ + public static final String RP_DEST = RP_PREFIX + "dest"; + + /** + * Name of the request parameter indicating whether the destination for a + * copy or move operation is to be replaced if existing (value is + * ":replace"). Copy or move is only possible if the destination exists if + * the replace parameter is set to the case-insignificant value true. + * + * This request parameter is also used to indicate whether the destination node + * for an import operation is to be replaced if existing. The parameter value is + * checked to see if it matches the case-insignificant value true. + */ + public static final String RP_REPLACE = RP_PREFIX + "replace"; + + /** + * Name of the request parameter indicating whether the destination for a + * property change during an import operation is to be replaced if existing. + * The parameter value is checked to see if it matches the case-insignificant + * value true. + */ + public static final String RP_REPLACE_PROPERTIES = RP_PREFIX + "replaceProperties"; + + /** + * Optional request parameter indicating the order of newly created nodes in + * creation, copy and move operation requests (value is ":order"). + *

+ * The value of this parameter may be {@link #ORDER_FIRST}, + * {@link #ORDER_BEFORE}, {@link #ORDER_AFTER}, {@link #ORDER_LAST} or a + * numberic value indicating the absolute position in the child list of the + * parent node. + */ + public static final String RP_ORDER = RP_PREFIX + "order"; + + /** + * Name of the request parameter indicating whether the nodes existing + * outside the versioning graph should be removed. See + * {@link javax.jcr.version.VersionManager#restore(javax.jcr.version.Version, boolean) + * VersionManager#restore()} for more info. + *

+ * This request parameter is optional and can be used by the {@link #OPERATION_RESTORE}. + */ + public static final String RP_REMOVE_EXISTING = RP_PREFIX + "removeExisting"; + + /** + * Name of the request parameter indicating the name or the label of the + * resource version. This request parameter is required by the + * {@link #OPERATION_RESTORE}. + */ + public static final String RP_VERSION = RP_PREFIX + "version"; + + /** + * Possible value of the {@link #RP_ORDER} parameter indicating that the + * node by moved to the first position amongst its sibblings (value is + * "first"). + */ + public static final String ORDER_FIRST = "first"; + + /** + * Possible value of the {@link #RP_ORDER} parameter indicating that the + * node by moved immediately before the sibbling whose name is contained in + * the {@link #RP_ORDER} parameter (value is "before "). + */ + public static final String ORDER_BEFORE = "before "; + + /** + * Possible value of the {@link #RP_ORDER} parameter indicating that the + * node by moved immediately after the sibbling whose name is contained in + * the {@link #RP_ORDER} parameter (value is "after "). + */ + public static final String ORDER_AFTER = "after "; + + /** + * Possible value of the {@link #RP_ORDER} parameter indicating that the + * node by moved to the last position amongst its sibblings (value is + * "last"). + */ + public static final String ORDER_LAST = "last"; + + /** + * Optional request paramter specifying a node name for a newly created node + * (value is ":name"). + */ + public static final String RP_NODE_NAME = RP_PREFIX + "name"; + + /** + * Optional request paramter specifying a node name hint for a newly created + * node (value is ":nameHint"). + */ + public static final String RP_NODE_NAME_HINT = RP_PREFIX + "nameHint"; + + /** + * Prefix for properties addressing repository items with an absolute path + * (value is "/"). + * + * @see #ITEM_PREFIX_RELATIVE_CURRENT + */ + public static final String ITEM_PREFIX_ABSOLUTE = "/"; + + /** + * Prefix for properties addressing repository items with a path relative to + * the current request item (value is "./"). + *

+ * When collecting parameters addressing repository items for modification, + * the parameters are first scanned to see whether there is a parameter with + * this relative path prefix. If such a parameter exists, the modification + * operations only assumes parameters whose name is prefixes with this + * prefix or the {@link #ITEM_PREFIX_ABSOLUTE} or the + * {@link #ITEM_PREFIX_RELATIVE_PARENT} to be parameters addressing + * properties to modify. Otherwise, that is if no parameter starts with this + * prefix, all parameters not starting with the + * {@link #RP_PREFIX command prefix} are considered addressing properties to + * modify. + */ + public static final String ITEM_PREFIX_RELATIVE_CURRENT = "./"; + + /** + * Prefix for properties addressing repository items with a path relative to + * the parent of the request item (value is "../"). + * + * @see #ITEM_PREFIX_RELATIVE_CURRENT + */ + public static final String ITEM_PREFIX_RELATIVE_PARENT = "../"; + + /** + * Optional request parameter: redirect to the specified URL after POST + */ + public static final String RP_REDIRECT_TO = RP_PREFIX + "redirect"; + + /** + * Optional request parameter: define how the response is sent back to the + * client. Supported values for this property are + * {@link #STATUS_VALUE_BROWSER} and {@link #STATUS_VALUE_STANDARD}. The + * default is to assume {@link #STATUS_VALUE_STANDARD} if the parameter is + * not set or set to any other value. + */ + public static final String RP_STATUS = RP_PREFIX + "status"; + + /** + * Optional request parameter: defines if to enable the error handling + * also for POST request. + * The parameter value is checked to see if it matches the case-insensitive + * value true. + * + * @since 2.2.0 (Bundle version 2.3.0) + */ + public static final String RP_SEND_ERROR = RP_PREFIX + "sendError"; + + /** + * The supported value for the {@link #RP_STATUS} request parameter + * requesting to report success or failure of request processing using + * standard HTTP status codes. This value is assumed as the default value + * for the {@link #RP_STATUS} parameter if the parameter is missing or not + * any of the two supported values. + * + * @see #RP_STATUS + * @see #STATUS_VALUE_BROWSER + */ + public static final String STATUS_VALUE_STANDARD = "standard"; + + /** + * The supported value for the {@link #RP_STATUS} request parameter + * requesting to not report success or failure of request processing using + * standard HTTP status codes but instead alwas set the status to 200/OK and + * only report the real success or failure status in the XHTML response. + * + * @see #RP_STATUS + * @see #STATUS_VALUE_STANDARD + */ + public static final String STATUS_VALUE_BROWSER = "browser"; + + /** + * Optional request parameter to indicate the actual response status to + * send back as a result of calling the #OPERATION_NOP (value is ":nopstatus"). + *

+ * This parameter is expected to be single-valued and by an integer being a + * valid HTTP status code. If this parameter is missing or the parameter + * value cannot be converted to a HTTP status code (integer in the range + * [100..999]), the default status code 200/OK is returned. + * + * @see #OPERATION_NOP + * @see #RP_STATUS + */ + public static final String RP_NOP_STATUS = RP_PREFIX + "nopstatus"; + + /** + * The default response status sent back by a {@link #OPERATION_NOP} if the + * {@link #RP_NOP_STATUS} parameter is not provided or the parameter value + * cannot be converted into a valid response status code (value is 200). + * + * @see #RP_NOP_STATUS + */ + public static final int NOPSTATUS_VALUE_DEFAULT = 200; + + /** + * Optional request parameter: if provided, added at the end of the computed + * (or supplied) redirect URL + */ + public static final String RP_DISPLAY_EXTENSION = RP_PREFIX + + "displayExtension"; + + /** + * SLING-130, suffix that maps form field names to different JCR property + * names + */ + public static final String VALUE_FROM_SUFFIX = "@ValueFrom"; + + /** + * Suffix indicating a type hint for the property (value is "@TypeHint"). + */ + public static final String TYPE_HINT_SUFFIX = "@TypeHint"; + + /** + * Suffix indicating a default value for a property (value is + * "@DefaultValue"). + */ + public static final String DEFAULT_VALUE_SUFFIX = "@DefaultValue"; + + /** + * Suffix indicating that the named property is to be removed before + * applying any new content (value is "@Delete"). + */ + public static final String SUFFIX_DELETE = "@Delete"; + + /** + * Suffix indicating that the named item is to be set from an item whose + * absolute or relative path is given in the parameter's value (value is + * "@MoveFrom"). + *

+ * This suffix is similar to the {@link #VALUE_FROM_SUFFIX} in that the + * value for the item is not taken from the request parameter itself but + * from somewhere else. In this case the value is set by moving another + * repository item (in the same workspace) to the location addressed by the + * parameter. + */ + public static final String SUFFIX_MOVE_FROM = "@MoveFrom"; + + /** + * Suffix indicating that the named item is to be set from an item whose + * absolute or relative path is given in the parameter's value (value is + * "@CopyFrom"). + *

+ * This suffix is similar to the {@link #VALUE_FROM_SUFFIX} in that the + * value for the item is not taken from the request parameter itself but + * from somewhere else. In this case the value is set by copying another + * repository item (in the same workspace) to the location addressed by the + * parameter. + */ + public static final String SUFFIX_COPY_FROM = "@CopyFrom"; + + /** + * Suffix indicating that blank value or values for this property will be + * ignored. + */ + public static final String SUFFIX_IGNORE_BLANKS = "@IgnoreBlanks"; + + /** + * Suffix indicating that the default value should be used when the property + * is not defined. By default the default value is only used when the property + * is defined, but blank (i.e. an empty form field). With this suffix, the + * default value will also be used if the property isn't provided at all. This is + * useful for HTML checkboxes. + */ + public static final String SUFFIX_USE_DEFAULT_WHEN_MISSING = "@UseDefaultWhenMissing"; + + /** + * Suffix indicating that a multi-value property is to be handled as an + * ordered set and the sent values start with either "+" or "-" to indicate + * wether a value should be added to or removed from the set. + *

+ * If a property is marked to be patched with this suffix only properties + * whose value start with {@link #PATCH_ADD +} or {@link #PATCH_REMOVE -} + * are considered. Other values are ignored. + * + * @see #PATCH_ADD + * @see #PATCH_REMOVE + */ + public static final String SUFFIX_PATCH = "@Patch"; + + /** + * Indicates a value to be added to the named multi-value property if the + * property is being #{@link #SUFFIX_PATCH patched}. + *

+ * If the given value + * already exists amongst the values of the multi-value properties it is + * not added. + */ + public static final char PATCH_ADD = '+'; + + /** + * Indicates a value to be removed from the named multi-value property if + * the property is being #{@link #SUFFIX_PATCH patched}. + *

+ * If the given value exists multiple times amongst the values of the + * multi-value properties all occurrences are removed. + */ + public static final char PATCH_REMOVE = '-'; + + /** + * Name of the request parameter containing the content to be imported + * by the 'import' operation. + */ + public static final String RP_CONTENT = RP_PREFIX + "content"; + + /** + * Name of the request parameter containing the content type of the content + * to be imported by the 'import' operation. + */ + public static final String RP_CONTENT_TYPE = RP_PREFIX + "contentType"; + + /** + * Name of the request parameter containing the file to be imported + * by the 'import' operation. + */ + public static final String RP_CONTENT_FILE = RP_PREFIX + "contentFile"; + + /** + * Name of the request parameter indicating whether versionable nodes should + * be checked in during an {@link SlingPostConstants#OPERATION_IMPORT} operation. + */ + public static final String RP_CHECKIN = RP_PREFIX + "checkin"; + + /** + * Name of the request parameter indicating whether versionable nodes should + * be checked in during an {@link SlingPostConstants#OPERATION_IMPORT} operation. + * + * @since 2.1.2 + */ + public static final String RP_AUTO_CHECKOUT = RP_PREFIX + "autoCheckout"; + + /** + * Name of the request attribute (not parameter) indicating that a post operation + * should not invoke the commit method on the resource resolver upon completion. + * + * @since 2.1.2 + */ + public static final String ATTR_SKIP_SESSION_HANDLING = "skip-session-handling"; + + /** + * Name of the request parameter indicating offset of the chunk in request. + * @since 2.3.4 + */ + public static final String SUFFIX_OFFSET = "@Offset"; + + /** + * Name of the request parameter indicating length of complete file. + * @since 2.3.4 + */ + public static final String SUFFIX_LENGTH = "@Length"; + + /** + * Name of the request parameter indicating request contains last chunk + * and as a result upload should be finished. It is useful in scenarios + * like file streaming where file size is not known in advance. + * @since 2.3.4 + */ + public static final String SUFFIX_COMPLETED = "@Completed"; + + /** + * Name of the request parameter indicating request operation is applicable + * to chunks. + * @since 2.3.4 + */ + public static final String RP_APPLY_TO_CHUNKS = RP_PREFIX + "applyToChunks"; + + /** + * Constant for the sling:chunks mixin. Used to identify that node + * contains chunks. + * @since 2.3.4 + */ + public static final String NT_SLING_CHUNK_MIXIN = "sling:chunks"; + + /** + * Constant for the sling:fileLength property. The property stores file + * length. + * @since 2.3.4 + */ + public static final String NT_SLING_FILE_LENGTH = "sling:fileLength"; + + /** + * Constant for the sling:length property. The property stores + * cumulative length of all uploaded chunks. + * @since 2.3.4 + */ + public static final String NT_SLING_CHUNKS_LENGTH = "sling:length"; + + /** + * Constant for the sling:chunk node type. The node type is used + * to store chunk. + * @since 2.3.4 + */ + public static final String NT_SLING_CHUNK_NODETYPE = "sling:chunk"; + + /** + * Constant for the sling:offset property. The property stores start + * offset of chunk. + * @since 2.3.4 + */ + public static final String NT_SLING_CHUNK_OFFSET = "sling:offset"; + + /** + * Constant for prefix for sling:chunk node name. + * @since 2.3.4 + */ + public static final String CHUNK_NODE_NAME = "chunk"; + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java new file mode 100644 index 000000000..4d430cb56 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.servlets.HtmlResponse; + +/** + * The SlingPostOperation interface defines the service API to be + * implemented by service providers extending the Sling default POST servlet. + * Service providers may register OSGi services of this type to be used by the + * Sling default POST servlet to handle specific operations. + *

+ * The SlingPostOperation service must be registered with a + * {@link #PROP_OPERATION_NAME} registration property giving the name(s) of the + * operations supported by the service. The names will be used to find the + * actual operation from the {@link SlingPostConstants#RP_OPERATION + * :operation} request parameter. + *

+ * The Sling default POST servlet defines the copy, + * move and delete operation names. These names should + * not be used by SlingPostOperation service providers. + * + * @deprecated as of 2.0.8 (Bundle version 2.2.0) and replaced by + * {@link PostOperation}. + */ +@Deprecated +public interface SlingPostOperation { + + /** + * The name of the Sling POST operation service. + */ + public static final String SERVICE_NAME = "org.apache.sling.servlets.post.SlingPostOperation"; + + /** + * The name of the service registration property indicating the name(s) of + * the operation provided by the operation implementation (value is + * "sling.post.operation"). The value of this service property must be a + * single String or an array or java.util.Collection of + * Strings. If multiple strings are defined, the service is registered for + * all operation names. + */ + public static final String PROP_OPERATION_NAME = "sling.post.operation"; + + /** + * Executes the operation provided by this service implementation. This + * method is called by the Sling default POST servlet. + * + * @param request The SlingHttpServletRequest object providing + * the request input for the operation. + * @param response The HtmlResponse into which the operation + * steps should be recorded. + * @param processors The {@link SlingPostProcessor} services to be called + * after applying the operation. This may be null if + * there are none. + * @throws org.apache.sling.api.resource.ResourceNotFoundException May be + * thrown if the operation requires an existing request + * resource. If this exception is thrown the Sling default POST + * servlet sends back a 404/NOT FOUND response to + * the client. + * @throws org.apache.sling.api.SlingException May be thrown if an error + * occurrs running the operation. + */ + void run(SlingHttpServletRequest request, HtmlResponse response, + SlingPostProcessor[] processors); +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostProcessor.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostProcessor.java new file mode 100644 index 000000000..4d64f3dc9 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/SlingPostProcessor.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post; + +import java.util.List; + +import org.apache.sling.api.SlingHttpServletRequest; + +/** + * The SlingPostProcessor interface defines a service API to be + * implemented by service providers extending the Sling default POST servlet. + * Service providers may register OSGi services of this type to be used by the + * Sling default POST servlet to handle specific operations. + *

+ * During a request the SlingPostOperation service is called + * with a list of registered post processors. After the operation has performed + * its changes but before the changes are persisted, all post processors + * are called. + */ +public interface SlingPostProcessor { + + /** + * Process the current request. + * The post processor can inspect the list of changes and perform additional + * changes. If the processor performs a change it should make the change + * and add a {@link Modification} object to the changes list. + * @param request The current request. + * @param changes The list of changes for this request. + * @throws Exception If an error occurs. + */ + void process(SlingHttpServletRequest request, List changes) + throws Exception; +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/VersioningConfiguration.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/VersioningConfiguration.java new file mode 100644 index 000000000..fd64e3be0 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/VersioningConfiguration.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post; + + +/** + * Data structure to hold the various options associated with how versionable + * nodes are handled in the post servlet. + */ +public class VersioningConfiguration implements Cloneable { + + private boolean autoCheckout = false; + + private boolean checkinOnNewVersionableNode = false; + + private boolean autoCheckin = true; + + @Override + public VersioningConfiguration clone() { + VersioningConfiguration cfg = new VersioningConfiguration(); + cfg.checkinOnNewVersionableNode = checkinOnNewVersionableNode; + cfg.autoCheckout = autoCheckout; + cfg.autoCheckin = autoCheckin; + return cfg; + } + + public boolean isAutoCheckout() { + return autoCheckout; + } + + public boolean isCheckinOnNewVersionableNode() { + return checkinOnNewVersionableNode; + } + + public boolean isAutoCheckin() { + return autoCheckin; + } + + public void setAutoCheckin(boolean autoCheckin) { + this.autoCheckin = autoCheckin; + } + + public void setAutoCheckout(boolean autoCheckout) { + this.autoCheckout = autoCheckout; + } + + public void setCheckinOnNewVersionableNode(boolean checkinOnNewVersionableNode) { + this.checkinOnNewVersionableNode = checkinOnNewVersionableNode; + } + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostOperationProxyProvider.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostOperationProxyProvider.java new file mode 100644 index 000000000..5ad331d33 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostOperationProxyProvider.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.IdentityHashMap; +import java.util.Map; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.servlets.HtmlResponse; +import org.apache.sling.servlets.post.PostOperation; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostOperation; +import org.apache.sling.servlets.post.SlingPostProcessor; +import org.apache.sling.servlets.post.impl.helper.HtmlResponseProxy; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The PostOperationProxyProvider listens for legacy + * {@link SlingPostOperation} services being registered and wraps them with a + * proxy for the new {@link PostOperation} API and registers the procies. + */ +@Component(service = {}) +public class PostOperationProxyProvider implements ServiceListener { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * The service listener filter to listen for SlingPostOperation services + */ + private static final String REFERENCE_FILTER = "(" + Constants.OBJECTCLASS + + "=" + SlingPostOperation.SERVICE_NAME + ")"; + + // maps references to the SlingPostOperation services to the registrations + // of the PostOperation proxies for unregistration purposes + private final Map proxies = new IdentityHashMap<>(); + + // The DS component context to access the services to proxy + private BundleContext bundleContext; + + // DS activation/deactivation + + /** + * Activates the proxy provider component: + *

    + *
  1. Keep BundleContext reference
  2. + *
  3. Start listening for SlingPostOperation services
  4. + *
  5. Register proxies for all existing SlingPostOperation services
  6. + *
+ */ + @SuppressWarnings("unused") + @Activate + private void activate(final BundleContext bundleContext) { + this.bundleContext = bundleContext; + + try { + bundleContext.addServiceListener(this, REFERENCE_FILTER); + final ServiceReference[] serviceReferences = bundleContext.getServiceReferences( + SlingPostOperation.SERVICE_NAME, null); + if (serviceReferences != null) { + for (ServiceReference serviceReference : serviceReferences) { + register(serviceReference); + } + } + } catch (InvalidSyntaxException ise) { + // not expected for tested static filter + // TODO:log !! + } + } + + /** + * Deactivates the proxy provide component: + *
    + *
  1. Unregister as a service listener
  2. + *
  3. Unregister all proxies
  4. + *
  5. Drop BundleContext reference
  6. + *
+ */ + @SuppressWarnings("unused") + @Deactivate + private void deactivate() { + + this.bundleContext.removeServiceListener(this); + + final ServiceReference[] serviceReferences; + synchronized (this.proxies) { + serviceReferences = this.proxies.keySet().toArray( + new ServiceReference[this.proxies.size()]); + } + + for (ServiceReference serviceReference : serviceReferences) { + unregister(serviceReference); + } + + this.bundleContext = null; + } + + // ServiceEvent handling + + @Override + public void serviceChanged(ServiceEvent event) { + + /* + * There is a slight chance for a race condition on deactivation where + * the component may be deactivating and the bundle context reference + * has been removed but the framework is still sending service events. + * In this situation we don't want to handle the event any way and so we + * can safely ignore it + */ + if (this.bundleContext == null) { + return; + } + + switch (event.getType()) { + case ServiceEvent.REGISTERED: + register(event.getServiceReference()); + break; + case ServiceEvent.MODIFIED: + update(event.getServiceReference()); + break; + case ServiceEvent.UNREGISTERING: + unregister(event.getServiceReference()); + break; + } + } + + /** + * Access SlingPostOperation service and register proxy. + *

+ * Called by serviceChanged + */ + private void register(final ServiceReference serviceReference) { + final SlingPostOperation service = (SlingPostOperation) this.bundleContext.getService(serviceReference); + final PostOperationProxy proxy = new PostOperationProxy(service); + + final BundleContext bundleContext = serviceReference.getBundle().getBundleContext(); + final Dictionary props = copyServiceProperties(serviceReference); + final ServiceRegistration reg = bundleContext.registerService( + PostOperation.SERVICE_NAME, proxy, props); + + log.debug("Registering {}", proxy); + synchronized (this.proxies) { + this.proxies.put(serviceReference, reg); + } + } + + /** + * Update proxy service registration properties + *

+ * Called by serviceChanged + */ + private void update(final ServiceReference serviceReference) { + final ServiceRegistration proxyRegistration; + synchronized (this.proxies) { + proxyRegistration = this.proxies.get(serviceReference); + } + + if (proxyRegistration != null) { + log.debug("Updating {}", proxyRegistration); + proxyRegistration.setProperties(copyServiceProperties(serviceReference)); + } + } + + /** + * Unregister proxy and unget SlingPostOperation service + *

+ * Called by serviceChanged + */ + private void unregister(final ServiceReference serviceReference) { + final ServiceRegistration proxyRegistration; + synchronized (this.proxies) { + proxyRegistration = this.proxies.remove(serviceReference); + } + + if (proxyRegistration != null) { + log.debug("Unregistering {}", proxyRegistration); + this.bundleContext.ungetService(serviceReference); + proxyRegistration.unregister(); + } + } + + // Helpers + + /** + * Creates a Dictionary for use as the service registration properties of + * the PostOperation proxy. + */ + private Dictionary copyServiceProperties( + final ServiceReference serviceReference) { + final Dictionary props = new Hashtable<>(); + for (String key : serviceReference.getPropertyKeys()) { + props.put(key, serviceReference.getProperty(key)); + } + props.put(PostOperation.PROP_OPERATION_NAME, + serviceReference.getProperty(SlingPostOperation.PROP_OPERATION_NAME)); + props.put(Constants.SERVICE_DESCRIPTION, "Proxy for " + + serviceReference); + return props; + } + + /** + * The PostOperationProxy is the proxy implementing the + * {@link PostOperation} service interface by calling the + * {@link SlingPostOperation} service. + */ + private class PostOperationProxy implements PostOperation { + + private final SlingPostOperation delegatee; + + PostOperationProxy(final SlingPostOperation delegatee) { + this.delegatee = delegatee; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " for " + delegatee.getClass().getName(); + } + + @Override + public void run(SlingHttpServletRequest request, PostResponse response, + SlingPostProcessor[] processors) { + HtmlResponse apiResponse = new HtmlResponseProxy(response); + delegatee.run(request, apiResponse, processors); + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostResponseWithErrorHandling.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostResponseWithErrorHandling.java new file mode 100644 index 000000000..a9af0860a --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/PostResponseWithErrorHandling.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.servlets.post.HtmlResponse; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.PostResponseCreator; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.osgi.service.component.annotations.Component; + +@Component(service = PostResponseCreator.class, + property = { + "service.vendor=The Apache Software Foundation" + }) +public class PostResponseWithErrorHandling implements PostResponseCreator { + + @Override + public PostResponse createPostResponse(SlingHttpServletRequest request) { + if (isSendError(request)) { + return new HtmlResponse() { + + @Override + protected void doSend(HttpServletResponse response) throws IOException { + if (!this.isSuccessful()) { + response.sendError(this.getStatusCode(), this.getError().toString()); + return; + }else{ + super.doSend(response); + } + } + }; + }else{ + return null; + } + } + + protected boolean isSendError(SlingHttpServletRequest request){ + boolean sendError=false; + String sendErrorParam=request.getParameter(SlingPostConstants.RP_SEND_ERROR); + if (sendErrorParam!=null && "true".equalsIgnoreCase(sendErrorParam)){ + sendError=true; + } + return sendError; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java new file mode 100644 index 000000000..86f607c88 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java @@ -0,0 +1,792 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.ResourceNotFoundException; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.servlets.SlingAllMethodsServlet; +import org.apache.sling.jcr.contentloader.ContentImporter; +import org.apache.sling.servlets.post.HtmlResponse; +import org.apache.sling.servlets.post.JSONResponse; +import org.apache.sling.servlets.post.NodeNameGenerator; +import org.apache.sling.servlets.post.PostOperation; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.PostResponseCreator; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.SlingPostProcessor; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.apache.sling.servlets.post.impl.helper.DateParser; +import org.apache.sling.servlets.post.impl.helper.DefaultNodeNameGenerator; +import org.apache.sling.servlets.post.impl.helper.JCRSupport; +import org.apache.sling.servlets.post.impl.helper.MediaRangeList; +import org.apache.sling.servlets.post.impl.operations.CheckinOperation; +import org.apache.sling.servlets.post.impl.operations.CheckoutOperation; +import org.apache.sling.servlets.post.impl.operations.CopyOperation; +import org.apache.sling.servlets.post.impl.operations.DeleteOperation; +import org.apache.sling.servlets.post.impl.operations.ImportOperation; +import org.apache.sling.servlets.post.impl.operations.ModifyOperation; +import org.apache.sling.servlets.post.impl.operations.MoveOperation; +import org.apache.sling.servlets.post.impl.operations.NopOperation; +import org.apache.sling.servlets.post.impl.operations.RestoreOperation; +import org.apache.sling.servlets.post.impl.operations.StreamedUploadOperation; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * POST servlet that implements the sling client library "protocol" + */ +@Component(service = Servlet.class, + property = { + "service.description=Sling Post Servlet", + "service.vendor=The Apache Software Foundation", + "sling.servlet.prefix:Integer=-1", + "sling.servlet.paths=sling/servlet/default/POST" + }) +@Designate(ocd = SlingPostServlet.Config.class) +public class SlingPostServlet extends SlingAllMethodsServlet { + + private static final long serialVersionUID = 1837674988291697074L; + + @ObjectClassDefinition(name = "Apache Sling POST Servlet", + description="The Sling POST Servlet is registered as the default " + + "servlet to handle POST requests in Sling.") + public @interface Config { + + @AttributeDefinition(name = "Date Format", + description = "List SimpleDateFormat strings for date "+ + "formats supported for parsing from request input to data fields. The special "+ + "format \"ISO8601\" (without the quotes) can be used to designate strict ISO-8601 "+ + "parser which is able to parse strings generated by the Property.getString() "+ + "method for Date properties. The default "+ + "value is [ \"EEE MMM dd yyyy HH:mm:ss 'GMT'Z\", \"ISO8601\", "+ + "\"yyyy-MM-dd'T'HH:mm:ss.SSSZ\", "+ + "\"yyyy-MM-dd'T'HH:mm:ss\", \"yyyy-MM-dd\", \"dd.MM.yyyy HH:mm:ss\", \"dd.MM.yyyy\" ].") + String[] servlet_post_dateFormats() default { "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", "ISO8601", + "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd", + "dd.MM.yyyy HH:mm:ss", "dd.MM.yyyy" }; + + @AttributeDefinition(name = "Node Name Hint Properties", + description = "The list of properties whose values "+ + "may be used to derive a name for newly created nodes. When handling a request "+ + "to create a new node, the name of the node is automatically generated if the "+ + "request URL ends with a star (\"*\") or a slash (\"/\"). In this case the request "+ + "parameters listed in this configuration value may be used to create the name. "+ + "Default value is [ \"title\", \"jcr:title\", \"name\", \"description\", "+ + "\"jcr:description\", \"abstract\", \"text\", \"jcr:text\" ].") + String[] servlet_post_nodeNameHints() default { "title", "jcr:title", "name", "description", + "jcr:description", "abstract", "text", "jcr:text" }; + + @AttributeDefinition(name = "Maximum Node Name Length", + description = "Maximum number of characters to "+ + "use for automatically generated node names. The default value is 20. Note, "+ + "that actual node names may be generated with at most 4 more characters if the "+ + "numeric suffixes must be appended to make the name unique.") + int servlet_post_nodeNameMaxLength() default 20; + + @AttributeDefinition(name = "Checkin New Versionable Nodes", + description = "If true, newly created "+ + "versionable nodes or non-versionable nodes which are made versionable by the "+ + "addition of the mix:versionable mixin are checked in. By default, false.") + boolean servlet_post_checkinNewVersionableNodes() default false; + + @AttributeDefinition(name = "Auto Checkout Nodes", + description = "If true, checked in nodes are "+ + "checked out when necessary. By default, false.") + boolean servlet_post_autoCheckout() default false; + + @AttributeDefinition(name = "Auto Checkin Nodes", + description = "If true, nodes which are checked out "+ + "by the post servlet are checked in. By default, true.") + boolean servlet_post_autoCheckin() default true; + + @AttributeDefinition(name = "Ignored Parameters", + description = "Configures a regular expression "+ + "pattern to select request parameters which should be ignored when writing "+ + "content to the repository. By default this is \"j_.*\" thus ignoring all "+ + "request parameters starting with j_ such as j_username.") + String servlet_post_ignorePattern() default "j_.*"; + } + + /** + * default log + */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + private static final String PARAM_CHECKIN_ON_CREATE = ":checkinNewVersionableNodes"; + + private static final String PARAM_AUTO_CHECKOUT = ":autoCheckout"; + + private static final String PARAM_AUTO_CHECKIN = ":autoCheckin"; + + private final ModifyOperation modifyOperation = new ModifyOperation(); + + private final StreamedUploadOperation streamedUploadOperation = new StreamedUploadOperation(); + + private ServiceRegistration[] internalOperations; + + /** Map of post operations. */ + private final Map postOperations = new HashMap<>(); + + /** Sorted list of post processor holders. */ + private final List postProcessors = new ArrayList<>(); + + /** Cached list of post processors, used during request processing. */ + private SlingPostProcessor[] cachedPostProcessors = new SlingPostProcessor[0]; + + /** Sorted list of node name generator holders. */ + private final List nodeNameGenerators = new ArrayList<>(); + + /** Cached list of node name generators used during request processing. */ + private NodeNameGenerator[] cachedNodeNameGenerators = new NodeNameGenerator[0]; + + /** Sorted list of post response creator holders. */ + private final List postResponseCreators = new ArrayList<>(); + + /** Cached array of post response creators used during request processing. */ + private PostResponseCreator[] cachedPostResponseCreators = new PostResponseCreator[0]; + + private VersioningConfiguration baseVersioningConfiguration; + + private ImportOperation importOperation; + + public SlingPostServlet() { + // the following operations require JCR: + if ( JCRSupport.INSTANCE.jcrEnabled()) { + try { + importOperation = new ImportOperation(); + } catch ( final Throwable t) { + log.warn("Support for JCR operations like checkin, checkout, import, ordering etc. is currently disabled " + + "in the servlets post module. Check whether the JCR API is available.", t); + } + } + } + + @Override + protected void doPost(final SlingHttpServletRequest request, + final SlingHttpServletResponse response) throws IOException { + final VersioningConfiguration localVersioningConfig = createRequestVersioningConfiguration(request); + + request.setAttribute(VersioningConfiguration.class.getName(), localVersioningConfig); + + // prepare the response + final PostResponse htmlResponse = createPostResponse(request); + htmlResponse.setReferer(request.getHeader("referer")); + + final PostOperation operation = getSlingPostOperation(request); + if (operation == null) { + + htmlResponse.setStatus( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Invalid operation specified for POST request"); + + } else { + request.getRequestProgressTracker().log( + "Calling PostOperation: {0}", operation.getClass().getName()); + final SlingPostProcessor[] processors = this.cachedPostProcessors; + try { + operation.run(request, htmlResponse, processors); + } catch (ResourceNotFoundException rnfe) { + htmlResponse.setStatus(HttpServletResponse.SC_NOT_FOUND, + rnfe.getMessage()); + } catch (final Exception exception) { + log.warn("Exception while handling POST " + + request.getResource().getPath() + " with " + + operation.getClass().getName(), exception); + htmlResponse.setError(exception); + } + + } + + // check for redirect URL if processing succeeded + if (htmlResponse.isSuccessful()) { + if (redirectIfNeeded(request, htmlResponse, response)) { + return; + } + } + + // create a html response and send if unsuccessful or no redirect + htmlResponse.send(response, isSetStatus(request)); + } + + /** + * Redirects the HttpServletResponse, if redirectURL is not empty + * @param htmlResponse + * @param request + * @param response The HttpServletResponse to use for redirection + * @return Whether a redirect was requested + * @throws IOException + */ + boolean redirectIfNeeded(final SlingHttpServletRequest request, final PostResponse htmlResponse, final SlingHttpServletResponse response) + throws IOException { + final String redirectURL = getRedirectUrl(request, htmlResponse); + if (redirectURL != null) { + final Matcher m = REDIRECT_WITH_SCHEME_PATTERN.matcher(redirectURL); + final boolean hasScheme = m.matches(); + final String encodedURL; + if (hasScheme && m.group(2).length() > 0) { + encodedURL = m.group(1) + response.encodeRedirectURL(m.group(2)); + } else if (hasScheme) { + encodedURL = redirectURL; + } else { + log.debug("Request path is [{}]", request.getPathInfo()); + encodedURL = response.encodeRedirectURL(redirectURL); + } + log.debug("redirecting to URL [{}] - encoded as [{}]", redirectURL, encodedURL); + response.sendRedirect(encodedURL); + return true; + } + return false; + } + private static final Pattern REDIRECT_WITH_SCHEME_PATTERN = Pattern.compile("^(https?://[^/]+)(.*)$"); + + /** + * Creates an instance of a PostResponse. + * @param req The request being serviced + * @return a {@link org.apache.sling.servlets.post.impl.helper.JSONResponse} if any of these conditions are true: + *

    + *
  • the request has an Accept header of application/json
  • + *
  • the request is a JSON POST request (see SLING-1172)
  • + *
  • the request has a request parameter :accept=application/json
  • + *
+ * or a {@link org.apache.sling.api.servlets.PostResponse} otherwise + */ + PostResponse createPostResponse(final SlingHttpServletRequest req) { + for (final PostResponseCreator creator : cachedPostResponseCreators) { + final PostResponse response = creator.createPostResponse(req); + if (response != null) { + return response; + } + } + + // Fall through to default behavior + final MediaRangeList mediaRangeList = new MediaRangeList(req); + if (JSONResponse.RESPONSE_CONTENT_TYPE.equals(mediaRangeList.prefer("text/html", JSONResponse.RESPONSE_CONTENT_TYPE))) { + return new JSONResponse(); + } else { + return new HtmlResponse(); + } + } + + private PostOperation getSlingPostOperation( + final SlingHttpServletRequest request) { + if (streamedUploadOperation.isRequestStreamed(request)) { + return streamedUploadOperation; + } + final String operation = request.getParameter(SlingPostConstants.RP_OPERATION); + if (operation == null || operation.length() == 0) { + // standard create/modify operation; + return modifyOperation; + } + + // named operation, retrieve from map + synchronized ( this.postOperations ) { + return postOperations.get(operation); + } + } + + /** + * compute redirect URL (SLING-126) + * + * @param ctx the post processor + * @return the redirect location or null + */ + protected String getRedirectUrl(final SlingHttpServletRequest request, final PostResponse ctx) { + // redirect param has priority (but see below, magic star) + String result = request.getParameter(SlingPostConstants.RP_REDIRECT_TO); + if (result != null) { + try { + URI redirectUri = new URI(result); + if (redirectUri.getAuthority() != null) { + // if it has a host information + log.warn("redirect target ({}) does include host information ({}). This is not allowed for security reasons!", result, redirectUri.getAuthority()); + return null; + } + } catch (URISyntaxException e) { + log.warn("given redirect target ({}) is not a valid uri: {}", result, e); + return null; + } + + log.debug("redirect requested as [{}] for path [{}]", result, ctx.getPath()); + + // redirect to created/modified Resource + final int star = result.indexOf('*'); + if (star >= 0 && ctx.getPath() != null) { + final StringBuilder buf = new StringBuilder(); + + // anything before the star + if (star > 0) { + buf.append(result.substring(0, star)); + } + + // append the name of the manipulated node + buf.append(ResourceUtil.getName(ctx.getPath())); + + // anything after the star + if (star < result.length() - 1) { + buf.append(result.substring(star + 1)); + } + + // Prepend request path if it ends with create suffix and result isn't absolute + final String requestPath = request.getPathInfo(); + if (requestPath.endsWith(SlingPostConstants.DEFAULT_CREATE_SUFFIX) && buf.charAt(0) != '/' && + !REDIRECT_WITH_SCHEME_PATTERN.matcher(buf).matches()) { + buf.insert(0, requestPath); + } + + // use the created path as the redirect result + result = buf.toString(); + + } else if (result.endsWith(SlingPostConstants.DEFAULT_CREATE_SUFFIX)) { + // if the redirect has a trailing slash, append modified node + // name + result = result.concat(ResourceUtil.getName(ctx.getPath())); + } + + log.debug("Will redirect to {}", result); + } + return result; + } + + protected boolean isSetStatus(final SlingHttpServletRequest request) { + final String statusParam = request.getParameter(SlingPostConstants.RP_STATUS); + if (statusParam == null) { + log.debug( + "getStatusMode: Parameter {} not set, assuming standard status code", + SlingPostConstants.RP_STATUS); + return true; + } + + if (SlingPostConstants.STATUS_VALUE_BROWSER.equals(statusParam)) { + log.debug( + "getStatusMode: Parameter {} asks for user-friendly status code", + SlingPostConstants.RP_STATUS); + return false; + } + + if (SlingPostConstants.STATUS_VALUE_STANDARD.equals(statusParam)) { + log.debug( + "getStatusMode: Parameter {} asks for standard status code", + SlingPostConstants.RP_STATUS); + return true; + } + + log.debug( + "getStatusMode: Parameter {} set to unknown value {}, assuming standard status code", + SlingPostConstants.RP_STATUS); + return true; + } + + // ---------- SCR Integration ---------------------------------------------- + + @SuppressWarnings("unchecked") + @Activate + protected void activate(final BundleContext bundleContext, + final Config configuration) { + // configure now + this.configure(configuration); + + // other predefined operations + final ArrayList> providedServices = new ArrayList<>(); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_MODIFY, modifyOperation)); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_COPY, new CopyOperation())); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_MOVE, new MoveOperation())); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_DELETE, new DeleteOperation())); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_NOP, new NopOperation())); + + // the following operations require JCR: + if ( JCRSupport.INSTANCE.jcrEnabled() && importOperation != null) { + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_IMPORT, importOperation)); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_CHECKIN, new CheckinOperation())); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_CHECKOUT, new CheckoutOperation())); + providedServices.add(registerOperation(bundleContext, + SlingPostConstants.OPERATION_RESTORE, new RestoreOperation())); + } + internalOperations = providedServices.toArray(new ServiceRegistration[providedServices.size()]); + } + + private ServiceRegistration registerOperation(final BundleContext context, + final String opCode, final PostOperation operation) { + final Hashtable properties = new Hashtable<>(); + properties.put(PostOperation.PROP_OPERATION_NAME, opCode); + properties.put(Constants.SERVICE_DESCRIPTION, + "Apache Sling POST Servlet Operation " + opCode); + properties.put(Constants.SERVICE_VENDOR, + context.getBundle().getHeaders().get(Constants.BUNDLE_VENDOR)); + return context.registerService(PostOperation.class, operation, + properties); + } + + @Override + public void init() throws ServletException { + modifyOperation.setServletContext(getServletContext()); + streamedUploadOperation.setServletContext(getServletContext()); + } + + @Modified + private void configure(final Config configuration) { + this.baseVersioningConfiguration = createBaseVersioningConfiguration(configuration); + + final DateParser dateParser = new DateParser(); + final String[] dateFormats = configuration.servlet_post_dateFormats(); + for (String dateFormat : dateFormats) { + try { + dateParser.register(dateFormat); + } catch (Throwable t) { + log.warn( + "configure: Ignoring DateParser format {} because it is invalid: {}", + dateFormat, t); + } + } + + final String[] nameHints = configuration.servlet_post_nodeNameHints(); + final int nameMax = configuration.servlet_post_nodeNameMaxLength(); + final NodeNameGenerator nodeNameGenerator = new DefaultNodeNameGenerator( + nameHints, nameMax); + + final String paramMatch = configuration.servlet_post_ignorePattern(); + final Pattern paramMatchPattern = Pattern.compile(paramMatch); + + this.modifyOperation.setDateParser(dateParser); + this.modifyOperation.setDefaultNodeNameGenerator(nodeNameGenerator); + this.modifyOperation.setIgnoredParameterNamePattern(paramMatchPattern); + if ( this.importOperation != null ) { + this.importOperation.setDefaultNodeNameGenerator(nodeNameGenerator); + this.importOperation.setIgnoredParameterNamePattern(paramMatchPattern); + } + } + + @Override + public void destroy() { + modifyOperation.setServletContext(null); + streamedUploadOperation.setServletContext(null); + } + + @Deactivate + protected void deactivate() { + if (internalOperations != null) { + for (final ServiceRegistration registration : internalOperations) { + registration.unregister(); + } + internalOperations = null; + } + modifyOperation.setExtraNodeNameGenerators(null); + if ( this.importOperation != null ) { + this.importOperation = null; + } + } + + /** + * Bind a new post operation + */ + @Reference(service = PostOperation.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC) + protected void bindPostOperation(final PostOperation operation, final Map properties) { + final String operationName = (String) properties.get(PostOperation.PROP_OPERATION_NAME); + if ( operationName != null && operation != null ) { + synchronized (this.postOperations) { + this.postOperations.put(operationName, operation); + } + } + } + + /** + * Unbind a post operation + */ + protected void unbindPostOperation(final PostOperation operation, final Map properties) { + final String operationName = (String) properties.get(PostOperation.PROP_OPERATION_NAME); + if ( operationName != null ) { + synchronized (this.postOperations) { + this.postOperations.remove(operationName); + } + } + } + + private int getRanking(final Map properties) { + final Object val = properties.get(Constants.SERVICE_RANKING); + return val instanceof Integer ? (Integer)val : 0; + } + + /** + * Bind a new post processor + */ + @Reference(service = SlingPostProcessor.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC) + protected void bindPostProcessor(final SlingPostProcessor processor, final Map properties) { + final PostProcessorHolder pph = new PostProcessorHolder(); + pph.processor = processor; + pph.ranking = getRanking(properties); + + synchronized ( this.postProcessors ) { + int index = 0; + while ( index < this.postProcessors.size() && + pph.ranking < this.postProcessors.get(index).ranking ) { + index++; + } + if ( index == this.postProcessors.size() ) { + this.postProcessors.add(pph); + } else { + this.postProcessors.add(index, pph); + } + this.updatePostProcessorCache(); + } + } + + /** + * Unbind a post processor + */ + protected void unbindPostProcessor(final SlingPostProcessor processor, final Map properties) { + synchronized ( this.postProcessors ) { + final Iterator i = this.postProcessors.iterator(); + while ( i.hasNext() ) { + final PostProcessorHolder current = i.next(); + if ( current.processor == processor ) { + i.remove(); + } + } + this.updatePostProcessorCache(); + } + } + + /** + * Update the post processor cache + * This method is called by sync'ed methods, no need to add additional syncing. + */ + private void updatePostProcessorCache() { + final SlingPostProcessor[] localCache = new SlingPostProcessor[this.postProcessors.size()]; + int index = 0; + for(final PostProcessorHolder current : this.postProcessors) { + localCache[index] = current.processor; + index++; + } + this.cachedPostProcessors = localCache; + } + + /** + * Bind a new node name generator + */ + @Reference(service = NodeNameGenerator.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC) + protected void bindNodeNameGenerator(final NodeNameGenerator generator, final Map properties) { + final NodeNameGeneratorHolder nngh = new NodeNameGeneratorHolder(); + nngh.generator = generator; + nngh.ranking = getRanking(properties); + + synchronized ( this.nodeNameGenerators ) { + int index = 0; + while ( index < this.nodeNameGenerators.size() && + nngh.ranking < this.nodeNameGenerators.get(index).ranking ) { + index++; + } + if ( index == this.nodeNameGenerators.size() ) { + this.nodeNameGenerators.add(nngh); + } else { + this.nodeNameGenerators.add(index, nngh); + } + this.updateNodeNameGeneratorCache(); + } + } + + /** + * Unbind a node name generator + */ + protected void unbindNodeNameGenerator(final NodeNameGenerator generator, final Map properties) { + synchronized ( this.nodeNameGenerators ) { + final Iterator i = this.nodeNameGenerators.iterator(); + while ( i.hasNext() ) { + final NodeNameGeneratorHolder current = i.next(); + if ( current.generator == generator ) { + i.remove(); + } + } + this.updateNodeNameGeneratorCache(); + } + } + + /** + * Update the node name generator cache + * This method is called by sync'ed methods, no need to add additional syncing. + */ + private void updateNodeNameGeneratorCache() { + final NodeNameGenerator[] localCache = new NodeNameGenerator[this.nodeNameGenerators.size()]; + int index = 0; + for(final NodeNameGeneratorHolder current : this.nodeNameGenerators) { + localCache[index] = current.generator; + index++; + } + this.cachedNodeNameGenerators = localCache; + this.modifyOperation.setExtraNodeNameGenerators(this.cachedNodeNameGenerators); + if ( this.importOperation != null ) { + this.importOperation.setExtraNodeNameGenerators(this.cachedNodeNameGenerators); + } + } + + /** + * Bind a new post response creator + */ + @Reference(service = PostResponseCreator.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC) + protected void bindPostResponseCreator(final PostResponseCreator creator, final Map properties) { + final PostResponseCreatorHolder nngh = new PostResponseCreatorHolder(); + nngh.creator = creator; + nngh.ranking = getRanking(properties); + + synchronized ( this.postResponseCreators ) { + int index = 0; + while ( index < this.postResponseCreators.size() && + nngh.ranking < this.postResponseCreators.get(index).ranking ) { + index++; + } + if ( index == this.postResponseCreators.size() ) { + this.postResponseCreators.add(nngh); + } else { + this.postResponseCreators.add(index, nngh); + } + this.updatePostResponseCreatorCache(); + } + } + + /** + * Unbind a post response creator + */ + protected void unbindPostResponseCreator(final PostResponseCreator creator, final Map properties) { + synchronized ( this.postResponseCreators ) { + final Iterator i = this.postResponseCreators.iterator(); + while ( i.hasNext() ) { + final PostResponseCreatorHolder current = i.next(); + if ( current.creator == creator ) { + i.remove(); + } + } + this.updatePostResponseCreatorCache(); + } + } + + /** + * Update the post response creator cache + * This method is called by sync'ed methods, no need to add additional syncing. + */ + private void updatePostResponseCreatorCache() { + final PostResponseCreator[] localCache = new PostResponseCreator[this.postResponseCreators.size()]; + int index = 0; + for(final PostResponseCreatorHolder current : this.postResponseCreators) { + localCache[index] = current.creator; + index++; + } + this.cachedPostResponseCreators = localCache; + } + + @Reference(service = ContentImporter.class, + cardinality = ReferenceCardinality.OPTIONAL, + policy = ReferencePolicy.DYNAMIC, + policyOption = ReferencePolicyOption.GREEDY) + protected void bindContentImporter(final Object importer) { + if ( this.importOperation != null ) { + importOperation.setContentImporter(importer); + } + } + + protected void unbindContentImporter(final Object importer) { + if ( this.importOperation != null ) { + importOperation.unsetContentImporter(importer); + } + } + + private VersioningConfiguration createBaseVersioningConfiguration(Config config) { + VersioningConfiguration cfg = new VersioningConfiguration(); + cfg.setCheckinOnNewVersionableNode(config.servlet_post_checkinNewVersionableNodes()); + cfg.setAutoCheckout(config.servlet_post_autoCheckout()); + cfg.setAutoCheckin(config.servlet_post_autoCheckin()); + return cfg; + } + + private VersioningConfiguration createRequestVersioningConfiguration(SlingHttpServletRequest request) { + VersioningConfiguration cfg = baseVersioningConfiguration.clone(); + + String paramValue = request.getParameter(PARAM_CHECKIN_ON_CREATE); + if (paramValue != null) { + cfg.setCheckinOnNewVersionableNode(Boolean.parseBoolean(paramValue)); + } + paramValue = request.getParameter(PARAM_AUTO_CHECKOUT); + if (paramValue != null) { + cfg.setAutoCheckout(Boolean.parseBoolean(paramValue)); + } + paramValue = request.getParameter(PARAM_AUTO_CHECKIN); + if (paramValue != null) { + cfg.setAutoCheckin(Boolean.parseBoolean(paramValue)); + } + return cfg; + } + + private static final class PostProcessorHolder { + public SlingPostProcessor processor; + public int ranking; + } + + private static final class NodeNameGeneratorHolder { + public NodeNameGenerator generator; + public int ranking; + } + + private static final class PostResponseCreatorHolder { + public PostResponseCreator creator; + public int ranking; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/Chunk.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/Chunk.java new file mode 100644 index 000000000..b95e079b1 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/Chunk.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +/** + * Chunk encapsulates all chunk upload attributes. + * + * @since 2.3.4 + */ +public class Chunk { + + private long offset; + + private long length; + + private boolean completed; + + /** + * Return offset of the chunk. + */ + public long getOffset() { + return offset; + } + + /** + * Set offset value. + */ + public void setOffsetValue(long offset) { + this.offset = offset; + } + + /** + * Return length of the file parameter. + */ + public long getLength() { + return length; + } + + /** + * Set length of file parameter. + */ + public void setLength(long length) { + this.length = length; + } + + /** + * Return true if request contains last chunk as a result upload should be + * finished. It is useful in scenarios where file streaming where file size + * is not known in advance. + */ + public boolean isCompleted() { + return completed; + } + + /** + * Set complete flag + */ + public void setCompleted(boolean complete) { + this.completed = complete; + } + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ChunkCleanUpTask.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ChunkCleanUpTask.java new file mode 100644 index 000000000..6d8724063 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ChunkCleanUpTask.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.util.Calendar; +import java.util.Iterator; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceResolverFactory; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The ChunkCleanUpTask implements a job run at regular intervals + * to find incomplete chunk uploads and remove them from the repository to + * prevent littering the repository with incomplete chunks. + *

+ * This task is configured with OSGi configuration for the PID + * org.apache.sling.servlets.post.impl.helper.ChunkCleanUpTask with + * property scheduler.expression being the schedule to execute the + * task. The schedule is a cron job expression as described at Cron + * Trigger with the default value configured to run twice a day at 0h41m31s + * and 12h4131s. + *

+ * The property chunk.cleanup.age specifies chunk's age in minutes + * before it is considered for clean up. + *

+ * Currently the cleanup tasks connects as the administrative user to the + * default workspace assuming users are stored in that workspace and the + * administrative user has full access. + */ +@Component(service = Runnable.class, + property = { + "service.description=Periodic Chunk Cleanup Job", + "service.vendor=The Apache Software Foundation" + }) +@Designate(ocd = ChunkCleanUpTask.Config.class) +public class ChunkCleanUpTask implements Runnable { + + @ObjectClassDefinition(name = "Apache Sling Post Chunk Upload : Cleanup Task", + description = "Task to regularly purge incomplete chunks from the repository") + public @interface Config { + + @AttributeDefinition(name = "Schedule", description = "Cron expression scheudling this job. Default is hourly 17m23s after the hour. " + + "See http://www.docjar.com/docs/api/org/quartz/CronTrigger.html for a description " + + "of the format for this value.") + String scheduler_expression() default "31 41 0/12 * * ?"; + + @AttributeDefinition(name = "scheduler.concurrent", + description = "Allow Chunk Cleanup Task to run concurrently (default: false).") + boolean scheduler_concurrent() default false; + + @AttributeDefinition(name = "Cleanup Age", + description = "The chunk's age in minutes before it is considered for clean up.") + int chunk_cleanup_age() default 360; + } + + /** default log */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Reference + private ResourceResolverFactory rrFactory; + + private SlingFileUploadHandler uploadhandler = new SlingFileUploadHandler(); + + /** + * Clean up age criterion in millisec. + */ + private long chunkCleanUpAge; + + /** + * Executes the job. Is called for each triggered schedule point. + */ + @Override + public void run() { + log.debug("ChunkCleanUpTask: Starting cleanup"); + cleanup(); + } + + /** + * This method deletes chunks which are {@link #isEligibleForCleanUp(Resource)} + * for cleanup. It queries all + * {@link SlingPostConstants#NT_SLING_CHUNK_MIXIN} nodes and filter nodes + * which are {@link #isEligibleForCleanUp(Resource)} for cleanup. It then + * deletes old chunks upload. + */ + private void cleanup() { + + long start = System.currentTimeMillis(); + + int numCleaned = 0; + int numLive = 0; + + ResourceResolver admin = null; + try { + admin = rrFactory.getAdministrativeResourceResolver(null); + + final Iterator rsrcIter = admin.findResources( + "SELECT * FROM [sling:chunks] ", "sql"); + while (rsrcIter.hasNext()) { + final Resource rsrc = rsrcIter.next(); + if (isEligibleForCleanUp(rsrc)) { + numCleaned++; + uploadhandler.deleteChunks(rsrc); + } else { + numLive++; + } + } + if (admin.hasChanges()) { + try { + admin.refresh(); + admin.commit(); + } catch (PersistenceException re) { + log.info("ChunkCleanUpTask: Failed persisting chunk removal. Retrying later"); + } + } + + } catch (Throwable t) { + log.error( + "ChunkCleanUpTask: General failure while trying to cleanup chunks", + t); + } finally { + if (admin != null) { + admin.close(); + } + } + long end = System.currentTimeMillis(); + log.info( + "ChunkCleanUpTask finished: Removed {} chunk upload(s) in {}ms ({} chunk upload(s) still active)", + new Object[] { numCleaned, (end - start), numLive }); + } + + /** + * Check if {@link Resource} is eligible of + * {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} cleanup. To be + * eligible the age of last + * {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} uploaded should be + * greater than @link {@link #chunkCleanUpAge} + * + * @param rsrc {@link Resource} containing + * {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} + * {@link Resource}s + * @return true if eligible else false. + */ + private boolean isEligibleForCleanUp(Resource rsrc) { + boolean result = false; + final Resource lastChunkNode = uploadhandler.getLastChunk(rsrc); + if ( lastChunkNode != null ) { + final Calendar created = lastChunkNode.getValueMap().get(JcrConstants.JCR_CREATED, Calendar.class); + if ( created != null && System.currentTimeMillis() - created.getTimeInMillis() > chunkCleanUpAge ) { + result = true; + } + } + return result; + } + + @Activate + protected void activate(final Config configuration) { + chunkCleanUpAge = configuration.chunk_cleanup_age(); + log.info("scheduler config [{}], chunkGarbageTime [{}] ms", + configuration.scheduler_expression(), + chunkCleanUpAge); + + } +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java new file mode 100644 index 000000000..2268b1c1f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import org.apache.jackrabbit.util.ISO8601; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Takes a string representation of a time-date string and tries for parse it + * using different formats. + */ +public class DateParser { + + /** + * default log + */ + private static final Logger log = LoggerFactory.getLogger(DateParser.class); + + /** + * lits of formats + */ + private final List formats = new LinkedList<>(); + + /** + * Registers a format string to the list of internally checked ones. + * Uses the {@link SimpleDateFormat}. + * @param format format as in {@link SimpleDateFormat} + * @throws IllegalArgumentException if the format is not valid. + */ + public void register(String format) { + final CalendarParserSupport parser; + if (Iso8601ParserSupport.FORMAT_MARKER.equalsIgnoreCase(format)) { + parser = new Iso8601ParserSupport(); + } else { + parser = new SimpleDateFormatParserSupport(format); + } + formats.add(parser); + } + + /** + * Parses the given source string and returns the respective calendar + * instance. If no format matches returns null. + *

+ * + * @param source date time source string + * @return calendar representation of the source or null + */ + public Calendar parse(String source) { + for (CalendarParserSupport fmt : formats) { + try { + final Calendar c = fmt.parse(source); + if (log.isDebugEnabled()) { + log.debug("Parsed " + source + " using " + fmt + " into " + + c); + } + return c; + } catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Failed parsing " + source + " using " + fmt); + } + } + } + return null; + } + + /** + * Parses the given source strings and returns the respective calendar + * instances. If no format matches for any of the sources + * returns null. + *

+ * + * @param sources date time source strings + * @return calendar representations of the source or null + */ + public Calendar[] parse(String sources[]) { + Calendar ret[] = new Calendar[sources.length]; + for (int i=0; i< sources.length; i++) { + if ((ret[i] = parse(sources[i])) == null) { + return null; + } + } + return ret; + } + + private static interface CalendarParserSupport { + Calendar parse(String dateTime) throws ParseException; + } + + private static class SimpleDateFormatParserSupport implements CalendarParserSupport { + private final SimpleDateFormat dateFormat; + + SimpleDateFormatParserSupport(String format) { + this.dateFormat = new SimpleDateFormat(format, Locale.US); + } + + @Override + public Calendar parse(String dateTime) throws ParseException { + final Date d; + synchronized (dateFormat) { + d = dateFormat.parse(dateTime); + } + + final Calendar c = Calendar.getInstance(); + c.setTime(d); + + return c; + } + + @Override + public String toString() { + return "SimpleDateFormat:" + dateFormat.toPattern(); + } + } + + private static class Iso8601ParserSupport implements CalendarParserSupport { + + static final String FORMAT_MARKER = "ISO8601"; + + @Override + public Calendar parse(String dateTime) throws ParseException { + try { + final Calendar c = ISO8601.parse(dateTime); + if (c == null) { + throw new ParseException(dateTime + + " cannot be parsed as ISO8601 formatted date string", + 0); + } + return c; + } catch (Exception e) { + throw new ParseException(e.getMessage(), 0); + } + } + + @Override + public String toString() { + return "ISO8601 Parser"; + } + } + +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DefaultNodeNameGenerator.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DefaultNodeNameGenerator.java new file mode 100644 index 000000000..2fcd92634 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/DefaultNodeNameGenerator.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.request.RequestParameterMap; +import org.apache.sling.servlets.post.NodeNameGenerator; +import org.apache.sling.servlets.post.SlingPostConstants; + +/** + * Generates a node name based on a set of well-known request parameters + * like title, description, etc. + * See SLING-128. + */ +public class DefaultNodeNameGenerator implements NodeNameGenerator { + + private final String[] parameterNames; + private final NodeNameFilter filter = new NodeNameFilter(); + + public static final int DEFAULT_MAX_NAME_LENGTH = 20; + + private int maxLength = DEFAULT_MAX_NAME_LENGTH; + private int counter; + + public DefaultNodeNameGenerator() { + this(null, -1); + } + + public DefaultNodeNameGenerator(String[] parameterNames, int maxNameLength) { + if (parameterNames == null) { + this.parameterNames = new String[0]; + } else { + this.parameterNames = parameterNames; + } + + this.maxLength = (maxNameLength > 0) + ? maxNameLength + : DEFAULT_MAX_NAME_LENGTH; + } + + /** + * Get a "nice" node name, if possible, based on given request + * + * @param request the request + * @param basePath the base path + * @param requirePrefix true if the parameter names for + * properties requires a prefix + * @param defaultNodeNameGenerator a default generator + * @return a nice node name + */ + public String getNodeName(SlingHttpServletRequest request, String basePath, + boolean requirePrefix, NodeNameGenerator defaultNodeNameGenerator) { + RequestParameterMap parameters = request.getRequestParameterMap(); + String valueToUse = null; + boolean doFilter = true; + + // find the first request parameter that matches one of + // our parameterNames, in order, and has a value + if (parameters!=null) { + // we first check for the special sling parameters + RequestParameter specialParam = parameters.getValue(SlingPostConstants.RP_NODE_NAME); + if ( specialParam != null ) { + if ( specialParam.getString() != null && specialParam.getString().length() > 0 ) { + valueToUse = specialParam.getString(); + doFilter = false; + } + } + if ( valueToUse == null ) { + specialParam = parameters.getValue(SlingPostConstants.RP_NODE_NAME_HINT); + if ( specialParam != null ) { + if ( specialParam.getString() != null && specialParam.getString().length() > 0 ) { + valueToUse = specialParam.getString(); + } + } + } + + if (valueToUse == null) { + for (String param : parameterNames) { + if (valueToUse != null) { + break; + } + if (requirePrefix) { + param = SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT.concat(param); + } + final RequestParameter[] pp = parameters.get(param); + if (pp != null) { + for (RequestParameter p : pp) { + valueToUse = p.getString(); + if (valueToUse != null && valueToUse.length() > 0) { + break; + } + valueToUse = null; + } + } + } + } + } + String result; + // should we filter? + if (valueToUse != null) { + if ( doFilter ) { + // filter value so that it works as a node name + result = filter.filter(valueToUse); + } else { + result = valueToUse; + } + } else { + // default value if none provided + result = nextCounter() + "_" + System.currentTimeMillis(); + } + + if ( doFilter ) { + // max length + if (result.length() > maxLength) { + result = result.substring(0,maxLength); + } + } + + return result; + } + + public synchronized int nextCounter() { + return ++counter; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlPostResponseProxy.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlPostResponseProxy.java new file mode 100644 index 000000000..3ae5da214 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlPostResponseProxy.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.servlets.HtmlResponse; +import org.apache.sling.servlets.post.PostResponse; + +/** + * The HtmlPostResponseProxy class implements the + * {@link PostResponse} interface using a Sling API HtmlResponse. + *

+ * This class is mainly used by the deprecated + * {@link org.apache.sling.servlets.post.AbstractSlingPostOperation} for + * bridging into the new + * {@link org.apache.sling.servlets.post.AbstractPostOperation}. + * @deprecated + */ +@Deprecated +public class HtmlPostResponseProxy implements PostResponse { + + private final HtmlResponse apiHtmlResponse; + + public HtmlPostResponseProxy(final HtmlResponse apiHtmlResponse) { + this.apiHtmlResponse = apiHtmlResponse; + } + + public HtmlResponse getHtmlResponse() { + return apiHtmlResponse; + } + + @Override + public Throwable getError() { + return apiHtmlResponse.getError(); + } + + @Override + public String getLocation() { + return apiHtmlResponse.getLocation(); + } + + @Override + public String getParentLocation() { + return apiHtmlResponse.getParentLocation(); + } + + @Override + public String getPath() { + return apiHtmlResponse.getPath(); + } + + public Type getProperty(String name, Class type) { + return apiHtmlResponse.getProperty(name, type); + } + + public Object getProperty(String name) { + return apiHtmlResponse.getProperty(name); + } + + @Override + public String getReferer() { + return apiHtmlResponse.getReferer(); + } + + @Override + public int getStatusCode() { + return apiHtmlResponse.getStatusCode(); + } + + @Override + public String getStatusMessage() { + return apiHtmlResponse.getStatusMessage(); + } + + @Override + public boolean isCreateRequest() { + return apiHtmlResponse.isCreateRequest(); + } + + @Override + public boolean isSuccessful() { + return apiHtmlResponse.isSuccessful(); + } + + @Override + public void onChange(String type, String... arguments) { + apiHtmlResponse.onChange(type, arguments); + } + + @Override + public void onCopied(String srcPath, String dstPath) { + apiHtmlResponse.onCopied(srcPath, dstPath); + } + + @Override + public void onCreated(String path) { + apiHtmlResponse.onCreated(path); + } + + @Override + public void onDeleted(String path) { + apiHtmlResponse.onDeleted(path); + } + + @Override + public void onModified(String path) { + apiHtmlResponse.onModified(path); + } + + @Override + public void onMoved(String srcPath, String dstPath) { + apiHtmlResponse.onMoved(srcPath, dstPath); + } + + @Override + public void send(HttpServletResponse response, boolean setStatus) + throws IOException { + apiHtmlResponse.send(response, setStatus); + } + + @Override + public void setCreateRequest(boolean isCreateRequest) { + apiHtmlResponse.setCreateRequest(isCreateRequest); + } + + @Override + public void setError(Throwable error) { + apiHtmlResponse.setError(error); + } + + @Override + public void setLocation(String location) { + apiHtmlResponse.setLocation(location); + } + + @Override + public void setParentLocation(String parentLocation) { + apiHtmlResponse.setParentLocation(parentLocation); + } + + @Override + public void setPath(String path) { + apiHtmlResponse.setPath(path); + } + + public void setProperty(String name, Object value) { + apiHtmlResponse.setProperty(name, value); + } + + @Override + public void setReferer(String referer) { + apiHtmlResponse.setReferer(referer); + } + + @Override + public void setStatus(int code, String message) { + apiHtmlResponse.setStatus(code, message); + } + + @Override + public void setTitle(String title) { + apiHtmlResponse.setTitle(title); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxy.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxy.java new file mode 100644 index 000000000..492b9f00f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxy.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.servlets.HtmlResponse; +import org.apache.sling.servlets.post.PostResponse; + +/** + * The HtmlResponseProxy extends the Sling API + * HtmlResponse overwriting all public methods and redirecting to a + * proxied {@link PostResponse}. As a consequence the underlying (extended) + * Sling API HtmlResponse will not be fed with data and thus will + * remain "empty". + *

+ * This class is mainly used by the deprecated + * {@link org.apache.sling.servlets.post.AbstractSlingPostOperation} for + * bridging into the new + * {@link org.apache.sling.servlets.post.AbstractPostOperation}. + */ +public class HtmlResponseProxy extends HtmlResponse { + + private final PostResponse postResponse; + private boolean createRequest; + + public HtmlResponseProxy(final PostResponse postResponse) { + if(postResponse == null) { + throw new IllegalArgumentException("Null PostResponse, cannot build HtmlResponseProxy"); + } + this.postResponse = postResponse; + postResponse.setCreateRequest(createRequest); + } + + public PostResponse getPostResponse() { + return postResponse; + } + + public Type getProperty(String name, Class type) { + // return postResponse.getProperty(name, type); + return null; + } + + public Object getProperty(String name) { + // return postResponse.getProperty(name); + return null; + } + + public void setProperty(String name, Object value) { + // postResponse.setProperty(name, value); + } + + public Throwable getError() { + return postResponse.getError(); + } + + public String getLocation() { + return postResponse.getLocation(); + } + + public String getParentLocation() { + return postResponse.getParentLocation(); + } + + public String getPath() { + return postResponse.getPath(); + } + + public String getReferer() { + return postResponse.getReferer(); + } + + public int getStatusCode() { + return postResponse.getStatusCode(); + } + + public String getStatusMessage() { + return postResponse.getStatusMessage(); + } + + public boolean isCreateRequest() { + return postResponse.isCreateRequest(); + } + + public boolean isSuccessful() { + return postResponse.isSuccessful(); + } + + public void onChange(String type, String... arguments) { + postResponse.onChange(type, arguments); + } + + public void onCopied(String srcPath, String dstPath) { + postResponse.onCopied(srcPath, dstPath); + } + + public void onCreated(String path) { + postResponse.onCreated(path); + } + + public void onDeleted(String path) { + postResponse.onDeleted(path); + } + + public void onModified(String path) { + postResponse.onModified(path); + } + + public void onMoved(String srcPath, String dstPath) { + postResponse.onMoved(srcPath, dstPath); + } + + public void send(HttpServletResponse response, boolean setStatus) + throws IOException { + postResponse.send(response, setStatus); + } + + public void setCreateRequest(boolean isCreateRequest) { + createRequest = isCreateRequest; + if(postResponse != null) { + // ugly...needed because of SLING-2453, this is called + // by the base class's constructor before postResponse is set + postResponse.setCreateRequest(isCreateRequest); + } + } + + public void setError(Throwable error) { + postResponse.setError(error); + } + + public void setLocation(String location) { + postResponse.setLocation(location); + } + + public void setParentLocation(String parentLocation) { + postResponse.setParentLocation(parentLocation); + } + + public void setPath(String path) { + postResponse.setPath(path); + } + + public void setReferer(String referer) { + postResponse.setReferer(referer); + } + + public void setStatus(int code, String message) { + postResponse.setStatus(code, message); + } + + public void setTitle(String title) { + postResponse.setTitle(title); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupport.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupport.java new file mode 100644 index 000000000..c09cf04d3 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupport.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.util.List; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JCRSupport { + + public static final JCRSupport INSTANCE = new JCRSupport(); + + /** Logger. */ + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final Object supportImpl; + + public JCRSupport() { + Object impl = null; + try { + impl = new JCRSupportImpl(); + } catch ( final Throwable t) { + logger.warn("Support for JCR operations like checkin, checkout, import, ordering etc. is currently disabled " + + "in the servlets post module. Check whether the JCR API is available."); + } + this.supportImpl = impl; + } + + public void orderNode(final SlingHttpServletRequest request, + final Resource resource, + final List changes) throws PersistenceException { + if ( supportImpl != null ) { + ((JCRSupportImpl)supportImpl).orderNode(request, resource, changes); + } + } + + public boolean checkin(final Resource rsrc) + throws PersistenceException { + if ( rsrc != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).checkin(rsrc); + } + return false; + } + + public void checkoutIfNecessary(final Resource rsrc, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + if ( rsrc != null && supportImpl != null ) { + ((JCRSupportImpl)supportImpl).checkoutIfNecessary(rsrc, changes, versioningConfiguration); + } + } + + public boolean isNode(final Resource rsrc) { + if ( rsrc != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isNode(rsrc); + } + return false; + } + + public boolean isVersionable(final Resource rsrc) throws PersistenceException { + if ( supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isVersionable(rsrc); + } + return false; + } + + public boolean isNodeType(final Resource rsrc, final String typeHint) { + if ( rsrc != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isNodeType(rsrc, typeHint); + } + return false; + } + + public Boolean isFileNodeType(final ResourceResolver resolver, final String nodeType) { + if ( supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isFileNodeType(resolver, nodeType); + } + return false; + } + + public boolean isPropertyProtectedOrNewAutoCreated(final Object node, final String name) + throws PersistenceException { + if ( node != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isPropertyProtectedOrNewAutoCreated(node, name); + } + return false; + } + + public boolean isPropertyMandatory(final Object node, final String name) + throws PersistenceException { + if ( node != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isPropertyMandatory(node, name); + } + return false; + } + + public boolean isPropertyMultiple(final Object node, final String name) + throws PersistenceException { + if ( node != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isPropertyMultiple(node, name); + } + return false; + } + + public boolean isNewNode(final Object node) { + if ( node != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).isNewNode(node); + } + return true; + } + + public Integer getPropertyType(final Object node, final String name) + throws PersistenceException { + if ( node != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).getPropertyType(node, name); + } + return null; + } + + public boolean hasSession(final ResourceResolver resolver) { + if ( supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).hasSession(resolver); + } + return false; + } + + /** + * Stores property value(s) as reference(s). Will parse the reference(s) from the string + * value(s) in the {@link RequestProperty}. + * + * @return true only if parsing was successful and the property was actually changed + */ + public Modification storeAsReference( + final Resource resource, + final Object node, + final String name, + final String[] values, + final int type, + final boolean multiValued) + throws PersistenceException { + if ( node != null && supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).storeAsReference(node, name, values, type, multiValued); + } + throw new PersistenceException("Resource " + resource.getPath() + " does not support reference properties.", null, resource.getPath(), name); + } + + public void setTypedProperty(final Object n, + final String name, + final String[] values, + final int type, + final boolean multiValued) + throws PersistenceException { + if ( n != null && supportImpl != null ) { + ((JCRSupportImpl)supportImpl).setTypedProperty(n, name, values, type, multiValued); + } else { + throw new PersistenceException("Property should be stored through JCR but JCR support is not available"); + } + } + + public Object getNode(final Resource rsrc) { + if ( supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).getNode(rsrc); + } + return null; + } + + public Object getItem(final Resource rsrc) { + if ( supportImpl != null ) { + return ((JCRSupportImpl)supportImpl).getItem(rsrc); + } + return null; + } + + public void setPrimaryNodeType(final Object node, final String type) + throws PersistenceException { + if ( node != null && supportImpl != null ) { + ((JCRSupportImpl)supportImpl).setPrimaryNodeType(node, type); + } else { + throw new PersistenceException("Node type should be set but JCR support is not available"); + } + } + + public String copy(Object src, Object dstParent, String name) + throws PersistenceException { + // the caller already got an item and a node, so supportImpl is available + return ((JCRSupportImpl)supportImpl).copy(src, dstParent, name); + } + + public void move(Object src, Object dstParent, String name) + throws PersistenceException { + // the caller already got an item and a node, so supportImpl is available + ((JCRSupportImpl)supportImpl).move(src, dstParent, name); + } + + public boolean jcrEnabled() { + return this.supportImpl != null; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupportImpl.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupportImpl.java new file mode 100644 index 000000000..724928f69 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/JCRSupportImpl.java @@ -0,0 +1,559 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.util.List; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JCRSupportImpl { + + /** Logger. */ + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * Orders the given node according to the specified command. The following + * syntax is supported: <xmp> | first | before all child nodes | before A | + * before child node A | after A | after child node A | last | after all + * nodes | N | at a specific position, N being an integer </xmp> + * + * @param request The http request + * @param item node to order + * @param changes The list of modifications + * @throws RepositoryException if an error occurs + */ + public void orderNode(final SlingHttpServletRequest request, + final Resource resource, + final List changes) throws PersistenceException { + + final String command = request.getParameter(SlingPostConstants.RP_ORDER); + if (command == null || command.length() == 0) { + // nothing to do + return; + } + + final Node node = resource.adaptTo(Node.class); + if (node == null) { + return; + } + + try { + final Node parent = node.getParent(); + + String next = null; + if (command.equals(SlingPostConstants.ORDER_FIRST)) { + + next = parent.getNodes().nextNode().getName(); + + } else if (command.equals(SlingPostConstants.ORDER_LAST)) { + + next = ""; + + } else if (command.startsWith(SlingPostConstants.ORDER_BEFORE)) { + + next = command.substring(SlingPostConstants.ORDER_BEFORE.length()); + + } else if (command.startsWith(SlingPostConstants.ORDER_AFTER)) { + + String name = command.substring(SlingPostConstants.ORDER_AFTER.length()); + NodeIterator iter = parent.getNodes(); + while (iter.hasNext()) { + Node n = iter.nextNode(); + if (n.getName().equals(name)) { + if (iter.hasNext()) { + next = iter.nextNode().getName(); + } else { + next = ""; + } + } + } + + } else { + // check for integer + try { + // 01234 + // abcde move a -> 2 (above 3) + // bcade move a -> 1 (above 1) + // bacde + int newPos = Integer.parseInt(command); + next = ""; + NodeIterator iter = parent.getNodes(); + while (iter.hasNext() && newPos >= 0) { + Node n = iter.nextNode(); + if (n.getName().equals(node.getName())) { + // if old node is found before index, need to + // inc index + newPos++; + } + if (newPos == 0) { + next = n.getName(); + break; + } + newPos--; + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "provided node ordering command is invalid: " + command); + } + } + + if (next != null) { + if (next.equals("")) { + next = null; + } + parent.orderBefore(node.getName(), next); + changes.add(Modification.onOrder(node.getPath(), next)); + if (logger.isDebugEnabled()) { + logger.debug("Node {} moved '{}'", node.getPath(), command); + } + } else { + throw new IllegalArgumentException( + "provided node ordering command is invalid: " + command); + } + } catch ( final RepositoryException re) { + throw new PersistenceException("Unable to order resource", re, resource.getPath(), null); + } + } + + private boolean isVersionable(final Node node) throws RepositoryException { + return node.isNodeType(JcrConstants.MIX_VERSIONABLE); + } + + public boolean isVersionable(final Resource rsrc) throws PersistenceException { + try { + final Node node = rsrc.adaptTo(Node.class); + return node != null && isVersionable(node); + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re, rsrc.getPath(), null); + } + } + + public boolean checkin(final Resource rsrc) + throws PersistenceException { + final Node node = rsrc.adaptTo(Node.class); + if (node != null) { + try { + if (node.isCheckedOut() && isVersionable(node)) { + node.getSession().getWorkspace().getVersionManager().checkin(node.getPath()); + return true; + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re, rsrc.getPath(), null); + } + } + return false; + } + + private Node findVersionableAncestor(Node node) throws RepositoryException { + if (isVersionable(node)) { + return node; + } + try { + node = node.getParent(); + return findVersionableAncestor(node); + } catch (ItemNotFoundException e) { + // top-level + return null; + } + } + + public void checkoutIfNecessary(final Resource resource, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + if (resource != null && versioningConfiguration.isAutoCheckout()) { + final Node node = resource.adaptTo(Node.class); + if ( node != null ) { + try { + Node versionableNode = findVersionableAncestor(node); + if (versionableNode != null) { + if (!versionableNode.isCheckedOut()) { + versionableNode.getSession().getWorkspace().getVersionManager().checkout(versionableNode.getPath()); + changes.add(Modification.onCheckout(versionableNode.getPath())); + } + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + } + } + + public boolean isNode(final Resource rsrc) { + return rsrc.adaptTo(Node.class) != null; + } + + public boolean isNodeType(final Resource rsrc, final String typeHint) { + final Node node = rsrc.adaptTo(Node.class); + if ( node != null ) { + try { + return node.isNodeType(typeHint); + } catch ( final RepositoryException re) { + // ignore + } + } + return false; + } + + public Boolean isFileNodeType(final ResourceResolver resolver, final String nodeType) { + final Session session = resolver.adaptTo(Session.class); + if ( session != null ) { + try { + final NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + final NodeType nt = ntMgr.getNodeType(nodeType); + return nt.isNodeType(JcrConstants.NT_FILE); + } catch (RepositoryException e) { + // assuming type not valid. + return null; + } + } + return false; + } + + private PropertyDefinition searchPropertyDefinition(final NodeType nodeType, final String name) { + if ( nodeType.getPropertyDefinitions() != null ) { + for(final PropertyDefinition pd : nodeType.getPropertyDefinitions()) { + if ( pd.getName().equals(name) ) { + return pd; + } + } + } + // SLING-2877: + // no need to search property definitions of super types, as nodeType.getPropertyDefinitions() + // already includes those. see javadoc of {@link NodeType#getPropertyDefinitions()} + return null; + } + + private PropertyDefinition searchPropertyDefinition(final Node node, final String name) + throws RepositoryException { + PropertyDefinition result = searchPropertyDefinition(node.getPrimaryNodeType(), name); + if ( result == null ) { + if ( node.getMixinNodeTypes() != null ) { + for(final NodeType mt : node.getMixinNodeTypes()) { + result = this.searchPropertyDefinition(mt, name); + if ( result != null ) { + return result; + } + } + } + } + return result; + } + + public boolean isPropertyProtectedOrNewAutoCreated(final Object n, final String name) + throws PersistenceException { + final Node node = (Node)n; + try { + final PropertyDefinition pd = this.searchPropertyDefinition(node, name); + if ( pd != null ) { + // SLING-2877 (autocreated check is only required for new nodes) + if ( (node.isNew() && pd.isAutoCreated()) || pd.isProtected() ) { + return true; + } + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + return false; + } + + public boolean isNewNode(final Object node) { + return ((Node)node).isNew(); + } + + public boolean isPropertyMandatory(final Object node, final String name) + throws PersistenceException { + try { + final Property prop = ((Node)node).getProperty(name); + return prop.getDefinition().isMandatory(); + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + public boolean isPropertyMultiple(final Object node, final String name) + throws PersistenceException { + try { + final Property prop = ((Node)node).getProperty(name); + return prop.getDefinition().isMultiple(); + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + public Integer getPropertyType(final Object node, final String name) + throws PersistenceException { + try { + if ( ((Node)node).hasProperty(name) ) { + return ((Node)node).getProperty(name).getType(); + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + return null; + } + + private boolean isWeakReference(int propertyType) { + return propertyType == PropertyType.WEAKREFERENCE; + } + + /** + * Stores property value(s) as reference(s). Will parse the reference(s) from the string + * value(s) in the {@link RequestProperty}. + * + * @return A modification only if parsing was successful and the property was actually changed + */ + public Modification storeAsReference( + final Object n, + final String name, + final String[] values, + final int type, + final boolean multiValued) + throws PersistenceException { + try { + final Node node = (Node)n; + if (multiValued) { + Value[] array = ReferenceParser.parse(node.getSession(), values, isWeakReference(type)); + if (array != null) { + return Modification.onModified( + node.setProperty(name, array).getPath()); + } + } else { + if (values.length >= 1) { + Value v = ReferenceParser.parse(node.getSession(), values[0], isWeakReference(type)); + if (v != null) { + return Modification.onModified( + node.setProperty(name, v).getPath()); + } + } + } + return null; + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + public boolean hasSession(final ResourceResolver resolver) { + return resolver.adaptTo(Session.class) != null; + } + + public void setTypedProperty(final Object n, + final String name, + final String[] values, + final int type, + final boolean multiValued) + throws PersistenceException { + try { + if (multiValued) { + ((Node)n).setProperty(name, values, type); + } else if (values.length >= 1) { + ((Node)n).setProperty(name, values[0], type); + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + public Object getNode(final Resource rsrc) { + return rsrc.adaptTo(Node.class); + } + + public Object getItem(final Resource rsrc) { + return rsrc.adaptTo(Item.class); + } + + public void setPrimaryNodeType(final Object node, final String type) + throws PersistenceException { + try { + ((Node)node).setPrimaryType(type); + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + public void move(Object src, Object dstParent, String name) + throws PersistenceException { + try { + final Session session = ((Item)src).getSession(); + final Item source = ((Item)src); + final String targetParentPath = ((Node)dstParent).getPath(); + final String targetPath = (targetParentPath.equals("/") ? "" : targetParentPath) + '/' + name; + session.move(source.getPath(), targetPath); + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + /** + * Copy the src item into the dstParent node. + * The name of the newly created item is set to name. + * + * @param src The item to copy to the new location + * @param dstParent The node into which the src node is to be + * copied + * @param name The name of the newly created item. If this is + * null the new item gets the same name as the + * src item. + * @throws PersistenceException May be thrown in case of any problem copying + * the content. + * @see #copy(Node, Node, String) + * @see #copy(Property, Node, String) + */ + public String copy(Object src, Object dstParent, String name) + throws PersistenceException { + try { + final Item result; + if (((Item)src).isNode()) { + result = copy((Node) src, (Node)dstParent, name); + } else { + result = copy((Property) src, (Node)dstParent, name); + } + return result.getPath(); + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + /** + * Copy the src node into the dstParent node. + * The name of the newly created node is set to name. + *

+ * This method does a recursive (deep) copy of the subtree rooted at the + * source node to the destination. Any protected child nodes and and + * properties are not copied. + * + * @param src The node to copy to the new location + * @param dstParent The node into which the src node is to be + * copied + * @param name The name of the newly created node. If this is + * null the new node gets the same name as the + * src node. + * @throws RepositoryException May be thrown in case of any problem copying + * the content. + */ + private Item copy(Node src, Node dstParent, String name) + throws RepositoryException { + + if(isAncestorOrSameNode(src, dstParent)) { + throw new RepositoryException( + "Cannot copy ancestor " + src.getPath() + " to descendant " + dstParent.getPath()); + } + + // ensure destination name + if (name == null) { + name = src.getName(); + } + + // ensure new node creation + if (dstParent.hasNode(name)) { + dstParent.getNode(name).remove(); + } + + // create new node + Node dst = dstParent.addNode(name, src.getPrimaryNodeType().getName()); + for (NodeType mix : src.getMixinNodeTypes()) { + dst.addMixin(mix.getName()); + } + + // copy the properties + for (PropertyIterator iter = src.getProperties(); iter.hasNext();) { + copy(iter.nextProperty(), dst, null); + } + + // copy the child nodes + for (NodeIterator iter = src.getNodes(); iter.hasNext();) { + Node n = iter.nextNode(); + if (!n.getDefinition().isProtected()) { + copy(n, dst, null); + } + } + return dst; + } + + /** @return true if src is an ancestor node of dest, or if + * both are the same node */ + public static boolean isAncestorOrSameNode(Node src, Node dest) throws RepositoryException { + if(src.getPath().equals("/")) { + return true; + } else if(src.getPath().equals(dest.getPath())) { + return true; + } else if(dest.getPath().startsWith(src.getPath() + "/")) { + return true; + } + return false; + } + + /** + * Copy the src property into the dstParent + * node. The name of the newly created property is set to name. + *

+ * If the source property is protected, this method does nothing. + * + * @param src The property to copy to the new location + * @param dstParent The node into which the src property is + * to be copied + * @param name The name of the newly created property. If this is + * null the new property gets the same name as the + * src property. + * @throws RepositoryException May be thrown in case of any problem copying + * the content. + */ + private Item copy(Property src, Node dstParent, String name) + throws RepositoryException { + if (!src.getDefinition().isProtected()) { + if (name == null) { + name = src.getName(); + } + + // ensure new property creation + if (dstParent.hasProperty(name)) { + dstParent.getProperty(name).remove(); + } + + if (src.getDefinition().isMultiple()) { + return dstParent.setProperty(name, src.getValues()); + } + return dstParent.setProperty(name, src.getValue()); + } + return null; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java new file mode 100644 index 000000000..eedaa9036 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Facilitates parsing of the Accept HTTP request header. + * See RFC 2616 section 14.1 + */ +public class MediaRangeList extends TreeSet { + public static final String HEADER_ACCEPT = "Accept"; + public static final String PARAM_ACCEPT = ":http-equiv-accept"; + public static final String WILDCARD = "*"; + boolean matchesAll = false; + + private static final Logger log = LoggerFactory.getLogger(MediaRangeList.class); + + /** + * Constructs a MediaRangeList using information from the supplied HttpServletRequest. + * if the request contains a {@link #PARAM_ACCEPT} query parameter, the query parameter value overrides any + * {@link #HEADER_ACCEPT} header value. + * If the request contains no {@link #PARAM_ACCEPT} parameter, or the parameter value is empty, the value of the + * {@link #HEADER_ACCEPT} is used. If both values are missing, it is assumed that the client accepts all media types, + * as per the RFC. See also {@link MediaRangeList#MediaRangeList(java.lang.String)} + * @param request The HttpServletRequest to extract a MediaRangeList from + */ + public MediaRangeList(HttpServletRequest request) { + String queryParam = request.getParameter(PARAM_ACCEPT); + if (queryParam != null && queryParam.trim().length() != 0) { + init(queryParam); + } else { + init(request.getHeader(HEADER_ACCEPT)); + } + } + + /** + * Constructs a MediaRangeList using a list of media ranges specified in a java.lang.String. + * The string is a comma-separated list of media ranges, as specified by the RFC.
+ * Examples: + *

    + *
  • text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5
  • + *
  • text/html;q=0.8, application/json
  • + *
+ * + * @param listStr The list of media range specifications + */ + public MediaRangeList(String listStr) { + try { + init(listStr); + } catch (Throwable t) { + log.error("Error building MediaRangeList from '" + listStr + "' - will assume client accepts all media types", t); + init(null); + } + } + + private void init(String headerValue) { + if (headerValue == null || headerValue.trim().length() == 0) { + // RFC 2616: "If no Accept header field is present, + // then it is assumed that the client accepts all media types." + this.matchesAll = true; + this.add(new MediaRange(WILDCARD + "/" + WILDCARD)); + } else { + String[] mediaTypes = headerValue.split(","); + for (String type : mediaTypes) { + try { + MediaRange range = new MediaRange(type); + this.add(range); + if (range.matchesAll()) { + this.matchesAll = true; + } + } catch (Throwable throwable) { + log.warn("Error registering media type " + type, throwable); + } + } + } + } + + /** + * Determines if this MediaRangeList contains a given media type. + * @param mediaType A string on the form type/subtype. Neither type + * or subtype should be wildcard (*). + * @return true if this MediaRangeList contains a media type that matches + * mediaType, false otherwise + * @throws IllegalArgumentException if mediaType is not on an accepted form + * @throws NullPointerException if mediaType is null + */ + public boolean contains(String mediaType) { + //noinspection SuspiciousMethodCalls + MediaRange comp = new MediaRange(mediaType); + return this.matchesAll || this.contains(comp); + } + + /** + * Given a list of media types, returns the one is preferred by this MediaRangeList. + * @param mediaRanges An array of possible {@link org.apache.sling.servlets.post.impl.helper.MediaRangeList.MediaRange}s + * @return One of the mediaRanges that this MediaRangeList prefers; + * or null if this MediaRangeList does not contain any of the mediaRanges + * @throws NullPointerException if mediaRanges is null or contains a null value + */ + public MediaRange prefer(Set mediaRanges) { + for (MediaRange range : this) { + for (MediaRange mediaType : mediaRanges) { + if (range.equals(mediaType)) { + return mediaType; + } + } + } + return null; + } + + /** + * Determines which of the mediaRanges specifiactions is prefered by this MediaRangeList. + * @param mediaRanges String representations of MediaRanges. The strings must be + * on the form required by {@link MediaRange#MediaRange(String)} + * @see #prefer(java.util.Set) + * @return the toString representation of the prefered MediaRange, or null + * if this MediaRangeList does not contain any of the mediaRanges + */ + public String prefer(String... mediaRanges) { + Set ranges = new HashSet(); + for (String mediaRange : mediaRanges) { + ranges.add(new MediaRange(mediaRange)); + } + final MediaRange preferred = prefer(ranges); + return(preferred == null ? null : preferred.toString()); + } + + /** + * A code MediaRange represents an entry in a MediaRangeList. + * The MediaRange consists of a supertype and a subtype, + * optionally a quality factor parameter q and other arbitrary parameters. + */ + public class MediaRange implements Comparable { + private String supertype; + private double q = 1; + private Map parameters; + private String subtype; + + /** + * Constructs a MediaRange from a String expression. + * @param exp The String to constuct the MediaRange from. The string is + * expected to be on the form ( "*/*" + * | ( type "/" "*" ) + * | ( type "/" subtype ) + * ) *( ";" parameter )
+ * as specified by RFC 2616, section 14.1.
+ * Examples: + *
    + *
  • text/html;q=0.8
  • + *
  • text/html
  • + *
  • text/html;level=3
  • + *
  • text/html;level=3;q=0.7
  • + *
  • text/*
  • + *
  • */*
  • + *
+ * Note that if the supertype component is wildcard (*), then the subtype component + * must also be wildcard.
+ * The quality factor parameter must be between 0 and 1, inclusive + * (see RFC 2616 section 3.9). + * If the expression does not contain a q parameter, the MediaRange is given + * a default quality factor of 1. + * @throws IllegalArgumentException if exp can not be parsed to a valid media range + * @throws NullPointerException if exp is null + */ + public MediaRange(String exp) { + String[] parts = exp.split(";"); + this.setType(parts[0].trim()); + if (parts.length > 1) { + this.parameters = new HashMap(parts.length - 1); + } + for (int i = 1, partsLength = parts.length; i < partsLength; i++) { + String parameter = parts[i]; + String[] keyValue = parameter.split("="); + if (keyValue[0].equals("q")) { + this.q = Double.parseDouble(keyValue[1]); + if (this.q < 0 || this.q > 1) { + throw new IllegalArgumentException("Quality factor out of bounds: " + exp); + } + } + this.parameters.put(keyValue[0], keyValue[1]); + } + } + + /** + * Constructs a MediaRange of the given supertype and subtype. + * The quality factor is given the default value of 1. + * @param supertype The super type of the media range + * @param subtype The sub type of the media range + */ + MediaRange(String supertype, String subtype) { + this.setType(supertype, subtype); + } + + + /** + * Returns true if this is a catch-all media range (*/*). + * @return true if this range is a catch-all media range, false otherwise + */ + public boolean matchesAll() { + return this.supertype.equals(WILDCARD) && this.subtype.equals(WILDCARD); + } + + private void setType(String supertype, String subtype) { + this.supertype = supertype == null ? WILDCARD : supertype; + this.subtype = subtype == null ? WILDCARD : subtype; + if (this.supertype.equals(WILDCARD) && !this.subtype.equals(WILDCARD)) { + throw new IllegalArgumentException("Supertype cannot be wildcard if subtype is not"); + } + } + + private void setType(String typeDef) { + String[] parts = typeDef.split("/"); + String superType = parts[0]; + String subType = WILDCARD; + if(parts.length > 1){ + subType = parts[1]; + } + this.setType(superType,subType); + } + + MediaRange(String supertype, String subtype, double q) { + this(supertype, subtype); + this.q = q; + } + + + public String getParameter(String key) { + if (parameters != null) { + return parameters.get(key); + } else { + return null; + } + } + + public String getSupertype() { + return supertype; + } + + public String getSubtype() { + return subtype; + } + + /** + * Get the value of the quality factor parameter (q). + * @return the quality factor + */ + public double getQ() { + return q; + } + + public Map getParameters() { + return parameters != null ? parameters : new HashMap(0); + } + + /* -- Comparable implementation -- */ + public int compareTo(MediaRange o) { + double diff = this.q - o.getQ(); + if (diff == 0) { + // Compare parameters + int paramDiff = o.getParameters().size() - this.getParameters().size(); + if (paramDiff != 0) { + return paramDiff; + } + // Compare wildcards + if (this.supertype.equals(WILDCARD) && !o.getSupertype().equals(WILDCARD)) { + return 1; + } else if (!this.supertype.equals(WILDCARD) && o.getSupertype().equals(WILDCARD)) { + return -1; + } + if (this.subtype.equals(WILDCARD) && !o.getSubtype().equals(WILDCARD)) { + return 1; + } else if (!this.subtype.equals(WILDCARD) && o.getSubtype().equals(WILDCARD)) { + return -1; + } + // Compare names + return this.toString().compareTo(o.toString()); + } else { + return diff > 0 ? -1 : 1; + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MediaRange) { + MediaRange mr = (MediaRange) obj; + return mr.getSupertype().equals(this.supertype) && mr.getSubtype().equals(this.subtype); + } + return super.equals(obj); + } + + public boolean equals(String s) { + return (this.supertype + "/" + this.subtype).equals(s); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(this.supertype); + buf.append('/'); + buf.append(this.subtype); + if (parameters != null) { + String delimiter = ";"; + for (String key : parameters.keySet()) { + buf.append(delimiter); + buf.append(key).append("=").append(parameters.get(key)); + } + } + return buf.toString(); + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java new file mode 100644 index 000000000..15d61846f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + + +/** + * Filter a String so that it can be used as a NodeName. + */ +public class NodeNameFilter { + + public static final String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_"; + public static final char REPLACEMENT_CHAR = '_'; + + public String filter(String nodeName) { + final StringBuilder sb = new StringBuilder(); + char lastAdded = 0; + + nodeName = nodeName.toLowerCase(); + for(int i=0; i < nodeName.length(); i++) { + final char c = nodeName.charAt(i); + char toAdd = c; + + if (ALLOWED_CHARS.indexOf(c) < 0) { + if (lastAdded == REPLACEMENT_CHAR) { + // do not add several _ in a row + continue; + } + toAdd = REPLACEMENT_CHAR; + + } else if(i == 0 && Character.isDigit(c)) { + sb.append(REPLACEMENT_CHAR); + } + + sb.append(toAdd); + lastAdded = toAdd; + } + + if (sb.length()==0) { + sb.append(REPLACEMENT_CHAR); + } + + return sb.toString(); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ReferenceParser.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ReferenceParser.java new file mode 100644 index 000000000..aa0690ce4 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ReferenceParser.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +/** + * Takes a string representation of a node (either a path or a uuid) and tries to parse it. + * ReferenceParser is only used if JCR is available. + */ +public class ReferenceParser { + + /** + * Parses the given source string and returns the correct Value object. + * If no node matches returns null. + *

+ * + * @param value a path or UUID + * @param weak true to create a WeakReference value + * @return the value or null + * @throws RepositoryException + */ + public static Value parse(Session session, String value, boolean weak) throws RepositoryException { + Node n = parse(session, value); + if (n == null) { + return null; + } + return createReferenceValue(n, session.getValueFactory(), weak); + } + + /** + * Parses the given source strings and returns the respective reference value + * instances. If no node matches for any of the sources + * returns null. + *

+ * + * @param values path or UUID strings + * @param factory the value factory + * @param weak true to create a WeakReference value + * @return the values or null + * @throws RepositoryException + */ + public static Value[] parse(Session session, String[] values, boolean weak) throws RepositoryException { + Value ret[] = new Value[values.length]; + for (int i=0; i< values.length; i++) { + Node n = parse(session, values[i]); + if (n == null) { + return null; + } + ret[i] = createReferenceValue(n, session.getValueFactory(), weak); + } + return ret; + } + + private static Value createReferenceValue(Node node, ValueFactory factory, boolean weak) throws RepositoryException { + if (weak) { + return factory.createValue(node, true); + } else { + return factory.createValue(node); + } + } + + private static Node parse(Session session, String value) throws RepositoryException { + try { + if (session.itemExists(value)) { + return (Node) session.getItem(value); + } + } catch (RepositoryException ignore) { + // we ignore this + } + try { + return session.getNodeByIdentifier(value); + } catch (RepositoryException ignore) { + // we ignore this + } + return null; + } + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java new file mode 100644 index 000000000..a8b2d13d5 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.servlets.post.SlingPostConstants; + +/** + * Encapsulates all infos from the respective request parameters that are needed + * to create the repository property + */ +public class RequestProperty { + + private static final RequestParameter[] EMPTY_PARAM_ARRAY = new RequestParameter[0]; + + public static final String DEFAULT_IGNORE = SlingPostConstants.RP_PREFIX + + "ignore"; + + public static final String DEFAULT_NULL = SlingPostConstants.RP_PREFIX + + "null"; + + private final String path; + + private final String name; + + private final String parentPath; + + private RequestParameter[] values; + + private String[] stringValues; + + private String typeHint; + + private boolean hasMultiValueTypeHint; + + private RequestParameter[] defaultValues = EMPTY_PARAM_ARRAY; + + private boolean isDelete; + + private String repositoryResourcePath; + + private boolean isRepositoryResourceMove; + + private boolean ignoreBlanks; + + private boolean useDefaultWhenMissing; + + private boolean patch = false; + + private Chunk chunk; + + public RequestProperty(String path) { + assert path.startsWith("/"); + this.path = ResourceUtil.normalize(path); + this.parentPath = ResourceUtil.getParent(path); + this.name = ResourceUtil.getName(path); + } + + public String getTypeHint() { + return typeHint; + } + + public boolean hasMultiValueTypeHint() { + return this.hasMultiValueTypeHint; + } + + public void setTypeHintValue(String typeHint) { + if ( typeHint != null && typeHint.endsWith("[]") ) { + this.typeHint = typeHint.substring(0, typeHint.length() - 2); + this.hasMultiValueTypeHint = true; + } else { + this.typeHint = typeHint; + this.hasMultiValueTypeHint = false; + } + } + + public String getPath() { + return path; + } + + public String getName() { + return name; + } + + public String getParentPath() { + return parentPath; + } + + public boolean hasValues() { + if (useDefaultWhenMissing && defaultValues != null && defaultValues.length > 0) { + return true; + } else { + if (ignoreBlanks) { + return (values != null && getStringValues().length > 0); + } else { + return values != null; + } + } + } + + public RequestParameter[] getValues() { + return values; + } + + public void setValues(RequestParameter[] values) { + this.values = values; + } + + public RequestParameter[] getDefaultValues() { + return defaultValues; + } + + public void setDefaultValues(RequestParameter[] defaultValues) { + if (defaultValues == null) { + this.defaultValues = EMPTY_PARAM_ARRAY; + } else { + this.defaultValues = defaultValues; + } + } + + public boolean isFileUpload() { + return values != null && !values[0].isFormField(); + } + + /** + * Checks if this property provides any values. this is the case if one of + * the values is not empty or if the default handling is not 'ignore' + * + * @return true if this property provides values + */ + public boolean providesValue() { + // should void double creation of string values + String[] sv = getStringValues(); + if (sv == null) { + // is missleading return type. but means that property should not + // get auto-create values + return true; + } + for (String s : sv) { + if (!s.equals("")) { + return true; + } + } + return false; + } + + /** + * Returns the assembled string array out of the provided request values and + * default values. + * + * @return a String array or null if the property needs to be + * removed. + */ + public String[] getStringValues() { + if (stringValues == null) { + if (values == null && useDefaultWhenMissing) { + stringValues = new String[] { defaultValues[0].getString() }; + } else if (values.length > 1) { + // TODO: how the default values work for MV props is not very + // clear + List stringValueList = new ArrayList(values.length); + for (int i = 0; i < values.length; i++) { + String value = values[i].getString(); + if ((!ignoreBlanks) || value.length() > 0) { + stringValueList.add(value); + } + } + stringValues = stringValueList.toArray(new String[stringValueList.size()]); + } else { + String value = values[0].getString(); + if (value.equals("")) { + if (ignoreBlanks) { + return new String[0]; + } else { + if (defaultValues.length == 1) { + String defValue = defaultValues[0].getString(); + if (defValue.equals(DEFAULT_IGNORE)) { + // ignore means, do not create empty values + return new String[0]; + } else if (defValue.equals(DEFAULT_NULL)) { + // null means, remove property if exist + return null; + } + value = defValue; + } + } + } + stringValues = new String[] { value }; + } + } + return stringValues; + } + + /** + * Specifies whether this property should be deleted before any new content + * is to be set according to the values stored. + * + * @param isDelete true if the repository item described by + * this is to be deleted before any other operation. + */ + public void setDelete(boolean isDelete) { + this.isDelete = isDelete; + } + + /** + * Returns true if the repository item described by this is + * to be deleted before setting new content to it. + */ + public boolean isDelete() { + return isDelete; + } + + /** + * Sets the path of the repository item from which the content for this + * property is to be copied or moved. The path may be relative in which case + * it will be resolved relative to the absolute path of this property. + * + * @param sourcePath The path of the repository item to get the content from + * @param isMove true if the source content is to be moved, + * otherwise the source content is copied from the repository + * item. + */ + public void setRepositorySource(String sourcePath, boolean isMove) { + + // make source path absolute + if (!sourcePath.startsWith("/")) { + sourcePath = getParentPath() + "/" + sourcePath; + sourcePath = ResourceUtil.normalize(sourcePath); + } + + this.repositoryResourcePath = sourcePath; + this.isRepositoryResourceMove = isMove; + } + + /** + * Returns true if the content of this property is to be set + * by moving content from another repository item. + * + * @see #getRepositorySource() + */ + public boolean hasRepositoryMoveSource() { + return isRepositoryResourceMove; + } + + /** + * Returns true if the content of this property is to be set + * by copying content from another repository item. + * + * @see #getRepositorySource() + */ + public boolean hasRepositoryCopySource() { + return getRepositorySource() != null && !hasRepositoryMoveSource(); + } + + /** + * Returns the absolute path of the repository item from which the content + * for this property is to be copied or moved. + * + * @see #hasRepositoryCopySource() + * @see #hasRepositoryMoveSource() + * @see #setRepositorySource(String, boolean) + */ + public String getRepositorySource() { + return repositoryResourcePath; + } + + public void setIgnoreBlanks(boolean b) { + ignoreBlanks = b; + } + + public void setUseDefaultWhenMissing(boolean b) { + useDefaultWhenMissing = b; + } + + public void setPatch(boolean b) { + patch = b; + } + + /** + * Returns whether this property is to be handled as a multi-value property + * seen as set. + */ + public boolean isPatch() { + return patch; + } + + /** + * Return true if request is chunk upload. + */ + public boolean isChunkUpload() { + return chunk != null; + } + + public Chunk getChunk() { + return chunk; + } + + public void setChunk(Chunk chunk) { + this.chunk = chunk; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStream.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStream.java new file mode 100644 index 000000000..3e6603e76 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStream.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import org.apache.sling.api.resource.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +/** + * An input stream that reads from a list of resources that can be adapted into input streams. + */ +public class ResourceIteratorInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(ResourceIteratorInputStream.class); + private int n; + private InputStream currentStream; + private final Iterator iterator; + private int streamNo = 0; + + public ResourceIteratorInputStream(Iterator iterator) { + this.iterator = iterator; + while(iterator.hasNext()) { + currentStream = iterator.next().adaptTo(InputStream.class); + if ( currentStream != null) { + n = 0; + streamNo = 1; + return; + } + } + throw new IllegalArgumentException("Resource iterator does not contain any resources that can be adapted to an input stream."); + } + + @Override + public int read() throws IOException { + int i = currentStream.read(); + while ( i == -1 ) { + if ( iterator.hasNext()) { + LOGGER.debug("Stream {} provided {} bytes. ",streamNo, n); + currentStream = iterator.next().adaptTo(InputStream.class); + streamNo++; + n = 0; + if (currentStream != null) { + i = currentStream.read(); + } + } else { + LOGGER.debug("Last Stream {} provided {} bytes. ",streamNo, n); + return -1; + } + } + n++; + return i; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java new file mode 100644 index 000000000..470b580d4 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java @@ -0,0 +1,594 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.io.ByteArrayInputStream; +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.io.SequenceInputStream; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletContext; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.util.Text; +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles file uploads. + *

+ * + * Simple example: + *

+ * <form action="/home/admin" method="POST" enctype="multipart/form-data"> + * <input type="file" name="./portrait" /> + * </form> + * + * + * this will create a nt:file node below "/home/admin" if the node type of + * "admin" is (derived from) nt:folder, a nt:resource node otherwise. + *

+ * + * Filename example: + *

+ * <form action="/home/admin" method="POST" enctype="multipart/form-data"> + * <input type="file" name="./*" /> + * </form> + * + * + * same as above, but uses the filename of the uploaded file as name for the + * new node. + *

+ * + * Type hint example: + *

+ * <form action="/home/admin" method="POST" enctype="multipart/form-data"> + * <input type="file" name="./portrait" /> + * <input type="hidden" name="./portrait@TypeHint" value="my:file" /> + * </form> + * + * + * this will create a new node with the type my:file below admin. if the hinted + * type extends from nt:file an intermediate file node is created otherwise + * directly a resource node. + */ +public class SlingFileUploadHandler { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * The servlet context. + */ + private volatile ServletContext servletContext; + + private final JCRSupport jcrSupport = JCRSupport.INSTANCE; + + public void setServletContext(final ServletContext servletContext) { + this.servletContext = servletContext; + } + + /** + * Uses the file(s) in the request parameter for creation of new nodes. + * if the parent node is a nt:folder a new nt:file is created. otherwise + * just a nt:resource. if the name is '*', the filename of + * the uploaded file is used. + * + * @param parent the parent node + * @param prop the assembled property info + * @throws PersistenceException if an error occurs + */ + private void setFile(final Resource parentResource, + final RequestProperty prop, + final RequestParameter value, + final List changes, String name, + final String contentType) + throws PersistenceException { + // check type hint. if the type is ok and extends from nt:file, + // create an nt:file with that type. if it's invalid, drop it and let + // the parent node type decide. + boolean createNtFile = parentResource.isResourceType(JcrConstants.NT_FOLDER) || this.jcrSupport.isNodeType(parentResource, JcrConstants.NT_FOLDER); + String typeHint = prop.getTypeHint(); + if (typeHint != null) { + Boolean isFileNodeType = this.jcrSupport.isFileNodeType(parentResource.getResourceResolver(), typeHint); + if ( isFileNodeType == null ) { + // assuming type not valid. + createNtFile = false; + typeHint = null; + } else { + createNtFile = isFileNodeType; + } + } + + // also create an nt:file if the name contains an extension + // the rationale is that if the file name is "important" we want + // an nt:file, and an image name with an extension is probably "important" + if (!createNtFile && name.indexOf('.') > 0) { + createNtFile = true; + } + + // set empty type + if (typeHint == null) { + typeHint = createNtFile ? JcrConstants.NT_FILE : JcrConstants.NT_RESOURCE; + } + + // create nt:file resource if needed + Resource resParent; + if (createNtFile) { + // create nt:file + resParent = getOrCreateChildResource(parentResource, name, typeHint, changes); + name = JcrConstants.JCR_CONTENT; + typeHint = JcrConstants.NT_RESOURCE; + } else { + resParent = parentResource; + } + + // create resource + final Resource newResource = getOrCreateChildResource(resParent, name, typeHint, changes); + final ModifiableValueMap mvm = newResource.adaptTo(ModifiableValueMap.class); + // set properties + mvm.put(JcrConstants.JCR_LASTMODIFIED, Calendar.getInstance()); + mvm.put(JcrConstants.JCR_MIMETYPE, contentType); + changes.add(Modification.onModified(newResource.getPath() + "/" + JcrConstants.JCR_LASTMODIFIED)); + changes.add(Modification.onModified(newResource.getPath() + "/" + JcrConstants.JCR_MIMETYPE)); + + try { + // process chunk upload request separately + if (prop.isChunkUpload()) { + processChunk(resParent, newResource, prop, value, changes); + } else { + mvm.put(JcrConstants.JCR_DATA, value.getInputStream()); + changes.add(Modification.onModified(newResource.getPath() + "/" + JcrConstants.JCR_DATA)); + } + } catch (IOException e) { + throw new PersistenceException("Error while retrieving inputstream from parameter value.", e); + } + } + + /** + * Process chunk upload. For first and intermediate chunks request persists + * chunks at jcr:content/chunk_start_end/jcr:data or + * nt:resource/chunk_start_end/jcr:data. For last last chunk, + * merge all previous chunks and last chunk and replace binary at + * destination. + */ + private void processChunk(final Resource resParent, + final Resource res, + final RequestProperty prop, + final RequestParameter value, + final List changes) + throws PersistenceException { + try { + final ModifiableValueMap mvm = res.adaptTo(ModifiableValueMap.class); + long chunkOffset = prop.getChunk().getOffset(); + if (chunkOffset == 0) { + // first chunk + // check if another chunk upload is already in progress. throw + // exception + final Iterator itr = new FilteringResourceIterator(res.listChildren(), SlingPostConstants.CHUNK_NODE_NAME); + if (itr.hasNext()) { + throw new PersistenceException( + "Chunk upload already in progress at {" + res.getPath() + + "}"); + } + addChunkMixin(mvm); + mvm.put(SlingPostConstants.NT_SLING_CHUNKS_LENGTH, 0); + changes.add(Modification.onModified(res.getPath() + "/" + SlingPostConstants.NT_SLING_CHUNKS_LENGTH)); + if (mvm.get(JcrConstants.JCR_DATA) == null ) { + // create a empty jcr:data property + mvm.put(JcrConstants.JCR_DATA, + new ByteArrayInputStream("".getBytes())); + } + } + if (mvm.get(SlingPostConstants.NT_SLING_CHUNKS_LENGTH) == null) { + throw new PersistenceException("no chunk upload found at {" + + res.getPath() + "}"); + } + long currentLength = mvm.get(SlingPostConstants.NT_SLING_CHUNKS_LENGTH, Long.class); + long totalLength = prop.getChunk().getLength(); + if (chunkOffset != currentLength) { + throw new PersistenceException("Chunk's offset {" + + chunkOffset + + "} doesn't match expected offset {" + + currentLength + + "}"); + } + if (totalLength != 0) { + if (mvm.get(SlingPostConstants.NT_SLING_FILE_LENGTH) != null ) { + long expectedLength = mvm.get( + SlingPostConstants.NT_SLING_FILE_LENGTH, Long.class); + if (totalLength != expectedLength) { + throw new PersistenceException("File length {" + + totalLength + "} doesn't match expected length {" + + expectedLength + "}"); + } + } else { + mvm.put(SlingPostConstants.NT_SLING_FILE_LENGTH, + totalLength); + } + } + final Iterator itr = new FilteringResourceIterator(res.listChildren(), SlingPostConstants.CHUNK_NODE_NAME + "_" + String.valueOf(chunkOffset)); + if (itr.hasNext()) { + throw new PersistenceException("Chunk already present at {" + + itr.next().getPath() + "}"); + } + String nodeName = SlingPostConstants.CHUNK_NODE_NAME + "_" + + String.valueOf(chunkOffset) + "_" + + String.valueOf(chunkOffset + value.getSize() - 1); + if (totalLength == (currentLength + value.getSize()) + || prop.getChunk().isCompleted()) { + File file = null; + InputStream fileIns = null; + try { + file = mergeChunks(res, value.getInputStream()); + fileIns = new FileInputStream(file); + mvm.put(JcrConstants.JCR_DATA, fileIns); + changes.add(Modification.onModified(res.getPath() + "/" + JcrConstants.JCR_DATA)); + final Iterator rsrcItr = new FilteringResourceIterator(res.listChildren(), SlingPostConstants.CHUNK_NODE_NAME); + while (rsrcItr.hasNext()) { + Resource rsrcRange = rsrcItr.next(); + changes.add(Modification.onDeleted(rsrcRange.getPath())); + rsrcRange.getResourceResolver().delete(rsrcRange); + } + if (mvm.get(SlingPostConstants.NT_SLING_FILE_LENGTH) != null) { + changes.add(Modification.onDeleted(res.getPath() + "/" + SlingPostConstants.NT_SLING_FILE_LENGTH)); + mvm.remove(SlingPostConstants.NT_SLING_FILE_LENGTH); + } + if (mvm.get(SlingPostConstants.NT_SLING_CHUNKS_LENGTH) != null) { + changes.add(Modification.onDeleted(res.getPath() + "/" + SlingPostConstants.NT_SLING_CHUNKS_LENGTH)); + mvm.remove(SlingPostConstants.NT_SLING_CHUNKS_LENGTH); + } + removeChunkMixin(mvm); + } finally { + try { + fileIns.close(); + file.delete(); + } catch (IOException ign) { + + } + + } + } else { + final Map props = new HashMap<>(); + props.put(JcrConstants.JCR_DATA, value.getInputStream()); + props.put(SlingPostConstants.NT_SLING_CHUNK_OFFSET, chunkOffset); + props.put(SlingPostConstants.NT_SLING_CHUNKS_LENGTH, currentLength + value.getSize()); + for(final String key : props.keySet()) { + changes.add(Modification.onModified(res.getPath() + "/" + nodeName + "/" + key)); + } + props.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, + SlingPostConstants.NT_SLING_CHUNK_NODETYPE); + final Resource rangeRsrc = res.getResourceResolver().create(res, nodeName, props); + + changes.add(Modification.onCreated(rangeRsrc.getPath())); + } + } catch (IOException e) { + throw new PersistenceException( + "Error while retrieving inputstream from parameter value.", e); + } + } + + private static final class FilteringResourceIterator implements Iterator, Iterable { + + private final String prefix; + + private final Iterator iter; + + private Resource next; + + public FilteringResourceIterator(final Iterator iter, final String prefix) { + this.prefix = prefix; + this.iter = iter; + this.next = seek(); + } + + private Resource seek() { + Resource result = null; + while ( iter.hasNext() && result == null ) { + final Resource c = iter.next(); + if ( c.getName().startsWith(prefix) ) { + result = c; + } + } + return result; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Resource next() { + final Resource result = next; + next = seek(); + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + + } + + @Override + public Iterator iterator() { + return this; + } + } + + /** + * Merge all previous chunks with last chunk's stream into a temporary file + * and return it. + */ + private File mergeChunks(final Resource parentResource, + final InputStream lastChunkStream) + throws PersistenceException { + OutputStream out = null; + SequenceInputStream mergeStrm = null; + File file = null; + try { + file = File.createTempFile("tmp-", "-mergechunk"); + out = new FileOutputStream(file); + String startPattern = SlingPostConstants.CHUNK_NODE_NAME + "_" + "0_"; + Iterator itr = new FilteringResourceIterator(parentResource.listChildren(), startPattern); + final Set inpStrmSet = new LinkedHashSet<>(); + while (itr.hasNext()) { + final Resource rangeResource = itr.next(); + if (itr.hasNext() ) { + throw new PersistenceException( + "more than one resource found for pattern: " + startPattern + "*"); + } + + inpStrmSet.add(rangeResource.adaptTo(InputStream.class)); + log.debug("added chunk {} to merge stream", rangeResource.getName()); + String[] indexBounds = rangeResource.getName().substring( + (SlingPostConstants.CHUNK_NODE_NAME + "_").length()).split( + "_"); + startPattern = SlingPostConstants.CHUNK_NODE_NAME + "_" + + String.valueOf(Long.valueOf(indexBounds[1]) + 1) + "_"; + itr = new FilteringResourceIterator(parentResource.listChildren(), startPattern); + } + + inpStrmSet.add(lastChunkStream); + mergeStrm = new SequenceInputStream(Collections.enumeration(inpStrmSet)); + IOUtils.copyLarge(mergeStrm, out); + } catch (final IOException e) { + throw new PersistenceException("Exception during chunk merge occured: " + e.getMessage(), e); + } finally { + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(mergeStrm); + + } + return file; + } + + private Resource getChunkParent(final Resource rsrc) { + // parent resource containing all chunks and has mixin sling:chunks applied + // on it. + Resource chunkParent = null; + Resource jcrContentNode = null; + if (hasChunks(rsrc)) { + chunkParent = rsrc; + } else { + jcrContentNode = rsrc.getChild(JcrConstants.JCR_CONTENT); + if ( hasChunks(jcrContentNode)) { + chunkParent = jcrContentNode; + } + } + return chunkParent; + } + + /** + * Delete all chunks saved within a resource. If no chunks exist, it is no-op. + */ + public void deleteChunks(final Resource rsrc) throws PersistenceException { + final Resource chunkParent = getChunkParent(rsrc); + + if (chunkParent != null) { + for(final Resource c : new FilteringResourceIterator(rsrc.listChildren(), SlingPostConstants.CHUNK_NODE_NAME) ) { + c.getResourceResolver().delete(c); + } + final ModifiableValueMap vm = chunkParent.adaptTo(ModifiableValueMap.class); + vm.remove(SlingPostConstants.NT_SLING_FILE_LENGTH); + vm.remove(SlingPostConstants.NT_SLING_CHUNKS_LENGTH); + removeChunkMixin(vm); + } + } + + private final void addChunkMixin(final ModifiableValueMap vm) { + final String[] mixins = vm.get(JcrConstants.JCR_MIXINTYPES, String[].class); + if ( mixins == null ) { + vm.put(JcrConstants.JCR_MIXINTYPES, new String[] {SlingPostConstants.NT_SLING_CHUNK_MIXIN}); + } else { + final Set types = new HashSet<>(Arrays.asList(mixins)); + if ( !types.contains(SlingPostConstants.NT_SLING_CHUNK_MIXIN) ) { + types.add(SlingPostConstants.NT_SLING_CHUNK_MIXIN); + vm.put(JcrConstants.JCR_MIXINTYPES, types.toArray(new String[types.size()])); + } + } + } + + private final void removeChunkMixin(final ModifiableValueMap vm) { + final String[] mixins = vm.get(JcrConstants.JCR_MIXINTYPES, String[].class); + if ( mixins != null ) { + final Set types = new HashSet<>(Arrays.asList(mixins)); + if ( types.remove(SlingPostConstants.NT_SLING_CHUNK_MIXIN) ) { + vm.put(JcrConstants.JCR_MIXINTYPES, types.toArray(new String[types.size()])); + } + } + } + + /** + * Get the last {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} + * {@link Resource}. + * + * @param rsrc {@link Resource} containing + * {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} + * {@link Resource}s + * @return the {@link SlingPostConstants#NT_SLING_CHUNK_NODETYPE} chunk + * resource. + */ + public Resource getLastChunk(Resource rsrc) { + final Resource chunkParent = getChunkParent(rsrc); + if (chunkParent == null) { + return null; + } + Resource lastChunkRsrc = null; + long lastChunkStartIndex = -1; + for(final Resource chunkRsrc : new FilteringResourceIterator(rsrc.listChildren(), SlingPostConstants.CHUNK_NODE_NAME + "_") ) { + final String[] indexBounds = chunkRsrc.getName().substring( + (SlingPostConstants.CHUNK_NODE_NAME + "_").length()).split("_"); + long chunkStartIndex = Long.valueOf(indexBounds[0]); + if (chunkStartIndex > lastChunkStartIndex) { + lastChunkRsrc = chunkRsrc; + lastChunkStartIndex = chunkStartIndex; + } + } + + return lastChunkRsrc; + } + + /** + * Return true if resource has chunks stored in it, otherwise false. + */ + private boolean hasChunks(final Resource rsrc) { + final ValueMap vm = rsrc.getValueMap(); + final String[] mixinTypes = vm.get(JcrConstants.JCR_MIXINTYPES, String[].class); + if ( mixinTypes != null ) { + for (final String nodeType : mixinTypes) { + if (nodeType.equals(SlingPostConstants.NT_SLING_CHUNK_MIXIN)) { + return true; + } + } + } + return false; + } + + private static final String MT_APP_OCTET = "application/octet-stream"; + + /** + * Uses the file(s) in the request parameter for creation of new nodes. + * if the parent node is a nt:folder a new nt:file is created. otherwise + * just a nt:resource. if the name is '*', the filename of + * the uploaded file is used. + * + * @param parent the parent node + * @param prop the assembled property info + * @throws PersistenceException if an error occurs + */ + public void setFile(final Resource parent, final RequestProperty prop, final List changes) + throws PersistenceException { + for (final RequestParameter value : prop.getValues()) { + + // ignore if a plain form field or empty + if (value.isFormField() || value.getSize() <= 0) { + continue; + } + + // get node name + String name = prop.getName(); + if (name.equals("*")) { + name = value.getFileName(); + // strip of possible path (some browsers include the entire path) + name = name.substring(name.lastIndexOf('/') + 1); + name = name.substring(name.lastIndexOf('\\') + 1); + } + name = Text.escapeIllegalJcrChars(name); + + // get content type + String contentType = value.getContentType(); + if (contentType != null) { + int idx = contentType.indexOf(';'); + if (idx > 0) { + contentType = contentType.substring(0, idx); + } + } + if (contentType == null || contentType.equals(MT_APP_OCTET)) { + // try to find a better content type + final ServletContext ctx = this.servletContext; + if (ctx != null) { + contentType = ctx.getMimeType(value.getFileName()); + } + if ( contentType == null ) { + contentType = MT_APP_OCTET; + } + } + + this.setFile(parent, prop, value, changes, name, contentType); + } + } + + private Resource getOrCreateChildResource(final Resource parent, + final String name, + final String typeHint, + final List changes) + throws PersistenceException { + Resource result = parent.getChild(name); + if ( result != null ) { + if ( !result.isResourceType(typeHint) && jcrSupport.isNode(result) && !jcrSupport.isNodeType(result, typeHint) ) { + parent.getResourceResolver().delete(result); + result = createWithChanges(parent, name, typeHint, changes); + } + } else { + result = createWithChanges(parent, name, typeHint, changes); + } + return result; + } + + private Resource createWithChanges(final Resource parent, final String name, + final String typeHint, + final List changes) + throws PersistenceException { + Map properties = null; + if ( typeHint != null ) { + // sling resource type not allowed for nt:file nor nt:resource + if ( !jcrSupport.isNode(parent) + || (!JcrConstants.NT_FILE.equals(typeHint) && !JcrConstants.NT_RESOURCE.equals(typeHint)) ) { + properties = Collections.singletonMap(ResourceResolver.PROPERTY_RESOURCE_TYPE, (Object)typeHint); + } else { + properties = Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, (Object)typeHint); + } + } + final Resource result = parent.getResourceResolver().create(parent, name, properties); + changes.add(Modification.onCreated(result.getPath())); + return result; + } + +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java new file mode 100644 index 000000000..c5c69fbdd --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java @@ -0,0 +1,557 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.SlingPostConstants; + +/** + * Sets a property on the given resource, in some cases with a specific type and + * value. For example, "lastModified" with an empty value is stored as the + * current Date. + * Special handling might apply if the resource is backed by a JCR node. + */ +public class SlingPropertyValueHandler { + + /** + * Defines a map of auto properties + */ + private static final Map AUTO_PROPS = new HashMap<>(); + static { + AUTO_PROPS.put("created", AutoType.CREATED); + AUTO_PROPS.put("createdBy", AutoType.CREATED_BY); + AUTO_PROPS.put(JcrConstants.JCR_CREATED, AutoType.CREATED); + AUTO_PROPS.put("jcr:createdBy", AutoType.CREATED_BY); + AUTO_PROPS.put("lastModified", AutoType.MODIFIED); + AUTO_PROPS.put("lastModifiedBy", AutoType.MODIFIED_BY); + AUTO_PROPS.put(JcrConstants.JCR_LASTMODIFIED, AutoType.MODIFIED); + AUTO_PROPS.put("jcr:lastModifiedBy", AutoType.MODIFIED_BY); + } + + /** + * the post processor + */ + private final List changes; + + private final DateParser dateParser; + + private final JCRSupport jcrSupport; + + /** + * current date for all properties in this request + */ + private final Calendar now = Calendar.getInstance(); + + /** + * Constructs a property value handler + */ + public SlingPropertyValueHandler(final DateParser dateParser, + final JCRSupport jcrSupport, + final List changes) { + this.dateParser = dateParser; + this.jcrSupport = jcrSupport; + this.changes = changes; + } + + /** Return the AutoType for a given property name + * @return null if not found + * */ + static AutoType getAutoType(String propertyName) { + return AUTO_PROPS.get(propertyName); + } + + /** + * Set property on given node, with some automatic values when user provides + * the field name but no value. + * + * html example for testing: + * + * <input type="hidden" name="created"/> + * <input type="hidden" name="lastModified"/> + * <input type="hidden" name="createdBy" /> + * <input type="hidden" name="lastModifiedBy"/> + * + * + * @param parent the parent node + * @param prop the request property + * @throws PersistenceException if a resource error occurs + */ + public void setProperty(final Resource parent, final RequestProperty prop) + throws PersistenceException { + final Modifiable mod = new Modifiable(); + mod.resource = parent; + mod.node = jcrSupport.getNode(parent); + mod.valueMap = parent.adaptTo(ModifiableValueMap.class); + if ( mod.valueMap == null ) { + throw new PersistenceException("Resource at '" + parent.getPath() + "' is not modifiable."); + } + + final String name = prop.getName(); + if (prop.providesValue()) { + // if user provided a value, don't mess with it + setPropertyAsIs(mod, prop); + + } else if (AUTO_PROPS.containsKey(name)) { + // check if this is a JCR resource and check node type + if ( this.jcrSupport.isPropertyProtectedOrNewAutoCreated(mod.node, name) ) { + return; + } + + // avoid collision with protected properties + final boolean isNew = jcrSupport.isNewNode(mod.node); + switch (getAutoType(name)) { + case CREATED: + if (isNew) { + setCurrentDate(mod, name); + } + break; + case CREATED_BY: + if (isNew) { + setCurrentUser(mod, name); + } + break; + case MODIFIED: + setCurrentDate(mod, name); + break; + case MODIFIED_BY: + setCurrentUser(mod, name); + break; + } + } else { + // no magic field, set value as provided + setPropertyAsIs(mod, prop); + } + } + + /** + * Sets the property to the given date + * @param parent parent resource + * @param name name of the property + * @throws PersistenceException if a resource error occurs + */ + private void setCurrentDate(final Modifiable parent, final String name) + throws PersistenceException { + removePropertyIfExists(parent, name); + parent.valueMap.put(name, now); + changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); + } + + /** + * set property to the current User id + * @param parent parent resource + * @param name name of the property + * @throws PersistenceException if a resource error occurs + */ + private void setCurrentUser(final Modifiable parent, final String name) + throws PersistenceException { + removePropertyIfExists(parent, name); + final String user = parent.resource.getResourceResolver().getUserID(); + parent.valueMap.put(name, user); + changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); + } + + /** + * Removes the property with the given name from the parent resource if it + * exists and if it's not a mandatory property. + * + * @param parent the parent resource + * @param name the name of the property to remove + * @return path of the property that was removed or null if + * it was not removed + * @throws PersistenceException if a repository error occurs. + */ + private String removePropertyIfExists(final Modifiable parent, final String name) + throws PersistenceException { + if (parent.valueMap.containsKey(name) && !jcrSupport.isPropertyMandatory(parent.node, name)) { + parent.valueMap.remove(name); + return parent.resource.getPath() + '/' + name; + } + return null; + } + + /** + * set property without processing, except for type hints + * + * @param parent the parent resource + * @param prop the request property + * @throws PersistenceException if a resource error occurs. + */ + private void setPropertyAsIs(final Modifiable parent, final RequestProperty prop) + throws PersistenceException { + + String[] values = prop.getStringValues(); + + // RequestProperty#getStringValues already takes care of the configs ignoreBlanks, defaultValues etc. + // and provides values as null, new String[0] etc. accordingly. + if (values == null || (values.length == 1 && values[0].length() == 0)) { + // if no value is present or a single empty string is given, + // just remove the existing property (if any) + removeProperty(parent, prop); + + } else if (values.length == 0) { + // do not create new prop here, but clear existing + clearProperty(parent, prop); + + } else { + // when patching, simply update the value list using the patch operations + if (prop.isPatch()) { + values = patch(parent, prop.getName(), values); + if (values == null) { + return; + } + } + + final boolean multiValue = isMultiValue(parent, prop, values); + + if (multiValue) { + // converting single into multi value props requires deleting it first + removeIfSingleValueProperty(parent, prop); + } + + final int type = getType(parent, prop); + if (jcrSupport.hasSession(parent.resource.getResourceResolver())) { + + if (type == PropertyType.DATE) { + if (storeAsDate(parent, prop.getName(), values, multiValue)) { + return; + } + } else if (isReferencePropertyType(type)) { + if (storeAsReference(parent, prop.getName(), values, type, multiValue)) { + return; + } + } + } + + store(parent, prop.getName(), values, type, multiValue); + } + } + + /** + * Patches a multi-value property using add and remove operations per value. + */ + private String[] patch(final Modifiable parent, String name, String[] values) + throws PersistenceException { + // we do not use a Set here, as we want to be very restrictive in our + // actions and avoid touching elements that are not modified through the + // add/remove patch operations; e.g. if the value "foo" occurs twice + // in the existing array, and is not touched, afterwards there should + // still be two times "foo" in the list, even if this is not a real set. + List oldValues = new ArrayList<>(); + + if (parent.valueMap.containsKey(name)) { + if ( parent.node != null && !jcrSupport.isPropertyMultiple(parent.node, name)) { + + // can only patch multi-value props + return null; + } + + final String[] setValues = parent.valueMap.get(name, String[].class); + if ( setValues != null ) { + for(final String v : setValues) { + oldValues.add(v); + } + } + } + + boolean modified = false; + for (String v : values) { + if (v != null && v.length() > 0) { + final char op = v.charAt(0); + final String val = v.substring(1); + + if (op == SlingPostConstants.PATCH_ADD) { + if (!oldValues.contains(val)) { + oldValues.add(val); + modified = true; + } + } else if (op == SlingPostConstants.PATCH_REMOVE) { + while (oldValues.remove(val)) { + modified = true; + } + } + } + } + + // if the patch does not include any operations (e.g. invalid ops) + // return null to indicate that nothing should be done + if (modified) { + return oldValues.toArray(new String[oldValues.size()]); + } + + return null; + } + + + private boolean isReferencePropertyType(int propertyType) { + return propertyType == PropertyType.REFERENCE || propertyType == PropertyType.WEAKREFERENCE; + } + + /** + * Returns the property type to use for the given property. This is defined + * either by an explicit type hint in the request or simply the type of the + * existing property. + */ + private int getType(final Modifiable parent, RequestProperty prop) + throws PersistenceException { + // no explicit typehint + int type = PropertyType.UNDEFINED; + if (prop.getTypeHint() != null) { + try { + type = PropertyType.valueFromName(prop.getTypeHint()); + } catch (Exception e) { + // ignore + } + } + String[] values = prop.getStringValues(); + if ( type == PropertyType.UNDEFINED && values != null && values.length > 0 ) { + final Integer jcrType = jcrSupport.getPropertyType(parent.node, prop.getName()); + if ( jcrType != null ) { + type = jcrType; + } + } + return type; + } + + /** + * Returns whether the property should be handled as multi-valued. + */ + private boolean isMultiValue(final Modifiable parent, RequestProperty prop, String[] values) + throws PersistenceException { + // multiple values are provided + if (values != null && values.length > 1) { + return true; + } + // TypeHint with [] + if (prop.hasMultiValueTypeHint()) { + return true; + } + // patch method requires multi value + if (prop.isPatch()) { + return true; + } + // nothing in the request, so check the current JCR property definition + final Object value = parent.valueMap.get(prop.getName()); + if ( parent.node != null ) { + if ( value != null ) { + return jcrSupport.isPropertyMultiple(parent.node, prop.getName()); + } + } else { + if ( value != null && value.getClass().isArray() ) { + return true; + } + } + return false; + } + + /** + * Clears a property: sets an empty string for single-value properties, and + * removes multi-value properties. + */ + private void clearProperty(final Modifiable parent, RequestProperty prop) + throws PersistenceException { + if (parent.valueMap.containsKey(prop.getName())) { + if ( jcrSupport.isPropertyMultiple(parent.node, prop.getName()) ) { + // the existing property is multi-valued, so just delete it? + final String removePath = removePropertyIfExists(parent, prop.getName()); + if ( removePath != null ) { + changes.add(Modification.onDeleted(removePath)); + } + } else { + parent.valueMap.put(prop.getName(), ""); + changes.add(Modification.onModified(parent.resource.getPath() + '/' + prop.getName())); + } + } + } + + /** + * Removes the property if it exists. + */ + private void removeProperty(final Modifiable parent, final RequestProperty prop) + throws PersistenceException { + final String removePath = removePropertyIfExists(parent, prop.getName()); + if ( removePath != null ) { + changes.add(Modification.onDeleted(removePath)); + } + } + + /** + * Removes the property if it exists and is single-valued. + */ + private void removeIfSingleValueProperty(final Modifiable parent, + final RequestProperty prop) + throws PersistenceException { + if (parent.valueMap.containsKey(prop.getName())) { + if ( jcrSupport.isPropertyMultiple(parent.node, prop.getName()) ) { + // do nothing, multi value + return; + } + final String removePath = removePropertyIfExists(parent, prop.getName()); + if ( removePath != null ) { + changes.add(Modification.onDeleted(removePath)); + } + } + } + + /** + * Parses the given source strings and returns the respective Calendar value + * instances. If no format matches for any of the sources + * returns null. + *

+ * + * @param sources date time source strings + * @return Calendar value representations of the source or null + */ + private Calendar[] parse(final String sources[]) { + final Calendar ret[] = new Calendar[sources.length]; + for (int i=0; i< sources.length; i++) { + final Calendar c = dateParser.parse(sources[i]); + if (c == null) { + return null; + } + ret[i] = c; + } + return ret; + } + + + /** + * Stores property value(s) as date(s). Will parse the date(s) from the string + * value(s) in the {@link RequestProperty}. + * + * @return true only if parsing was successful and the property was actually changed + */ + private boolean storeAsDate(final Modifiable parent, String name, String[] values, boolean multiValued) + throws PersistenceException { + if (multiValued) { + final Calendar[] array = parse(values); + if (array != null) { + parent.valueMap.put(name, array); + changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); + return true; + } + } else { + if (values.length >= 1) { + final Calendar c = dateParser.parse(values[0]); + if (c != null) { + parent.valueMap.put(name, c); + changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); + + return true; + } + } + } + return false; + } + + /** + * Stores property value(s) as reference(s). Will parse the reference(s) from the string + * value(s) in the {@link RequestProperty}. + * + * @return true only if parsing was successful and the property was actually changed + */ + private boolean storeAsReference(final Modifiable parent, + final String name, + final String[] values, + final int type, + final boolean multiValued) + throws PersistenceException { + final Modification mod = this.jcrSupport.storeAsReference(parent.resource, parent.node, name, values, type, multiValued); + return mod != null; + } + + /** + * Stores the property as string or via a string value, but with an explicit + * type. Both multi-value or single-value. + */ + private void store(final Modifiable parent, + final String name, + final String[] values, + final int type, + final boolean multiValued) + throws PersistenceException { + if ( parent.node != null && type != PropertyType.UNDEFINED ) { + jcrSupport.setTypedProperty(parent.node, name, values, type, multiValued); + + } else { + if (multiValued) { + parent.valueMap.put(name, toJavaObject(values, type)); + } else if (values.length >= 1) { + parent.valueMap.put(name, toJavaObject(values[0], type)); + } + } + changes.add(Modification.onModified(parent.resource.getPath() + '/' + name)); + } + + /** Converts a value */ + private static Object toJavaObject(final String value, final int type) { + final boolean isEmpty = value == null || value.trim().length() == 0; + switch (type) { + case PropertyType.DECIMAL: + return isEmpty ? BigDecimal.ZERO : new BigDecimal(value); + case PropertyType.BOOLEAN: + return isEmpty ? Boolean.FALSE : Boolean.valueOf(value); + case PropertyType.DOUBLE: + return isEmpty ? (double)0.0 : Double.valueOf(value); + case PropertyType.LONG: + return isEmpty ? 0 : Long.valueOf(value); + default: // fallback + return value; + } + } + + /** Converts a value */ + private static Object toJavaObject(final String values[], final int type) { + final Object[] result = new Object[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] != null ) { + result[i] = toJavaObject(values[i], type); + } + } + return result; + } + + + /** + * Defines an auto property behavior + */ + private enum AutoType { + CREATED, + CREATED_BY, + MODIFIED, + MODIFIED_BY + } + + public final static class Modifiable { + public Resource resource; + public ModifiableValueMap valueMap; + public Object node; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/StreamedChunk.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/StreamedChunk.java new file mode 100644 index 000000000..1283acb8c --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/helper/StreamedChunk.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.ServletContext; +import javax.servlet.http.Part; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.servlets.post.Modification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Supports streamed uploads including where the stream is made up of partial body parts. + * The behaviour is documented here https://cwiki.apache.org/confluence/display/SLING/Chunked+File+Upload+Support, adding the ability + * to define a body part with a Content-Range header on the part. Since the body parts are streamed there are some restrictions. If the + * length of a body part is missing from either the Content-Range header or a Content-Length header in the Part, then the length of the part is + * assumed to be the rest of the body to make the total length of the upload that specified in earlier Content-Range headers or a @Length property. + * + * When using only Content-Range headers (see the HTTP 1.1 spec) the Content-Range header must be complete and applied to the Part of the body. + * The length of the full file must be specified and be the same on all body parts, and the body parts must be sent in order. This is a restriction + * of the Sling Chunked File Upload protocol. When the total uploaded equals the file length the chunked uploads are processed to generate the final upload. + * + * When using request parameters, the most recent request parameters are used for @Completed, @Offset and @Length. When using request parameters if the + * Content-Length header is missing from the body Part, then the Body part is assumed to be the final body part. Then the total uploaded equals the value of + * the @Length parameter or a @Completed parameter is present, then the body parts are joined into a single body part. + * + * Consolidating body parts will cause all body parts to be read from the DS, which will incure 3x the IO of a non body part or chunked upload. For FS DS the IO may be from + * OS level disk cache. For other styles of DS the IO may consume more resources. Chunked or Body part uploads are not as efficient as whole body uploads and should + * be avoided wherever possible. This could be avoided if Oak would expose a seekable OutputStream, or allow writes to Binaries to specify and offset. + * + * + * + */ +public class StreamedChunk { + private static final String SLING_CHUNKS_LENGTH = "sling:length"; + private static final String SLING_FILE_LENGTH = "sling:fileLength"; + private static final String SLING_CHUNK_MIXIN = "sling:chunks"; + private static final String SLING_CHUNK_NT = "sling:chunk"; + private static final String SLING_OFFSET = "sling:offset"; + private static final String MT_APP_OCTET = "application/octet-stream"; + private final Logger LOGGER = LoggerFactory.getLogger(StreamedChunk.class); + + private final long offset; + private final long chunkLength; + private final long fileLength; + private final Part part; + private ServletContext servletContext; + private final boolean completed; + private final boolean chunked; + private final String chunkResourceName; + + /** + * Construct a chunk from the part and form fields. Once constructed it is immutable exposing a store method to store the chunk. + * If the part does not represent a chunk then the class behaves as if the chunk is a upload of 1 chunk (ie the whole file). + * @param part the current part, not read other than headers. + * @param formFields form fields encountered in teh request stream prior to this part. + * @param servletContext the current servlet context needed to resolve mimetypes. + */ + public StreamedChunk(Part part, Map> formFields, ServletContext servletContext) { + this.part = part; + this.servletContext = servletContext; + + String contentRangeHeader = part.getHeader("Content-Range"); + String contentLengthHeader = part.getHeader("Content-Length"); + if ( contentRangeHeader != null ) { + ContentRange contentRange = new ContentRange(contentRangeHeader); + fileLength = contentRange.length; + offset = contentRange.offset; + chunkLength = contentRange.range; + chunked = true; + + } else if ( formFields.containsKey(part.getName()+"@Length") && formFields.containsKey(part.getName()+"@Offset")) { + fileLength = Long.parseLong(lastFrom(formFields.get(part.getName() + "@Length"))); + offset = Long.parseLong(lastFrom(formFields.get(part.getName() + "@Offset"))); + if (contentLengthHeader != null) { + chunkLength = Long.parseLong(contentLengthHeader); + } else if ( formFields.containsKey(part.getName() + "@PartLength")) { + chunkLength = Long.parseLong(lastFrom(formFields.get(part.getName() + "@PartLength"))); + } else { + // must assume the chunk contains all the data. + LOGGER.info("No part length specified assuming this is the final part of the chunked upload."); + chunkLength = fileLength - offset; + } + chunked = true; + } else { + offset = 0; + if (contentLengthHeader != null) { + fileLength = Long.parseLong(contentLengthHeader); + chunkLength = fileLength; + } else { + fileLength = -1; + chunkLength = -1; + } + chunked = false; + + } + chunkResourceName = "chunk_"+offset+"-"+(offset+chunkLength); + completed = ((offset+chunkLength) == fileLength) || formFields.containsKey(part.getName()+"@Completed"); + LOGGER.debug(" chunkResourceName {}, chunked {},completed {}, fileLength {}, chunkLength {}, offset {} ", + new Object[]{chunkResourceName, chunked, completed, fileLength, chunkLength, offset}); + } + + /** + * Store the chunk in a file resource under a jcr:content sub node. The method does not commit the resource resolver. The caller + * must perform the commit. If the stream is a stream of body parts and the parts are complete, the store operation will commit + * the body part but leave the consolitation of all parts to be committed by the caller. ie, always call resourceResolver.commit() after + * calling this method. + * @param fileResource the file request. + * @param changes changes that were made. + * @return the jcr:content sub node. + * @throws PersistenceException + */ + public Resource store(Resource fileResource, List changes) throws PersistenceException { + Resource result = fileResource.getChild(JcrConstants.JCR_CONTENT); + if (result != null) { + updateState(result, changes); + } else { + result = initState(fileResource, changes); + } + storeChunk(result, changes); + return result; + } + + + /** + * The last element of strings. + * @param strings a non null non zero string array. + * @return the last element. + */ + private String lastFrom(List strings) { + return strings.get(strings.size()-1); + } + + /** + * Update the state of the content resource to reflect a new body part being streamd. + * @param contentResource the content resource + * @param changes changes made. + * @throws IllegalStateException if the contentResource is not consistent with the part being streamed. + * @throws PersistenceException if the part cant be streamed. + */ + private void updateState(Resource contentResource, List changes) throws IllegalStateException, PersistenceException { + final ModifiableValueMap vm = contentResource.adaptTo(ModifiableValueMap.class); + if ( vm == null ) { + throw new PersistenceException("Resource at " + contentResource.getPath() + " is not modifiable."); + } + vm.put(JcrConstants.JCR_LASTMODIFIED, Calendar.getInstance()); + vm.put(JcrConstants.JCR_MIMETYPE, getContentType(part)); + if (chunked) { + if ( vm.containsKey(SLING_FILE_LENGTH)) { + long previousFileLength = vm.get(SLING_FILE_LENGTH, Long.class); + if (previousFileLength != fileLength) { + throw new IllegalStateException("Chunk file length has changed while cunks were being uploaded expected " + previousFileLength + " chunk contained " + fileLength); + } + } + long previousChunksLength = 0; + if ( vm.containsKey(SLING_CHUNKS_LENGTH)) { + previousChunksLength = vm.get(SLING_CHUNKS_LENGTH, Long.class); + if (previousChunksLength != offset) { + throw new IllegalStateException("Chunks recieved out of order, was expecting chunk starting at " + offset + " found last chunk ending at " + previousChunksLength); + } + } + vm.put(SLING_CHUNKS_LENGTH, previousChunksLength + chunkLength); + vm.put(JcrConstants.JCR_MIXINTYPES, SLING_CHUNK_MIXIN); + } else { + try { + vm.put(JcrConstants.JCR_DATA, part.getInputStream()); + } catch (IOException e) { + throw new PersistenceException("Error while retrieving inputstream from request part.", e); + } + } + } + + /** + * Initialise the state of the jcr:content sub resource. + * @param fileResource the fileResource parent resource. + * @param changes changes that were made. + * @return the content resource. + * @throws PersistenceException + */ + private Resource initState(Resource fileResource, List changes) throws PersistenceException { + Map resourceProps = new HashMap<>(); + resourceProps.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_RESOURCE); + resourceProps.put(JcrConstants.JCR_LASTMODIFIED, Calendar.getInstance()); + resourceProps.put(JcrConstants.JCR_MIMETYPE, getContentType(part)); + + if (chunked) { + resourceProps.put(SLING_CHUNKS_LENGTH, chunkLength); + resourceProps.put(SLING_FILE_LENGTH, fileLength); + resourceProps.put(JcrConstants.JCR_MIXINTYPES, SLING_CHUNK_MIXIN); + // add a zero size file to satisfy JCR constraints. + resourceProps.put(JcrConstants.JCR_DATA, new ByteArrayInputStream(new byte[0])); + } else { + try { + resourceProps.put(JcrConstants.JCR_DATA, part.getInputStream()); + } catch (IOException e) { + throw new PersistenceException("Error while retrieving inputstream from request part.", e); + } + } + + Resource result = fileResource.getResourceResolver().create(fileResource, JcrConstants.JCR_CONTENT, resourceProps); + for( String key : resourceProps.keySet()) { + changes.add(Modification.onModified(result.getPath() + '/' + key)); + } + return result; + } + + /** + * Store the chunk in a chunked resource. If not chunked does nothing. + * @param contentResource + * @param changes + * @throws PersistenceException + */ + private void storeChunk(Resource contentResource, List changes) throws PersistenceException { + if (chunked) { + Map chunkProperties = new HashMap<>(); + chunkProperties.put(JcrConstants.JCR_PRIMARYTYPE, SLING_CHUNK_NT); + chunkProperties.put(SLING_OFFSET, offset); + try { + chunkProperties.put(JcrConstants.JCR_DATA, part.getInputStream()); + } catch (IOException e) { + throw new PersistenceException("Error while retrieving inputstream from request part.", e); + } + LOGGER.debug("Creating chunk at {} with properties {} ", chunkResourceName, chunkProperties); + Resource chunkResource = contentResource.getResourceResolver().create(contentResource, chunkResourceName, chunkProperties); + + + for (String key : chunkProperties.keySet()) { + changes.add(Modification.onModified(chunkResource.getPath() + '/' + key)); + } + + + processChunks(contentResource, changes); + } + + } + + /** + * process all chunks formed so far to create the final body. + * @param contentResource + * @param changes + * @throws PersistenceException + */ + private void processChunks(Resource contentResource, List changes) throws PersistenceException { + if (completed) { + + // have to commit before processing chunks. + contentResource.getResourceResolver().commit(); + ModifiableValueMap vm = contentResource.adaptTo(ModifiableValueMap.class); + vm.put("jcr:data", getChunksInputStream(contentResource)); + // might have to commit before removing chunk data, depending on if the InputStream still works. + removeChunkData(contentResource, vm); + } + } + + /** + * remove chunk data. + * @param contentResource + * @param vm + * @throws PersistenceException + */ + private void removeChunkData(Resource contentResource, ModifiableValueMap vm) throws PersistenceException { + for ( Resource r : contentResource.getChildren()) { + if (r.isResourceType(SLING_CHUNK_NT)) { + r.getResourceResolver().delete(r); + } + } + vm.remove(SLING_CHUNKS_LENGTH); + vm.remove(SLING_FILE_LENGTH); + } + + /** + * Create an input stream that will read though the chunks in order. + * @param contentResource + * @return + */ + private InputStream getChunksInputStream(Resource contentResource) { + List chunkResources = new ArrayList<>(); + for ( Resource r : contentResource.getChildren()) { + if (r.isResourceType(SLING_CHUNK_NT)) { + chunkResources.add(r); + } + } + Collections.sort(chunkResources, new Comparator() { + @Override + public int compare(Resource o1, Resource o2) { + long offset1 = o1.adaptTo(ValueMap.class).get(SLING_OFFSET, Long.class); + long offset2 = o2.adaptTo(ValueMap.class).get(SLING_OFFSET, Long.class); + return (int) (offset1 - offset2); + } + }); + if ( LOGGER.isDebugEnabled()) { + LOGGER.debug("Finishing Chunk upload at {} consolidating {} chunks into one file of ", + new Object[]{ + contentResource.getPath(), + chunkResources.size(), + contentResource.adaptTo(ValueMap.class).get(SLING_CHUNKS_LENGTH) + }); + LOGGER.debug("Content Resource Properties {} ", contentResource.adaptTo(ValueMap.class)); + for (Resource r : chunkResources) { + LOGGER.debug("Chunk {} properties {} ", r.getPath(), r.adaptTo(ValueMap.class)); + } + } + return new ResourceIteratorInputStream(chunkResources.iterator()); + } + + /** + * Get the content type of the part. + * @param part + * @return + */ + private String getContentType(final Part part) { + String contentType = part.getContentType(); + if (contentType != null) { + int idx = contentType.indexOf(';'); + if (idx > 0) { + contentType = contentType.substring(0, idx); + } + } + if (contentType == null || contentType.equals(MT_APP_OCTET)) { + // try to find a better content type + ServletContext ctx = this.servletContext; + if (ctx != null) { + contentType = ctx.getMimeType(part.getSubmittedFileName()); + } + if (contentType == null || contentType.equals(MT_APP_OCTET)) { + contentType = MT_APP_OCTET; + } + } + return contentType; + } + + /** + * Parses Content-Range headers according to spec https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html section 14.16 + * + * Content-Range = "Content-Range" ":" content-range-spec + * content-range-spec = byte-content-range-spec + * byte-content-range-spec = bytes-unit SP + * byte-range-resp-spec "/" + * ( instance-length | "*" ) + * byte-range-resp-spec = (first-byte-pos "-" last-byte-pos) + * | "*" + * instance-length = 1*DIGIT + * + * eg + * bytes 0-1233/1234 + * bytes 500-1233/1234 + * bytes 500-1233/* + * + * According to https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 "bytes" is the only valid range unit. + */ + public static class ContentRange { + private static final Pattern rangePattern = Pattern.compile("bytes\\s([0-9]+)-([0-9]+)\\/([0-9]*)(\\**)"); + public long length; + public long offset; + public long range; + + + public ContentRange(String contentRangeHeader) { + Matcher m = rangePattern.matcher(contentRangeHeader); + if ( m.find() ) { + offset = Long.parseLong(m.group(1)); + long end = Long.parseLong(m.group(2)); + range = end-offset+1; + if ("*".equals(m.group(4))) { + length = -1; + } else { + length = Long.parseLong(m.group(3)); + if ( offset > length ) { + throw new IllegalArgumentException("Range header "+contentRangeHeader+" is invalid, offset beyond end."); + } + if ( end > length ) { + throw new IllegalArgumentException("Range header "+contentRangeHeader+" is invalid, range end beyond end."); + } + if ( range > length ) { + throw new IllegalArgumentException("Range header "+contentRangeHeader+" is invalid, range greater than length."); + } + } + if ( offset > end ) { + throw new IllegalArgumentException("Range header "+contentRangeHeader+" is invalid, offset beyond end of range."); + } + } else { + throw new IllegalArgumentException("Range header "+contentRangeHeader+" is invalid"); + } + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java new file mode 100644 index 000000000..f6574b073 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.Iterator; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.VersioningConfiguration; + +/** + * The AbstractCopyMoveOperation is the abstract base close for + * the {@link CopyOperation} and {@link MoveOperation} classes implementing + * common behavior. + */ +abstract class AbstractCopyMoveOperation extends AbstractPostOperation { + + @Override + protected final void doRun(final SlingHttpServletRequest request, + final PostResponse response, + final List changes) + throws PersistenceException { + final VersioningConfiguration versioningConfiguration = getVersioningConfiguration(request); + + final Resource resource = request.getResource(); + final String source = resource.getPath(); + + // ensure dest is not empty/null and is absolute + String dest = request.getParameter(SlingPostConstants.RP_DEST); + if (dest == null || dest.length() == 0) { + throw new IllegalArgumentException("Unable to process " + + getOperationName() + ". Missing destination"); + } + + // register whether the path ends with a trailing slash + final boolean trailingSlash = dest.endsWith("/"); + + // ensure destination is an absolute and normalized path + if (!dest.startsWith("/")) { + dest = ResourceUtil.getParent(source) + "/" + dest; + } + dest = ResourceUtil.normalize(dest); + + // destination parent and name + final String dstParent = trailingSlash ? dest : ResourceUtil.getParent(dest); + + // delete destination if already exists + if (!trailingSlash && request.getResourceResolver().getResource(dest) != null ) { + + final String replaceString = request.getParameter(SlingPostConstants.RP_REPLACE); + final boolean isReplace = "true".equalsIgnoreCase(replaceString); + if (!isReplace) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED, + "Cannot " + getOperationName() + " " + resource + " to " + + dest + ": destination exists"); + return; + } else { + this.jcrSsupport.checkoutIfNecessary(request.getResourceResolver().getResource(dstParent), + changes, versioningConfiguration); + } + + } else { + + // check if path to destination exists and create it, but only + // if it's a descendant of the current node + if (!dstParent.equals("")) { + final Resource parentResource = request.getResourceResolver().getResource(dstParent); + if (parentResource != null ) { + this.jcrSsupport.checkoutIfNecessary(parentResource, changes, versioningConfiguration); + } else { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED, + "Cannot " + getOperationName() + " " + resource + " to " + + dest + ": parent of destination does not exist"); + return; + } + } + + // the destination is newly created, hence a create request + response.setCreateRequest(true); + } + + final Iterator resources = getApplyToResources(request); + final Resource destResource; + if (resources == null) { + + final String dstName = trailingSlash ? null : ResourceUtil.getName(dest); + destResource = execute(changes, resource, dstParent, dstName, versioningConfiguration); + + } else { + + // multiple applyTo requires trailing slash on destination + if (!trailingSlash) { + throw new IllegalArgumentException( + "Applying " + + getOperationName() + + " to multiple resources requires a trailing slash on the destination"); + } + + // multiple copy will never return 201/CREATED + response.setCreateRequest(false); + + while (resources.hasNext()) { + final Resource applyTo = resources.next(); + execute(changes, applyTo, dstParent, null, versioningConfiguration); + } + destResource = request.getResourceResolver().getResource(dest); + + } + + if ( destResource == null ) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND, + "Missing source " + resource + " for " + getOperationName()); + return; + } + // finally apply the ordering parameter + this.jcrSsupport.orderNode(request, destResource, changes); + } + + /** + * Returns a short name to be used in log and status messages. + */ + protected abstract String getOperationName(); + + /** + * Actually executes the operation. + * + * @param response The HtmlResponse used to record success of + * the operation. + * @param source The source item to act upon. + * @param destParent The absolute path of the parent of the target item. + * @param destName The name of the target item inside the + * destParent. If null the name of + * the source is used as the target item name. + * @throws PersistenceException May be thrown if an error occurs executing + * the operation. + */ + protected abstract Resource execute(List changes, + Resource source, + String destParent, + String destName, + VersioningConfiguration versioningConfiguration) + throws PersistenceException; + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperation.java new file mode 100644 index 000000000..036de5b37 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperation.java @@ -0,0 +1,716 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.operations; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.regex.Pattern; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.request.RequestParameterMap; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.NodeNameGenerator; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.apache.sling.servlets.post.impl.helper.Chunk; +import org.apache.sling.servlets.post.impl.helper.DefaultNodeNameGenerator; +import org.apache.sling.servlets.post.impl.helper.RequestProperty; + +abstract class AbstractCreateOperation extends AbstractPostOperation { + private final Random randomCollisionIndex = new Random(); + + /** + * The default node name generator + */ + private NodeNameGenerator defaultNodeNameGenerator; + + /** + * utility class for generating node names + */ + private NodeNameGenerator[] extraNodeNameGenerators; + + /** + * regular expression for parameters to ignore + */ + private Pattern ignoredParameterNamePattern; + + protected AbstractCreateOperation() { + this.defaultNodeNameGenerator = new DefaultNodeNameGenerator(); + this.ignoredParameterNamePattern = null; + } + + public void setDefaultNodeNameGenerator( + NodeNameGenerator defaultNodeNameGenerator) { + this.defaultNodeNameGenerator = defaultNodeNameGenerator; + } + + public void setExtraNodeNameGenerators( + NodeNameGenerator[] extraNodeNameGenerators) { + this.extraNodeNameGenerators = extraNodeNameGenerators; + } + + public void setIgnoredParameterNamePattern( + final Pattern ignoredParameterNamePattern) { + this.ignoredParameterNamePattern = ignoredParameterNamePattern; + } + + /** + * Returns true if any of the request parameters starts with + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}. + * In this case only parameters starting with either of the prefixes + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}, + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT ../} + * and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE /} are + * considered as providing content to be stored. Otherwise all parameters + * not starting with the command prefix : are considered as + * parameters to be stored. + * + * @param request The http request + * @return If a prefix is required. + */ + private final boolean requireItemPathPrefix( + SlingHttpServletRequest request) { + + boolean requirePrefix = false; + + Enumeration names = request.getParameterNames(); + while (names.hasMoreElements() && !requirePrefix) { + String name = (String) names.nextElement(); + requirePrefix = name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT); + } + + return requirePrefix; + } + + + + /** + * Returns true if the name starts with either + * of the prefixes + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}, + * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT ../} + * and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE /}. + * + * @param name The name + * @return {@code true} if the name has a prefix + */ + private boolean hasItemPathPrefix(String name) { + return name.startsWith(SlingPostConstants.ITEM_PREFIX_ABSOLUTE) + || name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT) + || name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_PARENT); + } + + /** + * Create resource(s) according to current request + * + * @throws PersistenceException if a resource error occurs + */ + protected void processCreate(final ResourceResolver resolver, + final Map reqProperties, + final PostResponse response, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + + final String path = response.getPath(); + final Resource resource = resolver.getResource(path); + + if ( resource == null || ResourceUtil.isSyntheticResource(resource) ) { + deepGetOrCreateResource(resolver, path, reqProperties, changes, versioningConfiguration); + response.setCreateRequest(true); + + } else { + updateNodeType(resolver, path, reqProperties, changes, versioningConfiguration); + updateMixins(resolver, path, reqProperties, changes, versioningConfiguration); + } + } + + protected void updateNodeType(final ResourceResolver resolver, + final String path, + final Map reqProperties, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + final String nodeType = getPrimaryType(reqProperties, path); + if (nodeType != null) { + final Resource rsrc = resolver.getResource(path); + final ModifiableValueMap mvm = rsrc.adaptTo(ModifiableValueMap.class); + if ( mvm != null ) { + final Object node = this.jcrSsupport.getNode(rsrc); + final boolean wasVersionable = (node == null ? false : this.jcrSsupport.isVersionable(rsrc)); + + if ( node != null ) { + this.jcrSsupport.checkoutIfNecessary(rsrc, changes, versioningConfiguration); + this.jcrSsupport.setPrimaryNodeType(node, nodeType); + } else { + mvm.put(JcrConstants.JCR_PRIMARYTYPE, nodeType); + } + + if ( node != null ) { + // this is a bit of a cheat; there isn't a formal checkout, but assigning + // the mix:versionable mixin does an implicit checkout + if (!wasVersionable && + versioningConfiguration.isCheckinOnNewVersionableNode() && + this.jcrSsupport.isVersionable(rsrc)) { + changes.add(Modification.onCheckout(path)); + } + } + } + } + } + + protected void updateMixins(final ResourceResolver resolver, + final String path, + final Map reqProperties, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + final String[] mixins = getMixinTypes(reqProperties, path); + if (mixins != null) { + + final Resource rsrc = resolver.getResource(path); + final ModifiableValueMap mvm = rsrc.adaptTo(ModifiableValueMap.class); + if ( mvm != null ) { + this.jcrSsupport.checkoutIfNecessary(rsrc, changes, versioningConfiguration); + mvm.put(JcrConstants.JCR_MIXINTYPES, mixins); + + for(final String mixin : mixins) { + // this is a bit of a cheat; there isn't a formal checkout, but assigning + // the mix:versionable mixin does an implicit checkout + if (mixin.equals(JcrConstants.MIX_VERSIONABLE) && + versioningConfiguration.isCheckinOnNewVersionableNode()) { + changes.add(Modification.onCheckout(path)); + } + } + } + } + } + + /** + * Collects the properties that form the content to be written back to the + * resource tree. + */ + protected Map collectContent( + final SlingHttpServletRequest request, + final PostResponse response) { + + final boolean requireItemPrefix = requireItemPathPrefix(request); + + // walk the request parameters and collect the properties + final LinkedHashMap reqProperties = new LinkedHashMap<>(); + for (final Map.Entry e : request.getRequestParameterMap().entrySet()) { + final String paramName = e.getKey(); + + if (ignoreParameter(paramName)) { + continue; + } + + // skip parameters that do not start with the save prefix + if (requireItemPrefix && !hasItemPathPrefix(paramName)) { + continue; + } + + // ensure the paramName is an absolute property name + final String propPath = toPropertyPath(paramName, response); + + // @TypeHint example + // + // + // causes the setProperty using the 'long' property type + if (propPath.endsWith(SlingPostConstants.TYPE_HINT_SUFFIX)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.TYPE_HINT_SUFFIX); + + final RequestParameter[] rp = e.getValue(); + if (rp.length > 0) { + prop.setTypeHintValue(rp[0].getString()); + } + + continue; + } + + // @DefaultValue + if (propPath.endsWith(SlingPostConstants.DEFAULT_VALUE_SUFFIX)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.DEFAULT_VALUE_SUFFIX); + + prop.setDefaultValues(e.getValue()); + + continue; + } + + // SLING-130: VALUE_FROM_SUFFIX means take the value of this + // property from a different field + // @ValueFrom example: + // + // causes the JCR Text property to be set to the value of the + // fulltext form field. + if (propPath.endsWith(SlingPostConstants.VALUE_FROM_SUFFIX)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.VALUE_FROM_SUFFIX); + + // @ValueFrom params must have exactly one value, else ignored + if (e.getValue().length == 1) { + final String refName = e.getValue()[0].getString(); + final RequestParameter[] refValues = request.getRequestParameters(refName); + if (refValues != null) { + prop.setValues(refValues); + } + } + + continue; + } + + // SLING-458: Allow Removal of properties prior to update + // @Delete example: + // + // causes the JCR Text property to be deleted before update + if (propPath.endsWith(SlingPostConstants.SUFFIX_DELETE)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, SlingPostConstants.SUFFIX_DELETE); + + prop.setDelete(true); + + continue; + } + + // SLING-455: @MoveFrom means moving content to another location + // @MoveFrom example: + // + // causes the JCR Text property to be set by moving the /tmp/path + // property to Text. + if (propPath.endsWith(SlingPostConstants.SUFFIX_MOVE_FROM)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_MOVE_FROM); + + // @MoveFrom params must have exactly one value, else ignored + if (e.getValue().length == 1) { + final String sourcePath = e.getValue()[0].getString(); + prop.setRepositorySource(sourcePath, true); + } + + continue; + } + + // SLING-455: @CopyFrom means moving content to another location + // @CopyFrom example: + // + // causes the JCR Text property to be set by copying the /tmp/path + // property to Text. + if (propPath.endsWith(SlingPostConstants.SUFFIX_COPY_FROM)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_COPY_FROM); + + // @MoveFrom params must have exactly one value, else ignored + if (e.getValue().length == 1) { + final String sourcePath = e.getValue()[0].getString(); + prop.setRepositorySource(sourcePath, false); + } + + continue; + } + + // SLING-1412: @IgnoreBlanks + // @Ignore example: + // + // + // + // + // causes the JCR Text property to be set by copying the /tmp/path + // property to Text. + if (propPath.endsWith(SlingPostConstants.SUFFIX_IGNORE_BLANKS)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_IGNORE_BLANKS); + + if (e.getValue().length == 1) { + prop.setIgnoreBlanks(true); + } + + continue; + } + + if (propPath.endsWith(SlingPostConstants.SUFFIX_USE_DEFAULT_WHEN_MISSING)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_USE_DEFAULT_WHEN_MISSING); + + if (e.getValue().length == 1) { + prop.setUseDefaultWhenMissing(true); + } + + continue; + } + // @Patch + // Example: + // + // + // + // + if (propPath.endsWith(SlingPostConstants.SUFFIX_PATCH)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_PATCH); + + prop.setPatch(true); + + continue; + } + if (propPath.endsWith(SlingPostConstants.SUFFIX_OFFSET)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_OFFSET); + if (e.getValue().length == 1) { + Chunk chunk = prop.getChunk(); + if(chunk == null){ + chunk = new Chunk(); + } + chunk.setOffsetValue(Long.parseLong(e.getValue()[0].toString())); + prop.setChunk(chunk); + } + continue; + } + + if (propPath.endsWith(SlingPostConstants.SUFFIX_COMPLETED)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_COMPLETED); + if (e.getValue().length == 1) { + Chunk chunk = prop.getChunk(); + if(chunk == null){ + chunk = new Chunk(); + } + chunk.setCompleted(Boolean.parseBoolean((e.getValue()[0].toString()))); + prop.setChunk(chunk); + } + continue; + } + + if (propPath.endsWith(SlingPostConstants.SUFFIX_LENGTH)) { + final RequestProperty prop = getOrCreateRequestProperty( + reqProperties, propPath, + SlingPostConstants.SUFFIX_LENGTH); + if (e.getValue().length == 1) { + Chunk chunk = prop.getChunk(); + if(chunk == null){ + chunk = new Chunk(); + } + chunk.setLength(Long.parseLong(e.getValue()[0].toString())); + prop.setChunk(chunk); + } + continue; + } + + // plain property, create from values + final RequestProperty prop = getOrCreateRequestProperty(reqProperties, + propPath, null); + prop.setValues(e.getValue()); + } + + return reqProperties; + } + + /** + * Returns true if the parameter of the given name should be + * ignored. + */ + private boolean ignoreParameter(final String paramName) { + // do not store parameters with names starting with sling:post + if (paramName.startsWith(SlingPostConstants.RP_PREFIX)) { + return true; + } + + // SLING-298: skip form encoding parameter + if (paramName.equals("_charset_")) { + return true; + } + + // SLING-2120: ignore parameter match ignoredParameterNamePattern + if (this.ignoredParameterNamePattern != null + && this.ignoredParameterNamePattern.matcher(paramName).matches()) { + return true; + } + + return false; + } + + /** + * Returns the paramName as an absolute (unnormalized) property + * path by prepending the response path (response.getPath) to + * the parameter name if not already absolute. + */ + private String toPropertyPath(String paramName, PostResponse response) { + if (!paramName.startsWith("/")) { + paramName = ResourceUtil.normalize(response.getPath() + '/' + paramName); + } + + return paramName; + } + + /** + * Returns the request property for the given property path. If such a + * request property does not exist yet it is created and stored in the + * props. + * + * @param props The map of already seen request properties. + * @param paramName The absolute path of the property including the + * suffix to be looked up. + * @param suffix The (optional) suffix to remove from the + * paramName before looking it up. + * @return The {@link RequestProperty} for the paramName. + */ + private RequestProperty getOrCreateRequestProperty( + Map props, String paramName, String suffix) { + if (suffix != null && paramName.endsWith(suffix)) { + paramName = paramName.substring(0, paramName.length() + - suffix.length()); + } + + RequestProperty prop = props.get(paramName); + if (prop == null) { + prop = new RequestProperty(paramName); + props.put(paramName, prop); + } + + return prop; + } + + + /** + * Deep gets or creates a resource, parent-padding with default resources. If + * the path is empty, the given parent resource is returned. + * + * @param path path to resources that needs to be deep-created + * @return Resource at path + * @throws PersistenceException if an error occurs + * @throws IllegalArgumentException if the path is relative and parent is + * null + */ + protected Resource deepGetOrCreateResource(final ResourceResolver resolver, + final String path, + final Map reqProperties, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + if (log.isDebugEnabled()) { + log.debug("Deep-creating resource '{}'", path); + } + if (path == null || !path.startsWith("/")) { + throw new IllegalArgumentException("path must be an absolute path."); + } + // get the starting resource + String startingResourcePath = path; + Resource startingResource = null; + while (startingResource == null) { + if (startingResourcePath.equals("/")) { + startingResource = resolver.getResource("/"); + if (startingResource == null){ + throw new PersistenceException("Access denied for root resource, resource can't be created: " + path); + } + } else { + final Resource r = resolver.getResource(startingResourcePath); + if ( r != null && !ResourceUtil.isSyntheticResource(r)) { + startingResource = resolver.getResource(startingResourcePath); + updateNodeType(resolver, startingResourcePath, reqProperties, changes, versioningConfiguration); + updateMixins(resolver, startingResourcePath, reqProperties, changes, versioningConfiguration); + } else { + int pos = startingResourcePath.lastIndexOf('/'); + if (pos > 0) { + startingResourcePath = startingResourcePath.substring(0, pos); + } else { + startingResourcePath = "/"; + } + } + } + } + // is the searched resource already existing? + if (startingResourcePath.length() == path.length()) { + return startingResource; + } + // create nodes + int from = (startingResourcePath.length() == 1 + ? 1 + : startingResourcePath.length() + 1); + Resource resource = startingResource; + while (from > 0) { + final int to = path.indexOf('/', from); + final String name = to < 0 ? path.substring(from) : path.substring( + from, to); + // although the resource should not exist (according to the first test + // above) + // we do a sanety check. + final Resource child = resource.getChild(name); + if (child != null && !ResourceUtil.isSyntheticResource(child)) { + resource = child; + updateNodeType(resolver, resource.getPath(), reqProperties, changes, versioningConfiguration); + updateMixins(resolver, resource.getPath(), reqProperties, changes, versioningConfiguration); + } else { + final String tmpPath = to < 0 ? path : path.substring(0, to); + // check for node type + final String nodeType = getPrimaryType(reqProperties, tmpPath); + + this.jcrSsupport.checkoutIfNecessary(resource, changes, versioningConfiguration); + + try { + final Map props = new HashMap<>(); + if (nodeType != null) { + props.put("jcr:primaryType", nodeType); + } + // check for mixin types + final String[] mixinTypes = getMixinTypes(reqProperties, + tmpPath); + if (mixinTypes != null) { + props.put("jcr:mixinTypes", mixinTypes); + } + + resource = resolver.create(resource, name, props); + } catch (final PersistenceException e) { + log.error("Unable to create resource named " + name + " in " + resource.getPath()); + throw e; + } + changes.add(Modification.onCreated(resource.getPath())); + } + from = to + 1; + } + return resource; + } + + /** + * Checks the collected content for a jcr:primaryType property at the + * specified path. + * + * @param path path to check + * @return the primary type or null + */ + private String getPrimaryType(Map reqProperties, + String path) { + RequestProperty prop = reqProperties.get(path + "/jcr:primaryType"); + return prop == null ? null : prop.getStringValues()[0]; + } + + /** + * Checks the collected content for a jcr:mixinTypes property at the + * specified path. + * + * @param path path to check + * @return the mixin types or null + */ + private String[] getMixinTypes(Map reqProperties, + String path) { + RequestProperty prop = reqProperties.get(path + "/jcr:mixinTypes"); + return (prop == null) || !prop.hasValues() ? null : prop.getStringValues(); + } + + + protected String generateName(SlingHttpServletRequest request, String basePath) + throws PersistenceException { + + // SLING-1091: If a :name parameter is supplied, the (first) value of this parameter is used unmodified as the name + // for the new node. If the name is illegally formed with respect to JCR name requirements, an exception will be + // thrown when trying to create the node. The assumption with the :name parameter is, that the caller knows what + // he (or she) is supplying and should get the exact result if possible. + RequestParameterMap parameters = request.getRequestParameterMap(); + RequestParameter specialParam = parameters.getValue(SlingPostConstants.RP_NODE_NAME); + if ( specialParam != null ) { + if ( specialParam.getString() != null && specialParam.getString().length() > 0 ) { + // If the path ends with a *, create a node under its parent, with + // a generated node name + basePath = basePath += "/" + specialParam.getString(); + + // if the resulting path already exists then report an error + if (request.getResourceResolver().getResource(basePath) != null) { + throw new PersistenceException( + "Collision in node names for path=" + basePath); + } + + return basePath; + } + } + + // no :name value was supplied, so generate a name + boolean requirePrefix = requireItemPathPrefix(request); + + String generatedName = null; + if (extraNodeNameGenerators != null) { + for (NodeNameGenerator generator : extraNodeNameGenerators) { + generatedName = generator.getNodeName(request, basePath, requirePrefix, defaultNodeNameGenerator); + if (generatedName != null) { + break; + } + } + } + if (generatedName == null) { + generatedName = defaultNodeNameGenerator.getNodeName(request, basePath, requirePrefix, defaultNodeNameGenerator); + } + + // If the path ends with a *, create a node under its parent, with + // a generated node name + basePath += "/" + generatedName; + + basePath = ensureUniquePath(request, basePath); + + return basePath; + } + + /** Generate a unique path in case the node name generator didn't */ + private String ensureUniquePath(SlingHttpServletRequest request, String basePath) throws PersistenceException { + // if resulting path exists, add a suffix until it's not the case + // anymore + final ResourceResolver resolver = request.getResourceResolver(); + + // if resulting path exists, add a random suffix until it's not the case + // anymore + final int MAX_TRIES = 1000; + if (resolver.getResource(basePath) != null ) { + for(int i=0; i < MAX_TRIES; i++) { + final int uniqueIndex = Math.abs(randomCollisionIndex.nextInt()); + String newPath = basePath + "_" + uniqueIndex; + if (resolver.getResource(newPath) == null) { + basePath = basePath + "_" + uniqueIndex; + basePath = newPath; + break; + } + } + + // Give up after MAX_TRIES + if (resolver.getResource(basePath) != null ) { + throw new PersistenceException( + "Collision in generated node names under " + basePath + ", generated path " + basePath + " already exists"); + } + } + + return basePath; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractPostOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractPostOperation.java new file mode 100644 index 000000000..022bb944e --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractPostOperation.java @@ -0,0 +1,402 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.wrappers.SlingRequestPaths; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostOperation; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.SlingPostProcessor; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.apache.sling.servlets.post.impl.helper.JCRSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The AbstractPostOperation class is a base implementation of the + * {@link PostOperation} service interface providing actual implementations with + * useful tooling and common functionality like preparing the change logs or + * saving or refreshing. + */ +public abstract class AbstractPostOperation implements PostOperation { + + /** + * Default logger + */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** The JCR support provides additional functionality if the resources a backed up by JCR. */ + protected final JCRSupport jcrSsupport = JCRSupport.INSTANCE; + + /** + * Prepares and finalizes the actual operation. Preparation encompasses + * getting the absolute path of the item to operate on by calling the + * {@link #getResourcePath(SlingHttpServletRequest)} method and setting the + * location and parent location on the response. After the operation has + * been done in the {@link #doRun(SlingHttpServletRequest, PostResponse, List)} + * method the session is saved if there are unsaved modifications. In case + * of errors, the unsaved changes in the session are rolled back. + * + * @param request the request to operate on + * @param response The PostResponse to record execution + * progress. + * @param processors The array of processors + */ + @Override + public void run(final SlingHttpServletRequest request, + final PostResponse response, + final SlingPostProcessor[] processors) { + final VersioningConfiguration versionableConfiguration = getVersioningConfiguration(request); + + try { + // calculate the paths + String path = this.getResourcePath(request); + response.setPath(path); + + // location + response.setLocation(externalizePath(request, path)); + + // parent location + path = ResourceUtil.getParent(path); + if (path != null) { + response.setParentLocation(externalizePath(request, path)); + } + + final List changes = new ArrayList<>(); + + doRun(request, response, changes); + + // invoke processors + if (processors != null) { + for (SlingPostProcessor processor : processors) { + processor.process(request, changes); + } + } + + // check modifications for remaining postfix and store the base path + final Map modificationSourcesContainingPostfix = new HashMap<>(); + final Set allModificationSources = new HashSet<>(changes.size()); + for (final Modification modification : changes) { + final String source = modification.getSource(); + if (source != null) { + allModificationSources.add(source); + final int atIndex = source.indexOf('@'); + if (atIndex > 0) { + modificationSourcesContainingPostfix.put(source.substring(0, atIndex), source); + } + } + } + + // fail if any of the base paths (before the postfix) which had a postfix are contained in the modification set + if (modificationSourcesContainingPostfix.size() > 0) { + for (final Map.Entry sourceToCheck : modificationSourcesContainingPostfix.entrySet()) { + if (allModificationSources.contains(sourceToCheck.getKey())) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Postfix-containing path " + sourceToCheck.getValue() + + " contained in the modification list. Check configuration."); + return; + } + } + } + + final Set nodesToCheckin = new LinkedHashSet<>(); + + // set changes on html response + for(Modification change : changes) { + switch ( change.getType() ) { + case MODIFY : response.onModified(change.getSource()); break; + case DELETE : response.onDeleted(change.getSource()); break; + case MOVE : response.onMoved(change.getSource(), change.getDestination()); break; + case COPY : response.onCopied(change.getSource(), change.getDestination()); break; + case CREATE : + response.onCreated(change.getSource()); + if (versionableConfiguration.isCheckinOnNewVersionableNode()) { + nodesToCheckin.add(change.getSource()); + } + break; + case ORDER : response.onChange("ordered", change.getSource(), change.getDestination()); break; + case CHECKOUT : + response.onChange("checkout", change.getSource()); + nodesToCheckin.add(change.getSource()); + break; + case CHECKIN : + response.onChange("checkin", change.getSource()); + nodesToCheckin.remove(change.getSource()); + break; + case RESTORE : response.onChange("restore", change.getSource()); + break; + } + } + + if (isResourceResolverCommitRequired(request)) { + request.getResourceResolver().commit(); + } + + if (!isSkipCheckin(request)) { + // now do the checkins + for(String checkinPath : nodesToCheckin) { + if (this.jcrSsupport.checkin(request.getResourceResolver().getResource(checkinPath))) { + response.onChange("checkin", checkinPath); + } + } + } + + } catch (Exception e) { + + log.error("Exception during response processing.", e); + response.setError(e); + + } finally { + if (isResourceResolverCommitRequired(request)) { + request.getResourceResolver().revert(); + } + } + } + + /** + * Actually performs the desired operation filling progress into the + * changes list and preparing and further information in the + * response. + *

+ * The response comes prepared with the path, location and + * parent location set. Other properties are expected to be set by this + * implementation. + * + * @param request The SlingHttpServletRequest providing the + * input, mostly in terms of request parameters, to the + * operation. + * @param response The {@link PostResponse} to fill with response + * information + * @param changes A container to add {@link Modification} instances + * representing the operations done. + * @throws PersistenceException Maybe thrown if any error occurs while + * accessing the repository. + */ + protected abstract void doRun(SlingHttpServletRequest request, + PostResponse response, + List changes) throws PersistenceException; + + /** + * Get the versioning configuration. + * @param request The http request + * @return The versioning configuration + */ + protected VersioningConfiguration getVersioningConfiguration(final SlingHttpServletRequest request) { + VersioningConfiguration versionableConfiguration = + (VersioningConfiguration) request.getAttribute(VersioningConfiguration.class.getName()); + return versionableConfiguration != null ? versionableConfiguration : new VersioningConfiguration(); + } + + /** + * Check if checkin should be skipped + * @param request The http request + * @return {@code true} if checkin should be skipped + */ + protected boolean isSkipCheckin(SlingHttpServletRequest request) { + return !getVersioningConfiguration(request).isAutoCheckin(); + } + + /** + * Check whether changes should be written back + * @param request The http request + * @return {@code true} If committing be skipped + */ + private boolean isSkipSessionHandling(SlingHttpServletRequest request) { + return Boolean.parseBoolean((String) request.getAttribute(SlingPostConstants.ATTR_SKIP_SESSION_HANDLING)) == true; + } + + /** + * Check whether commit to the resource resolver should be called. + * @param request The http request + * @return {@code true} if a commit is required. + */ + private boolean isResourceResolverCommitRequired(SlingHttpServletRequest request) { + return !isSkipSessionHandling(request) && request.getResourceResolver().hasChanges(); + } + + /** + * Returns an iterator on Resource instances addressed in the + * {@link SlingPostConstants#RP_APPLY_TO} request parameter. If the request + * parameter is not set, null is returned. If the parameter + * is set with valid resources an empty iterator is returned. Any resources + * addressed in the {@link SlingPostConstants#RP_APPLY_TO} parameter is + * ignored. + * + * @param request The SlingHttpServletRequest object used to + * get the {@link SlingPostConstants#RP_APPLY_TO} parameter. + * @return The iterator of resources listed in the parameter or + * null if the parameter is not set in the request. + */ + protected Iterator getApplyToResources( + final SlingHttpServletRequest request) { + + final String[] applyTo = request.getParameterValues(SlingPostConstants.RP_APPLY_TO); + if (applyTo == null) { + return null; + } + + return new ApplyToIterator(request, applyTo); + } + + /** + * Returns an external form of the given path prepending the context path + * and appending a display extension. + * + * @param request The http request + * @param path the path to externalize + * @return the url + */ + protected final String externalizePath(final SlingHttpServletRequest request, + final String path) { + StringBuilder ret = new StringBuilder(); + ret.append(SlingRequestPaths.getContextPath(request)); + ret.append(request.getResourceResolver().map(path)); + + // append optional extension + String ext = request.getParameter(SlingPostConstants.RP_DISPLAY_EXTENSION); + if (ext != null && ext.length() > 0) { + if (ext.charAt(0) != '.') { + ret.append('.'); + } + ret.append(ext); + } + + return ret.toString(); + } + + /** + * Returns the path of the resource of the request as the resource path. + *

+ * This method may be overwritten by extension if the operation has + * different requirements on path processing. + * @param request The http request + * @return The resource path + */ + protected String getResourcePath(SlingHttpServletRequest request) { + return request.getResource().getPath(); + } + + private static class ApplyToIterator implements Iterator { + + private final ResourceResolver resolver; + private final Resource baseResource; + private final String[] paths; + + private int pathIndex; + + private Resource nextResource; + + private Iterator resourceIterator = null; + + ApplyToIterator(SlingHttpServletRequest request, String[] paths) { + this.resolver = request.getResourceResolver(); + this.baseResource = request.getResource(); + this.paths = paths; + this.pathIndex = 0; + + nextResource = seek(); + } + + @Override + public boolean hasNext() { + return nextResource != null; + } + + @Override + public Resource next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + Resource result = nextResource; + nextResource = seek(); + + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private Resource seek() { + if (resourceIterator != null) { + if (resourceIterator.hasNext()) { + //return the next resource in the iterator + Resource res = resourceIterator.next(); + return res; + } + resourceIterator = null; + } + while (pathIndex < paths.length) { + String path = paths[pathIndex]; + pathIndex++; + + //SLING-2415 - support wildcard as the last segment of the applyTo path + if (path.endsWith("*")) { + if (path.length() == 1) { + resourceIterator = baseResource.listChildren(); + } else if (path.endsWith("/*")) { + path = path.substring(0, path.length() - 2); + if (path.length() == 0) { + resourceIterator = baseResource.listChildren(); + } else { + Resource res = resolver.getResource(baseResource, path); + if (res != null) { + resourceIterator = res.listChildren(); + } + } + } + if (resourceIterator != null) { + //return the first resource in the iterator + if (resourceIterator.hasNext()) { + Resource res = resourceIterator.next(); + return res; + } + resourceIterator = null; + } + } else { + Resource res = resolver.getResource(baseResource, path); + if (res != null) { + return res; + } + } + } + + // no more elements in the array + return null; + } + } +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckinOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckinOperation.java new file mode 100644 index 000000000..355b6b352 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckinOperation.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; + +/** + * The CheckinOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_CHECKIN checkin} + * operation for the Sling default POST servlet. + * The checkin operation depends on the resources being backed up by a JCR node. + */ +public class CheckinOperation extends AbstractPostOperation { + + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) + throws PersistenceException { + try { + Iterator res = getApplyToResources(request); + if (res == null) { + + Resource resource = request.getResource(); + Node node = resource.adaptTo(Node.class); + if (node == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND, + "Missing source " + resource + " for checkout"); + return; + } + + node.getSession().getWorkspace().getVersionManager().checkin(node.getPath()); + changes.add(Modification.onCheckin(resource.getPath())); + + } else { + + while (res.hasNext()) { + Resource resource = res.next(); + Node node = resource.adaptTo(Node.class); + if (node != null) { + node.getSession().getWorkspace().getVersionManager().checkin(node.getPath()); + changes.add(Modification.onCheckin(resource.getPath())); + } + } + + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + /** + * Checkin operation always checks in. + */ + @Override + protected boolean isSkipCheckin(SlingHttpServletRequest request) { + return false; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckoutOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckoutOperation.java new file mode 100644 index 000000000..e029c762f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CheckoutOperation.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; + +/** + * The CheckoutOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_CHECKOUT checkout} + * operation for the Sling default POST servlet. + * The checkout operation depends on the resources being backed up by a JCR node. + */ +public class CheckoutOperation extends AbstractPostOperation { + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) + throws PersistenceException { + try { + Iterator res = getApplyToResources(request); + if (res == null) { + + Resource resource = request.getResource(); + Node node = resource.adaptTo(Node.class); + if (node == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND, + "Missing source " + resource + " for checkout"); + return; + } + + node.getSession().getWorkspace().getVersionManager().checkout(node.getPath()); + changes.add(Modification.onCheckout(resource.getPath())); + + } else { + + while (res.hasNext()) { + Resource resource = res.next(); + Node node = resource.adaptTo(Node.class); + if (node != null) { + node.getSession().getWorkspace().getVersionManager().checkout(node.getPath()); + changes.add(Modification.onCheckout(resource.getPath())); + } + } + + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + /** + * Checkout operation is always skipping checkin. + */ + @Override + protected boolean isSkipCheckin(SlingHttpServletRequest request) { + return true; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java new file mode 100644 index 000000000..340bf5779 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.List; + +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.VersioningConfiguration; + +/** + * The CopyOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_COPY copy} + * operation for the Sling default POST servlet. + */ +public class CopyOperation extends AbstractCopyMoveOperation { + + @Override + protected String getOperationName() { + return "copy"; + } + + @Override + protected Resource execute(final List changes, + final Resource source, + final String destParent, + final String destName, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + final Resource parentRsrc = source.getResourceResolver().getResource(destParent); + // check if the item is backed by JCR + final Object item = this.jcrSsupport.getItem(source); + final Object parentItem = this.jcrSsupport.getNode(parentRsrc); + if ( item == null || parentItem == null ) { + // no JCR, copy via resources + final Resource result = copy(source, parentRsrc); + changes.add(Modification.onCopied(source.getPath(), result.getPath())); + return result; + } else { + final String dest = this.jcrSsupport.copy(item, parentItem, destName); + changes.add(Modification.onCopied(source.getPath(), dest)); + log.debug("copy {} to {}", source, dest); + return source.getResourceResolver().getResource(dest); + } + } + + /** + * Copy the source as a child resource to the parent + */ + private Resource copy(final Resource source, final Resource dest) + throws PersistenceException { + final ValueMap vm = source.getValueMap(); + final Resource result = source.getResourceResolver().create(dest, source.getName(), vm); + for(final Resource c : source.getChildren()) { + copy(c, result); + } + return result; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java new file mode 100644 index 000000000..78c9847c1 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.Iterator; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestPathInfo; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.apache.sling.servlets.post.impl.helper.SlingFileUploadHandler; + +/** + * The DeleteOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_DELETE + * delete} operation for the Sling default POST servlet. + */ +public class DeleteOperation extends AbstractPostOperation { + + /** + * handler that deals with file upload + */ + private final SlingFileUploadHandler uploadHandler; + + public DeleteOperation() { + this.uploadHandler = new SlingFileUploadHandler(); + } + + @Override + protected void doRun(final SlingHttpServletRequest request, + final PostResponse response, final List changes) + throws PersistenceException { + + // SLING-3203: selectors, extension and suffix make no sense here and + // might lead to deleting other resources than the one the user means. + final RequestPathInfo rpi = request.getRequestPathInfo(); + if( (rpi.getSelectors() != null && rpi.getSelectors().length > 0) + || (rpi.getExtension() != null && rpi.getExtension().length() > 0) + || (rpi.getSuffix() != null && rpi.getSuffix().length() > 0)) { + response.setStatus( + HttpServletResponse.SC_FORBIDDEN, + "DeleteOperation request cannot include any selectors, extension or suffix"); + return; + } + + final VersioningConfiguration versioningConfiguration = getVersioningConfiguration(request); + final boolean deleteChunks = isDeleteChunkRequest(request); + final Iterator res = getApplyToResources(request); + if (res == null) { + final Resource resource = request.getResource(); + deleteResource(resource, changes, versioningConfiguration, + deleteChunks); + } else { + while (res.hasNext()) { + final Resource resource = res.next(); + deleteResource(resource, changes, versioningConfiguration, + deleteChunks); + } + + } + } + + /** + * Delete chunks if + * {@link DeleteOperation#isDeleteChunkRequest(SlingHttpServletRequest)} is + * true otherwise delete resource. + */ + private void deleteResource(final Resource resource, + final List changes, + final VersioningConfiguration versioningConfiguration, + final boolean deleteChunks) + throws PersistenceException { + if (deleteChunks) { + uploadHandler.deleteChunks(resource); + } else { + this.jcrSsupport.checkoutIfNecessary(resource.getParent(), changes, + versioningConfiguration); + } + + resource.getResourceResolver().delete(resource); + + changes.add(Modification.onDeleted(resource.getPath())); + } + + /** + * Return true if request is to delete chunks. To return true, request will + * should parameter ":applyToChunks" and it should be true. + */ + protected boolean isDeleteChunkRequest(SlingHttpServletRequest request) { + + return Boolean.parseBoolean(request.getParameter(SlingPostConstants.RP_APPLY_TO_CHUNKS)); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ImportOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ImportOperation.java new file mode 100644 index 000000000..9d934d1e1 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ImportOperation.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.jcr.contentloader.ContentImportListener; +import org.apache.sling.jcr.contentloader.ContentImporter; +import org.apache.sling.jcr.contentloader.ImportOptions; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.ModificationType; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.apache.sling.servlets.post.impl.helper.RequestProperty; + +/** + * The ImportOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_IMPORT} + * import operation for the Sling default POST servlet. + */ +public class ImportOperation extends AbstractCreateOperation { + + /** + * Reference to the content importer service + */ + private Object contentImporter; + + public void setContentImporter(Object importer) { + this.contentImporter = importer; + } + + public void unsetContentImporter(Object importer) { + if ( this.contentImporter == importer ) { + this.contentImporter = null; + } + } + + private String getRequestParamAsString(SlingHttpServletRequest request, String key) { + RequestParameter requestParameter = request.getRequestParameter(key); + if (requestParameter == null) { + return null; + } + return requestParameter.getString(); + } + + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, final List changes) + throws PersistenceException { + try { + Object importer = contentImporter; + if (importer == null) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Missing content importer for import"); + return; + } + Map reqProperties = collectContent(request, + response); + + VersioningConfiguration versioningConfiguration = getVersioningConfiguration(request); + + // do not change order unless you have a very good reason. + Session session = request.getResourceResolver().adaptTo(Session.class); + + processCreate(request.getResourceResolver(), reqProperties, response, changes, versioningConfiguration); + + String path = response.getPath(); + Node node = null; + try { + node = (Node) session.getItem(path); + } catch ( RepositoryException e ) { + log.warn(e.getMessage(),e); + // was not able to resolve the node + } catch ( ClassCastException e) { + log.warn(e.getMessage(),e); + // it was not a node + } + if (node == null) { + + response.setStatus(HttpServletResponse.SC_NOT_FOUND, + "Missing target node " + path + " for import"); + return; + } + + String contentType = getRequestParamAsString(request, SlingPostConstants.RP_CONTENT_TYPE); + if (contentType == null) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED, + "Required :contentType parameter is missing"); + return; + } + + //import options passed as request parameters. + final boolean replace = "true".equalsIgnoreCase(getRequestParamAsString(request, SlingPostConstants.RP_REPLACE)); + final boolean replaceProperties = "true".equalsIgnoreCase(getRequestParamAsString(request, SlingPostConstants.RP_REPLACE_PROPERTIES)); + final boolean checkin = "true".equalsIgnoreCase(getRequestParamAsString(request, SlingPostConstants.RP_CHECKIN)); + final boolean autoCheckout = "true".equalsIgnoreCase(getRequestParamAsString(request, SlingPostConstants.RP_AUTO_CHECKOUT)); + + String basePath = getResourcePath(request); + if (basePath.endsWith("/")) { + //remove the trailing slash + basePath = basePath.substring(0, basePath.length() - 1); + } + + // default to creating content + response.setCreateRequest(true); + + final String targetName; + //check if a name was posted to use as the name of the imported root node + if (getRequestParamAsString(request, SlingPostConstants.RP_NODE_NAME) != null) { + // exact name + targetName = getRequestParamAsString(request, SlingPostConstants.RP_NODE_NAME); + if (targetName.length() > 0 && node.hasNode(targetName)) { + if (replace) { + response.setCreateRequest(false); + } else { + response.setStatus( + HttpServletResponse.SC_PRECONDITION_FAILED, + "Cannot import " + path + "/" + targetName + + ": node exists"); + return; + } + } + } else if (getRequestParamAsString(request, SlingPostConstants.RP_NODE_NAME_HINT) != null) { + // node name hint only + String nodePath = generateName(request, basePath); + String name = nodePath.substring(nodePath.lastIndexOf('/') + 1); + targetName = name; + } else { + // no name posted, so the import won't create a root node + targetName = ""; + } + final String contentRootName = targetName + "." + contentType; + try { + InputStream contentStream = null; + RequestParameter contentParameter = request.getRequestParameter(SlingPostConstants.RP_CONTENT); + if (contentParameter != null) { + contentStream = contentParameter.getInputStream(); + } else { + RequestParameter contentFile = request.getRequestParameter(SlingPostConstants.RP_CONTENT_FILE); + if (contentFile != null) { + contentStream = contentFile.getInputStream(); + } + } + + if (contentStream == null) { + response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED, + "Missing content for import"); + return; + } else { + ((ContentImporter)importer).importContent(node, contentRootName, contentStream, + new ImportOptions() { + + @Override + public boolean isCheckin() { + return checkin; + } + + @Override + public boolean isAutoCheckout() { + return autoCheckout; + } + + @Override + public boolean isIgnoredImportProvider( + String extension) { + // this probably isn't important in this context. + return false; + } + + @Override + public boolean isOverwrite() { + return replace; + } + + /* (non-Javadoc) + * @see org.apache.sling.jcr.contentloader.ImportOptions#isPropertyOverwrite() + */ + @Override + public boolean isPropertyOverwrite() { + return replaceProperties; + } + }, + new ContentImportListener() { + + @Override + public void onReorder(String orderedPath, String beforeSibbling) { + changes.add(Modification.onOrder(orderedPath, beforeSibbling)); + } + + @Override + public void onMove(String srcPath, String destPath) { + changes.add(Modification.onMoved(srcPath, destPath)); + } + + @Override + public void onModify(String srcPath) { + changes.add(Modification.onModified(srcPath)); + } + + @Override + public void onDelete(String srcPath) { + changes.add(Modification.onDeleted(srcPath)); + } + + @Override + public void onCreate(String srcPath) { + changes.add(Modification.onCreated(srcPath)); + } + + @Override + public void onCopy(String srcPath, String destPath) { + changes.add(Modification.onCopied(srcPath, destPath)); + } + + @Override + public void onCheckin(String srcPath) { + changes.add(Modification.onCheckin(srcPath)); + } + @Override + public void onCheckout(String srcPath) { + changes.add(Modification.onCheckout(srcPath)); + } + }); + } + + if (!changes.isEmpty()) { + //fill in the data for the response report + Modification modification = changes.get(0); + if (modification.getType() == ModificationType.CREATE) { + String importedPath = modification.getSource(); + response.setLocation(externalizePath(request, importedPath)); + response.setPath(importedPath); + int lastSlashIndex = importedPath.lastIndexOf('/'); + if (lastSlashIndex != -1) { + String parentPath = importedPath.substring(0, lastSlashIndex); + response.setParentLocation(externalizePath(request, parentPath)); + } + } + } + } catch (IOException e) { + throw new PersistenceException(e.getMessage(), e); + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + + + } +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java new file mode 100644 index 000000000..d22d10c98 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.SlingException; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.VersioningConfiguration; +import org.apache.sling.servlets.post.impl.helper.DateParser; +import org.apache.sling.servlets.post.impl.helper.RequestProperty; +import org.apache.sling.servlets.post.impl.helper.SlingFileUploadHandler; +import org.apache.sling.servlets.post.impl.helper.SlingPropertyValueHandler; + +/** + * The ModifyOperation class implements the default operation + * called by the Sling default POST servlet if no operation is requested by the + * client. This operation is able to create and/or modify content. + */ +public class ModifyOperation extends AbstractCreateOperation { + + private DateParser dateParser; + + /** + * handler that deals with file upload + */ + private final SlingFileUploadHandler uploadHandler; + + public ModifyOperation() { + this.dateParser = new DateParser(); + this.uploadHandler = new SlingFileUploadHandler(); + } + + public void setServletContext(final ServletContext servletContext) { + this.uploadHandler.setServletContext(servletContext); + } + + public void setDateParser(final DateParser dateParser) { + this.dateParser = dateParser; + } + + @Override + protected void doRun(final SlingHttpServletRequest request, + final PostResponse response, + final List changes) + throws PersistenceException { + final Map reqProperties = collectContent(request, response); + + final VersioningConfiguration versioningConfiguration = getVersioningConfiguration(request); + + // do not change order unless you have a very good reason. + + // ensure root of new content + processCreate(request.getResourceResolver(), reqProperties, response, changes, versioningConfiguration); + + // write content from existing content (@Move/CopyFrom parameters) + processMoves(request.getResourceResolver(), reqProperties, changes, versioningConfiguration); + processCopies(request.getResourceResolver(), reqProperties, changes, versioningConfiguration); + + // cleanup any old content (@Delete parameters) + processDeletes(request.getResourceResolver(), reqProperties, changes, versioningConfiguration); + + // write content from form + writeContent(request.getResourceResolver(), reqProperties, changes, versioningConfiguration); + + // order content + final Resource newResource = request.getResourceResolver().getResource(response.getPath()); + this.jcrSsupport.orderNode(request, newResource, changes); + } + + @Override + protected String getResourcePath(SlingHttpServletRequest request) { + + // calculate the paths + StringBuilder rootPathBuf = new StringBuilder(); + String suffix; + Resource currentResource = request.getResource(); + if (ResourceUtil.isSyntheticResource(currentResource)) { + + // no resource, treat the missing resource path as suffix + suffix = currentResource.getPath(); + + } else { + + // resource for part of the path, use request suffix + suffix = request.getRequestPathInfo().getSuffix(); + + if (suffix != null) { + // cut off any selectors/extension from the suffix + int dotPos = suffix.indexOf('.'); + if (dotPos > 0) { + suffix = suffix.substring(0, dotPos); + } + } + + // and preset the path buffer with the resource path + rootPathBuf.append(currentResource.getPath()); + + } + + // check for extensions or create suffix in the suffix + boolean doGenerateName = false; + if (suffix != null) { + + // check whether it is a create request (trailing /) + if (suffix.endsWith(SlingPostConstants.DEFAULT_CREATE_SUFFIX)) { + suffix = suffix.substring(0, suffix.length() + - SlingPostConstants.DEFAULT_CREATE_SUFFIX.length()); + doGenerateName = true; + + // or with the star suffix /* + } else if (suffix.endsWith(SlingPostConstants.STAR_CREATE_SUFFIX)) { + suffix = suffix.substring(0, suffix.length() + - SlingPostConstants.STAR_CREATE_SUFFIX.length()); + doGenerateName = true; + } + + // append the remains of the suffix to the path buffer + rootPathBuf.append(suffix); + + } + + String path = rootPathBuf.toString(); + + if (doGenerateName) { + try { + path = generateName(request, path); + } catch (PersistenceException re) { + throw new SlingException("Failed to generate name", re); + } + } + + return path; + } + + /** + * Moves all repository content listed as repository move source in the + * request properties to the locations indicated by the resource properties. + * @param checkedOutNodes + */ + private void processMoves(final ResourceResolver resolver, + Map reqProperties, List changes, + VersioningConfiguration versioningConfiguration) + throws PersistenceException { + + for (RequestProperty property : reqProperties.values()) { + if (property.hasRepositoryMoveSource()) { + processMovesCopiesInternal(property, true, resolver, + reqProperties, changes, versioningConfiguration); + } + } + } + + /** + * Copies all repository content listed as repository copy source in the + * request properties to the locations indicated by the resource properties. + * @param checkedOutNodes + */ + private void processCopies(final ResourceResolver resolver, + Map reqProperties, List changes, + VersioningConfiguration versioningConfiguration) + throws PersistenceException { + + for (RequestProperty property : reqProperties.values()) { + if (property.hasRepositoryCopySource()) { + processMovesCopiesInternal(property, false, resolver, + reqProperties, changes, versioningConfiguration); + } + } + } + + /** + * Internal implementation of the + * {@link #processCopies(ResourceResolver, Map, HtmlResponse)} and + * {@link #processMoves(ResourceResolver, Map, HtmlResponse)} methods taking into + * account whether the source is actually a property or a node. + *

+ * Any intermediary nodes to the destination as indicated by the + * property path are created using the + * reqProperties as indications for required node types. + * + * @param property The {@link RequestProperty} identifying the source + * content of the operation. + * @param isMove true if the source item is to be moved. + * Otherwise the source item is just copied. + * @param resolver The resource resolver to use to access the content + * @param reqProperties All accepted request properties. This is used to + * create intermediary nodes along the property path. + * @param response The HtmlResponse into which successful + * copies and moves as well as intermediary node creations are + * recorded. + * @throws PersistenceException May be thrown if an error occurs. + */ + private void processMovesCopiesInternal( + RequestProperty property, + boolean isMove, final ResourceResolver resolver, + Map reqProperties, List changes, + VersioningConfiguration versioningConfiguration) + throws PersistenceException { + + String propPath = property.getPath(); + String source = property.getRepositorySource(); + + // only continue here, if the source really exists + if (resolver.getResource(source) != null ) { + + // if the destination item already exists, remove it + // first, otherwise ensure the parent location + if (resolver.getResource(propPath) != null) { + final Resource parent = resolver.getResource(propPath).getParent(); + this.jcrSsupport.checkoutIfNecessary(parent, changes, versioningConfiguration); + + resolver.delete(resolver.getResource(propPath)); + changes.add(Modification.onDeleted(propPath)); + } else { + Resource parent = deepGetOrCreateResource(resolver, property.getParentPath(), + reqProperties, changes, versioningConfiguration); + this.jcrSsupport.checkoutIfNecessary(parent, changes, versioningConfiguration); + } + + // move through the session and record operation + // check if the item is backed by JCR + Resource sourceRsrc = resolver.getResource(source); + final Object sourceItem = this.jcrSsupport.getItem(sourceRsrc); + final Object destItem = this.jcrSsupport.getItem(resolver.getResource(property.getParentPath())); + if ( sourceItem != null && destItem != null ) { + if ( this.jcrSsupport.isNode(sourceRsrc) ) { + if ( isMove ) { + this.jcrSsupport.checkoutIfNecessary(sourceRsrc.getParent(), changes, versioningConfiguration); + this.jcrSsupport.move(sourceItem, destItem, ResourceUtil.getName(propPath)); + } else { + this.jcrSsupport.checkoutIfNecessary(resolver.getResource(property.getParentPath()), changes, versioningConfiguration); + this.jcrSsupport.copy(sourceItem, destItem, property.getName()); + } + } else { + // property: move manually + this.jcrSsupport.checkoutIfNecessary(resolver.getResource(property.getParentPath()), changes, versioningConfiguration); + // create destination property + this.jcrSsupport.copy(sourceItem, destItem, ResourceUtil.getName(source)); + + // remove source property (if not just copying) + if ( isMove ) { + this.jcrSsupport.checkoutIfNecessary(sourceRsrc.getParent(), changes, versioningConfiguration); + resolver.delete(sourceRsrc); + } + } + } + + // make sure the property is not deleted even in case for a given + // property both @MoveFrom and @Delete is set + property.setDelete(false); + + // record successful move + if (isMove) { + changes.add(Modification.onMoved(source, propPath)); + } else { + changes.add(Modification.onCopied(source, propPath)); + } + } + } + + /** + * Removes all properties listed as {@link RequestProperty#isDelete()} from + * the resource. + * + * @param resolver The ResourceResolver used to access the + * resources to delete the properties. + * @param reqProperties The map of request properties to check for + * properties to be removed. + * @param response The HtmlResponse to be updated with + * information on deleted properties. + * @throws PersistenceException Is thrown if an error occurs checking or + * removing properties. + */ + private void processDeletes(final ResourceResolver resolver, + final Map reqProperties, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + + for (final RequestProperty property : reqProperties.values()) { + + if (property.isDelete()) { + final Resource parent = resolver.getResource(property.getParentPath()); + if ( parent == null ) { + continue; + } + this.jcrSsupport.checkoutIfNecessary(parent, changes, versioningConfiguration); + + final ValueMap vm = parent.adaptTo(ModifiableValueMap.class); + if ( vm == null ) { + throw new PersistenceException("Resource '" + parent.getPath() + "' is not modifiable."); + } + if ( vm.containsKey(property.getName()) ) { + if ( JcrConstants.JCR_MIXINTYPES.equals(property.getName()) ) { + vm.put(JcrConstants.JCR_MIXINTYPES, new String[0]); + } else { + vm.remove(property.getName()); + } + } else { + final Resource childRsrc = resolver.getResource(parent.getPath() + '/' + property.getName()); + if ( childRsrc != null ) { + resolver.delete(childRsrc); + } + } + + changes.add(Modification.onDeleted(property.getPath())); + } + } + + } + + /** + * Writes back the content + * + * @throws PersistenceException if a persistence error occurs + */ + private void writeContent(final ResourceResolver resolver, + final Map reqProperties, + final List changes, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + + final SlingPropertyValueHandler propHandler = new SlingPropertyValueHandler( + dateParser, this.jcrSsupport, changes); + + for (final RequestProperty prop : reqProperties.values()) { + if (prop.hasValues()) { + final Resource parent = deepGetOrCreateResource(resolver, + prop.getParentPath(), reqProperties, changes, versioningConfiguration); + + this.jcrSsupport.checkoutIfNecessary(parent, changes, versioningConfiguration); + + // skip jcr special properties + if (prop.getName().equals(JcrConstants.JCR_PRIMARYTYPE) + || prop.getName().equals(JcrConstants.JCR_MIXINTYPES)) { + continue; + } + + if (prop.isFileUpload()) { + uploadHandler.setFile(parent, prop, changes); + } else { + propHandler.setProperty(parent, prop); + } + } + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java new file mode 100644 index 000000000..ddde17d01 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.List; + +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.VersioningConfiguration; + +/** + * The MoveOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_MOVE move} + * operation for the Sling default POST servlet. + */ +public class MoveOperation extends AbstractCopyMoveOperation { + + @Override + protected String getOperationName() { + return "move"; + } + + @Override + protected Resource execute(final List changes, + final Resource source, + final String destParent, + String destName, + final VersioningConfiguration versioningConfiguration) + throws PersistenceException { + if (destName == null) { + destName = source.getName(); + } + + final Resource destParentRsrc = source.getResourceResolver().getResource(destParent); + final Resource dest = destParentRsrc.getChild(destName); + if (dest != null ) { + source.getResourceResolver().delete(dest); + } + + // ensure we have an item underlying the request's resource + final Object item = this.jcrSsupport.getItem(source); + final Object target = this.jcrSsupport.getNode(destParentRsrc); + + if (item == null || target == null ) { + move(source, destParentRsrc); + } else { + this.jcrSsupport.checkoutIfNecessary(source.getParent(), changes, versioningConfiguration); + this.jcrSsupport.move(item, target, destName); + } + final Resource result = destParentRsrc.getChild(destName); + if ( result != null ) { + changes.add(Modification.onMoved(source.getPath(), result.getPath())); + } + return result; + } + + /** + * Move the source as a child resource to the parent + */ + private void move(final Resource source, final Resource dest) + throws PersistenceException { + // first copy + final ValueMap vm = source.getValueMap(); + final Resource result = source.getResourceResolver().create(dest, source.getName(), vm); + for(final Resource c : source.getChildren()) { + move(c, result); + } + // then delete + source.getResourceResolver().delete(source); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/NopOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/NopOperation.java new file mode 100644 index 000000000..eceb8d081 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/NopOperation.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.servlets.post.PostOperation; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.SlingPostProcessor; + +/** + * The NopOperation class implements no operation at all. It just + * sets the response status according to the :nopstatus parameter if + * availables. Otherwise the status is set as 200/OK. + */ +public class NopOperation implements PostOperation { + + @Override + public void run(SlingHttpServletRequest request, PostResponse response, + SlingPostProcessor[] processors) { + + // get the :nopstatus parameter for a specific code + int status = SlingPostConstants.NOPSTATUS_VALUE_DEFAULT; + String nopStatusString = request.getParameter(SlingPostConstants.RP_NOP_STATUS); + if (nopStatusString != null) { + try { + int nopStatusPar = Integer.parseInt(nopStatusString); + if (nopStatusPar >= 100 && nopStatusPar <= 999) { + status = nopStatusPar; + } + } catch (NumberFormatException nfe) { + // illegal number, use default + } + } + + response.setStatus(status, "Null Operation Status: " + status); + } + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/RestoreOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/RestoreOperation.java new file mode 100644 index 000000000..40c728f22 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/RestoreOperation.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; + +/** + * The RestoreOperation class implements the + * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_RESTORE restore} + * operation for the Sling default POST servlet. + * The restore operation depends on the resources being backed up by a JCR node. + */ +public class RestoreOperation extends AbstractPostOperation { + + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) + throws PersistenceException { + try { + final String version = request.getParameter(SlingPostConstants.RP_VERSION); + if (version == null || version.length() == 0) { + throw new IllegalArgumentException("Unable to process restore. Missing version"); + } + final String removeString = request.getParameter(SlingPostConstants.RP_REMOVE_EXISTING); + final boolean removeExisting = Boolean.parseBoolean(removeString); + + Iterator res = getApplyToResources(request); + if (res == null) { + Resource resource = request.getResource(); + Node node = resource.adaptTo(Node.class); + if (node == null) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND, + "Missing source " + resource + " for restore"); + return; + } + restore(node, version, removeExisting); + changes.add(Modification.onRestore(resource.getPath(), version)); + } else { + while (res.hasNext()) { + Resource resource = res.next(); + Node node = resource.adaptTo(Node.class); + if (node != null) { + restore(node, version, removeExisting); + changes.add(Modification.onRestore(resource.getPath(), version)); + } + } + } + } catch ( final RepositoryException re) { + throw new PersistenceException(re.getMessage(), re); + } + } + + private void restore(Node node, String versionSpecifier, boolean removeExisting) + throws RepositoryException { + final VersionManager vm = node.getSession().getWorkspace().getVersionManager(); + final VersionHistory history = vm.getVersionHistory(node.getPath()); + final Version version; + if (history.hasVersionLabel(versionSpecifier)) { + version = history.getVersionByLabel(versionSpecifier); + } else if (history.hasNode(versionSpecifier)) { + version = history.getVersion(versionSpecifier); + } else { + throw new IllegalArgumentException("Unable to process restore. Invalid version: " + + versionSpecifier); + } + vm.restore(version, removeExisting); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/StreamedUploadOperation.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/StreamedUploadOperation.java new file mode 100644 index 000000000..d8753299f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/impl/operations/StreamedUploadOperation.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.operations; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.http.Part; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.util.Text; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.impl.helper.StreamedChunk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Performs a streamed modification of the content. + * Each File body encountered will result in a session save operation, to cause the underlying Resource implementation + * to stream content from the request to the target. + * + * This implements PostOperation but does not touch the normal Sling Request processing which is not streamed. + * + * The map of available fields is built up as the request is streamed. It is advisable to submit the request with all the form + * fields at the start of the request (normally based on DOM order) to ensure they are available before the streamed bodies are processed. + * + * The implementation does not implement the full Sling protocol aiming to keep it simple, and just deal with a streaming upload operation. + * The implementation binds to the Sling Resource API rather than JCR to keep it independent of the type of persistence. + */ +public class StreamedUploadOperation extends AbstractPostOperation { + private static final Logger LOG = LoggerFactory.getLogger(StreamedUploadOperation.class); + public static final String NT_FILE = "nt:file"; + private ServletContext servletContext; + + public void setServletContext(final ServletContext servletContext) { + this.servletContext = servletContext; + } + + + /** + * Check the request and return true if there is a parts iterator attribute present. This attribute + * will have been put there by the Sling Engine ParameterSupport class. If its not present, the request + * is not streamed and cant be processed by this class. Check this first before using this class. + * @param request the request. + * @return true if the request can be streamed. + */ + public boolean isRequestStreamed(SlingHttpServletRequest request) { + return request.getAttribute("request-parts-iterator") != null; + } + + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) + throws PersistenceException { + @SuppressWarnings("unchecked") + Iterator partsIterator = (Iterator) request.getAttribute("request-parts-iterator"); + Map> formFields = new HashMap<>(); + boolean streamingBodies = false; + while (partsIterator.hasNext()) { + Part part = partsIterator.next(); + String name = part.getName(); + + if (isFormField(part)) { + addField(formFields, name, part); + if (streamingBodies) { + LOG.warn("Form field {} was sent after the bodies started to be streamed. " + + "Will not have been available to all streamed bodies. " + + "It is recommended to send all form fields before streamed bodies in the POST ", name); + } + } else { + streamingBodies = true; + // process the file body and commit. + writeContent(request.getResourceResolver(), part, formFields, response, changes); + + } + } + } + + /** + * Add a field to the store of formFields. + * @param formFields the formFileds + * @param name the name of the field. + * @param part the part. + */ + private void addField(Map> formFields, String name, Part part) { + List values = formFields.get(name); + if ( values == null ) { + values = new ArrayList<>(); + formFields.put(name, values); + } + try { + values.add(IOUtils.toString(part.getInputStream(),"UTF-8")); + } catch (IOException e) { + LOG.error("Failed to read form field "+name,e); + } + } + + + /** + * Write content to the resource API creating a standard JCR structure of nt:file - nt:resource - jcr:data. + * This method will commit to the repository to force the repository to read from the input stream and write + * to the target. How efficient that is depends on the repository implementation. + * @param resolver the resource resolver. + * @param part the part containing the file body. + * @param formFields form fields collected so far. + * @param response the response object, updated by the operation. + * @param changes changes made to the repo. + * @throws PersistenceException + */ + private void writeContent(final ResourceResolver resolver, + final Part part, + final Map> formFields, + final PostResponse response, + final List changes) + throws PersistenceException { + + final String path = response.getPath(); + final Resource parentResource = resolver.getResource(path); + if ( !resourceExists(parentResource)) { + throw new IllegalArgumentException("Parent resource must already exist to be able to stream upload content. Please create first "); + } + String name = getUploadName(part); + Resource fileResource = parentResource.getChild(name); + Map fileProps = new HashMap<>(); + if (fileResource == null) { + fileProps.put("jcr:primaryType", NT_FILE); + fileResource = parentResource.getResourceResolver().create(parentResource, name, fileProps); + } + + + StreamedChunk chunk = new StreamedChunk(part, formFields, servletContext); + Resource result = chunk.store(fileResource, changes); + result.getResourceResolver().commit(); + + } + + /** + * Is the part a form field ? + * @param part + * @return + */ + private boolean isFormField(Part part) { + return (part.getSubmittedFileName() == null); + } + + /** + * Get the upload file name from the part. + * @param part + * @return + */ + private String getUploadName(Part part) { + // only return non null if the submitted file name is non null. + // the Sling API states that if the field name is '*' then the submitting file name is used, + // otherwise the field name is used. + String name = part.getName(); + String fileName = part.getSubmittedFileName(); + if ("*".equals(name)) { + name = fileName; + } + // strip of possible path (some browsers include the entire path) + name = name.substring(name.lastIndexOf('/') + 1); + name = name.substring(name.lastIndexOf('\\') + 1); + return Text.escapeIllegalJcrChars(name); + } + + /** + * Does the resource exist ? + * @param resource + * @return + */ + private boolean resourceExists(final Resource resource) { + return (resource != null && !ResourceUtil.isSyntheticResource(resource)); + } + + + + + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/package-info.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/package-info.java new file mode 100644 index 000000000..d6fab44f2 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/java/org/apache/sling/servlets/post/package-info.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +@Version("2.3.1") +package org.apache.sling.servlets.post; + +import org.osgi.annotation.versioning.Version; + diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/SLING-INF/nodetypes/chunk.cnd b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/SLING-INF/nodetypes/chunk.cnd new file mode 100644 index 000000000..5f5afab0a --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/SLING-INF/nodetypes/chunk.cnd @@ -0,0 +1,39 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + + + +//----------------------------------------------------------------------------- +// node type to store chunk +// offset: offset of chunk in file +// jcr:data: binary of chunk +[sling:chunk] > nt:hierarchyNode + primaryitem jcr:data + - sling:offset (long) mandatory + - jcr:data (binary) mandatory + + //----------------------------------------------------------------------------- + // Mixin type to identify that a node has chunks + // sling:fileLength : length of complete file + // sling:length: cumulative length of all uploaded chunks +[sling:chunks] + mixin + - sling:fileLength (long) + - sling:length (long) + + * (sling:chunk) multiple diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlNoGoBackResponse.html b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlNoGoBackResponse.html new file mode 100644 index 000000000..c498dc511 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlNoGoBackResponse.html @@ -0,0 +1,42 @@ + + + ${title} + + +

${title}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Status
${status.code}
Message
${status.message}
Location${location}
Parent Location${parentLocation}
Path
${path}
Referer
${referer}
ChangeLog
${changeLog}
+

Modified Resource

+

Parent of Modified Resource

+ + \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlResponse.html b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlResponse.html new file mode 100644 index 000000000..11c80ce5e --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/org/apache/sling/servlets/post/HtmlResponse.html @@ -0,0 +1,43 @@ + + + ${title} + + +

${title}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Status
${status.code}
Message
${status.message}
Location${location}
Parent Location${parentLocation}
Path
${path}
Referer${referer}
ChangeLog
${changeLog}
+

Go Back

+

Modified Resource

+

Parent of Modified Resource

+ + \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/system/sling.js b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/system/sling.js new file mode 100644 index 000000000..565e8ce32 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/main/resources/system/sling.js @@ -0,0 +1,444 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The sling javascript client gives access to a JCR repository + * from client-side java code, using the sling post servlet as a back-end. + * + * @version $Rev: $, $Date: 2007-03-27 16:30:52 +0200 (Tue, 27 Mar 2007) $ + */ + +var Sling = null; + +// start sling code scope +(function() { + + Sling = new Object(); + Sling.NAME_OF_THIS_FILE = "sling.js"; + Sling.PATH_OF_THIS_FILE = "/system/sling.js"; + + /** This method tries to figure out what to do with a page */ + Sling.wizard = function() { + //TODO add lots of magic here + var form=document.getElementById("slingform"); + if (!form) form=document.forms[0]; + if (form) { + var sp=new Object(); + sp.formElement=form; + Sling.setupPage(sp); + } + + } + /** Call this to merge sling data in an HTML page + TODO deprecate other functions + */ + Sling.setupPage = function(options) { + var tree = Sling.getContent(Sling._getJsonUrl(),1); + + if(options.formElement) { + Sling._setFormValues(options.formElement,Sling._getJsonUrl(),tree); + } + + if(options.displayElement) { + Sling.displayValues(options.displayElement,tree); + } + } + + /** + * HTTP GET XHR Helper + * @param {String} url The URL + * @param {Function} optional second parameter for async version of the method. + * The callback will get the XHR object, method returns immediately + * @return the XHR object, use .responseText for the data + * @type String + */ + Sling.httpGet = function(url, callback) { + var httpcon = Sling.getXHR(); + if (httpcon) { + if(callback) { + httpcon.onload = function() { callback(this); }; + httpcon.open('GET', url); + httpcon.send(null); + } else { + httpcon.open('GET', url, false); + httpcon.send(null); + return httpcon; + } + } else { + return null; + } + } + /** + * Produces a "sort-of-json" string representation of a object + * for debugging purposes only + * @param {Object} obj The object + * @param {int} level The indentation level + * @return The result + * @type String + */ + Sling.dumpObj = function(obj, level) { + var res = ""; + for (var a in obj) { + if (typeof(obj[a])!="object") { + res+=a+":"+obj[a]+" "; + } else { + res+=a+": { "; + res+=Sling.dumpObj(obj[a])+"} "; + } + } + return (res); + } + + /** Produces an aggregate of get all the property names used + * in a tree as a helper for table oriented display + * @param {Object} obj The Content Tree object + * @param {Object} names internal object used for collecting all + * the names during the recursion + * @return An Array of names of properties that exist in a tree + * @type Array + */ + Sling.getAllPropNames = function(obj, names) { + var root = false; + if (!names) { + names=new Object(); + root=true; + } + for (var a in obj) { + if (typeof(obj[a])!="object") { + names[a]="1"; + } else { + getAllPropNames(obj[a], names); + } + } + if (root) { + var ar=new Array(); + var i=0; + for (var a in ar) { + ar[i]=a; + i++; + } + names=ar; + } + return (names); + } + + /** Reads a tree of items given a maxlevel from the repository as JSON + * @param {String} path Path into the current workspace + * @param {int} maxlevel maximum depth to traverse to + * @param {Array} filters filter only these properties + * @return An Object tree of content nodes and properties, null if not found + * @type Object + */ + Sling.getContent = function(path, maxlevels, filter) { + var obj=new Object(); + if (!path) { + path=Sling.currentPath; + } + if (path.indexOf("/")==0) { + /* + this assumes that paths that start with a slash + are meant to be workspace paths rather than URLs + and therefore need some additions before they are sent + to the server + */ + if(maxlevels == "0" || maxlevels) { + maxlevels = "." + maxlevels; + } else { + maxlevels = ""; + } + path=Sling.baseurl + path + maxlevels + ".json"; + } + //checking for a trailing "/*" + if (path.indexOf("/*")>=0) return obj; + + // TODO for now we explicitely defeat caching on this...there must be a better way + // but in tests IE6 tends to cache too much + var passThroughCacheParam = "?clock=" + new Date().getTime(); + var res=Sling.httpGet(path + passThroughCacheParam + (maxlevels?"&maxlevels="+maxlevels:"")); + + if(res.status == 200) { + var obj=Sling.evalString(res.responseText); + if (!filter) { + for (var a in obj) { + if (a.indexOf("jcr:")==0) delete(obj[a]); + } + } + return obj; + } + return null; + } + + /** Remove content by path */ + Sling.removeContent = function(path) { + var httpcon = Sling.getXHR(); + if (httpcon) { + var params = ":operation=delete"; + httpcon.open('POST', Sling.baseurl + path, false); + + // Send the proper header information along with the request + httpcon.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + httpcon.setRequestHeader("Content-length", params.length); + httpcon.setRequestHeader("Connection", "close"); + httpcon.send(params); + return httpcon; + } else { + return false; + } + } + + /** eval str, accepting various object delimiters */ + Sling.evalString = function(str) { + return JSON.parse(str); + } + + /** Get "session info" from repository. Mainly answers the question: "Who am I" + * and "Which workspace am I logged into. + * @return An Object tree containing the session information, null if server status <> 200 + * @type Object + */ + Sling.getSessionInfo = function() { + var res=Sling.httpGet(Sling.baseurl+"/system/sling/info.sessionInfo.json"); + if(res.status == 200) { + return Sling.evalString(res.responseText); + } + return null; + } + + /** Get "session info" from repository. Mainly answers the question: "Who am I" + * and "Which workspace am I logged into. Async version of getSessionInfo + * @param {Function} callback function, will get an An Object tree as first argument + * containing the session information, null if server status <> 200 + */ + Sling.getSessionInfoAsync = function(callback) { + var res = Sling.httpGet(Sling.baseurl+"/system/sling/info.sessionInfo.json", + function(res) { + if(res.status == 200) { + var info = Sling.evalString(res.responseText); + callback(info); + } else { + callback(null); + } + }); + } + + /** Replace extension in a path */ + Sling._replaceExtension = function(path,newExtension) { + var i = path.lastIndexOf("."); + if(i >= 0) path = path.substring(0,i); + i = path.lastIndexOf("."); + if(i >= 0) path = path.substring(0,i); + return path + newExtension; + } + + /** Get the JSON data URL that for the current page + * (assuming a .extension for the current page, .html or something else) + */ + Sling._getJsonUrl = function() { + return Sling._replaceExtension(window.location.href,".json"); + } + + /** Get the content repository path from the URL + * (assuming a .extension for the current page, .html or something else) + */ + Sling._getPath = function() { + + var noextensions=Sling._replaceExtension(window.location.href,""); + var path=noextensions.substring(Sling.baseurl.length); + return (path); + } + + /** Display values inside a container: an element inside given container, + * with an id like ./stuff, has its innerHTML set to the value of stuff + * in the tree, if it exists. + */ + Sling.displayValues = function(container,tree) { + if(!tree) { + tree = Sling.getContent(Sling._getJsonUrl(),1); + } + + var elements = container.getElementsByTagName("*"); + var toSet = new Array(); + for (var i = 0; i < elements.length; i++) { + var value = Sling._getElementValue(elements[i],tree); + if(value) { + toSet[toSet.length] = { e:elements[i], v:value }; + } + } + + for(var i = 0; i < toSet.length; i++) { + toSet[i].e.innerHTML = toSet[i].v; + } + } + + /** If e has an ID that matches a property of tree, set e's innerHTML accordingly */ + Sling._getElementValue = function(e,tree) { + var id = e.getAttribute("id"); + if(id) { + return tree[id.substring(2)]; + } + } + + + /** Set form elements based on the tree of items passed into the method + * @param {IdOrElement} form the Form element to set, or its id + * @param {String} path passes a string specifying the path + * @param {Object} tree optionally pass the content that you want the + * form to be populated with. This assumes an item tree as returned by + * getContent(). + * Returns an object indicating whether data was found on the server. + * + */ + Sling._setFormValues = function(form, path, tree) { + var result = new Object(); + + /** TODO: deal with abolute paths? + * TODO: deal with @ValueFrom + */ + if (!path) return; + + form.setAttribute("action", path); + + if (!tree) { + tree=Sling.getContent(path,1); + } + + var elems=form.elements; + var i=0; + formfieldprefix=""; + + while (elems.length > i) { + var elem=elems[i]; + var a=elem.name; + if (a.indexOf("./")==0) { + formfieldprefix="./"; + break; + } + i++; + } + + var i=0; + while (elems.length > i) { + var elem=elems[i]; + var a=elem.name; + + if (a.indexOf("/")==0) { + var nodepath=a.substring(0,a.lastIndexOf("/")); + var propname=a.substring(a.lastIndexOf("/")+1); + var node=Sling.getContent(nodepath); + var propval=node[propname]; + } else if (a.indexOf(formfieldprefix)==0) { + var propname=a.substring(formfieldprefix.length); + var propval=tree[propname]; + } + + if (propval) { + if (elem.type == "file") { + // cannot edit uplodaded files for now + } else if (elem.type == "checkbox") { + var vals; + if (typeof(propval)=="object") vals=propval; + else { + vals=new Array(); + vals[0]=propval; + } + var j=0; + while (vals.length > j) { + if (vals[j] == elem.value) elem.checked=true; + j++; + } + } else { + elem.value=propval; + } + } + i++; + } + + } + + /** return Path as specified as the URL Parameter + * @param URL + * @return The Path parameter isolated from the URL + * @type String + */ + Sling.TODO_NOT_USED_isolatePathFromUrl = function(url) { + var pattern = "[\\?&]Path=([^&#]*)"; + var regex = new RegExp( pattern ); + var results = regex.exec( url ); + if( results == null ) + // none found + return ""; + else + // found + return unescape(results[1]); + } + + /** + * Get an XMLHttpRequest in a portable way + * + */ + Sling.getXHR = function () { + var xhr=null; + + if(!xhr) { + try { + // built-in (firefox, recent Opera versions, etc) + xhr=new XMLHttpRequest(); + } catch (e) { + // ignore + } + } + + if(!xhr) { + try { + // IE, newer versions + xhr=new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + // ignore + } + } + + if(!xhr) { + try { + // IE, older versions + xhr=new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) { + // ignore + } + } + + if(!xhr) { + alert("Unable to access XMLHttpRequest object, sling will not work!"); + } + + return xhr; + } + + // obtain the base_url to communicate with sling on the server + var scripts = document.getElementsByTagName("SCRIPT") + for (var i = 0; i < scripts.length; i++) { + var scriptSrc = scripts[i].src + if (scriptSrc.match(Sling.PATH_OF_THIS_FILE+"$")) { + Sling.baseurl = scriptSrc.substring(0,scriptSrc.length - Sling.PATH_OF_THIS_FILE.length); + Sling.currentPath = Sling._getPath(); + Sling.isNew = (Sling.currentPath.indexOf("/*")>=0)?true:false; + + break; + } + } + +// end sling code scope +})(); diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/AbstractPostOperationTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/AbstractPostOperationTest.java new file mode 100644 index 000000000..832839ac5 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/AbstractPostOperationTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.commons.testing.sling.MockResourceResolver; +import org.apache.sling.servlets.post.impl.helper.MockSlingHttpServlet3Request; +import org.junit.Test; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.List; + +import static org.junit.Assert.*; + +public class AbstractPostOperationTest { + + @Test + public void testRemainingPostfixCausesFailure() { + TestingResourceResolver resourceResolver = new TestingResourceResolver(); + + MockSlingHttpServlet3Request request = new MockSlingHttpServlet3Request("/test", null, null, null, null); + request.setResourceResolver(resourceResolver); + + final PostOperation operation = new AbstractPostOperation() { + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) throws RepositoryException { + changes.add(Modification.onChange(ModificationType.CREATE, "/content/test")); + changes.add(Modification.onChange(ModificationType.CREATE, "/content/test@Postfix")); + } + }; + + HtmlResponse response = new HtmlResponse(); + operation.run(request, response, new SlingPostProcessor[0]); + assertFalse(response.isSuccessful()); + assertFalse(resourceResolver.commitCalled); + assertTrue(resourceResolver.revertCalled); + } + + @Test + public void testNoRemainingPostfixIsSuccessful() { + TestingResourceResolver resourceResolver = new TestingResourceResolver(); + + MockSlingHttpServlet3Request request = new MockSlingHttpServlet3Request("/test", null, null, null, null); + request.setResourceResolver(resourceResolver); + + final PostOperation operation = new AbstractPostOperation() { + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) throws RepositoryException { + changes.add(Modification.onChange(ModificationType.CREATE, "/content/test")); + } + }; + + HtmlResponse response = new HtmlResponse(); + operation.run(request, response, new SlingPostProcessor[0]); + assertTrue(response.isSuccessful()); + assertTrue(resourceResolver.commitCalled); + assertFalse(resourceResolver.revertCalled); + } + + @Test + public void testRemainingPostfixWithoutUnPostfixedIsSuccessful() { + TestingResourceResolver resourceResolver = new TestingResourceResolver(); + + MockSlingHttpServlet3Request request = new MockSlingHttpServlet3Request("/test", null, null, null, null); + request.setResourceResolver(resourceResolver); + + final PostOperation operation = new AbstractPostOperation() { + @Override + protected void doRun(SlingHttpServletRequest request, PostResponse response, List changes) throws RepositoryException { + changes.add(Modification.onChange(ModificationType.CREATE, "/content/test@Postfix")); + } + }; + + HtmlResponse response = new HtmlResponse(); + operation.run(request, response, new SlingPostProcessor[0]); + assertTrue(response.isSuccessful()); + assertTrue(resourceResolver.commitCalled); + assertFalse(resourceResolver.revertCalled); + } + + private class TestingResourceResolver extends MockResourceResolver { + private boolean revertCalled; + private boolean commitCalled; + + @Override + public AdapterType adaptTo(Class type) { + if (type == Session.class) { + return null; + } else { + return super.adaptTo(type); + } + } + + @Override + public boolean hasChanges() { + return !commitCalled; + } + + @Override + public void commit() { + commitCalled = true; + } + + @Override + public void revert() { + revertCalled = true; + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/HtmlResponseTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/HtmlResponseTest.java new file mode 100644 index 000000000..c9528343a --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/HtmlResponseTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post; + +import org.apache.sling.commons.testing.sling.MockSlingHttpServletResponse; +import org.junit.Before; +import org.junit.Test; + +import javax.json.JsonObject; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.junit.Assert.*; + +public class HtmlResponseTest { + protected HtmlResponse res; + + @Before + public void setUp() throws Exception { + res = new HtmlResponse(); + res.setReferer(""); + } + + @Test + public void testNoChangesOnError() throws Exception { + res.onChange("modified", "argument1"); + res.setError(new Exception("some exception")); + final StringWriter stringWriter = new StringWriter(); + MockSlingHttpServletResponse response = new MockSlingHttpServletResponse() { + @Override + public PrintWriter getWriter() throws IOException { + return new PrintWriter(stringWriter); + } + }; + res.doSend(response); + String output = stringWriter.toString(); + assertTrue(output.contains("
")); + } + + @Test + public void testChangesOnNoError() throws Exception { + res.onChange("modified", "argument1"); + final StringWriter stringWriter = new StringWriter(); + MockSlingHttpServletResponse response = new MockSlingHttpServletResponse() { + @Override + public PrintWriter getWriter() throws IOException { + return new PrintWriter(stringWriter); + } + }; + res.doSend(response); + String output = stringWriter.toString(); + assertTrue(output.contains("
<pre>modified("argument1");<br/></pre>
")); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/JsonResponseTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/JsonResponseTest.java new file mode 100644 index 000000000..c2b18ee6c --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/JsonResponseTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post; + +import java.io.IOException; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.apache.sling.commons.testing.sling.MockSlingHttpServletResponse; + +public class JsonResponseTest extends TestCase { + protected JSONResponse res; + + public void setUp() throws Exception { + res = new JSONResponse(); + super.setUp(); + } + + public void testOnChange() throws Exception { + res.onChange("modified", "argument1", "argument2"); + Object prop = res.getProperty("changes"); + JsonArray changes = assertInstanceOf(prop, JsonArray.class); + assertEquals(1, changes.size()); + Object obj = changes.getJsonObject(0); + JsonObject change = assertInstanceOf(obj, JsonObject.class); + assertProperty(change, JSONResponse.PROP_TYPE, "modified"); + JsonArray arguments = change.getJsonArray(JSONResponse.PROP_ARGUMENT); + assertEquals(2, arguments.size()); + } + + public void testSetProperty() throws Exception { + res.setProperty("prop", "value"); + assertProperty(res.getJson(), "prop", "value"); + } + + @SuppressWarnings({"ThrowableInstanceNeverThrown"}) + public void testSetError() throws IOException { + String errMsg = "Dummy error"; + res.setError(new Error(errMsg)); + MockSlingHttpServletResponse resp = new MockSlingHttpServletResponse(); + res.send(resp, true); + JsonObject json = res.getJson(); + JsonValue error = assertProperty(json, "error"); + assertProperty((JsonObject) error, "class", Error.class.getName()); + assertProperty((JsonObject) error, "message", errMsg); + } + + public void testSend() throws Exception { + res.onChange("modified", "argument1"); + MockSlingHttpServletResponse response = new MockSlingHttpServletResponse(); + res.send(response, true); + JsonObject result = Json.createReader(new StringReader(response.getOutput().toString())).readObject(); + assertProperty(result, HtmlResponse.PN_STATUS_CODE, HttpServletResponse.SC_OK); + assertEquals(JSONResponse.RESPONSE_CONTENT_TYPE, response.getContentType()); + assertEquals(JSONResponse.RESPONSE_CHARSET, response.getCharacterEncoding()); + } + + public void testSend_201() throws Exception { + final String location = "http://example.com/test_location"; + res.onChange("modified", "argument1"); + res.setStatus(HttpServletResponse.SC_CREATED, "Created"); + res.setLocation(location); + MockResponseWithHeader response = new MockResponseWithHeader(); + res.send(response, true); + JsonObject result = Json.createReader(new StringReader(response.getOutput().toString())).readObject(); + assertProperty(result, HtmlResponse.PN_STATUS_CODE, HttpServletResponse.SC_CREATED); + assertEquals(location, response.getHeader("Location")); + } + + public void testSend_3xx() throws Exception { + final String location = "http://example.com/test_location"; + res.onChange("modified", "argument1"); + + for (int status = 300; status < 308; status++) { + res.setStatus(status, "3xx Status"); + res.setLocation(location); + MockResponseWithHeader response = new MockResponseWithHeader(); + res.send(response, true); + JsonObject result = Json.createReader(new StringReader(response.getOutput().toString())).readObject(); + assertProperty(result, HtmlResponse.PN_STATUS_CODE, status); + assertEquals(location, response.getHeader("Location")); + } + } + + public void testNoChangesOnError() throws Exception { + res.onChange("modified", "argument1"); + res.setError(new Exception("some exception")); + JsonObject obj = res.getJson(); + assertTrue(obj.containsKey("changes")); + assertEquals(0, obj.getJsonArray("changes").size()); + } + + public void testSendWithJsonAsPropertyValue() throws Exception { + String testResponseJson = "{\"user\":\"testUser\",\"properties\":{\"id\":\"testId\", \"name\":\"test\"}}"; + JsonObject customProperty = Json.createReader(new StringReader(testResponseJson)).readObject(); + res.setProperty("response", customProperty); + MockResponseWithHeader response = new MockResponseWithHeader(); + res.send(response, true); + JsonObject result = Json.createReader(new StringReader(response.getOutput().toString())).readObject(); + assertProperty(result, "response", customProperty); + } + + private static JsonValue assertProperty(JsonObject obj, String key) { + assertTrue("JSON object does not have property " + key, obj.containsKey(key)); + return obj.get(key); + } + + @SuppressWarnings({"unchecked"}) + private static JsonValue assertProperty(JsonObject obj, String key, int expected) { + JsonNumber res = (JsonNumber) assertProperty(obj, key); + assertEquals(expected, res.intValue()); + return res; + } + + private static JsonValue assertProperty(JsonObject obj, String key, String expected) { + JsonString res = (JsonString) assertProperty(obj, key); + assertEquals(expected, res.getString()); + return res; + } + + private static JsonValue assertProperty(JsonObject obj, String key, JsonObject expected) { + JsonObject res = (JsonObject) assertProperty(obj, key); + assertEquals(expected, res); + return res; + } + + @SuppressWarnings({"unchecked"}) + private static T assertInstanceOf(Object obj, Class clazz) { + try { + return (T) obj; + } catch (ClassCastException e) { + TestCase.fail("Object is of unexpected type. Expected: " + clazz.getName() + ", actual: " + obj.getClass().getName()); + return null; + } + } + + private static class MockResponseWithHeader extends MockSlingHttpServletResponse { + private final Map headers = new HashMap(); + + @Override + public void setHeader(String name, String value) { + this.headers.put(name, value); + } + + public String getHeader(String name) { + Object result = this.headers.get(name); + if (result instanceof String) { + return (String) result; + } else if (result instanceof String[]) { + return ((String[]) result)[0]; + } else { + return null; + } + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java new file mode 100644 index 000000000..8039ca49b --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl; + +import junit.framework.TestCase; + +import org.apache.sling.servlets.post.impl.helper.NodeNameFilter; + +public class NodeNameFilterTest extends TestCase { + private final NodeNameFilter filter = new NodeNameFilter(); + + protected void runTest(String [] data) { + for(int i=0; i < data.length; i++) { + final String input = data[i]; + i++; + final String expected = data[i]; + final String actual = filter.filter(input); + assertEquals(expected, actual); + } + } + + public void testBasicFiltering() { + final String [] data = { + "test", "test", + "t?st", "t_st", + "t??st", "t_st" + }; + + runTest(data); + } + + public void testNoInitialNumber() { + final String [] data = { + "1234", "_1234", + "1", "_1" + }; + + runTest(data); + } +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/RequestPropertyTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/RequestPropertyTest.java new file mode 100644 index 000000000..ade82b39f --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/RequestPropertyTest.java @@ -0,0 +1,288 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sling.servlets.post.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import junitx.util.PrivateAccessor; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.request.RequestParameterMap; +import org.apache.sling.servlets.post.HtmlResponse; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.impl.helper.RequestProperty; +import org.apache.sling.servlets.post.impl.operations.ModifyOperation; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.integration.junit4.JMock; +import org.jmock.integration.junit4.JUnit4Mockery; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * This is primary a series of tests of the hasValues(), providesValues(), and getStringValues() methods of + * RequestProperty. It uses the collectContent() method of ModifyOperation to make the test cases more readable. + */ +@RunWith(JMock.class) +public class RequestPropertyTest { + + private Mockery context = new JUnit4Mockery(); + + @Test + public void testSingleValue() throws Throwable { + Map props = collectContent(p("./param", "true")); + + assertEquals(1, props.size()); + assertTrue(props.get("/test/path/param").hasValues()); + assertTrue(props.get("/test/path/param").providesValue()); + assertEquals(1, props.get("/test/path/param").getStringValues().length); + assertEquals("true", props.get("/test/path/param").getStringValues()[0]); + } + + @Test + public void testSingleValueWithBlank() throws Throwable { + Map props = collectContent(p("./param", "")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertFalse(prop.providesValue()); + assertEquals(1, prop.getStringValues().length); + assertEquals("", prop.getStringValues()[0]); + } + + @Test + public void testNullSingleValueWithDefaultToIgnore() throws Throwable { + Map props = collectContent(p("./param@DefaultValue", ":ignore")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertFalse(prop.hasValues()); + } + + @Test + public void testSingleValueWithDefaultToIgnore() throws Throwable { + Map props = collectContent(p("./param", ""), p("./param@DefaultValue", ":ignore")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertFalse(prop.providesValue()); + assertEquals(0, prop.getStringValues().length); + } + + @Test + public void testSingleValueWithDefaultToNull() throws Throwable { + Map props = collectContent(p("./param", ""), p("./param@DefaultValue", ":null")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertTrue(prop.providesValue()); + assertNull(prop.getStringValues()); + } + + @Test + public void testSingleValueIgnoringBlanks() throws Throwable { + Map props = collectContent(p("./param", ""), p("./param@IgnoreBlanks", "true")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertFalse(prop.hasValues()); + } + + @Test + public void testMultiValue() throws Throwable { + Map props = collectContent(p("./param", "true", "false")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertTrue(prop.providesValue()); + assertEquals(2, prop.getStringValues().length); + assertEquals("true", prop.getStringValues()[0]); + assertEquals("false", prop.getStringValues()[1]); + } + + @Test + public void testMultiValueWithBlank() throws Throwable { + Map props = collectContent(p("./param", "true", "")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertTrue(prop.providesValue()); + assertEquals(2, prop.getStringValues().length); + assertEquals("true", prop.getStringValues()[0]); + assertEquals("", prop.getStringValues()[1]); + } + + @Test + public void testMultiValueWithBlanks() throws Throwable { + Map props = collectContent(p("./param", "true", "", "")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertTrue(prop.providesValue()); + assertEquals(3, prop.getStringValues().length); + assertEquals("true", prop.getStringValues()[0]); + assertEquals("", prop.getStringValues()[1]); + assertEquals("", prop.getStringValues()[2]); + } + + @Test + public void testMultiValueWithAllBlanks() throws Throwable { + Map props = collectContent(p("./param", "", "", "")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertFalse(prop.providesValue()); + assertEquals(3, prop.getStringValues().length); + assertEquals("", prop.getStringValues()[0]); + assertEquals("", prop.getStringValues()[1]); + assertEquals("", prop.getStringValues()[2]); + } + + @Test + public void testMultiValueWithBlankIgnoringBlanks() throws Throwable { + Map props = collectContent(p("./param", "true", "")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertTrue(prop.providesValue()); + assertEquals(2, prop.getStringValues().length); + assertEquals("true", prop.getStringValues()[0]); + assertEquals("", prop.getStringValues()[1]); + } + + @Test + public void testMultiValueWithBlanksIgnoringBlanks() throws Throwable { + Map props = collectContent(p("./param", "true", "", ""), p("./param@IgnoreBlanks", "true")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertTrue(prop.hasValues()); + assertTrue(prop.providesValue()); + assertEquals(1, prop.getStringValues().length); + assertEquals("true", prop.getStringValues()[0]); + } + + @Test + public void testMultiValueWithAllBlanksIgnoringBlanks() throws Throwable { + Map props = collectContent(p("./param", "", "", ""), p("./param@IgnoreBlanks", "true")); + + assertEquals(1, props.size()); + RequestProperty prop = props.get("/test/path/param"); + assertFalse(prop.hasValues()); + } + + private static final Class[] COLLECT_CLASSES = new Class[] { SlingHttpServletRequest.class, PostResponse.class }; + + private class Param { + String key; + String[] value; + } + + private Param p(String key, String... value) { + Param kv = new Param(); + kv.key = key; + kv.value = value; + return kv; + } + + @SuppressWarnings("unchecked") + private Map collectContent(Param... kvs) throws Throwable { + final List> params = new ArrayList>(); + for (int i = 0; i < kvs.length; i++) { + final Param kv = kvs[i]; + final RequestParameter[] param = new RequestParameter[kv.value.length]; + for (int j = 0; j < kv.value.length; j++) { + final String strValue = kv.value[j]; + final RequestParameter aparam = context.mock(RequestParameter.class, "requestParameter" + i + "#" + j); + context.checking(new Expectations() { + { + allowing(aparam).getString(); + will(returnValue(strValue)); + } + }); + param[j] = aparam; + } + final Map.Entry entry = context.mock(Map.Entry.class, "entry" + i); + context.checking(new Expectations() { + { + allowing(entry).getKey(); + will(returnValue(kv.key)); + allowing(entry).getValue(); + will(returnValue(param)); + + } + }); + params.add(entry); + } + + final Set set = context.mock(Set.class); + context.checking(new Expectations() { + { + one(set).iterator(); + will(returnValue(params.iterator())); + } + }); + + final RequestParameterMap map = context.mock(RequestParameterMap.class); + context.checking(new Expectations() { + { + one(map).entrySet(); + will(returnValue(set)); + + } + }); + + final SlingHttpServletRequest request = context.mock(SlingHttpServletRequest.class); + context.checking(new Expectations() { + { + Vector names = new Vector(); + names.add("./param"); + + one(request).getParameterNames(); + will(returnValue(names.elements())); + one(request).getRequestParameterMap(); + will(returnValue(map)); + + } + }); + final HtmlResponse response = new HtmlResponse(); + response.setPath("/test/path"); + + Map props = (Map) PrivateAccessor.invoke( + new ModifyOperation(), "collectContent", COLLECT_CLASSES, + new Object[] { request, response }); + return props; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java new file mode 100644 index 000000000..4609052a9 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.StringTokenizer; + +import junit.framework.TestCase; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.commons.testing.sling.MockSlingHttpServletRequest; +import org.apache.sling.servlets.post.HtmlResponse; +import org.apache.sling.servlets.post.JSONResponse; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.SlingPostConstants; +import org.apache.sling.servlets.post.impl.helper.MediaRangeList; +import org.apache.sling.servlets.post.impl.helper.MockSlingHttpServlet3Request; +import org.apache.sling.servlets.post.impl.helper.MockSlingHttpServlet3Response; + +public class SlingPostServletTest extends TestCase { + + private SlingPostServlet servlet; + + @Override + protected void setUp() throws Exception { + super.setUp(); + servlet = new SlingPostServlet(); + } + + public void testIsSetStatus() { + StatusParamSlingHttpServletRequest req = new StatusParamSlingHttpServletRequest(); + + // 1. null parameter, expect true + req.setStatusParam(null); + assertTrue("Standard status expected for null param", + servlet.isSetStatus(req)); + + // 2. "standard" parameter, expect true + req.setStatusParam(SlingPostConstants.STATUS_VALUE_STANDARD); + assertTrue("Standard status expected for '" + + SlingPostConstants.STATUS_VALUE_STANDARD + "' param", + servlet.isSetStatus(req)); + + // 3. "browser" parameter, expect false + req.setStatusParam(SlingPostConstants.STATUS_VALUE_BROWSER); + assertFalse("Browser status expected for '" + + SlingPostConstants.STATUS_VALUE_BROWSER + "' param", + servlet.isSetStatus(req)); + + // 4. any parameter, expect true + String param = "knocking on heaven's door"; + req.setStatusParam(param); + assertTrue("Standard status expected for '" + param + "' param", + servlet.isSetStatus(req)); + } + + public void testGetJsonResponse() { + MockSlingHttpServletRequest req = new MockSlingHttpServlet3Request(null, null, null, null, null) { + @Override + public String getHeader(String name) { + return name.equals(MediaRangeList.HEADER_ACCEPT) ? "application/json" : super.getHeader(name); + } + + public AdapterType adaptTo(Class type) { + return null; + } + }; + PostResponse result = servlet.createPostResponse(req); + assertTrue(result instanceof JSONResponse); + } + + public void testRedirection() throws Exception { + String utf8Path = "\u0414\u0440\u0443\u0433\u0430"; + String encodedUtf8 = "%D0%94%D1%80%D1%83%D0%B3%D0%B0"; + testRedirection("/", "/fred", "*.html", "/fred.html"); + testRedirection("/xyz/", "/xyz/"+utf8Path, "*", "/xyz/"+encodedUtf8); + testRedirection("/", "/fred/"+utf8Path, "/xyz/*", "/xyz/"+encodedUtf8); + testRedirection("/", "/fred/"+utf8Path, null, null); + // test redirect with host information + testRedirection("/", "/fred/abc", "http://forced", null); + testRedirection("/", "/fred/abc", "//forced.com/test", null); + testRedirection("/", "/fred/abc", "https://forced.com/test", null); + // invalid URI + testRedirection("/", "/fred/abc", "file://c:\\Users\\workspace\\test.java", null); + } + + private void testRedirection(String requestPath, String resourcePath, String redirect, String expected) + throws Exception { + RedirectServletResponse resp = new RedirectServletResponse(); + SlingHttpServletRequest request = new RedirectServletRequest(redirect, requestPath); + PostResponse htmlResponse = new HtmlResponse(); + htmlResponse.setPath(resourcePath); + assertEquals(expected != null, servlet.redirectIfNeeded(request, htmlResponse, resp)); + assertEquals(expected, resp.redirectLocation); + } + + /** + * + */ + private final class RedirectServletRequest extends MockSlingHttpServlet3Request { + + private String requestPath; + private String redirect; + + private RedirectServletRequest(String redirect, String requestPath) { + super(null, null, null, null, null); + this.requestPath = requestPath; + this.redirect = redirect; + } + + public String getPathInfo() { + return requestPath; + } + + @Override + public String getParameter(String name) { + return SlingPostConstants.RP_REDIRECT_TO.equals(name) ? redirect : null; + } + } + + private final class RedirectServletResponse extends MockSlingHttpServlet3Response { + + private String redirectLocation; + + @Override + public String encodeRedirectURL(String s) { + StringTokenizer st = new StringTokenizer(s, "/", true); + StringBuilder sb = new StringBuilder(); + try { + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if ("/".equals(token)) { + sb.append(token); + } else { + sb.append(URLEncoder.encode(token, "UTF-8")); + } + } + } catch (UnsupportedEncodingException e) { + fail("Should have UTF-8?? " + e); + return null; + } + return sb.toString(); + } + + @Override + public void sendRedirect(String s) throws IOException { + redirectLocation = s; + } + } + + private static class StatusParamSlingHttpServletRequest extends + MockSlingHttpServlet3Request { + + private String statusParam; + + public StatusParamSlingHttpServletRequest() { + // nothing to setup, we don't care + super(null, null, null, null, null); + } + + @Override + public String getParameter(String name) { + if (SlingPostConstants.RP_STATUS.equals(name)) { + return statusParam; + } + + return super.getParameter(name); + } + + void setStatusParam(String statusParam) { + this.statusParam = statusParam; + } + + public AdapterType adaptTo(Class type) { + return null; + } + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxyTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxyTest.java new file mode 100644 index 000000000..2249e1816 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxyTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.servlets.post.PostResponse; +import org.junit.Test; + +public class HtmlResponseProxyTest { + + @Test + public void testConstructor() { + new HtmlResponseProxy(POST_RESPONSE); + } + + private static final PostResponse POST_RESPONSE = new PostResponse() { + + public void setTitle(String title) { + } + + public void setStatus(int code, String message) { + } + + public void setReferer(String referer) { + } + + public void setPath(String path) { + } + + public void setParentLocation(String parentLocation) { + } + + public void setLocation(String location) { + } + + public void setError(Throwable error) { + } + + public void setCreateRequest(boolean isCreateRequest) { + } + + public void send(HttpServletResponse response, boolean setStatus) throws IOException { + } + + public void onMoved(String srcPath, String dstPath) { + } + + public void onModified(String path) { + } + + public void onDeleted(String path) { + } + + public void onCreated(String path) { + } + + public void onCopied(String srcPath, String dstPath) { + } + + public void onChange(String type, String... arguments) { + } + + public boolean isSuccessful() { + return false; + } + + public boolean isCreateRequest() { + return false; + } + + public String getStatusMessage() { + return null; + } + + public int getStatusCode() { + return 0; + } + + public String getReferer() { + return null; + } + + public String getPath() { + return null; + } + + public String getParentLocation() { + return null; + } + + public String getLocation() { + return null; + } + + public Throwable getError() { + return null; + } + }; +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MediaRangeListTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MediaRangeListTest.java new file mode 100644 index 000000000..0bc917708 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MediaRangeListTest.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import junit.framework.TestCase; +import org.apache.sling.commons.testing.sling.MockSlingHttpServletRequest; + +import java.util.Enumeration; + +public class MediaRangeListTest extends TestCase { + protected MediaRangeList rangeList; + + public void setUp() throws Exception { + super.setUp(); + rangeList = new MediaRangeList("text/*;q=0.3, text/html;q=0.7, text/html;level=1,\n" + + " text/html;level=2;q=0.4, */*;q=0.5"); + } + + public void testContains() throws Exception { + assertTrue(rangeList.contains("text/html")); + assertTrue(rangeList.contains("application/json")); // Since rangeList contains */* + assertTrue(rangeList.contains("text/plain")); + } + + public void testPrefer() throws Exception { + assertEquals("text/html;level=1", rangeList.prefer("text/html;level=1", "*/*")); + } + + public void testPreferJson() { + MediaRangeList rangeList = new MediaRangeList("text/html;q=0.8, application/json"); + assertEquals("application/json", rangeList.prefer("text/html", "application/json")); + } + + public void testHttpEquivParam() { + MockSlingHttpServletRequest req = new MockSlingHttpServlet3Request(null, null, null, null, null) { + @Override + public String getHeader(String name) { + return name.equals(MediaRangeList.HEADER_ACCEPT) ? "text/plain" : super.getHeader(name); + } + + @Override + public String getParameter(String name) { + return name.equals(MediaRangeList.PARAM_ACCEPT) ? "text/html" : super.getParameter(name); + } + + @Override + public Enumeration getHeaderNames() { + return null; + } + + public AdapterType adaptTo(Class type) { + return null; + } + }; + MediaRangeList rangeList = new MediaRangeList(req); + assertTrue("Did not contain media type from query param", rangeList.contains("text/html")); + assertFalse("Contained media type from overridden Accept header", rangeList.contains("text/plain")); + } + + public void testInvalidJdkAcceptHeader() { + //This header is sent by Java client which make use of URLConnection on Oracle JDK + //See acceptHeader at http://hg.openjdk.java.net/jdk6/jdk6-gate/jdk/file/tip/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java + //To support such case the MediaRange parser has to be made bit linient + final String invalidHeader = "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"; + MockSlingHttpServletRequest req = new MockSlingHttpServlet3Request(null, null, null, null, null) { + @Override + public String getHeader(String name) { + return name.equals(MediaRangeList.HEADER_ACCEPT) ? invalidHeader : super.getHeader(name); + } + + public AdapterType adaptTo(Class type) { + return null; + } + }; + MediaRangeList rangeList = new MediaRangeList(req); + assertTrue("Did not contain media type from query param", rangeList.contains("text/html")); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Request.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Request.java new file mode 100644 index 000000000..5fa0ba2b2 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Request.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import org.apache.sling.commons.testing.sling.MockSlingHttpServletRequest; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; +import java.io.IOException; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +/** + * When MockSlingHttpServletRequest supports Servlet 3 correctly, delete this class. + */ +public class MockSlingHttpServlet3Request extends MockSlingHttpServletRequest { + public MockSlingHttpServlet3Request(String o, String o1, String o2, String o3, String o4) { + super(o,o1,o2,o3,o4); + } + + @Override + public Enumeration getLocales() { + return (Enumeration) super.getLocales(); + } + + @Override + public Enumeration getHeaders(String name) { + return (Enumeration) super.getHeaders(name); + } + + @Override + public Enumeration getHeaderNames() { + return (Enumeration) super.getHeaderNames(); + } + + @Override + public Enumeration getAttributeNames() { + return (Enumeration) super.getAttributeNames(); + } + + @Override + public Map getParameterMap() { + return ( Map ) super.getParameterMap(); + } + + @Override + public Enumeration getParameterNames() { + return (Enumeration) super.getParameterNames(); + } + + @Override + public Enumeration getResponseContentTypes() { + return (Enumeration) super.getResponseContentTypes(); + } + + @Override + public String changeSessionId() { + return null; + } + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException { + return false; + } + + @Override + public void login(String s, String s1) throws ServletException { + + } + + @Override + public void logout() throws ServletException { + + } + + @Override + public Collection getParts() throws IOException, ServletException { + return null; + } + + @Override + public Part getPart(String s) throws IOException, ServletException { + return null; + } + + @Override + public T upgrade(Class aClass) throws IOException, ServletException { + return null; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Response.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Response.java new file mode 100644 index 000000000..8bc8b1aee --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/MockSlingHttpServlet3Response.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import org.apache.sling.commons.testing.sling.MockSlingHttpServletResponse; + +import java.util.Collection; + +/** + * When MockSlingHttpServletResponse supports Servlet 3 correctly, delete this class. + */ +public class MockSlingHttpServlet3Response extends MockSlingHttpServletResponse { + @Override + public int getStatus() { + return 0; + } + + @Override + public String getHeader(String s) { + return null; + } + + @Override + public Collection getHeaders(String s) { + return null; + } + + @Override + public Collection getHeaderNames() { + return null; + } + + @Override + public void setContentLengthLong(long l) { + + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStreamTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStreamTest.java new file mode 100644 index 000000000..e91a02047 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/ResourceIteratorInputStreamTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.commons.testing.sling.MockResource; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by ieb on 06/09/2016. + */ +public class ResourceIteratorInputStreamTest { + + @Test + public void test() throws IOException { + List resources = new ArrayList(); + for (int i = 0; i < 10; i++ ) { + final int initialState = i; + final InputStream in = new InputStream() { + private int state = initialState; + @Override + public int read() throws IOException { + return state--; + } + }; + resources.add(new MockResource(null,null,null){ + @Override + public AdapterType adaptTo(Class type) { + if (InputStream.class.equals(type)) { + return (AdapterType) in; + } + return super.adaptTo(type); + } + + @Override + public String getName() { + return "chunk-"+(initialState*100)+"-"+(((initialState+1)*100)-1); + } + }); + } + ResourceIteratorInputStream resourceIteratorInputStream = new ResourceIteratorInputStream(resources.iterator()); + int expected = 0; + int cycle = 0; + for(int i = resourceIteratorInputStream.read(); i >= 0; i = resourceIteratorInputStream.read()) { + Assert.assertEquals(expected, i); + if ( expected == 0 ) { + cycle++; + expected=cycle; + } else { + expected--; + } + } + Assert.assertEquals(10,cycle); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerAutotypeTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerAutotypeTest.java new file mode 100644 index 000000000..ec8a14fc6 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerAutotypeTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** Verify the AutoType values of property names for which + * we automatically set values. + */ +public class SlingPropertyValueHandlerAutotypeTest { + + private void assertAlias(String propertyNameA) { + final String propertyNameB = "jcr:" + propertyNameA; + + assertEquals("Expecting same AutotType for " + propertyNameA + " and " + propertyNameB, + SlingPropertyValueHandler.getAutoType(propertyNameA), + SlingPropertyValueHandler.getAutoType(propertyNameB) + ); + } + @Test + public void checkPropertyAliases() { + assertAlias("created"); + assertAlias("createdBy"); + assertAlias("lastModified"); + assertAlias("lastModifiedBy"); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerTest.java new file mode 100644 index 000000000..0e819c896 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandlerTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.wrappers.ModifiableValueMapDecorator; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.ModificationType; +import org.junit.Test; +import org.mockito.Mockito; + +public class SlingPropertyValueHandlerTest { + + @Test + public void testEmptyPropertyValueWithTypeLong() throws Exception { + final List mods = new ArrayList(); + final JCRSupport support = new JCRSupport(); + + final SlingPropertyValueHandler handler = new SlingPropertyValueHandler(new DateParser(), support, mods); + + final Session jcrSession = Mockito.mock(Session.class); + + final ResourceResolver resolver = Mockito.mock(ResourceResolver.class); + Mockito.when(resolver.adaptTo(Session.class)).thenReturn(jcrSession); + + final Node node = Mockito.mock(Node.class); + final Property jcrProp = Mockito.mock(Property.class); + Mockito.when(node.getProperty("property")).thenReturn(jcrProp); + final PropertyDefinition jcrPropDef = Mockito.mock(PropertyDefinition.class); + Mockito.when(jcrProp.getDefinition()).thenReturn(jcrPropDef); + Mockito.when(jcrPropDef.isMandatory()).thenReturn(false); + // throw exception for previous behaviour + Mockito.when(node.setProperty("property", "", 3)).thenThrow(new RepositoryException()); + + final Resource rsrc = Mockito.mock(Resource.class); + final ModifiableValueMap valueMap = new ModifiableValueMapDecorator(new HashMap()); + valueMap.put("property", 7L); + + Mockito.when(rsrc.getPath()).thenReturn("/content"); + Mockito.when(rsrc.getName()).thenReturn("content"); + Mockito.when(rsrc.adaptTo(Node.class)).thenReturn(node); + Mockito.when(rsrc.adaptTo(ModifiableValueMap.class)).thenReturn(valueMap); + Mockito.when(rsrc.getResourceResolver()).thenReturn(resolver); + + final RequestParameter req = Mockito.mock(RequestParameter.class); + Mockito.when(req.isFormField()).thenReturn(true); + Mockito.when(req.getName()).thenReturn("property"); + Mockito.when(req.getString()).thenReturn(""); + + final RequestProperty prop = new RequestProperty("/content/property"); + prop.setTypeHintValue("Long"); + prop.setValues(new RequestParameter[] { req }); + + handler.setProperty(rsrc, prop); + + // value map should be empty, one change: delete + assertTrue(valueMap.isEmpty()); + assertEquals(1, mods.size()); + assertEquals(ModificationType.DELETE, mods.get(0).getType()); + assertEquals("/content/property", mods.get(0).getSource()); + } + + @Test + public void testEmptyPropertyValueWithoutType() throws Exception { + final List mods = new ArrayList(); + final JCRSupport support = new JCRSupport(); + + final SlingPropertyValueHandler handler = new SlingPropertyValueHandler(new DateParser(), support, mods); + + final Session jcrSession = Mockito.mock(Session.class); + + final ResourceResolver resolver = Mockito.mock(ResourceResolver.class); + Mockito.when(resolver.adaptTo(Session.class)).thenReturn(jcrSession); + + final Node node = Mockito.mock(Node.class); + final Property jcrProp = Mockito.mock(Property.class); + Mockito.when(node.getProperty("property")).thenReturn(jcrProp); + final PropertyDefinition jcrPropDef = Mockito.mock(PropertyDefinition.class); + Mockito.when(jcrProp.getDefinition()).thenReturn(jcrPropDef); + Mockito.when(jcrPropDef.isMandatory()).thenReturn(false); + // throw exception for previous behaviour + Mockito.when(node.setProperty("property", "", 3)).thenThrow(new RepositoryException()); + + final Resource rsrc = Mockito.mock(Resource.class); + final ModifiableValueMap valueMap = new ModifiableValueMapDecorator(new HashMap()); + valueMap.put("property", "hello"); + + Mockito.when(rsrc.getPath()).thenReturn("/content"); + Mockito.when(rsrc.getName()).thenReturn("content"); + Mockito.when(rsrc.adaptTo(Node.class)).thenReturn(node); + Mockito.when(rsrc.adaptTo(ModifiableValueMap.class)).thenReturn(valueMap); + Mockito.when(rsrc.getResourceResolver()).thenReturn(resolver); + + final RequestParameter req = Mockito.mock(RequestParameter.class); + Mockito.when(req.isFormField()).thenReturn(true); + Mockito.when(req.getName()).thenReturn("property"); + Mockito.when(req.getString()).thenReturn(""); + + final RequestProperty prop = new RequestProperty("/content/property"); + prop.setValues(new RequestParameter[] { req }); + + handler.setProperty(rsrc, prop); + + // value map should be empty, one change: delete + assertTrue(valueMap.isEmpty()); + assertEquals(1, mods.size()); + assertEquals(ModificationType.DELETE, mods.get(0).getType()); + assertEquals("/content/property", mods.get(0).getSource()); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/StreamedChunkTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/StreamedChunkTest.java new file mode 100644 index 000000000..414704c93 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/helper/StreamedChunkTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + + +import org.junit.Assert; +import org.junit.Test; + +/** + * Created by ieb on 06/09/2016. + */ +public class StreamedChunkTest { + + @Test + public void testContentRange() { + checkRange("bytes 0-1234/1235", 0, 1235, 1235); + checkRange("bytes 10-123/1235", 10,123-10+1,1235); + checkRange("bytes 12-123/*", 12, 123 - 12+1, -1); + checkInvalidRange("byte 10-123/1234"); // byte is not valid. + checkInvalidRange("bytes 1000-123/1234"); // offset before end + checkInvalidRange("bytes 1000-12300/1234"); // end before length + checkInvalidRange("bytes 1000-12300/big"); // big not valid + checkInvalidRange("bytes 1000-12300/"); // no length + checkInvalidRange("bytes 1000-12300"); // no length + } + + private void checkInvalidRange(String rangeHeader) { + try { + StreamedChunk.ContentRange cr = new StreamedChunk.ContentRange(rangeHeader); + Assert.fail("Should have rejected "+rangeHeader); + } catch (IllegalArgumentException e) { + // ok + } + } + + private void checkRange(String rangeHeader, long offset, long range, long length) { + StreamedChunk.ContentRange cr = new StreamedChunk.ContentRange(rangeHeader); + Assert.assertEquals(offset,cr.offset); + Assert.assertEquals(range,cr.range); + Assert.assertEquals(length,cr.length); + + } + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperationTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperationTest.java new file mode 100644 index 000000000..af81486f1 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/AbstractCreateOperationTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.regex.Pattern; + +import junit.framework.TestCase; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; + +public class AbstractCreateOperationTest extends TestCase { + + private AbstractCreateOperation op = new AbstractCreateOperation() { + + @Override + protected void doRun(SlingHttpServletRequest request, + PostResponse response, List changes) { + // none here + } + }; + + public void test_ignoreParameter() throws Exception { + Method ip = getMethod("ignoreParameter", String.class); + + // default setup without matching regexp + assertEquals(true, ip.invoke(op, "_charset_")); + assertEquals(true, ip.invoke(op, ":operation")); + assertEquals(false, ip.invoke(op, "j_username")); + assertEquals(false, ip.invoke(op, "j_password")); + assertEquals(false, ip.invoke(op, "some_random_j_name")); + + // setup: j_.* + op.setIgnoredParameterNamePattern(Pattern.compile("j_.*")); + assertEquals(true, ip.invoke(op, "_charset_")); + assertEquals(true, ip.invoke(op, ":operation")); + assertEquals(true, ip.invoke(op, "j_username")); + assertEquals(true, ip.invoke(op, "j_password")); + assertEquals(false, ip.invoke(op, "some_random_j_name")); + + // setup: .*j_.* + op.setIgnoredParameterNamePattern(Pattern.compile(".*j_.*")); + assertEquals(true, ip.invoke(op, "_charset_")); + assertEquals(true, ip.invoke(op, ":operation")); + assertEquals(true, ip.invoke(op, "j_username")); + assertEquals(true, ip.invoke(op, "j_password")); + assertEquals(true, ip.invoke(op, "some_random_j_name")); + + // setup: .+j_.* + op.setIgnoredParameterNamePattern(Pattern.compile(".+j_.*")); + assertEquals(true, ip.invoke(op, "_charset_")); + assertEquals(true, ip.invoke(op, ":operation")); + assertEquals(false, ip.invoke(op, "j_username")); + assertEquals(false, ip.invoke(op, "j_password")); + assertEquals(true, ip.invoke(op, "some_random_j_name")); + } + + private Method getMethod(String name, Class... parameterTypes) { + try { + Method m = AbstractCreateOperation.class.getDeclaredMethod(name, + parameterTypes); + m.setAccessible(true); + return m; + } catch (Throwable t) { + fail(t.toString()); + return null; // compiler wants this + } + } + +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/CopyOperationTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/CopyOperationTest.java new file mode 100644 index 000000000..dca13aa09 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/CopyOperationTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.sling.servlets.post.impl.helper.JCRSupportImpl; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.integration.junit4.JMock; +import org.jmock.integration.junit4.JUnit4Mockery; +import org.junit.Test; +import org.junit.runner.RunWith; + +import junit.framework.TestCase; + +@RunWith(JMock.class) +public class CopyOperationTest extends TestCase { + private Mockery context = new JUnit4Mockery(); + private int counter; + + private void assertResult(final String srcPath, final String destPath, Boolean expectedResult) throws RepositoryException { + counter++; + final Node src = context.mock(Node.class, "src" + counter); + final Node dest = context.mock(Node.class, "dest" + counter); + + context.checking(new Expectations() { + { + allowing(src).getPath(); + will(returnValue(srcPath)); + allowing(dest).getPath(); + will(returnValue(destPath)); + } + }); + + final boolean result = JCRSupportImpl.isAncestorOrSameNode(src, dest); + assertEquals( + "Expecting isAncestorOrSameNode to be " + expectedResult + " for " + srcPath + " and " + destPath, + expectedResult.booleanValue(), result); + } + + @Test + public void testIsAncestorOrSameNode() throws RepositoryException { + final Object [] testCases = { + "/", "/", true, + "/a", "/a", true, + "/a/bee/ceee", "/a/bee/ceee", true, + "/", "/tmp", true, + "/a", "/a/b", true, + "/a", "/a/b/c/dee/eeee", true, + "/a", "/ab", false, + "/ab/cd", "/ab/cde", false, + "/ab", "/cd", false, + }; + + for(int i=0; i < testCases.length; i+=3) { + assertResult((String)testCases[i], (String)testCases[i+1], (Boolean)(testCases[i+2])); + } + + } +} \ No newline at end of file diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockPart.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockPart.java new file mode 100644 index 000000000..a48d753d1 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockPart.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sling.servlets.post.impl.operations; + +import javax.servlet.http.Part; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Map; + +public class MockPart implements Part { + + private Map headers; + private long size; + private String submittedFileName; + private String name; + private String contentType; + private InputStream inputStream; + + public MockPart(String name, String contentType, String submittedFileName, long size, InputStream inputStream, Map headers) { + this.name = name; + this.contentType = contentType; + this.submittedFileName = submittedFileName; + this.size = size; + this.inputStream = inputStream; + this.headers = headers; + } + + @Override + public InputStream getInputStream() throws IOException { + + return inputStream; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getSubmittedFileName() { + return submittedFileName; + } + + @Override + public long getSize() { + return size; + } + + @Override + public void write(String s) throws IOException { + throw new UnsupportedOperationException("Writing a part to disk is not supported."); + + } + + @Override + public void delete() throws IOException { + + } + + @Override + public String getHeader(String s) { + return (String) headers.get(s); + } + + @Override + public Collection getHeaders(String s) { + return (Collection) headers.get(s); + } + + @Override + public Collection getHeaderNames() { + return headers.keySet(); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockRealResource.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockRealResource.java new file mode 100644 index 000000000..2653260e1 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockRealResource.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sling.servlets.post.impl.operations; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Iterator; +import java.util.Map; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceMetadata; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ValueMap; + +/** + * MockResource is synthetic, which doesnt work here. + */ +public class MockRealResource implements Resource { + private final ResourceResolver resourceResolver; + private final String path; + private final String resourceType; + private final String name; + private final String parentPath; + private MockValueMap properties = new MockValueMap(); + + public MockRealResource(ResourceResolver resourceResolver, String path, String resourceType) { + this.resourceResolver = resourceResolver; + this.path = path; + this.resourceType = resourceType; + this.name = path.substring(path.lastIndexOf('/')); + this.parentPath = path.substring(0,path.lastIndexOf('/')); + } + + public MockRealResource(ResourceResolver resourceResolver, String path, String resourceType, Map properties) { + this.resourceResolver = resourceResolver; + this.path = path; + this.name = path.substring(path.lastIndexOf('/')+1); + this.parentPath = path.substring(0,path.lastIndexOf('/')); + this.resourceType = resourceType; + this.properties.putAll(properties); + } + + + @Override + public String getPath() { + return path; + } + + @Override + public String getName() { + return name; + } + + @Override + public Resource getParent() { + return resourceResolver.getResource(parentPath); + } + + @Override + public Iterator listChildren() { + return resourceResolver.listChildren(this); + } + + @Override public Iterable getChildren() { + return resourceResolver.getChildren(this); + } + + @Override + public Resource getChild(String s) { + return resourceResolver.getResource(path +"/"+ s); + } + + @Override + public String getResourceType() { + return resourceType; + } + + @Override + public String getResourceSuperType() { + return null; + } + + @Override + public boolean isResourceType(String s) { + return s.equals(resourceType); + } + + @Override + public ResourceMetadata getResourceMetadata() { + return null; + } + + @Override + public ResourceResolver getResourceResolver() { + return resourceResolver; + } + + @Override + public AdapterType adaptTo(Class aClass) { + if (ValueMap.class.isAssignableFrom(aClass)) { + return (AdapterType) properties; + } + if (InputStream.class.isAssignableFrom(aClass) && properties.containsKey("jcr:data")) { + Object o = properties.get("jcr:data"); + if (o instanceof InputStream) { + return (AdapterType) o; + } else { + try { + return (AdapterType) new ByteArrayInputStream(String.valueOf(properties.get("jcr:data")).getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Cant convert UTF-8 to byte[]"); + } + } + } + return null; + } + + @Override + public boolean hasChildren() { + return resourceResolver.listChildren(this).hasNext(); + } + + @Override + public ValueMap getValueMap() { + return adaptTo(ValueMap.class); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockValueMap.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockValueMap.java new file mode 100644 index 000000000..f9246e6f9 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/MockValueMap.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.operations; + +import org.apache.sling.api.resource.ModifiableValueMap; + +import java.util.HashMap; + +public class MockValueMap extends HashMap implements ModifiableValueMap { + public MockValueMap() { + } + + @Override + public T get(String s, Class aClass) { + return (T) super.get(s); + } + + @Override + public T get(String s, T t) { + return (T) super.get(s); + } +} diff --git a/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/StreamingUploadOperationTest.java b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/StreamingUploadOperationTest.java new file mode 100644 index 000000000..064bdf261 --- /dev/null +++ b/Java-base/sling-org-apache-sling-servlets-post/src/src/test/java/org/apache/sling/servlets/post/impl/operations/StreamingUploadOperationTest.java @@ -0,0 +1,681 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sling.servlets.post.impl.operations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; + +import org.apache.commons.io.IOUtils; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.PersistenceException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.commons.testing.sling.MockResourceResolver; +import org.apache.sling.servlets.post.AbstractPostResponse; +import org.apache.sling.servlets.post.Modification; +import org.apache.sling.servlets.post.PostResponse; +import org.apache.sling.servlets.post.impl.helper.MockSlingHttpServlet3Request; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class StreamingUploadOperationTest { + + private static final Logger LOG = LoggerFactory.getLogger(StreamingUploadOperationTest.class); + private StreamedUploadOperation streamedUplodOperation; + + @Before + public void before() { + streamedUplodOperation = new StreamedUploadOperation(); + + } + + @After + public void after() { + + } + + @Test + public void test() throws PersistenceException, RepositoryException, UnsupportedEncodingException { + List changes = new ArrayList<>(); + PostResponse response = new AbstractPostResponse() { + @Override + protected void doSend(HttpServletResponse response) throws IOException { + + } + + @Override + public void onChange(String type, String... arguments) { + + } + + @Override + public String getPath() { + return "/test/upload/location"; + } + }; + + List partsList = new ArrayList<>(); + partsList.add(new MockPart("formfield1", null, null, 0, new ByteArrayInputStream("testformfield1".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("formfield2", null, null, 0, new ByteArrayInputStream("testformfield2".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("test1.txt", "text/plain", "test1bad.txt", 4, new ByteArrayInputStream("test".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("*", "text/plain2", "test2.txt", 8, new ByteArrayInputStream("test1234".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("badformfield2", null, null, 0, new ByteArrayInputStream("testbadformfield2".getBytes("UTF-8")), Collections.EMPTY_MAP)); + final Iterator partsIterator = partsList.iterator(); + final Map repository = new HashMap<>(); + final ResourceResolver resourceResolver = new MockResourceResolver() { + @Override + public Resource getResource(String path) { + + Resource resource = repository.get(path); + + if ( resource == null ) { + if ( "/test/upload/location".equals(path)) { + resource = new MockRealResource(this, path, "sling:Folder"); + repository.put(path,resource); + LOG.debug("Created {} ", path); + + } + } + LOG.debug("Resource {} is {} {}", path, resource, ResourceUtil.isSyntheticResource(resource)); + return resource; + } + + + + + @Override + public Iterable getChildren(Resource resource) { + return null; + } + + @Override + public void delete(Resource resource) throws PersistenceException { + + } + + @Override + public Resource create(Resource resource, String s, Map map) throws PersistenceException { + Resource childResource = resource.getChild(s); + if ( childResource != null) { + throw new IllegalArgumentException("Child "+s+" already exists "); + } + Resource newResource = new MockRealResource(this, resource.getPath()+"/"+s, (String)map.get("sling:resourceType"), map); + repository.put(newResource.getPath(), newResource); + return newResource; + } + + @Override + public void revert() { + + } + + @Override + public void commit() throws PersistenceException { + LOG.debug("Committing"); + for(Map.Entry e : repository.entrySet()) { + LOG.debug("Committing {} ", e.getKey()); + Resource r = e.getValue(); + ModifiableValueMap vm = r.adaptTo(ModifiableValueMap.class); + for (Map.Entry me : vm.entrySet()) { + if (me.getValue() instanceof InputStream) { + try { + String value = IOUtils.toString((InputStream) me.getValue()); + LOG.debug("Converted {} {} ", me.getKey(), value); + vm.put(me.getKey(), value); + + } catch (IOException e1) { + throw new PersistenceException("Failed to commit input stream", e1); + } + } + } + LOG.debug("Converted {} ", vm); + } + LOG.debug("Committted {} ", repository); + + + } + + @Override + public boolean hasChanges() { + return false; + } + }; + + SlingHttpServletRequest request = new MockSlingHttpServlet3Request(null, null, null, null, null) { + @Override + public Object getAttribute(String name) { + if ( "request-parts-iterator".equals(name)) { + return partsIterator; + } + return super.getAttribute(name); + } + + @Override + public ResourceResolver getResourceResolver() { + return resourceResolver; + } + }; + streamedUplodOperation.doRun(request, response, changes); + + + { + Resource r = repository.get("/test/upload/location/test1.txt"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:file", m.get("jcr:primaryType")); + + } + { + Resource r = repository.get("/test/upload/location/test1.txt/jcr:content"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + Assert.assertEquals("nt:resource", m.get("jcr:primaryType")); + Assert.assertTrue(m.get("jcr:lastModified") instanceof Calendar); + Assert.assertEquals("text/plain", m.get("jcr:mimeType")); + Assert.assertEquals("test", m.get("jcr:data")); + + } + { + Resource r = repository.get("/test/upload/location/test2.txt"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:file", m.get("jcr:primaryType")); + + } + { + Resource r = repository.get("/test/upload/location/test2.txt/jcr:content"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:resource", m.get("jcr:primaryType")); + Assert.assertTrue(m.get("jcr:lastModified") instanceof Calendar); + Assert.assertEquals("text/plain2", m.get("jcr:mimeType")); + Assert.assertEquals("test1234", m.get("jcr:data")); + } + + + } + + @Test + public void testParts() throws PersistenceException, RepositoryException, UnsupportedEncodingException { + List changes = new ArrayList<>(); + PostResponse response = new AbstractPostResponse() { + @Override + protected void doSend(HttpServletResponse response) throws IOException { + + } + + @Override + public void onChange(String type, String... arguments) { + + } + + @Override + public String getPath() { + return "/test/upload/location"; + } + }; + + List partsList = new ArrayList<>(); + partsList.add(new MockPart("test1.txt@Length", null, null, 0, new ByteArrayInputStream("8".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("test1.txt@Offset", null, null, 0, new ByteArrayInputStream("0".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart( + "test1.txt", + "text/plain", + "test1bad.txt", + 4, + new ByteArrayInputStream("test".getBytes("UTF-8")), + mapOf("Content-Length", "4"))); + partsList.add(new MockPart("test1.txt@Offset", null, null, 0, new ByteArrayInputStream("4".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart( + "test1.txt", + "text/plain", + "test1bad.txt", + 4, + new ByteArrayInputStream("part".getBytes("UTF-8")), + mapOf("Content-Length", "4"))); + partsList.add(new MockPart("*", "text/plain2", "test2.txt", 8, new ByteArrayInputStream("test1234".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("badformfield2", null, null, 0, new ByteArrayInputStream("testbadformfield2".getBytes("UTF-8")), Collections.EMPTY_MAP)); + final Iterator partsIterator = partsList.iterator(); + final Map repository = new HashMap<>(); + final ResourceResolver resourceResolver = new MockResourceResolver() { + @Override + public Resource getResource(String path) { + + Resource resource = repository.get(path); + + if ( resource == null ) { + if ( "/test/upload/location".equals(path)) { + resource = new MockRealResource(this, path, "sling:Folder"); + repository.put(path,resource); + LOG.debug("Created {} ", path); + + } + } + LOG.debug("Resource {} is {} {}", path, resource, ResourceUtil.isSyntheticResource(resource)); + return resource; + } + + + + + @Override + public Iterable getChildren(Resource resource) { + + List children = new ArrayList<>(); + for(Map.Entry e : repository.entrySet()) { + if (isChild(resource.getPath(), e.getKey())) { + children.add(e.getValue()); + } + } + return children; + } + + private boolean isChild(String path, String key) { + if ( key.length() > path.length() && key.startsWith(path)) { + return !key.substring(path.length()+1).contains("/"); + } + return false; + } + + @Override + public Iterator listChildren(Resource parent) { + return getChildren(parent).iterator(); + } + + @Override + public void delete(Resource resource) throws PersistenceException { + + } + + @Override + public Resource create(Resource resource, String s, Map map) throws PersistenceException { + Resource childResource = resource.getChild(s); + if ( childResource != null) { + throw new IllegalArgumentException("Child "+s+" already exists "); + } + String resourceType = (String)map.get("sling:resourceType"); + if ( resourceType == null) { + resourceType = (String)map.get("jcr:primaryType"); + } + if ( resourceType == null) { + LOG.warn("Resource type null for {} {} ", resource, resource.getPath()+"/"+s); + } + Resource newResource = new MockRealResource(this, resource.getPath()+"/"+s, resourceType, map); + repository.put(newResource.getPath(), newResource); + LOG.debug("Created Resource {} ", newResource.getPath()); + return newResource; + } + + @Override + public void revert() { + + } + + @Override + public void commit() throws PersistenceException { + LOG.debug("Committing"); + for(Map.Entry e : repository.entrySet()) { + LOG.debug("Committing {} ", e.getKey()); + Resource r = e.getValue(); + ModifiableValueMap vm = r.adaptTo(ModifiableValueMap.class); + for (Map.Entry me : vm.entrySet()) { + if (me.getValue() instanceof InputStream) { + try { + String value = IOUtils.toString((InputStream) me.getValue()); + LOG.debug("Converted {} {} ", me.getKey(), value); + vm.put(me.getKey(), value); + + } catch (IOException e1) { + throw new PersistenceException("Failed to commit input stream", e1); + } + } + } + LOG.debug("Converted {} ", vm); + } + LOG.debug("Comittted {} ", repository); + + + } + + @Override + public boolean hasChanges() { + return false; + } + }; + + SlingHttpServletRequest request = new MockSlingHttpServlet3Request(null, null, null, null, null) { + @Override + public Object getAttribute(String name) { + if ( "request-parts-iterator".equals(name)) { + return partsIterator; + } + return super.getAttribute(name); + } + + @Override + public ResourceResolver getResourceResolver() { + return resourceResolver; + } + }; + streamedUplodOperation.doRun(request, response, changes); + + + { + Resource r = repository.get("/test/upload/location/test1.txt"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:file", m.get("jcr:primaryType")); + + } + { + Resource r = repository.get("/test/upload/location/test1.txt/jcr:content"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + Assert.assertEquals("nt:resource", m.get("jcr:primaryType")); + Assert.assertTrue(m.get("jcr:lastModified") instanceof Calendar); + Assert.assertEquals("text/plain", m.get("jcr:mimeType")); + Assert.assertEquals("testpart", m.get("jcr:data")); + + } + { + Resource r = repository.get("/test/upload/location/test2.txt"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:file", m.get("jcr:primaryType")); + + } + { + Resource r = repository.get("/test/upload/location/test2.txt/jcr:content"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:resource", m.get("jcr:primaryType")); + Assert.assertTrue(m.get("jcr:lastModified") instanceof Calendar); + Assert.assertEquals("text/plain2", m.get("jcr:mimeType")); + Assert.assertEquals("test1234", m.get("jcr:data")); + } + + + } + + @Test + public void testPartsContentRange() throws PersistenceException, RepositoryException, UnsupportedEncodingException { + List changes = new ArrayList<>(); + PostResponse response = new AbstractPostResponse() { + @Override + protected void doSend(HttpServletResponse response) throws IOException { + + } + + @Override + public void onChange(String type, String... arguments) { + + } + + @Override + public String getPath() { + return "/test/upload/location"; + } + }; + + List partsList = new ArrayList<>(); + partsList.add(new MockPart("formfield1", null, null, 0, new ByteArrayInputStream("testformfield1".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("formfield2", null, null, 0, new ByteArrayInputStream("testformfield2".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart( + "test1.txt", + "text/plain", + "test1bad.txt", + 4, + new ByteArrayInputStream("test".getBytes("UTF-8")), + mapOf("Content-Range","bytes 0-3/8", "Content-Length", "4"))); + partsList.add(new MockPart( + "test1.txt", + "text/plain", + "test1bad.txt", + 4, + new ByteArrayInputStream("part".getBytes("UTF-8")), + mapOf("Content-Range","bytes 4-7/8", "Content-Length", "4"))); + partsList.add(new MockPart("*", "text/plain2", "test2.txt", 8, new ByteArrayInputStream("test1234".getBytes("UTF-8")), Collections.EMPTY_MAP)); + partsList.add(new MockPart("badformfield2", null, null, 0, new ByteArrayInputStream("testbadformfield2".getBytes("UTF-8")), Collections.EMPTY_MAP)); + final Iterator partsIterator = partsList.iterator(); + final Map repository = new HashMap<>(); + final ResourceResolver resourceResolver = new MockResourceResolver() { + @Override + public Resource getResource(String path) { + + Resource resource = repository.get(path); + + if ( resource == null ) { + if ( "/test/upload/location".equals(path)) { + resource = new MockRealResource(this, path, "sling:Folder"); + repository.put(path,resource); + LOG.debug("Created {} ", path); + + } + } + LOG.debug("Resource {} is {} {}", path, resource, ResourceUtil.isSyntheticResource(resource)); + return resource; + } + + + + + @Override + public Iterable getChildren(Resource resource) { + + List children = new ArrayList<>(); + for(Map.Entry e : repository.entrySet()) { + if (isChild(resource.getPath(), e.getKey())) { + children.add(e.getValue()); + } + } + return children; + } + + private boolean isChild(String path, String key) { + if ( key.length() > path.length() && key.startsWith(path)) { + return !key.substring(path.length()+1).contains("/"); + } + return false; + } + + @Override + public Iterator listChildren(Resource parent) { + return getChildren(parent).iterator(); + } + + @Override + public void delete(Resource resource) throws PersistenceException { + + } + + @Override + public Resource create(Resource resource, String s, Map map) throws PersistenceException { + Resource childResource = resource.getChild(s); + if ( childResource != null) { + throw new IllegalArgumentException("Child "+s+" already exists "); + } + String resourceType = (String)map.get("sling:resourceType"); + if ( resourceType == null) { + resourceType = (String)map.get("jcr:primaryType"); + } + if ( resourceType == null) { + LOG.warn("Resource type null for {} {} ", resource, resource.getPath()+"/"+s); + } + Resource newResource = new MockRealResource(this, resource.getPath()+"/"+s, resourceType, map); + repository.put(newResource.getPath(), newResource); + LOG.debug("Created Resource {} ", newResource.getPath()); + return newResource; + } + + @Override + public void revert() { + + } + + @Override + public void commit() throws PersistenceException { + LOG.debug("Committing"); + for(Map.Entry e : repository.entrySet()) { + LOG.debug("Committing {} ", e.getKey()); + Resource r = e.getValue(); + ModifiableValueMap vm = r.adaptTo(ModifiableValueMap.class); + for (Map.Entry me : vm.entrySet()) { + if (me.getValue() instanceof InputStream) { + try { + String value = IOUtils.toString((InputStream) me.getValue()); + LOG.debug("Converted {} {} ", me.getKey(), value); + vm.put(me.getKey(), value); + + } catch (IOException e1) { + throw new PersistenceException("Failed to commit input stream", e1); + } + } + } + LOG.debug("Converted {} ", vm); + } + LOG.debug("Comittted {} ", repository); + + + } + + @Override + public boolean hasChanges() { + return false; + } + }; + + SlingHttpServletRequest request = new MockSlingHttpServlet3Request(null, null, null, null, null) { + @Override + public Object getAttribute(String name) { + if ( "request-parts-iterator".equals(name)) { + return partsIterator; + } + return super.getAttribute(name); + } + + @Override + public ResourceResolver getResourceResolver() { + return resourceResolver; + } + }; + streamedUplodOperation.doRun(request, response, changes); + + + { + Resource r = repository.get("/test/upload/location/test1.txt"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:file", m.get("jcr:primaryType")); + + } + { + Resource r = repository.get("/test/upload/location/test1.txt/jcr:content"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + Assert.assertEquals("nt:resource", m.get("jcr:primaryType")); + Assert.assertTrue(m.get("jcr:lastModified") instanceof Calendar); + Assert.assertEquals("text/plain", m.get("jcr:mimeType")); + Assert.assertEquals("testpart", m.get("jcr:data")); + + } + { + Resource r = repository.get("/test/upload/location/test2.txt"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:file", m.get("jcr:primaryType")); + + } + { + Resource r = repository.get("/test/upload/location/test2.txt/jcr:content"); + Assert.assertNotNull(r); + ValueMap m = r.adaptTo(ValueMap.class); + Assert.assertNotNull(m); + + + Assert.assertEquals("nt:resource", m.get("jcr:primaryType")); + Assert.assertTrue(m.get("jcr:lastModified") instanceof Calendar); + Assert.assertEquals("text/plain2", m.get("jcr:mimeType")); + Assert.assertEquals("test1234", m.get("jcr:data")); + } + + + } + + + + private Map mapOf(String ... s) { + Map m = new HashMap<>(); + for (int i = 0; i < s.length; i+=2) { + m.put(s[i],s[i+1]); + } + return m; + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/Dockerfile b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/Dockerfile new file mode 100644 index 000000000..bf68e595a --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/Dockerfile @@ -0,0 +1,18 @@ +FROM ghcr.io/kupl/starlab-benchmarks/java-base:sling-org-apache-sling-servlets-post + +ENV TZ=Asia/Seoul + +COPY ./metadata.json . +COPY ./npe.json . +COPY ./buggy.java /tmp/buggy.java +RUN export BUGGY_PATH=$(cat metadata.json | jq -r ".npe.filepath") \ + && export BUGGY_LINE=$(cat metadata.json | jq -r ".npe.line") \ + && export BUGGY_MTHD=$(cat metadata.json | jq -r ".npe.npe_method") \ + && mv /tmp/buggy.java $BUGGY_PATH \ + && echo "[{\"filepath\": \"$BUGGY_PATH\", \"line\": $BUGGY_LINE, \"method_name\": \"$BUGGY_MTHD\"}]" | jq . > traces.json + +RUN git init . && git add -A + +RUN $(cat metadata.json | jq -r ".buildCommand") + +RUN $(cat metadata.json | jq -r ".testCommand"); if [ $? -eq 0 ]; then exit 1; fi diff --git a/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/buggy.java b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/buggy.java new file mode 100644 index 000000000..8648c6269 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/buggy.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.sling.api.servlets.HtmlResponse; +import org.apache.sling.servlets.post.PostResponse; + +/** + * The HtmlResponseProxy extends the Sling API + * HtmlResponse overwriting all public methods and redirecting to a + * proxied {@link PostResponse}. As a consequence the underlying (extended) + * Sling API HtmlResponse will not be fed with data and thus will + * remain "empty". + *

+ * This class is mainly used by the deprecated + * {@link org.apache.sling.servlets.post.AbstractSlingPostOperation} for + * bridging into the new + * {@link org.apache.sling.servlets.post.AbstractPostOperation}. + */ +public class HtmlResponseProxy extends HtmlResponse { + + private final PostResponse postResponse; + private boolean createRequest; + + public HtmlResponseProxy(final PostResponse postResponse) { + if(postResponse == null) { + throw new IllegalArgumentException("Null PostResponse, cannot build HtmlResponseProxy"); + } + this.postResponse = postResponse; + postResponse.setCreateRequest(createRequest); + } + + public PostResponse getPostResponse() { + return postResponse; + } + + public Type getProperty(String name, Class type) { + // return postResponse.getProperty(name, type); + return null; + } + + public Object getProperty(String name) { + // return postResponse.getProperty(name); + return null; + } + + public void setProperty(String name, Object value) { + // postResponse.setProperty(name, value); + } + + public Throwable getError() { + return postResponse.getError(); + } + + public String getLocation() { + return postResponse.getLocation(); + } + + public String getParentLocation() { + return postResponse.getParentLocation(); + } + + public String getPath() { + return postResponse.getPath(); + } + + public String getReferer() { + return postResponse.getReferer(); + } + + public int getStatusCode() { + return postResponse.getStatusCode(); + } + + public String getStatusMessage() { + return postResponse.getStatusMessage(); + } + + public boolean isCreateRequest() { + return postResponse.isCreateRequest(); + } + + public boolean isSuccessful() { + return postResponse.isSuccessful(); + } + + public void onChange(String type, String... arguments) { + postResponse.onChange(type, arguments); + } + + public void onCopied(String srcPath, String dstPath) { + postResponse.onCopied(srcPath, dstPath); + } + + public void onCreated(String path) { + postResponse.onCreated(path); + } + + public void onDeleted(String path) { + postResponse.onDeleted(path); + } + + public void onModified(String path) { + postResponse.onModified(path); + } + + public void onMoved(String srcPath, String dstPath) { + postResponse.onMoved(srcPath, dstPath); + } + + public void send(HttpServletResponse response, boolean setStatus) + throws IOException { + postResponse.send(response, setStatus); + } + +public void setCreateRequest(boolean isCreateRequest) { + createRequest = isCreateRequest; + { + // ugly...needed because of SLING-2453, this is called + // by the base class's constructor before postResponse is set + /* NPEX_NULL_EXP */ + postResponse.setCreateRequest(isCreateRequest); + } +} + + public void setError(Throwable error) { + postResponse.setError(error); + } + + public void setLocation(String location) { + postResponse.setLocation(location); + } + + public void setParentLocation(String parentLocation) { + postResponse.setParentLocation(parentLocation); + } + + public void setPath(String path) { + postResponse.setPath(path); + } + + public void setReferer(String referer) { + postResponse.setReferer(referer); + } + + public void setStatus(int code, String message) { + postResponse.setStatus(code, message); + } + + public void setTitle(String title) { + postResponse.setTitle(title); + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json new file mode 100644 index 000000000..ef876ce50 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json @@ -0,0 +1,21 @@ +{ + "language": "java", + "id": "sling-org-apache-sling-servlets-post-HtmlResponseProxy_138", + "buggyPath": ".", + "referencePath": null, + "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", + "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "categories": [ + "safety", + "npe" + ], + "npe": { + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxy.java", + "line": 142, + "npe_method": "setCreateRequest", + "deref_field": "postResponse", + "npe_class": "HtmlResponseProxy", + "repo": "sling-org-apache-sling-servlets-post", + "bug_id": "HtmlResponseProxy_138" + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/npe.json b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/npe.json new file mode 100644 index 000000000..96abf5732 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/npe.json @@ -0,0 +1,7 @@ +{ + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/HtmlResponseProxy.java", + "line": 142, + "npe_method": "setCreateRequest", + "deref_field": "postResponse", + "npe_class": "HtmlResponseProxy" +} \ No newline at end of file diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/Dockerfile b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/Dockerfile new file mode 100644 index 000000000..bf68e595a --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/Dockerfile @@ -0,0 +1,18 @@ +FROM ghcr.io/kupl/starlab-benchmarks/java-base:sling-org-apache-sling-servlets-post + +ENV TZ=Asia/Seoul + +COPY ./metadata.json . +COPY ./npe.json . +COPY ./buggy.java /tmp/buggy.java +RUN export BUGGY_PATH=$(cat metadata.json | jq -r ".npe.filepath") \ + && export BUGGY_LINE=$(cat metadata.json | jq -r ".npe.line") \ + && export BUGGY_MTHD=$(cat metadata.json | jq -r ".npe.npe_method") \ + && mv /tmp/buggy.java $BUGGY_PATH \ + && echo "[{\"filepath\": \"$BUGGY_PATH\", \"line\": $BUGGY_LINE, \"method_name\": \"$BUGGY_MTHD\"}]" | jq . > traces.json + +RUN git init . && git add -A + +RUN $(cat metadata.json | jq -r ".buildCommand") + +RUN $(cat metadata.json | jq -r ".testCommand"); if [ $? -eq 0 ]; then exit 1; fi diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/buggy.java b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/buggy.java new file mode 100644 index 000000000..2d39140b3 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/buggy.java @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Facilitates parsing of the Accept HTTP request header. + * See RFC 2616 section 14.1 + */ +public class MediaRangeList extends TreeSet { + public static final String HEADER_ACCEPT = "Accept"; + public static final String PARAM_ACCEPT = ":http-equiv-accept"; + public static final String WILDCARD = "*"; + boolean matchesAll = false; + + private static final Logger log = LoggerFactory.getLogger(MediaRangeList.class); + + /** + * Constructs a MediaRangeList using information from the supplied HttpServletRequest. + * if the request contains a {@link #PARAM_ACCEPT} query parameter, the query parameter value overrides any + * {@link #HEADER_ACCEPT} header value. + * If the request contains no {@link #PARAM_ACCEPT} parameter, or the parameter value is empty, the value of the + * {@link #HEADER_ACCEPT} is used. If both values are missing, it is assumed that the client accepts all media types, + * as per the RFC. See also {@link MediaRangeList#MediaRangeList(java.lang.String)} + * @param request The HttpServletRequest to extract a MediaRangeList from + */ + public MediaRangeList(HttpServletRequest request) { + String queryParam = request.getParameter(PARAM_ACCEPT); + if (queryParam != null && queryParam.trim().length() != 0) { + init(queryParam); + } else { + init(request.getHeader(HEADER_ACCEPT)); + } + } + + /** + * Constructs a MediaRangeList using a list of media ranges specified in a java.lang.String. + * The string is a comma-separated list of media ranges, as specified by the RFC.
+ * Examples: + *

    + *
  • text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5
  • + *
  • text/html;q=0.8, application/json
  • + *
+ * + * @param listStr The list of media range specifications + */ + public MediaRangeList(String listStr) { + try { + init(listStr); + } catch (Throwable t) { + log.error("Error building MediaRangeList from '" + listStr + "' - will assume client accepts all media types", t); + init(null); + } + } + + private void init(String headerValue) { + if (headerValue == null || headerValue.trim().length() == 0) { + // RFC 2616: "If no Accept header field is present, + // then it is assumed that the client accepts all media types." + this.matchesAll = true; + this.add(new MediaRange(WILDCARD + "/" + WILDCARD)); + } else { + String[] mediaTypes = headerValue.split(","); + for (String type : mediaTypes) { + try { + MediaRange range = new MediaRange(type); + this.add(range); + if (range.matchesAll()) { + this.matchesAll = true; + } + } catch (Throwable throwable) { + log.warn("Error registering media type " + type, throwable); + } + } + } + } + + /** + * Determines if this MediaRangeList contains a given media type. + * @param mediaType A string on the form type/subtype. Neither type + * or subtype should be wildcard (*). + * @return true if this MediaRangeList contains a media type that matches + * mediaType, false otherwise + * @throws IllegalArgumentException if mediaType is not on an accepted form + * @throws NullPointerException if mediaType is null + */ + public boolean contains(String mediaType) { + //noinspection SuspiciousMethodCalls + MediaRange comp = new MediaRange(mediaType); + return this.matchesAll || this.contains(comp); + } + + /** + * Given a list of media types, returns the one is preferred by this MediaRangeList. + * @param mediaRanges An array of possible {@link org.apache.sling.servlets.post.impl.helper.MediaRangeList.MediaRange}s + * @return One of the mediaRanges that this MediaRangeList prefers; + * or null if this MediaRangeList does not contain any of the mediaRanges + * @throws NullPointerException if mediaRanges is null or contains a null value + */ + public MediaRange prefer(Set mediaRanges) { + for (MediaRange range : this) { + for (MediaRange mediaType : mediaRanges) { + if (range.equals(mediaType)) { + return mediaType; + } + } + } + return null; + } + + /** + * Determines which of the mediaRanges specifiactions is prefered by this MediaRangeList. + * @param mediaRanges String representations of MediaRanges. The strings must be + * on the form required by {@link MediaRange#MediaRange(String)} + * @see #prefer(java.util.Set) + * @return the toString representation of the prefered MediaRange, or null + * if this MediaRangeList does not contain any of the mediaRanges + */ + public String prefer(String... mediaRanges) { + Set ranges = new HashSet(); + for (String mediaRange : mediaRanges) { + ranges.add(new MediaRange(mediaRange)); + } + final MediaRange preferred = prefer(ranges); + return(preferred == null ? null : preferred.toString()); + } + + /** + * A code MediaRange represents an entry in a MediaRangeList. + * The MediaRange consists of a supertype and a subtype, + * optionally a quality factor parameter q and other arbitrary parameters. + */ + public class MediaRange implements Comparable { + private String supertype; + private double q = 1; + private Map parameters; + private String subtype; + + /** + * Constructs a MediaRange from a String expression. + * @param exp The String to constuct the MediaRange from. The string is + * expected to be on the form ( "*/*" + * | ( type "/" "*" ) + * | ( type "/" subtype ) + * ) *( ";" parameter )
+ * as specified by RFC 2616, section 14.1.
+ * Examples: + *
    + *
  • text/html;q=0.8
  • + *
  • text/html
  • + *
  • text/html;level=3
  • + *
  • text/html;level=3;q=0.7
  • + *
  • text/*
  • + *
  • */*
  • + *
+ * Note that if the supertype component is wildcard (*), then the subtype component + * must also be wildcard.
+ * The quality factor parameter must be between 0 and 1, inclusive + * (see RFC 2616 section 3.9). + * If the expression does not contain a q parameter, the MediaRange is given + * a default quality factor of 1. + * @throws IllegalArgumentException if exp can not be parsed to a valid media range + * @throws NullPointerException if exp is null + */ + public MediaRange(String exp) { + String[] parts = exp.split(";"); + this.setType(parts[0].trim()); + if (parts.length > 1) { + this.parameters = new HashMap(parts.length - 1); + } + for (int i = 1, partsLength = parts.length; i < partsLength; i++) { + String parameter = parts[i]; + String[] keyValue = parameter.split("="); + if (keyValue[0].equals("q")) { + this.q = Double.parseDouble(keyValue[1]); + if (this.q < 0 || this.q > 1) { + throw new IllegalArgumentException("Quality factor out of bounds: " + exp); + } + } + this.parameters.put(keyValue[0], keyValue[1]); + } + } + + /** + * Constructs a MediaRange of the given supertype and subtype. + * The quality factor is given the default value of 1. + * @param supertype The super type of the media range + * @param subtype The sub type of the media range + */ + MediaRange(String supertype, String subtype) { + this.setType(supertype, subtype); + } + + + /** + * Returns true if this is a catch-all media range (*/*). + * @return true if this range is a catch-all media range, false otherwise + */ + public boolean matchesAll() { + return this.supertype.equals(WILDCARD) && this.subtype.equals(WILDCARD); + } + + private void setType(String supertype, String subtype) { + this.supertype = supertype == null ? WILDCARD : supertype; + this.subtype = subtype == null ? WILDCARD : subtype; + if (this.supertype.equals(WILDCARD) && !this.subtype.equals(WILDCARD)) { + throw new IllegalArgumentException("Supertype cannot be wildcard if subtype is not"); + } + } + + private void setType(String typeDef) { + String[] parts = typeDef.split("/"); + String superType = parts[0]; + String subType = WILDCARD; + if(parts.length > 1){ + subType = parts[1]; + } + this.setType(superType,subType); + } + + MediaRange(String supertype, String subtype, double q) { + this(supertype, subtype); + this.q = q; + } + + + public String getParameter(String key) { + if (parameters != null) { + return parameters.get(key); + } else { + return null; + } + } + + public String getSupertype() { + return supertype; + } + + public String getSubtype() { + return subtype; + } + + /** + * Get the value of the quality factor parameter (q). + * @return the quality factor + */ + public double getQ() { + return q; + } + +public java.util.Map getParameters() { + return /* NPEX_NULL_EXP */ + parameters; +} + + /* -- Comparable implementation -- */ + public int compareTo(MediaRange o) { + double diff = this.q - o.getQ(); + if (diff == 0) { + // Compare parameters + int paramDiff = o.getParameters().size() - this.getParameters().size(); + if (paramDiff != 0) { + return paramDiff; + } + // Compare wildcards + if (this.supertype.equals(WILDCARD) && !o.getSupertype().equals(WILDCARD)) { + return 1; + } else if (!this.supertype.equals(WILDCARD) && o.getSupertype().equals(WILDCARD)) { + return -1; + } + if (this.subtype.equals(WILDCARD) && !o.getSubtype().equals(WILDCARD)) { + return 1; + } else if (!this.subtype.equals(WILDCARD) && o.getSubtype().equals(WILDCARD)) { + return -1; + } + // Compare names + return this.toString().compareTo(o.toString()); + } else { + return diff > 0 ? -1 : 1; + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MediaRange) { + MediaRange mr = (MediaRange) obj; + return mr.getSupertype().equals(this.supertype) && mr.getSubtype().equals(this.subtype); + } + return super.equals(obj); + } + + public boolean equals(String s) { + return (this.supertype + "/" + this.subtype).equals(s); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(this.supertype); + buf.append('/'); + buf.append(this.subtype); + if (parameters != null) { + String delimiter = ";"; + for (String key : parameters.keySet()) { + buf.append(delimiter); + buf.append(key).append("=").append(parameters.get(key)); + } + } + return buf.toString(); + } + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json new file mode 100644 index 000000000..20a663115 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json @@ -0,0 +1,21 @@ +{ + "language": "java", + "id": "sling-org-apache-sling-servlets-post-MediaRangeList_277", + "buggyPath": ".", + "referencePath": null, + "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", + "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "categories": [ + "safety", + "npe" + ], + "npe": { + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java", + "line": 278, + "npe_method": "getParameters", + "deref_field": "parameters", + "npe_class": "MediaRange", + "repo": "sling-org-apache-sling-servlets-post", + "bug_id": "MediaRangeList_277" + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/npe.json b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/npe.json new file mode 100644 index 000000000..9efd2669a --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/npe.json @@ -0,0 +1,7 @@ +{ + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java", + "line": 278, + "npe_method": "getParameters", + "deref_field": "parameters", + "npe_class": "MediaRange" +} \ No newline at end of file diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/Dockerfile b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/Dockerfile new file mode 100644 index 000000000..bf68e595a --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/Dockerfile @@ -0,0 +1,18 @@ +FROM ghcr.io/kupl/starlab-benchmarks/java-base:sling-org-apache-sling-servlets-post + +ENV TZ=Asia/Seoul + +COPY ./metadata.json . +COPY ./npe.json . +COPY ./buggy.java /tmp/buggy.java +RUN export BUGGY_PATH=$(cat metadata.json | jq -r ".npe.filepath") \ + && export BUGGY_LINE=$(cat metadata.json | jq -r ".npe.line") \ + && export BUGGY_MTHD=$(cat metadata.json | jq -r ".npe.npe_method") \ + && mv /tmp/buggy.java $BUGGY_PATH \ + && echo "[{\"filepath\": \"$BUGGY_PATH\", \"line\": $BUGGY_LINE, \"method_name\": \"$BUGGY_MTHD\"}]" | jq . > traces.json + +RUN git init . && git add -A + +RUN $(cat metadata.json | jq -r ".buildCommand") + +RUN $(cat metadata.json | jq -r ".testCommand"); if [ $? -eq 0 ]; then exit 1; fi diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/buggy.java b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/buggy.java new file mode 100644 index 000000000..1a78327db --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/buggy.java @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sling.servlets.post.impl.helper; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Facilitates parsing of the Accept HTTP request header. + * See RFC 2616 section 14.1 + */ +public class MediaRangeList extends TreeSet { + public static final String HEADER_ACCEPT = "Accept"; + public static final String PARAM_ACCEPT = ":http-equiv-accept"; + public static final String WILDCARD = "*"; + boolean matchesAll = false; + + private static final Logger log = LoggerFactory.getLogger(MediaRangeList.class); + + /** + * Constructs a MediaRangeList using information from the supplied HttpServletRequest. + * if the request contains a {@link #PARAM_ACCEPT} query parameter, the query parameter value overrides any + * {@link #HEADER_ACCEPT} header value. + * If the request contains no {@link #PARAM_ACCEPT} parameter, or the parameter value is empty, the value of the + * {@link #HEADER_ACCEPT} is used. If both values are missing, it is assumed that the client accepts all media types, + * as per the RFC. See also {@link MediaRangeList#MediaRangeList(java.lang.String)} + * @param request The HttpServletRequest to extract a MediaRangeList from + */ + public MediaRangeList(HttpServletRequest request) { + String queryParam = request.getParameter(PARAM_ACCEPT); + if (queryParam != null && queryParam.trim().length() != 0) { + init(queryParam); + } else { + init(request.getHeader(HEADER_ACCEPT)); + } + } + + /** + * Constructs a MediaRangeList using a list of media ranges specified in a java.lang.String. + * The string is a comma-separated list of media ranges, as specified by the RFC.
+ * Examples: + *
    + *
  • text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5
  • + *
  • text/html;q=0.8, application/json
  • + *
+ * + * @param listStr The list of media range specifications + */ + public MediaRangeList(String listStr) { + try { + init(listStr); + } catch (Throwable t) { + log.error("Error building MediaRangeList from '" + listStr + "' - will assume client accepts all media types", t); + init(null); + } + } + + private void init(String headerValue) { + if (headerValue == null || headerValue.trim().length() == 0) { + // RFC 2616: "If no Accept header field is present, + // then it is assumed that the client accepts all media types." + this.matchesAll = true; + this.add(new MediaRange(WILDCARD + "/" + WILDCARD)); + } else { + String[] mediaTypes = headerValue.split(","); + for (String type : mediaTypes) { + try { + MediaRange range = new MediaRange(type); + this.add(range); + if (range.matchesAll()) { + this.matchesAll = true; + } + } catch (Throwable throwable) { + log.warn("Error registering media type " + type, throwable); + } + } + } + } + + /** + * Determines if this MediaRangeList contains a given media type. + * @param mediaType A string on the form type/subtype. Neither type + * or subtype should be wildcard (*). + * @return true if this MediaRangeList contains a media type that matches + * mediaType, false otherwise + * @throws IllegalArgumentException if mediaType is not on an accepted form + * @throws NullPointerException if mediaType is null + */ + public boolean contains(String mediaType) { + //noinspection SuspiciousMethodCalls + MediaRange comp = new MediaRange(mediaType); + return this.matchesAll || this.contains(comp); + } + + /** + * Given a list of media types, returns the one is preferred by this MediaRangeList. + * @param mediaRanges An array of possible {@link org.apache.sling.servlets.post.impl.helper.MediaRangeList.MediaRange}s + * @return One of the mediaRanges that this MediaRangeList prefers; + * or null if this MediaRangeList does not contain any of the mediaRanges + * @throws NullPointerException if mediaRanges is null or contains a null value + */ + public MediaRange prefer(Set mediaRanges) { + for (MediaRange range : this) { + for (MediaRange mediaType : mediaRanges) { + if (range.equals(mediaType)) { + return mediaType; + } + } + } + return null; + } + + /** + * Determines which of the mediaRanges specifiactions is prefered by this MediaRangeList. + * @param mediaRanges String representations of MediaRanges. The strings must be + * on the form required by {@link MediaRange#MediaRange(String)} + * @see #prefer(java.util.Set) + * @return the toString representation of the prefered MediaRange, or null + * if this MediaRangeList does not contain any of the mediaRanges + */ + public String prefer(String... mediaRanges) { + Set ranges = new HashSet(); + for (String mediaRange : mediaRanges) { + ranges.add(new MediaRange(mediaRange)); + } + final MediaRange preferred = prefer(ranges); + return(preferred == null ? null : preferred.toString()); + } + + /** + * A code MediaRange represents an entry in a MediaRangeList. + * The MediaRange consists of a supertype and a subtype, + * optionally a quality factor parameter q and other arbitrary parameters. + */ + public class MediaRange implements Comparable { + private String supertype; + private double q = 1; + private Map parameters; + private String subtype; + + /** + * Constructs a MediaRange from a String expression. + * @param exp The String to constuct the MediaRange from. The string is + * expected to be on the form ( "*/*" + * | ( type "/" "*" ) + * | ( type "/" subtype ) + * ) *( ";" parameter )
+ * as specified by RFC 2616, section 14.1.
+ * Examples: + *
    + *
  • text/html;q=0.8
  • + *
  • text/html
  • + *
  • text/html;level=3
  • + *
  • text/html;level=3;q=0.7
  • + *
  • text/*
  • + *
  • */*
  • + *
+ * Note that if the supertype component is wildcard (*), then the subtype component + * must also be wildcard.
+ * The quality factor parameter must be between 0 and 1, inclusive + * (see RFC 2616 section 3.9). + * If the expression does not contain a q parameter, the MediaRange is given + * a default quality factor of 1. + * @throws IllegalArgumentException if exp can not be parsed to a valid media range + * @throws NullPointerException if exp is null + */ + public MediaRange(String exp) { + String[] parts = exp.split(";"); + this.setType(parts[0].trim()); + if (parts.length > 1) { + this.parameters = new HashMap(parts.length - 1); + } + for (int i = 1, partsLength = parts.length; i < partsLength; i++) { + String parameter = parts[i]; + String[] keyValue = parameter.split("="); + if (keyValue[0].equals("q")) { + this.q = Double.parseDouble(keyValue[1]); + if (this.q < 0 || this.q > 1) { + throw new IllegalArgumentException("Quality factor out of bounds: " + exp); + } + } + this.parameters.put(keyValue[0], keyValue[1]); + } + } + + /** + * Constructs a MediaRange of the given supertype and subtype. + * The quality factor is given the default value of 1. + * @param supertype The super type of the media range + * @param subtype The sub type of the media range + */ + MediaRange(String supertype, String subtype) { + this.setType(supertype, subtype); + } + + + /** + * Returns true if this is a catch-all media range (*/*). + * @return true if this range is a catch-all media range, false otherwise + */ + public boolean matchesAll() { + return this.supertype.equals(WILDCARD) && this.subtype.equals(WILDCARD); + } + + private void setType(String supertype, String subtype) { + this.supertype = supertype == null ? WILDCARD : supertype; + this.subtype = subtype == null ? WILDCARD : subtype; + if (this.supertype.equals(WILDCARD) && !this.subtype.equals(WILDCARD)) { + throw new IllegalArgumentException("Supertype cannot be wildcard if subtype is not"); + } + } + + private void setType(String typeDef) { + String[] parts = typeDef.split("/"); + String superType = parts[0]; + String subType = WILDCARD; + if(parts.length > 1){ + subType = parts[1]; + } + this.setType(superType,subType); + } + + MediaRange(String supertype, String subtype, double q) { + this(supertype, subtype); + this.q = q; + } + + + public String getParameter(String key) { + if (parameters != null) { + return parameters.get(key); + } else { + return null; + } + } + + public String getSupertype() { + return supertype; + } + + public String getSubtype() { + return subtype; + } + + /** + * Get the value of the quality factor parameter (q). + * @return the quality factor + */ + public double getQ() { + return q; + } + + public Map getParameters() { + return parameters != null ? parameters : new HashMap(0); + } + + /* -- Comparable implementation -- */ + public int compareTo(MediaRange o) { + double diff = this.q - o.getQ(); + if (diff == 0) { + // Compare parameters + int paramDiff = o.getParameters().size() - this.getParameters().size(); + if (paramDiff != 0) { + return paramDiff; + } + // Compare wildcards + if (this.supertype.equals(WILDCARD) && !o.getSupertype().equals(WILDCARD)) { + return 1; + } else if (!this.supertype.equals(WILDCARD) && o.getSupertype().equals(WILDCARD)) { + return -1; + } + if (this.subtype.equals(WILDCARD) && !o.getSubtype().equals(WILDCARD)) { + return 1; + } else if (!this.subtype.equals(WILDCARD) && o.getSubtype().equals(WILDCARD)) { + return -1; + } + // Compare names + return this.toString().compareTo(o.toString()); + } else { + return diff > 0 ? -1 : 1; + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MediaRange) { + MediaRange mr = (MediaRange) obj; + return mr.getSupertype().equals(this.supertype) && mr.getSubtype().equals(this.subtype); + } + return super.equals(obj); + } + + public boolean equals(String s) { + return (this.supertype + "/" + this.subtype).equals(s); + } + + @Override +public java.lang.String toString() { + final java.lang.StringBuilder buf = new java.lang.StringBuilder(this.supertype); + buf.append('/'); + buf.append(this.subtype); + { + java.lang.String delimiter = ";"; + for (java.lang.String key : /* NPEX_NULL_EXP */ + parameters.keySet()) { + buf.append(delimiter); + buf.append(key).append("=").append(parameters.get(key)); + } + } + return buf.toString(); +} + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json new file mode 100644 index 000000000..3530c258e --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json @@ -0,0 +1,21 @@ +{ + "language": "java", + "id": "sling-org-apache-sling-servlets-post-MediaRangeList_325", + "buggyPath": ".", + "referencePath": null, + "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", + "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "categories": [ + "safety", + "npe" + ], + "npe": { + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java", + "line": 328, + "npe_method": "toString", + "deref_field": "parameters", + "npe_class": "MediaRange", + "repo": "sling-org-apache-sling-servlets-post", + "bug_id": "MediaRangeList_325" + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/npe.json b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/npe.json new file mode 100644 index 000000000..923633d06 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/npe.json @@ -0,0 +1,7 @@ +{ + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/MediaRangeList.java", + "line": 328, + "npe_method": "toString", + "deref_field": "parameters", + "npe_class": "MediaRange" +} \ No newline at end of file diff --git a/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/Dockerfile b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/Dockerfile new file mode 100644 index 000000000..bf68e595a --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/Dockerfile @@ -0,0 +1,18 @@ +FROM ghcr.io/kupl/starlab-benchmarks/java-base:sling-org-apache-sling-servlets-post + +ENV TZ=Asia/Seoul + +COPY ./metadata.json . +COPY ./npe.json . +COPY ./buggy.java /tmp/buggy.java +RUN export BUGGY_PATH=$(cat metadata.json | jq -r ".npe.filepath") \ + && export BUGGY_LINE=$(cat metadata.json | jq -r ".npe.line") \ + && export BUGGY_MTHD=$(cat metadata.json | jq -r ".npe.npe_method") \ + && mv /tmp/buggy.java $BUGGY_PATH \ + && echo "[{\"filepath\": \"$BUGGY_PATH\", \"line\": $BUGGY_LINE, \"method_name\": \"$BUGGY_MTHD\"}]" | jq . > traces.json + +RUN git init . && git add -A + +RUN $(cat metadata.json | jq -r ".buildCommand") + +RUN $(cat metadata.json | jq -r ".testCommand"); if [ $? -eq 0 ]; then exit 1; fi diff --git a/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/buggy.java b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/buggy.java new file mode 100644 index 000000000..6cb6593e3 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/buggy.java @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.servlets.post.impl.helper; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.sling.api.request.RequestParameter; +import org.apache.sling.api.resource.ResourceUtil; +import org.apache.sling.servlets.post.SlingPostConstants; + +/** + * Encapsulates all infos from the respective request parameters that are needed + * to create the repository property + */ +public class RequestProperty { + + private static final RequestParameter[] EMPTY_PARAM_ARRAY = new RequestParameter[0]; + + public static final String DEFAULT_IGNORE = SlingPostConstants.RP_PREFIX + + "ignore"; + + public static final String DEFAULT_NULL = SlingPostConstants.RP_PREFIX + + "null"; + + private final String path; + + private final String name; + + private final String parentPath; + + private RequestParameter[] values; + + private String[] stringValues; + + private String typeHint; + + private boolean hasMultiValueTypeHint; + + private RequestParameter[] defaultValues = EMPTY_PARAM_ARRAY; + + private boolean isDelete; + + private String repositoryResourcePath; + + private boolean isRepositoryResourceMove; + + private boolean ignoreBlanks; + + private boolean useDefaultWhenMissing; + + private boolean patch = false; + + private Chunk chunk; + + public RequestProperty(String path) { + assert path.startsWith("/"); + this.path = ResourceUtil.normalize(path); + this.parentPath = ResourceUtil.getParent(path); + this.name = ResourceUtil.getName(path); + } + + public String getTypeHint() { + return typeHint; + } + + public boolean hasMultiValueTypeHint() { + return this.hasMultiValueTypeHint; + } + + public void setTypeHintValue(String typeHint) { + if ( typeHint != null && typeHint.endsWith("[]") ) { + this.typeHint = typeHint.substring(0, typeHint.length() - 2); + this.hasMultiValueTypeHint = true; + } else { + this.typeHint = typeHint; + this.hasMultiValueTypeHint = false; + } + } + + public String getPath() { + return path; + } + + public String getName() { + return name; + } + + public String getParentPath() { + return parentPath; + } + + public boolean hasValues() { + if (useDefaultWhenMissing && defaultValues != null && defaultValues.length > 0) { + return true; + } else { + if (ignoreBlanks) { + return (values != null && getStringValues().length > 0); + } else { + return values != null; + } + } + } + + public RequestParameter[] getValues() { + return values; + } + + public void setValues(RequestParameter[] values) { + this.values = values; + } + + public RequestParameter[] getDefaultValues() { + return defaultValues; + } + + public void setDefaultValues(RequestParameter[] defaultValues) { + if (defaultValues == null) { + this.defaultValues = EMPTY_PARAM_ARRAY; + } else { + this.defaultValues = defaultValues; + } + } + + public boolean isFileUpload() { + return values != null && !values[0].isFormField(); + } + + /** + * Checks if this property provides any values. this is the case if one of + * the values is not empty or if the default handling is not 'ignore' + * + * @return true if this property provides values + */ +/** + * Checks if this property provides any values. this is the case if one of + * the values is not empty or if the default handling is not 'ignore' + * + * @return true if this property provides values + */ +public boolean providesValue() { + // should void double creation of string values + java.lang.String[] sv = getStringValues(); + { + for (java.lang.String s : /* NPEX_NULL_EXP */ + sv) { + if (!s.equals("")) { + return true; + } + } + return false; + } +} + + /** + * Returns the assembled string array out of the provided request values and + * default values. + * + * @return a String array or null if the property needs to be + * removed. + */ + public String[] getStringValues() { + if (stringValues == null) { + if (values == null && useDefaultWhenMissing) { + stringValues = new String[] { defaultValues[0].getString() }; + } else if (values.length > 1) { + // TODO: how the default values work for MV props is not very + // clear + List stringValueList = new ArrayList(values.length); + for (int i = 0; i < values.length; i++) { + String value = values[i].getString(); + if ((!ignoreBlanks) || value.length() > 0) { + stringValueList.add(value); + } + } + stringValues = stringValueList.toArray(new String[stringValueList.size()]); + } else { + String value = values[0].getString(); + if (value.equals("")) { + if (ignoreBlanks) { + return new String[0]; + } else { + if (defaultValues.length == 1) { + String defValue = defaultValues[0].getString(); + if (defValue.equals(DEFAULT_IGNORE)) { + // ignore means, do not create empty values + return new String[0]; + } else if (defValue.equals(DEFAULT_NULL)) { + // null means, remove property if exist + return null; + } + value = defValue; + } + } + } + stringValues = new String[] { value }; + } + } + return stringValues; + } + + /** + * Specifies whether this property should be deleted before any new content + * is to be set according to the values stored. + * + * @param isDelete true if the repository item described by + * this is to be deleted before any other operation. + */ + public void setDelete(boolean isDelete) { + this.isDelete = isDelete; + } + + /** + * Returns true if the repository item described by this is + * to be deleted before setting new content to it. + */ + public boolean isDelete() { + return isDelete; + } + + /** + * Sets the path of the repository item from which the content for this + * property is to be copied or moved. The path may be relative in which case + * it will be resolved relative to the absolute path of this property. + * + * @param sourcePath The path of the repository item to get the content from + * @param isMove true if the source content is to be moved, + * otherwise the source content is copied from the repository + * item. + */ + public void setRepositorySource(String sourcePath, boolean isMove) { + + // make source path absolute + if (!sourcePath.startsWith("/")) { + sourcePath = getParentPath() + "/" + sourcePath; + sourcePath = ResourceUtil.normalize(sourcePath); + } + + this.repositoryResourcePath = sourcePath; + this.isRepositoryResourceMove = isMove; + } + + /** + * Returns true if the content of this property is to be set + * by moving content from another repository item. + * + * @see #getRepositorySource() + */ + public boolean hasRepositoryMoveSource() { + return isRepositoryResourceMove; + } + + /** + * Returns true if the content of this property is to be set + * by copying content from another repository item. + * + * @see #getRepositorySource() + */ + public boolean hasRepositoryCopySource() { + return getRepositorySource() != null && !hasRepositoryMoveSource(); + } + + /** + * Returns the absolute path of the repository item from which the content + * for this property is to be copied or moved. + * + * @see #hasRepositoryCopySource() + * @see #hasRepositoryMoveSource() + * @see #setRepositorySource(String, boolean) + */ + public String getRepositorySource() { + return repositoryResourcePath; + } + + public void setIgnoreBlanks(boolean b) { + ignoreBlanks = b; + } + + public void setUseDefaultWhenMissing(boolean b) { + useDefaultWhenMissing = b; + } + + public void setPatch(boolean b) { + patch = b; + } + + /** + * Returns whether this property is to be handled as a multi-value property + * seen as set. + */ + public boolean isPatch() { + return patch; + } + + /** + * Return true if request is chunk upload. + */ + public boolean isChunkUpload() { + return chunk != null; + } + + public Chunk getChunk() { + return chunk; + } + + public void setChunk(Chunk chunk) { + this.chunk = chunk; + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json new file mode 100644 index 000000000..99f37f1d1 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json @@ -0,0 +1,21 @@ +{ + "language": "java", + "id": "sling-org-apache-sling-servlets-post-RequestProperty_152", + "buggyPath": ".", + "referencePath": null, + "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", + "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "categories": [ + "safety", + "npe" + ], + "npe": { + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java", + "line": 160, + "npe_method": "providesValue", + "deref_field": "sv", + "npe_class": "RequestProperty", + "repo": "sling-org-apache-sling-servlets-post", + "bug_id": "RequestProperty_152" + } +} diff --git a/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/npe.json b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/npe.json new file mode 100644 index 000000000..d926d3688 --- /dev/null +++ b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/npe.json @@ -0,0 +1,7 @@ +{ + "filepath": "src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java", + "line": 160, + "npe_method": "providesValue", + "deref_field": "sv", + "npe_class": "RequestProperty" +} \ No newline at end of file From 6b9da4f990107d8fcd17ef4baecc041f940d2bbd Mon Sep 17 00:00:00 2001 From: junheeL Date: Fri, 1 Dec 2023 10:41:46 +0900 Subject: [PATCH 2/2] fix(java): metadata --- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- .../metadata.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json index ef876ce50..c73ab7b18 100644 --- a/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json +++ b/Java/sling-org-apache-sling-servlets-post-HtmlResponseProxy_138/metadata.json @@ -4,7 +4,7 @@ "buggyPath": ".", "referencePath": null, "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", - "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "testCommand": "mvn clean test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", "categories": [ "safety", "npe" diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json index 20a663115..4124430d2 100644 --- a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_277/metadata.json @@ -4,7 +4,7 @@ "buggyPath": ".", "referencePath": null, "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", - "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "testCommand": "mvn clean test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", "categories": [ "safety", "npe" diff --git a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json index 3530c258e..9925b2776 100644 --- a/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json +++ b/Java/sling-org-apache-sling-servlets-post-MediaRangeList_325/metadata.json @@ -4,7 +4,7 @@ "buggyPath": ".", "referencePath": null, "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", - "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "testCommand": "mvn clean test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", "categories": [ "safety", "npe" diff --git a/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json index 99f37f1d1..1f06a8dbc 100644 --- a/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json +++ b/Java/sling-org-apache-sling-servlets-post-RequestProperty_152/metadata.json @@ -4,7 +4,7 @@ "buggyPath": ".", "referencePath": null, "buildCommand": "mvn package -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false", - "testCommand": "mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", + "testCommand": "mvn clean test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100", "categories": [ "safety", "npe"