mirror of
				https://github.com/Alfresco/alfresco-community-repo.git
				synced 2025-10-29 15:21:53 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			20.107
			...
			TagCreatio
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e737daa801 | ||
| 
						 | 
					640a1120fd | ||
| 
						 | 
					803cea84e3 | ||
| 
						 | 
					db2da8338a | ||
| 
						 | 
					bb5e4d42ac | ||
| 
						 | 
					dbe0d75764 | ||
| 
						 | 
					093bee9ace | ||
| 
						 | 
					e1974e6b26 | 
@@ -7,7 +7,7 @@
 | 
			
		||||
   <parent>
 | 
			
		||||
      <groupId>org.alfresco</groupId>
 | 
			
		||||
      <artifactId>alfresco-community-repo-amps</artifactId>
 | 
			
		||||
      <version>20.107</version>
 | 
			
		||||
      <version>20.90-SNAPSHOT</version>
 | 
			
		||||
   </parent>
 | 
			
		||||
 | 
			
		||||
   <modules>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
   <parent>
 | 
			
		||||
      <groupId>org.alfresco</groupId>
 | 
			
		||||
      <artifactId>alfresco-governance-services-community-parent</artifactId>
 | 
			
		||||
      <version>20.107</version>
 | 
			
		||||
      <version>20.90-SNAPSHOT</version>
 | 
			
		||||
   </parent>
 | 
			
		||||
 | 
			
		||||
   <modules>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
   <parent>
 | 
			
		||||
      <groupId>org.alfresco</groupId>
 | 
			
		||||
      <artifactId>alfresco-governance-services-automation-community-repo</artifactId>
 | 
			
		||||
      <version>20.107</version>
 | 
			
		||||
      <version>20.90-SNAPSHOT</version>
 | 
			
		||||
   </parent>
 | 
			
		||||
 | 
			
		||||
   <build>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
   <parent>
 | 
			
		||||
      <groupId>org.alfresco</groupId>
 | 
			
		||||
      <artifactId>alfresco-governance-services-community-parent</artifactId>
 | 
			
		||||
      <version>20.107</version>
 | 
			
		||||
      <version>20.90-SNAPSHOT</version>
 | 
			
		||||
   </parent>
 | 
			
		||||
 | 
			
		||||
   <modules>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
ARG BASE_IMAGE
 | 
			
		||||
# BUILD STAGE AGS
 | 
			
		||||
FROM debian:11-slim AS AGSBUILDER
 | 
			
		||||
 | 
			
		||||
@@ -13,7 +12,7 @@ RUN unzip -q /build/gs-api-explorer-*.war -d /build/gs-api-explorer && \
 | 
			
		||||
    chmod -R g-w,o= /build
 | 
			
		||||
 | 
			
		||||
# ACTUAL IMAGE
 | 
			
		||||
FROM ${BASE_IMAGE}
 | 
			
		||||
FROM alfresco/alfresco-community-repo-base:${image.tag}
 | 
			
		||||
 | 
			
		||||
# Alfresco user does not have permissions to modify webapps or configuration. Switch to root.
 | 
			
		||||
# The access will be fixed after all operations are done.
 | 
			
		||||
 
 | 
			
		||||
@@ -8,15 +8,13 @@
 | 
			
		||||
   <parent>
 | 
			
		||||
      <groupId>org.alfresco</groupId>
 | 
			
		||||
      <artifactId>alfresco-governance-services-community-repo-parent</artifactId>
 | 
			
		||||
      <version>20.107</version>
 | 
			
		||||
      <version>20.90-SNAPSHOT</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>
 | 
			
		||||
@@ -539,43 +537,9 @@
 | 
			
		||||
         </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-multiarch-docker-images</id>
 | 
			
		||||
          <id>build-docker-images</id>
 | 
			
		||||
          <!-- builds "image:latest" locally -->
 | 
			
		||||
          <build>
 | 
			
		||||
             <plugins>
 | 
			
		||||
                <plugin>
 | 
			
		||||
@@ -584,56 +548,20 @@
 | 
			
		||||
                   <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>
 | 
			
		||||
                               <args>
 | 
			
		||||
                                  <BASE_IMAGE>${local.registry}/${base.image}:${image.tag}</BASE_IMAGE>
 | 
			
		||||
                               </args>
 | 
			
		||||
                            </build>
 | 
			
		||||
                            <name>${image.name}:${image.tag}</name>
 | 
			
		||||
                         </image>
 | 
			
		||||
                      </images>
 | 
			
		||||
                   </configuration>
 | 
			
		||||
                   <executions>
 | 
			
		||||
                      <execution>
 | 
			
		||||
                         <id>build-push-image</id>
 | 
			
		||||
                         <id>build-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>
 | 
			
		||||
@@ -650,37 +578,12 @@
 | 
			
		||||
                      <images>
 | 
			
		||||
                         <image>
 | 
			
		||||
                            <!-- Quay image -->
 | 
			
		||||
                            <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>
 | 
			
		||||
                            <name>${image.name}:${image.tag}</name>
 | 
			
		||||
                            <registry>${image.registry}</registry>
 | 
			
		||||
                         </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>
 | 
			
		||||
@@ -695,28 +598,6 @@
 | 
			
		||||
                      </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>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-governance-services-community-repo-parent</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <modules>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-amps</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
   <parent>
 | 
			
		||||
      <groupId>org.alfresco</groupId>
 | 
			
		||||
      <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
      <version>20.107</version>
 | 
			
		||||
      <version>20.90-SNAPSHOT</version>
 | 
			
		||||
   </parent>
 | 
			
		||||
 | 
			
		||||
   <dependencies>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,13 +45,6 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,6 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-packaging</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
</project>
 | 
			
		||||
 
 | 
			
		||||
@@ -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-202303081618
 | 
			
		||||
FROM alfresco/alfresco-base-tomcat:tomcat9-jre17-rockylinux8-202209261711
 | 
			
		||||
 | 
			
		||||
# 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-9.el8 \
 | 
			
		||||
                   freetype-2.9.1-4.el8_3.1 \
 | 
			
		||||
                   libpng-1.6.34-5.el8 \
 | 
			
		||||
                   dejavu-sans-fonts-2.35-7.el8 && \
 | 
			
		||||
    yum clean all
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,11 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-packaging</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <image.name>alfresco/alfresco-community-repo-base</image.name>
 | 
			
		||||
        <scripts.directory>${project.parent.parent.basedir}/scripts</scripts.directory>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
@@ -157,67 +156,6 @@
 | 
			
		||||
            </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 -->
 | 
			
		||||
@@ -230,29 +168,12 @@
 | 
			
		||||
                            <images>
 | 
			
		||||
                                <!-- Quay image -->
 | 
			
		||||
                                <image>
 | 
			
		||||
                                    <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>
 | 
			
		||||
                                    <name>${image.name}:${image.tag}</name>
 | 
			
		||||
                                    <registry>${image.registry}</registry>
 | 
			
		||||
                                </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>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <modules>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-packaging</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <modules>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-tests</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <organization>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-tests</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <developers>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-tests</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <developers>
 | 
			
		||||
