Compare commits

...

114 Commits

Author SHA1 Message Date
alfresco-build
ae29ae8581 [maven-release-plugin][skip ci] prepare release 20.106 2023-03-16 18:42:24 +00:00
Jared Ottley
4b351fbfdc [MNT-23553] REST API call fails when using Oracle database
- Remove semicolon from sql
2023-03-16 11:56:00 -06:00
alfresco-build
3e9964d53f [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-16 08:22:17 +00:00
alfresco-build
23761f6c56 [maven-release-plugin][skip ci] prepare release 20.105 2023-03-16 08:22:13 +00:00
Piotr Żurek
2f8c283ada ACS-4844 Use matching json-smart version (#1805) 2023-03-16 07:58:25 +01:00
alfresco-build
5232b8c3fe [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-15 16:35:10 +00:00
alfresco-build
b90938bb99 [maven-release-plugin][skip ci] prepare release 20.104 2023-03-15 16:35:07 +00:00
Piotr Żurek
d243ac04c6 ACS-4844 json-path should be provided by the repo to avoid conflicts with AMPs (#1803) 2023-03-15 16:49:17 +01:00
alfresco-build
e651f6e104 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-15 14:55:19 +00:00
alfresco-build
e4776e2594 [maven-release-plugin][skip ci] prepare release 20.103 2023-03-15 14:55:15 +00:00
Tom Page
acc5425d68 ACS-4759 Replace default password hashing algorithm. (#1789)
ACS-4759 Replace default password hashing algorithm with bcrypt10.

Update tests to use sha256 hashing and fix tests that relied on md4 being the default hashing algorithm.

Remove test for default password hashing algorithm since this is being overridden for the tests anyway.
2023-03-15 14:11:25 +00:00
alfresco-build
a96e805d52 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-15 09:37:38 +00:00
alfresco-build
45a9a1ae49 [maven-release-plugin][skip ci] prepare release 20.102 2023-03-15 09:37:34 +00:00
Piotr Żurek
bf18c6b419 ACS-4751 Keycloak Migration - OAuth2 Client Role (#1798) 2023-03-15 09:53:17 +01:00
dependabot[bot]
e728489b69 Bump groovy-json from 3.0.12 to 3.0.16 (#1800)
Bumps [groovy-json](https://github.com/apache/groovy) from 3.0.12 to 3.0.16.
- [Release notes](https://github.com/apache/groovy/releases)
- [Commits](https://github.com/apache/groovy/commits)

---
updated-dependencies:
- dependency-name: org.codehaus.groovy:groovy-json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 09:52:13 +01:00
Krystian Dabrowski
f2fdf958f2 ACS-4023: Update GET /tags to support getting tags by name (#1766)
* ACS-4023: Update GET /tags to support getting tags by name
2023-03-15 09:46:58 +01:00
alfresco-build
0cb03c2a38 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-14 09:23:52 +00:00
alfresco-build
b13ca1f68b [maven-release-plugin][skip ci] prepare release 20.101 2023-03-14 09:23:49 +00:00
dependabot[bot]
484699b266 Bump groovy from 3.0.12 to 3.0.16 (#1801)
Bumps [groovy](https://github.com/apache/groovy) from 3.0.12 to 3.0.16.
- [Release notes](https://github.com/apache/groovy/releases)
- [Commits](https://github.com/apache/groovy/commits)

---
updated-dependencies:
- dependency-name: org.codehaus.groovy:groovy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-14 09:55:22 +01:00
alfresco-build
a15161c872 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-13 21:34:16 +00:00
alfresco-build
b3a6150655 [maven-release-plugin][skip ci] prepare release 20.100 2023-03-13 21:34:13 +00:00
Damian Ujma
42e06da4f8 ACS-4137 Change the 'build-multiarch-docker-image' profile phase to package (#1799)
* ACS-4137 Change the build-multiarch-docker-image profile phase to package

* ACS-4137 Change the build-multiarch-docker-image profile phase to package
2023-03-13 22:05:22 +01:00
alfresco-build
4ac30c2173 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-13 15:30:38 +00:00
alfresco-build
d1d84d849e [maven-release-plugin][skip ci] prepare release 20.99 2023-03-13 15:30:35 +00:00
dependabot[bot]
40af1799fe Bump alfresco/alfresco-base-tomcat from tomcat9-jre17-rockylinux8-202209261711 to tomcat9-jre17-rockylinux8-202303081618 in /packaging/docker-alfresco (#1792)
* Bump alfresco/alfresco-base-tomcat in /packaging/docker-alfresco

Bumps alfresco/alfresco-base-tomcat from tomcat9-jre17-rockylinux8-202209261711 to tomcat9-jre17-rockylinux8-202303081618.

---
updated-dependencies:
- dependency-name: alfresco/alfresco-base-tomcat
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* ACS-4768 Update freetype package for Rocky Linux 8.7

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Damian.Ujma@hyland.com <Damian.Ujma@hyland.com>
2023-03-13 16:03:51 +01:00
alfresco-build
f59e4a044a [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-13 10:24:01 +00:00
alfresco-build
5924263c18 [maven-release-plugin][skip ci] prepare release 20.98 2023-03-13 10:23:58 +00:00
Damian Ujma
082e38692f ACS-4137 Produce Multi-Arch Docker images (#1797)
* Revert "Revert "ACS-4137 Produce Multi-Arch Docker images (#1656)" (#1796)"

This reverts commit ddbaabb713.

* ACS-4137 Remove default multi-arch docker-maven-plugin configuration
2023-03-13 10:44:06 +01:00
alfresco-build
f5c4112e65 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-12 00:06:40 +00:00
alfresco-build
ad8251c054 [maven-release-plugin][skip ci] prepare release 20.97 2023-03-12 00:06:37 +00:00
Alfresco CI User
32d0182096 [force] Force release for 2023-03-12. 2023-03-12 00:03:26 +00:00
alfresco-build
2ac9779a3b [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-10 10:45:43 +00:00
alfresco-build
af999fb0fd [maven-release-plugin][skip ci] prepare release 20.96 2023-03-10 10:45:40 +00:00
Damian Ujma
ddbaabb713 Revert "ACS-4137 Produce Multi-Arch Docker images (#1656)" (#1796)
This reverts commit 212fa9b362.
2023-03-10 11:20:43 +01:00
alfresco-build
b93136f2c0 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-10 09:06:46 +00:00
alfresco-build
923aadb12a [maven-release-plugin][skip ci] prepare release 20.95 2023-03-10 09:06:42 +00:00
Wojtek Świętoń
212fa9b362 ACS-4137 Produce Multi-Arch Docker images (#1656)
* ACS-4137 Update pom.xml to use buildx

* ACS-4137 Update travis.yml to use buildx

* ACS-4137 Update travis.yml to use buildx, newest docker version

* ACS-4137 reverted unnecessary changes in travis.yml file

* ACS-4137 Change deprecated <dockerFileDir> property to <contextDir>. Moved <build> section from subpoms to global pom.

* ACS-4137 POM indentation fixes

* ACS-4137 Fix building a multiarch image (#1697)

* ACS-4137 Test wih GHA [ags][tas]

* ACS-4137 Test wih GHA [ags][tas]

* ACS-4137 Fix setting network [ags][tas]

* ACS-4137 Fix creating builder [ags][tas]

* ACS-4137 Change image tag [ags][tas]

* ACS-4137 Fix starting the registry [ags][tas]

* ACS-4137 Refactor code [ags][tas]

* ACS-4137 Uncomment all jobs [tas][ags]

* ACS-4137 Improve prepare_buildx.sh [tas][ags]

* ACS-4137 Implement timeout + remove hardcoded base image tag [tas][ags]

* ACS-4137 Added exec-maven-plugin to build-push-image for alfresco-governance-repository-community-base image

* ACS-4137 Generalize prepare_buildx.sh + increase registry timeout

* ACS-4137 merged local.registry.host and local.registry.port. Builder name changed to entitled-builder. In prepare_builder script added localhost registry checking

* ACS-4137 added build-multiarch-docker-images maven profile

* ACS-4137 added <BASE_IMAGE> arg to build-docker-images profile

* ACS-4137 added combine.self="override" attribute to configuration in build-docker-images profile to not use buildx. Delete redundant base.image.tag

* ACS-4137 Push docker images to local repository

* ACS-4137 Remove useless scripts

* ACS-4137 Move builder.name and local.registry properties to main pom.xml

* ACS-4137 Remove useless properties definitions

---------

Co-authored-by: Damian.Ujma@hyland.com <Damian.Ujma@hyland.com>
Co-authored-by: Damian Ujma <92095156+damianujma@users.noreply.github.com>
2023-03-10 09:20:22 +01:00
alfresco-build
c3e37b96b4 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-08 10:53:06 +00:00
alfresco-build
517f40e150 [maven-release-plugin][skip ci] prepare release 20.94 2023-03-08 10:53:02 +00:00
dependabot[bot]
f86d0f1fd2 Bump alfresco-jlan-embed from 7.2 to 7.4 (#1773)
Bumps alfresco-jlan-embed from 7.2 to 7.4.

---
updated-dependencies:
- dependency-name: org.alfresco:alfresco-jlan-embed
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 10:38:40 +01:00
alfresco-build
09da8640a0 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-08 09:14:04 +00:00
alfresco-build
6bd9bf768e [maven-release-plugin][skip ci] prepare release 20.93 2023-03-08 09:14:00 +00:00
pzurek
f75c0c8f9e Just to check if the recent problems are related to Dependabot commits 2023-03-08 09:14:54 +01:00
dependabot[bot]
ff9364a3b1 Bump utility from 3.0.58 to 3.0.61 (#1776) 2023-03-07 19:22:57 +00:00
dependabot[bot]
db832663b4 Bump dependency.activemq.version from 5.17.1 to 5.17.4 (#1778) 2023-03-07 11:39:30 +00:00
alfresco-build
890e1173c5 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-07 11:36:43 +00:00
alfresco-build
9399c38741 [maven-release-plugin][skip ci] prepare release 20.92 2023-03-07 11:36:39 +00:00
mikolajbrzezinski
bf16e843fd ACS-3975 Fix Intermittent Test Failure (#1785)
Fix Intermittent Test Failure
2023-03-07 11:50:33 +01:00
dependabot[bot]
69a5a95dae Bump docker-maven-plugin from 0.40.2 to 0.42.0 (#1784) 2023-03-07 10:50:09 +00:00
alfresco-build
be37440e24 [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-07 10:29:48 +00:00
alfresco-build
6504406861 [maven-release-plugin][skip ci] prepare release 20.91 2023-03-07 10:29:44 +00:00
dependabot[bot]
38d7dbfa00 Bump json from 20220320 to 20230227 (#1779)
Bumps [json](https://github.com/douglascrockford/JSON-java) from 20220320 to 20230227.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-07 10:46:42 +01:00
dependabot[bot]
1ed635275f Bump commons-fileupload from 1.4 to 1.5 (#1749)
Bumps commons-fileupload from 1.4 to 1.5.

---
updated-dependencies:
- dependency-name: commons-fileupload:commons-fileupload
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-07 10:46:26 +01:00
Domenico Sibilio
7b87ee628d ACS-4776 Finalize deprecation of BitlyUrlShortenerImpl (#1787) 2023-03-07 10:32:14 +01:00
alfresco-build
9cff80362d [maven-release-plugin][skip ci] prepare for next development iteration 2023-03-05 00:06:40 +00:00
alfresco-build
8311c889bb [maven-release-plugin][skip ci] prepare release 20.90 2023-03-05 00:06:38 +00:00
Alfresco CI User
bd5b5a240e [force] Force release for 2023-03-05. 2023-03-05 00:03:38 +00:00
alfresco-build
c2d13e3177 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-27 11:08:57 +00:00
alfresco-build
e8f50da5a2 [maven-release-plugin][skip ci] prepare release 20.89 2023-02-27 11:08:54 +00:00
Tom Page
2053e3b83a Code freeze testing. 2023-02-27 09:35:11 +00:00
alfresco-build
bcc2eadba6 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-26 00:06:28 +00:00
alfresco-build
eb40dd3a45 [maven-release-plugin][skip ci] prepare release 20.88 2023-02-26 00:06:25 +00:00
Alfresco CI User
4c511423d5 [force] Force release for 2023-02-26. 2023-02-26 00:03:24 +00:00
alfresco-build
b12b21e773 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-24 07:28:12 +00:00
alfresco-build
569078f408 [maven-release-plugin][skip ci] prepare release 20.87 2023-02-24 07:28:08 +00:00
Damian Ujma
90de678d7d ACS-3750 Fix cancelled workflow condition (#1768) 2023-02-23 16:11:09 +01:00
alfresco-build
0a75cbbc67 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-22 11:38:44 +00:00
alfresco-build
c5f174a55d [maven-release-plugin][skip ci] prepare release 20.86 2023-02-22 11:38:42 +00:00
Piotr Żurek
119596647a ACS-4424 AGS Community tests for ES (#1761) 2023-02-22 12:08:30 +01:00
Piyush Joshi
e5d5969fc4 [ACS-3960][ACS-4339] Count is only returned for a tag if it is greater than zero (#1755)
* Added fix for ACS-3960

* Added fix for ACS-3960

* Added fix for ACS-3960

* Added formatting

* Added Fix for ACS-3960

* Revert "Added Fix for ACS-3960"

This reverts commit c06dba2d93.

* Addressed review comment

Co-authored-by: Tom Page <tpage-alfresco@users.noreply.github.com>

* Issue Fixed and added unit test cases.

* Removed extra assertion in testcases.

---------

Co-authored-by: suneet.gupta <suneet.gupta@hyland.com>
Co-authored-by: Tom Page <tpage-alfresco@users.noreply.github.com>
2023-02-22 10:21:59 +05:30
alfresco-build
b79dc538c9 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-19 00:17:09 +00:00
alfresco-build
72965efb57 [maven-release-plugin][skip ci] prepare release 20.85 2023-02-19 00:17:06 +00:00
Alfresco CI User
d467c3cdb1 [force] Force release for 2023-02-19. 2023-02-19 00:03:32 +00:00
alfresco-build
73d919435c [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-15 17:42:28 +00:00
alfresco-build
5fb86e1bbc [maven-release-plugin][skip ci] prepare release 20.84 2023-02-15 17:42:23 +00:00
Krystian Dabrowski
23de0fefd7 ACS-4633: Duplicated tag is not detected when tag string contains upper case (#1750) 2023-02-15 18:13:19 +01:00
Piyush Joshi
7c3e8a2549 [MNT-22633] Creation, Modification TimeStamp values setted for download as zip (#1717)
* Creation, Modification date timestamp added to zip file

* Addressed review comments

* Addressed review comments

* Revert "Addressed review comments"

This reverts commit 58214ee427.

* Addressed review comments

* Revert "Addressed review comments"

This reverts commit a584f53023.

* Update ZipDownloadExporter.java

Addresed review comments

---------

Co-authored-by: suneet.gupta <suneet.gupta@hyland.com>
2023-02-15 21:45:46 +05:30
alfresco-build
6a3dbff290 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-13 12:39:27 +00:00
alfresco-build
0ca29a76a9 [maven-release-plugin][skip ci] prepare release 20.83 2023-02-13 12:39:23 +00:00
George Evangelopoulos
561e7330e3 ACS-4027: add E2Es for DELETE /tags/{tagsId} endpoint (#1743)
* ACS-4027: add E2Es and helper methods
2023-02-13 13:31:53 +02:00
alfresco-build
628b267a2b [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-12 00:06:42 +00:00
alfresco-build
ea9687ac33 [maven-release-plugin][skip ci] prepare release 20.82 2023-02-12 00:06:39 +00:00
Alfresco CI User
4292a3ce1c [force] Force release for 2023-02-12. 2023-02-12 00:03:14 +00:00
alfresco-build
09721dca4e [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-11 17:33:49 +00:00
alfresco-build
0e4020452c [maven-release-plugin][skip ci] prepare release 20.81 2023-02-11 17:33:46 +00:00
Krystian Dabrowski
96c1464b6c ACS-4026: Create orphaned tag with POST /tags (#1748)
* ACS-4026: Create orphaned tag with POST /tags
2023-02-10 16:51:22 +01:00
alfresco-build
9457e019ef [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-10 13:03:16 +00:00
alfresco-build
41d82cc5ce [maven-release-plugin][skip ci] prepare release 20.80 2023-02-10 13:03:13 +00:00
Suneet Gupta
439e8587cf Changed line separation formatting to unix (#1741) 2023-02-08 17:47:06 +05:30
George Evangelopoulos
a59234f47f ACS-4027: Introduce changes to support removing tag from all associated nodes (#1709)
* ACS-4027: Introduce changes to support removing tag from all associated nodes

* ACS-4027: fix implementation and add tests
2023-02-07 18:32:52 +02:00
Domenico Sibilio
c9522f299e ACS-3750 Use set Git token instead of cached one (#1736) 2023-02-07 16:20:18 +01:00
alfresco-build
30dd78a6ab [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-07 11:42:09 +00:00
alfresco-build
008134f5b0 [maven-release-plugin][skip ci] prepare release 20.79 2023-02-07 11:42:05 +00:00
Krystian Dabrowski
285ebc29a4 ACS-4154: Add category count to relevant CRUD endpoints for categories (#1724)
* ACS-4154: Add category count to relevant CRUD endpoints for categories
2023-02-07 11:14:35 +01:00
alfresco-build
9c6e8aad9c [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-05 00:06:49 +00:00
alfresco-build
915fe193e2 [maven-release-plugin][skip ci] prepare release 20.78 2023-02-05 00:06:46 +00:00
Alfresco CI User
a2756aa868 [force] Force release for 2023-02-05. 2023-02-05 00:03:19 +00:00
Domenico Sibilio
b28e02c6fa ACS-3750 Improve branch detection in if conditions (#1722) 2023-02-02 17:16:06 +01:00
alfresco-build
8d00a1ab61 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-02 12:45:11 +00:00
alfresco-build
a86ad3c973 [maven-release-plugin][skip ci] prepare release 20.77 2023-02-02 12:45:08 +00:00
Piotr Żurek
2cdbe6f306 ACS-4180 Add localized properties to the repo event (#1714) 2023-02-02 12:22:08 +01:00
Tom Page
556e1923d1 Merge pull request #1720 from Alfresco/feature/ACS-4155_FixWebscriptsForES
ACS-4155 Update webscripts since we only index full category path in ES.
2023-02-01 13:10:58 +00:00
Tom Page
22e8a506ff ACS-4155 We only index full category path in ES. 2023-02-01 12:19:50 +00:00
alfresco-build
f63c9ee8c4 [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-01 12:16:19 +00:00
alfresco-build
b5b9b9e2c4 [maven-release-plugin][skip ci] prepare release 20.76 2023-02-01 12:16:15 +00:00
pzurek
9e88a6562e Trigger CI 2023-02-01 10:44:42 +01:00
alfresco-build
dbddc08fcc [maven-release-plugin][skip ci] prepare for next development iteration 2023-02-01 09:08:32 +00:00
alfresco-build
9b5e69e070 [maven-release-plugin][skip ci] prepare release 20.75 2023-02-01 09:08:28 +00:00
pzurek
23f6f6fc90 [force] release for downstream projects 2023-02-01 10:04:39 +01:00
dependabot[bot]
9d8e93474c Bump postgresql from 42.5.0 to 42.5.2 (#1718) 2023-02-01 08:19:27 +00:00
alfresco-build
3d450e1d8d [maven-release-plugin][skip ci] prepare for next development iteration 2023-01-31 14:28:10 +00:00
alfresco-build
e587ef124e [maven-release-plugin][skip ci] prepare release 20.74 2023-01-31 14:28:06 +00:00
Domenico Sibilio
34c8f9e431 ACS-4455 Bump alfresco-build-tools to v1.33.0 (#1713) 2023-01-31 14:57:10 +01:00
alfresco-build
736cc74479 [maven-release-plugin][skip ci] prepare for next development iteration 2023-01-31 06:57:34 +00:00
107 changed files with 6324 additions and 2099 deletions

View File

@@ -35,8 +35,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Prepare maven cache and check compilation"
@@ -49,16 +49,16 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
((github.ref_name == 'master' || contains(github.ref_name, 'release/')) && github.event_name != 'pull_request') &&
((github.ref_name == 'master' || startsWith(github.ref_name, 'release/')) && github.event_name != 'pull_request') &&
!contains(github.event.head_commit.message, '[skip tests]') &&
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- uses: Alfresco/alfresco-build-tools/.github/actions/veracode@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/veracode@v1.33.0
continue-on-error: true
with:
srcclr-api-token: ${{ secrets.SRCCLR_API_TOKEN }}
@@ -75,8 +75,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run tests"
@@ -112,8 +112,8 @@ jobs:
REQUIRES_INSTALLED_ARTIFACTS: true
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -133,7 +133,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/')) &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/')) &&
github.event_name != 'pull_request' &&
!contains(github.event.head_commit.message, '[skip db]')) ||
contains(github.event.head_commit.message, '[db]')) &&
@@ -145,8 +145,8 @@ jobs:
version: ['10.2.18', '10.4', '10.5']
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: Run MariaDB ${{ matrix.version }} database
@@ -163,7 +163,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/') || github.event_name == 'pull_request') &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request') &&
!contains(github.event.head_commit.message, '[skip db]')) ||
contains(github.event.head_commit.message, '[latest db]') ||
contains(github.event.head_commit.message, '[db]')) &&
@@ -171,8 +171,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run MariaDB 10.6 database"
@@ -189,7 +189,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/') || github.event_name == 'pull_request') &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request') &&
!contains(github.event.head_commit.message, '[skip db]')) ||
contains(github.event.head_commit.message, '[latest db]') ||
contains(github.event.head_commit.message, '[db]')) &&
@@ -197,8 +197,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run MySQL 8 database"
@@ -215,15 +215,15 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/')) && github.event_name != 'pull_request' &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/')) && github.event_name != 'pull_request' &&
!contains(github.event.head_commit.message, '[skip db]')) ||
contains(github.event.head_commit.message, '[db]')) &&
!contains(github.event.head_commit.message, '[skip tests]') &&
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run PostgreSQL 13.7 database"
@@ -247,8 +247,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run PostgreSQL 14.4 database"
@@ -270,8 +270,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run ActiveMQ"
@@ -315,8 +315,8 @@ jobs:
mvn-options: '-Dindex.subsystem.name=solr6'
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Set transformers tag"
@@ -337,7 +337,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
!contains(github.event.head_commit.message, '[skip tas]')) ||
contains(github.event.head_commit.message, '[tas]')) &&
!contains(github.event.head_commit.message, '[skip tests]') &&
@@ -374,8 +374,8 @@ jobs:
REQUIRES_LOCAL_IMAGES: true
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -411,8 +411,8 @@ jobs:
!contains(github.event.head_commit.message, '[force]')
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- name: "Run Postgres 14.4 database"
@@ -427,7 +427,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
!contains(github.event.head_commit.message, '[skip ags]')) ||
contains(github.event.head_commit.message, '[ags]')) &&
!contains(github.event.head_commit.message, '[skip tests]') &&
@@ -440,8 +440,8 @@ jobs:
REQUIRES_INSTALLED_ARTIFACTS: true
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -458,7 +458,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
!contains(github.event.head_commit.message, '[skip ags]')) ||
contains(github.event.head_commit.message, '[ags on MySQL]')) &&
!contains(github.event.head_commit.message, '[skip tests]') &&
@@ -471,8 +471,8 @@ jobs:
REQUIRES_INSTALLED_ARTIFACTS: true
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |
@@ -489,7 +489,7 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare]
if: >
(((github.ref_name == 'master' || contains(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
(((github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request' ) &&
!contains(github.event.head_commit.message, '[skip ags]') && !contains(github.event.head_commit.message, '[skip tas]')) ||
(contains(github.event.head_commit.message, '[ags]') && contains(github.event.head_commit.message, '[tas]'))) &&
!contains(github.event.head_commit.message, '[skip tests]') &&
@@ -498,8 +498,8 @@ jobs:
REQUIRES_LOCAL_IMAGES: true
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Build"
timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }}
run: |

View File

@@ -27,16 +27,18 @@ jobs:
runs-on: ubuntu-latest
needs: [run_ci]
if: >
!failure() &&
!(failure() || cancelled()) &&
!contains(github.event.head_commit.message, '[no release]') &&
github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
with:
persist-credentials: false
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- uses: Alfresco/alfresco-build-tools/.github/actions/configure-git-author@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/configure-git-author@v1.33.0
with:
username: ${{ env.GIT_USERNAME }}
email: ${{ env.GIT_EMAIL }}
@@ -53,16 +55,18 @@ jobs:
runs-on: ubuntu-latest
needs: [push_to_nexus]
if: >
!failure() &&
!(failure() || cancelled()) &&
!contains(github.event.head_commit.message, '[no downstream]') &&
github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v3
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.32.0
with:
persist-credentials: false
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v1.33.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v1.33.0
- name: "Init"
run: bash ./scripts/ci/init.sh
- uses: Alfresco/alfresco-build-tools/.github/actions/configure-git-author@v1.32.0
- uses: Alfresco/alfresco-build-tools/.github/actions/configure-git-author@v1.33.0
with:
username: ${{ env.GIT_USERNAME }}
email: ${{ env.GIT_EMAIL }}
@@ -72,4 +76,4 @@ jobs:
env:
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
- name: "Clean Maven cache"
run: bash ./scripts/ci/cleanup_cache.sh
run: bash ./scripts/ci/cleanup_cache.sh

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-automation-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<build>

View File

@@ -33,7 +33,9 @@ import org.alfresco.rest.rm.community.model.record.Record;
import org.alfresco.rest.v0.RecordsAPI;
import org.alfresco.utility.Utility;
import org.alfresco.utility.constants.UserRole;
import org.alfresco.utility.data.RandomData;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.FileType;
import org.alfresco.utility.model.SiteModel;
import org.alfresco.utility.model.UserModel;
import org.json.JSONObject;
@@ -97,9 +99,10 @@ public class InplaceRecordSearchTests extends BaseRMRestTest {
@Test
public void searchForInplaceRecord() {
// And a document that isn't a record
final String fileName = "File" + RandomData.getRandomAlphanumeric();
uploadedDocbyCollabUser = dataContent.usingSite(privateSite)
.usingUser(siteCollaborator)
.createContent(CMISUtil.DocumentType.TEXT_PLAIN);
.createContent(new FileModel(fileName, FileType.fromName(fileName + "." + CMISUtil.DocumentType.TEXT_PLAIN.extention)));
assertNotNull(uploadedDocbyCollabUser.getNodeRef());

View File

@@ -72,7 +72,7 @@ public class SearchDocumentsV1Test extends BaseRMRestTest
cmisQueryModel.setLanguage("cmis");
RestRequestQueryModel aftsQueryModel = new RestRequestQueryModel();
aftsQueryModel.setQuery("cm:name:*" + SEARCH_TERM);
aftsQueryModel.setQuery("cm:name:*" + SEARCH_TERM + ".txt");
aftsQueryModel.setLanguage("afts");
return new RestRequestQueryModel[][] {
@@ -107,7 +107,7 @@ public class SearchDocumentsV1Test extends BaseRMRestTest
private void waitIndexing() throws Exception
{
RestRequestQueryModel queryType = new RestRequestQueryModel();
queryType.setQuery("cm:name:*" + SEARCH_TERM);
queryType.setQuery("cm:name:*" + SEARCH_TERM + ".txt");
queryType.setLanguage("afts");
Utility.sleep(1000, 80000, () ->
{

View File

@@ -66,7 +66,6 @@ import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanCo
import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.CUT_OFF_ASPECT;
import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.*;
import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -84,26 +83,29 @@ public class DispositionScheduleLinkedRecordsTest extends BaseRMRestTest {
@Autowired
private RecordFoldersAPI recordFoldersAPI;
private final static String TEST_PREFIX = generateTestPrefix(DispositionScheduleLinkedRecordsTest.class);
private RecordCategory Category1,catsameLevel1,catsameLevel2;
private RecordCategory Category1;
private RecordCategoryChild CopyCatFolder,folder1,CatFolder,folder2;
private static final String categoryRM3077 = TEST_PREFIX + "RM-3077_manager_sees_me";
private static final String copyCategoryRM3077 = "Copy_of_" + categoryRM3077;
private static final String folderRM3077 = "RM-3077_folder_"+ categoryRM3077;
private static final String copyFolderRM3077 = "Copy_of_" + folderRM3077;
private final String electronicRecord = "RM-2937 electronic 2 record";
private final String folder = TEST_PREFIX + "RM-2937 folder ghosting";
private static final String categoryRecordsRM2526 = TEST_PREFIX + "RM-2526_category_records_immediately";
private static final String category2RecordsRM2526 = TEST_PREFIX + "RM-2526_category_2_records_1_day";
private static final String firstCategoryRM3060 = TEST_PREFIX + "RM-3060_category_record";
private static final String secondCategoryRM3060 = "Copy_of_" + firstCategoryRM3060;
private static final String firstFolderRM3060 = TEST_PREFIX + "RM-3060_folder";
private static final String secondFolderRM3060 = TEST_PREFIX + "RM-3060_disposition_on_Record_Level";
private static final String electronicRecordRM3060 = TEST_PREFIX + "RM-3060_electronic_1_record";
private static final String nonElectronicRecordRM3060 = TEST_PREFIX + "RM-3060_non-electronic_record";
private static final String firstCategoryRM1622 = TEST_PREFIX + "RM-1622_category_record";
private static final String secondCategoryRM1622 = "Copy_of_" + firstCategoryRM1622;;
private static final String firstFolderRM1622 = TEST_PREFIX + "RM-1622_folder";
private static final String electronicRecordRM1622 = TEST_PREFIX + "RM-1622_electronic_1_record";
private static final String secondFolderRM1622 = TEST_PREFIX + "RM-1622_disposition_on_Record_Level";
private static final String TRANSFER_LOCATION = TEST_PREFIX + "RM-3060_transferred_records";
public static final String TRANSFER_TYPE = "rma:transferred";
private FilePlan filePlanModel;
private UserModel rmAdmin, rmManager;
@BeforeClass(alwaysRun = true)
public void setupDispositionScheduleLinkedRecordsTest() {
createRMSiteIfNotExists();
@@ -368,35 +370,35 @@ public class DispositionScheduleLinkedRecordsTest extends BaseRMRestTest {
// create a category with retention applied on records level
RecordCategory catsameLevel1 = getRestAPIFactory().getFilePlansAPI(rmAdmin)
.createRootRecordCategory(RecordCategory.builder().name(firstCategoryRM3060).build(),
RecordCategory.DEFAULT_FILE_PLAN_ALIAS);
.createRootRecordCategory(RecordCategory.builder().name(firstCategoryRM1622).build(),
RecordCategory.DEFAULT_FILE_PLAN_ALIAS);
RecordCategory catsameLevel2 = getRestAPIFactory().getFilePlansAPI(rmAdmin)
.createRootRecordCategory(RecordCategory.builder().name(secondCategoryRM3060).build(),
RecordCategory.DEFAULT_FILE_PLAN_ALIAS);
.createRootRecordCategory(RecordCategory.builder().name(secondCategoryRM1622).build(),
RecordCategory.DEFAULT_FILE_PLAN_ALIAS);
// create retention schedule applied on records for category 1
dispositionScheduleService.createCategoryRetentionSchedule(firstCategoryRM3060, true);
dispositionScheduleService.createCategoryRetentionSchedule(firstCategoryRM1622, true);
// with retain immediately after record creation date and cut 1 day after record creation date
dispositionScheduleService.addCutOffAfterPeriodStep(firstCategoryRM3060, "day|1", DATE_FILED);
dispositionScheduleService.addCutOffAfterPeriodStep(firstCategoryRM1622, "day|1", DATE_FILED);
// create a folder on the category firstCategoryRM3060 with a complete electronic record
RecordCategoryChild firstFolderRecordCategoryChild = createRecordFolder(catsameLevel1.getId(),firstFolderRM3060);
Record firstElectronicRecord = createElectronicRecord(firstFolderRecordCategoryChild.getId(),electronicRecordRM3060);
// create a folder on the category firstCategoryRM1622 with a complete electronic record
RecordCategoryChild firstFolderRecordCategoryChild = createRecordFolder(catsameLevel1.getId(),firstFolderRM1622);
Record firstElectronicRecord = createElectronicRecord(firstFolderRecordCategoryChild.getId(),electronicRecordRM1622);
String elRecordFullName = recordsAPI.getRecordFullName(getDataUser().getAdminUser().getUsername(),
getDataUser().getAdminUser().getPassword(),firstFolderRM3060, electronicRecordRM3060);
getDataUser().getAdminUser().getPassword(),firstFolderRM1622, electronicRecordRM1622);
String elRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(),
getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM3060 + "/" + firstFolderRM3060);
getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM1622 + "/" + firstFolderRM1622);
recordsAPI.completeRecord(getDataUser().getAdminUser().getUsername(),
getDataUser().getAdminUser().getPassword(), elRecordFullName);
// create a folder on the category secondCategoryRM3060 with a non electronic record
RecordCategoryChild secondFolderRecordCategoryChild = createRecordFolder(catsameLevel2.getId(),secondFolderRM3060);
// create a folder on the category secondCategoryRM1622 with a non electronic record
RecordCategoryChild secondFolderRecordCategoryChild = createRecordFolder(catsameLevel2.getId(),secondFolderRM1622);
String elRecordNameNodeRefs = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(),
getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM3060 + "/" + firstFolderRM3060);
getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM1622 + "/" + firstFolderRM1622);
// link it to the folder in second category through the details page
@@ -404,8 +406,8 @@ public class DispositionScheduleLinkedRecordsTest extends BaseRMRestTest {
recordLists.add(NODE_REF_WORKSPACE_SPACES_STORE + firstElectronicRecord.getId());
linksAPI.linkRecord(getDataUser().getAdminUser().getUsername(),
getDataUser().getAdminUser().getPassword(), HttpStatus.SC_OK,secondCategoryRM3060 + "/" +
secondFolderRM3060, recordLists);
getDataUser().getAdminUser().getPassword(), HttpStatus.SC_OK,secondCategoryRM1622 + "/" +
secondFolderRM1622, recordLists);
// edit disposition date
recordFoldersAPI.postRecordAction(getAdminUser().getUsername(),

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-parent</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<modules>

View File

@@ -1,3 +1,4 @@
ARG BASE_IMAGE
# BUILD STAGE AGS
FROM debian:11-slim AS AGSBUILDER
@@ -12,7 +13,7 @@ RUN unzip -q /build/gs-api-explorer-*.war -d /build/gs-api-explorer && \
chmod -R g-w,o= /build
# ACTUAL IMAGE
FROM alfresco/alfresco-community-repo-base:${image.tag}
FROM ${BASE_IMAGE}
# Alfresco user does not have permissions to modify webapps or configuration. Switch to root.
# The access will be fixed after all operations are done.

View File

@@ -8,13 +8,15 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<properties>
<app.amp.client.war.folder>${project.build.directory}/${project.build.finalName}-war</app.amp.client.war.folder>
<image.name>alfresco/alfresco-governance-repository-community-base</image.name>
<base.image>alfresco/alfresco-community-repo-base</base.image>
<scripts.directory>${project.parent.parent.parent.parent.basedir}/scripts</scripts.directory>
</properties>
<dependencies>
@@ -537,9 +539,43 @@
</build>
</profile>
<profile>
<id>build-docker-images</id>
<!-- builds "image:latest" locally -->
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<image>
<name>${image.name}:${image.tag}</name>
<build>
<args>
<BASE_IMAGE>${base.image}:${image.tag}</BASE_IMAGE>
</args>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>build-docker-images</id>
<!-- builds "image:latest" locally -->
<id>build-multiarch-docker-images</id>
<build>
<plugins>
<plugin>
@@ -548,20 +584,56 @@
<configuration>
<images>
<image>
<name>${image.name}:${image.tag}</name>
<name>${local.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<contextDir>${project.basedir}</contextDir>
<args>
<BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
</args>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-image</id>
<id>build-push-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>prepare-buildx</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${scripts.directory}/prepare_buildx.sh</executable>
<arguments>
<argument>${builder.name}</argument>
<argument>${image.registry}</argument>
<argument>${image.name}</argument>
<argument>${image.tag}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
@@ -578,12 +650,37 @@
<images>
<image>
<!-- Quay image -->
<name>${image.name}:${image.tag}</name>
<registry>${image.registry}</registry>
<name>${image.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<args>
<BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
</args>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
<image>
<!-- DockerHub image -->
<name>${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<args>
<BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
</args>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
@@ -598,6 +695,28 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>prepare-buildx</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${scripts.directory}/prepare_buildx.sh</executable>
<arguments>
<argument>${builder.name}</argument>
<argument>${image.registry}</argument>
<argument>${image.name}</argument>
<argument>${image.tag}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-governance-services-community-repo-parent</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<build>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<modules>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-amps</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<properties>

View File

@@ -241,7 +241,7 @@ var Filters =
filterData = filterData.slice(0, -1);
}
filterQuery = this.constructPathQuery(parsedArgs);
filterParams.query = filterQuery + " +PATH:\"/cm:generalclassifiable" + Filters.iso9075EncodePath(filterData) + "/member\"";
filterParams.query = filterQuery + " +PATH:\"/cm:categoryRoot/cm:generalclassifiable" + Filters.iso9075EncodePath(filterData) + "/member\"";
break;
case "aspect":

View File

@@ -230,7 +230,7 @@ var Filters =
filterData = filterData.slice(0, -1);
}
filterQuery = this.constructPathQuery(parsedArgs);
filterParams.query = filterQuery + " +PATH:\"/cm:generalclassifiable" + Filters.iso9075EncodePath(filterData) + "/member\"";
filterParams.query = filterQuery + " +PATH:\"/cm:categoryRoot/cm:generalclassifiable" + Filters.iso9075EncodePath(filterData) + "/member\"";
break;
default: // "path"

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<dependencies>

View File

@@ -45,6 +45,13 @@ public class ListBackedPagingResults<R> implements PagingResults<R>
size = list.size();
hasMore = false;
}
public ListBackedPagingResults(List<R> list, boolean hasMore)
{
this(list);
this.hasMore = hasMore;
}
public ListBackedPagingResults(List<R> list, PagingRequest paging)
{
// Excerpt

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<properties>

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Data model classes
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -51,6 +51,18 @@ public class MLText extends HashMap<Locale, String>
{
private static final long serialVersionUID = -3696135175650511841L;
/**
* Returns default locale used by the {@link MLText} implementation
*
* @see I18NUtil#getLocale()
*
* @return default locale
*/
public static Locale getDefaultLocale()
{
return I18NUtil.getLocale();
}
public MLText()
{
super(3, 0.75F);
@@ -61,13 +73,13 @@ public class MLText extends HashMap<Locale, String>
*
* @param value the value for the current default locale
*
* @see I18NUtil#getLocale()
* @see #getDefaultLocale()
* @see #MLText(Locale, String)
* @see #getDefaultValue()
*/
public MLText(String value)
{
this(I18NUtil.getLocale(), value);
this(getDefaultLocale(), value);
}
/**
@@ -124,7 +136,7 @@ public class MLText extends HashMap<Locale, String>
/**
* Retrieves a default value from the set of available locales.<br/>
*
* @see I18NUtil#getLocale()
* @see #getDefaultLocale()
* @see #getClosestValue(Locale)
*/
public String getDefaultValue()
@@ -135,7 +147,7 @@ public class MLText extends HashMap<Locale, String>
return null;
}
// There is some hope of getting a match
Locale locale = I18NUtil.getLocale();
Locale locale = getDefaultLocale();
return getClosestValue(locale);
}
@@ -168,7 +180,7 @@ public class MLText extends HashMap<Locale, String>
if (match == null)
{
// No close matches for the locale - go for the default locale
locale = I18NUtil.getLocale();
locale = getDefaultLocale();
match = I18NUtil.getNearestLocale(locale, options);
if (match == null)
{

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<dependencies>

View File

@@ -9,6 +9,6 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
</project>

View File

@@ -1,6 +1,6 @@
# Fetch image based on Tomcat 9.0, Java 17 and Rocky Linux 8
# More infos about this image: https://github.com/Alfresco/alfresco-docker-base-tomcat
FROM alfresco/alfresco-base-tomcat:tomcat9-jre17-rockylinux8-202209261711
FROM alfresco/alfresco-base-tomcat:tomcat9-jre17-rockylinux8-202303081618
# Set default docker_context.
ARG resource_path=target
@@ -65,7 +65,7 @@ RUN sed -i -e "s_appender.rolling.fileName\=alfresco.log_appender.rolling.fileNa
RUN yum install -y fontconfig-2.13.1-4.el8 \
dejavu-fonts-common-2.35-7.el8 \
fontpackages-filesystem-1.44-22.el8 \
freetype-2.9.1-4.el8_3.1 \
freetype-2.9.1-9.el8 \
libpng-1.6.34-5.el8 \
dejavu-sans-fonts-2.35-7.el8 && \
yum clean all

View File

@@ -7,11 +7,12 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<properties>
<image.name>alfresco/alfresco-community-repo-base</image.name>
<scripts.directory>${project.parent.parent.basedir}/scripts</scripts.directory>
</properties>
<build>
@@ -156,6 +157,67 @@
</build>
</profile>
<profile>
<id>build-multiarch-docker-images</id>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<image>
<name>${local.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
<builderName>${builder.name}</builderName>
</buildx>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-push-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>prepare-buildx</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${scripts.directory}/prepare_buildx.sh</executable>
<arguments>
<argument>${builder.name}</argument>
<argument>${image.registry}</argument>
<argument>${image.name}</argument>
<argument>${image.tag}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>push-docker-images</id>
<!-- publishes "image:latest" on Quay & DockerHub -->
@@ -168,12 +230,29 @@
<images>
<!-- Quay image -->
<image>
<name>${image.name}:${image.tag}</name>
<registry>${image.registry}</registry>
<name>${image.registry}/${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
</buildx>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
<!-- DockerHub image -->
<image>
<name>${image.name}:${image.tag}</name>
<build>
<buildx>
<platforms>
<platform>linux/amd64</platform>
<platform>linux/arm64</platform>
</platforms>
</buildx>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<modules>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<modules>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<organization>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<developers>

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<developers>
@@ -95,7 +95,6 @@
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${dependency.jakarta-json-path.version}</version>
</dependency>
</dependencies>

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<properties>
@@ -165,14 +165,14 @@
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.12</version>
<version>3.0.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-json-->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-json</artifactId>
<version>3.0.12</version>
<version>3.0.16</version>
</dependency>
<dependency>

View File

@@ -30,7 +30,10 @@ import static org.alfresco.utility.report.log.Step.STEP;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.alfresco.rest.core.IRestModelsCollection;
import org.alfresco.utility.exception.TestConfigurationException;
@@ -117,7 +120,7 @@ public class ModelsCollectionAssertion<C>
return (C) modelCollection;
}
@SuppressWarnings("unchecked")
@SuppressWarnings("unchecked")
public C entriesListDoesNotContain(String key, String value)
{
boolean exist = false;
@@ -143,6 +146,53 @@ public class ModelsCollectionAssertion<C>
return (C) modelCollection;
}
public C entrySetContains(String key, String... expectedValues)
{
return entrySetContains(key, Arrays.stream(expectedValues).collect(Collectors.toSet()));
}
@SuppressWarnings("unchecked")
public C entrySetContains(String key, Collection<String> expectedValues)
{
Collection<String> actualValues = ((List<Model>) modelCollection.getEntries()).stream()
.map(model -> extractValueAsString(model, key))
.collect(Collectors.toSet());
Assert.assertTrue(actualValues.containsAll(expectedValues), String.format("Entry with key: \"%s\" is expected to contain values: %s, but actual values are: %s",
key, expectedValues, actualValues));
return (C) modelCollection;
}
@SuppressWarnings("unchecked")
public C entrySetMatches(String key, Collection<String> expectedValues)
{
Collection<String> actualValues = ((List<Model>) modelCollection.getEntries()).stream()
.map(model -> extractValueAsString(model, key))
.collect(Collectors.toSet());
Assert.assertEqualsNoOrder(actualValues, expectedValues, String.format("Entry with key: \"%s\" is expected to match values: %s, but actual values are: %s",
key, expectedValues, actualValues));
return (C) modelCollection;
}
private String extractValueAsString(Model model, String key)
{
String fieldValue;
Object modelObject = loadModel(model);
try {
ObjectMapper mapper = new ObjectMapper();
String jsonInString = mapper.writeValueAsString(modelObject);
fieldValue = JsonPath.with(jsonInString).get(key);
} catch (Exception e) {
throw new TestConfigurationException(String.format(
"You try to assert field [%s] that doesn't exist in class: [%s]. Exception: %s, Please check your code!",
key, getClass().getCanonicalName(), e.getMessage()));
}
return fieldValue;
}
@SuppressWarnings("unchecked")
public C entriesListDoesNotContain(String key)
{

View File

@@ -2,7 +2,7 @@
* #%L
* alfresco-tas-restapi
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -27,6 +27,8 @@ package org.alfresco.rest.model;
import static org.alfresco.utility.report.log.Step.STEP;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.alfresco.rest.core.IRestModel;
@@ -73,4 +75,65 @@ public class RestTagModel extends TagModel implements IRestModel<RestTagModel>
this.count = count;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
RestTagModel tagModel = (RestTagModel) o;
return Objects.equals(id, tagModel.id) && Objects.equals(tag, tagModel.tag) && Objects.equals(count, tagModel.count);
}
@Override
public int hashCode()
{
return Objects.hash(id, tag, count);
}
@Override
public String toString()
{
return "RestTagModel{" + "id='" + id + ", tag='" + tag + '\'' + ", count=" + count + '\'' + '}';
}
public static Builder builder()
{
return new Builder();
}
public static class Builder
{
private String id;
private String tag;
private Integer count;
public Builder id(String id)
{
this.id = id;
return this;
}
public Builder tag(String tag)
{
this.tag = tag;
return this;
}
public Builder count(Integer count)
{
this.count = count;
return this;
}
public RestTagModel create()
{
final RestTagModel tag = new RestTagModel();
tag.setId(id);
tag.setTag(this.tag);
tag.setCount(count);
return tag;
}
}
}

View File

@@ -82,4 +82,15 @@ public class Tags extends ModelRequest<Tags>
return restWrapper.processModel(RestTagModel.class, request);
}
/**
* Delete tag.
* - DELETE /tags/{tagId}
*/
public void deleteTag()
{
RestRequest request = RestRequest.
simpleRequest(HttpMethod.DELETE, "/tags/{tagId}", tag.getId());
restWrapper.processEmptyModel(request);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* alfresco-tas-restapi
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,12 @@
*/
package org.alfresco.rest.requests.coreAPI;
import static org.alfresco.rest.core.JsonBodyGenerator.arrayToJson;
import java.util.List;
import io.restassured.RestAssured;
import org.alfresco.rest.core.RestRequest;
import org.alfresco.rest.core.RestWrapper;
import org.alfresco.rest.model.RestCategoryModel;
import org.alfresco.rest.model.RestDownloadsModel;
@@ -49,8 +55,7 @@ import org.alfresco.rest.requests.Trashcan;
import org.alfresco.utility.model.RepoTestModel;
import org.alfresco.utility.model.SiteModel;
import org.alfresco.utility.model.UserModel;
import io.restassured.RestAssured;
import org.springframework.http.HttpMethod;
/**
* Defines the entire Rest Core API
@@ -172,6 +177,30 @@ public class RestCoreAPI extends ModelRequest<RestCoreAPI>
return new Networks(restWrapper);
}
/**
* Create a single orphan tag.
*
* @param tag Tag model to create.
* @return Created tag.
*/
public RestTagModel createSingleTag(RestTagModel tag)
{
RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, tag.toJson(), "tags/");
return restWrapper.processModel(RestTagModel.class, request);
}
/**
* Create several orphan tags in one request.
*
* @param tags Tags models to create.
* @return Created tags.
*/
public RestTagModelsCollection createTags(List<RestTagModel> tags)
{
RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, arrayToJson(tags), "tags/");
return restWrapper.processModels(RestTagModelsCollection.class, request);
}
public Tags usingTag(RestTagModel tag)
{
return new Tags(tag, restWrapper);

View File

@@ -0,0 +1,207 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.categories;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
import static org.testng.Assert.assertTrue;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.model.RestCategoryModel;
import org.alfresco.rest.model.RestCategoryModelsCollection;
import org.alfresco.utility.Utility;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.FolderModel;
import org.alfresco.utility.model.SiteModel;
import org.alfresco.utility.model.TestGroup;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class CategoriesCountTests extends CategoriesRestTest
{
private RestCategoryModel categoryLinkedWithFolder;
private RestCategoryModel categoryLinkedWithFile;
private RestCategoryModel categoryLinkedWithBoth;
private RestCategoryModel notLinkedCategory;
@BeforeClass(alwaysRun = true)
public void dataPreparation() throws Exception
{
STEP("Create user and site");
user = dataUser.createRandomTestUser();
SiteModel site = dataSite.usingUser(user).createPublicRandomSite();
STEP("Create a folder, file in it and few categories");
FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder();
FileModel file = dataContent.usingUser(user).usingResource(folder).createContent(CMISUtil.DocumentType.TEXT_PLAIN);
categoryLinkedWithFolder = prepareCategoryUnderRoot();
categoryLinkedWithFile = prepareCategoryUnderRoot();
categoryLinkedWithBoth = prepareCategoryUnder(prepareCategoryUnderRoot());
notLinkedCategory = prepareCategoryUnderRoot();
STEP("Link folder and file to categories");
linkContentToCategories(folder, categoryLinkedWithFolder, categoryLinkedWithBoth);
linkContentToCategories(file, categoryLinkedWithFile, categoryLinkedWithBoth);
STEP("Wait for indexing to complete");
Utility.sleep(1000, 60000, () -> restClient.authenticateUser(user)
.withCoreAPI()
.usingCategory(categoryLinkedWithFolder)
.include(INCLUDE_COUNT_PARAM)
.getCategory()
.assertThat()
.field(FIELD_COUNT)
.isNot(0));
}
/**
* Verify count for a category linked with file and folder.
*/
@Test(groups = { TestGroup.REST_API })
public void testGetCategoryById_includeCount()
{
STEP("Get linked category and verify if count is higher than 0");
final RestCategoryModel actualCategory = restClient.authenticateUser(user)
.withCoreAPI()
.usingCategory(categoryLinkedWithBoth)
.include(INCLUDE_COUNT_PARAM)
.getCategory();
restClient.assertStatusCodeIs(OK);
actualCategory.assertThat().field(FIELD_ID).is(categoryLinkedWithBoth.getId());
actualCategory.assertThat().field(FIELD_COUNT).is(2);
}
/**
* Verify count for a category not linked with any content.
*/
@Test(groups = { TestGroup.REST_API })
public void testGetCategoryById_includeCountForNonLinkedCategory()
{
STEP("Get non-linked category and verify if count is 0");
final RestCategoryModel actualCategory = restClient.authenticateUser(user)
.withCoreAPI()
.usingCategory(notLinkedCategory)
.include(INCLUDE_COUNT_PARAM)
.getCategory();
restClient.assertStatusCodeIs(OK);
actualCategory.assertThat().field(FIELD_ID).is(notLinkedCategory.getId());
actualCategory.assertThat().field(FIELD_COUNT).is(0);
}
/**
* Verify count for three categories: linked with file, linked with folder and third not linked to any content.
*/
@Test(groups = { TestGroup.REST_API })
public void testGetCategories_includeCount()
{
STEP("Get few categories and verify its counts");
final RestCategoryModel parentCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModelsCollection actualCategories = restClient.authenticateUser(user)
.withCoreAPI()
.usingCategory(parentCategory)
.include(INCLUDE_COUNT_PARAM)
.getCategoryChildren();
restClient.assertStatusCodeIs(OK);
assertTrue(actualCategories.getEntries().stream()
.map(RestCategoryModel::onModel)
.anyMatch(category -> category.getId().equals(categoryLinkedWithFolder.getId()) && category.getCount() == 1));
assertTrue(actualCategories.getEntries().stream()
.map(RestCategoryModel::onModel)
.anyMatch(category -> category.getId().equals(categoryLinkedWithFile.getId()) && category.getCount() == 1));
assertTrue(actualCategories.getEntries().stream()
.map(RestCategoryModel::onModel)
.anyMatch(category -> category.getId().equals(notLinkedCategory.getId()) && category.getCount() == 0));
}
/**
* Create category and verify that its count is 0.
*/
@Test(groups = { TestGroup.REST_API })
public void testCreateCategory_includingCount()
{
STEP("Create a category under root and verify if count is 0");
final String categoryName = getRandomName("Category");
final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModel aCategory = createCategoryModelWithName(categoryName);
final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.include(INCLUDE_COUNT_PARAM)
.usingCategory(rootCategory)
.createSingleCategory(aCategory);
STEP("Create a category under root category (as admin)");
restClient.assertStatusCodeIs(CREATED);
createdCategory.assertThat().field(FIELD_NAME).is(categoryName);
createdCategory.assertThat().field(FIELD_COUNT).is(0);
}
/**
* Update category linked to file and folder and verify that its count is 2.
*/
@Test(groups = { TestGroup.REST_API })
public void testUpdateCategory_includeCount()
{
STEP("Update linked category and verify if count is higher than 0");
final String categoryNewName = getRandomName("NewCategoryName");
final RestCategoryModel fixedCategoryModel = createCategoryModelWithName(categoryNewName);
final RestCategoryModel updatedCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(categoryLinkedWithBoth)
.include(INCLUDE_COUNT_PARAM)
.updateCategory(fixedCategoryModel);
restClient.assertStatusCodeIs(OK);
updatedCategory.assertThat().field(FIELD_ID).is(categoryLinkedWithBoth.getId());
updatedCategory.assertThat().field(FIELD_COUNT).is(2);
}
/**
* Update category not linked to any content and verify that its count is 0.
*/
@Test(groups = { TestGroup.REST_API })
public void testUpdateCategory_includeCountForNonLinkedCategory()
{
STEP("Update non-linked category and verify if count is 0");
final String categoryNewName = getRandomName("NewCategoryName");
final RestCategoryModel fixedCategoryModel = createCategoryModelWithName(categoryNewName);
final RestCategoryModel updatedCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(notLinkedCategory)
.include(INCLUDE_COUNT_PARAM)
.updateCategory(fixedCategoryModel);
restClient.assertStatusCodeIs(OK);
updatedCategory.assertThat().field(FIELD_ID).is(notLinkedCategory.getId());
updatedCategory.assertThat().field(FIELD_COUNT).is(0);
}
}

View File

@@ -30,6 +30,7 @@ import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.CREATED;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -37,18 +38,21 @@ import java.util.stream.IntStream;
import org.alfresco.rest.RestTest;
import org.alfresco.rest.model.RestCategoryLinkBodyModel;
import org.alfresco.rest.model.RestCategoryModel;
import org.alfresco.rest.model.RestCategoryModelsCollection;
import org.alfresco.utility.model.RepoTestModel;
import org.alfresco.utility.model.UserModel;
import org.testng.annotations.BeforeClass;
abstract class CategoriesRestTest extends RestTest
{
protected static final String INCLUDE_COUNT_PARAM = "count";
protected static final String ROOT_CATEGORY_ID = "-root-";
protected static final String CATEGORY_NAME_PREFIX = "CategoryName";
protected static final String FIELD_NAME = "name";
protected static final String FIELD_ID = "id";
protected static final String FIELD_PARENT_ID = "parentId";
protected static final String FIELD_HAS_CHILDREN = "hasChildren";
protected static final String FIELD_COUNT = "count";
protected UserModel user;
@@ -59,14 +63,26 @@ abstract class CategoriesRestTest extends RestTest
user = dataUser.createRandomTestUser();
}
protected RestCategoryModel prepareCategoryUnderRoot()
protected RestCategoryModelsCollection linkContentToCategories(final RepoTestModel node, final RestCategoryModel... categories)
{
return prepareCategoryUnder(ROOT_CATEGORY_ID);
final List<RestCategoryLinkBodyModel> categoryLinkModels = Arrays.stream(categories)
.map(RestCategoryModel::getId)
.map(this::createCategoryLinkModelWithId)
.collect(Collectors.toList());
final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(node).linkToCategories(categoryLinkModels);
restClient.assertStatusCodeIs(CREATED);
return linkedCategories;
}
protected RestCategoryModel prepareCategoryUnder(final String parentId)
protected RestCategoryModel prepareCategoryUnderRoot()
{
return prepareCategoryUnder(createCategoryModelWithId(ROOT_CATEGORY_ID));
}
protected RestCategoryModel prepareCategoryUnder(final RestCategoryModel parentCategory)
{
final RestCategoryModel parentCategory = createCategoryModelWithId(parentId);
final RestCategoryModel categoryModel = createCategoryModelWithName(getRandomName(CATEGORY_NAME_PREFIX));
final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,6 +26,7 @@
package org.alfresco.rest.categories;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CREATED;
@@ -38,7 +39,6 @@ import java.util.stream.IntStream;
import org.alfresco.rest.model.RestCategoryModel;
import org.alfresco.rest.model.RestCategoryModelsCollection;
import org.alfresco.utility.data.RandomData;
import org.alfresco.utility.model.FolderModel;
import org.alfresco.utility.model.SiteModel;
import org.alfresco.utility.model.TestGroup;
@@ -56,7 +56,7 @@ public class CreateCategoriesTests extends CategoriesRestTest
{
STEP("Create a category under root category (as admin)");
final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModel aCategory = createCategoryModelWithName(RandomData.getRandomName("Category"));
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(rootCategory)
@@ -92,7 +92,7 @@ public class CreateCategoriesTests extends CategoriesRestTest
{
STEP("Create a category under root category (as admin)");
final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModel aCategory = createCategoryModelWithName(RandomData.getRandomName("Category"));
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(rootCategory)
@@ -140,7 +140,7 @@ public class CreateCategoriesTests extends CategoriesRestTest
{
STEP("Create a category under root category (as admin)");
final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModel aCategory = createCategoryModelWithName(RandomData.getRandomName("Category"));
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(rootCategory)
@@ -186,7 +186,7 @@ public class CreateCategoriesTests extends CategoriesRestTest
{
STEP("Create a category under root category (as user)");
final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModel aCategory = createCategoryModelWithName(RandomData.getRandomName("Category"));
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
restClient.authenticateUser(user)
.withCoreAPI()
.usingCategory(rootCategory)
@@ -203,7 +203,7 @@ public class CreateCategoriesTests extends CategoriesRestTest
STEP("Create a category under non existing category node (as admin)");
final String id = "non-existing-node-id";
final RestCategoryModel rootCategory = createCategoryModelWithId(id);
final RestCategoryModel aCategory = createCategoryModelWithName(RandomData.getRandomName("Category"));
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(rootCategory)
@@ -223,7 +223,7 @@ public class CreateCategoriesTests extends CategoriesRestTest
STEP("Create a category under folder node (as admin)");
final RestCategoryModel rootCategory = createCategoryModelWithId(folder.getNodeRef());
final RestCategoryModel aCategory = createCategoryModelWithName(RandomData.getRandomName("Category"));
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(rootCategory)
@@ -231,10 +231,31 @@ public class CreateCategoriesTests extends CategoriesRestTest
restClient.assertStatusCodeIs(BAD_REQUEST).assertLastError().containsSummary("Node id does not refer to a valid category");
}
/**
* Check weather count present in create category request will be ignored.
*/
@Test(groups = { TestGroup.REST_API })
public void testCreateCategoryUnderRoot_verifyIfCountInRequestIsIgnored()
{
STEP("Try to create a category with filled count under root");
final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID);
final RestCategoryModel aCategory = createCategoryModelWithName(getRandomName("Category"));
aCategory.setCount(2);
final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(rootCategory)
.include(INCLUDE_COUNT_PARAM)
.createSingleCategory(aCategory);
restClient.assertStatusCodeIs(CREATED);
createdCategory.assertThat().field(FIELD_NAME).is(aCategory.getName());
createdCategory.assertThat().field(FIELD_COUNT).is(0);
}
static List<RestCategoryModel> getCategoriesToCreate(final int count)
{
return IntStream.range(0, count)
.mapToObj(i -> RestCategoryModel.builder().name(RandomData.getRandomName("SubCategory")).create())
.mapToObj(i -> RestCategoryModel.builder().name(getRandomName("SubCategory")).create())
.collect(Collectors.toList());
}
}

View File

@@ -79,7 +79,7 @@ public class UpdateCategoriesTests extends CategoriesRestTest
final RestCategoryModel createdCategory = prepareCategoryUnderRoot();
STEP("Prepare as admin a subcategory of root's child category");
final RestCategoryModel createdSubcategory = prepareCategoryUnder(createdCategory.getId());
final RestCategoryModel createdSubcategory = prepareCategoryUnder(createdCategory);
STEP("Update as admin newly created subcategory");
final String categoryNewName = getRandomName(CATEGORY_NEW_NAME_PREFIX);
@@ -233,4 +233,27 @@ public class UpdateCategoriesTests extends CategoriesRestTest
restClient.assertStatusCodeIs(OK);
updatedCategory.assertThat().field(FIELD_NAME).is(categoryNewName);
}
/**
* Check whether count present in update category request will be ignored.
*/
@Test(groups = { TestGroup.REST_API })
public void testUpdateCategory_verifyIfCountInRequestIsIgnored()
{
STEP("Prepare a category under root category");
final RestCategoryModel createdCategory = prepareCategoryUnderRoot();
STEP("Try to update newly created category providing new name and count number");
final RestCategoryModel fixedCategoryModel = createCategoryModelWithName(getRandomName(CATEGORY_NEW_NAME_PREFIX));
fixedCategoryModel.setCount(2);
final RestCategoryModel updatedCategory = restClient.authenticateUser(dataUser.getAdminUser())
.withCoreAPI()
.usingCategory(createdCategory)
.include(INCLUDE_COUNT_PARAM)
.updateCategory(fixedCategoryModel);
restClient.assertStatusCodeIs(OK);
updatedCategory.assertThat().field(FIELD_ID).is(createdCategory.getId());
updatedCategory.assertThat().field(FIELD_COUNT).is(0);
}
}

View File

@@ -14,7 +14,6 @@ import org.alfresco.utility.testrail.annotation.TestRail;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Ignore;
import org.testng.annotations.Test;
public class AddFavoritesTests extends RestTest
@@ -355,7 +354,6 @@ public class AddFavoritesTests extends RestTest
@TestRail(section = { TestGroup.REST_API, TestGroup.FAVORITES }, executionType = ExecutionType.REGRESSION,
description = "Verify add file favorite with tag id returns status code 404")
@Test(groups = { TestGroup.REST_API, TestGroup.FAVORITES, TestGroup.REGRESSION })
@Ignore
public void addFileFavoriteUsingTagId() throws Exception
{
FileModel file = dataContent.usingSite(siteModel).usingUser(adminUserModel).createContent(CMISUtil.DocumentType.TEXT_PLAIN);

View File

@@ -0,0 +1,226 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.tags;
import static org.alfresco.utility.data.RandomData.getRandomName;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CONFLICT;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.OK;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.alfresco.rest.RestTest;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.rest.model.RestTagModelsCollection;
import org.alfresco.utility.model.TestGroup;
import org.alfresco.utility.model.UserModel;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
public class CreateTagsTests extends RestTest
{
private static final String FIELD_ID = "id";
private static final String FIELD_TAG = "tag";
private static final String FIELD_COUNT = "count";
private static final String TAG_NAME_PREFIX = "tag-name";
private UserModel admin;
private UserModel user;
@BeforeClass
public void init()
{
admin = dataUser.getAdminUser();
user = dataUser.createRandomTestUser();
}
/**
* Verify if tag does not exist in the system, create one as admin and check if now it's there.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateSingleTag()
{
STEP("Create single tag as admin");
final RestTagModel tagModel = createTagModelWithName(getRandomName("99gat").toLowerCase());
final RestTagModel createdTag = restClient.authenticateUser(admin).withCoreAPI().createSingleTag(tagModel);
restClient.assertStatusCodeIs(CREATED);
createdTag.assertThat().field(FIELD_TAG).is(tagModel.getTag())
.assertThat().field(FIELD_ID).isNotEmpty();
STEP("Verify that tag does exist in the system");
RestTagModel tag = restClient.authenticateUser(admin).withCoreAPI().getTag(createdTag);
restClient.assertStatusCodeIs(OK);
tag.assertThat().isEqualTo(createdTag);
}
/**
* Create multiple orphan tags.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateMultipleTags()
{
STEP("Create several tags as admin");
final List<RestTagModel> tagModels = IntStream.range(0, 3)
.mapToObj(i -> createTagModelWithName(getRandomName(TAG_NAME_PREFIX + "-" + i).toLowerCase()))
.collect(Collectors.toList());
final RestTagModelsCollection createdTags = restClient.authenticateUser(admin).withCoreAPI().createTags(tagModels);
restClient.assertStatusCodeIs(CREATED);
IntStream.range(0, tagModels.size())
.forEach(i -> createdTags.getEntries().get(i).onModel()
.assertThat().field(FIELD_TAG).is(tagModels.get(i).getTag())
.assertThat().field(FIELD_ID).isNotEmpty()
);
}
/**
* Verify that tag name's case will be lowered.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateSingleTag_usingUppercaseName()
{
STEP("Create single tag as admin using uppercase name");
final RestTagModel tagModel = createTagModelWithName(getRandomName(TAG_NAME_PREFIX).toUpperCase());
final RestTagModel createdTag = restClient.authenticateUser(admin).withCoreAPI().createSingleTag(tagModel);
restClient.assertStatusCodeIs(CREATED);
createdTag.assertThat().field(FIELD_TAG).is(tagModel.getTag().toLowerCase())
.assertThat().field(FIELD_ID).isNotEmpty();
}
/**
* Try to create few tags including repeating ones. Repeated tags should be omitted.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateMultipleTags_withRepeatedName()
{
STEP("Create models of tags");
final String repeatedTagName = getRandomName(TAG_NAME_PREFIX).toLowerCase();
final List<RestTagModel> tagModels = List.of(
createTagModelWithName(repeatedTagName),
createTagModelWithName(getRandomName(TAG_NAME_PREFIX).toLowerCase()),
createTagModelWithName(repeatedTagName)
);
STEP("Create several tags skipping repeating names");
final RestTagModelsCollection createdTags = restClient.authenticateUser(admin).withCoreAPI().createTags(tagModels);
restClient.assertStatusCodeIs(CREATED);
createdTags.assertThat().entriesListCountIs(2);
createdTags.assertThat().entriesListContains(FIELD_TAG, tagModels.get(0).getTag())
.and().entriesListContains(FIELD_TAG, tagModels.get(1).getTag());
}
/**
* Try to create a tag as a common user and expect 403 (Forbidden)
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateTag_asUser()
{
STEP("Try to create single tag as a common user and expect 403");
final RestTagModel tagModel = createTagModelWithRandomName();
restClient.authenticateUser(user).withCoreAPI().createSingleTag(tagModel);
restClient.assertStatusCodeIs(FORBIDDEN);
}
/**
* Try to call create tag API passing empty list and expect 400 (Bad Request)
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateTags_passingEmptyList()
{
STEP("Pass empty list while creating tags and expect 400");
restClient.authenticateUser(admin).withCoreAPI().createTags(Collections.emptyList());
restClient.assertStatusCodeIs(BAD_REQUEST);
}
/**
* Try to create a tag, which already exists in the system and expect 409 (Conflict)
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateTag_usingAlreadyExistingTagName()
{
STEP("Create some tag in the system");
final RestTagModel tagToCreate = createTagModelWithRandomName();
final RestTagModel alreadyExistingTag = prepareOrphanTag(tagToCreate);
// set original name instead the case lowered one
alreadyExistingTag.setTag(tagToCreate.getTag());
STEP("Try to use already existing tag to create duplicate and expect 409");
restClient.authenticateUser(admin).withCoreAPI().createSingleTag(alreadyExistingTag);
restClient
.assertStatusCodeIs(CONFLICT)
.assertLastError().containsSummary("Duplicate child name not allowed: " + alreadyExistingTag.getTag().toLowerCase());
}
/**
* Verify if count field is 0 for newly created tags.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS })
public void testCreateTag_includingCount()
{
STEP("Create single tag as admin including count and verify if it is 0");
final RestTagModel tagModel = createTagModelWithName(getRandomName(TAG_NAME_PREFIX).toLowerCase());
final RestTagModel createdTag = restClient.authenticateUser(admin).withCoreAPI().include(FIELD_COUNT).createSingleTag(tagModel);
restClient.assertStatusCodeIs(CREATED);
createdTag.assertThat().field(FIELD_TAG).is(tagModel.getTag())
.assertThat().field(FIELD_ID).isNotEmpty()
.assertThat().field(FIELD_COUNT).is(0);
}
private RestTagModel prepareOrphanTagWithRandomName()
{
return prepareOrphanTag(createTagModelWithRandomName());
}
private RestTagModel prepareOrphanTag(final RestTagModel tagModel)
{
final RestTagModel tag = restClient.authenticateUser(admin).withCoreAPI().createSingleTag(tagModel);
restClient.assertStatusCodeIs(CREATED);
return tag;
}
private static RestTagModel createTagModelWithRandomName()
{
return createTagModelWithName(getRandomName(TAG_NAME_PREFIX));
}
private static RestTagModel createTagModelWithName(final String tagName)
{
return RestTagModel.builder().tag(tagName).create();
}
}

View File

@@ -0,0 +1,88 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.tags;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.utility.constants.UserRole;
import org.alfresco.utility.model.TestGroup;
import org.testng.annotations.Test;
import static org.alfresco.utility.report.log.Step.STEP;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.NO_CONTENT;
public class DeleteTagsTests extends TagsDataPrep
{
/**
* Check we can delete a tag by its id.
*/
@Test(groups = {TestGroup.REST_API})
public void testDeleteTag()
{
STEP("Create a tag assigned to a document and send a request to delete it.");
document = dataContent.usingUser(adminUserModel).usingSite(siteModel).createContent(CMISUtil.DocumentType.TEXT_PLAIN);
RestTagModel aTag = createTagForDocument(document);
restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(aTag).deleteTag();
restClient.assertStatusCodeIs(NO_CONTENT);
STEP("Ensure that the tag has been deleted by sending a GET request and receiving 404.");
restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().getTag(aTag);
restClient.assertStatusCodeIs(NOT_FOUND);
}
/**
* Attempt to delete a tag as a site manager and receive 403 error.
* Other user roles have fewer permissions than a SiteManager and thus would also be forbidden from deleting a tag.
*/
@Test(groups = {TestGroup.REST_API})
public void testDeleteTagAsSiteManager_andFail()
{
STEP("Create a tag assigned to a document and attempt to delete as a site manager");
document = dataContent.usingUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager)).usingSite(siteModel).createContent(CMISUtil.DocumentType.TEXT_PLAIN);
RestTagModel aTag = createTagForDocument(document);
restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager)).withCoreAPI().usingTag(aTag).deleteTag();
restClient.assertStatusCodeIs(FORBIDDEN).assertLastError().containsSummary("Current user does not have permission to manage a tag");
}
/**
* Check we receive 404 error when trying to delete a tag with a non-existent id
*/
@Test(groups = {TestGroup.REST_API})
public void testDeleteNonExistentTag()
{
STEP("Attempt to delete tag with non-existent id and receive 404 error");
final String id = "non-existing-dummy-id";
final RestTagModel tagModel = createTagModelWithId(id);
restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(tagModel).deleteTag();
restClient.assertStatusCodeIs(NOT_FOUND);
}
}

View File

@@ -1,5 +1,9 @@
package org.alfresco.rest.tags;
import static org.alfresco.utility.report.log.Step.STEP;
import java.util.Set;
import org.alfresco.rest.model.RestErrorModel;
import org.alfresco.rest.model.RestTagModel;
import org.alfresco.rest.model.RestTagModelsCollection;
@@ -9,19 +13,11 @@ import org.alfresco.utility.model.TestGroup;
import org.alfresco.utility.testrail.ExecutionType;
import org.alfresco.utility.testrail.annotation.TestRail;
import org.springframework.http.HttpStatus;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@Test(groups = {TestGroup.REQUIRE_SOLR})
public class GetTagsTests extends TagsDataPrep
{
@BeforeClass(alwaysRun = true)
public void dataPreparation() throws Exception
{
init();
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.SANITY, description = "Verify user with Manager role gets tags using REST API and status code is OK (200)")
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.SANITY })
public void getTagsWithManagerRole() throws Exception
@@ -192,7 +188,7 @@ public class GetTagsTests extends TagsDataPrep
.and().field("hasMoreItems").is("false")
.and().field("count").is("0")
.and().field("skipCount").is(20000)
.and().field("totalItems").isNull();
.and().field("totalItems").is(0);
}
@TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
@@ -219,11 +215,128 @@ public class GetTagsTests extends TagsDataPrep
RestTagModel deletedTag = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager))
.withCoreAPI().usingResource(document).addTag(removedTag);
restClient.withCoreAPI().usingResource(document).deleteTag(deletedTag);
restClient.authenticateUser(adminUserModel).withCoreAPI().usingTag(deletedTag).deleteTag();
restClient.assertStatusCodeIs(HttpStatus.NO_CONTENT);
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
returnedCollection.assertThat().entriesListIsNotEmpty()
.and().entriesListDoesNotContain("tag", removedTag.toLowerCase());
}
/**
* Verify if exact name filter can be applied.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withSingleNameFilter()
{
STEP("Get tags with names filter using EQUALS and expect one item in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag='" + documentTag.getTag() + "')")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetMatches("tag", Set.of(documentTagValue.toLowerCase()));
}
/**
* Verify if multiple names can be applied as a filter.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withTwoNameFilters()
{
STEP("Get tags with names filter using IN and expect two items in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag IN ('" + documentTag.getTag() + "', '" + folderTag.getTag() + "'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetMatches("tag", Set.of(documentTagValue.toLowerCase(), folderTagValue.toLowerCase()));
}
/**
* Verify if alike name filter can be applied.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_whichNamesStartsWithOrphan()
{
STEP("Get tags with names filter using MATCHES and expect one item in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag MATCHES ('orphan*'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetContains("tag", orphanTag.getTag().toLowerCase());
}
/**
* Verify that tags can be filtered by exact name and alike name at the same time.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withExactNameAndAlikeFilters()
{
STEP("Get tags with names filter using EQUALS and MATCHES and expect four items in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag='" + orphanTag.getTag() + "' OR tag MATCHES ('*tag*'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetContains("tag", documentTagValue.toLowerCase(), documentTagValue2.toLowerCase(), folderTagValue.toLowerCase(), orphanTag.getTag().toLowerCase());
}
/**
* Verify if multiple alike filters can be applied.
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withTwoAlikeFilters()
{
STEP("Get tags applying names filter using MATCHES twice and expect four items in result");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(tag MATCHES ('orphan*') OR tag MATCHES ('tag*'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.OK);
returnedCollection.assertThat()
.entrySetContains("tag", documentTagValue.toLowerCase(), documentTagValue2.toLowerCase(), folderTagValue.toLowerCase(), orphanTag.getTag().toLowerCase());
}
/**
* Verify that providing incorrect field name in where query will result with 400 (Bad Request).
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_withWrongWherePropertyNameAndExpect400()
{
STEP("Try to get tags with names filter using EQUALS and wrong property name and expect 400");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(name=gat)")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST)
.assertLastError().containsSummary("Where query error: property with name: name is not expected");
}
/**
* Verify tht AND operator is not supported in where query and expect 400 (Bad Request).
*/
@Test(groups = { TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION })
public void testGetTags_queryAndOperatorNotSupported()
{
STEP("Try to get tags applying names filter using AND operator and expect 400");
returnedCollection = restClient.authenticateUser(adminUserModel)
.withParams("where=(name=tag AND name IN ('tag-', 'gat'))")
.withCoreAPI()
.getTags();
restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST)
.assertLastError().containsSummary("An invalid WHERE query was received. Unsupported Predicate");
}
}

View File

@@ -1,6 +1,5 @@
package org.alfresco.rest.tags;
import java.util.Date;
import org.alfresco.dataprep.CMISUtil;
import org.alfresco.rest.RestTest;
import org.alfresco.rest.model.RestTagModel;
@@ -12,12 +11,9 @@ import org.alfresco.utility.data.RandomData;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.FolderModel;
import org.alfresco.utility.model.SiteModel;
import org.alfresco.utility.model.TestGroup;
import org.alfresco.utility.model.UserModel;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@Test(groups = {TestGroup.REQUIRE_SOLR})
public class TagsDataPrep extends RestTest
{
@@ -28,13 +24,15 @@ public class TagsDataPrep extends RestTest
protected static FileModel document;
protected static FolderModel folder;
protected static String documentTagValue, documentTagValue2, folderTagValue;
protected static RestTagModel documentTag, documentTag2, folderTag, returnedModel;
protected static RestTagModel documentTag, documentTag2, folderTag, orphanTag, returnedModel;
protected static RestTagModelsCollection returnedCollection;
@BeforeClass
public void init() throws Exception
{
//Create users
adminUserModel = dataUser.getAdminUser();
userModel = dataUser.createRandomTestUser();
//Create public site
siteModel = dataSite.usingUser(adminUserModel).createPublicRandomSite();
usersWithRoles = dataUser.usingAdmin().addUsersWithRolesToSite(siteModel, UserRole.SiteManager, UserRole.SiteCollaborator, UserRole.SiteConsumer, UserRole.SiteContributor);
@@ -49,15 +47,35 @@ public class TagsDataPrep extends RestTest
documentTag = restClient.withCoreAPI().usingResource(document).addTag(documentTagValue);
documentTag2 = restClient.withCoreAPI().usingResource(document).addTag(documentTagValue2);
folderTag = restClient.withCoreAPI().usingResource(folder).addTag(folderTagValue);
orphanTag = restClient.withCoreAPI().createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("orphan-tag")).create());
// Allow indexing to complete.
Utility.sleep(500, 60000, () ->
{
returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
returnedCollection.assertThat().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase())
.and().entriesListContains("tag", folderTagValue.toLowerCase());
});
{
returnedCollection = restClient.withParams("maxItems=10000", "where=(tag MATCHES ('*tag*'))")
.withCoreAPI().getTags();
returnedCollection.assertThat().entriesListContains("tag", documentTagValue.toLowerCase())
.and().entriesListContains("tag", documentTagValue2.toLowerCase())
.and().entriesListContains("tag", folderTagValue.toLowerCase());
});
}
protected RestTagModel createTagForDocument(FileModel document)
{
String documentTagValue = RandomData.getRandomName("tag");
return restClient.withCoreAPI().usingResource(document).addTag(documentTagValue);
}
protected RestTagModel createTagModelWithId(final String id)
{
return createTagModelWithIdAndName(id, RandomData.getRandomName("tag"));
}
protected RestTagModel createTagModelWithIdAndName(final String id, final String tag)
{
return RestTagModel.builder()
.id(id)
.tag(tag)
.create();
}
}

View File

@@ -9,7 +9,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-tests</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<developers>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo-packaging</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<properties>

43
pom.xml
View File

@@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
<packaging>pom</packaging>
<name>Alfresco Community Repo Parent</name>
@@ -35,6 +35,8 @@
<build-number>local</build-number>
<image.tag>latest</image.tag>
<image.registry>quay.io</image.registry>
<builder.name>entitled-builder</builder.name>
<local.registry>127.0.0.1:5000</local.registry>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
@@ -45,7 +47,7 @@
<dependency.alfresco-hb-data-sender.version>1.0.12</dependency.alfresco-hb-data-sender.version>
<dependency.alfresco-trashcan-cleaner.version>2.4.1</dependency.alfresco-trashcan-cleaner.version>
<dependency.alfresco-jlan.version>7.2</dependency.alfresco-jlan.version>
<dependency.alfresco-jlan.version>7.4</dependency.alfresco-jlan.version>
<dependency.alfresco-server-root.version>6.0.1</dependency.alfresco-server-root.version>
<dependency.alfresco-messaging-repo.version>1.2.20</dependency.alfresco-messaging-repo.version>
<dependency.activiti-engine.version>5.23.0</dependency.activiti-engine.version>
@@ -64,7 +66,7 @@
<dependency.bouncycastle.version>1.70</dependency.bouncycastle.version>
<dependency.mockito-core.version>4.9.0</dependency.mockito-core.version>
<dependency.assertj.version>3.24.2</dependency.assertj.version>
<dependency.org-json.version>20220320</dependency.org-json.version>
<dependency.org-json.version>20230227</dependency.org-json.version>
<dependency.commons-dbcp.version>2.9.0</dependency.commons-dbcp.version>
<dependency.commons-io.version>2.11.0</dependency.commons-io.version>
<dependency.gson.version>2.8.9</dependency.gson.version>
@@ -75,9 +77,9 @@
<dependency.slf4j.version>2.0.3</dependency.slf4j.version>
<dependency.log4j.version>2.19.0</dependency.log4j.version>
<dependency.gytheio.version>0.17</dependency.gytheio.version>
<dependency.groovy.version>3.0.12</dependency.groovy.version>
<dependency.groovy.version>3.0.16</dependency.groovy.version>
<dependency.tika.version>2.4.1</dependency.tika.version>
<dependency.spring-security.version>5.7.5</dependency.spring-security.version>
<dependency.spring-security.version>5.8.2</dependency.spring-security.version>
<dependency.truezip.version>7.7.10</dependency.truezip.version>
<dependency.poi.version>5.2.2</dependency.poi.version>
<dependency.poi-ooxml-lite.version>5.2.3</dependency.poi-ooxml-lite.version>
@@ -87,7 +89,7 @@
<dependency.netty.version>4.1.79.Final</dependency.netty.version> <!-- must be in sync with camels transitive dependencies, e.g.: netty-common -->
<dependency.netty.qpid.version>4.1.72.Final</dependency.netty.qpid.version> <!-- must be in sync with camels transitive dependencies: native-unix-common/native-epoll/native-kqueue -->
<dependency.netty-tcnative.version>2.0.53.Final</dependency.netty-tcnative.version> <!-- must be in sync with camels transitive dependencies -->
<dependency.activemq.version>5.17.1</dependency.activemq.version>
<dependency.activemq.version>5.17.4</dependency.activemq.version>
<dependency.apache-compress.version>1.22</dependency.apache-compress.version>
<dependency.apache.taglibs.version>1.2.5</dependency.apache.taglibs.version>
<dependency.awaitility.version>4.2.0</dependency.awaitility.version>
@@ -107,6 +109,7 @@
<dependency.jakarta-mail-api.version>1.6.5</dependency.jakarta-mail-api.version>
<dependency.jakarta-json-api.version>1.1.6</dependency.jakarta-json-api.version>
<dependency.jakarta-json-path.version>2.7.0</dependency.jakarta-json-path.version>
<dependency.json-smart.version>2.4.8</dependency.json-smart.version>
<dependency.jakarta-rpc-api.version>1.1.4</dependency.jakarta-rpc-api.version>
<alfresco.googledrive.version>3.4.0-M1</alfresco.googledrive.version>
@@ -116,11 +119,11 @@
<alfresco.maven-plugin.version>2.2.0</alfresco.maven-plugin.version>
<license-maven-plugin.version>2.0.1.alfresco-2</license-maven-plugin.version>
<dependency.postgresql.version>42.5.0</dependency.postgresql.version>
<dependency.postgresql.version>42.5.2</dependency.postgresql.version>
<dependency.mysql.version>8.0.30</dependency.mysql.version>
<dependency.mysql-image.version>8</dependency.mysql-image.version>
<dependency.mariadb.version>2.7.4</dependency.mariadb.version>
<dependency.tas-utility.version>3.0.58</dependency.tas-utility.version>
<dependency.tas-utility.version>3.0.61</dependency.tas-utility.version>
<dependency.rest-assured.version>5.2.0</dependency.rest-assured.version>
<dependency.tas-email.version>1.11</dependency.tas-email.version>
<dependency.tas-webdav.version>1.7</dependency.tas-webdav.version>
@@ -148,7 +151,7 @@
<connection>scm:git:https://github.com/Alfresco/alfresco-community-repo.git</connection>
<developerConnection>scm:git:https://github.com/Alfresco/alfresco-community-repo.git</developerConnection>
<url>https://github.com/Alfresco/alfresco-community-repo</url>
<tag>20.73</tag>
<tag>20.106</tag>
</scm>
<distributionManagement>
@@ -239,6 +242,17 @@
<version>${dependency.jakarta-json-api.version}</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${dependency.jakarta-json-path.version}</version>
</dependency>
<dependency>
<groupId> net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>${dependency.json-smart.version}</version>
</dependency>
<dependency>
<groupId>jakarta.xml.rpc</groupId>
<artifactId>jakarta.xml.rpc-api</artifactId>
@@ -384,7 +398,7 @@
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
<version>1.5</version>
</dependency>
<dependency>
@@ -899,6 +913,13 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>${dependency.spring-security.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -916,7 +937,7 @@
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.40.2</version>
<version>0.42.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<dependencies>

View File

@@ -26,57 +26,101 @@
package org.alfresco.rest.api;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import java.util.List;
import org.alfresco.rest.api.model.Category;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.StoreRef;
@Experimental
public interface Categories
{
Category getCategoryById(String id, Parameters parameters);
Category getCategoryById(StoreRef storeRef, String id, Parameters parameters);
List<Category> createSubcategories(String parentCategoryId, List<Category> categories, Parameters parameters);
default Category getCategoryById(String id, Parameters parameters)
{
return getCategoryById(STORE_REF_WORKSPACE_SPACESSTORE, id, parameters);
}
CollectionWithPagingInfo<Category> getCategoryChildren(String parentCategoryId, Parameters parameters);
List<Category> createSubcategories(StoreRef storeRef, String parentCategoryId, List<Category> categories, Parameters parameters);
default List<Category> createSubcategories(String parentCategoryId, List<Category> categories, Parameters parameters)
{
return createSubcategories(STORE_REF_WORKSPACE_SPACESSTORE, parentCategoryId, categories, parameters);
}
List<Category> getCategoryChildren(StoreRef storeRef, String parentCategoryId, Parameters parameters);
default List<Category> getCategoryChildren(String parentCategoryId, Parameters parameters)
{
return getCategoryChildren(STORE_REF_WORKSPACE_SPACESSTORE, parentCategoryId, parameters);
}
/**
* Update category by ID. Currently, it's possible only to update the name of category.
*
* @param storeRef Reference to node store.
* @param id Category ID.
* @param fixedCategoryModel Fixed category model.
* @param parameters Additional parameters.
* @return Updated category.
*/
Category updateCategoryById(String id, Category fixedCategoryModel);
Category updateCategoryById(StoreRef storeRef, String id, Category fixedCategoryModel, Parameters parameters);
void deleteCategoryById(String id, Parameters parameters);
default Category updateCategoryById(String id, Category fixedCategoryModel, Parameters parameters)
{
return updateCategoryById(STORE_REF_WORKSPACE_SPACESSTORE, id, fixedCategoryModel, parameters);
}
void deleteCategoryById(StoreRef storeRef, String id, Parameters parameters);
default void deleteCategoryById(String id, Parameters parameters)
{
deleteCategoryById(STORE_REF_WORKSPACE_SPACESSTORE, id, parameters);
}
/**
* Get categories linked from node. Read permission on node is required.
* Node type is restricted to specified vales from: {@link org.alfresco.util.TypeConstraint}.
*
* @param nodeId Node ID.
* @param parameters Additional parameters.
* @return Categories linked from node.
*/
List<Category> listCategoriesForNode(String nodeId);
List<Category> listCategoriesForNode(String nodeId, Parameters parameters);
/**
* Link node to categories. Change permission on node is required.
* Node types allowed for categorization are specified within {@link org.alfresco.util.TypeConstraint}.
*
* @param storeRef Reference to node store.
* @param nodeId Node ID.
* @param categoryLinks Category IDs to which content should be linked to.
* @param parameters Additional parameters.
* @return Linked to categories.
*/
List<Category> linkNodeToCategories(String nodeId, List<Category> categoryLinks);
List<Category> linkNodeToCategories(StoreRef storeRef, String nodeId, List<Category> categoryLinks, Parameters parameters);
default List<Category> linkNodeToCategories(String nodeId, List<Category> categoryLinks, Parameters parameters)
{
return linkNodeToCategories(STORE_REF_WORKSPACE_SPACESSTORE, nodeId, categoryLinks, parameters);
}
/**
* Unlink node from a category.
*
* @param storeRef Reference to node store.
* @param nodeId Node ID.
* @param categoryId Category ID from which content node should be unlinked from.
* @param parameters Additional parameters.
*/
void unlinkNodeFromCategory(String nodeId, String categoryId, Parameters parameters);
void unlinkNodeFromCategory(StoreRef storeRef, String nodeId, String categoryId, Parameters parameters);
default void unlinkNodeFromCategory(String nodeId, String categoryId, Parameters parameters)
{
unlinkNodeFromCategory(STORE_REF_WORKSPACE_SPACESSTORE, nodeId, categoryId, parameters);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -23,22 +23,35 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api;
import java.util.List;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.StoreRef;
public interface Tags
{
public List<Tag> addTags(String nodeId, List<Tag> tags);
public Tag getTag(StoreRef storeRef, String tagId);
public void deleteTag(String nodeId, String tagId);
public CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params);
public Tag changeTag(StoreRef storeRef, String tagId, Tag tag);
public CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params);
}
package org.alfresco.rest.api;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import java.util.List;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.StoreRef;
public interface Tags
{
List<Tag> addTags(String nodeId, List<Tag> tags);
Tag getTag(StoreRef storeRef, String tagId);
void deleteTag(String nodeId, String tagId);
CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params);
Tag changeTag(StoreRef storeRef, String tagId, Tag tag);
CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params);
@Experimental
List<Tag> createTags(StoreRef storeRef, List<Tag> tags, Parameters parameters);
@Experimental
default List<Tag> createTags(List<Tag> tags, Parameters parameters)
{
return createTags(STORE_REF_WORKSPACE_SPACESSTORE, tags, parameters);
}
void deleteTagById(StoreRef storeRef, String tagId);
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -45,6 +45,7 @@ public class CategoriesEntityResource implements EntityResourceAction.ReadById<C
EntityResourceAction.Update<Category>,
EntityResourceAction.Delete
{
private final Categories categories;
public CategoriesEntityResource(Categories categories)
@@ -77,7 +78,7 @@ public class CategoriesEntityResource implements EntityResourceAction.ReadById<C
@Override
public Category update(String id, Category categoryModel, Parameters parameters)
{
return categories.updateCategoryById(id, categoryModel);
return categories.updateCategoryById(id, categoryModel, parameters);
}
/**

View File

@@ -63,7 +63,7 @@ public class NodesCategoryLinksRelation implements RelationshipResourceAction.Cr
@Override
public CollectionWithPagingInfo<Category> readAll(String nodeId, Parameters parameters)
{
return ListPage.of(categories.listCategoriesForNode(nodeId), parameters.getPaging());
return ListPage.of(categories.listCategoriesForNode(nodeId, parameters), parameters.getPaging());
}
/**
@@ -77,7 +77,7 @@ public class NodesCategoryLinksRelation implements RelationshipResourceAction.Cr
@Override
public List<Category> create(String nodeId, List<Category> categoryLinks, Parameters parameters)
{
return categories.linkNodeToCategories(nodeId, categoryLinks);
return categories.linkNodeToCategories(nodeId, categoryLinks, parameters);
}
/**
@@ -93,5 +93,4 @@ public class NodesCategoryLinksRelation implements RelationshipResourceAction.Cr
{
categories.unlinkNodeFromCategory(nodeId, categoryId, parameters);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,9 +26,8 @@
package org.alfresco.rest.api.categories;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import org.alfresco.rest.api.Categories;
import org.alfresco.rest.api.model.Category;
@@ -36,6 +35,7 @@ import org.alfresco.rest.framework.WebApiDescription;
import org.alfresco.rest.framework.resource.RelationshipResource;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.ListPage;
import org.alfresco.rest.framework.resource.parameters.Parameters;
@RelationshipResource(name = "subcategories", entityResource = CategoriesEntityResource.class, title = "Subcategories")
@@ -69,8 +69,8 @@ public class SubcategoriesRelation implements RelationshipResourceAction.Create<
description = "Lists direct children of a parent category",
successStatus = HttpServletResponse.SC_OK)
@Override
public CollectionWithPagingInfo<Category> readAll(String parentCategoryId, Parameters params)
public CollectionWithPagingInfo<Category> readAll(String parentCategoryId, Parameters parameters)
{
return categories.getCategoryChildren(parentCategoryId, params);
return ListPage.of(categories.getCategoryChildren(parentCategoryId, parameters), parameters.getPaging());
}
}

View File

@@ -46,10 +46,8 @@ import org.alfresco.rest.api.model.Category;
import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.core.exceptions.InvalidNodeTypeException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.ListPage;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
@@ -62,6 +60,7 @@ import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.Pair;
import org.alfresco.util.TypeConstraint;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -69,6 +68,7 @@ import org.apache.commons.lang3.StringUtils;
@Experimental
public class CategoriesImpl implements Categories
{
static final String INCLUDE_COUNT_PARAM = "count";
static final String NOT_A_VALID_CATEGORY = "Node id does not refer to a valid category";
static final String NO_PERMISSION_TO_MANAGE_A_CATEGORY = "Current user does not have permission to manage a category";
static final String NO_PERMISSION_TO_READ_CONTENT = "Current user does not have read permission to content";
@@ -83,8 +83,8 @@ public class CategoriesImpl implements Categories
private final PermissionService permissionService;
private final TypeConstraint typeConstraint;
public CategoriesImpl(AuthorityService authorityService, CategoryService categoryService, Nodes nodes, NodeService nodeService, PermissionService permissionService,
TypeConstraint typeConstraint)
public CategoriesImpl(AuthorityService authorityService, CategoryService categoryService, Nodes nodes, NodeService nodeService,
PermissionService permissionService, TypeConstraint typeConstraint)
{
this.authorityService = authorityService;
this.categoryService = categoryService;
@@ -95,63 +95,89 @@ public class CategoriesImpl implements Categories
}
@Override
public Category getCategoryById(final String id, final Parameters params)
public Category getCategoryById(final StoreRef storeRef, final String id, final Parameters parameters)
{
final NodeRef nodeRef = getCategoryNodeRef(id);
final NodeRef nodeRef = getCategoryNodeRef(storeRef, id);
if (isRootCategory(nodeRef))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{id});
}
return mapToCategory(nodeRef);
final Category category = mapToCategory(nodeRef);
if (parameters.getInclude().contains(INCLUDE_COUNT_PARAM))
{
final Map<String, Integer> categoriesCount = getCategoriesCount(storeRef);
category.setCount(categoriesCount.getOrDefault(category.getId(), 0));
}
return category;
}
@Override
public List<Category> createSubcategories(String parentCategoryId, List<Category> categories, Parameters parameters)
public List<Category> createSubcategories(final StoreRef storeRef, final String parentCategoryId, final List<Category> categories, final Parameters parameters)
{
verifyAdminAuthority();
final NodeRef parentNodeRef = getCategoryNodeRef(parentCategoryId);
final List<NodeRef> categoryNodeRefs = categories.stream()
final NodeRef parentNodeRef = getCategoryNodeRef(storeRef, parentCategoryId);
return categories.stream()
.map(c -> createCategoryNodeRef(parentNodeRef, c))
.collect(Collectors.toList());
return categoryNodeRefs.stream()
.map(this::mapToCategory)
.peek(category -> {
if (parameters.getInclude().contains(INCLUDE_COUNT_PARAM))
{
category.setCount(0);
}
})
.collect(Collectors.toList());
}
@Override
public CollectionWithPagingInfo<Category> getCategoryChildren(String parentCategoryId, Parameters params)
public List<Category> getCategoryChildren(final StoreRef storeRef, final String parentCategoryId, final Parameters parameters)
{
final NodeRef parentNodeRef = getCategoryNodeRef(parentCategoryId);
final List<ChildAssociationRef> childCategoriesAssocs = nodeService.getChildAssocs(parentNodeRef).stream()
.filter(ca -> ContentModel.ASSOC_SUBCATEGORIES.equals(ca.getTypeQName()))
.collect(Collectors.toList());
final List<Category> categories = childCategoriesAssocs.stream()
.map(c -> mapToCategory(c.getChildRef()))
.collect(Collectors.toList());
return ListPage.of(categories, params.getPaging());
final NodeRef parentNodeRef = getCategoryNodeRef(storeRef, parentCategoryId);
final List<Category> categories = nodeService.getChildAssocs(parentNodeRef).stream()
.filter(ca -> ContentModel.ASSOC_SUBCATEGORIES.equals(ca.getTypeQName()))
.map(ChildAssociationRef::getChildRef)
.map(this::mapToCategory)
.collect(Collectors.toList());
if (parameters.getInclude().contains(INCLUDE_COUNT_PARAM))
{
final Map<String, Integer> categoriesCount = getCategoriesCount(storeRef);
categories.forEach(category -> category.setCount(categoriesCount.getOrDefault(category.getId(), 0)));
}
return categories;
}
@Override
public Category updateCategoryById(final String id, final Category fixedCategoryModel)
public Category updateCategoryById(final StoreRef storeRef, final String id, final Category fixedCategoryModel, final Parameters parameters)
{
verifyAdminAuthority();
final NodeRef categoryNodeRef = getCategoryNodeRef(id);
final NodeRef categoryNodeRef = getCategoryNodeRef(storeRef, id);
if (isRootCategory(categoryNodeRef))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{id});
}
validateCategoryFields(fixedCategoryModel);
final Category category = mapToCategory(changeCategoryName(categoryNodeRef, fixedCategoryModel.getName()));
return mapToCategory(changeCategoryName(categoryNodeRef, fixedCategoryModel.getName()));
if (parameters.getInclude().contains(INCLUDE_COUNT_PARAM))
{
final Map<String, Integer> categoriesCount = getCategoriesCount(storeRef);
category.setCount(categoriesCount.getOrDefault(category.getId(), 0));
}
return category;
}
@Override
public void deleteCategoryById(String id, Parameters parameters)
public void deleteCategoryById(final StoreRef storeRef, final String id, final Parameters parameters)
{
verifyAdminAuthority();
final NodeRef nodeRef = getCategoryNodeRef(id);
final NodeRef nodeRef = getCategoryNodeRef(storeRef, id);
if (isRootCategory(nodeRef))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{id});
@@ -161,7 +187,7 @@ public class CategoriesImpl implements Categories
}
@Override
public List<Category> listCategoriesForNode(final String nodeId)
public List<Category> listCategoriesForNode(final String nodeId, final Parameters parameters)
{
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
verifyReadPermission(contentNodeRef);
@@ -178,7 +204,7 @@ public class CategoriesImpl implements Categories
}
@Override
public List<Category> linkNodeToCategories(final String nodeId, final List<Category> categoryLinks)
public List<Category> linkNodeToCategories(final StoreRef storeRef, final String nodeId, final List<Category> categoryLinks, final Parameters parameters)
{
if (CollectionUtils.isEmpty(categoryLinks))
{
@@ -194,7 +220,7 @@ public class CategoriesImpl implements Categories
.map(Category::getId)
.filter(StringUtils::isNotEmpty)
.distinct()
.map(this::getCategoryNodeRef)
.map(id -> getCategoryNodeRef(storeRef, id))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(categoryNodeRefs) || isRootCategoryPresent(categoryNodeRefs))
@@ -208,9 +234,9 @@ public class CategoriesImpl implements Categories
}
@Override
public void unlinkNodeFromCategory(final String nodeId, final String categoryId, Parameters parameters)
public void unlinkNodeFromCategory(final StoreRef storeRef, final String nodeId, final String categoryId, final Parameters parameters)
{
final NodeRef categoryNodeRef = getCategoryNodeRef(categoryId);
final NodeRef categoryNodeRef = getCategoryNodeRef(storeRef, categoryId);
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
verifyChangePermission(contentNodeRef);
verifyNodeType(contentNodeRef);
@@ -272,13 +298,14 @@ public class CategoriesImpl implements Categories
* This method gets category NodeRef for a given category id.
* If '-root-' is passed as category id, then it's retrieved as a call to {@link org.alfresco.service.cmr.search.CategoryService#getRootCategoryNodeRef}
* In all other cases it's retrieved as a node of a category type {@link #validateCategoryNode(String)}
* @param storeRef Reference to node store.
* @param nodeId category node id
* @return NodRef of category node
*/
private NodeRef getCategoryNodeRef(String nodeId)
private NodeRef getCategoryNodeRef(StoreRef storeRef, String nodeId)
{
return PATH_ROOT.equals(nodeId) ?
categoryService.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE)
categoryService.getRootCategoryNodeRef(storeRef)
.orElseThrow(() -> new EntityNotFoundException(nodeId)) :
validateCategoryNode(nodeId);
}
@@ -434,4 +461,18 @@ public class CategoriesImpl implements Categories
nodeService.setProperty(nodeRef, ContentModel.PROP_CATEGORIES, (Serializable) allCategories);
}
}
/**
* Get categories by usage count. Result is a map of category IDs (short form - UUID) as key and usage count as value.
*
* @param storeRef Reference to node store.
* @return Map of categories IDs and usage count.
*/
private Map<String, Integer> getCategoriesCount(final StoreRef storeRef)
{
final String idPrefix = storeRef + "/";
return categoryService.getTopCategories(storeRef, ContentModel.ASPECT_GEN_CLASSIFIABLE, Integer.MAX_VALUE)
.stream()
.collect(Collectors.toMap(pair -> pair.getFirst().toString().replace(idPrefix, StringUtils.EMPTY), Pair::getSecond));
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -23,122 +23,159 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.impl;
import java.util.AbstractList;
package org.alfresco.rest.api.impl;
import static org.alfresco.rest.antlr.WhereClauseParser.EQUALS;
import static org.alfresco.rest.antlr.WhereClauseParser.IN;
import static org.alfresco.rest.antlr.WhereClauseParser.MATCHES;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.tagging.NonExistentTagException;
import org.alfresco.repo.tagging.TagExistsException;
import org.alfresco.repo.tagging.TaggingException;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.Tags;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.NotFoundException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.util.Pair;
import org.alfresco.util.TypeConstraint;
/**
* Centralises access to tag services and maps between representations.
*
* @author steveglover
* @since publicapi1.0
*/
public class TagsImpl implements Tags
{
private static final Object PARAM_INCLUDE_COUNT = "count";
private Nodes nodes;
private TaggingService taggingService;
private TypeConstraint typeConstraint;
public void setTypeConstraint(TypeConstraint typeConstraint)
{
this.typeConstraint = typeConstraint;
}
public void setNodes(Nodes nodes)
{
this.nodes = nodes;
}
public void setTaggingService(TaggingService taggingService)
{
this.taggingService = taggingService;
}
public List<Tag> addTags(String nodeId, final List<Tag> tags)
{
NodeRef nodeRef = nodes.validateNode(nodeId);
if(!typeConstraint.matches(nodeRef))
{
throw new UnsupportedResourceOperationException("Cannot tag this node");
}
List<String> tagValues = new AbstractList<String>()
{
@Override
public String get(int arg0)
{
String tag = tags.get(arg0).getTag();
return tag;
}
@Override
public int size()
{
return tags.size();
}
};
try
{
List<Pair<String, NodeRef>> tagNodeRefs = taggingService.addTags(nodeRef, tagValues);
List<Tag> ret = new ArrayList<Tag>(tags.size());
for(Pair<String, NodeRef> pair : tagNodeRefs)
{
ret.add(new Tag(pair.getSecond(), pair.getFirst()));
}
return ret;
}
catch(IllegalArgumentException e)
{
throw new InvalidArgumentException(e.getMessage());
}
}
public void deleteTag(String nodeId, String tagId)
{
NodeRef nodeRef = nodes.validateNode(nodeId);
getTag(tagId);
NodeRef existingTagNodeRef = validateTag(tagId);
String tagValue = taggingService.getTagName(existingTagNodeRef);
taggingService.removeTag(nodeRef, tagValue);
}
import org.alfresco.query.PagingResults;
import org.alfresco.repo.tagging.NonExistentTagException;
import org.alfresco.repo.tagging.TagExistsException;
import org.alfresco.repo.tagging.TaggingException;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.Tags;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.NotFoundException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.resource.parameters.where.Query;
import org.alfresco.rest.framework.resource.parameters.where.QueryHelper;
import org.alfresco.rest.framework.resource.parameters.where.QueryImpl;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.util.Pair;
import org.alfresco.util.TypeConstraint;
import org.apache.commons.collections.CollectionUtils;
/**
* Centralises access to tag services and maps between representations.
*
* @author steveglover
* @since publicapi1.0
*/
public class TagsImpl implements Tags
{
private static final String PARAM_INCLUDE_COUNT = "count";
private static final String PARAM_WHERE_TAG = "tag";
static final String NOT_A_VALID_TAG = "An invalid parameter has been supplied";
static final String NO_PERMISSION_TO_MANAGE_A_TAG = "Current user does not have permission to manage a tag";
private Nodes nodes;
private TaggingService taggingService;
private TypeConstraint typeConstraint;
private AuthorityService authorityService;
public void setTypeConstraint(TypeConstraint typeConstraint)
{
this.typeConstraint = typeConstraint;
}
public void setNodes(Nodes nodes)
{
this.nodes = nodes;
}
public void setTaggingService(TaggingService taggingService)
{
this.taggingService = taggingService;
}
public void setAuthorityService(AuthorityService authorityService)
{
this.authorityService = authorityService;
}
public List<Tag> addTags(String nodeId, final List<Tag> tags)
{
NodeRef nodeRef = nodes.validateNode(nodeId);
if(!typeConstraint.matches(nodeRef))
{
throw new UnsupportedResourceOperationException("Cannot tag this node");
}
List<String> tagValues = new AbstractList<String>()
{
@Override
public String get(int arg0)
{
String tag = tags.get(arg0).getTag();
return tag;
}
@Override
public int size()
{
return tags.size();
}
};
try
{
List<Pair<String, NodeRef>> tagNodeRefs = taggingService.addTags(nodeRef, tagValues);
List<Tag> ret = new ArrayList<Tag>(tags.size());
for(Pair<String, NodeRef> pair : tagNodeRefs)
{
ret.add(new Tag(pair.getSecond(), pair.getFirst()));
}
return ret;
}
catch(IllegalArgumentException e)
{
throw new InvalidArgumentException(e.getMessage());
}
}
public void deleteTag(String nodeId, String tagId)
{
NodeRef nodeRef = nodes.validateNode(nodeId);
getTag(tagId);
NodeRef existingTagNodeRef = validateTag(tagId);
String tagValue = taggingService.getTagName(existingTagNodeRef);
taggingService.removeTag(nodeRef, tagValue);
}
@Override
public void deleteTagById(StoreRef storeRef, String tagId) {
verifyAdminAuthority();
NodeRef tagNodeRef = validateTag(storeRef, tagId);
String tagValue = taggingService.getTagName(tagNodeRef);
taggingService.deleteTag(storeRef, tagValue);
}
@Override
public CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params)
{
Paging paging = params.getPaging();
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging));
taggingService.getPagedTags(storeRef, 0, paging.getMaxItems());
Paging paging = params.getPaging();
Map<Integer, Collection<String>> namesFilters = resolveTagNamesQuery(params.getQuery());
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging), namesFilters.get(EQUALS), namesFilters.get(MATCHES));
Integer totalItems = results.getTotalResultCount().getFirst();
List<Pair<NodeRef, String>> page = results.getPage();
List<Tag> tags = new ArrayList<Tag>(page.size());
List<Pair<String, Integer>> tagsByCount = null;
Map<String, Integer> tagsByCountMap = new HashMap<String, Integer>();
List<Tag> tags = new ArrayList<>(page.size());
List<Pair<String, Integer>> tagsByCount;
Map<String, Integer> tagsByCountMap = new HashMap<>();
if (params.getInclude().contains(PARAM_INCLUDE_COUNT))
{
tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef);
@@ -153,82 +190,150 @@ public class TagsImpl implements Tags
for (Pair<NodeRef, String> pair : page)
{
Tag selectedTag = new Tag(pair.getFirst(), pair.getSecond());
selectedTag.setCount(tagsByCountMap.get(selectedTag.getTag()));
selectedTag.setCount(Optional.ofNullable(tagsByCountMap.get(selectedTag.getTag())).orElse(0));
tags.add(selectedTag);
}
return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), totalItems);
}
public NodeRef validateTag(String tagId)
{
NodeRef tagNodeRef = nodes.validateNode(tagId);
if(tagNodeRef == null)
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
}
public NodeRef validateTag(StoreRef storeRef, String tagId)
{
NodeRef tagNodeRef = nodes.validateNode(storeRef, tagId);
if(tagNodeRef == null)
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
}
public Tag changeTag(StoreRef storeRef, String tagId, Tag tag)
{
try
{
NodeRef existingTagNodeRef = validateTag(storeRef, tagId);
String existingTagName = taggingService.getTagName(existingTagNodeRef);
String newTagName = tag.getTag();
NodeRef newTagNodeRef = taggingService.changeTag(storeRef, existingTagName, newTagName);
return new Tag(newTagNodeRef, newTagName);
}
catch(NonExistentTagException e)
{
throw new NotFoundException(e.getMessage());
}
catch(TagExistsException e)
{
throw new ConstraintViolatedException(e.getMessage());
}
catch(TaggingException e)
{
throw new InvalidArgumentException(e.getMessage());
}
}
public Tag getTag(String tagId)
{
return getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, tagId);
}
public Tag getTag(StoreRef storeRef, String tagId)
{
NodeRef tagNodeRef = validateTag(storeRef, tagId);
String tagValue = taggingService.getTagName(tagNodeRef);
return new Tag(tagNodeRef, tagValue);
}
public CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params)
{
NodeRef nodeRef = validateTag(nodeId);
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(nodeRef, Util.getPagingRequest(params.getPaging()));
Integer totalItems = results.getTotalResultCount().getFirst();
List<Pair<NodeRef, String>> page = results.getPage();
List<Tag> tags = new ArrayList<Tag>(page.size());
for(Pair<NodeRef, String> pair : page)
{
tags.add(new Tag(pair.getFirst(), pair.getSecond()));
}
return CollectionWithPagingInfo.asPaged(params.getPaging(), tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
}
}
public NodeRef validateTag(String tagId)
{
NodeRef tagNodeRef = nodes.validateNode(tagId);
if(tagNodeRef == null)
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
}
public NodeRef validateTag(StoreRef storeRef, String tagId)
{
NodeRef tagNodeRef = nodes.validateNode(storeRef, tagId);
if(tagNodeRef == null)
{
throw new EntityNotFoundException(tagId);
}
return tagNodeRef;
}
public Tag changeTag(StoreRef storeRef, String tagId, Tag tag)
{
try
{
NodeRef existingTagNodeRef = validateTag(storeRef, tagId);
String existingTagName = taggingService.getTagName(existingTagNodeRef);
String newTagName = tag.getTag();
NodeRef newTagNodeRef = taggingService.changeTag(storeRef, existingTagName, newTagName);
return new Tag(newTagNodeRef, newTagName);
}
catch(NonExistentTagException e)
{
throw new NotFoundException(e.getMessage());
}
catch(TagExistsException e)
{
throw new ConstraintViolatedException(e.getMessage());
}
catch(TaggingException e)
{
throw new InvalidArgumentException(e.getMessage());
}
}
public Tag getTag(String tagId)
{
return getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, tagId);
}
public Tag getTag(StoreRef storeRef, String tagId)
{
NodeRef tagNodeRef = validateTag(storeRef, tagId);
String tagValue = taggingService.getTagName(tagNodeRef);
return new Tag(tagNodeRef, tagValue);
}
public CollectionWithPagingInfo<Tag> getTags(String nodeId, Parameters params)
{
NodeRef nodeRef = validateTag(nodeId);
PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(nodeRef, Util.getPagingRequest(params.getPaging()));
Integer totalItems = results.getTotalResultCount().getFirst();
List<Pair<NodeRef, String>> page = results.getPage();
List<Tag> tags = new ArrayList<Tag>(page.size());
for(Pair<NodeRef, String> pair : page)
{
tags.add(new Tag(pair.getFirst(), pair.getSecond()));
}
return CollectionWithPagingInfo.asPaged(params.getPaging(), tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
}
@Experimental
@Override
public List<Tag> createTags(final StoreRef storeRef, final List<Tag> tags, final Parameters parameters)
{
verifyAdminAuthority();
final List<String> tagNames = Optional.ofNullable(tags).orElse(Collections.emptyList()).stream()
.filter(Objects::nonNull)
.map(Tag::getTag)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(tagNames))
{
throw new InvalidArgumentException(NOT_A_VALID_TAG);
}
return taggingService.createTags(storeRef, tagNames).stream()
.map(pair -> Tag.builder().tag(pair.getFirst()).nodeRef(pair.getSecond()).create())
.peek(tag -> {
if (parameters.getInclude().contains(PARAM_INCLUDE_COUNT))
{
tag.setCount(0);
}
}).collect(Collectors.toList());
}
private void verifyAdminAuthority()
{
if (!authorityService.hasAdminAuthority())
{
throw new PermissionDeniedException(NO_PERMISSION_TO_MANAGE_A_TAG);
}
}
/**
* Method resolves where query looking for clauses: EQUALS, IN or MATCHES.
* Expected values for EQUALS and IN will be merged under EQUALS clause.
* @param namesQuery Where query with expected tag name(s).
* @return Map of expected exact and alike tag names.
*/
private Map<Integer, Collection<String>> resolveTagNamesQuery(final Query namesQuery)
{
if (namesQuery == null || namesQuery == QueryImpl.EMPTY)
{
return Collections.emptyMap();
}
final Map<Integer, Collection<String>> properties = QueryHelper
.resolve(namesQuery)
.usingOrOperator()
.withoutNegations()
.getProperty(PARAM_WHERE_TAG)
.getExpectedValuesForAnyOf(EQUALS, IN, MATCHES)
.skipNegated();
return properties.entrySet().stream()
.collect(Collectors.groupingBy((entry) -> {
if (entry.getKey() == EQUALS || entry.getKey() == IN)
{
return EQUALS;
}
else
{
return MATCHES;
}
}, Collectors.flatMapping((entry) -> entry.getValue().stream().map(String::toLowerCase), Collectors.toCollection(HashSet::new))));
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -34,6 +34,7 @@ public class Category
private String name;
private String parentId;
private boolean hasChildren;
private Integer count;
public String getId()
{
@@ -80,20 +81,38 @@ public class Category
this.hasChildren = hasChildren;
}
public Integer getCount()
{
return count;
}
public void setCount(Integer count)
{
this.count = count;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Category category = (Category) o;
return hasChildren == category.hasChildren && Objects.equals(id, category.id) && name.equals(category.name) &&
Objects.equals(parentId, category.parentId);
return hasChildren == category.hasChildren && Objects.equals(id, category.id) && Objects.equals(name, category.name) && Objects.equals(parentId, category.parentId)
&& Objects.equals(count, category.count);
}
@Override
public int hashCode()
{
return Objects.hash(id, name, parentId, hasChildren);
return Objects.hash(id, name, parentId, hasChildren, count);
}
@Override
public String toString()
{
return "Category{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", parentId='" + parentId + '\'' + ", hasChildren=" + hasChildren + ", count=" + count + '}';
}
public static Builder builder()
@@ -107,6 +126,7 @@ public class Category
private String name;
private String parentId;
private boolean hasChildren;
private Integer count;
public Builder id(String id)
{
@@ -132,6 +152,12 @@ public class Category
return this;
}
public Builder count(Integer count)
{
this.count = count;
return this;
}
public Category create()
{
final Category category = new Category();
@@ -139,6 +165,7 @@ public class Category
category.setName(name);
category.setParentId(parentId);
category.setHasChildren(hasChildren);
category.setCount(count);
return category;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,8 @@
*/
package org.alfresco.rest.api.model;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.alfresco.rest.framework.resource.UniqueId;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -99,42 +101,64 @@ public class Tag implements Comparable<Tag>
}
@Override
public int hashCode()
public boolean equals(Object o)
{
final int prime = 31;
int result = 1;
result = prime * result + ((nodeRef == null) ? 0 : nodeRef.hashCode());
return result;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Tag tag1 = (Tag) o;
return Objects.equals(nodeRef, tag1.nodeRef) && Objects.equals(tag, tag1.tag) && Objects.equals(count, tag1.count);
}
/*
* Tags are equal if they have the same NodeRef
*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
public int hashCode()
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Tag other = (Tag) obj;
if (nodeRef == null) {
if (other.nodeRef != null)
return false;
} else if (!nodeRef.equals(other.nodeRef))
return false;
return true;
return Objects.hash(nodeRef, tag, count);
}
@Override
public String toString()
{
return "Tag [nodeRef=" + nodeRef + ", tag=" + tag + "]";
return "Tag{" + "nodeRef=" + nodeRef + ", tag='" + tag + '\'' + ", count=" + count + '}';
}
public static Builder builder()
{
return new Builder();
}
public static class Builder
{
private NodeRef nodeRef;
private String tag;
private Integer count;
public Builder nodeRef(NodeRef nodeRef)
{
this.nodeRef = nodeRef;
return this;
}
public Builder tag(String tag)
{
this.tag = tag;
return this;
}
public Builder count(Integer count)
{
this.count = count;
return this;
}
public Tag create()
{
final Tag tag = new Tag();
tag.setNodeRef(nodeRef);
tag.setTag(this.tag);
tag.setCount(count);
return tag;
}
}
}

View File

@@ -1,30 +1,33 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.tags;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import org.alfresco.rest.api.Tags;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.WebApiDescription;
@@ -33,12 +36,14 @@ import org.alfresco.rest.framework.resource.EntityResource;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.util.ParameterCheck;
import org.springframework.beans.factory.InitializingBean;
@EntityResource(name="tags", title = "Tags")
public class TagsEntityResource implements EntityResourceAction.Read<Tag>, EntityResourceAction.ReadById<Tag>, EntityResourceAction.Update<Tag>, InitializingBean
public class TagsEntityResource implements EntityResourceAction.Read<Tag>,
EntityResourceAction.ReadById<Tag>, EntityResourceAction.Update<Tag>, EntityResourceAction.Create<Tag>, EntityResourceAction.Delete, InitializingBean
{
private Tags tags;
@@ -54,9 +59,8 @@ public class TagsEntityResource implements EntityResourceAction.Read<Tag>, Entit
}
/**
*
* Returns a paged list of all currently used tags in the store workspace://SpacesStore for the current tenant.
*
* GET /tags
*/
@Override
@WebApiDescription(title="A paged list of all tags in the network.")
@@ -77,4 +81,25 @@ public class TagsEntityResource implements EntityResourceAction.Read<Tag>, Entit
{
return tags.getTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id);
}
/**
* POST /tags
*/
@Experimental
@WebApiDescription(
title = "Create an orphan tag",
description = "Creates a tag, which is not associated with any node",
successStatus = HttpServletResponse.SC_CREATED
)
@Override
public List<Tag> create(List<Tag> tags, Parameters parameters)
{
return this.tags.createTags(tags, parameters);
}
@Override
public void delete(String id, Parameters parameters)
{
tags.deleteTagById(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id);
}
}

View File

@@ -0,0 +1,259 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.framework.resource.parameters.where;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
/**
* Basic implementation of {@link QueryHelper.WalkerCallbackAdapter} providing universal handling of Where query clauses.
* This implementation supports AND operator and all clause types.
* Be default, walker verifies strictly if expected or unexpected properties, and it's comparison types are present in query
* and throws {@link InvalidQueryException} if they are missing.
*/
public class BasicQueryWalker extends QueryHelper.WalkerCallbackAdapter
{
private static final String EQUALS_AND_IN_NOT_ALLOWED_TOGETHER = "Where query error: cannot use '=' (EQUALS) AND 'IN' clauses with same property: %s";
private static final String MISSING_PROPERTY = "Where query error: property with name: %s not present";
static final String MISSING_CLAUSE_TYPE = "Where query error: property with name: %s expects clause: %s";
static final String MISSING_ANY_CLAUSE_OF_TYPE = "Where query error: property with name: %s expects at least one of clauses: %s";
private static final String PROPERTY_NOT_EXPECTED = "Where query error: property with name: %s is not expected";
private static final String PROPERTY_NOT_NEGATABLE = "Where query error: property with name: %s cannot be negated";
private static final String PROPERTY_NAMES_EMPTY = "Cannot verify WHERE query without expected property names";
private Collection<String> expectedPropertyNames;
private final Map<String, WhereProperty> properties;
protected boolean clausesNegatable = true;
protected boolean validateStrictly = true;
public BasicQueryWalker()
{
this.properties = new HashMap<>();
}
public BasicQueryWalker(final String... expectedPropertyNames)
{
this();
this.expectedPropertyNames = Set.of(expectedPropertyNames);
}
public BasicQueryWalker(final Collection<String> expectedPropertyNames)
{
this();
this.expectedPropertyNames = expectedPropertyNames;
}
public void setClausesNegatable(final boolean clausesNegatable)
{
this.clausesNegatable = clausesNegatable;
}
public void setValidateStrictly(boolean validateStrictly)
{
this.validateStrictly = validateStrictly;
}
@Override
public void exists(String propertyName, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
addProperties(propertyName, WhereClauseParser.EXISTS, negated);
}
@Override
public void between(String propertyName, String firstValue, String secondValue, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
addProperties(propertyName, WhereClauseParser.BETWEEN, negated, firstValue, secondValue);
}
@Override
public void comparison(int type, String propertyName, String propertyValue, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
if (WhereClauseParser.EQUALS == type && isAndSupported() && containsProperty(propertyName, WhereClauseParser.IN, negated))
{
throw new InvalidQueryException(String.format(EQUALS_AND_IN_NOT_ALLOWED_TOGETHER, propertyName));
}
addProperties(propertyName, type, negated, propertyValue);
}
@Override
public void in(String propertyName, boolean negated, String... propertyValues)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
if (isAndSupported() && containsProperty(propertyName, WhereClauseParser.EQUALS, negated))
{
throw new InvalidQueryException(String.format(EQUALS_AND_IN_NOT_ALLOWED_TOGETHER, propertyName));
}
addProperties(propertyName, WhereClauseParser.IN, negated, propertyValues);
}
@Override
public void matches(final String propertyName, String propertyValue, boolean negated)
{
verifyPropertyExpectedness(propertyName);
verifyClausesNegatability(negated, propertyName);
addProperties(propertyName, WhereClauseParser.MATCHES, negated, propertyValue);
}
@Override
public void and()
{
// Don't need to do anything here - it's enough to enable AND operator.
// OR is not supported at the same time.
}
/**
* Verify if property is expected, if not throws {@link InvalidQueryException}.
*/
protected void verifyPropertyExpectedness(final String propertyName)
{
if (validateStrictly && CollectionUtils.isNotEmpty(expectedPropertyNames) && !this.expectedPropertyNames.contains(propertyName))
{
throw new InvalidQueryException(String.format(PROPERTY_NOT_EXPECTED, propertyName));
}
else if (validateStrictly && CollectionUtils.isEmpty(expectedPropertyNames))
{
throw new IllegalStateException(PROPERTY_NAMES_EMPTY);
}
}
/**
* Verify if clause negations are allowed, if not throws {@link InvalidQueryException}.
*/
protected void verifyClausesNegatability(final boolean negated, final String propertyName)
{
if (!clausesNegatable && negated)
{
throw new InvalidQueryException(String.format(PROPERTY_NOT_NEGATABLE, propertyName));
}
}
protected boolean isAndSupported()
{
try
{
and();
return true;
}
catch (InvalidQueryException ignore)
{
return false;
}
}
protected void addProperties(final String propertyName, final int clauseType, final String... propertyValues)
{
this.addProperties(propertyName, clauseType, false, propertyValues);
}
protected void addProperties(final String propertyName, final int clauseType, final boolean negated, final String... propertyValues)
{
final WhereProperty.ClauseType type = WhereProperty.ClauseType.of(clauseType, negated);
final Set<String> propertiesToAdd = Optional.ofNullable(propertyValues).map(Set::of).orElse(Collections.emptySet());
if (this.containsProperty(propertyName))
{
this.properties.get(propertyName).addValuesToType(type, propertiesToAdd);
}
else
{
this.properties.put(propertyName, new WhereProperty(propertyName, type, propertiesToAdd, validateStrictly));
}
}
protected boolean containsProperty(final String propertyName)
{
return this.properties.containsKey(propertyName);
}
protected boolean containsProperty(final String propertyName, final int clauseType, final boolean negated)
{
return this.properties.containsKey(propertyName) && this.properties.get(propertyName).containsType(clauseType, negated);
}
@Override
public Collection<String> getProperty(String propertyName, int type, boolean negated)
{
return this.getProperty(propertyName).getExpectedValuesFor(type, negated);
}
public WhereProperty getProperty(final String propertyName)
{
if (validateStrictly && !this.containsProperty(propertyName))
{
throw new InvalidQueryException(String.format(MISSING_PROPERTY, propertyName));
}
return this.properties.get(propertyName);
}
public List<WhereProperty> getProperties(final String... propertyNames)
{
return Arrays.stream(propertyNames)
.filter(StringUtils::isNotBlank)
.distinct()
.peek(propertyName -> {
if (validateStrictly && !this.containsProperty(propertyName))
{
throw new InvalidQueryException(String.format(MISSING_PROPERTY, propertyName));
}
})
.map(this.properties::get)
.collect(Collectors.toList());
}
public Map<String, WhereProperty> getPropertiesAsMap(final String... propertyNames)
{
return Arrays.stream(propertyNames)
.filter(StringUtils::isNotBlank)
.distinct()
.peek(propertyName -> {
if (validateStrictly && !this.containsProperty(propertyName))
{
throw new InvalidQueryException(String.format(MISSING_PROPERTY, propertyName));
}
})
.collect(Collectors.toMap(propertyName -> propertyName, this.properties::get));
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,10 +25,19 @@
*/
package org.alfresco.rest.framework.resource.parameters.where;
import static org.alfresco.rest.antlr.WhereClauseParser.BETWEEN;
import static org.alfresco.rest.antlr.WhereClauseParser.EQUALS;
import static org.alfresco.rest.antlr.WhereClauseParser.EXISTS;
import static org.alfresco.rest.antlr.WhereClauseParser.IN;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.antlr.runtime.tree.CommonTree;
@@ -45,14 +54,19 @@ public abstract class QueryHelper
/**
* An interface used when walking a query tree. Calls are made to methods when the particular clause is encountered.
*/
public static interface WalkerCallback
public interface WalkerCallback
{
InvalidQueryException UNSUPPORTED = new InvalidQueryException("Unsupported Predicate");
/**
* Called any time an EXISTS clause is encountered.
* @param propertyName Name of the property
* @param negated returns true if "NOT EXISTS" was used
*/
void exists(String propertyName, boolean negated);
default void exists(String propertyName, boolean negated)
{
throw UNSUPPORTED;
}
/**
* Called any time a BETWEEN clause is encountered.
@@ -61,12 +75,18 @@ public abstract class QueryHelper
* @param secondValue String
* @param negated returns true if "NOT BETWEEN" was used
*/
void between(String propertyName, String firstValue, String secondValue, boolean negated);
default void between(String propertyName, String firstValue, String secondValue, boolean negated)
{
throw UNSUPPORTED;
}
/**
* One of EQUALS LESSTHAN GREATERTHAN LESSTHANOREQUALS GREATERTHANOREQUALS;
*/
void comparison(int type, String propertyName, String propertyValue, boolean negated);
default void comparison(int type, String propertyName, String propertyValue, boolean negated)
{
throw UNSUPPORTED;
}
/**
* Called any time an IN clause is encountered.
@@ -74,7 +94,10 @@ public abstract class QueryHelper
* @param negated returns true if "NOT IN" was used
* @param propertyValues the property values
*/
void in(String property, boolean negated, String... propertyValues);
default void in(String property, boolean negated, String... propertyValues)
{
throw UNSUPPORTED;
}
/**
* Called any time a MATCHES clause is encountered.
@@ -82,42 +105,37 @@ public abstract class QueryHelper
* @param propertyValue String
* @param negated returns true if "NOT MATCHES" was used
*/
void matches(String property, String propertyValue, boolean negated);
default void matches(String property, String propertyValue, boolean negated)
{
throw UNSUPPORTED;
}
/**
* Called any time an AND is encountered.
*/
void and();
*/
default void and()
{
throw UNSUPPORTED;
}
/**
* Called any time an OR is encountered.
*/
void or();
*/
default void or()
{
throw UNSUPPORTED;
}
default Collection<String> getProperty(String propertyName, int type, boolean negated)
{
throw UNSUPPORTED;
}
}
/**
* Default implementation. Override the methods you are interested in. If you don't
* override the methods then an InvalidQueryException will be thrown.
*/
public static class WalkerCallbackAdapter implements WalkerCallback
{
private static final String UNSUPPORTED_TEXT = "Unsupported Predicate";
protected static final InvalidQueryException UNSUPPORTED = new InvalidQueryException(UNSUPPORTED_TEXT);
@Override
public void exists(String propertyName, boolean negated) { throw UNSUPPORTED;}
@Override
public void between(String propertyName, String firstValue, String secondValue, boolean negated) { throw UNSUPPORTED;}
@Override
public void comparison(int type, String propertyName, String propertyValue, boolean negated) { throw UNSUPPORTED;}
@Override
public void in(String propertyName, boolean negated, String... propertyValues) { throw UNSUPPORTED;}
@Override
public void matches(String property, String value, boolean negated) { throw UNSUPPORTED;}
@Override
public void and() {throw UNSUPPORTED;}
@Override
public void or() {throw UNSUPPORTED;}
}
public static class WalkerCallbackAdapter implements WalkerCallback {}
/**
* Walks a query with a callback for each operation
@@ -146,7 +164,7 @@ public abstract class QueryHelper
if (tree != null)
{
switch (tree.getType()) {
case WhereClauseParser.EXISTS:
case EXISTS:
if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
{
callback.exists(tree.getChild(0).getText(), negated);
@@ -160,7 +178,7 @@ public abstract class QueryHelper
return;
}
break;
case WhereClauseParser.IN:
case IN:
if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
{
List<Tree> children = getChildren(tree);
@@ -174,14 +192,14 @@ public abstract class QueryHelper
return;
}
break;
case WhereClauseParser.BETWEEN:
case BETWEEN:
if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
{
callback.between(tree.getChild(0).getText(), stripQuotes(tree.getChild(1).getText()), stripQuotes(tree.getChild(2).getText()), negated);
return;
}
break;
case WhereClauseParser.EQUALS: //fall through (comparison)
case EQUALS: //fall through (comparison)
case WhereClauseParser.LESSTHAN: //fall through (comparison)
case WhereClauseParser.GREATERTHAN: //fall through (comparison)
case WhereClauseParser.LESSTHANOREQUALS: //fall through (comparison)
@@ -286,4 +304,180 @@ public abstract class QueryHelper
}
return toBeStripped; //default to return the String unchanged.
}
public static QueryResolver.WalkerSpecifier resolve(final Query query)
{
return new QueryResolver.WalkerSpecifier(query);
}
/**
* Helper class allowing WHERE query resolving using query walker. By default {@link BasicQueryWalker} is used, but different walker can be supplied.
*/
public static abstract class QueryResolver<S extends QueryResolver<?>>
{
private final Query query;
protected WalkerCallback queryWalker;
protected Function<Collection<String>, BasicQueryWalker> orQueryWalkerSupplier;
protected boolean clausesNegatable = true;
protected boolean validateLeniently = false;
protected abstract S self();
public QueryResolver(Query query)
{
this.query = query;
}
/**
* Get property expected values.
* @param propertyName Property name.
* @param clauseType Property comparison type.
* @param negated Comparison type negation.
* @return Map composed of all comparators and compared values.
*/
public Collection<String> getProperty(final String propertyName, final int clauseType, final boolean negated)
{
processQuery(propertyName);
return queryWalker.getProperty(propertyName, clauseType, negated);
}
protected void processQuery(final String... propertyNames)
{
if (queryWalker == null)
{
if (orQueryWalkerSupplier != null)
{
queryWalker = orQueryWalkerSupplier.apply(Set.of(propertyNames));
}
else
{
queryWalker = new BasicQueryWalker(propertyNames);
}
}
if (queryWalker instanceof BasicQueryWalker)
{
((BasicQueryWalker) queryWalker).setClausesNegatable(clausesNegatable);
((BasicQueryWalker) queryWalker).setValidateStrictly(!validateLeniently);
}
walk(query, queryWalker);
}
/**
* Helper class providing methods related with default query walker {@link BasicQueryWalker}.
*/
public static class DefaultWalkerOperations<R extends DefaultWalkerOperations<?>> extends QueryResolver<R>
{
public DefaultWalkerOperations(Query query)
{
super(query);
}
@SuppressWarnings("unchecked")
@Override
protected R self()
{
return (R) this;
}
/**
* Specifies that query properties and comparison types should NOT be verified strictly.
*/
public R leniently()
{
this.validateLeniently = true;
return self();
}
/**
* Specifies that clause types negations are not allowed in query.
*/
public R withoutNegations()
{
this.clausesNegatable = false;
return self();
}
/**
* Get property with expected values.
* @param propertyName Property name.
* @return Map composed of all comparators and compared values.
*/
public WhereProperty getProperty(final String propertyName)
{
processQuery(propertyName);
return ((BasicQueryWalker) this.queryWalker).getProperty(propertyName);
}
/**
* Get multiple properties with it's expected values.
* @param propertyNames Property names.
* @return List of maps composed of all comparators and compared values.
*/
public List<WhereProperty> getProperties(final String... propertyNames)
{
processQuery(propertyNames);
return ((BasicQueryWalker) this.queryWalker).getProperties(propertyNames);
}
/**
* Get multiple properties with it's expected values.
* @param propertyNames Property names.
* @return Map composed of property names and maps composed of all comparators and compared values.
*/
public Map<String, WhereProperty> getPropertiesAsMap(final String... propertyNames)
{
processQuery(propertyNames);
return ((BasicQueryWalker) this.queryWalker).getPropertiesAsMap(propertyNames);
}
}
/**
* Helper class allowing to specify custom {@link WalkerCallback} implementation or {@link BasicQueryWalker} extension.
*/
public static class WalkerSpecifier extends DefaultWalkerOperations<WalkerSpecifier>
{
public WalkerSpecifier(Query query)
{
super(query);
}
@Override
protected WalkerSpecifier self()
{
return this;
}
/**
* Specifies that OR operator instead of AND should be used while resolving the query.
*/
public DefaultWalkerOperations<? extends DefaultWalkerOperations<?>> usingOrOperator()
{
this.orQueryWalkerSupplier = (propertyNames) -> new BasicQueryWalker(propertyNames)
{
@Override
public void or() {/*Enable OR support, disable AND support*/}
@Override
public void and() {throw UNSUPPORTED;}
};
return this;
}
/**
* Allows to specify custom {@link BasicQueryWalker} extension, which should be used to resolve the query.
*/
public <T extends BasicQueryWalker> DefaultWalkerOperations<? extends DefaultWalkerOperations<?>> usingWalker(final T queryWalker)
{
this.queryWalker = queryWalker;
return this;
}
/**
* Allows to specify custom {@link WalkerCallback} implementation, which should be used to resolve the query.
*/
public <T extends WalkerCallback> QueryResolver<? extends QueryResolver<?>> usingWalker(final T queryWalker)
{
this.queryWalker = queryWalker;
return this;
}
}
}
}

View File

@@ -0,0 +1,351 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.framework.resource.parameters.where;
import static java.util.function.Predicate.not;
import static org.alfresco.rest.framework.resource.parameters.where.BasicQueryWalker.MISSING_ANY_CLAUSE_OF_TYPE;
import static org.alfresco.rest.framework.resource.parameters.where.BasicQueryWalker.MISSING_CLAUSE_TYPE;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.alfresco.rest.antlr.WhereClauseParser;
/**
* Map composed of property comparison type and compared values.
* Map key is clause (comparison) type.
*/
public class WhereProperty extends HashMap<WhereProperty.ClauseType, Collection<String>>
{
private final String name;
private boolean validateStrictly;
public WhereProperty(final String name, final ClauseType clauseType, final Collection<String> values)
{
super(Map.of(clauseType, new HashSet<>(values)));
this.name = name;
this.validateStrictly = true;
}
public WhereProperty(final String name, final ClauseType clauseType, final Collection<String> values, final boolean validateStrictly)
{
this(name, clauseType, values);
this.validateStrictly = validateStrictly;
}
public String getName()
{
return name;
}
public void addValuesToType(final ClauseType clauseType, final Collection<String> values)
{
if (this.containsKey(clauseType))
{
this.get(clauseType).addAll(values);
}
else
{
this.put(clauseType, new HashSet<>(values));
}
}
public boolean containsType(final ClauseType clauseType)
{
return this.containsKey(clauseType);
}
public boolean containsType(final int clauseType, final boolean negated)
{
return this.containsKey(ClauseType.of(clauseType, negated));
}
public boolean containsAllTypes(final ClauseType... clauseType)
{
return Arrays.stream(clauseType).distinct().filter(this::containsKey).count() == clauseType.length;
}
public boolean containsAnyOfTypes(final ClauseType... clauseType)
{
return Arrays.stream(clauseType).distinct().anyMatch(this::containsKey);
}
public Collection<String> getExpectedValuesFor(final ClauseType clauseType)
{
verifyAllClausesPresence(clauseType);
return this.get(clauseType);
}
public HashMap<ClauseType, Collection<String>> getExpectedValuesForAllOf(final ClauseType... clauseTypes)
{
verifyAllClausesPresence(clauseTypes);
return Arrays.stream(clauseTypes)
.distinct()
.collect(Collectors.toMap(type -> type, this::get, (type1, type2) -> type1, MultiTypeNegatableValuesMap::new));
}
public HashMap<ClauseType, Collection<String>> getExpectedValuesForAnyOf(final ClauseType... clauseTypes)
{
verifyAnyClausesPresence(clauseTypes);
return Arrays.stream(clauseTypes)
.distinct()
.collect(Collectors.toMap(type -> type, this::get, (type1, type2) -> type1, MultiTypeNegatableValuesMap::new));
}
public Collection<String> getExpectedValuesFor(final int clauseType, final boolean negated)
{
verifyAllClausesPresence(ClauseType.of(clauseType, negated));
return this.get(ClauseType.of(clauseType, negated));
}
public NegatableValuesMap getExpectedValuesFor(final int clauseType)
{
verifyAllClausesPresence(clauseType);
final NegatableValuesMap values = new NegatableValuesMap();
final ClauseType type = ClauseType.of(clauseType);
final ClauseType negatedType = type.negate();
if (this.containsKey(type))
{
values.put(false, this.get(type));
}
if (this.containsKey(negatedType))
{
values.put(true, this.get(negatedType));
}
return values;
}
public MultiTypeNegatableValuesMap getExpectedValuesForAllOf(final int... clauseTypes)
{
verifyAllClausesPresence(clauseTypes);
return getExpectedValuesFor(clauseTypes);
}
public MultiTypeNegatableValuesMap getExpectedValuesForAnyOf(final int... clauseTypes)
{
verifyAnyClausesPresence(clauseTypes);
return getExpectedValuesFor(clauseTypes);
}
private MultiTypeNegatableValuesMap getExpectedValuesFor(final int... clauseTypes)
{
final MultiTypeNegatableValuesMap values = new MultiTypeNegatableValuesMap();
Arrays.stream(clauseTypes).distinct().forEach(clauseType -> {
final ClauseType type = ClauseType.of(clauseType);
final ClauseType negatedType = type.negate();
if (this.containsKey(type))
{
values.put(type, this.get(type));
}
if (this.containsKey(negatedType))
{
values.put(negatedType, this.get(negatedType));
}
});
return values;
}
/**
* Verify if all specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
*/
private void verifyAllClausesPresence(final ClauseType... clauseTypes)
{
if (validateStrictly)
{
Arrays.stream(clauseTypes).distinct().forEach(clauseType -> {
if (!this.containsType(clauseType))
{
throw new InvalidQueryException(String.format(MISSING_CLAUSE_TYPE, this.name, WhereClauseParser.tokenNames[clauseType.getTypeNumber()]));
}
});
}
}
/**
* Verify if all specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
* Exception is thrown when both, negated and non-negated types are missing.
*/
private void verifyAllClausesPresence(final int... clauseTypes)
{
if (validateStrictly)
{
Arrays.stream(clauseTypes).distinct().forEach(clauseType -> {
if (!this.containsType(clauseType, false) && !this.containsType(clauseType, true))
{
throw new InvalidQueryException(String.format(MISSING_CLAUSE_TYPE, this.name, WhereClauseParser.tokenNames[clauseType]));
}
});
}
}
/**
* Verify if any of specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
*/
private void verifyAnyClausesPresence(final ClauseType... clauseTypes)
{
if (validateStrictly)
{
if (!this.containsAnyOfTypes(clauseTypes))
{
throw new InvalidQueryException(String.format(MISSING_ANY_CLAUSE_OF_TYPE,
this.name, Arrays.stream(clauseTypes).map(type -> WhereClauseParser.tokenNames[type.getTypeNumber()]).collect(Collectors.toList())));
}
}
}
/**
* Verify if any of specified clause types are present in this map, if not than throw {@link InvalidQueryException}.
* Exception is thrown when both, negated and non-negated types are missing.
*/
private void verifyAnyClausesPresence(final int... clauseTypes)
{
if (validateStrictly)
{
final Collection<ClauseType> expectedTypes = Arrays.stream(clauseTypes)
.distinct()
.boxed()
.flatMap(type -> Stream.of(ClauseType.of(type), ClauseType.of(type, true)))
.collect(Collectors.toSet());
if (!this.containsAnyOfTypes(expectedTypes.toArray(ClauseType[]::new)))
{
throw new InvalidQueryException(String.format(MISSING_ANY_CLAUSE_OF_TYPE,
this.name, Arrays.stream(clauseTypes).mapToObj(type -> WhereClauseParser.tokenNames[type]).collect(Collectors.toList())));
}
}
}
public enum ClauseType
{
EQUALS(WhereClauseParser.EQUALS),
NOT_EQUALS(WhereClauseParser.EQUALS, true),
GREATER_THAN(WhereClauseParser.GREATERTHAN),
NOT_GREATER_THAN(WhereClauseParser.GREATERTHAN, true),
LESS_THAN(WhereClauseParser.LESSTHAN),
NOT_LESS_THAN(WhereClauseParser.LESSTHAN, true),
GREATER_THAN_OR_EQUALS(WhereClauseParser.GREATERTHANOREQUALS),
NOT_GREATER_THAN_OR_EQUALS(WhereClauseParser.GREATERTHANOREQUALS, true),
LESS_THAN_OR_EQUALS(WhereClauseParser.LESSTHANOREQUALS),
NOT_LESS_THAN_OR_EQUALS(WhereClauseParser.LESSTHANOREQUALS, true),
BETWEEN(WhereClauseParser.BETWEEN),
NOT_BETWEEN(WhereClauseParser.BETWEEN, true),
IN(WhereClauseParser.IN),
NOT_IN(WhereClauseParser.IN, true),
MATCHES(WhereClauseParser.MATCHES),
NOT_MATCHES(WhereClauseParser.MATCHES, true),
EXISTS(WhereClauseParser.EXISTS),
NOT_EXISTS(WhereClauseParser.EXISTS, true);
private final int typeNumber;
private final boolean negated;
ClauseType(final int typeNumber)
{
this.typeNumber = typeNumber;
this.negated = false;
}
ClauseType(final int typeNumber, final boolean negated)
{
this.typeNumber = typeNumber;
this.negated = negated;
}
public static ClauseType of(final int type)
{
return of(type, false);
}
public static ClauseType of(final int type, final boolean negated)
{
return Arrays.stream(ClauseType.values())
.filter(clauseType -> clauseType.typeNumber == type && clauseType.negated == negated)
.findFirst()
.orElseThrow();
}
public ClauseType negate()
{
return of(typeNumber, !negated);
}
public int getTypeNumber()
{
return typeNumber;
}
public boolean isNegated()
{
return negated;
}
}
public static class NegatableValuesMap extends HashMap<Boolean, Collection<String>>
{
public Collection<String> skipNegated()
{
return this.get(false);
}
public Collection<String> onlyNegated()
{
return this.get(true);
}
}
public static class MultiTypeNegatableValuesMap extends HashMap<ClauseType, Collection<String>>
{
public Map<Integer, Collection<String>> skipNegated()
{
return this.keySet().stream()
.filter(not(ClauseType::isNegated))
.collect(Collectors.toMap(key -> key.typeNumber, this::get));
}
public Collection<String> skipNegated(final int clauseType)
{
return this.get(ClauseType.of(clauseType));
}
public Map<Integer, Collection<String>> onlyNegated()
{
return this.keySet().stream()
.filter(not(ClauseType::isNegated))
.collect(Collectors.toMap(key -> key.typeNumber, this::get));
}
public Collection<String> onlyNegated(final int clauseType)
{
return this.get(ClauseType.of(clauseType, true));
}
}
}

View File

@@ -1,32 +1,33 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.workflow.api.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -51,7 +52,7 @@ import org.apache.commons.beanutils.ConvertUtils;
* {@link InvalidArgumentException} is thrown unless the method
* {@link #handleUnmatchedComparison(int, String, String)} returns true (default
* implementation returns false).
*
*
* @author Frederik Heremans
* @author Tijs Rademakers
*/
@@ -72,21 +73,21 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
private Map<String, String> equalsProperties;
private Map<String, String> matchesProperties;
private Map<String, String> greaterThanProperties;
private Map<String, String> greaterThanOrEqualProperties;
private Map<String, String> lessThanProperties;
private Map<String, String> lessThanOrEqualProperties;
private List<QueryVariableHolder> variableProperties;
private boolean variablesEnabled;
private NamespaceService namespaceService;
private DictionaryService dictionaryService;
public MapBasedQueryWalker(Set<String> supportedEqualsParameters, Set<String> supportedMatchesParameters)
@@ -132,7 +133,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
lessThanOrEqualProperties = new HashMap<String, String>();
}
}
public void enableVariablesSupport(NamespaceService namespaceService, DictionaryService dictionaryService)
{
variablesEnabled = true;
@@ -148,7 +149,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
this.dictionaryService = dictionaryService;
variableProperties = new ArrayList<QueryVariableHolder>();
}
public List<QueryVariableHolder> getVariableProperties() {
return variableProperties;
}
@@ -158,9 +159,9 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
if(negated)
{
throw new InvalidArgumentException("Cannot use negated matching for property: " + property);
throw new InvalidArgumentException("Cannot use negated matching for property: " + property);
}
if (variablesEnabled && property.startsWith("variables/"))
if (variablesEnabled && property.startsWith("variables/"))
{
processVariable(property, value, WhereClauseParser.MATCHES);
}
@@ -170,19 +171,19 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throw new InvalidArgumentException("Cannot use matching for property: " + property);
throw new InvalidArgumentException("Cannot use matching for property: " + property);
}
}
@Override
public void comparison(int type, String propertyName, String propertyValue, boolean negated)
{
if (variablesEnabled && propertyName.startsWith("variables/"))
if (variablesEnabled && propertyName.startsWith("variables/"))
{
processVariable(propertyName, propertyValue, type);
processVariable(propertyName, propertyValue, type);
return;
}
}
boolean throwError = false;
if (type == WhereClauseParser.EQUALS)
{
@@ -192,7 +193,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.MATCHES)
@@ -203,7 +204,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.GREATERTHAN)
@@ -214,7 +215,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.GREATERTHANOREQUALS)
@@ -225,7 +226,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.LESSTHAN)
@@ -236,7 +237,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else if (type == WhereClauseParser.LESSTHANOREQUALS)
@@ -247,7 +248,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
else
{
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
}
else
@@ -255,15 +256,24 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
}
if (throwError)
{
throw new InvalidArgumentException("framework.exception.InvalidProperty", new Object[] {propertyName, propertyValue, WhereClauseParser.tokenNames[type]});
}
else if (negated)
{
// Throw error for the unsupported negation only if the property was valid for comparison, show the more meaningful error first.
throw new InvalidArgumentException("Cannot use NOT for " + WhereClauseParser.tokenNames[type] + " comparison.");
if (throwError)
{
throw new InvalidArgumentException("framework.exception.InvalidProperty", new Object[] {propertyName, propertyValue, WhereClauseParser.tokenNames[type]});
}
else if (negated)
{
// Throw error for the unsupported negation only if the property was valid for comparison, show the more meaningful error first.
throw new InvalidArgumentException("Cannot use NOT for " + WhereClauseParser.tokenNames[type] + " comparison.");
}
}
/**
* Get expected value for property and comparison type. This class supports only non-negated comparisons, thus parameter negated is ignored in bellow method.
*/
@Override
public Collection<String> getProperty(String propertyName, int type, boolean negated)
{
return Set.of(this.getProperty(propertyName, type));
}
public String getProperty(String propertyName, int type)
@@ -300,7 +310,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
/**
* Get the property value, converted to the requested type.
*
*
* @param propertyName name of the parameter
* @param type int
* @param returnType type of object to return
@@ -334,7 +344,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
// Conversion failed, wrap in Illegal
throw new InvalidArgumentException("Query property value for '" + propertyName + "' should be a valid "
+ returnType.getSimpleName());
+ returnType.getSimpleName());
}
}
@@ -345,7 +355,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
// method indicates that AND is
// supported. OR is not supported at the same time.
}
protected void processVariable(String propertyName, String propertyValue, int type)
{
String localPropertyName = propertyName.replaceFirst("variables/", "");
@@ -353,25 +363,25 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
DataTypeDefinition dataTypeDefinition = null;
// variable scope global is default
String scopeDef = "global";
// look for variable scope
if (localPropertyName.contains("local/"))
{
scopeDef = "local";
localPropertyName = localPropertyName.replaceFirst("local/", "");
}
if (localPropertyName.contains("global/"))
{
localPropertyName = localPropertyName.replaceFirst("global/", "");
}
// look for variable type definition
if ((propertyValue.contains("_") || propertyValue.contains(":")) && propertyValue.contains(" "))
if ((propertyValue.contains("_") || propertyValue.contains(":")) && propertyValue.contains(" "))
{
int indexOfSpace = propertyValue.indexOf(' ');
if ((propertyValue.contains("_") && indexOfSpace > propertyValue.indexOf("_")) ||
(propertyValue.contains(":") && indexOfSpace > propertyValue.indexOf(":")))
if ((propertyValue.contains("_") && indexOfSpace > propertyValue.indexOf("_")) ||
(propertyValue.contains(":") && indexOfSpace > propertyValue.indexOf(":")))
{
String typeDef = propertyValue.substring(0, indexOfSpace);
try
@@ -386,7 +396,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
}
}
}
if (dataTypeDefinition != null && "java.util.Date".equalsIgnoreCase(dataTypeDefinition.getJavaClassName()))
{
// fix for different ISO 8601 Date format classes in Alfresco (org.alfresco.util and Spring Surf)
@@ -396,18 +406,18 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
actualValue = DefaultTypeConverter.INSTANCE.convert(dataTypeDefinition, propertyValue);
}
else
else
{
actualValue = propertyValue;
}
variableProperties.add(new QueryVariableHolder(localPropertyName, type, actualValue, scopeDef));
}
/**
* Called when unsupported property is encountered or comparison operator
* other than equals.
*
*
* @return true, if the comparison is handles successfully. False, if an
* exception should be thrown because the comparison can't be
* handled.
@@ -416,25 +426,25 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
{
return false;
}
public static class QueryVariableHolder implements Serializable
{
private static final long serialVersionUID = 1L;
private String propertyName;
private int operator;
private Object propertyValue;
private String scope;
public QueryVariableHolder() {}
public QueryVariableHolder(String propertyName, int operator, Object propertyValue, String scope) {
this.propertyName = propertyName;
this.operator = operator;
this.propertyValue = propertyValue;
this.scope = scope;
}
public String getPropertyName()
{
return propertyName;

View File

@@ -856,6 +856,7 @@
<bean id="tags" class="org.alfresco.rest.api.impl.TagsImpl">
<property name="nodes" ref="nodes" />
<property name="taggingService" ref="TaggingService" />
<property name="authorityService" ref="AuthorityService" />
<property name="typeConstraint" ref="nodeTypeConstraint" />
</bean>

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -49,6 +49,8 @@ import org.junit.runners.Suite;
org.alfresco.repo.webdav.WebDAVHelperTest.class,
org.alfresco.repo.webdav.WebDAVLockServiceImplTest.class,
org.alfresco.rest.api.RulesUnitTests.class,
org.alfresco.rest.api.CategoriesUnitTests.class,
org.alfresco.rest.api.TagsUnitTests.class,
org.alfresco.rest.api.impl.ContentStorageInformationImplTest.class,
org.alfresco.rest.api.nodes.NodeStorageInfoRelationTest.class,
org.alfresco.rest.api.search.ResultMapperTests.class,

View File

@@ -0,0 +1,47 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api;
import org.alfresco.rest.api.categories.CategoriesEntityResourceTest;
import org.alfresco.rest.api.categories.NodesCategoryLinksRelationTest;
import org.alfresco.rest.api.categories.SubcategoriesRelationTest;
import org.alfresco.rest.api.impl.CategoriesImplTest;
import org.alfresco.service.Experimental;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@Experimental
@RunWith(Suite.class)
@Suite.SuiteClasses({
CategoriesImplTest.class,
CategoriesEntityResourceTest.class,
SubcategoriesRelationTest.class,
NodesCategoryLinksRelationTest.class
})
public class CategoriesUnitTests
{
}

View File

@@ -0,0 +1,42 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api;
import org.alfresco.rest.api.impl.TagsImplTest;
import org.alfresco.rest.api.tags.TagsEntityResourceTest;
import org.alfresco.service.Experimental;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@Experimental
@RunWith(Suite.class)
@Suite.SuiteClasses({
TagsImplTest.class,
TagsEntityResourceTest.class
})
public class TagsUnitTests
{
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,15 +26,21 @@
package org.alfresco.rest.api.categories;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willCallRealMethod;
import java.util.List;
import org.alfresco.rest.api.Categories;
import org.alfresco.rest.api.model.Category;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -45,6 +51,7 @@ import org.mockito.junit.MockitoJUnitRunner;
public class CategoriesEntityResourceTest
{
private static final String CATEGORY_ID = "category-node-id";
@Mock
private Categories categoriesMock;
@Mock
@@ -58,26 +65,37 @@ public class CategoriesEntityResourceTest
@Test
public void testReadCategoryById()
{
given(categoriesMock.getCategoryById(CATEGORY_ID, parametersMock)).willReturn(categoryMock);
given(categoriesMock.getCategoryById(any(), any())).willCallRealMethod();
given(categoriesMock.getCategoryById(any(), any(), any())).willReturn(categoryMock);
//when
final Category category = objectUnderTest.readById(CATEGORY_ID, parametersMock);
then(categoriesMock).should().getCategoryById(CATEGORY_ID, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
then(categoriesMock).should().getCategoryById(STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID, parametersMock);
assertEquals(categoryMock, category);
}
@Test
public void testUpdateCategoryById()
{
given(categoriesMock.updateCategoryById(any(), any())).willReturn(categoryMock);
given(categoriesMock.updateCategoryById(any(), any(), any())).willCallRealMethod();
given(categoriesMock.updateCategoryById(any(), any(), any(), any())).willReturn(categoryMock);
// when
final Category actualCategory = objectUnderTest.update(CATEGORY_ID, categoryMock, parametersMock);
then(categoriesMock).should().updateCategoryById(CATEGORY_ID, categoryMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
then(categoriesMock).should().updateCategoryById(STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID, categoryMock, parametersMock);
assertThat(actualCategory).isNotNull();
}
@Test
public void testDeleteCategoryById()
{
willCallRealMethod().given(categoriesMock).deleteCategoryById(any(), any());
// when
objectUnderTest.delete(CATEGORY_ID, parametersMock);
then(categoriesMock).should().deleteCategoryById(STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID, parametersMock);
}
}

View File

@@ -47,6 +47,7 @@ import org.mockito.junit.MockitoJUnitRunner;
public class NodesCategoryLinksRelationTest
{
private static final String CONTENT_ID = "content-node-id";
private static final String CATEGORY_ID = "category-id";
@Mock
private Categories categoriesMock;
@@ -61,12 +62,12 @@ public class NodesCategoryLinksRelationTest
@Test
public void testReadAll()
{
given(categoriesMock.listCategoriesForNode(any())).willReturn(List.of(categoryMock));
given(categoriesMock.listCategoriesForNode(any(), any())).willReturn(List.of(categoryMock));
// when
final CollectionWithPagingInfo<Category> actualCategoriesPage = objectUnderTest.readAll(CONTENT_ID, parametersMock);
then(categoriesMock).should().listCategoriesForNode(CONTENT_ID);
then(categoriesMock).should().listCategoriesForNode(CONTENT_ID, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
assertThat(actualCategoriesPage)
.isNotNull()
@@ -77,15 +78,25 @@ public class NodesCategoryLinksRelationTest
@Test
public void testCreate()
{
given(categoriesMock.linkNodeToCategories(any(), any())).willReturn(List.of(categoryMock));
given(categoriesMock.linkNodeToCategories(any(), any(), any())).willReturn(List.of(categoryMock));
// when
final List<Category> actualCategories = objectUnderTest.create(CONTENT_ID, List.of(categoryMock), parametersMock);
then(categoriesMock).should().linkNodeToCategories(CONTENT_ID, List.of(categoryMock));
then(categoriesMock).should().linkNodeToCategories(CONTENT_ID, List.of(categoryMock), parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
assertThat(actualCategories)
.isNotNull()
.isEqualTo(List.of(categoryMock));
}
@Test
public void testDelete()
{
// when
objectUnderTest.delete(CONTENT_ID, CATEGORY_ID, parametersMock);
then(categoriesMock).should().unlinkNodeFromCategory(CONTENT_ID, CATEGORY_ID, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,7 +26,10 @@
package org.alfresco.rest.api.categories;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@@ -37,8 +40,8 @@ import java.util.stream.IntStream;
import org.alfresco.rest.api.Categories;
import org.alfresco.rest.api.model.Category;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -67,39 +70,37 @@ public class SubcategoriesRelationTest
final Category categoryToCreate = Category.builder().name(CATEGORY_NAME).create();
final Category category = Category.builder().name(CATEGORY_NAME).parentId(PARENT_CATEGORY_ID).hasChildren(false).id(CATEGORY_ID).create();
final List<Category> categoriesToCreate = List.of(categoryToCreate);
given(categoriesMock.createSubcategories(PARENT_CATEGORY_ID, categoriesToCreate, parametersMock)).willReturn(List.of(category));
given(categoriesMock.createSubcategories(any(), any(), any())).willCallRealMethod();
given(categoriesMock.createSubcategories(any(), any(), any(), any())).willReturn(List.of(category));
//when
List<Category> categories = objectUnderTest.create(PARENT_CATEGORY_ID, categoriesToCreate, parametersMock);
then(categoriesMock).should().createSubcategories(PARENT_CATEGORY_ID, categoriesToCreate, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
then(categoriesMock).should().createSubcategories(STORE_REF_WORKSPACE_SPACESSTORE, PARENT_CATEGORY_ID, categoriesToCreate, parametersMock);
assertEquals(List.of(category), categories);
}
@Test
public void testGetCategoryChildren() {
final CollectionWithPagingInfo<Category> categoryChildren = getCategories(3);
given(categoriesMock.getCategoryChildren(PARENT_CATEGORY_ID, parametersMock)).willReturn(categoryChildren);
final List<Category> categoryChildren = getCategories(3);
given(categoriesMock.getCategoryChildren(any(), any())).willCallRealMethod();
given(categoriesMock.getCategoryChildren(any(), any(), any())).willReturn(categoryChildren);
//when
final CollectionWithPagingInfo<Category> returnedChildren = objectUnderTest.readAll(PARENT_CATEGORY_ID, parametersMock);
then(categoriesMock).should().getCategoryChildren(PARENT_CATEGORY_ID, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
assertEquals(categoryChildren, returnedChildren);
then(categoriesMock).should().getCategoryChildren(STORE_REF_WORKSPACE_SPACESSTORE, PARENT_CATEGORY_ID, parametersMock);
assertEquals(categoryChildren, returnedChildren.getCollection());
}
private CollectionWithPagingInfo<Category> getCategories(final int count)
private List<Category> getCategories(final int count)
{
return CollectionWithPagingInfo.asPaged(Paging.DEFAULT,
IntStream.range(0, count)
.mapToObj(i -> Category.builder().name(SUBCATEGORY_NAME_PREFIX + "-" + i)
.parentId(PARENT_CATEGORY_ID)
.hasChildren(false)
.id(CATEGORY_ID + "-" + i)
.create())
.collect(Collectors.toList())
);
return IntStream.range(0, count)
.mapToObj(i -> Category.builder().name(SUBCATEGORY_NAME_PREFIX + "-" + i)
.parentId(PARENT_CATEGORY_ID)
.hasChildren(false)
.id(CATEGORY_ID + "-" + i)
.create())
.collect(Collectors.toList());
}
}

View File

@@ -27,11 +27,13 @@
package org.alfresco.rest.api.impl;
import static org.alfresco.rest.api.Nodes.PATH_ROOT;
import static org.alfresco.rest.api.impl.CategoriesImpl.INCLUDE_COUNT_PARAM;
import static org.alfresco.rest.api.impl.CategoriesImpl.INVALID_NODE_TYPE;
import static org.alfresco.rest.api.impl.CategoriesImpl.NOT_A_VALID_CATEGORY;
import static org.alfresco.rest.api.impl.CategoriesImpl.NOT_NULL_OR_EMPTY;
import static org.alfresco.rest.api.impl.CategoriesImpl.NO_PERMISSION_TO_CHANGE_CONTENT;
import static org.alfresco.rest.api.impl.CategoriesImpl.NO_PERMISSION_TO_READ_CONTENT;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.catchThrowable;
@@ -39,6 +41,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@@ -64,7 +67,6 @@ import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.InvalidNodeTypeException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -77,6 +79,7 @@ import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.Pair;
import org.alfresco.util.TypeConstraint;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
@@ -94,7 +97,7 @@ public class CategoriesImplTest
private static final String PARENT_ID = "parent-node-id";
private static final String CAT_ROOT_NODE_ID = "cat-root-node-id";
private static final NodeRef CATEGORY_NODE_REF = createNodeRefWithId(CATEGORY_ID);
private static final Category CATEGORY = createDefaultCategoryWithName(CATEGORY_NAME);
private static final Category CATEGORY = createDefaultCategory();
private static final String CONTENT_NODE_ID = "content-node-id";
private static final NodeRef CONTENT_NODE_REF = createNodeRefWithId(CONTENT_NODE_ID);
@@ -103,8 +106,6 @@ public class CategoriesImplTest
@Mock
private NodeService nodeServiceMock;
@Mock
private Parameters parametersMock;
@Mock
private AuthorityService authorityServiceMock;
@Mock
private CategoryService categoryServiceMock;
@@ -116,6 +117,8 @@ public class CategoriesImplTest
private PermissionService permissionServiceMock;
@Mock
private TypeConstraint typeConstraint;
@Mock
private Parameters parametersMock;
@InjectMocks
private CategoriesImpl objectUnderTest;
@@ -135,7 +138,7 @@ public class CategoriesImplTest
@Test
public void shouldNotGetRootCategoryById()
{
final NodeRef categoryRootNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CAT_ROOT_NODE_ID);
final NodeRef categoryRootNodeRef = createNodeRefWithId(CAT_ROOT_NODE_ID);
given(nodesMock.validateNode(CAT_ROOT_NODE_ID)).willReturn(categoryRootNodeRef);
given(categoryChildAssociationRefMock.getQName()).willReturn(ContentModel.ASPECT_GEN_CLASSIFIABLE);
given(nodeServiceMock.getParentAssocs(categoryRootNodeRef)).willReturn(List.of(categoryChildAssociationRefMock));
@@ -155,31 +158,26 @@ public class CategoriesImplTest
@Test
public void testGetCategoryById_withChildren()
{
final NodeRef categoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID);
given(nodesMock.validateNode(CATEGORY_ID)).willReturn(categoryNodeRef);
final Node categoryNode = new Node();
categoryNode.setName(CATEGORY_NAME);
categoryNode.setNodeId(CATEGORY_ID);
final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
categoryNode.setParentId(parentNodeRef);
final NodeRef parentNodeRef = createNodeRefWithId(PARENT_ID);
final Node categoryNode = createNode(CATEGORY_NAME, CATEGORY_ID, parentNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(categoryNode);
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentNodeRef, null, categoryNodeRef);
given(nodeServiceMock.getPrimaryParent(categoryNodeRef)).willReturn(parentAssoc);
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentNodeRef, null, CATEGORY_NODE_REF);
given(nodeServiceMock.getPrimaryParent(CATEGORY_NODE_REF)).willReturn(parentAssoc);
final List<ChildAssociationRef> dummyChildren = List.of(dummyChildAssociationRefMock);
given(nodeServiceMock.getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false))
given(nodeServiceMock.getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false))
.willReturn(dummyChildren);
//when
final Category category = objectUnderTest.getCategoryById(CATEGORY_ID, parametersMock);
then(nodesMock).should().validateNode(CATEGORY_ID);
then(nodesMock).should().isSubClass(categoryNodeRef, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).should().isSubClass(CATEGORY_NODE_REF, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).should().getNode(CATEGORY_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should().getPrimaryParent(categoryNodeRef);
then(nodeServiceMock).should().getPrimaryParent(CATEGORY_NODE_REF);
then(nodeServiceMock).should().getParentAssocs(parentNodeRef);
then(nodeServiceMock).should().getParentAssocs(categoryNodeRef);
then(nodeServiceMock).should().getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).should().getParentAssocs(CATEGORY_NODE_REF);
then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
@@ -197,30 +195,25 @@ public class CategoriesImplTest
@Test
public void testGetCategoryById_withoutChildren()
{
final NodeRef categoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID);
given(nodesMock.validateNode(CATEGORY_ID)).willReturn(categoryNodeRef);
final Node categoryNode = new Node();
categoryNode.setName(CATEGORY_NAME);
categoryNode.setNodeId(CATEGORY_ID);
final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
categoryNode.setParentId(parentNodeRef);
final NodeRef parentNodeRef = createNodeRefWithId(PARENT_ID);
final Node categoryNode = createNode(CATEGORY_NAME, CATEGORY_ID, parentNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(categoryNode);
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentNodeRef, null, categoryNodeRef);
given(nodeServiceMock.getPrimaryParent(categoryNodeRef)).willReturn(parentAssoc);
given(nodeServiceMock.getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false))
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentNodeRef, null, CATEGORY_NODE_REF);
given(nodeServiceMock.getPrimaryParent(CATEGORY_NODE_REF)).willReturn(parentAssoc);
given(nodeServiceMock.getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false))
.willReturn(Collections.emptyList());
//when
final Category category = objectUnderTest.getCategoryById(CATEGORY_ID, parametersMock);
then(nodesMock).should().validateNode(CATEGORY_ID);
then(nodesMock).should().isSubClass(categoryNodeRef, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).should().isSubClass(CATEGORY_NODE_REF, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).should().getNode(CATEGORY_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should().getPrimaryParent(categoryNodeRef);
then(nodeServiceMock).should().getPrimaryParent(CATEGORY_NODE_REF);
then(nodeServiceMock).should().getParentAssocs(parentNodeRef);
then(nodeServiceMock).should().getParentAssocs(categoryNodeRef);
then(nodeServiceMock).should().getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).should().getParentAssocs(CATEGORY_NODE_REF);
then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
@@ -235,6 +228,30 @@ public class CategoriesImplTest
assertEquals(expectedCategory, category);
}
@Test
public void testGetCategoryById_includeCount()
{
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(parametersMock.getInclude()).willReturn(List.of(INCLUDE_COUNT_PARAM));
given(categoryServiceMock.getTopCategories(any(), any(), anyInt())).willReturn(List.of(new Pair<>(CATEGORY_NODE_REF, 1)));
// when
final Category actualCategory = objectUnderTest.getCategoryById(CATEGORY_ID, parametersMock);
then(categoryServiceMock).should().getTopCategories(STORE_REF_WORKSPACE_SPACESSTORE, ContentModel.ASPECT_GEN_CLASSIFIABLE, Integer.MAX_VALUE);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualCategory)
.isNotNull()
.extracting(Category::getCount)
.isNotNull()
.isEqualTo(1);
}
@Test
public void testGetCategoryById_notACategory()
{
@@ -274,14 +291,9 @@ public class CategoriesImplTest
public void testDeleteCategoryById_asAdmin()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
final NodeRef categoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID);
final NodeRef categoryNodeRef = createNodeRefWithId(CATEGORY_ID);
given(nodesMock.validateNode(CATEGORY_ID)).willReturn(categoryNodeRef);
given(nodesMock.isSubClass(categoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
final Node categoryNode = new Node();
categoryNode.setName(CATEGORY_NAME);
categoryNode.setNodeId(CATEGORY_ID);
final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
categoryNode.setParentId(parentNodeRef);
//when
objectUnderTest.deleteCategoryById(CATEGORY_ID, parametersMock);
@@ -317,7 +329,7 @@ public class CategoriesImplTest
public void testDeleteCategoryById_nonCategoryId()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
final NodeRef categoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID);
final NodeRef categoryNodeRef = createNodeRefWithId(CATEGORY_ID);
given(nodesMock.validateNode(CATEGORY_ID)).willReturn(categoryNodeRef);
given(nodesMock.isSubClass(categoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(false);
@@ -336,7 +348,7 @@ public class CategoriesImplTest
public void testDeleteCategoryById_rootCategory()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
final NodeRef categoryRootNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CAT_ROOT_NODE_ID);
final NodeRef categoryRootNodeRef = createNodeRefWithId(CAT_ROOT_NODE_ID);
given(nodesMock.validateNode(CAT_ROOT_NODE_ID)).willReturn(categoryRootNodeRef);
given(nodesMock.isSubClass(categoryRootNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
given(categoryChildAssociationRefMock.getQName()).willReturn(ContentModel.ASPECT_GEN_CLASSIFIABLE);
@@ -359,13 +371,13 @@ public class CategoriesImplTest
@Test
public void testCreateCategoryUnderRoot()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PATH_ROOT);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PATH_ROOT);
given(categoryServiceMock.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE))
.willReturn(Optional.of(parentCategoryNodeRef));
final NodeRef categoryNodeRef = createNodeRefWithId(CATEGORY_ID);
given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(prepareCategoryNode());
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentCategoryNodeRef, null, categoryNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(createNode());
final ChildAssociationRef parentAssoc = createAssociationOf(parentCategoryNodeRef, categoryNodeRef);
given(nodeServiceMock.getPrimaryParent(categoryNodeRef)).willReturn(parentAssoc);
given(nodeServiceMock.getParentAssocs(parentCategoryNodeRef)).willReturn(List.of(parentAssoc));
given(nodeServiceMock.getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false))
@@ -400,12 +412,12 @@ public class CategoriesImplTest
@Test
public void testCreateCategory()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
final NodeRef categoryNodeRef = createNodeRefWithId(CATEGORY_ID);
given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(prepareCategoryNode());
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentCategoryNodeRef, null, categoryNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(createNode());
final ChildAssociationRef parentAssoc = createAssociationOf(parentCategoryNodeRef, categoryNodeRef);
given(nodeServiceMock.getPrimaryParent(categoryNodeRef)).willReturn(parentAssoc);
given(nodeServiceMock.getParentAssocs(parentCategoryNodeRef)).willReturn(List.of(parentAssoc));
given(nodeServiceMock.getChildAssocs(categoryNodeRef, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false))
@@ -438,6 +450,35 @@ public class CategoriesImplTest
assertEquals(expectedCategory, createdCategory);
}
@Test
public void testCreateCategory_includeCount()
{
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef categoryNodeRef = createNodeRefWithId(CATEGORY_ID);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef);
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(parametersMock.getInclude()).willReturn(List.of(INCLUDE_COUNT_PARAM));
final List<Category> categoryModels = prepareCategories().stream().peek(category -> category.setCount(1)).collect(Collectors.toList());
// when
final List<Category> actualCreatedCategories = objectUnderTest.createSubcategories(PARENT_ID, categoryModels, parametersMock);
then(categoryServiceMock).should().createCategory(any(), any());
then(categoryServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualCreatedCategories)
.isNotNull()
.hasSize(1)
.element(0)
.extracting(Category::getCount)
.isNotNull()
.isEqualTo(0);
}
@Test
public void testCreateCategories_noPermissions()
{
@@ -494,7 +535,7 @@ public class CategoriesImplTest
@Test
public void testGetRootCategoryChildren()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PATH_ROOT);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PATH_ROOT);
given(categoryServiceMock.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE))
.willReturn(Optional.of(parentCategoryNodeRef));
final int childrenCount = 3;
@@ -503,7 +544,7 @@ public class CategoriesImplTest
childAssociationRefMocks.forEach(this::prepareCategoryNodeMocks);
//when
final CollectionWithPagingInfo<Category> categoryChildren = objectUnderTest.getCategoryChildren(PATH_ROOT, parametersMock);
final List<Category> categoryChildren = objectUnderTest.getCategoryChildren(PATH_ROOT, parametersMock);
then(categoryServiceMock).should().getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
@@ -521,16 +562,14 @@ public class CategoriesImplTest
then(authorityServiceMock).shouldHaveNoInteractions();
assertEquals(childAssociationRefMocks.size(), categoryChildren.getTotalItems().intValue());
final List<Category> categoryChildrenList = new ArrayList<>(categoryChildren.getCollection());
assertEquals(childAssociationRefMocks.size(), categoryChildrenList.size());
IntStream.range(0, childrenCount).forEach(i -> doCategoryAssertions(categoryChildrenList.get(i), i, PATH_ROOT));
assertEquals(childAssociationRefMocks.size(), categoryChildren.size());
IntStream.range(0, childrenCount).forEach(i -> doCategoryAssertions(categoryChildren.get(i), i, PATH_ROOT));
}
@Test
public void testGetCategoryChildren()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
final int childrenCount = 3;
@@ -539,7 +578,7 @@ public class CategoriesImplTest
childAssociationRefMocks.forEach(this::prepareCategoryNodeMocks);
//when
final CollectionWithPagingInfo<Category> categoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
final List<Category> categoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
then(nodesMock).should().validateNode(PARENT_ID);
then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
@@ -558,24 +597,47 @@ public class CategoriesImplTest
then(authorityServiceMock).shouldHaveNoInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
assertEquals(childAssociationRefMocks.size(), categoryChildren.getTotalItems().intValue());
final List<Category> categoryChildrenList = new ArrayList<>(categoryChildren.getCollection());
assertEquals(childAssociationRefMocks.size(), categoryChildrenList.size());
IntStream.range(0, childrenCount).forEach(i -> doCategoryAssertions(categoryChildrenList.get(i), i, PARENT_ID));
assertEquals(childAssociationRefMocks.size(), categoryChildren.size());
IntStream.range(0, childrenCount).forEach(i -> doCategoryAssertions(categoryChildren.get(i), i, PARENT_ID));
}
@Test
public void testGetCategoryChildren_includeCount()
{
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
final int childrenCount = 3;
final List<ChildAssociationRef> childAssociationRefMocks = prepareChildAssocMocks(childrenCount, parentCategoryNodeRef);
given(nodeServiceMock.getChildAssocs(parentCategoryNodeRef)).willReturn(childAssociationRefMocks);
childAssociationRefMocks.forEach(this::prepareCategoryNodeMocks);
given(parametersMock.getInclude()).willReturn(List.of(INCLUDE_COUNT_PARAM));
given(categoryServiceMock.getTopCategories(any(), any(), anyInt())).willReturn(List.of(new Pair<>(createNodeRefWithId(CATEGORY_ID.concat("-1")), 2)));
// when
final List<Category> actualCategoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
then(categoryServiceMock).should().getTopCategories(STORE_REF_WORKSPACE_SPACESSTORE, ContentModel.ASPECT_GEN_CLASSIFIABLE, Integer.MAX_VALUE);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualCategoryChildren)
.isNotNull()
.hasSize(3)
.extracting(Category::getCount)
.isNotNull()
.isEqualTo(List.of(0, 2, 0));
}
@Test
public void testGetCategoryChildren_noChildren()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
given(nodeServiceMock.getChildAssocs(parentCategoryNodeRef)).willReturn(Collections.emptyList());
//when
final CollectionWithPagingInfo<Category> categoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
final List<Category> categoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
then(nodesMock).should().validateNode(PARENT_ID);
then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
@@ -586,13 +648,13 @@ public class CategoriesImplTest
then(authorityServiceMock).shouldHaveNoInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
assertEquals(0, categoryChildren.getTotalItems().intValue());
assertEquals(0, categoryChildren.size());
}
@Test
public void testGetCategoryChildren_wrongParentNodeType()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(false);
@@ -633,12 +695,12 @@ public class CategoriesImplTest
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode(categoryNewName));
given(nodesMock.getNode(any())).willReturn(createNode(categoryNewName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.moveNode(any(), any(), any(), any())).willReturn(createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, createCmQNameOf(categoryNewName)));
// when
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, fixedCategory);
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, fixedCategory, parametersMock);
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
@@ -661,13 +723,41 @@ public class CategoriesImplTest
.isEqualTo(expectedCategory);
}
@Test
public void testUpdateCategoryById_includeCount()
{
final String categoryNewName = "categoryNewName";
final Category fixedCategory = createCategoryOnlyWithName(categoryNewName);
fixedCategory.setCount(9);
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.getNode(any())).willReturn(createNode(categoryNewName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.moveNode(any(), any(), any(), any())).willReturn(createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, createCmQNameOf(categoryNewName)));
given(parametersMock.getInclude()).willReturn(List.of(INCLUDE_COUNT_PARAM));
given(categoryServiceMock.getTopCategories(any(), any(), anyInt())).willReturn(List.of(new Pair<>(CATEGORY_NODE_REF, 1)));
// when
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, fixedCategory, parametersMock);
then(categoryServiceMock).should().getTopCategories(STORE_REF_WORKSPACE_SPACESSTORE, ContentModel.ASPECT_GEN_CLASSIFIABLE, Integer.MAX_VALUE);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualCategory)
.isNotNull()
.extracting(Category::getCount)
.isNotNull()
.isEqualTo(1);
}
@Test
public void testUpdateCategoryById_noPermission()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(false);
// when
assertThatExceptionOfType(PermissionDeniedException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, CATEGORY));
assertThatExceptionOfType(PermissionDeniedException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, CATEGORY, parametersMock));
then(nodesMock).shouldHaveNoInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
@@ -679,7 +769,7 @@ public class CategoriesImplTest
given(nodesMock.validateNode(any(String.class))).willThrow(EntityNotFoundException.class);
// when
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, CATEGORY));
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, CATEGORY, parametersMock));
then(nodeServiceMock).shouldHaveNoInteractions();
}
@@ -690,7 +780,7 @@ public class CategoriesImplTest
given(nodesMock.isSubClass(any(), any(), eq(false))).willReturn(false);
// when
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, CATEGORY))
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, CATEGORY, parametersMock))
.withMessageContaining(NOT_A_VALID_CATEGORY);
then(nodeServiceMock).shouldHaveNoInteractions();
@@ -704,7 +794,7 @@ public class CategoriesImplTest
given(categoryChildAssociationRefMock.getQName()).willReturn(ContentModel.ASPECT_GEN_CLASSIFIABLE);
// when
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(PATH_ROOT, CATEGORY))
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(PATH_ROOT, CATEGORY, parametersMock))
.withMessageContaining(NOT_A_VALID_CATEGORY);
then(categoryServiceMock).should().getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
@@ -727,7 +817,7 @@ public class CategoriesImplTest
final Category categoryWithoutName = createCategoryOnlyWithName(invalidName);
// when
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithoutName))
assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithoutName, parametersMock))
.withMessageContaining(NOT_NULL_OR_EMPTY);
}
}
@@ -741,12 +831,12 @@ public class CategoriesImplTest
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode(categoryNewName));
given(nodesMock.getNode(any())).willReturn(createNode(categoryNewName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.moveNode(any(), any(), any(), any())).willReturn(createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, createCmQNameOf(categoryNewName)));
// when
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithInvalidId);
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithInvalidId, parametersMock);
final Category expectedCategory = createDefaultCategoryWithName(categoryNewName);
assertThat(actualCategory)
@@ -763,12 +853,12 @@ public class CategoriesImplTest
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode(categoryNewName));
given(nodesMock.getNode(any())).willReturn(createNode(categoryNewName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.moveNode(any(), any(), any(), any())).willReturn(createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, createCmQNameOf(categoryNewName)));
// when
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithInvalidParentId);
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithInvalidParentId, parametersMock);
final Category expectedCategory = createDefaultCategoryWithName(categoryNewName);
assertThat(actualCategory)
@@ -785,12 +875,12 @@ public class CategoriesImplTest
final QName categoryQName = createCmQNameOf(CATEGORY_NAME);
final NodeRef parentCategoryNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, categoryQName);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode(categoryNewName));
given(nodesMock.getNode(any())).willReturn(createNode(categoryNewName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.moveNode(any(), any(), any(), any())).willReturn(createAssociationOf(parentCategoryNodeRef, CATEGORY_NODE_REF, createCmQNameOf(categoryNewName)));
// when
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithInvalidHasChildren);
final Category actualCategory = objectUnderTest.updateCategoryById(CATEGORY_ID, categoryWithInvalidHasChildren, parametersMock);
final Category expectedCategory = createDefaultCategoryWithName(categoryNewName);
assertThat(actualCategory)
@@ -804,11 +894,11 @@ public class CategoriesImplTest
final List<Category> categoryLinks = List.of(CATEGORY);
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks);
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks, parametersMock);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
@@ -838,12 +928,12 @@ public class CategoriesImplTest
{
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.hasAspect(any(), any())).willReturn(true);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY));
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock);
then(nodesMock).should().getNode(CATEGORY_ID);
then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
@@ -873,11 +963,11 @@ public class CategoriesImplTest
final ChildAssociationRef categoryParentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
final ChildAssociationRef secondCategoryParentAssociation = createAssociationOf(categoryParentNodeRef, secondCategoryNodeRef);
given(nodesMock.validateNode(secondCategoryId)).willReturn(secondCategoryNodeRef);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode(), prepareCategoryNode(secondCategoryName));
given(nodesMock.getNode(any())).willReturn(createNode(), createNode(secondCategoryName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(categoryParentAssociation, secondCategoryParentAssociation);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks);
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks, parametersMock);
then(nodesMock).should().validateNode(CATEGORY_ID);
then(nodesMock).should().validateNode(secondCategoryId);
@@ -902,13 +992,13 @@ public class CategoriesImplTest
final Serializable previousCategories = (Serializable) List.of(otherCategoryNodeRef);
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.hasAspect(any(), any())).willReturn(true);
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn(previousCategories);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY));
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock);
final Serializable expectedCategories = (Serializable) Set.of(otherCategoryNodeRef, CATEGORY_NODE_REF);
then(nodeServiceMock).should().setProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES, expectedCategories);
@@ -924,7 +1014,7 @@ public class CategoriesImplTest
given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY)));
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).shouldHaveNoInteractions();
@@ -939,7 +1029,7 @@ public class CategoriesImplTest
given(permissionServiceMock.hasPermission(any(), any())).willReturn(AccessStatus.DENIED);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY)));
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
@@ -954,9 +1044,8 @@ public class CategoriesImplTest
{
given(typeConstraint.matches(any())).willReturn(false);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY)));
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY), parametersMock));
then(typeConstraint).should().matches(CONTENT_NODE_REF);
then(nodeServiceMock).shouldHaveNoInteractions();
@@ -971,7 +1060,7 @@ public class CategoriesImplTest
final List<Category> categoryLinks = Collections.emptyList();
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks));
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks, parametersMock));
then(nodesMock).shouldHaveNoInteractions();
then(permissionServiceMock).shouldHaveNoInteractions();
@@ -992,7 +1081,7 @@ public class CategoriesImplTest
categoryLinks.add(categoryLinkWithEmptyId);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks));
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks, parametersMock));
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
@@ -1006,11 +1095,11 @@ public class CategoriesImplTest
final List<Category> categoryLinks = List.of(CATEGORY, CATEGORY);
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks);
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks, parametersMock);
final Map<QName, Serializable> expectedProperties = Map.of(ContentModel.PROP_CATEGORIES, (Serializable) List.of(CATEGORY_NODE_REF));
then(nodeServiceMock).should().addAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE, expectedProperties);
@@ -1045,7 +1134,7 @@ public class CategoriesImplTest
given(nodeServiceMock.hasAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE)).willReturn(false);
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.unlinkNodeFromCategory(CONTENT_NODE_ID,CATEGORY_ID, parametersMock));
final Throwable actualException = catchThrowable(() -> objectUnderTest.unlinkNodeFromCategory(CONTENT_NODE_ID, CATEGORY_ID, parametersMock));
then(nodeServiceMock).should().hasAspect(CONTENT_NODE_REF,ContentModel.ASPECT_GEN_CLASSIFIABLE);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
@@ -1060,11 +1149,11 @@ public class CategoriesImplTest
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) List.of(CATEGORY_NODE_REF));
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodesMock.getNode(any())).willReturn(createNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
// when
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID);
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
@@ -1090,7 +1179,7 @@ public class CategoriesImplTest
given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID));
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(nodeServiceMock).shouldHaveNoInteractions();
@@ -1104,7 +1193,7 @@ public class CategoriesImplTest
given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.DENIED);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID));
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF);
@@ -1120,7 +1209,7 @@ public class CategoriesImplTest
given(typeConstraint.matches(any())).willReturn(false);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID));
final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock));
then(typeConstraint).should().matches(CONTENT_NODE_REF);
then(nodeServiceMock).shouldHaveNoInteractions();
@@ -1136,45 +1225,19 @@ public class CategoriesImplTest
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) nullOrEmptyList);
// when
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID);
final List<Category> actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID, parametersMock);
assertThat(actualCategories).isNotNull().isEmpty();
});
}
private Node prepareCategoryNode(final String name, final String id, final NodeRef parentNodeRef)
{
final Node categoryNode = new Node();
categoryNode.setName(name);
categoryNode.setNodeId(id);
categoryNode.setParentId(parentNodeRef);
return categoryNode;
}
private Node prepareCategoryNode(final String name)
{
return prepareCategoryNode(name, CATEGORY_ID, createNodeRefWithId(PARENT_ID));
}
private Node prepareCategoryNode()
{
return prepareCategoryNode(CATEGORY_NAME);
}
private List<Category> prepareCategories()
{
return List.of(Category.builder()
.name(CATEGORY_NAME)
.create());
}
private List<ChildAssociationRef> prepareChildAssocMocks(final int count, NodeRef parentCategoryNodeRef)
{
return IntStream.range(0, count).mapToObj(i -> {
ChildAssociationRef dummyChildAssocMock = mock(ChildAssociationRef.class);
given(dummyChildAssocMock.getTypeQName()).willReturn(ContentModel.ASSOC_SUBCATEGORIES);
given(dummyChildAssocMock.getChildRef())
.willReturn(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID + "-" + i));
.willReturn(createNodeRefWithId(CATEGORY_ID + "-" + i));
given(dummyChildAssocMock.getParentRef()).willReturn(parentCategoryNodeRef);
return dummyChildAssocMock;
}).collect(Collectors.toList());
@@ -1186,8 +1249,8 @@ public class CategoriesImplTest
final String id = childRef.getId();
final String name = id.replace(CATEGORY_ID, CATEGORY_NAME);
final NodeRef parentRef = childAssociationRef.getParentRef();
given(nodesMock.getNode(id)).willReturn(prepareCategoryNode(name, id, parentRef));
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentRef, null, childRef);
given(nodesMock.getNode(id)).willReturn(createNode(name, id, parentRef));
final ChildAssociationRef parentAssoc = createAssociationOf(parentRef, childRef);
given(nodeServiceMock.getPrimaryParent(childRef)).willReturn(parentAssoc);
given(nodeServiceMock.getParentAssocs(parentRef)).willReturn(List.of(parentAssoc));
}
@@ -1195,14 +1258,38 @@ public class CategoriesImplTest
private void doCategoryAssertions(final Category category, final int index, final String parentId)
{
final Category expectedCategory = Category.builder()
.id(CATEGORY_ID + "-" + index)
.name(CATEGORY_NAME + "-" + index)
.parentId(parentId)
.hasChildren(false)
.create();
.id(CATEGORY_ID + "-" + index)
.name(CATEGORY_NAME + "-" + index)
.parentId(parentId)
.hasChildren(false)
.create();
assertEquals(expectedCategory, category);
}
private List<Category> prepareCategories()
{
return List.of(createCategoryOnlyWithName(CATEGORY_NAME));
}
private static Node createNode(final String name, final String id, final NodeRef parentNodeRef)
{
final Node categoryNode = new Node();
categoryNode.setName(name);
categoryNode.setNodeId(id);
categoryNode.setParentId(parentNodeRef);
return categoryNode;
}
private static Node createNode(final String name)
{
return createNode(name, CATEGORY_ID, createNodeRefWithId(PARENT_ID));
}
private static Node createNode()
{
return createNode(CATEGORY_NAME);
}
private static NodeRef createNodeRefWithId(final String id)
{
return new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id);
@@ -1213,6 +1300,11 @@ public class CategoriesImplTest
return Category.builder().name(name).create();
}
private static Category createDefaultCategory()
{
return createDefaultCategoryWithName(CATEGORY_NAME);
}
private static Category createDefaultCategoryWithName(final String name)
{
return Category.builder()

View File

@@ -0,0 +1,430 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.impl;
import static org.alfresco.rest.api.impl.TagsImpl.NOT_A_VALID_TAG;
import static org.alfresco.rest.api.impl.TagsImpl.NO_PERMISSION_TO_MANAGE_A_TAG;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException;
import org.alfresco.rest.framework.tools.RecognizedParamsExtractor;
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.util.Pair;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TagsImplTest
{
private static final String TAG_ID = "tag-node-id";
private static final String TAG_NAME = "tag-dummy-name";
private static final NodeRef TAG_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(TAG_NAME));
private final RecognizedParamsExtractor queryExtractor = new RecognizedParamsExtractor() {};
@Mock
private Nodes nodesMock;
@Mock
private AuthorityService authorityServiceMock;
@Mock
private TaggingService taggingServiceMock;
@Mock
private Parameters parametersMock;
@Mock
private Paging pagingMock;
@Mock
private PagingResults<Pair<NodeRef, String>> pagingResultsMock;
@InjectMocks
private TagsImpl objectUnderTest;
@Before
public void setup()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
given(nodesMock.validateNode(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID)).willReturn(TAG_NODE_REF);
given(taggingServiceMock.getTagName(TAG_NODE_REF)).willReturn(TAG_NAME);
}
@Test
public void testGetTags()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
given(pagingResultsMock.getPage()).willReturn(List.of(new Pair<>(TAG_NODE_REF, TAG_NAME)));
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), isNull(), isNull());
then(taggingServiceMock).shouldHaveNoMoreInteractions();
final List<Tag> expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME)).stream().peek(tag -> tag.setCount(0)).collect(Collectors.toList());
assertEquals(expectedTags, actualTags.getCollection());
}
@Test
public void testGetTags_verifyIfCountIsZero()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
given(pagingResultsMock.getPage()).willReturn(List.of(new Pair<>(TAG_NODE_REF, TAG_NAME)));
given(parametersMock.getInclude()).willReturn(List.of("count"));
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().findTaggedNodesAndCountByTagName(STORE_REF_WORKSPACE_SPACESSTORE);
final List<Tag> expectedTags = createTagsWithNodeRefs(List.of(TAG_NAME)).stream()
.peek(tag -> tag.setCount(0))
.collect(Collectors.toList());
assertEquals(expectedTags, actualTags.getCollection());
}
@Test
public void testGetTags_withEqualsClauseWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag=expectedName)"));
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
//when
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), eq(Set.of("expectedname")), isNull());
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualTags).isNotNull();
}
@Test
public void testGetTags_withInClauseWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag IN (expectedName1, expectedName2))"));
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
//when
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), eq(Set.of("expectedname1", "expectedname2")), isNull());
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualTags).isNotNull();
}
@Test
public void testGetTags_withMatchesClauseWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag MATCHES ('expectedName*'))"));
given(taggingServiceMock.getTags(any(StoreRef.class), any(PagingRequest.class), any(), any())).willReturn(pagingResultsMock);
given(pagingResultsMock.getTotalResultCount()).willReturn(new Pair<>(Integer.MAX_VALUE, 0));
//when
final CollectionWithPagingInfo<Tag> actualTags = objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock);
then(taggingServiceMock).should().getTags(eq(STORE_REF_WORKSPACE_SPACESSTORE), any(PagingRequest.class), isNull(), eq(Set.of("expectedname*")));
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualTags).isNotNull();
}
@Test
public void testGetTags_withBothInAndEqualsClausesInSingleWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag=expectedName AND tag IN (expectedName1, expectedName2))"));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testGetTags_withOtherClauseInWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(tag BETWEEN ('expectedName', 'expectedName2'))"));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testGetTags_withNotEqualsClauseInWhereQuery()
{
given(parametersMock.getPaging()).willReturn(pagingMock);
given(parametersMock.getQuery()).willReturn(queryExtractor.getWhereClause("(NOT tag=expectedName)"));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.getTags(STORE_REF_WORKSPACE_SPACESSTORE, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testDeleteTagById()
{
//when
objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(nodesMock).should().validateNode(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).should().getTagName(TAG_NODE_REF);
then(taggingServiceMock).should().deleteTag(STORE_REF_WORKSPACE_SPACESSTORE, TAG_NAME);
then(taggingServiceMock).shouldHaveNoMoreInteractions();
}
@Test
public void testDeleteTagById_asNonAdminUser()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(false);
//when
assertThrows(PermissionDeniedException.class, () -> objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID));
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(nodesMock).shouldHaveNoInteractions();
then(taggingServiceMock).shouldHaveNoInteractions();
}
@Test
public void testDeleteTagById_nonExistentTag()
{
//when
assertThrows(EntityNotFoundException.class, () -> objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id"));
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(nodesMock).should().validateNode(STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id");
then(nodesMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).shouldHaveNoInteractions();
}
@Test
public void testCreateTags()
{
final List<String> tagNames = List.of("tag1", "99gat");
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, tagNames);
then(taggingServiceMock).shouldHaveNoMoreInteractions();
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames);
assertThat(actualCreatedTags)
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedTags);
}
@Test
public void testCreateTags_withoutPermission()
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(false);
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(List.of(createTag(TAG_NAME)), parametersMock));
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(PermissionDeniedException.class)
.hasMessageContaining(NO_PERMISSION_TO_MANAGE_A_TAG);
}
@Test
public void testCreateTags_passingNullInsteadList()
{
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(null, parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_TAG);
}
@Test
public void testCreateTags_passingEmptyList()
{
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(Collections.emptyList(), parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_TAG);
}
@Test
public void testCreateTags_passingListOfNulls()
{
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(Collections.singletonList(null), parametersMock));
then(taggingServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_TAG);
}
@Test
public void testCreateTags_whileTagAlreadyExists()
{
given(taggingServiceMock.createTags(any(), any())).willThrow(new DuplicateChildNodeNameException(null, null, TAG_NAME, null));
//when
final Throwable actualException = catchThrowable(() -> objectUnderTest.createTags(List.of(createTag(TAG_NAME)), parametersMock));
then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
then(taggingServiceMock).shouldHaveNoMoreInteractions();
assertThat(actualException).isInstanceOf(DuplicateChildNodeNameException.class);
}
@Test
public void testCreateTags_withRepeatedTagName()
{
final List<String> tagNames = List.of(TAG_NAME, TAG_NAME);
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
final List<Tag> expectedTags = List.of(createTagWithNodeRef(TAG_NAME));
assertThat(actualCreatedTags)
.isNotNull()
.isEqualTo(expectedTags);
}
@Test
public void testCreateTags_includingCount()
{
final List<String> tagNames = List.of("tag1", "99gat");
final List<Tag> tagsToCreate = createTags(tagNames);
given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
given(parametersMock.getInclude()).willReturn(List.of("count"));
//when
final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).stream()
.peek(tag -> tag.setCount(0))
.collect(Collectors.toList());
assertThat(actualCreatedTags)
.isNotNull()
.isEqualTo(expectedTags);
}
private static List<Pair<String, NodeRef>> createTagAndNodeRefPairs(final List<String> tagNames)
{
return tagNames.stream()
.map(tagName -> createPair(tagName, new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName))))
.collect(Collectors.toList());
}
private static Pair<String, NodeRef> createPair(final String tagName, final NodeRef nodeRef)
{
return new Pair<>(tagName, nodeRef);
}
private static List<Tag> createTags(final List<String> tagNames)
{
return tagNames.stream().map(TagsImplTest::createTag).collect(Collectors.toList());
}
private static List<Tag> createTagsWithNodeRefs(final List<String> tagNames)
{
return tagNames.stream().map(TagsImplTest::createTagWithNodeRef).collect(Collectors.toList());
}
private static Tag createTag(final String tagName)
{
return Tag.builder()
.tag(tagName)
.create();
}
private static Tag createTagWithNodeRef(final String tagName)
{
return Tag.builder()
.nodeRef(new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName)))
.tag(tagName)
.create();
}
}

View File

@@ -0,0 +1,78 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.tags;
import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.List;
import org.alfresco.rest.api.Tags;
import org.alfresco.rest.api.model.Tag;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.NodeRef;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class TagsEntityResourceTest
{
private static final String TAG_ID = "tag-dummy-id";
private static final String TAG_NAME = "tag-dummy-name";
private static final NodeRef TAG_NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
@Mock
private Parameters parametersMock;
@Mock
private Tags tagsMock;
@InjectMocks
private TagsEntityResource tagsEntityResource;
@Test
public void testCreate()
{
final Tag tag = Tag.builder().tag(TAG_NAME).create();
final List<Tag> tags = List.of(tag);
given(tagsMock.createTags(any(), any())).willCallRealMethod();
given(tagsMock.createTags(any(), any(), any())).willReturn(List.of(Tag.builder().nodeRef(TAG_NODE_REF).tag(TAG_NAME).create()));
//when
final List<Tag> actualCreatedTags = tagsEntityResource.create(tags, parametersMock);
then(tagsMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, tags, parametersMock);
final List<Tag> expectedTags = List.of(Tag.builder().nodeRef(TAG_NODE_REF).tag(TAG_NAME).create());
assertThat(actualCreatedTags)
.isNotEmpty()
.isEqualTo(expectedTags);
}
}

View File

@@ -0,0 +1,666 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.framework.resource.parameters.where;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.catchThrowable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.framework.tools.RecognizedParamsExtractor;
import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker;
import org.junit.Test;
/**
* Tests verifying {@link QueryHelper.QueryResolver} functionality based on {@link BasicQueryWalker}.
*/
public class QueryResolverTest
{
private final RecognizedParamsExtractor queryExtractor = new RecognizedParamsExtractor() {};
@Test
public void testResolveQuery_equals()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, true)).isFalse();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_greaterThan()
{
final Query query = queryExtractor.getWhereClause("(propName > testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHAN, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_greaterThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(propName >= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHANOREQUALS, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_lessThan()
{
final Query query = queryExtractor.getWhereClause("(propName < testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHAN, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_lessThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(propName <= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHANOREQUALS, false)).containsOnly("testValue");
}
@Test
public void testResolveQuery_between()
{
final Query query = queryExtractor.getWhereClause("(propName BETWEEN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.BETWEEN, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_in()
{
final Query query = queryExtractor.getWhereClause("(propName IN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.IN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.IN, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_matches()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES ('*Value'))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.MATCHES, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.MATCHES, false)).containsOnly("*Value");
}
@Test
public void testResolveQuery_exists()
{
final Query query = queryExtractor.getWhereClause("(EXISTS (propName))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EXISTS, false)).isEmpty();
}
@Test
public void testResolveQuery_notEquals()
{
final Query query = queryExtractor.getWhereClause("(NOT propName=testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, true)).isTrue();
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHAN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.MATCHES, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.BETWEEN, true)).isFalse();
assertThat(property.containsType(WhereClauseParser.EXISTS, true)).isFalse();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notGreaterThan()
{
final Query query = queryExtractor.getWhereClause("(NOT propName > testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHAN, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notGreaterThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(NOT propName >= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHANOREQUALS, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notLessThan()
{
final Query query = queryExtractor.getWhereClause("(NOT propName < testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHAN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHAN, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notLessThanOrEquals()
{
final Query query = queryExtractor.getWhereClause("(NOT propName <= testValue)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.LESSTHANOREQUALS, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHANOREQUALS, true)).containsOnly("testValue");
}
@Test
public void testResolveQuery_notBetween()
{
final Query query = queryExtractor.getWhereClause("(NOT propName BETWEEN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.BETWEEN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.BETWEEN, true)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_notIn()
{
final Query query = queryExtractor.getWhereClause("(NOT propName IN (testValue, testValue2))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.IN, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.IN, true)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_notMatches()
{
final Query query = queryExtractor.getWhereClause("(NOT propName MATCHES ('*Value'))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.MATCHES, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.MATCHES, true)).containsOnly("*Value");
}
@Test
public void testResolveQuery_notExists()
{
final Query query = queryExtractor.getWhereClause("(NOT EXISTS (propName))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EXISTS, true)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EXISTS, true)).isEmpty();
}
@Test
public void testResolveQuery_propertyNotExpected()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND differentName>18)");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("differentName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_propertyNotExpectedUsingLenientApproach()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND differentName>18)");
//when
final WhereProperty property = QueryHelper.resolve(query).leniently().getProperty("differentName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.EQUALS, true)).isFalse();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).isNull();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, true)).isNull();
assertThat(property.containsType(WhereClauseParser.GREATERTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHAN, false)).containsOnly("18");
}
@Test
public void testResolveQuery_propertyNotPresentUsingLenientApproach()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("differentName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_slashInPropertyName()
{
final Query query = queryExtractor.getWhereClause("(EXISTS (prop/name/with/slashes))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("prop/name/with/slashes");
assertThat(property.containsType(WhereClauseParser.EXISTS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EXISTS, false)).isEmpty();
}
@Test
public void testResolveQuery_propertyBetweenDates()
{
final Query query = queryExtractor.getWhereClause("(propName BETWEEN ('2012-01-01', '2012-12-31'))");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.BETWEEN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.BETWEEN, false)).containsOnly("2012-01-01", "2012-12-31");
}
@Test
public void testResolveQuery_singlePropertyGreaterThanOrEqualsAndLessThan()
{
final Query query = queryExtractor.getWhereClause("(propName >= 18 AND propName < 65)");
//when
final WhereProperty property = QueryHelper.resolve(query).getProperty("propName");
assertThat(property.containsType(WhereClauseParser.GREATERTHANOREQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.GREATERTHANOREQUALS, false)).containsOnly("18");
assertThat(property.containsType(WhereClauseParser.LESSTHAN, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.LESSTHAN, false)).containsOnly("65");
}
@Test
public void testResolveQuery_onePropertyGreaterThanAndSecondPropertyNotMatches()
{
final Query query = queryExtractor.getWhereClause("(propName1 > 20 AND NOT propName2 MATCHES ('external*'))");
//when
final List<WhereProperty> property = QueryHelper.resolve(query).getProperties("propName1", "propName2");
assertThat(property.get(0).containsType(WhereClauseParser.GREATERTHAN, false)).isTrue();
assertThat(property.get(0).getExpectedValuesFor(WhereClauseParser.GREATERTHAN, false)).containsOnly("20");
assertThat(property.get(1).containsType(WhereClauseParser.MATCHES, true)).isTrue();
assertThat(property.get(1).getExpectedValuesFor(WhereClauseParser.MATCHES, true)).containsOnly("external*");
}
@Test
public void testResolveQuery_negationsForbidden()
{
final Query query = queryExtractor.getWhereClause("(NOT propName=testValue)");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).withoutNegations().getProperty("propName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_withoutNegations()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final WhereProperty actualProperty = QueryHelper.resolve(query).withoutNegations().getProperty("propName");
assertThat(actualProperty.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(actualProperty.containsType(WhereClauseParser.EQUALS, true)).isFalse();
assertThat(actualProperty.getExpectedValuesFor(WhereClauseParser.EQUALS).onlyNegated()).isNull();
assertThat(actualProperty.getExpectedValuesFor(WhereClauseParser.EQUALS).skipNegated()).containsOnly("testValue");
}
@Test
public void testResolveQuery_orNotAllowed()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName BETWEEN (testValue2, testValue3))");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("propName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_orAllowedInFavorOfAnd()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName=testValue2)");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.usingOrOperator()
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_usingCustomQueryWalker()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final Collection<String> propertyValues = QueryHelper
.resolve(query)
.usingWalker(new MapBasedQueryWalker(Set.of("propName"), null))
.getProperty("propName", WhereClauseParser.EQUALS, false);
assertThat(propertyValues).containsOnly("testValue");
}
@Test
public void testResolveQuery_usingCustomBasicQueryWalkerExtension()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName=testValue2)");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.usingWalker(new BasicQueryWalker("propName")
{
@Override
public void or() {}
@Override
public void and() {throw UNSUPPORTED;}
})
.withoutNegations()
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.getExpectedValuesFor(WhereClauseParser.EQUALS, false)).containsOnly("testValue", "testValue2");
}
@Test
public void testResolveQuery_equalsAndInNotAllowedTogether()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName IN (testValue2, testValue3))");
//when
final Throwable actualException = catchThrowable(() -> QueryHelper.resolve(query).getProperty("propName"));
assertThat(actualException).isInstanceOf(InvalidQueryException.class);
}
@Test
public void testResolveQuery_equalsOrInAllowedTogether()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue OR propName IN (testValue2, testValue3))");
//when
final WhereProperty whereProperty = QueryHelper.resolve(query).usingOrOperator().getProperty("propName");
assertThat(whereProperty).isNotNull();
assertThat(whereProperty.getExpectedValuesForAllOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated())
.isEqualTo(Map.of(WhereClauseParser.EQUALS, Set.of("testValue"), WhereClauseParser.IN, Set.of("testValue2", "testValue3")));
}
@Test
public void testResolveQuery_equalsAndInAllowedTogetherWithDifferentProperties()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName2 IN (testValue2, testValue3))");
//when
final List<WhereProperty> properties = QueryHelper
.resolve(query)
.getProperties("propName", "propName2");
assertThat(properties.get(0).containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(properties.get(0).containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(properties.get(0).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.EQUALS)).containsOnly("testValue");
assertThat(properties.get(0).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.IN)).isFalse();
assertThat(properties.get(1).containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(properties.get(1).containsType(WhereClauseParser.IN, false)).isTrue();
assertThat(properties.get(1).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.EQUALS)).isFalse();
assertThat(properties.get(1).getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.IN)).containsOnly("testValue2", "testValue3");
}
@Test
public void testResolveQuery_equalsAndInAllowedAlternately_equals()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue)");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isTrue();
assertThat(property.containsType(WhereClauseParser.IN, false)).isFalse();
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.EQUALS)).containsOnly("testValue");
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.IN)).isFalse();
}
@Test
public void testResolveQuery_equalsAndInAllowedAlternately_in()
{
final Query query = queryExtractor.getWhereClause("(propName IN (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.containsType(WhereClauseParser.EQUALS, false)).isFalse();
assertThat(property.containsType(WhereClauseParser.IN, false)).isTrue();
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().containsKey(WhereClauseParser.EQUALS)).isFalse();
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.IN).skipNegated().get(WhereClauseParser.IN)).containsOnly("testValue");
}
@Test
public void testResolveQuery_missingEqualsClauseType()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThatExceptionOfType(InvalidQueryException.class)
.isThrownBy(() -> property.getExpectedValuesForAllOf(WhereClauseParser.EQUALS, WhereClauseParser.MATCHES));
}
@Test
public void testResolveQuery_ignoreUnexpectedClauseType()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.getExpectedValuesForAllOf(WhereClauseParser.EQUALS).skipNegated(WhereClauseParser.EQUALS)).containsOnly("testValue");
}
@Test
public void testResolveQuery_complexAndQuery()
{
final Query query = queryExtractor.getWhereClause("(a=v1 AND b>18 AND b<=65 AND NOT c BETWEEN ('2012-01-01','2012-12-31') AND d IN (v1, v2) AND e MATCHES ('*@mail.com') AND EXISTS (f/g))");
//when
final List<WhereProperty> properties = QueryHelper
.resolve(query)
.getProperties("a", "b", "c", "d", "e", "f/g");
assertThat(properties).hasSize(6);
assertThat(properties.get(0).getExpectedValuesFor(WhereProperty.ClauseType.EQUALS)).containsOnly("v1");
assertThat(properties.get(1).containsAllTypes(WhereProperty.ClauseType.GREATER_THAN, WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).isTrue();
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.GREATER_THAN)).containsOnly("18");
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).containsOnly("65");
assertThat(properties.get(2).getExpectedValuesFor(WhereProperty.ClauseType.NOT_BETWEEN)).containsOnly("2012-01-01", "2012-12-31");
assertThat(properties.get(3).getExpectedValuesFor(WhereProperty.ClauseType.IN)).containsOnly("v1", "v2");
assertThat(properties.get(4).getExpectedValuesFor(WhereProperty.ClauseType.MATCHES)).containsOnly("*@mail.com");
assertThat(properties.get(5).containsType(WhereProperty.ClauseType.EXISTS)).isTrue();
assertThat(properties.get(5).getExpectedValuesFor(WhereProperty.ClauseType.EXISTS)).isEmpty();
}
@Test
public void testResolveQuery_complexOrQuery()
{
final Query query = queryExtractor.getWhereClause("(a=v1 OR b>18 OR b<=65 OR NOT c BETWEEN ('2012-01-01','2012-12-31') OR d IN (v1, v2) OR e MATCHES ('*@mail.com') OR EXISTS (f/g))");
//when
final List<WhereProperty> properties = QueryHelper
.resolve(query)
.usingOrOperator()
.getProperties("a", "b", "c", "d", "e", "f/g");
assertThat(properties).hasSize(6);
assertThat(properties.get(0).getExpectedValuesFor(WhereProperty.ClauseType.EQUALS)).containsOnly("v1");
assertThat(properties.get(1).containsAllTypes(WhereProperty.ClauseType.GREATER_THAN, WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).isTrue();
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.GREATER_THAN)).containsOnly("18");
assertThat(properties.get(1).getExpectedValuesFor(WhereProperty.ClauseType.LESS_THAN_OR_EQUALS)).containsOnly("65");
assertThat(properties.get(2).getExpectedValuesFor(WhereProperty.ClauseType.NOT_BETWEEN)).containsOnly("2012-01-01", "2012-12-31");
assertThat(properties.get(3).getExpectedValuesFor(WhereProperty.ClauseType.IN)).containsOnly("v1", "v2");
assertThat(properties.get(4).getExpectedValuesFor(WhereProperty.ClauseType.MATCHES)).containsOnly("*@mail.com");
assertThat(properties.get(5).containsType(WhereProperty.ClauseType.EXISTS)).isTrue();
assertThat(properties.get(5).getExpectedValuesFor(WhereProperty.ClauseType.EXISTS)).isEmpty();
}
@Test
public void testResolveQuery_clauseTypeOptional()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThat(property.getExpectedValuesForAnyOf(WhereClauseParser.EQUALS, WhereClauseParser.MATCHES).skipNegated(WhereClauseParser.MATCHES)).containsOnly("testValue");
}
@Test
public void testResolveQuery_optionalClauseTypesNotPresent()
{
final Query query = queryExtractor.getWhereClause("(propName=testValue AND propName MATCHES (testValue))");
//when
final WhereProperty property = QueryHelper
.resolve(query)
.getProperty("propName");
assertThatExceptionOfType(InvalidQueryException.class)
.isThrownBy(() -> property.getExpectedValuesForAnyOf(WhereClauseParser.IN));
}
@Test
public void testResolveQuery_matchesOrMatchesAllowed()
{
final Query query = queryExtractor.getWhereClause("(propName MATCHES ('test*') OR propName MATCHES ('*value*'))");
//when
final Collection<String> expectedValues = QueryHelper
.resolve(query)
.usingOrOperator()
.getProperty("propName")
.getExpectedValuesFor(WhereClauseParser.MATCHES)
.skipNegated();
assertThat(expectedValues).containsOnly("test*", "*value*");
}
}

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-community-repo</artifactId>
<version>20.73</version>
<version>20.106</version>
</parent>
<dependencies>
@@ -119,6 +119,10 @@
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
@@ -387,14 +391,11 @@
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${dependency.spring-security.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</exclusion>
</exclusions>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
@@ -557,17 +558,6 @@
</dependency>
<!-- Keycloak dependencies -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>${dependency.keycloak.version}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>

View File

@@ -1,28 +1,28 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.download;
import java.io.File;
@@ -31,6 +31,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.attribute.FileTime;
import java.util.Date;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
@@ -85,6 +87,8 @@ public class ZipDownloadExporter extends BaseExporter
private String currentName;
private OutputStream outputStream;
private Date zipTimestampCreated;
private Date zipTimestampModified;
/**
* Construct
@@ -137,6 +141,8 @@ public class ZipDownloadExporter extends BaseExporter
public void startNode(NodeRef nodeRef)
{
this.currentName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
this.zipTimestampCreated = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_CREATED);
this.zipTimestampModified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
path.push(new Pair<String, NodeRef>(currentName, nodeRef));
if (dictionaryService.isSubClass(nodeService.getType(nodeRef), ContentModel.TYPE_FOLDER))
{
@@ -144,6 +150,9 @@ public class ZipDownloadExporter extends BaseExporter
ZipArchiveEntry archiveEntry = new ZipArchiveEntry(path);
try
{
archiveEntry.setTime(zipTimestampCreated.getTime());
archiveEntry.setCreationTime(FileTime.fromMillis(zipTimestampCreated.getTime()));
archiveEntry.setLastModifiedTime(FileTime.fromMillis(zipTimestampModified.getTime()));
zipStream.putArchiveEntry(archiveEntry);
zipStream.closeArchiveEntry();
}
@@ -167,6 +176,9 @@ public class ZipDownloadExporter extends BaseExporter
{
// ALF-2016
ZipArchiveEntry zipEntry=new ZipArchiveEntry(getPath());
zipEntry.setTime(zipTimestampCreated.getTime());
zipEntry.setCreationTime(FileTime.fromMillis(zipTimestampCreated.getTime()));
zipEntry.setLastModifiedTime(FileTime.fromMillis(zipTimestampModified.getTime()));
zipStream.putArchiveEntry(zipEntry);
// copy export stream to zip

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -304,6 +304,14 @@ public class EventConsolidator implements EventSupportedPolicies
builder.setProperties(mappedProps);
resourceBeforeAllFieldsNull = false;
}
Map<String, Map<String, String>> localizedProps =helper.getLocalizedPropertiesBefore(changedPropsBefore, after);
if (!localizedProps.isEmpty())
{
builder.setLocalizedProperties(localizedProps);
resourceBeforeAllFieldsNull = false;
}
String name = (String) changedPropsBefore.get(ContentModel.PROP_NAME);
if (name != null)
{

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,8 @@
*/
package org.alfresco.repo.event2;
import static java.util.Optional.ofNullable;
import java.io.Serializable;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -34,8 +36,11 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.google.common.collect.Sets;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.event.v1.model.ContentInfo;
import org.alfresco.repo.event.v1.model.NodeResource;
@@ -43,6 +48,7 @@ import org.alfresco.repo.event.v1.model.UserInfo;
import org.alfresco.repo.event2.filter.EventFilterRegistry;
import org.alfresco.repo.event2.filter.NodeAspectFilter;
import org.alfresco.repo.event2.filter.NodePropertyFilter;
import org.alfresco.repo.node.MLPropertyInterceptor;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.service.cmr.dictionary.DictionaryService;
@@ -152,6 +158,7 @@ public class NodeResourceHelper implements InitializingBean
.setPrimaryAssocQName(getPrimaryAssocQName(nodeRef))
.setPrimaryHierarchy(PathUtil.getNodeIdsInReverse(path, false))
.setProperties(mapToNodeProperties(properties))
.setLocalizedProperties(mapToNodeLocalizedProperties(properties))
.setAspectNames(getMappedAspects(nodeRef));
}
@@ -200,9 +207,8 @@ public class NodeResourceHelper implements InitializingBean
props.forEach((k, v) -> {
if (!nodePropertyFilter.isExcluded(k))
{
if (v != null && v instanceof MLText)
if (v instanceof MLText)
{
//TODO - should we send all of the values if multiple locales exist?
v = ((MLText) v).getDefaultValue();
}
@@ -213,6 +219,23 @@ public class NodeResourceHelper implements InitializingBean
return filteredProps;
}
public Map<String, Map<String, String>> mapToNodeLocalizedProperties(Map<QName, Serializable> props)
{
Map<String, Map<String, String>> filteredProps = new HashMap<>(props.size());
props.forEach((k, v) -> {
if (!nodePropertyFilter.isExcluded(k) && v instanceof MLText)
{
final MLText mlTextValue = (MLText) v;
final HashMap<String, String> localizedValues = new HashMap<>(mlTextValue.size());
mlTextValue.forEach((locale, text) -> localizedValues.put(locale.toString(), text));
filteredProps.put(getQNamePrefixString(k), localizedValues);
}
});
return filteredProps.isEmpty() ? null : filteredProps;
}
public ContentInfo getContentInfo(Map<QName, Serializable> props)
{
final Serializable content = props.get(ContentModel.PROP_CONTENT);
@@ -313,11 +336,6 @@ public class NodeResourceHelper implements InitializingBean
return filteredAspects;
}
private boolean isNotEmptyString(Serializable ser)
{
return !(ser instanceof String) || !((String) ser).isEmpty();
}
public QName getNodeType(NodeRef nodeRef)
{
return nodeService.getType(nodeRef);
@@ -330,7 +348,58 @@ public class NodeResourceHelper implements InitializingBean
public Map<QName, Serializable> getProperties(NodeRef nodeRef)
{
return nodeService.getProperties(nodeRef);
//We need to have full MLText properties here. This is why we are marking the current thread as MLAware
final boolean toRestore = MLPropertyInterceptor.isMLAware();
MLPropertyInterceptor.setMLAware(true);
try
{
return nodeService.getProperties(nodeRef);
} finally
{
MLPropertyInterceptor.setMLAware(toRestore);
}
}
public Map<String, Map<String, String>> getLocalizedPropertiesBefore(Map<QName, Serializable> propsBefore, NodeResource nodeAfter)
{
final Map<String, Map<String, String>> locPropsBefore = ofNullable(propsBefore)
.map(this::mapToNodeLocalizedProperties)
.orElseGet(Map::of);
final Map<String, Map<String, String>> locPropsAfter = ofNullable(nodeAfter)
.map(NodeResource::getLocalizedProperties)
.orElseGet(Map::of);
return getLocalizedPropertiesBefore(locPropsBefore, locPropsAfter);
}
static Map<String, Map<String, String>> getLocalizedPropertiesBefore(Map<String, Map<String, String>> locPropsBefore,
Map<String, Map<String, String>> locPropsAfter)
{
final Map<String, Map<String, String>> result = new HashMap<>(locPropsBefore.size());
Sets.union(locPropsBefore.keySet(), locPropsAfter.keySet()).forEach(propertyName -> {
final Map<String, String> valuesBefore = ofNullable(locPropsBefore.get(propertyName)).orElseGet(Map::of);
final Map<String, String> valuesAfter = ofNullable(locPropsAfter.get(propertyName)).orElseGet(Map::of);
if (!valuesAfter.isEmpty() || !valuesBefore.isEmpty())
{
final Map<String, String> diff = new HashMap<>(valuesBefore.size());
Sets.union(valuesBefore.keySet(), valuesAfter.keySet()).forEach(lang -> {
final String valueBefore = valuesBefore.get(lang);
final String valueAfter = valuesAfter.get(lang);
if (!Objects.equals(valueBefore, valueAfter))
{
diff.put(lang, valueBefore);
}
});
if (!diff.isEmpty())
{
result.put(propertyName, diff);
}
}
});
return result;
}
public Set<String> getMappedAspects(NodeRef nodeRef)

View File

@@ -1,119 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.HttpClient;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.springframework.beans.factory.FactoryBean;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
*
* Creates an instance of {@link AuthzClient}. <br>
* The creation of {@link AuthzClient} requires connection to a Keycloak server, disable this factory if Keycloak cannot be reached. <br>
* This factory can return a null if it is disabled.
*
*/
public class AuthenticatorAuthzClientFactoryBean implements FactoryBean<AuthzClient>
{
private static Log logger = LogFactory.getLog(AuthenticatorAuthzClientFactoryBean.class);
private IdentityServiceConfig identityServiceConfig;
private boolean enabled;
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
{
this.identityServiceConfig = identityServiceConfig;
}
@Override
public AuthzClient getObject() throws Exception
{
// The creation of the client can be disabled for testing or when the username/password authentication is not required,
// for instance when Keycloak is configured for 'bearer only' authentication or Direct Access Grants are disabled.
if (!enabled)
{
return null;
}
// Build default http client using the keycloak client builder.
int conTimeout = identityServiceConfig.getClientConnectionTimeout();
int socTimeout = identityServiceConfig.getClientSocketTimeout();
HttpClient client = new HttpClientBuilder()
.establishConnectionTimeout(conTimeout, TimeUnit.MILLISECONDS)
.socketTimeout(socTimeout, TimeUnit.MILLISECONDS)
.build(this.identityServiceConfig);
// Add secret to credentials if needed.
// AuthzClient configuration needs credentials with a secret even if the client in Keycloak is configured as public.
Map<String, Object> credentials = identityServiceConfig.getCredentials();
if (credentials == null || !credentials.containsKey("secret"))
{
credentials = credentials == null ? new HashMap<>() : new HashMap<>(credentials);
credentials.put("secret", "");
}
// Create default AuthzClient for authenticating users against keycloak
String authServerUrl = identityServiceConfig.getAuthServerUrl();
String realm = identityServiceConfig.getRealm();
String resource = identityServiceConfig.getResource();
Configuration authzConfig = new Configuration(authServerUrl, realm, resource, credentials, client);
AuthzClient authzClient = AuthzClient.create(authzConfig);
if (logger.isDebugEnabled())
{
logger.debug(" Created Keycloak AuthzClient");
logger.debug(" Keycloak AuthzClient server URL: " + authzClient.getConfiguration().getAuthServerUrl());
logger.debug(" Keycloak AuthzClient realm: " + authzClient.getConfiguration().getRealm());
logger.debug(" Keycloak AuthzClient resource: " + authzClient.getConfiguration().getResource());
}
return authzClient;
}
@Override
public Class<?> getObjectType()
{
return AuthenticatorAuthzClientFactoryBean.class;
}
@Override
public boolean isSingleton()
{
return true;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,38 +25,35 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.net.ConnectException;
import org.alfresco.error.ExceptionStackUtil;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.util.HttpResponseException;
/**
*
* Authenticates a user against Keycloak.
* Keycloak's {@link AuthzClient} is used to retrieve an access token for the provided user credentials,
* user is set as the current user if the user's access token can be obtained.
* Authenticates a user against Identity Service (Keycloak).
* {@link OAuth2Client} is used to verify provided user credentials. User is set as the current user if the user
* credentials are valid.
* <br>
* The AuthzClient can be null in which case this authenticator will just fall through to the next one in the chain.
* The {@link IdentityServiceAuthenticationComponent#oAuth2Client} can be null in which case this authenticator will
* just fall through to the next one in the chain.
*
*/
public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
{
private final Log logger = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
/** client used to authenticate user credentials against Keycloak **/
private AuthzClient authzClient;
private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
/** client used to authenticate user credentials against Authorization Server **/
private OAuth2Client oAuth2Client;
/** enabled flag for the identity service subsystem**/
private boolean active;
private boolean allowGuestLogin;
public void setAuthenticatorAuthzClient(AuthzClient authenticatorAuthzClient)
public void setOAuth2Client(OAuth2Client oAuth2Client)
{
this.authzClient = authenticatorAuthzClient;
this.oAuth2Client = oAuth2Client;
}
public void setAllowGuestLogin(boolean allowGuestLogin)
@@ -67,49 +64,31 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
{
if (authzClient == null)
if (oAuth2Client == null)
{
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("AuthzClient was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property. ");
LOGGER.debug("OAuth2Client was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
}
throw new AuthenticationException("User not authenticated because AuthzClient was not set.");
throw new AuthenticationException("User not authenticated because OAuth2Client was not set.");
}
try
{
// Attempt to get an access token using the user credentials
authzClient.obtainAccessToken(userName, new String(password));
// Attempt to verify user credentials
oAuth2Client.verifyCredentials(userName, new String(password));
// Successfully obtained access token so treat as authenticated user
// Verification was successful so treat as authenticated user
setCurrentUser(userName);
}
catch (HttpResponseException e)
catch (CredentialsVerificationException e)
{
if (logger.isDebugEnabled())
{
logger.debug("Failed to authenticate user against Keycloak. Status: " + e.getStatusCode() + " Reason: "+ e.getReasonPhrase());
}
throw new AuthenticationException("Failed to authenticate user against Keycloak.", e);
throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", e);
}
catch (RuntimeException e)
{
Throwable cause = ExceptionStackUtil.getCause(e, ConnectException.class);
if (cause != null)
{
if (logger.isWarnEnabled())
{
logger.warn("Couldn't connect to Keycloak server to authenticate user. Reason: " + cause.getMessage());
}
throw new AuthenticationException("Couldn't connect to Keycloak server to authenticate user.", cause);
}
if (logger.isDebugEnabled())
{
logger.debug("Error occurred while authenticating user against Keycloak. Reason: " + e.getMessage());
}
throw new AuthenticationException("Error occurred while authenticating user against Keycloak.", e);
throw new AuthenticationException("Failed to verify user credentials.", e);
}
}
@@ -129,4 +108,32 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
{
return allowGuestLogin;
}
/**
* An abstraction for acting as an OAuth2 Client
*/
interface OAuth2Client
{
/**
* The OAuth2's Client role is only used to verify the user credentials (Resource Owner Password
* Credentials Flow) this is why there is an explicit method for verifying these.
* @param userName user's name
* @param password user's password
* @throws CredentialsVerificationException when the verification failed or couldn't be performed
*/
void verifyCredentials(String userName, String password);
class CredentialsVerificationException extends RuntimeException
{
CredentialsVerificationException(String message)
{
super(message);
}
CredentialsVerificationException(String message, Throwable cause)
{
super(message, cause);
}
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,6 +26,7 @@
package org.alfresco.repo.security.authentication.identityservice;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.TreeMap;
@@ -33,6 +34,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Class to hold configuration for the Identity Service.
@@ -41,8 +43,9 @@ import org.springframework.beans.factory.InitializingBean;
*/
public class IdentityServiceConfig extends AdapterConfig implements InitializingBean
{
private static Log logger = LogFactory.getLog(IdentityServiceConfig.class);
private static final Log LOGGER = LogFactory.getLog(IdentityServiceConfig.class);
private static final String REALMS = "realms";
private static final String SECRET = "secret";
private static final String CREDENTIALS_SECRET = "identity-service.credentials.secret";
private static final String CREDENTIALS_PROVIDER = "identity-service.credentials.provider";
@@ -95,13 +98,13 @@ public class IdentityServiceConfig extends AdapterConfig implements Initializing
@Override
public void afterPropertiesSet() throws Exception
{
// programatically build the more complex objects i.e. credentials
// programmatically build the more complex objects i.e. credentials
Map<String, Object> credentials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
String secret = this.globalProperties.getProperty(CREDENTIALS_SECRET);
if (secret != null && !secret.isEmpty())
{
credentials.put("secret", secret);
credentials.put(SECRET, secret);
}
String provider = this.globalProperties.getProperty(CREDENTIALS_PROVIDER);
@@ -116,10 +119,27 @@ public class IdentityServiceConfig extends AdapterConfig implements Initializing
{
this.setCredentials(credentials);
if (logger.isDebugEnabled())
if (LOGGER.isDebugEnabled())
{
logger.debug("Created credentials map from config: " + credentials);
LOGGER.debug("Created credentials map from config: " + credentials);
}
}
}
String getIssuerUrl()
{
return UriComponentsBuilder.fromUriString(getAuthServerUrl())
.pathSegment(REALMS, getRealm())
.build()
.toString();
}
public String getClientSecret()
{
return Optional.ofNullable(getCredentials())
.map(c -> c.get(SECRET))
.filter(String.class::isInstance)
.map(String.class::cast)
.orElse("");
}
}

View File

@@ -0,0 +1,270 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder.PasswordGrantBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
*
* Creates an instance of {@link OAuth2Client}. <br>
* The creation of {@link OAuth2Client} requires connection to the Identity Service (Keycloak), disable this factory if
* the server cannot be reached. <br>
* This factory can return a null if it is disabled.
*
*/
public class OAuth2ClientFactoryBean implements FactoryBean<OAuth2Client>
{
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientFactoryBean.class);
private IdentityServiceConfig identityServiceConfig;
private boolean enabled;
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
{
this.identityServiceConfig = identityServiceConfig;
}
@Override
public OAuth2Client getObject() throws Exception
{
// The creation of the client can be disabled for testing or when the username/password authentication is not required,
// for instance when Keycloak is configured for 'bearer only' authentication or Direct Access Grants are disabled.
if (!enabled)
{
return null;
}
// The OAuth2AuthorizedClientManager isn't created upfront to make the code resilient to Identity Service being down.
// If it's down the Application Context will start and when it's back online it can be used.
return new SpringOAuth2Client(this::createOAuth2AuthorizedClientManager);
}
private OAuth2AuthorizedClientManager createOAuth2AuthorizedClientManager()
{
//Here we preserve the behaviour of previously used Keycloak Adapter
// * Client is authenticating itself using basic auth
// * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
// * There is no caching of authenticated clients (NoStoredAuthorizedClient)
// * There is only one Authorization Server/Client pair (SingleClientRegistration)
final ClientRegistration clientRegistration = ClientRegistrations
.fromIssuerLocation(identityServiceConfig.getIssuerUrl())
.clientId(identityServiceConfig.getResource())
.clientSecret(identityServiceConfig.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.registrationId(SpringOAuth2Client.CLIENT_REGISTRATION_ID)
.build();
final AuthorizedClientServiceOAuth2AuthorizedClientManager oauth2 =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
new SingleClientRegistration(clientRegistration),
new NoStoredAuthorizedClient());
oauth2.setContextAttributesMapper(OAuth2AuthorizeRequest::getAttributes);
oauth2.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder()
.password(this::configureTimeouts)
.build());
if (LOGGER.isDebugEnabled())
{
LOGGER.debug(" Created OAuth2 Client");
LOGGER.debug(" OAuth2 Issuer URL: " + clientRegistration.getProviderDetails().getIssuerUri());
LOGGER.debug(" OAuth2 ClientId: " + clientRegistration.getClientId());
}
return oauth2;
}
private void configureTimeouts(PasswordGrantBuilder builder)
{
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(identityServiceConfig.getClientConnectionTimeout());
requestFactory.setReadTimeout(identityServiceConfig.getClientSocketTimeout());
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
client.setRestOperations(restTemplate);
builder.accessTokenResponseClient(client);
}
@Override
public Class<?> getObjectType()
{
return OAuth2Client.class;
}
@Override
public boolean isSingleton()
{
return true;
}
static class SpringOAuth2Client implements OAuth2Client
{
private static final String CLIENT_REGISTRATION_ID = "ids";
private final Supplier<OAuth2AuthorizedClientManager> authorizedClientManagerSupplier;
private final AtomicReference<OAuth2AuthorizedClientManager> authorizedClientManager = new AtomicReference<>();
public SpringOAuth2Client(Supplier<OAuth2AuthorizedClientManager> authorizedClientManagerSupplier)
{
this.authorizedClientManagerSupplier = Objects.requireNonNull(authorizedClientManagerSupplier);
}
@Override
public void verifyCredentials(String userName, String password)
{
final OAuth2AuthorizedClientManager clientManager;
try
{
clientManager = getAuthorizedClientManager();
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to instantiate OAuth2AuthorizedClientManager.", e);
throw new CredentialsVerificationException("Unable to use the Authorization Server.", e);
}
final OAuth2AuthorizedClient authorizedClient;
try
{
final OAuth2AuthorizeRequest authRequest = createPasswordCredentialsRequest(userName, password);
authorizedClient = clientManager.authorize(authRequest);
}
catch (OAuth2AuthorizationException e)
{
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
throw new CredentialsVerificationException("Authorization against the Authorization Server failed with " + e.getError() + ".", e);
}
catch (RuntimeException e)
{
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
throw new CredentialsVerificationException("Failed to authorize against Authorization Server.", e);
}
if (authorizedClient == null || authorizedClient.getAccessToken() == null)
{
throw new CredentialsVerificationException("Resource Owner Password Credentials is not supported by the Authorization Server.");
}
}
private OAuth2AuthorizedClientManager getAuthorizedClientManager()
{
final OAuth2AuthorizedClientManager current = authorizedClientManager.get();
if (current != null)
{
return current;
}
return authorizedClientManager
.updateAndGet(prev -> prev != null ? prev : authorizedClientManagerSupplier.get());
}
private OAuth2AuthorizeRequest createPasswordCredentialsRequest(String userName, String password)
{
return OAuth2AuthorizeRequest
.withClientRegistrationId(CLIENT_REGISTRATION_ID)
.principal(userName)
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, userName)
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password)
.build();
}
}
private static class NoStoredAuthorizedClient implements OAuth2AuthorizedClientService
{
@Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName)
{
return null;
}
@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal)
{
//do nothing
}
@Override
public void removeAuthorizedClient(String clientRegistrationId, String principalName)
{
//do nothing
}
}
private static class SingleClientRegistration implements ClientRegistrationRepository
{
private final ClientRegistration clientRegistration;
private SingleClientRegistration(ClientRegistration clientRegistration)
{
this.clientRegistration = Objects.requireNonNull(clientRegistration);
}
@Override
public ClientRegistration findByRegistrationId(String registrationId)
{
return Objects.equals(registrationId, clientRegistration.getRegistrationId()) ? clientRegistration : null;
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,7 +26,6 @@
package org.alfresco.repo.tagging;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
@@ -41,9 +40,11 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
@@ -64,11 +65,13 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListener;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
@@ -912,51 +915,23 @@ public class TaggingServiceImpl implements TaggingService,
return new EmptyPagingResults<Pair<NodeRef, String>>();
}
public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest)
{
return getTags(storeRef, pagingRequest, null, null);
}
/**
* @see org.alfresco.service.cmr.tagging.TaggingService#getTags(org.alfresco.service.cmr.repository.StoreRef, org.alfresco.query.PagingRequest)
*/
public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest)
public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest, Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
{
ParameterCheck.mandatory("storeRef", storeRef);
PagingResults<ChildAssociationRef> rootCategories = this.categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, pagingRequest, true);
final List<Pair<NodeRef, String>> result = new ArrayList<Pair<NodeRef, String>>(rootCategories.getPage().size());
for (ChildAssociationRef rootCategory : rootCategories.getPage())
{
String name = (String)this.nodeService.getProperty(rootCategory.getChildRef(), ContentModel.PROP_NAME);
result.add(new Pair<NodeRef, String>(rootCategory.getChildRef(), name));
}
final boolean hasMoreItems = rootCategories.hasMoreItems();
final Pair<Integer, Integer> totalResultCount = rootCategories.getTotalResultCount();
final String queryExecutionId = rootCategories.getQueryExecutionId();
rootCategories = null;
PagingResults<ChildAssociationRef> rootCategories = categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, pagingRequest, true,
exactNamesFilter, alikeNamesFilter);
return new PagingResults<Pair<NodeRef, String>>()
{
@Override
public List<Pair<NodeRef, String>> getPage()
{
return result;
}
@Override
public boolean hasMoreItems()
{
return hasMoreItems;
}
@Override
public Pair<Integer, Integer> getTotalResultCount()
{
return totalResultCount;
}
@Override
public String getQueryExecutionId()
{
return queryExecutionId;
}
};
return mapPagingResult(rootCategories,
(childAssociation) -> new Pair<>(childAssociation.getChildRef(), childAssociation.getQName().getLocalName()));
}
/**
@@ -1575,4 +1550,59 @@ public class TaggingServiceImpl implements TaggingService,
}
}
@Experimental
@Override
public List<Pair<String, NodeRef>> createTags(final StoreRef storeRef, final List<String> tagNames)
{
updateTagBehaviour.disable();
createTagBehaviour.disable();
try
{
return tagNames.stream()
.map(String::toLowerCase)
.peek(tagName -> categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, tagName, false).stream()
.filter(association -> Objects.nonNull(association.getChildRef()))
.findAny()
.ifPresent(association -> { throw new DuplicateChildNodeNameException(association.getParentRef(), association.getTypeQName(), tagName, null); }))
.map(tagName -> new Pair<>(tagName, getTagNodeRef(storeRef, tagName, true)))
.collect(Collectors.toList());
}
finally
{
updateTagBehaviour.enable();
createTagBehaviour.enable();
}
}
private <T, R> PagingResults<R> mapPagingResult(final PagingResults<T> pagingResults, final Function<T, R> mapper)
{
return new PagingResults<R>()
{
@Override
public List<R> getPage()
{
return pagingResults.getPage().stream()
.map(mapper)
.collect(Collectors.toList());
}
@Override
public boolean hasMoreItems()
{
return pagingResults.hasMoreItems();
}
@Override
public Pair<Integer, Integer> getTotalResultCount()
{
return pagingResults.getTotalResultCount();
}
@Override
public String getQueryExecutionId()
{
return pagingResults.getQueryExecutionId();
}
};
}
}

View File

@@ -1,140 +0,0 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.urlshortening;
import java.util.ArrayList;
import java.util.List;
import org.alfresco.service.cmr.urlshortening.UrlShortener;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* @deprecated as it is no longer used in the core repository code.
*/
@Deprecated
public class BitlyUrlShortenerImpl implements UrlShortener
{
private static final Log log = LogFactory.getLog(BitlyUrlShortenerImpl.class);
private int urlLength = 20;
private String username;
private String apiKey = "R_ca15c6c89e9b25ccd170bafd209a0d4f";
private HttpClient httpClient;
public BitlyUrlShortenerImpl()
{
httpClient = new HttpClient();
httpClient.setHttpConnectionManager(new MultiThreadedHttpConnectionManager());
HostConfiguration hostConfiguration = new HostConfiguration();
hostConfiguration.setHost("api-ssl.bitly.com", 443, Protocol.getProtocol("https"));
httpClient.setHostConfiguration(hostConfiguration);
}
@Override
public String shortenUrl(String longUrl)
{
if (log.isDebugEnabled())
{
log.debug("Shortening URL: " + longUrl);
}
String shortUrl = longUrl;
if (longUrl.length() > urlLength)
{
GetMethod getMethod = new GetMethod();
getMethod.setPath("/v3/shorten");
List<NameValuePair> args = new ArrayList<NameValuePair>();
args.add(new NameValuePair("login", username));
args.add(new NameValuePair("apiKey", apiKey));
args.add(new NameValuePair("longUrl", longUrl));
args.add(new NameValuePair("format", "txt"));
getMethod.setQueryString(args.toArray(new NameValuePair[args.size()]));
try
{
int resultCode = httpClient.executeMethod(getMethod);
if (resultCode == 200)
{
shortUrl = getMethod.getResponseBodyAsString();
}
else
{
log.warn("Failed to shorten URL " + longUrl + " - response code == " + resultCode);
log.warn(getMethod.getResponseBodyAsString());
}
}
catch (Exception ex)
{
log.error("Failed to shorten URL " + longUrl, ex);
}
if (log.isDebugEnabled())
{
log.debug("URL " + longUrl + " has been shortened to " + shortUrl);
}
}
return shortUrl.trim();
}
/**
* {@inheritDoc}
*/
@Override
public int getUrlLength()
{
return urlLength;
}
/**
* @param urlLength the urlLength to set
*/
public void setUrlLength(int urlLength)
{
this.urlLength = urlLength;
}
/**
* @param username the username to set
*/
public void setUsername(String username)
{
this.username = username;
}
/**
* @param apiKey the apiKey to set
*/
public void setApiKey(String apiKey)
{
this.apiKey = apiKey;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Optional;
import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.service.Auditable;
@@ -136,6 +137,24 @@ public interface CategoryService
@Auditable(parameters = {"storeRef", "aspectName", "pagingRequest", "sortByName", "filter"})
PagingResults<ChildAssociationRef> getRootCategories(StoreRef storeRef, QName aspectName, PagingRequest pagingRequest, boolean sortByName, String filter);
/**
* Get a paged list of the root categories for an aspect/classification supporting multiple name filters.
*
* @param storeRef
* @param aspectName
* @param pagingRequest
* @param sortByName
* @param exactNamesFilter
* @param alikeNamesFilter
* @return
*/
@Auditable(parameters = {"storeRef", "aspectName", "pagingRequest", "sortByName", "exactNamesFilter", "alikeNamesFilter"})
default PagingResults<ChildAssociationRef> getRootCategories(StoreRef storeRef, QName aspectName, PagingRequest pagingRequest, boolean sortByName,
Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
{
return new EmptyPagingResults<>();
}
/**
* Get the root categories for an aspect/classification with names that start with filter
*

View File

@@ -1,36 +1,40 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.service.cmr.tagging;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.service.Auditable;
import org.alfresco.service.Experimental;
import org.alfresco.service.NotAuditable;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.StoreRef;
@@ -73,6 +77,21 @@ public interface TaggingService
*/
@NotAuditable
PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest);
/**
* Get a paged list of tags filtered by name
*
* @param storeRef StoreRef
* @param pagingRequest PagingRequest
* @param exactNamesFilter PagingRequest
* @param alikeNamesFilter PagingRequest
* @return PagingResults
*/
@NotAuditable
default PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest, Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
{
return new EmptyPagingResults<>();
}
/**
* Get all the tags currently available that match the provided filter.
@@ -306,17 +325,31 @@ public interface TaggingService
*/
@NotAuditable
Pair<List<String>, Integer> getPagedTags(StoreRef storeRef, String filter, int fromTag, int pageSize);
/**
* Get tagged nodes and count of nodes group by tag name
*
* @param storeRef
* @return
*/
@NotAuditable
List<Pair<String, Integer>> findTaggedNodesAndCountByTagName(StoreRef storeRef);
/**
* Get tagged nodes and count of nodes group by tag name
*
* @param storeRef
* @return
*/
@NotAuditable
List<Pair<String, Integer>> findTaggedNodesAndCountByTagName(StoreRef storeRef);
/**
* Creates orphan tags. Tag names case will be lowered.
*
* @param storeRef Reference to node store.
* @param tagNames List of tag names.
* @return {@link List} of {@link Pair}s of tag names and node references.
* @throws org.alfresco.service.cmr.repository.DuplicateChildNodeNameException if tag already exists.
*/
@Experimental
@Auditable(parameters = {"tagNames"})
default List<Pair<String, NodeRef>> createTags(StoreRef storeRef, List<String> tagNames)
{
return Collections.emptyList();
}
}

View File

@@ -1,5 +1,5 @@
// check category exists?
var category = search.luceneSearch("PATH:\"/cm:generalclassifiable//cm:" + url.extension + "\"");
var category = search.luceneSearch("PATH:\"/cm:categoryRoot/cm:generalclassifiable//cm:" + url.extension + "\"");
if (category == undefined)
{
status.code = 404;
@@ -9,6 +9,6 @@ if (category == undefined)
else
{
// perform category search
var nodes = search.luceneSearch("PATH:\"/cm:generalclassifiable//cm:" + url.extension + "//member\"");
var nodes = search.luceneSearch("PATH:\"/cm:categoryRoot/cm:generalclassifiable//cm:" + url.extension + "//member\"");
model.resultset = nodes;
}
}

View File

@@ -34,7 +34,7 @@
<value>{http://www.alfresco.org/model/content/1.0}categories</value>
</key>
<!-- Note - FreeMarker ${..} entries must be escaped in Spring context files -->
<value>\$\{selectSingleNode('workspace://SpacesStore', 'lucene', 'PATH:"/cm:generalclassifiable/cm:Languages/cm:English"' )\}</value>
<value>\$\{selectSingleNode('workspace://SpacesStore', 'lucene', 'PATH:"/cm:categoryRoot/cm:generalclassifiable/cm:Languages/cm:English"' )\}</value>
</entry>
</map>
</property>

View File

@@ -1159,7 +1159,7 @@
on z.parent_node_id = #{parentNode.id}
and z.child_node_id = a.parent_node_id
where c.child_node_id = a.child_node_id
);
)
</select>
<select id="select_ChildAssocsByPropertyValue" parameterType="ChildProperty" resultMap="result_ChildAssoc">