@@ -95,6 +95,7 @@
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.jayway.jsonpath</groupId>
 | 
			
		||||
            <artifactId>json-path</artifactId>
 | 
			
		||||
            <version>${dependency.jakarta-json-path.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-tests</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
@@ -165,14 +165,14 @@
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.codehaus.groovy</groupId>
 | 
			
		||||
            <artifactId>groovy</artifactId>
 | 
			
		||||
            <version>3.0.16</version>
 | 
			
		||||
            <version>3.0.12</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-json-->
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.codehaus.groovy</groupId>
 | 
			
		||||
            <artifactId>groovy-json</artifactId>
 | 
			
		||||
            <version>3.0.16</version>
 | 
			
		||||
            <version>3.0.12</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
       <dependency>
 | 
			
		||||
 
 | 
			
		||||
@@ -30,10 +30,7 @@ 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;
 | 
			
		||||
@@ -120,7 +117,7 @@ public class ModelsCollectionAssertion<C>
 | 
			
		||||
    return (C) modelCollection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @SuppressWarnings("unchecked")
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
  public C entriesListDoesNotContain(String key, String value)
 | 
			
		||||
  {
 | 
			
		||||
    boolean exist = false;
 | 
			
		||||
@@ -146,53 +143,6 @@ 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)
 | 
			
		||||
  {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ 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
 | 
			
		||||
@@ -354,6 +355,7 @@ 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);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,5 @@
 | 
			
		||||
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;
 | 
			
		||||
@@ -13,11 +9,19 @@ 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
 | 
			
		||||
@@ -188,7 +192,7 @@ public class GetTagsTests extends TagsDataPrep
 | 
			
		||||
                .and().field("hasMoreItems").is("false")
 | 
			
		||||
                .and().field("count").is("0")
 | 
			
		||||
                .and().field("skipCount").is(20000)
 | 
			
		||||
                .and().field("totalItems").is(0);
 | 
			
		||||
                .and().field("totalItems").isNull();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TestRail(section = { TestGroup.REST_API, TestGroup.TAGS }, executionType = ExecutionType.REGRESSION,
 | 
			
		||||
@@ -215,128 +219,11 @@ public class GetTagsTests extends TagsDataPrep
 | 
			
		||||
        RestTagModel deletedTag = restClient.authenticateUser(usersWithRoles.getOneUserWithRole(UserRole.SiteManager))
 | 
			
		||||
                .withCoreAPI().usingResource(document).addTag(removedTag);
 | 
			
		||||
 | 
			
		||||
        restClient.authenticateUser(adminUserModel).withCoreAPI().usingTag(deletedTag).deleteTag();
 | 
			
		||||
        restClient.withCoreAPI().usingResource(document).deleteTag(deletedTag);
 | 
			
		||||
        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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ 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, orphanTag, returnedModel;
 | 
			
		||||
    protected static RestTagModel documentTag, documentTag2, folderTag, returnedModel;
 | 
			
		||||
    protected static RestTagModelsCollection returnedCollection;
 | 
			
		||||
 | 
			
		||||
    @BeforeClass
 | 
			
		||||
@@ -47,17 +47,16 @@ 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", "where=(tag MATCHES ('*tag*'))")
 | 
			
		||||
                .withCoreAPI().getTags();
 | 
			
		||||
            returnedCollection.assertThat().entriesListContains("tag", documentTagValue.toLowerCase())
 | 
			
		||||
                              .and().entriesListContains("tag", documentTagValue2.toLowerCase())
 | 
			
		||||
                              .and().entriesListContains("tag", folderTagValue.toLowerCase());
 | 
			
		||||
        });
 | 
			
		||||
            {
 | 
			
		||||
                returnedCollection = restClient.withParams("maxItems=10000").withCoreAPI().getTags();
 | 
			
		||||
                returnedCollection.assertThat().entriesListContains("tag", documentTagValue.toLowerCase())
 | 
			
		||||
                                  .and().entriesListContains("tag", documentTagValue2.toLowerCase())
 | 
			
		||||
                                  .and().entriesListContains("tag", folderTagValue.toLowerCase());
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected RestTagModel createTagForDocument(FileModel document)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-tests</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <developers>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo-packaging</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								pom.xml
									
									
									
									
									
								
							@@ -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.107</version>
 | 
			
		||||
    <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    <packaging>pom</packaging>
 | 
			
		||||
    <name>Alfresco Community Repo Parent</name>
 | 
			
		||||
 | 
			
		||||
@@ -35,8 +35,6 @@
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -47,7 +45,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.4</dependency.alfresco-jlan.version>
 | 
			
		||||
        <dependency.alfresco-jlan.version>7.2</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>
 | 
			
		||||
@@ -66,7 +64,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>20230227</dependency.org-json.version>
 | 
			
		||||
        <dependency.org-json.version>20220320</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>
 | 
			
		||||
@@ -77,9 +75,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.16</dependency.groovy.version>
 | 
			
		||||
        <dependency.groovy.version>3.0.12</dependency.groovy.version>
 | 
			
		||||
        <dependency.tika.version>2.4.1</dependency.tika.version>
 | 
			
		||||
        <dependency.spring-security.version>5.8.2</dependency.spring-security.version>
 | 
			
		||||
        <dependency.spring-security.version>5.7.5</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>
 | 
			
		||||
@@ -89,7 +87,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.4</dependency.activemq.version>
 | 
			
		||||
        <dependency.activemq.version>5.17.1</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>
 | 
			
		||||
@@ -109,7 +107,6 @@
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -123,7 +120,7 @@
 | 
			
		||||
        <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.61</dependency.tas-utility.version>
 | 
			
		||||
        <dependency.tas-utility.version>3.0.58</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>
 | 
			
		||||
@@ -151,7 +148,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.107</tag>
 | 
			
		||||
        <tag>HEAD</tag>
 | 
			
		||||
    </scm>
 | 
			
		||||
 | 
			
		||||
    <distributionManagement>
 | 
			
		||||
@@ -242,17 +239,6 @@
 | 
			
		||||
                <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>
 | 
			
		||||
@@ -398,7 +384,7 @@
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>commons-fileupload</groupId>
 | 
			
		||||
                <artifactId>commons-fileupload</artifactId>
 | 
			
		||||
                <version>1.5</version>
 | 
			
		||||
                <version>1.4</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <dependency>
 | 
			
		||||
@@ -913,13 +899,6 @@
 | 
			
		||||
                    </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>
 | 
			
		||||
 | 
			
		||||
@@ -937,7 +916,7 @@
 | 
			
		||||
                <plugin>
 | 
			
		||||
                    <groupId>io.fabric8</groupId>
 | 
			
		||||
                    <artifactId>docker-maven-plugin</artifactId>
 | 
			
		||||
                    <version>0.42.0</version>
 | 
			
		||||
                    <version>0.40.2</version>
 | 
			
		||||
                </plugin>
 | 
			
		||||
                <plugin>
 | 
			
		||||
                    <artifactId>maven-surefire-plugin</artifactId>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
 
 | 
			
		||||
@@ -25,16 +25,10 @@
 | 
			
		||||
 */
 | 
			
		||||
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;
 | 
			
		||||
@@ -57,9 +51,6 @@ import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationE
 | 
			
		||||
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;
 | 
			
		||||
@@ -77,8 +68,7 @@ import org.apache.commons.collections.CollectionUtils;
 | 
			
		||||
 */
 | 
			
		||||
public class TagsImpl implements Tags
 | 
			
		||||
{
 | 
			
		||||
	private static final String PARAM_INCLUDE_COUNT = "count";
 | 
			
		||||
	private static final String PARAM_WHERE_TAG = "tag";
 | 
			
		||||
	private static final Object PARAM_INCLUDE_COUNT = "count";
 | 
			
		||||
	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";
 | 
			
		||||
 | 
			
		||||
@@ -118,6 +108,7 @@ public class TagsImpl implements Tags
 | 
			
		||||
	        List<String> tagValues = new AbstractList<String>()
 | 
			
		||||
            {
 | 
			
		||||
                @Override
 | 
			
		||||
 | 
			
		||||
                public String get(int arg0)
 | 
			
		||||
                {
 | 
			
		||||
                	String tag = tags.get(arg0).getTag();
 | 
			
		||||
@@ -134,9 +125,14 @@ public class TagsImpl implements Tags
 | 
			
		||||
            {
 | 
			
		||||
		        List<Pair<String, NodeRef>> tagNodeRefs = taggingService.addTags(nodeRef, tagValues);
 | 
			
		||||
		        List<Tag> ret = new ArrayList<Tag>(tags.size());
 | 
			
		||||
				List<Pair<String, Integer>> tagsCountPairList = taggingService.findTaggedNodesAndCountByTagName(nodeRef.getStoreRef());
 | 
			
		||||
				Map<String, Integer> tagsCountMap = tagsCountPairList.stream().collect(Collectors.toMap(Pair::getFirst,Pair::getSecond));
 | 
			
		||||
		        for(Pair<String, NodeRef> pair : tagNodeRefs)
 | 
			
		||||
		        {
 | 
			
		||||
		        	ret.add(new Tag(pair.getSecond(), pair.getFirst()));
 | 
			
		||||
					Tag createdTag=new Tag(pair.getSecond(), pair.getFirst());
 | 
			
		||||
					// The new use of the tag will not be indexed yet, so add one to whatever count we get back
 | 
			
		||||
					createdTag.setCount(Optional.ofNullable(tagsCountMap.get(createdTag.getTag())).orElse(0) + 1);
 | 
			
		||||
		        	ret.add(createdTag);
 | 
			
		||||
		        }
 | 
			
		||||
		        return ret;
 | 
			
		||||
            }
 | 
			
		||||
@@ -164,18 +160,17 @@ public class TagsImpl implements Tags
 | 
			
		||||
		taggingService.deleteTag(storeRef, tagValue);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
    public CollectionWithPagingInfo<Tag> getTags(StoreRef storeRef, Parameters params)
 | 
			
		||||
    {
 | 
			
		||||
	    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));
 | 
			
		||||
 | 
			
		||||
        Paging paging = params.getPaging();
 | 
			
		||||
        PagingResults<Pair<NodeRef, String>> results = taggingService.getTags(storeRef, Util.getPagingRequest(paging));
 | 
			
		||||
        taggingService.getPagedTags(storeRef, 0, paging.getMaxItems());
 | 
			
		||||
        Integer totalItems = results.getTotalResultCount().getFirst();
 | 
			
		||||
        List<Pair<NodeRef, String>> page = results.getPage();
 | 
			
		||||
        List<Tag> tags = new ArrayList<>(page.size());
 | 
			
		||||
        List<Pair<String, Integer>> tagsByCount;
 | 
			
		||||
        Map<String, Integer> tagsByCountMap = new HashMap<>();
 | 
			
		||||
        List<Tag> tags = new ArrayList<Tag>(page.size());
 | 
			
		||||
        List<Pair<String, Integer>> tagsByCount = null;
 | 
			
		||||
        Map<String, Integer> tagsByCountMap = new HashMap<String, Integer>();
 | 
			
		||||
 | 
			
		||||
        if (params.getInclude().contains(PARAM_INCLUDE_COUNT))
 | 
			
		||||
        {
 | 
			
		||||
            tagsByCount = taggingService.findTaggedNodesAndCountByTagName(storeRef);
 | 
			
		||||
@@ -194,7 +189,7 @@ public class TagsImpl implements Tags
 | 
			
		||||
            tags.add(selectedTag);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), totalItems);
 | 
			
		||||
        return CollectionWithPagingInfo.asPaged(paging, tags, results.hasMoreItems(), (totalItems == null ? null : totalItems.intValue()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public NodeRef validateTag(String tagId)
 | 
			
		||||
@@ -290,6 +285,7 @@ public class TagsImpl implements Tags
 | 
			
		||||
			.peek(tag -> {
 | 
			
		||||
				if (parameters.getInclude().contains(PARAM_INCLUDE_COUNT))
 | 
			
		||||
				{
 | 
			
		||||
					// this method creates orphan tags, which are not related with any content so setting count to 0.
 | 
			
		||||
					tag.setCount(0);
 | 
			
		||||
				}
 | 
			
		||||
			}).collect(Collectors.toList());
 | 
			
		||||
@@ -302,38 +298,4 @@ public class TagsImpl implements Tags
 | 
			
		||||
			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))));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -100,21 +100,51 @@ public class Tag implements Comparable<Tag>
 | 
			
		||||
		return ret;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean equals(Object o)
 | 
			
		||||
	{
 | 
			
		||||
		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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int hashCode()
 | 
			
		||||
	{
 | 
			
		||||
		return Objects.hash(nodeRef, tag, count);
 | 
			
		||||
		final int prime = 31;
 | 
			
		||||
		int result = 1;
 | 
			
		||||
		result = prime * result + ((nodeRef == null) ? 0 : nodeRef.hashCode());
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
	 * Tags are equal if they have the same NodeRef
 | 
			
		||||
	 *
 | 
			
		||||
	 * (non-Javadoc)
 | 
			
		||||
	 * @see java.lang.Object#equals(java.lang.Object)
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean equals(Object obj)
 | 
			
		||||
	{
 | 
			
		||||
		if (this == obj)
 | 
			
		||||
			return true;
 | 
			
		||||
		if (obj == null)
 | 
			
		||||
			return false;
 | 
			
		||||
		if (getClass() != obj.getClass())
 | 
			
		||||
			return false;
 | 
			
		||||
		Tag other = (Tag) obj;
 | 
			
		||||
		if (nodeRef == null) {
 | 
			
		||||
			if (other.nodeRef != null)
 | 
			
		||||
				return false;
 | 
			
		||||
		} else if (!nodeRef.equals(other.nodeRef))
 | 
			
		||||
			return false;
 | 
			
		||||
		if(tag==null)
 | 
			
		||||
		{
 | 
			
		||||
			if(other.tag != null)
 | 
			
		||||
				return false;
 | 
			
		||||
		}
 | 
			
		||||
		else if (!tag.equals(other.tag))
 | 
			
		||||
			return false;
 | 
			
		||||
		if (count == null)
 | 
			
		||||
		{
 | 
			
		||||
			if (other.count != null)
 | 
			
		||||
				return false;
 | 
			
		||||
		}
 | 
			
		||||
		else if (!count.equals(other.count))
 | 
			
		||||
			return false;
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
 
 | 
			
		||||
@@ -59,8 +59,9 @@ public class TagsEntityResource implements EntityResourceAction.Read<Tag>,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 
 | 
			
		||||
	 * 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.")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,259 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 * #%L
 | 
			
		||||
 * Alfresco Remote API
 | 
			
		||||
 * %%
 | 
			
		||||
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 | 
			
		||||
 * Copyright (C) 2005 - 2016 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,19 +25,10 @@
 | 
			
		||||
 */
 | 
			
		||||
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;
 | 
			
		||||
@@ -54,19 +45,14 @@ public abstract class QueryHelper
 | 
			
		||||
    /**
 | 
			
		||||
     * An interface used when walking a query tree.  Calls are made to methods when the particular clause is encountered.
 | 
			
		||||
     */
 | 
			
		||||
    public interface WalkerCallback
 | 
			
		||||
    public static 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
 | 
			
		||||
         */
 | 
			
		||||
        default void exists(String propertyName, boolean negated)
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
        void exists(String propertyName, boolean negated);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Called any time a BETWEEN clause is encountered.
 | 
			
		||||
@@ -75,18 +61,12 @@ public abstract class QueryHelper
 | 
			
		||||
         * @param secondValue String
 | 
			
		||||
         * @param negated returns true if "NOT BETWEEN" was used
 | 
			
		||||
         */
 | 
			
		||||
        default void between(String propertyName, String firstValue, String secondValue, boolean negated)
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
        void between(String propertyName, String firstValue, String secondValue, boolean negated);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * One of EQUALS LESSTHAN GREATERTHAN LESSTHANOREQUALS GREATERTHANOREQUALS;
 | 
			
		||||
         */
 | 
			
		||||
        default void comparison(int type, String propertyName, String propertyValue, boolean negated)
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
        void comparison(int type, String propertyName, String propertyValue, boolean negated);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Called any time an IN clause is encountered.
 | 
			
		||||
@@ -94,10 +74,7 @@ public abstract class QueryHelper
 | 
			
		||||
         * @param negated returns true if "NOT IN" was used
 | 
			
		||||
         * @param propertyValues the property values
 | 
			
		||||
         */
 | 
			
		||||
        default void in(String property, boolean negated, String... propertyValues)
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
        void in(String property, boolean negated, String... propertyValues);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Called any time a MATCHES clause is encountered.
 | 
			
		||||
@@ -105,37 +82,42 @@ public abstract class QueryHelper
 | 
			
		||||
         * @param propertyValue String
 | 
			
		||||
         * @param negated returns true if "NOT MATCHES" was used
 | 
			
		||||
         */
 | 
			
		||||
        default void matches(String property, String propertyValue, boolean negated)
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
        void matches(String property, String propertyValue, boolean negated);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Called any time an AND is encountered.
 | 
			
		||||
         */
 | 
			
		||||
        default void and()
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
         */  
 | 
			
		||||
        void and();
 | 
			
		||||
        /**
 | 
			
		||||
         * Called any time an OR is encountered.
 | 
			
		||||
         */
 | 
			
		||||
        default void or()
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default Collection<String> getProperty(String propertyName, int type, boolean negated)
 | 
			
		||||
        {
 | 
			
		||||
            throw UNSUPPORTED;
 | 
			
		||||
        }
 | 
			
		||||
         */  
 | 
			
		||||
        void or();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 {}
 | 
			
		||||
    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;}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Walks a query with a callback for each operation
 | 
			
		||||
@@ -164,7 +146,7 @@ public abstract class QueryHelper
 | 
			
		||||
        if (tree != null)
 | 
			
		||||
        {
 | 
			
		||||
            switch (tree.getType()) {
 | 
			
		||||
            case EXISTS:
 | 
			
		||||
            case WhereClauseParser.EXISTS:
 | 
			
		||||
                if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
 | 
			
		||||
                {
 | 
			
		||||
                    callback.exists(tree.getChild(0).getText(), negated);
 | 
			
		||||
@@ -178,7 +160,7 @@ public abstract class QueryHelper
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case IN:
 | 
			
		||||
            case WhereClauseParser.IN:
 | 
			
		||||
                if (WhereClauseParser.PROPERTYNAME == tree.getChild(0).getType())
 | 
			
		||||
                {
 | 
			
		||||
                    List<Tree> children = getChildren(tree);
 | 
			
		||||
@@ -192,14 +174,14 @@ public abstract class QueryHelper
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case BETWEEN:
 | 
			
		||||
            case WhereClauseParser.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 EQUALS: //fall through (comparison)
 | 
			
		||||
            case WhereClauseParser.EQUALS: //fall through (comparison)
 | 
			
		||||
            case WhereClauseParser.LESSTHAN: //fall through (comparison)
 | 
			
		||||
            case WhereClauseParser.GREATERTHAN: //fall through (comparison)
 | 
			
		||||
            case WhereClauseParser.LESSTHANOREQUALS: //fall through (comparison)
 | 
			
		||||
@@ -304,180 +286,4 @@ 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;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,351 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,33 +1,32 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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%
 | 
			
		||||
 */
 | 
			
		||||
/*
 | 
			
		||||
 * #%L
 | 
			
		||||
 * Alfresco Remote API
 | 
			
		||||
 * %%
 | 
			
		||||
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 | 
			
		||||
 * %%
 | 
			
		||||
 * This file is part of the Alfresco software. 
 | 
			
		||||
 * If the software was purchased under a paid Alfresco license, the terms of 
 | 
			
		||||
 * the paid license agreement will prevail.  Otherwise, the software is 
 | 
			
		||||
 * provided under the following open source license terms:
 | 
			
		||||
 * 
 | 
			
		||||
 * Alfresco is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Lesser General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 * 
 | 
			
		||||
 * Alfresco is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Lesser General 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;
 | 
			
		||||
@@ -52,7 +51,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
 | 
			
		||||
 */
 | 
			
		||||
@@ -73,21 +72,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)
 | 
			
		||||
@@ -133,7 +132,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            lessThanOrEqualProperties = new HashMap<String, String>();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    public void enableVariablesSupport(NamespaceService namespaceService, DictionaryService dictionaryService)
 | 
			
		||||
    {
 | 
			
		||||
        variablesEnabled = true;
 | 
			
		||||
@@ -149,7 +148,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
        this.dictionaryService = dictionaryService;
 | 
			
		||||
        variableProperties = new ArrayList<QueryVariableHolder>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    public List<QueryVariableHolder> getVariableProperties() {
 | 
			
		||||
        return variableProperties;
 | 
			
		||||
    }
 | 
			
		||||
@@ -159,9 +158,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);
 | 
			
		||||
        }
 | 
			
		||||
@@ -171,19 +170,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)
 | 
			
		||||
        {
 | 
			
		||||
@@ -193,7 +192,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == WhereClauseParser.MATCHES)
 | 
			
		||||
@@ -204,7 +203,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == WhereClauseParser.GREATERTHAN)
 | 
			
		||||
@@ -215,7 +214,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == WhereClauseParser.GREATERTHANOREQUALS)
 | 
			
		||||
@@ -226,7 +225,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == WhereClauseParser.LESSTHAN)
 | 
			
		||||
@@ -237,7 +236,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type == WhereClauseParser.LESSTHANOREQUALS)
 | 
			
		||||
@@ -248,7 +247,7 @@ public class MapBasedQueryWalker extends WalkerCallbackAdapter
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
                throwError = !handleUnmatchedComparison(type, propertyName, propertyValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
@@ -256,24 +255,15 @@ 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]});
 | 
			
		||||
        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."); 
 | 
			
		||||
        }
 | 
			
		||||
        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)
 | 
			
		||||
@@ -310,7 +300,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
 | 
			
		||||
@@ -344,7 +334,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());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -355,7 +345,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/", "");
 | 
			
		||||
@@ -363,25 +353,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
 | 
			
		||||
@@ -396,7 +386,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)
 | 
			
		||||
@@ -406,18 +396,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.
 | 
			
		||||
@@ -426,25 +416,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,40 +27,32 @@ 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.Node;
 | 
			
		||||
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.alfresco.util.TypeConstraint;
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.runner.RunWith;
 | 
			
		||||
@@ -72,23 +64,22 @@ import org.mockito.junit.MockitoJUnitRunner;
 | 
			
		||||
public class TagsImplTest
 | 
			
		||||
{
 | 
			
		||||
    private static final String TAG_ID = "tag-node-id";
 | 
			
		||||
    private static final String NODE_ID = "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() {};
 | 
			
		||||
    private static final NodeRef TAG_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private Nodes nodesMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private Node createdNodeMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private AuthorityService authorityServiceMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private TaggingService taggingServiceMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private Parameters parametersMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private Paging pagingMock;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private PagingResults<Pair<NodeRef, String>> pagingResultsMock;
 | 
			
		||||
    private TypeConstraint typeConstraint;
 | 
			
		||||
 | 
			
		||||
    @InjectMocks
 | 
			
		||||
    private TagsImpl objectUnderTest;
 | 
			
		||||
@@ -97,145 +88,41 @@ public class TagsImplTest
 | 
			
		||||
    public void setup()
 | 
			
		||||
    {
 | 
			
		||||
        given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
 | 
			
		||||
        given(nodesMock.validateNode(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID)).willReturn(TAG_NODE_REF);
 | 
			
		||||
        given(nodesMock.validateNode(NODE_ID)).willReturn(TAG_NODE_REF);
 | 
			
		||||
        given(nodesMock.validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID)).willReturn(TAG_NODE_REF);
 | 
			
		||||
        given(nodesMock.getNode(any())).willReturn(createdNodeMock);
 | 
			
		||||
        given(createdNodeMock.getNodeId()).willReturn(NODE_ID);
 | 
			
		||||
        given(typeConstraint.matches(any())).willReturn(true);
 | 
			
		||||
        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)));
 | 
			
		||||
    public void testGetTags() {
 | 
			
		||||
        final List<String> tagNames = List.of("testTag","tag11");
 | 
			
		||||
        final List<Tag> tagsToCreate = createTags(tagNames);
 | 
			
		||||
        given(taggingServiceMock.createTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
 | 
			
		||||
        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()
 | 
			
		||||
        final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
 | 
			
		||||
        final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).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);
 | 
			
		||||
        assertEquals(expectedTags, actualCreatedTags);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testDeleteTagById()
 | 
			
		||||
    {
 | 
			
		||||
        //when
 | 
			
		||||
        objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID);
 | 
			
		||||
        objectUnderTest.deleteTagById(StoreRef.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).should().validateNode(StoreRef.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).should().deleteTag(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_NAME);
 | 
			
		||||
        then(taggingServiceMock).shouldHaveNoMoreInteractions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -245,7 +132,7 @@ public class TagsImplTest
 | 
			
		||||
        given(authorityServiceMock.hasAdminAuthority()).willReturn(false);
 | 
			
		||||
 | 
			
		||||
        //when
 | 
			
		||||
        assertThrows(PermissionDeniedException.class, () -> objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID));
 | 
			
		||||
        assertThrows(PermissionDeniedException.class, () -> objectUnderTest.deleteTagById(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID));
 | 
			
		||||
 | 
			
		||||
        then(authorityServiceMock).should().hasAdminAuthority();
 | 
			
		||||
        then(authorityServiceMock).shouldHaveNoMoreInteractions();
 | 
			
		||||