View File

@@ -813,13 +813,6 @@ solr6.store.mappings.value.solrMappingHistory.baseUrl=/solr/history
solr6.store.mappings.value.solrMappingHistory.protocol=workspace
solr6.store.mappings.value.solrMappingHistory.identifier=history
#
# URL Shortening Properties
#
urlshortening.bitly.username=brianalfresco
urlshortening.bitly.api.key=R_ca15c6c89e9b25ccd170bafd209a0d4f
urlshortening.bitly.url.length=20
#
# Bulk Filesystem Importer
#
@@ -1156,7 +1149,7 @@ smart.folders.config.type.templates.path=${spaces.dictionary.childname}/${spaces
smart.folders.config.type.templates.qname.filter=none
# Preferred password encoding, md4, sha256, bcrypt10
system.preferred.password.encoding=md4
system.preferred.password.encoding=bcrypt10
# Upgrade Password Hash Job
system.upgradePasswordHash.jobBatchSize=100

View File

@@ -21,12 +21,12 @@
<property name="allowGuestLogin">
<value>${identity-service.authentication.allowGuestLogin}</value>
</property>
<property name="authenticatorAuthzClient">
<ref bean="authenticatorAuthzClient"/>
<property name="oAuth2Client">
<ref bean="oAuth2Client"/>
</property>
</bean>
<bean name="authenticatorAuthzClient" class="org.alfresco.repo.security.authentication.identityservice.AuthenticatorAuthzClientFactoryBean">
<bean name="oAuth2Client" class="org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean">
<property name="identityServiceConfig">
<ref bean="identityServiceConfig" />
</property>

View File

@@ -84,7 +84,6 @@ import org.junit.runners.Suite;
org.alfresco.repo.transfer.HttpClientTransmitterImplTest.class,
org.alfresco.repo.transfer.manifest.TransferManifestTest.class,
org.alfresco.repo.transfer.TransferVersionCheckerImplTest.class,
org.alfresco.repo.urlshortening.BitlyUrlShortenerTest.class,
org.alfresco.service.cmr.calendar.CalendarRecurrenceHelperTest.class,
org.alfresco.service.cmr.calendar.CalendarTimezoneHelperTest.class,
org.alfresco.tools.RenameUserTest.class,
@@ -137,6 +136,7 @@ import org.junit.runners.Suite;
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
org.alfresco.repo.security.authentication.identityservice.SpringOAuth2ClientUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
@@ -244,7 +244,8 @@ import org.junit.runners.Suite;
org.alfresco.repo.event2.RepoEvent2UnitSuite.class,
org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class
org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class,
org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class
})
public class AllUnitTestsSuite
{

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -32,6 +32,7 @@ import static org.awaitility.Awaitility.await;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.jms.ConnectionFactory;
@@ -47,6 +48,7 @@ import org.alfresco.repo.event.v1.model.Resource;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.service.cmr.dictionary.CustomModelService;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
@@ -89,6 +91,11 @@ public abstract class AbstractContextAwareRepoEvent extends BaseSpringTest
private static final String CAMEL_ROUTE = "jms:topic:" + TOPIC_NAME;
private static final CamelContext CAMEL_CONTEXT = new DefaultCamelContext();
protected final Locale defaultLocale = new Locale(MLText.getDefaultLocale().getLanguage());
protected final Locale germanLocale = new Locale(Locale.GERMAN.getLanguage(), "XX");
protected final Locale frenchLocale = new Locale(Locale.FRENCH.getLanguage());
protected final Locale japaneseLocale = new Locale(Locale.JAPANESE.getLanguage(), "YY", "ZZ");
private static boolean isCamelConfigured;
private static DataFormat dataFormat;
@@ -388,6 +395,20 @@ public abstract class AbstractContextAwareRepoEvent extends BaseSpringTest
return (T) resource.getProperties().get(propertyName);
}
protected String getLocalizedProperty(NodeResource resource, String propertyName, Locale locale)
{
assertTrue(containsLocalizedProperty(resource, propertyName, locale));
return resource.getLocalizedProperties().get(propertyName).get(locale.toString());
}
protected boolean containsLocalizedProperty(NodeResource resource, String propertyName, Locale locale)
{
assertNotNull(resource);
assertNotNull(resource.getLocalizedProperties());
assertNotNull(resource.getLocalizedProperties().get(propertyName));
return resource.getLocalizedProperties().get(propertyName).containsKey(locale.toString());
}
/**
* Await at most 5 seconds for the condition (ie. {@code numOfEvents})
* to be met before throwing a timeout exception.

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -38,6 +38,7 @@ import org.alfresco.repo.event.v1.model.EventData;
import org.alfresco.repo.event.v1.model.EventType;
import org.alfresco.repo.event.v1.model.NodeResource;
import org.alfresco.repo.event.v1.model.RepoEvent;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
@@ -50,7 +51,6 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
public class CreateRepoEventIT extends AbstractContextAwareRepoEvent
{
@Autowired
private NodeDAO nodeDAO;
@@ -63,6 +63,9 @@ public class CreateRepoEventIT extends AbstractContextAwareRepoEvent
PropertyMap propertyMap = new PropertyMap();
propertyMap.put(ContentModel.PROP_TITLE, "test title");
propertyMap.put(ContentModel.PROP_NAME, name);
final MLText localizedDescription = new MLText(germanLocale, "german description");
localizedDescription.addValue(defaultLocale, "default description");
propertyMap.put(ContentModel.PROP_DESCRIPTION, localizedDescription);
final NodeRef nodeRef = createNode(ContentModel.TYPE_CONTENT, localName, propertyMap);
final RepoEvent<EventData<NodeResource>> resultRepoEvent = getRepoEvent(1);
@@ -91,6 +94,10 @@ public class CreateRepoEventIT extends AbstractContextAwareRepoEvent
assertNotNull(nodeResource.getPrimaryHierarchy());
assertNotNull("Default aspects were not added. ", nodeResource.getAspectNames());
assertEquals("test title", getProperty(nodeResource, "cm:title"));
assertEquals("test title", getLocalizedProperty(nodeResource, "cm:title", defaultLocale));
assertEquals("default description", getProperty(nodeResource, "cm:description"));
assertEquals("default description", getLocalizedProperty(nodeResource, "cm:description", defaultLocale));
assertEquals("german description", getLocalizedProperty(nodeResource, "cm:description", germanLocale));
assertNull("There is no content.", nodeResource.getContent());
assertNotNull("Missing createdByUser property.", nodeResource.getCreatedByUser());

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -31,6 +31,7 @@ import org.alfresco.repo.event.v1.model.EventData;
import org.alfresco.repo.event.v1.model.EventType;
import org.alfresco.repo.event.v1.model.NodeResource;
import org.alfresco.repo.event.v1.model.RepoEvent;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
@@ -48,6 +49,9 @@ public class DeleteRepoEventIT extends AbstractContextAwareRepoEvent
String localName = GUID.generate();
PropertyMap propertyMap = new PropertyMap();
propertyMap.put(ContentModel.PROP_TITLE, "test title");
final MLText localizedDescription = new MLText(germanLocale, "german description");
localizedDescription.addValue(defaultLocale, "default description");
propertyMap.put(ContentModel.PROP_DESCRIPTION, localizedDescription);
NodeRef nodeRef = createNode(ContentModel.TYPE_CONTENT, localName, propertyMap);
NodeResource createdResource = getNodeResource(1);
@@ -62,10 +66,16 @@ public class DeleteRepoEventIT extends AbstractContextAwareRepoEvent
deleteNode(nodeRef);
final RepoEvent<EventData<NodeResource>> resultRepoEvent = getRepoEvent(2);
final NodeResource nodeResource = getNodeResource(resultRepoEvent);
assertEquals("Repo event type:", EventType.NODE_DELETED.getType(), resultRepoEvent.getType());
assertEquals(createdResource.getId(), getNodeResource(resultRepoEvent).getId());
assertEquals(createdResource.getId(), nodeResource.getId());
assertEquals("Wrong primaryAssocQName prefix.", "ce:" + localName, createdResource.getPrimaryAssocQName());
assertEquals("test title", getProperty(nodeResource, "cm:title"));
assertEquals("test title", getLocalizedProperty(nodeResource, "cm:title", defaultLocale));
assertEquals("default description", getProperty(nodeResource, "cm:description"));
assertEquals("default description", getLocalizedProperty(nodeResource, "cm:description", defaultLocale));
assertEquals("german description", getLocalizedProperty(nodeResource, "cm:description", germanLocale));
// There should be no resourceBefore
EventData<NodeResource> eventData = getEventData(resultRepoEvent);

View File

@@ -0,0 +1,114 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.event2;
import static org.alfresco.repo.event2.NodeResourceHelper.getLocalizedPropertiesBefore;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
public class NodeResourceHelperUnitTest
{
@Test
public void shouldExtractOnlyRelevantPropertiesForBeforeNode()
{
final Map<String, Map<String, String>> before =
Map.of(
"unchanged-empty", locValues(),
"unchanged-non-empty", locValues("pl", "Kiełbasa", "en", "Sausage"),
"changed-added", locValues("pl", "Kiełbasa"),
"changed-modified", locValues("pl", "XYZ", "en", "Sausage"),
"changed-deleted", locValues("pl", "Kiełbasa", "en", "Sausage"),
"changed-added-modified-deleted", locValues("pl", "XYZ", "en", "Sausage"),
"changed-to-empty", locValues("pl", "Kiełbasa", "en", "Sausage"),
"changed-from-empty", locValues(),
"removed-empty", locValues(),
"removed-non-empty", locValues("pl", "Kiełbasa", "en", "Sausage")
);
final Map<String, Map<String, String>> after =
Map.of(
"unchanged-empty", locValues(),
"unchanged-non-empty", locValues("pl", "Kiełbasa", "en", "Sausage"),
"changed-added", locValues("pl", "Kiełbasa", "en", "Sausage"),
"changed-modified", locValues("pl", "Kiełbasa", "en", "Sausage"),
"changed-deleted", locValues("en", "Sausage"),
"changed-added-modified-deleted", locValues("pl", "Kiełbasa", "de", "Würst"),
"changed-to-empty", locValues(),
"changed-from-empty", locValues("pl", "Kiełbasa", "en", "Sausage"),
"new-empty", locValues(),
"new-non-empty", locValues("de", "Würst")
);
final Map<String, Map<String, String>> diff = getLocalizedPropertiesBefore(before, after);
assertFalse(diff.containsKey("unchanged-empty"));
assertFalse(diff.containsKey("unchanged-non-empty"));
assertEquals(locValues("en", null), diff.get("changed-added"));
assertEquals(locValues("pl", "XYZ"), diff.get("changed-modified"));
assertEquals(locValues("pl", "Kiełbasa"), diff.get("changed-deleted"));
assertEquals(locValues("pl", "XYZ", "en", "Sausage", "de", null), diff.get("changed-added-modified-deleted"));
assertEquals(locValues("pl", "Kiełbasa", "en", "Sausage"), diff.get("changed-to-empty"));
assertEquals(locValues("pl", null, "en", null), diff.get("changed-from-empty"));
assertFalse(diff.containsKey("removed-empty"));
assertEquals(locValues("pl", "Kiełbasa", "en", "Sausage"), diff.get("removed-non-empty"));
assertFalse(diff.containsKey("new-empty"));
assertEquals(locValues("de", null), diff.get("new-non-empty"));
}
private LocalizedValues locValues(String l1, String v1, String l2, String v2, String l3, String v3)
{
return locValues(l1, v1, l2, v2).append(l3, v3);
}
private LocalizedValues locValues(String l1, String v1, String l2, String v2)
{
return locValues(l1, v1).append(l2, v2);
}
private LocalizedValues locValues(String l1, String v1)
{
return locValues().append(l1, v1);
}
private LocalizedValues locValues()
{
return new LocalizedValues();
}
private static class LocalizedValues extends HashMap<String, String>
{
public LocalizedValues append(String language, String value)
{
this.put(language, value);
return this;
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -34,7 +34,8 @@ import org.junit.runners.Suite.SuiteClasses;
@SuiteClasses({ EventFilterUnitTest.class,
EventConsolidatorUnitTest.class,
EventJSONSchemaUnitTest.class,
EventGeneratorQueueUnitTest.class
EventGeneratorQueueUnitTest.class,
NodeResourceHelperUnitTest.class
})
public class RepoEvent2UnitSuite
{

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2020 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -26,6 +26,8 @@
package org.alfresco.repo.event2;
import static org.alfresco.model.ContentModel.PROP_DESCRIPTION;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
@@ -46,6 +48,7 @@ import org.alfresco.service.cmr.dictionary.CustomModelDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.MLText;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
@@ -279,6 +282,60 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent
assertNull(resourceBefore.getPrimaryAssocQName());
}
@Test
public void testUpdateContentWithLocalizedProperties()
{
final String description = "cm:description";
final NodeRef nodeRef = createNode(ContentModel.TYPE_CONTENT);
NodeResource resource = getNodeResource(1);
assertNull(getProperty(resource, description));
assertNull(resource.getLocalizedProperties());
assertNull(getEventData(1).getResourceBefore());
retryingTransactionHelper.doInTransaction(() -> {
final MLText localizedDescription = new MLText(germanLocale, "german description");
localizedDescription.addValue(defaultLocale, "default description");
localizedDescription.addValue(japaneseLocale, "japanese description");
nodeService.setProperty(nodeRef, PROP_DESCRIPTION, localizedDescription);
return null;
});
resource = getNodeResource(2);
NodeResource resourceBefore = getNodeResourceBefore(2);
assertEquals("default description", getProperty(resource, description));
assertEquals("default description", getLocalizedProperty(resource, description, defaultLocale));
assertEquals("german description", getLocalizedProperty(resource, description, germanLocale));
assertEquals("japanese description", getLocalizedProperty(resource, description, japaneseLocale));
assertNull(getLocalizedProperty(resourceBefore, description, defaultLocale));
assertNull(getLocalizedProperty(resourceBefore, description, germanLocale));
assertNull(getLocalizedProperty(resourceBefore, description, japaneseLocale));
retryingTransactionHelper.doInTransaction(() -> {
final MLText localizedDescription = new MLText(frenchLocale, "french description added");
localizedDescription.addValue(defaultLocale, "default description modified");
localizedDescription.addValue(japaneseLocale, "japanese description");
nodeService.setProperty(nodeRef, PROP_DESCRIPTION, localizedDescription);
return null;
});
resource = getNodeResource(3);
resourceBefore = getNodeResourceBefore(3);
assertEquals("default description modified", getProperty(resource, description));
assertEquals("default description modified", getLocalizedProperty(resource, description, defaultLocale));
assertEquals("french description added", getLocalizedProperty(resource, description, frenchLocale));
assertEquals("japanese description", getLocalizedProperty(resource, description, japaneseLocale));
assertFalse(containsLocalizedProperty(resource, description, germanLocale));
assertEquals("default description", getLocalizedProperty(resourceBefore, description, defaultLocale));
assertEquals("german description", getLocalizedProperty(resourceBefore, description, germanLocale));
assertNull(getLocalizedProperty(resourceBefore, description, frenchLocale));
assertFalse(containsLocalizedProperty(resourceBefore, description, japaneseLocale));
}
@Test
public void testUpdateContentTitle()
{
@@ -296,8 +353,11 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent
});
resource = getNodeResource(2);
NodeResource resourceBefore = getNodeResourceBefore(2);
title = getProperty(resource, "cm:title");
assertEquals("test title", title);
assertEquals("test title", getLocalizedProperty(resource, "cm:title", defaultLocale));
assertNull(getLocalizedProperty(resourceBefore, "cm:title", defaultLocale));
// update content cm:title property again with "new test title" value
retryingTransactionHelper.doInTransaction(() -> {
@@ -308,10 +368,13 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent
resource = getNodeResource(3);
title = getProperty(resource, "cm:title");
assertEquals("new test title", title);
assertEquals("new test title", getLocalizedProperty(resource, "cm:title", defaultLocale));
NodeResource resourceBefore = getNodeResourceBefore(3);
resourceBefore = getNodeResourceBefore(3);
title = getProperty(resourceBefore, "cm:title");
assertEquals("Wrong old property.", "test title", title);
assertEquals("test title", getLocalizedProperty(resourceBefore, "cm:title", defaultLocale));
assertNotNull(resourceBefore.getModifiedAt());
}
@@ -354,7 +417,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent
// update content cm:description property with "test_description" value
retryingTransactionHelper.doInTransaction(() -> {
nodeService.setProperty(nodeRef, ContentModel.PROP_DESCRIPTION, "test description");
nodeService.setProperty(nodeRef, PROP_DESCRIPTION, "test description");
return null;
});
@@ -492,7 +555,7 @@ public class UpdateRepoEventIT extends AbstractContextAwareRepoEvent
QName.createQName(TEST_NAMESPACE, GUID.generate()),
ContentModel.TYPE_CONTENT).getChildRef();
nodeService.setProperty(node1, ContentModel.PROP_DESCRIPTION, "test description");
nodeService.setProperty(node1, PROP_DESCRIPTION, "test description");
return null;
});
//Create and update node are done in the same transaction so one event is expected

View File

@@ -26,15 +26,12 @@
package org.alfresco.repo.security.authentication;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
@@ -48,7 +45,6 @@ import net.sf.acegisecurity.DisabledException;
import net.sf.acegisecurity.LockedException;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.admin.SysAdminParamsImpl;
@@ -519,50 +515,48 @@ public class AuthenticationTest extends TestCase
assertTrue("The user should exist", dao.userExists(userName));
}
public void testCreateAndyUserAndOtherCRUD() throws NoSuchAlgorithmException, UnsupportedEncodingException
public void testCreateAndyUserAndUpdatePassword()
{
RepositoryAuthenticationDao dao = createRepositoryAuthenticationDao();
dao.createUser("Andy", "cabbage".toCharArray());
assertNotNull(dao.getUserOrNull("Andy"));
UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(AndyDetails);
assertEquals("Andy", AndyDetails.getUsername());
// assertNotNull(dao.getSalt(AndyDetails));
assertTrue(AndyDetails.isAccountNonExpired());
assertTrue(AndyDetails.isAccountNonLocked());
assertTrue(AndyDetails.isCredentialsNonExpired());
assertTrue(AndyDetails.isEnabled());
assertNotSame("cabbage", AndyDetails.getPassword());
assertTrue(compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(),"cabbage", AndyDetails.getPassword(), null));
assertEquals(1, AndyDetails.getAuthorities().length);
RepositoryAuthenticatedUser andyDetails = (RepositoryAuthenticatedUser) dao.loadUserByUsername("Andy");
assertNotNull("User unexpectedly null", andyDetails);
assertEquals("Unexpected username", "Andy", andyDetails.getUsername());
Object originalSalt = andyDetails.getSalt();
assertNotNull("Salt was not generated", originalSalt);
assertTrue("Account unexpectedly expired", andyDetails.isAccountNonExpired());
assertTrue("Account unexpectedly locked", andyDetails.isAccountNonLocked());
assertTrue("Credentials unexpectedly expired", andyDetails.isCredentialsNonExpired());
assertTrue("User unexpectedly disabled", andyDetails.isEnabled());
assertNotSame("Password was not hashed", "cabbage", andyDetails.getPassword());
assertTrue("Failed to recalculate same password hash", compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(),"cabbage", andyDetails.getPassword(), originalSalt));
assertEquals("User does not have a single authority", 1, andyDetails.getAuthorities().length);
// Object oldSalt = dao.getSalt(AndyDetails);
dao.updateUser("Andy", "carrot".toCharArray());
UserDetails newDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(newDetails);
assertEquals("Andy", newDetails.getUsername());
// assertNotNull(dao.getSalt(newDetails));
assertTrue(newDetails.isAccountNonExpired());
assertTrue(newDetails.isAccountNonLocked());
assertTrue(newDetails.isCredentialsNonExpired());
assertTrue(newDetails.isEnabled());
assertNotSame("carrot", newDetails.getPassword());
assertEquals(1, newDetails.getAuthorities().length);
RepositoryAuthenticatedUser newDetails = (RepositoryAuthenticatedUser) dao.loadUserByUsername("Andy");
assertNotNull("New details were null", newDetails);
assertEquals("New details contain wrong username", "Andy", newDetails.getUsername());
Object updatedSalt = newDetails.getSalt();
assertNotNull("New details contain null salt", updatedSalt);
assertTrue("Updated account is expired", newDetails.isAccountNonExpired());
assertTrue("Updated account is locked", newDetails.isAccountNonLocked());
assertTrue("Updated account has expired credentials", newDetails.isCredentialsNonExpired());
assertTrue("Updated account is not enabled", newDetails.isEnabled());
assertNotSame("Updated account contains unhashed password", "carrot", newDetails.getPassword());
assertEquals("Updated account should have a single authority", 1, newDetails.getAuthorities().length);
assertTrue("Failed to validate updated password hash", compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(),"carrot", newDetails.getPassword(), updatedSalt));
assertNotSame("Expected salt to be replaced when password was updated", originalSalt, updatedSalt);
assertNotSame(AndyDetails.getPassword(), newDetails.getPassword());
RepositoryAuthenticatedUser rau = (RepositoryAuthenticatedUser) newDetails;
assertTrue(compositePasswordEncoder.matchesPassword("carrot", newDetails.getPassword(), null, rau.getHashIndicator()));
// assertNotSame(oldSalt, dao.getSalt(newDetails));
//Update again
dao.updateUser("Andy", "potato".toCharArray());
newDetails = (UserDetails) dao.loadUserByUsername("Andy");
assertNotNull(newDetails);
assertEquals("Andy", newDetails.getUsername());
rau = (RepositoryAuthenticatedUser) newDetails;
assertTrue(compositePasswordEncoder.matchesPassword("potato", newDetails.getPassword(), null, rau.getHashIndicator()));
// Update back to first password again.
dao.updateUser("Andy", "cabbage".toCharArray());
RepositoryAuthenticatedUser thirdDetails = (RepositoryAuthenticatedUser) dao.loadUserByUsername("Andy");
Object thirdSalt = thirdDetails.getSalt();
assertNotSame("New salt should not match original salt", thirdSalt, originalSalt);
assertNotSame("New salt should not match previous salt", thirdSalt, updatedSalt);
assertTrue("New password hash was not reproducible", compositePasswordEncoder.matches(compositePasswordEncoder.getPreferredEncoding(), "cabbage", thirdDetails.getPassword(), thirdSalt));
dao.deleteUser("Andy");
assertFalse("Should not be a cache entry for 'Andy'.", authenticationCache.contains("Andy"));
@@ -1989,131 +1983,142 @@ public class AuthenticationTest extends TestCase
* Tests the scenario where a user logs in after the system has been upgraded.
* Their password should get re-hashed using the preferred encoding.
*/
public void testRehashedPasswordOnAuthentication() throws Exception
public void testRehashedPasswordOnAuthentication()
{
// create the Andy authentication
assertNull(authenticationComponent.getCurrentAuthentication());
authenticationComponent.setSystemUserAsCurrentUser();
pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray());
// find the node representing the Andy user and it's properties
NodeRef andyUserNodeRef = getRepositoryAuthenticationDao(). getUserOrNull("Andy");
assertNotNull(andyUserNodeRef);
// ensure the properties are in the state we're expecting
Map<QName, Serializable> userProps = nodeService.getProperties(andyUserNodeRef);
String passwordProp = (String)userProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
String password2Prop = (String)userProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
String passwordHashProp = (String)userProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
List<String> hashIndicatorProp = (List<String>)userProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
// re-generate an md4 hashed password
MD4PasswordEncoderImpl md4PasswordEncoder = new MD4PasswordEncoderImpl();
String md4Password = md4PasswordEncoder.encodePassword("auth1", null);
// re-generate a sha256 hashed password
String salt = (String)userProps.get(ContentModel.PROP_SALT);
ShaPasswordEncoderImpl sha256PasswordEncoder = new ShaPasswordEncoderImpl(256);
String sha256Password = sha256PasswordEncoder.encodePassword("auth1", salt);
// change the underlying user object to represent state in previous release
userProps.put(ContentModel.PROP_PASSWORD, md4Password);
userProps.put(ContentModel.PROP_PASSWORD_SHA256, sha256Password);
userProps.remove(ContentModel.PROP_PASSWORD_HASH);
userProps.remove(ContentModel.PROP_HASH_INDICATOR);
nodeService.setProperties(andyUserNodeRef, userProps);
// make sure the changes took effect
Map<QName, Serializable> updatedProps = nodeService.getProperties(andyUserNodeRef);
String usernameProp = (String)updatedProps.get(ContentModel.PROP_USER_USERNAME);
assertEquals("Expected the username property to be 'Andy'", "Andy", usernameProp);
passwordProp = (String)updatedProps.get(ContentModel.PROP_PASSWORD);
assertNotNull("Expected the password property to be populated", passwordProp);
password2Prop = (String)updatedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNotNull("Expected the password2 property to be populated", password2Prop);
passwordHashProp = (String)updatedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNull("Expected the passwordHash property to be null", passwordHashProp);
hashIndicatorProp = (List<String>)updatedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNull("Expected the hashIndicator property to be null", hashIndicatorProp);
// authenticate the user
authenticationComponent.clearCurrentSecurityContext();
pubAuthenticationService.authenticate("Andy", "auth1".toCharArray());
assertEquals("Andy", authenticationService.getCurrentUserName());
// commit the transaction to invoke the password hashing of the user
userTransaction.commit();
// start another transaction and change to system user
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
authenticationComponent.setSystemUserAsCurrentUser();
// verify that the new properties are populated and the old ones are cleaned up
Map<QName, Serializable> upgradedProps = nodeService.getProperties(andyUserNodeRef);
passwordProp = (String)upgradedProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
password2Prop = (String)upgradedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
passwordHashProp = (String)upgradedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
hashIndicatorProp = (List<String>)upgradedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
assertTrue("Expected there to be a single hash indicator entry", (hashIndicatorProp.size() == 1));
String preferredEncoding = compositePasswordEncoder.getPreferredEncoding();
String hashEncoding = (String)hashIndicatorProp.get(0);
assertEquals("Expected hash indicator to be '" + preferredEncoding + "' but it was: " + hashEncoding,
// This test requires upgrading from md4 to sha256 hashing.
String defaultPreferredEncoding = compositePasswordEncoder.getPreferredEncoding();
compositePasswordEncoder.setPreferredEncoding("md4");
try
{
// create the Andy authentication
assertNull(authenticationComponent.getCurrentAuthentication());
authenticationComponent.setSystemUserAsCurrentUser();
pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray());
// find the node representing the Andy user and its properties
NodeRef andyUserNodeRef = getRepositoryAuthenticationDao().getUserOrNull("Andy");
assertNotNull(andyUserNodeRef);
// ensure the properties are in the state we're expecting
Map<QName, Serializable> userProps = nodeService.getProperties(andyUserNodeRef);
String passwordProp = (String) userProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
String password2Prop = (String) userProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
String passwordHashProp = (String) userProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
List<String> hashIndicatorProp = (List<String>) userProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
// re-generate an md4 hashed password
MD4PasswordEncoderImpl md4PasswordEncoder = new MD4PasswordEncoderImpl();
String md4Password = md4PasswordEncoder.encodePassword("auth1", null);
// re-generate a sha256 hashed password
String salt = (String) userProps.get(ContentModel.PROP_SALT);
ShaPasswordEncoderImpl sha256PasswordEncoder = new ShaPasswordEncoderImpl(256);
String sha256Password = sha256PasswordEncoder.encodePassword("auth1", salt);
// change the underlying user object to represent state in previous release
userProps.put(ContentModel.PROP_PASSWORD, md4Password);
userProps.put(ContentModel.PROP_PASSWORD_SHA256, sha256Password);
userProps.remove(ContentModel.PROP_PASSWORD_HASH);
userProps.remove(ContentModel.PROP_HASH_INDICATOR);
nodeService.setProperties(andyUserNodeRef, userProps);
// make sure the changes took effect
Map<QName, Serializable> updatedProps = nodeService.getProperties(andyUserNodeRef);
String usernameProp = (String) updatedProps.get(ContentModel.PROP_USER_USERNAME);
assertEquals("Expected the username property to be 'Andy'", "Andy", usernameProp);
passwordProp = (String) updatedProps.get(ContentModel.PROP_PASSWORD);
assertNotNull("Expected the password property to be populated", passwordProp);
password2Prop = (String) updatedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNotNull("Expected the password2 property to be populated", password2Prop);
passwordHashProp = (String) updatedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNull("Expected the passwordHash property to be null", passwordHashProp);
hashIndicatorProp = (List<String>) updatedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNull("Expected the hashIndicator property to be null", hashIndicatorProp);
// authenticate the user
authenticationComponent.clearCurrentSecurityContext();
pubAuthenticationService.authenticate("Andy", "auth1".toCharArray());
assertEquals("Andy", authenticationService.getCurrentUserName());
// commit the transaction to invoke the password hashing of the user
userTransaction.commit();
// start another transaction and change to system user
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
authenticationComponent.setSystemUserAsCurrentUser();
// verify that the new properties are populated and the old ones are cleaned up
Map<QName, Serializable> upgradedProps = nodeService.getProperties(andyUserNodeRef);
passwordProp = (String) upgradedProps.get(ContentModel.PROP_PASSWORD);
assertNull("Expected the password property to be null", passwordProp);
password2Prop = (String) upgradedProps.get(ContentModel.PROP_PASSWORD_SHA256);
assertNull("Expected the password2 property to be null", password2Prop);
passwordHashProp = (String) upgradedProps.get(ContentModel.PROP_PASSWORD_HASH);
assertNotNull("Expected the passwordHash property to be populated", passwordHashProp);
hashIndicatorProp = (List<String>) upgradedProps.get(ContentModel.PROP_HASH_INDICATOR);
assertNotNull("Expected the hashIndicator property to be populated", hashIndicatorProp);
assertTrue("Expected there to be a single hash indicator entry", (hashIndicatorProp.size() == 1));
String preferredEncoding = compositePasswordEncoder.getPreferredEncoding();
String hashEncoding = hashIndicatorProp.get(0);
assertEquals("Expected hash indicator to be '" + preferredEncoding + "' but it was: " + hashEncoding,
preferredEncoding, hashEncoding);
// delete the user and clear the security context
this.deleteAndy();
authenticationComponent.clearCurrentSecurityContext();
// delete the user and clear the security context
this.deleteAndy();
authenticationComponent.clearCurrentSecurityContext();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
finally
{
compositePasswordEncoder.setPreferredEncoding(defaultPreferredEncoding);
}
}
/**
* For on premise the default is MD4, for cloud BCRYPT10
*
* @throws Exception
* Test password encoding with MD4 without a salt.
*/
public void testDefaultEncodingIsMD4() throws Exception
public void testGetsMD4Password()
{
assertNotNull(compositePasswordEncoder);
assertEquals("md4", compositePasswordEncoder.getPreferredEncoding());
}
String defaultPreferredEncoding = compositePasswordEncoder.getPreferredEncoding();
compositePasswordEncoder.setPreferredEncoding("md4");
/**
* For on premise the default is MD4, get it
*
* @throws Exception
*/
public void testGetsMD4Password() throws Exception
{
String user = "mduzer";
String rawPass = "roarPazzw0rd";
assertEquals("md4", compositePasswordEncoder.getPreferredEncoding());
dao.createUser(user, null, rawPass.toCharArray());
NodeRef userNodeRef = getRepositoryAuthenticationDao().getUserOrNull(user);
assertNotNull(userNodeRef);
String pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertTrue(compositePasswordEncoder.matches("md4", rawPass, pass, null));
try
{
String user = "mduzer";
String rawPass = "roarPazzw0rd";
dao.createUser(user, null, rawPass.toCharArray());
NodeRef userNodeRef = getRepositoryAuthenticationDao().getUserOrNull(user);
assertNotNull(userNodeRef);
String pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertTrue(compositePasswordEncoder.matches("md4", rawPass, pass, null));
Map<QName, Serializable> properties = nodeService.getProperties(userNodeRef);
properties.remove(ContentModel.PROP_PASSWORD_HASH);
properties.remove(ContentModel.PROP_HASH_INDICATOR);
properties.remove(ContentModel.PROP_PASSWORD);
properties.remove(ContentModel.PROP_PASSWORD_SHA256);
String encoded = compositePasswordEncoder.encode("md4",new String(rawPass), null);
properties.put(ContentModel.PROP_PASSWORD, encoded);
nodeService.setProperties(userNodeRef, properties);
pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertEquals(encoded, pass);
dao.deleteUser(user);
Map<QName, Serializable> properties = nodeService.getProperties(userNodeRef);
properties.remove(ContentModel.PROP_PASSWORD_HASH);
properties.remove(ContentModel.PROP_HASH_INDICATOR);
properties.remove(ContentModel.PROP_PASSWORD);
properties.remove(ContentModel.PROP_PASSWORD_SHA256);
String encoded = compositePasswordEncoder.encodePassword("md4", rawPass, List.of("md4"));
properties.put(ContentModel.PROP_PASSWORD, encoded);
nodeService.setProperties(userNodeRef, properties);
pass = dao.getMD4HashedPassword(user);
assertNotNull(pass);
assertEquals(encoded, pass);
dao.deleteUser(user);
}
finally
{
compositePasswordEncoder.setPreferredEncoding(defaultPreferredEncoding);
}
}
/**

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 Alfresco Software Limited
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,14 +25,17 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.ConnectException;
import org.alfresco.error.ExceptionStackUtil;
import org.alfresco.repo.security.authentication.AuthenticationContext;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
@@ -41,9 +44,6 @@ import org.alfresco.util.BaseSpringTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.representations.AccessTokenResponse;
import org.springframework.beans.factory.annotation.Autowired;
public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@@ -65,7 +65,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Autowired
private PersonService personService;
private AuthzClient mockAuthzClient;
private OAuth2Client mockOAuth2Client;
@Before
public void setUp()
@@ -76,8 +76,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
authComponent.setNodeService(nodeService);
authComponent.setPersonService(personService);
mockAuthzClient = mock(AuthzClient.class);
authComponent.setAuthenticatorAuthzClient(mockAuthzClient);
mockOAuth2Client = mock(OAuth2Client.class);
authComponent.setOAuth2Client(mockOAuth2Client);
}
@After
@@ -89,8 +89,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test (expected=AuthenticationException.class)
public void testAuthenticationFail()
{
when(mockAuthzClient.obtainAccessToken("username", "password"))
.thenThrow(new HttpResponseException("Unauthorized", 401, "Unauthorized", null));
doThrow(new CredentialsVerificationException("Failed"))
.when(mockOAuth2Client)
.verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
}
@@ -98,8 +99,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test(expected = AuthenticationException.class)
public void testAuthenticationFail_connectionException()
{
when(mockAuthzClient.obtainAccessToken("username", "password")).thenThrow(
new RuntimeException("Couldn't connect to server", new ConnectException("ConnectionRefused")));
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
.when(mockOAuth2Client)
.verifyCredentials("username", "password");
try
{
@@ -116,8 +118,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test (expected=AuthenticationException.class)
public void testAuthenticationFail_otherException()
{
when(mockAuthzClient.obtainAccessToken("username", "password"))
.thenThrow(new RuntimeException("Some other errors!"));
doThrow(new RuntimeException("Some other errors!"))
.when(mockOAuth2Client)
.verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
}
@@ -125,8 +128,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test
public void testAuthenticationPass()
{
when(mockAuthzClient.obtainAccessToken("username", "password"))
.thenReturn(new AccessTokenResponse());
doNothing().when(mockOAuth2Client).verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
@@ -135,9 +137,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
}
@Test (expected= AuthenticationException.class)
public void testFallthroughWhenAuthzClientIsNull()
public void testFallthroughWhenOAuth2ClientIsNull()
{
authComponent.setAuthenticatorAuthzClient(null);
authComponent.setOAuth2Client(null);
authComponent.authenticateImpl("username", "password".toCharArray());
}

Some files were not shown because too many files have changed in this diff Show More