@@ -259,12 +146,12 @@ public class TagsImplTest
 | 
			
		||||
    public void testDeleteTagById_nonExistentTag()
 | 
			
		||||
    {
 | 
			
		||||
        //when
 | 
			
		||||
        assertThrows(EntityNotFoundException.class, () -> objectUnderTest.deleteTagById(STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id"));
 | 
			
		||||
        assertThrows(EntityNotFoundException.class, () -> objectUnderTest.deleteTagById(StoreRef.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).should().validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "dummy-id");
 | 
			
		||||
        then(nodesMock).shouldHaveNoMoreInteractions();
 | 
			
		||||
 | 
			
		||||
        then(taggingServiceMock).shouldHaveNoInteractions();
 | 
			
		||||
@@ -282,11 +169,11 @@ public class TagsImplTest
 | 
			
		||||
 | 
			
		||||
        then(authorityServiceMock).should().hasAdminAuthority();
 | 
			
		||||
        then(authorityServiceMock).shouldHaveNoMoreInteractions();
 | 
			
		||||
        then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, tagNames);
 | 
			
		||||
        then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, tagNames);
 | 
			
		||||
        then(taggingServiceMock).shouldHaveNoMoreInteractions();
 | 
			
		||||
        final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames);
 | 
			
		||||
        assertThat(actualCreatedTags)
 | 
			
		||||
            .isNotNull().usingRecursiveComparison()
 | 
			
		||||
            .isNotNull()
 | 
			
		||||
            .isEqualTo(expectedTags);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -350,7 +237,7 @@ public class TagsImplTest
 | 
			
		||||
        //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).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
 | 
			
		||||
        then(taggingServiceMock).shouldHaveNoMoreInteractions();
 | 
			
		||||
        assertThat(actualException).isInstanceOf(DuplicateChildNodeNameException.class);
 | 
			
		||||
    }
 | 
			
		||||
@@ -365,7 +252,7 @@ public class TagsImplTest
 | 
			
		||||
        //when
 | 
			
		||||
        final List<Tag> actualCreatedTags = objectUnderTest.createTags(tagsToCreate, parametersMock);
 | 
			
		||||
 | 
			
		||||
        then(taggingServiceMock).should().createTags(STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
 | 
			
		||||
        then(taggingServiceMock).should().createTags(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, List.of(TAG_NAME));
 | 
			
		||||
        final List<Tag> expectedTags = List.of(createTagWithNodeRef(TAG_NAME));
 | 
			
		||||
        assertThat(actualCreatedTags)
 | 
			
		||||
            .isNotNull()
 | 
			
		||||
@@ -394,7 +281,7 @@ public class TagsImplTest
 | 
			
		||||
    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))))
 | 
			
		||||
            .map(tagName -> createPair(tagName, new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName))))
 | 
			
		||||
            .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -423,8 +310,37 @@ public class TagsImplTest
 | 
			
		||||
    private static Tag createTagWithNodeRef(final String tagName)
 | 
			
		||||
    {
 | 
			
		||||
        return Tag.builder()
 | 
			
		||||
            .nodeRef(new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName)))
 | 
			
		||||
            .nodeRef(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, TAG_ID.concat("-").concat(tagName)))
 | 
			
		||||
            .tag(tagName)
 | 
			
		||||
            .create();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testAddTagsToNode()
 | 
			
		||||
    {
 | 
			
		||||
        final List<String> tagNames = List.of("tag1","tag2");
 | 
			
		||||
        final List<Tag> tagsToCreate = createTags(tagNames);
 | 
			
		||||
        given(taggingServiceMock.addTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
 | 
			
		||||
        final List<Tag> actualCreatedTags = objectUnderTest.addTags(nodesMock.getNode(any()).getNodeId(),tagsToCreate);
 | 
			
		||||
        then(taggingServiceMock).should().addTags(TAG_NODE_REF, tagNames);
 | 
			
		||||
        final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).stream().peek(tag -> tag.setCount(1)).collect(Collectors.toList());;
 | 
			
		||||
        assertThat(actualCreatedTags)
 | 
			
		||||
                .isNotNull()
 | 
			
		||||
                .isEqualTo(expectedTags);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testAddTagsToNodeWithResponseNotIndexed()
 | 
			
		||||
    {
 | 
			
		||||
        final List<String> tagNames = List.of("tag1","tag2");
 | 
			
		||||
        final List<Tag> tagsToCreate = createTags(tagNames);
 | 
			
		||||
        given(taggingServiceMock.addTags(any(), any())).willAnswer(invocation -> createTagAndNodeRefPairs(invocation.getArgument(1)));
 | 
			
		||||
        final List<Tag> actualCreatedTags = objectUnderTest.addTags(nodesMock.getNode(any()).getNodeId(),tagsToCreate);
 | 
			
		||||
        then(taggingServiceMock).should().addTags(TAG_NODE_REF, tagNames);
 | 
			
		||||
        final List<Tag> expectedTags = createTagsWithNodeRefs(tagNames).stream().peek(tag -> tag.setCount(1)).collect(Collectors.toList());;
 | 
			
		||||
        assertThat(actualCreatedTags)
 | 
			
		||||
                .isNotNull()
 | 
			
		||||
                .isEqualTo(expectedTags);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,666 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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*");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
    <parent>
 | 
			
		||||
        <groupId>org.alfresco</groupId>
 | 
			
		||||
        <artifactId>alfresco-community-repo</artifactId>
 | 
			
		||||
        <version>20.107</version>
 | 
			
		||||
        <version>20.90-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
@@ -119,10 +119,6 @@
 | 
			
		||||
            <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>
 | 
			
		||||
@@ -391,11 +387,14 @@
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.security</groupId>
 | 
			
		||||
            <artifactId>spring-security-crypto</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.springframework.security</groupId>
 | 
			
		||||
            <artifactId>spring-security-oauth2-client</artifactId>
 | 
			
		||||
            <artifactId>spring-security-core</artifactId>
 | 
			
		||||
            <version>${dependency.spring-security.version}</version>
 | 
			
		||||
            <exclusions>
 | 
			
		||||
                <exclusion>
 | 
			
		||||
                    <groupId>org.springframework</groupId>
 | 
			
		||||
                    <artifactId>spring-expression</artifactId>
 | 
			
		||||
                </exclusion>
 | 
			
		||||
            </exclusions>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.quartz-scheduler</groupId>
 | 
			
		||||
@@ -558,6 +557,17 @@
 | 
			
		||||
        </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>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 * #%L
 | 
			
		||||
 * Alfresco Repository
 | 
			
		||||
 * %%
 | 
			
		||||
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -25,35 +25,38 @@
 | 
			
		||||
 */
 | 
			
		||||
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 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.
 | 
			
		||||
 * 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.
 | 
			
		||||
 * <br>
 | 
			
		||||
 * The {@link IdentityServiceAuthenticationComponent#oAuth2Client} can be null in which case this authenticator will
 | 
			
		||||
 * just fall through to the next one in the chain.
 | 
			
		||||
 * The AuthzClient 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 Authorization Server **/
 | 
			
		||||
    private OAuth2Client oAuth2Client;
 | 
			
		||||
    private final Log logger = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
 | 
			
		||||
    /** client used to authenticate user credentials against Keycloak **/
 | 
			
		||||
    private AuthzClient authzClient;
 | 
			
		||||
    /** enabled flag for the identity service subsystem**/
 | 
			
		||||
    private boolean active;
 | 
			
		||||
    private boolean allowGuestLogin;
 | 
			
		||||
 | 
			
		||||
    public void setOAuth2Client(OAuth2Client oAuth2Client)
 | 
			
		||||
    public void setAuthenticatorAuthzClient(AuthzClient authenticatorAuthzClient)
 | 
			
		||||
    {
 | 
			
		||||
        this.oAuth2Client = oAuth2Client;
 | 
			
		||||
        this.authzClient = authenticatorAuthzClient;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setAllowGuestLogin(boolean allowGuestLogin)
 | 
			
		||||
@@ -64,31 +67,49 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
 | 
			
		||||
    public void authenticateImpl(String userName, char[] password) throws AuthenticationException
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (oAuth2Client == null)
 | 
			
		||||
        if (authzClient == null)
 | 
			
		||||
        {
 | 
			
		||||
            if (LOGGER.isDebugEnabled())
 | 
			
		||||
            if (logger.isDebugEnabled())
 | 
			
		||||
            {
 | 
			
		||||
                LOGGER.debug("OAuth2Client was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
 | 
			
		||||
                logger.debug("AuthzClient was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property. ");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw new AuthenticationException("User not authenticated because OAuth2Client was not set.");
 | 
			
		||||
            throw new AuthenticationException("User not authenticated because AuthzClient was not set.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // Attempt to verify user credentials
 | 
			
		||||
            oAuth2Client.verifyCredentials(userName, new String(password));
 | 
			
		||||
            // Attempt to get an access token using the user credentials
 | 
			
		||||
            authzClient.obtainAccessToken(userName, new String(password));
 | 
			
		||||
 | 
			
		||||
            // Verification was successful so treat as authenticated user
 | 
			
		||||
            // Successfully obtained access token so treat as authenticated user
 | 
			
		||||
            setCurrentUser(userName);
 | 
			
		||||
        }
 | 
			
		||||
        catch (CredentialsVerificationException e)
 | 
			
		||||
        catch (HttpResponseException e)
 | 
			
		||||
        {
 | 
			
		||||
            throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", 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);
 | 
			
		||||
        }
 | 
			
		||||
        catch (RuntimeException e)
 | 
			
		||||
        {
 | 
			
		||||
            throw new AuthenticationException("Failed to verify user credentials.", 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -108,32 +129,4 @@ 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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 * #%L
 | 
			
		||||
 * Alfresco Repository
 | 
			
		||||
 * %%
 | 
			
		||||
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 | 
			
		||||
 * Copyright (C) 2005 - 2016 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.security.authentication.identityservice;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.Properties;
 | 
			
		||||
import java.util.TreeMap;
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +33,6 @@ 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.
 | 
			
		||||
@@ -43,9 +41,8 @@ import org.springframework.web.util.UriComponentsBuilder;
 | 
			
		||||
 */
 | 
			
		||||
public class IdentityServiceConfig extends AdapterConfig implements InitializingBean
 | 
			
		||||
{
 | 
			
		||||
    private static final Log LOGGER = LogFactory.getLog(IdentityServiceConfig.class);
 | 
			
		||||
    private static final String REALMS = "realms";
 | 
			
		||||
    private static final String SECRET = "secret";
 | 
			
		||||
    private static Log logger = LogFactory.getLog(IdentityServiceConfig.class);
 | 
			
		||||
    
 | 
			
		||||
    private static final String CREDENTIALS_SECRET = "identity-service.credentials.secret";
 | 
			
		||||
    private static final String CREDENTIALS_PROVIDER = "identity-service.credentials.provider";
 | 
			
		||||
    
 | 
			
		||||
@@ -98,13 +95,13 @@ public class IdentityServiceConfig extends AdapterConfig implements Initializing
 | 
			
		||||
    @Override
 | 
			
		||||
    public void afterPropertiesSet() throws Exception
 | 
			
		||||
    {
 | 
			
		||||
        // programmatically build the more complex objects i.e. credentials
 | 
			
		||||
        // programatically 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);
 | 
			
		||||
@@ -119,27 +116,10 @@ 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("");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,270 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +42,6 @@ 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.model.ContentModel;
 | 
			
		||||
@@ -915,23 +914,51 @@ 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, Collection<String> exactNamesFilter, Collection<String> alikeNamesFilter)
 | 
			
		||||
    public PagingResults<Pair<NodeRef, String>> getTags(StoreRef storeRef, PagingRequest pagingRequest)
 | 
			
		||||
    {
 | 
			
		||||
        ParameterCheck.mandatory("storeRef", storeRef);
 | 
			
		||||
 | 
			
		||||
        PagingResults<ChildAssociationRef> rootCategories = categoryService.getRootCategories(storeRef, ContentModel.ASPECT_TAGGABLE, pagingRequest, true,
 | 
			
		||||
            exactNamesFilter, alikeNamesFilter);
 | 
			
		||||
    	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;
 | 
			
		||||
 | 
			
		||||
        return mapPagingResult(rootCategories,
 | 
			
		||||
            (childAssociation) -> new Pair<>(childAssociation.getChildRef(), childAssociation.getQName().getLocalName()));
 | 
			
		||||
        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;
 | 
			
		||||
        	}
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
@@ -1573,36 +1600,4 @@ public class TaggingServiceImpl implements TaggingService,
 | 
			
		||||
            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();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,140 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -30,7 +30,6 @@ 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;
 | 
			
		||||
@@ -137,24 +136,6 @@ 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
 | 
			
		||||
     * 
 | 
			
		||||
 
 | 
			
		||||
@@ -25,12 +25,10 @@
 | 
			
		||||
 */
 | 
			
		||||
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.query.EmptyPagingResults;
 | 
			
		||||
import org.alfresco.api.AlfrescoPublicApi; 
 | 
			
		||||
import org.alfresco.query.PagingRequest;
 | 
			
		||||
import org.alfresco.query.PagingResults;
 | 
			
		||||
import org.alfresco.service.Auditable;
 | 
			
		||||
@@ -77,21 +75,6 @@ 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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -813,6 +813,13 @@ 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
 | 
			
		||||
#
 | 
			
		||||
@@ -1149,7 +1156,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=bcrypt10
 | 
			
		||||
system.preferred.password.encoding=md4
 | 
			
		||||
 | 
			
		||||
# Upgrade Password Hash Job
 | 
			
		||||
system.upgradePasswordHash.jobBatchSize=100
 | 
			
		||||
 
 | 
			
		||||
@@ -21,12 +21,12 @@
 | 
			
		||||
      <property name="allowGuestLogin">
 | 
			
		||||
         <value>${identity-service.authentication.allowGuestLogin}</value>
 | 
			
		||||
      </property>
 | 
			
		||||
      <property name="oAuth2Client">
 | 
			
		||||
         <ref bean="oAuth2Client"/>
 | 
			
		||||
      <property name="authenticatorAuthzClient">
 | 
			
		||||
         <ref bean="authenticatorAuthzClient"/>
 | 
			
		||||
      </property>
 | 
			
		||||
   </bean>
 | 
			
		||||
 | 
			
		||||
   <bean name="oAuth2Client" class="org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean">
 | 
			
		||||
   <bean name="authenticatorAuthzClient" class="org.alfresco.repo.security.authentication.identityservice.AuthenticatorAuthzClientFactoryBean">
 | 
			
		||||
      <property name="identityServiceConfig">
 | 
			
		||||
         <ref bean="identityServiceConfig" />
 | 
			
		||||
      </property>
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,7 @@ 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,
 | 
			
		||||
@@ -136,7 +137,6 @@ 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,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,15 @@
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
@@ -45,6 +48,7 @@ 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;
 | 
			
		||||
@@ -515,48 +519,50 @@ public class AuthenticationTest extends TestCase
 | 
			
		||||
        assertTrue("The user should exist", dao.userExists(userName));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void testCreateAndyUserAndUpdatePassword()
 | 
			
		||||
    public void testCreateAndyUserAndOtherCRUD() throws NoSuchAlgorithmException, UnsupportedEncodingException
 | 
			
		||||
    {
 | 
			
		||||
        RepositoryAuthenticationDao dao = createRepositoryAuthenticationDao();
 | 
			
		||||
        
 | 
			
		||||
        dao.createUser("Andy", "cabbage".toCharArray());
 | 
			
		||||
        assertNotNull(dao.getUserOrNull("Andy"));
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        // Object oldSalt = dao.getSalt(AndyDetails);
 | 
			
		||||
        dao.updateUser("Andy", "carrot".toCharArray());
 | 
			
		||||
        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);
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        // 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));
 | 
			
		||||
        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()));
 | 
			
		||||
 | 
			
		||||
        dao.deleteUser("Andy");
 | 
			
		||||
        assertFalse("Should not be a cache entry for 'Andy'.", authenticationCache.contains("Andy"));
 | 
			
		||||
@@ -1983,142 +1989,131 @@ 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()
 | 
			
		||||
    public void testRehashedPasswordOnAuthentication() throws Exception
 | 
			
		||||
    {
 | 
			
		||||
        // 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,
 | 
			
		||||
        // 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, 
 | 
			
		||||
                    preferredEncoding, hashEncoding);
 | 
			
		||||
 | 
			
		||||
            // delete the user and clear the security context
 | 
			
		||||
            this.deleteAndy();
 | 
			
		||||
            authenticationComponent.clearCurrentSecurityContext();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception e)
 | 
			
		||||
        {
 | 
			
		||||
            throw new RuntimeException(e);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            compositePasswordEncoder.setPreferredEncoding(defaultPreferredEncoding);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // delete the user and clear the security context
 | 
			
		||||
        this.deleteAndy();
 | 
			
		||||
        authenticationComponent.clearCurrentSecurityContext();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Test password encoding with MD4 without a salt.
 | 
			
		||||
     * For on premise the default is MD4, for cloud BCRYPT10
 | 
			
		||||
     *
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    public void testGetsMD4Password()
 | 
			
		||||
    public void testDefaultEncodingIsMD4() throws Exception
 | 
			
		||||
    {
 | 
			
		||||
        String defaultPreferredEncoding = compositePasswordEncoder.getPreferredEncoding();
 | 
			
		||||
        compositePasswordEncoder.setPreferredEncoding("md4");
 | 
			
		||||
        assertNotNull(compositePasswordEncoder);
 | 
			
		||||
        assertEquals("md4", compositePasswordEncoder.getPreferredEncoding());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        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));
 | 
			
		||||
    /**
 | 
			
		||||
     * 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));
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
        }
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 * #%L
 | 
			
		||||
 * Alfresco Repository
 | 
			
		||||
 * %%
 | 
			
		||||
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -25,17 +25,14 @@
 | 
			
		||||
 */
 | 
			
		||||
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;
 | 
			
		||||
@@ -44,6 +41,9 @@ 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 OAuth2Client mockOAuth2Client;
 | 
			
		||||
    private AuthzClient mockAuthzClient;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp()
 | 
			
		||||
@@ -76,8 +76,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
 | 
			
		||||
        authComponent.setNodeService(nodeService);
 | 
			
		||||
        authComponent.setPersonService(personService);
 | 
			
		||||
 | 
			
		||||
        mockOAuth2Client = mock(OAuth2Client.class);
 | 
			
		||||
        authComponent.setOAuth2Client(mockOAuth2Client);
 | 
			
		||||
        mockAuthzClient = mock(AuthzClient.class);
 | 
			
		||||
        authComponent.setAuthenticatorAuthzClient(mockAuthzClient);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After
 | 
			
		||||
@@ -89,9 +89,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
 | 
			
		||||
    @Test (expected=AuthenticationException.class)
 | 
			
		||||
    public void testAuthenticationFail()
 | 
			
		||||
    {
 | 
			
		||||
        doThrow(new CredentialsVerificationException("Failed"))
 | 
			
		||||
                .when(mockOAuth2Client)
 | 
			
		||||
                .verifyCredentials("username", "password");
 | 
			
		||||
        when(mockAuthzClient.obtainAccessToken("username", "password"))
 | 
			
		||||
                .thenThrow(new HttpResponseException("Unauthorized", 401, "Unauthorized", null));
 | 
			
		||||
 | 
			
		||||
        authComponent.authenticateImpl("username", "password".toCharArray());
 | 
			
		||||
    }
 | 
			
		||||
@@ -99,9 +98,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
 | 
			
		||||
    @Test(expected = AuthenticationException.class)
 | 
			
		||||
    public void testAuthenticationFail_connectionException()
 | 
			
		||||
    {
 | 
			
		||||
        doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
 | 
			
		||||
                .when(mockOAuth2Client)
 | 
			
		||||
                .verifyCredentials("username", "password");
 | 
			
		||||
        when(mockAuthzClient.obtainAccessToken("username", "password")).thenThrow(
 | 
			
		||||
                    new RuntimeException("Couldn't connect to server", new ConnectException("ConnectionRefused")));
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -118,9 +116,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
 | 
			
		||||
    @Test (expected=AuthenticationException.class)
 | 
			
		||||
    public void testAuthenticationFail_otherException()
 | 
			
		||||
    {
 | 
			
		||||
        doThrow(new RuntimeException("Some other errors!"))
 | 
			
		||||
                .when(mockOAuth2Client)
 | 
			
		||||
                .verifyCredentials("username", "password");
 | 
			
		||||
        when(mockAuthzClient.obtainAccessToken("username", "password"))
 | 
			
		||||
                    .thenThrow(new RuntimeException("Some other errors!"));
 | 
			
		||||
 | 
			
		||||
        authComponent.authenticateImpl("username", "password".toCharArray());
 | 
			
		||||
    }
 | 
			
		||||
@@ -128,7 +125,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testAuthenticationPass()
 | 
			
		||||
    {
 | 
			
		||||
        doNothing().when(mockOAuth2Client).verifyCredentials("username", "password");
 | 
			
		||||
        when(mockAuthzClient.obtainAccessToken("username", "password"))
 | 
			
		||||
                .thenReturn(new AccessTokenResponse());
 | 
			
		||||
 | 
			
		||||
        authComponent.authenticateImpl("username", "password".toCharArray());
 | 
			
		||||
 | 
			
		||||
@@ -137,9 +135,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test (expected= AuthenticationException.class)
 | 
			
		||||
    public void testFallthroughWhenOAuth2ClientIsNull()
 | 
			
		||||
    public void testFallthroughWhenAuthzClientIsNull()
 | 
			
		||||
    {
 | 
			
		||||
        authComponent.setOAuth2Client(null);
 | 
			
		||||
        authComponent.setAuthenticatorAuthzClient(null);
 | 
			
		||||
        authComponent.authenticateImpl("username", "password".toCharArray());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,124 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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 static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.argThat;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
import static org.mockito.Mockito.verifyNoInteractions;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
 | 
			
		||||
import org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean.SpringOAuth2Client;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
 | 
			
		||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
 | 
			
		||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
 | 
			
		||||
 | 
			
		||||
public class SpringOAuth2ClientUnitTest
 | 
			
		||||
{
 | 
			
		||||
    private static final String USER_NAME = "user";
 | 
			
		||||
    private static final String PASSWORD = "password";
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void shouldRecoverFromInitialAuthorizationServerUnavailability()
 | 
			
		||||
    {
 | 
			
		||||
        final OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
 | 
			
		||||
        when(authorizedClient.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
 | 
			
		||||
        final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
 | 
			
		||||
        when(authClientManager.authorize(any())).thenReturn(authorizedClient);
 | 
			
		||||
 | 
			
		||||
        final SpringOAuth2Client client = new SpringOAuth2Client(faultySupplier(3, authClientManager));
 | 
			
		||||
 | 
			
		||||
        assertThatExceptionOfType(CredentialsVerificationException.class)
 | 
			
		||||
                .isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
 | 
			
		||||
                .havingCause().withNoCause().withMessage("Expected failure #1");
 | 
			
		||||
        verifyNoInteractions(authClientManager);
 | 
			
		||||
 | 
			
		||||
        assertThatExceptionOfType(CredentialsVerificationException.class)
 | 
			
		||||
                .isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
 | 
			
		||||
                .havingCause().withNoCause().withMessage("Expected failure #2");
 | 
			
		||||
        verifyNoInteractions(authClientManager);
 | 
			
		||||
 | 
			
		||||
        assertThatExceptionOfType(CredentialsVerificationException.class)
 | 
			
		||||
                .isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
 | 
			
		||||
                .havingCause().withNoCause().withMessage("Expected failure #3");
 | 
			
		||||
        verifyNoInteractions(authClientManager);
 | 
			
		||||
 | 
			
		||||
        client.verifyCredentials(USER_NAME, PASSWORD);
 | 
			
		||||
        verify(authClientManager).authorize(argThat(r -> r.getPrincipal() != null && USER_NAME.equals(r.getPrincipal().getPrincipal())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void shouldThrowVerificationExceptionOnFailure()
 | 
			
		||||
    {
 | 
			
		||||
        final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
 | 
			
		||||
        when(authClientManager.authorize(any())).thenThrow(new RuntimeException("Expected"));
 | 
			
		||||
 | 
			
		||||
        final SpringOAuth2Client client = new SpringOAuth2Client(() -> authClientManager);
 | 
			
		||||
 | 
			
		||||
        assertThatExceptionOfType(CredentialsVerificationException.class)
 | 
			
		||||
                .isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
 | 
			
		||||
                .havingCause().withNoCause().withMessage("Expected");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void shouldAvoidCreatingMultipleInstanceOfOAuth2AuthorizedClientManager()
 | 
			
		||||
    {
 | 
			
		||||
        final OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
 | 
			
		||||
        when(authorizedClient.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
 | 
			
		||||
        final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
 | 
			
		||||
        when(authClientManager.authorize(any())).thenReturn(authorizedClient);
 | 
			
		||||
        final Supplier<OAuth2AuthorizedClientManager> supplier = mock(Supplier.class);
 | 
			
		||||
        when(supplier.get()).thenReturn(authClientManager);
 | 
			
		||||
 | 
			
		||||
        final SpringOAuth2Client client = new SpringOAuth2Client(supplier);
 | 
			
		||||
 | 
			
		||||
        client.verifyCredentials(USER_NAME, PASSWORD);
 | 
			
		||||
        client.verifyCredentials(USER_NAME, PASSWORD);
 | 
			
		||||
        client.verifyCredentials(USER_NAME, PASSWORD);
 | 
			
		||||
        verify(supplier, times(1)).get();
 | 
			
		||||
        verify(authClientManager, times(3)).authorize(any());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Supplier<OAuth2AuthorizedClientManager> faultySupplier(int numberOfInitialFailures, OAuth2AuthorizedClientManager authClientManager)
 | 
			
		||||
    {
 | 
			
		||||
        final int[] counter = new int[]{0};
 | 
			
		||||
        return () -> {
 | 
			
		||||
            if (counter[0]++ < numberOfInitialFailures)
 | 
			
		||||
            {
 | 
			
		||||
                throw new RuntimeException("Expected failure #" + counter[0]);
 | 
			
		||||
            }
 | 
			
		||||
            return authClientManager;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
/*
 | 
			
		||||
 * #%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 junit.framework.TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @deprecated as BitlyUrlShortenerImpl is no longer used in the core repository code.
 | 
			
		||||
 */
 | 
			
		||||
@Deprecated
 | 
			
		||||
public class BitlyUrlShortenerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private BitlyUrlShortenerImpl shortener;
 | 
			
		||||
    
 | 
			
		||||
    public void testShorten()
 | 
			
		||||
    {
 | 
			
		||||
        String url = "http://www.alfresco.com/";
 | 
			
		||||
        String shortUrl = shortener.shortenUrl(url);
 | 
			
		||||
        assertNotNull(shortUrl);
 | 
			
		||||
        assertFalse(shortUrl.isEmpty());
 | 
			
		||||
        assertFalse(url.equals(shortUrl));
 | 
			
		||||
        assertTrue(shortUrl.length() < url.length());
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
    * {@inheritDoc}
 | 
			
		||||
    */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void setUp() throws Exception
 | 
			
		||||
    {
 | 
			
		||||
        this.shortener = new BitlyUrlShortenerImpl();;
 | 
			
		||||
        shortener.setApiKey("R_ca15c6c89e9b25ccd170bafd209a0d4f");
 | 
			
		||||
        shortener.setUrlLength(20);
 | 
			
		||||
        shortener.setUsername("brianalfresco");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -66,5 +66,3 @@ encryption.cipherAlgorithm=DESede/CBC/PKCS5Padding
 | 
			
		||||
encryption.keystore.type=JCEKS
 | 
			
		||||
encryption.keystore.backup.type=JCEKS
 | 
			
		||||
 | 
			
		||||
# For CI override the default hashing algorithm for password storage to save build time.
 | 
			
		||||
system.preferred.password.encoding=sha256
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
BUILDER_NAME="${1}"
 | 
			
		||||
TARGET_REGISTRY="${2}"
 | 
			
		||||
TARGET_IMAGE="${3}"
 | 
			
		||||
IMAGE_TAG="${4}"
 | 
			
		||||
 | 
			
		||||
#Create a `docker-container` builder with host networking and required flags (quay.io)
 | 
			
		||||
docker --config target/docker/"${TARGET_REGISTRY}"/"${TARGET_IMAGE}"/"${IMAGE_TAG}"/docker \
 | 
			
		||||
buildx create --use --name "${BUILDER_NAME}" --driver-opt network=host \
 | 
			
		||||
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
 | 
			
		||||
 | 
			
		||||
#Create a `docker-container` builder with host networking and required flags (docker.io)
 | 
			
		||||
docker --config target/docker/"${TARGET_IMAGE}"/"${IMAGE_TAG}"/docker \
 | 
			
		||||
buildx create --use --name "${BUILDER_NAME}" --driver-opt network=host \
 | 
			
		||||
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
 | 
			
		||||
 | 
			
		||||
#Create a `docker-container` builder with host networking and required flags (local registry)
 | 
			
		||||
docker --config target/docker/127.0.0.1/5000/"${TARGET_IMAGE}"/"${IMAGE_TAG}"/docker \
 | 
			
		||||
buildx create --use --name "${BUILDER_NAME}" --driver-opt network=host \
 | 
			
		||||
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host'
 | 
			
		||||
		Reference in New Issue
	
	Block a user