diff --git a/travis/integration_tests.sh b/.github/bin/integration_tests.sh similarity index 100% rename from travis/integration_tests.sh rename to .github/bin/integration_tests.sh diff --git a/.github/bin/waitHttp.sh b/.github/bin/waitHttp.sh new file mode 100755 index 00000000..88f85737 --- /dev/null +++ b/.github/bin/waitHttp.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +http_code=100 +while [ "$http_code" != "200" ] +do + sleep 5 + http_code=`curl --write-out %{http_code} --silent --output /dev/null "$1"` + echo "$http_code"; +done + + diff --git a/.github/workflows/elastic.yml b/.github/workflows/elastic.yml new file mode 100644 index 00000000..57e663dd --- /dev/null +++ b/.github/workflows/elastic.yml @@ -0,0 +1,23 @@ +name: Elastic + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Starting ElasticSearch + run: docker compose -f docker/sas-elastic/compose.yaml --project-directory . up -d elastic + - name: Wait for Elastic to start + run: .github/bin/waitHttp.sh "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=50s" + - name: Run Tests + run: export "config=elastic.properties" && mvn test + diff --git a/.github/workflows/jena.yml b/.github/workflows/jena.yml new file mode 100644 index 00000000..a9aa9bed --- /dev/null +++ b/.github/workflows/jena.yml @@ -0,0 +1,22 @@ +name: Jena + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Selecting Jena config + run: export "config=Jena.properties" + + - name: Run Tests + run: mvn test diff --git a/.github/workflows/solr-cloud.yml b/.github/workflows/solr-cloud.yml new file mode 100644 index 00000000..b8422c02 --- /dev/null +++ b/.github/workflows/solr-cloud.yml @@ -0,0 +1,21 @@ +name: SOLR Cloud + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Starting SOLR + run: docker compose -f docker/sas-solr-cloud/compose.yaml --project-directory . up -d + + - name: Wait for SOLR to start + run: docker exec -t solr1 /opt/docker-solr/scripts/wait-for-solr.sh --max-attempts 10 --wait-seconds 5 --solr-url http://0.0.0.0:8983/ + + # Due to the way docker-compose and SOLR works we can't access the SOLR cloud + # from this machine. Instead we have to run the test within the cluster + - name: Run Tests + run: docker exec --workdir /usr/src/sas simpleannotationserver_web_1 /usr/bin/mvn -q test diff --git a/.github/workflows/solr.yml b/.github/workflows/solr.yml new file mode 100644 index 00000000..1125511d --- /dev/null +++ b/.github/workflows/solr.yml @@ -0,0 +1,27 @@ +name: SOLR + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Starting SOLR + run: mkdir solr-data && chmod 777 solr-data && docker compose -f docker/sas-solr/compose.yaml --project-directory . up -d solr + + - name: checking docker + run: docker ps && docker logs sas_solr + + - name: Wait for SOLR to start + run: docker exec -t sas_solr /opt/docker-solr/scripts/wait-for-solr.sh --max-attempts 10 --wait-seconds 10 --solr-url http://0.0.0.0:8983/ + + - name: Run Tests + run: export "config=solr.properties" && mvn test diff --git a/.gitignore b/.gitignore index 3d69a653..c0f2ff03 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,10 @@ src/main/webapp/demo.html /data cache index-2.6.1.html -src/main/webapp/stats +src/main/webapp/stats/*.json .aws-credentials +auth.json +*auth.json +node +node_modules +solr-data diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 63417712..00000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -dist: xenial -language: java -jdk: - - openjdk11 -script: chmod 755 ./travis/integration_tests.sh && ./travis/integration_tests.sh diff --git a/buildspec.yml b/buildspec.yml index 0841a873..60d873fb 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -6,6 +6,8 @@ phases: - echo Logging in to Amazon ECR... - aws --version - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email) + - echo Logging in to Docker Hub... + - echo $DOCKERHUB_PASSWORD | docker login --username $DOCKERHUB_USERNAME --password-stdin - IMAGE_NAME="sas" - REPOSITORY_URI=082101253860.dkr.ecr.eu-west-2.amazonaws.com/sas - IMAGE_TAG=prod_$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) @@ -14,13 +16,13 @@ phases: - echo Build started on `date` - echo Building the Docker image... - echo Image_tag $IMAGE_TAG - - docker build -t $REPOSITORY_URI:$IMAGE_TAG -f docker/sas-tomcat/Dockerfile . - - docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:latest + - docker build --build-arg AUTH_JSON_LOCATION=$AUTH_JSON_LOCATION --build-arg AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION --build-arg AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI -t $REPOSITORY_URI:$IMAGE_TAG -t $REPOSITORY_URI:latest -f docker/sas-auth/Dockerfile . post_build: commands: - echo Build completed on `date` - echo Pushing the Docker images... - - docker push $REPOSITORY_URI + - docker push $REPOSITORY_URI:latest + - docker push $REPOSITORY_URI:$IMAGE_TAG - echo Writing image definitions file... - printf '[{"name":"SAS","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json artifacts: diff --git a/doc/Auth.md b/doc/Auth.md new file mode 100644 index 00000000..ab559e47 --- /dev/null +++ b/doc/Auth.md @@ -0,0 +1,143 @@ +# Authentication + +The SimpleAnnotationServer now supports Authentication through OAuth. This allows users to login using Google, GitHub or other OAuth provider and to work on a private workspace of Annotations, Manifests and Collections. + +## Configuration + +The presence of a file called `auth.json` in the `src/main/webapp/WEB-INF` is enough for SAS to know that it should use authentication for all requests. The `auth.json` file has settings for OAuth providers and should be kept secret and outside of GitHub. An example configuration for Google is shown below: + +``` +[{ + "id":"google", + "class": "com.github.scribejava.apis.GoogleApi20", + "clientId": "**google_client_id**", + "clientSecret": "**google_client_secret", + "scope": "profile email", + "additionalParam": { + "access_type": "offline" + }, + "button": { + "logo": "/images/GoogleLogo.svg", + "text": "Sign in with Google" + }, + "userMapping": { + "endpoint": "https://www.googleapis.com/oauth2/v3/userinfo", + "responseKeys": { + "id":"sub", + "name": "name", + "email": "email", + "pic": "picture" + } + } +}] +``` + +The file is split into three sections; OAuth settings, button config and userMappings and details for each section can be seen below. To offer multiple login options it is possible to add extra configs to this file as the root of the JSON is a list. + +### OAuth Settings + +The OAuth settings from above are copied below for convenience: + +``` +"id":"google", +"class": "com.github.scribejava.apis.GoogleApi20", +"clientId": "**google_client_id**", +"clientSecret": "**google_client_secret", +"scope": "profile email", +"additionalParam": { + "access_type": "offline" +}, +``` + +The files are: + + * __id__ this should be unique in the file and be used to identify the authentication method + * __class__ this is the [ScribeJava](https://github.com/scribejava/scribejava) OAuth library class which implements this authentication method. The ScribeJava github site gives examples with lots of different OAuth providers. + * __clientId__ and __clientSecret__ these are generated by Google and you can apply for a set of keys by going to the [Google Developer Console](https://console.developers.google.com/apis/credentials). When you apply for keys you will need to add a Authorized redirect URI to the SAS system. This is the URL google will return the user if they authenticated correctly. The redirect URI should be: + +https://example.com/login-callback + +where example.com is the public domain name you are using to host SAS. + + * __scope__ this is the information SAS is asking the user to give permission for. To find out what is required for your OAuth provider check the ScribeJava examples. + * __additionalParam__ some OAuth providers also require extra parameters. Again check ScribeJava to see if this is required. + +### Button config +When you login to SAS it will present you with a login page where users are asked to choose which login service they would like to register with. The button config allows customisation of the logo and text that is offered to the user for this authentication method: + +``` +"button": { + "logo": "/images/GoogleLogo.svg", + "text": "Sign in with Google" +}, +``` + +### User Mapping +Once a user has been authenticated then SAS will request the name, email and profile picture from the OAuth provider. This is usually done using a standard API that returns a JSON list of keys. The Key mapping configuration maps the OAuth User JSON to SAS's users. + +``` +"userMapping": { + "endpoint": "https://www.googleapis.com/oauth2/v3/userinfo", + "responseKeys": { + "id":"sub", + "name": "name", + "email": "email", + "pic": "picture" + } +} +``` + +## Extra customisation + +As well as the general configuration above its also possible to customise the authentication in the following ways. + + + +## Deployment with Docker + +If you are working with SAS in the cloud you will need to make sure the that the auth.json doesn't end up in GitHub where it will be public. There is an example [Dockerfile](../docker/sas-auth/Dockerfile) in the sas-auth directory which will work with a `auth.json` held in an Amazon S3 bucket. To get this to work you will need to ensure the Code Pipeline role has the following permissions: + + +Codepipeline Service role to access s3: + +Build project -> build details -> Service role ARN + +Ensure ROLE has this: +``` +{ + "Action": [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning" + ], + "Resource": [ + "arn:aws:s3:::sasconfig*" + ], + "Effect": "Allow" +}, +{ + "Effect": "Allow", + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:GenerateDataKeyPairWithoutPlaintext", + "ssm:GetParameters", + "kms:GenerateDataKeyPair", + "ssm:GetParameter" + ], + "Resource": [ + "arn:aws:kms:$REGION:$AWS_ACCOUNT_ID:key/$ENCRYPT_KEY", + "arn:aws:ssm:$REGION:$AWS_ACCOUNT_ID:parameter/$PARAM_KEYS/*" + ] +} +``` + +To find the relevant service role you can navigate to your Code Builder project where you can see the history of your build. At the top there is a tab called 'Build Details'. Click this then scroll down until you see a clickable link for the "Service role". + +For your configuration you to will need to change `$REGION`, `$AWS_ACCOUNT_ID`, `$ENCRYPT_KEY` and $PARAM_KEYS to fit your AWS account details. To setup the required parameters there is a great write up here: + +https://medium.com/rockedscience/fixing-docker-hub-rate-limiting-errors-in-ci-cd-pipelines-ea3c80017acb + +## Migrating from previous versions of SAS +SAS previously hasn't had the concept of users or Authentication so this version will be a breaking change and any annotations created using previous versions of SAS will no longer be accessible because they are not associated with a user. Ensure you have backed up any annotations you would like to keep and it is advisable to use a new ElasticSearch or SOLR index to run this version of SAS. diff --git a/docker/sas-auth/Dockerfile b/docker/sas-auth/Dockerfile new file mode 100644 index 00000000..23a32815 --- /dev/null +++ b/docker/sas-auth/Dockerfile @@ -0,0 +1,28 @@ +# build stage +FROM maven:3-jdk-11 AS buildstage +WORKDIR /usr/src/sas +COPY . /usr/src/sas +ARG MVN_ARGS="-DskipTests" +# build SAS using maven +RUN mvn $MVN_ARGS package + +# runnable container stage +FROM tomcat:9-jre11 AS runstage +ARG AWS_DEFAULT_REGION +ARG AWS_CONTAINER_CREDENTIALS_RELATIVE_URI +ARG AUTH_JSON_LOCATION +# remove tomcat default webapps and create data directory +RUN rm -rf /usr/local/tomcat/webapps/* +# copy SAS from build image +COPY --from=buildstage /usr/src/sas/target/simpleAnnotationStore /usr/local/tomcat/webapps/ROOT +# copy properties +COPY docker/sas-auth/sas.properties /usr/local/tomcat/webapps/ROOT/WEB-INF + +# Download auth config +# Install the AWS CLI +RUN apt-get update && \ + apt-get -y install awscli +RUN aws --region eu-west-2 s3 cp $AUTH_JSON_LOCATION /usr/local/tomcat/webapps/ROOT/WEB-INF/ +# For testing locally: +#COPY docker/sas-auth/auth.json /usr/local/tomcat/webapps/ROOT/WEB-INF/ +# use default port and entrypoint diff --git a/docker/sas-auth/compose.yaml b/docker/sas-auth/compose.yaml new file mode 100644 index 00000000..db0026c0 --- /dev/null +++ b/docker/sas-auth/compose.yaml @@ -0,0 +1,20 @@ +version: '3' +services: + web: + container_name: sas + build: + context: . + dockerfile: docker/sas-auth/Dockerfile + ports: + - "8888:8080" + elastic: + image: "elasticsearch:7.8.1" + container_name: elasticsearch + environment: + - discovery.type=single-node + ulimits: + memlock: + soft: -1 + hard: -1 + ports: + - 9200:9200 diff --git a/docker/sas-auth/sas.properties b/docker/sas-auth/sas.properties new file mode 100644 index 00000000..e875b597 --- /dev/null +++ b/docker/sas-auth/sas.properties @@ -0,0 +1,36 @@ +# Generic properties +# ================== + +# Uncomment this if you are behind a proxy or want a public URI +# baseURI=http://dev.llgc.org.uk/annotation/ + +# Uncomment this if you would like to use an encoder which will work on +# the annotation before its stored in the triplestore +# encoder=uk.org.llgc.annotation.store.encoders.BookOfPeaceEncode + +# if you are using Mirador versions greater than 2.1.4 then you need to uncomment the following +# as the annotation structure changed between versions +#encoder=uk.org.llgc.annotation.store.encoders.Mirador214 + +# Store configuration +# ================== + +# Uncomment this if you would like to use Jena as a backend +#store=jena +#data_dir=/annotation-data + +# Uncomment the following if you want to use Sesame +# store=sesame +# repo_url=http://localhost:8080/openrdf-sesame/repositories/test-anno + +# Uncomment the following if you want to use SOLR cores +#store=solr +#solr_connection=http://solr:8983/solr/annotations + +# Uncomment the following if you want to use SOLR collections (Cloud) +#store=solr-cloud +#solr_connection=http://solr:8983/solr,http://solr:7574/solr +#solr_collection=annotations + +store=elastic +elastic_connection=http://elasticsearch:9200/annotations diff --git a/docker/sas-elastic/Dockerfile b/docker/sas-elastic/Dockerfile index 25fc81a6..e1b87d8a 100644 --- a/docker/sas-elastic/Dockerfile +++ b/docker/sas-elastic/Dockerfile @@ -9,11 +9,11 @@ RUN mvn $MVN_ARGS package # runnable container stage FROM tomcat:9-jre11 AS runstage # remove tomcat default webapps and create data directory -RUN rm -r /usr/local/tomcat/webapps/* && \ +RUN rm -rf /usr/local/tomcat/webapps/* && \ mkdir /annotation-data # copy SAS from build image COPY --from=buildstage /usr/src/sas/target/simpleAnnotationStore /usr/local/tomcat/webapps/ROOT # copy properties -COPY docker/sas-tomcat/sas.properties /usr/local/tomcat/webapps/ROOT/WEB-INF +COPY docker/sas-elastic/sas.properties /usr/local/tomcat/webapps/ROOT/WEB-INF # use default port and entrypoint diff --git a/docker/sas-elastic/docker-compose.yml b/docker/sas-elastic/compose.yaml similarity index 100% rename from docker/sas-elastic/docker-compose.yml rename to docker/sas-elastic/compose.yaml diff --git a/docker/sas-elastic/sas.properties b/docker/sas-elastic/sas.properties index 7f9cf125..b9ac0d3a 100644 --- a/docker/sas-elastic/sas.properties +++ b/docker/sas-elastic/sas.properties @@ -24,8 +24,10 @@ # repo_url=http://localhost:8080/openrdf-sesame/repositories/test-anno # Uncomment the following if you want to use SOLR cores -store=solr -solr_connection=http://solr:8983/solr/annotations +store=elastic +elastic_connection=http://elasticsearch:9200/annotations + +admin=glen.robson@gmail.com # Uncomment the following if you want to use SOLR collections (Cloud) #store=solr-cloud diff --git a/docker/sas-solr-cloud/Dockerfile b/docker/sas-solr-cloud/Dockerfile index 84d333bc..9461320b 100644 --- a/docker/sas-solr-cloud/Dockerfile +++ b/docker/sas-solr-cloud/Dockerfile @@ -2,14 +2,14 @@ FROM maven:3-jdk-11 AS buildstage WORKDIR /usr/src/sas COPY . /usr/src/sas -ARG MVN_ARGS="-DskipTests" +ARG MVN_ARGS="-DskipTests -q" # build SAS using maven RUN mvn $MVN_ARGS package # runnable container stage -FROM tomcat:9-jre11 AS runstage +FROM tomcat:9-jdk11-temurin AS runstage # remove tomcat default webapps and create data directory -RUN rm -r /usr/local/tomcat/webapps/* +RUN rm -rf /usr/local/tomcat/webapps/* # copy SAS from build image COPY --from=buildstage /usr/src/sas/target/simpleAnnotationStore /usr/local/tomcat/webapps/ROOT # copy properties @@ -17,7 +17,7 @@ COPY docker/sas-solr-cloud/sas.properties /usr/local/tomcat/webapps/ROOT/WEB-INF # Used for testing: COPY . /usr/src/sas -RUN apt-get update && apt-get -y install maven +RUN apt-get update && apt-get -y install maven npm COPY docker/sas-solr-cloud/sas.properties /usr/src/sas/src/test/resources/test.properties # use default port and entrypoint diff --git a/docker/sas-solr-cloud/docker-compose.yml b/docker/sas-solr-cloud/compose.yaml similarity index 100% rename from docker/sas-solr-cloud/docker-compose.yml rename to docker/sas-solr-cloud/compose.yaml diff --git a/docker/sas-solr/docker-compose.yml b/docker/sas-solr/compose.yaml similarity index 68% rename from docker/sas-solr/docker-compose.yml rename to docker/sas-solr/compose.yaml index 086430c8..4182d100 100644 --- a/docker/sas-solr/docker-compose.yml +++ b/docker/sas-solr/compose.yaml @@ -9,12 +9,13 @@ services: - "8888:8080" solr: container_name: sas_solr - image: "solr:latest" + image: "solr:8" ports: - "8983:8983" volumes: - "./src/main/resources/solr:/tmp/config:ro" + - "./solr-data:/var/solr/data" entrypoint: - bash - "-c" - - "precreate-core testannotations /tmp/config; precreate-core annotations /tmp/config; exec solr -f" + - "solr-precreate testannotations /tmp/config; solr-precreate annotations /tmp/config; exec solr -f" diff --git a/docker/sas-tomcat/Dockerfile b/docker/sas-tomcat/Dockerfile index 25fc81a6..97020f0f 100644 --- a/docker/sas-tomcat/Dockerfile +++ b/docker/sas-tomcat/Dockerfile @@ -9,7 +9,7 @@ RUN mvn $MVN_ARGS package # runnable container stage FROM tomcat:9-jre11 AS runstage # remove tomcat default webapps and create data directory -RUN rm -r /usr/local/tomcat/webapps/* && \ +RUN rm -rf /usr/local/tomcat/webapps/* && \ mkdir /annotation-data # copy SAS from build image COPY --from=buildstage /usr/src/sas/target/simpleAnnotationStore /usr/local/tomcat/webapps/ROOT diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2b4b3681 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,110 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "jasmine": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.9.0.tgz", + "integrity": "sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "jasmine-core": "~3.9.0" + } + }, + "jasmine-core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.9.0.tgz", + "integrity": "sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..156e6a0a --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "test": "jasmine --config=src/test/javascript/jasmine.json" + }, + "devDependencies": { + "jasmine": "^3.9.0" + } +} diff --git a/pom.xml b/pom.xml index 9eb8ca9a..7f3a0d8c 100644 --- a/pom.xml +++ b/pom.xml @@ -43,17 +43,17 @@ org.apache.jena jena-core - 3.16.0 + 4.3.1 org.apache.jena jena-tdb - 3.16.0 + 4.3.1 org.apache.jena jena-arq - 3.16.0 + 4.3.1 org.apache.jena @@ -68,12 +68,17 @@ commons-codec commons-codec - 1.14 + 1.15 + + + commons-fileupload + commons-fileupload + 1.4 com.github.jsonld-java jsonld-java - 0.13.0 + 0.13.4 org.apache.httpcomponents @@ -88,7 +93,7 @@ org.jdom jdom2 - 2.0.6 + 2.0.6.1 jaxen @@ -103,12 +108,12 @@ com.fasterxml.jackson.core jackson-core - 2.11.1 + 2.13.0 com.fasterxml.jackson.core jackson-databind - 2.11.1 + 2.13.0 org.openrdf.sesame @@ -123,7 +128,7 @@ org.apache.httpcomponents httpclient - 4.5.12 + 4.5.13 commons-logging @@ -144,45 +149,58 @@ org.apache.logging.log4j log4j-api - 2.13.3 + 2.16.0 org.apache.logging.log4j log4j-core - 2.13.3 + 2.16.0 org.apache.logging.log4j log4j-slf4j-impl - 2.13.3 + 2.16.0 org.apache.solr solr-solrj - 8.6.0 + 8.11.0 - - org.elasticsearch - elasticsearch - 7.7.1 - org.elasticsearch.client elasticsearch-rest-high-level-client - 7.7.1 + 7.13.4 com.amazonaws aws-java-sdk-core - 1.11.839 + 1.12.129 + + + + org.mockito + mockito-core + 4.1.0 + test junit junit - 4.13 + 4.13.2 test + + + com.github.scribejava + scribejava-apis + 8.3.1 + + + org.springframework.security + spring-security-core + 5.6.0 + simpleAnnotationStore @@ -263,6 +281,57 @@ + + com.github.eirslett + frontend-maven-plugin + 1.10.4 + + + + install node and npm + + install-node-and-npm + + generate-resources + + v14.15.1 + 6.14.8 + + + + npm install + + npm + + + + install + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.3.2 + + + npm run test (test) + + exec + + test + + npm + + run + test + + ${skipTests} + + + + diff --git a/runDocker.sh b/runDocker.sh index 129b6468..efd829b1 100755 --- a/runDocker.sh +++ b/runDocker.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ $# -eq 0 ]; then - echo "What backend do you want to use? Jena / Solr / Cloud" + echo "What backend do you want to use? Jena / Solr / Cloud / Elastic" read backend else backend="$1" @@ -19,6 +19,9 @@ elif [[ "$backend" =~ [Cc]loud ]]; then elif [[ "$backend" =~ [Ee]lastic ]]; then echo "Running SAS with Elastic Cloud on port 8888" docker-compose -f docker/sas-elastic/docker-compose.yml --project-directory . up +elif [[ "$backend" =~ [Aa]uth ]]; then + echo "Running SAS with Elastic Cloud on port 8888 with Auth enabled" + docker-compose -f docker/sas-auth/docker-compose.yml --project-directory . up else echo "I don't recognise '$backend'. Options are Jena / Solr / Cloud" fi diff --git a/src/main/java/uk/org/llgc/annotation/store/AnnotationUtils.java b/src/main/java/uk/org/llgc/annotation/store/AnnotationUtils.java index bcc915c4..a30541b2 100644 --- a/src/main/java/uk/org/llgc/annotation/store/AnnotationUtils.java +++ b/src/main/java/uk/org/llgc/annotation/store/AnnotationUtils.java @@ -88,12 +88,7 @@ public List> readAnnotationList(final InputStream pStream, fi int tAnnoCount = 0; for (Map tAnno : tAnnotations) { if (tAnno.get("@id") == null) { - - StringBuffer tBuff = new StringBuffer(pBaseURL); - tBuff.append(getHash(getTarget(tAnno), "md5")); - tBuff.append("/"); - tBuff.append(tAnnoCount++); - tAnno.put("@id", tBuff.toString()); + tAnno.put("@id", this.generateAnnoId(pBaseURL, tAnnoCount++)); } tAnno.put("@context", this.getContext()); // need to add context to each annotation fixes issue #18 @@ -185,7 +180,7 @@ public Map readAnnotaion(final InputStream pStream, final String Map tRoot = (Map)tAnnotation; if (tRoot.get("@id") == null) { - String tID = pBaseURL + "/" + this.generateAnnoId(); + String tID = this.generateAnnoId(pBaseURL); tRoot.put("@id", tID); } // Change context to local for quick processing @@ -308,6 +303,19 @@ public Map frameManifest(final Model pManifest) throws JsonLdErro return this.frame(pManifest, tContextJson); } + public Map frameCollection(final Model pCollection) throws JsonLdError, IOException { + final Map tFrameJson = (Map)JsonUtils.fromInputStream(new FileInputStream(new File(_contextDir,"collection_frame.json"))); + Map tCollectionsJson = this.frame(pCollection, tFrameJson); + tCollectionsJson.remove("sc:hasCollections"); + tCollectionsJson.remove("sc:hasManifests"); + tCollectionsJson.remove("sc:hasParts"); + if (tCollectionsJson.get("dcterms:creator") instanceof Map) { + tCollectionsJson.put("dcterms:creator", ((Map)tCollectionsJson.get("dcterms:creator")).get("@id")); + } + return tCollectionsJson; + } + + public Map frame(final Model pModel, final Map pFrame) throws JsonLdError, IOException { pFrame.put("@context", this.getContext()); @@ -324,7 +332,6 @@ public Map frame(final Model pModel, final Map pFr pModel.commit(); } Map tFramed = (Map)JsonLdProcessor.frame(JsonUtils.fromString(tStringOut.toString()), pFrame, tOptions); - Map tJsonLd = (Map)((List)tFramed.get("@graph")).get(0); if (tJsonLd.get("@context") == null) { tJsonLd.put("@context", this.getExternalContext()); @@ -365,7 +372,22 @@ public void collapseFragmentOn(final Map pAnnotationJson, final M } } - protected String generateAnnoId() { - return "" + new java.util.Date().getTime(); + protected String generateAnnoId(final String pBaseURL) { + StringBuffer tBuff = new StringBuffer(pBaseURL); + if (!pBaseURL.endsWith("/")) { + tBuff.append("/"); + } + tBuff.append("anno/"); + tBuff.append("" + new java.util.Date().getTime()); + return tBuff.toString(); } + + // To be used when potenially lots of annotations are added at once + protected String generateAnnoId(final String pBaseURL, final int pOffest) { + StringBuffer tBuff = new StringBuffer(this.generateAnnoId(pBaseURL)); + tBuff.append("/"); + tBuff.append("" + pOffest); + return tBuff.toString(); + } + } diff --git a/src/main/java/uk/org/llgc/annotation/store/IIIFSearchAPI.java b/src/main/java/uk/org/llgc/annotation/store/IIIFSearchAPI.java index 8ab4b214..f02ba8c7 100644 --- a/src/main/java/uk/org/llgc/annotation/store/IIIFSearchAPI.java +++ b/src/main/java/uk/org/llgc/annotation/store/IIIFSearchAPI.java @@ -11,10 +11,12 @@ import org.apache.jena.rdf.model.Model; import java.util.Map; +import java.util.HashMap; import java.util.List; import java.text.ParseException; import java.net.URI; +import java.net.URL; import java.net.URISyntaxException; import javax.servlet.http.HttpServlet; @@ -27,7 +29,11 @@ import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.data.SearchQuery; import uk.org.llgc.annotation.store.data.DateRange; +import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.data.AnnotationList; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; public class IIIFSearchAPI extends HttpServlet { protected static Logger _logger = LogManager.getLogger(IIIFSearchAPI.class.getName()); @@ -45,40 +51,98 @@ public void init(final ServletConfig pConfig) throws ServletException { } // http://universalviewer.io/examples/?manifest=http://193.61.220.59:8888/manifests/4642022.json&locale=en-GB#?c=0&m=0&s=0&cv=6&z=-37.9666%2C0%2C9949.9332%2C7360 + // example URL: http://localhost:8888/search-api/1245635613/1969268/5bbada360fbe7c8f72a8153896686398/search + // baseURL + ///search public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { - String[] tRequestURI = pReq.getRequestURI().split("/"); - String tManifestShortId = tRequestURI[tRequestURI.length - 2]; - String tBaseURI = pReq.getParameter("base"); + String relativeId = pReq.getRequestURI().substring(pReq.getRequestURI().lastIndexOf("/iiif-search/") + "/iiif-search/".length()); + + String tManifestShortId = ""; + String tUserId = ""; + String[] tSplit = relativeId.split("/"); + for (int i = tSplit.length - 1; i >= 0; i--) { + if (i == tSplit.length - 2) { + tManifestShortId = tSplit[i]; + } + if (i < tSplit.length - 2) { + if (tUserId.length() == 0){ + tUserId = tSplit[i]; + } else { + tUserId = tSplit[i] + "/" + tUserId; + } + } + } + User tUser = null; + try { + System.out.println("Userid '" + tUserId + "'"); + if ((tUserId == null || tUserId.length() == 0) && pReq.getParameter("user") != null) { + System.out.println("Found user param " + pReq.getParameter("user")); + // User has been passed as a parameter + tUser = new User(); + tUser.setId(pReq.getParameter("user")); + tUser = _store.getUser(tUser); + } else { + tUser = _store.getUser(User.createUserFromShortID(StoreConfig.getConfig().getBaseURI(pReq), tUserId)); + } + } catch (URISyntaxException tExcpt) { + throw new IOException("Unable to create user due to " + tExcpt); + } + SearchQuery tQuery = null; - try { - StringBuffer tURI = null; - if (tBaseURI != null) { - // a supplied base overides config - tURI= new StringBuffer(tBaseURI); - } else { - tURI = new StringBuffer(StoreConfig.getConfig().getBaseURI(pReq)); - tURI.append("/search-api/" + tManifestShortId + "/search"); - } - if (pReq.getQueryString() != null) { - tURI.append("?"); - tURI.append(pReq.getQueryString()); - } - System.out.println("URI " + tURI.toString()); - tQuery = new SearchQuery(new URI(tURI.toString())); - tQuery.setResultsPerPage(_resultsPerPage); - tQuery.setScope(_store.getManifestId(tManifestShortId)); - } catch (ParseException tExcpt) { - pRes.sendError(pRes.SC_BAD_REQUEST,"Failed to parse date paratmeters " + tExcpt); - return; - } catch (URISyntaxException tExcpt) { - pRes.sendError(pRes.SC_BAD_REQUEST,"Failed to parse uri " + tExcpt); + Manifest tManifest = new Manifest(); + tManifest.setURI(_store.getManifestId(tManifestShortId)); + + if (tUser == null || tManifest.getURI() == null || tManifest.getURI().length() == 0) { + System.out.println("User " + tUserId + " Manifest " + tManifestShortId); + // unable to find user or manifest + pRes.sendError(pRes.SC_NOT_FOUND,"Failed to find user or manifest supplied"); return; - } + } + + AuthorisationController tAuth = new AuthorisationController(pReq); + if (tAuth.allowSearchManifest(tManifest, tUser)) { + URL tSearchURL = tManifest.getSearchURL(StoreConfig.getConfig().getBaseURI(pReq), tUser); - AnnotationList tResults = _store.search(tQuery); + try { + StringBuffer tURI = new StringBuffer(tSearchURL.toString()); + if (pReq.getQueryString() != null) { + tURI.append("?"); + tURI.append(pReq.getQueryString()); + } + System.out.println("Search URI: " + tURI.toString()); + tQuery = new SearchQuery(new URI(tURI.toString())); + tQuery.setResultsPerPage(_resultsPerPage); + tQuery.setScope(tManifest.getURI()); - pRes.setContentType("application/ld+json; charset=UTF-8"); - pRes.setCharacterEncoding("UTF-8"); - pRes.getWriter().println(JsonUtils.toPrettyString(tResults.toJson())); + // Query may already contain user from Query string + // If so don't duplicate + tQuery.addUser(tUser); + System.out.println("Users: " + tQuery.getUsers()); + } catch (ParseException tExcpt) { + pRes.sendError(pRes.SC_BAD_REQUEST,"Failed to parse date paratmeters " + tExcpt); + return; + } catch (URISyntaxException tExcpt) { + pRes.sendError(pRes.SC_BAD_REQUEST,"Failed to parse uri " + tExcpt); + return; + } + + AnnotationList tResults = _store.search(tQuery); + + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + pRes.getWriter().println(JsonUtils.toPrettyString(tResults.toJson())); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You are not allowed to search this users annotations"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } } + + protected void sendJson(final HttpServletResponse pRes, final int pCode, final Map pPayload) throws IOException { + pRes.setStatus(pCode); + pRes.setContentType("application/json"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.writePrettyPrint(pRes.getWriter(), pPayload); + } + } diff --git a/src/main/java/uk/org/llgc/annotation/store/ListAnnoPages.java b/src/main/java/uk/org/llgc/annotation/store/ListAnnoPages.java deleted file mode 100644 index 19bad532..00000000 --- a/src/main/java/uk/org/llgc/annotation/store/ListAnnoPages.java +++ /dev/null @@ -1,76 +0,0 @@ -package uk.org.llgc.annotation.store; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.io.File; -import java.io.FileReader; -import java.io.BufferedReader; - -import com.github.jsonldjava.utils.JsonUtils; - -import org.apache.jena.rdf.model.Model; - -import java.util.Map; -import java.util.List; - -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; - -import uk.org.llgc.annotation.store.data.PageAnnoCount; -import uk.org.llgc.annotation.store.adapters.StoreAdapter; - -public class ListAnnoPages extends HttpServlet { - protected static Logger _logger = LogManager.getLogger(ListAnnoPages.class.getName()); - protected AnnotationUtils _annotationUtils = null; - protected StoreAdapter _store = null; - - public void init(final ServletConfig pConfig) throws ServletException { - super.init(pConfig); - _annotationUtils = new AnnotationUtils(new File(super.getServletContext().getRealPath("/contexts")),StoreConfig.getConfig().getEncoder()); - _store = StoreConfig.getConfig().getStore(); - _store.init(_annotationUtils); - } - - public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { - List tAnnotations = _store.listAnnoPages(); - _logger.debug(tAnnotations); - - StringBuffer tContent = new StringBuffer(); - for (PageAnnoCount tPage : tAnnotations) { - tContent.append("
  • "); - tContent.append(tPage.getCanvas().getId()); - tContent.append(" "); - tContent.append("("); - tContent.append(tPage.getCount()); - tContent.append(" annotations)"); - tContent.append("
  • "); - } - - File tTemplate = new File(new File(super.getServletContext().getRealPath("/templates")), "list.template"); - BufferedReader tReader = null; - try { - tReader = new BufferedReader(new FileReader(tTemplate)); - String tLine = null; - StringBuffer tHTML = new StringBuffer(); - while ((tLine = tReader.readLine()) != null ) { - tHTML.append(tLine); - } - - String tResult = tHTML.toString().replaceAll("##CONTENT##", tContent.toString()); - - pRes.setContentType("text/html"); - pRes.getOutputStream().println(tResult); - } finally { - if (tReader != null) { - tReader.close(); - } - } - } -} diff --git a/src/main/java/uk/org/llgc/annotation/store/ListAnnotations.java b/src/main/java/uk/org/llgc/annotation/store/ListAnnotations.java index ef35ef49..526d70f2 100644 --- a/src/main/java/uk/org/llgc/annotation/store/ListAnnotations.java +++ b/src/main/java/uk/org/llgc/annotation/store/ListAnnotations.java @@ -24,6 +24,7 @@ import uk.org.llgc.annotation.store.data.PageAnnoCount; import uk.org.llgc.annotation.store.adapters.StoreAdapter; import uk.org.llgc.annotation.store.data.AnnotationList; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; public class ListAnnotations extends HttpServlet { protected static Logger _logger = LogManager.getLogger(ListAnnotations.class.getName()); @@ -37,14 +38,17 @@ public void init(final ServletConfig pConfig) throws ServletException { } public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { - AnnotationList tAnnotations = _store.getAllAnnotations(); - - StringBuffer tURI = new StringBuffer(StoreConfig.getConfig().getBaseURI(pReq)); - tURI.append("/annotation/"); - tAnnotations.setId(tURI.toString()); - - pRes.setContentType("application/ld+json; charset=UTF-8"); - pRes.setCharacterEncoding("UTF-8"); - pRes.getWriter().println(JsonUtils.toPrettyString(tAnnotations.toJson())); + AuthorisationController tAuth = new AuthorisationController(pReq); + if (tAuth.allowExportAllAnnotations()) { + AnnotationList tAnnotations = _store.getAllAnnotations(); + + StringBuffer tURI = new StringBuffer(StoreConfig.getConfig().getBaseURI(pReq)); + tURI.append("/annotation/"); + tAnnotations.setId(tURI.toString()); + + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + pRes.getWriter().println(JsonUtils.toPrettyString(tAnnotations.toJson())); + } } } diff --git a/src/main/java/uk/org/llgc/annotation/store/Populate.java b/src/main/java/uk/org/llgc/annotation/store/Populate.java index 6f057d36..de6ad162 100644 --- a/src/main/java/uk/org/llgc/annotation/store/Populate.java +++ b/src/main/java/uk/org/llgc/annotation/store/Populate.java @@ -7,8 +7,14 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; import javax.servlet.ServletException; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; + import java.io.IOException; import java.io.File; import java.io.InputStream; @@ -17,6 +23,7 @@ import java.util.Map; import java.util.List; +import java.util.Iterator; import com.github.jsonldjava.utils.JsonUtils; @@ -24,6 +31,7 @@ import uk.org.llgc.annotation.store.adapters.StoreAdapter; import uk.org.llgc.annotation.store.data.AnnotationList; +import uk.org.llgc.annotation.store.controllers.UserService; import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; @@ -46,22 +54,56 @@ public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes if (pReq.getParameter("uri") != null) { _logger.debug("Reading from " + pReq.getParameter("uri")); tAnnotationList = new URL(pReq.getParameter("uri")).openStream(); - } else { - /*java.io.BufferedReader tReader = new java.io.BufferedReader( new java.io.InputStreamReader( pReq.getInputStream())); - String tLine = ""; - System.out.println("Printing results"); - while ((tLine = tReader.readLine()) != null) { - System.out.println("line:" + tLine); - } - System.out.println("done");*/ + + List> tAnnotationListJSON = _annotationUtils.readAnnotationList(tAnnotationList, StoreConfig.getConfig().getBaseURI(pReq) + "/annotation"); //annotaiton list + addAnnoList(tAnnotationListJSON, pReq, pRes); + } else if (ServletFileUpload.isMultipartContent(pReq)){ + System.out.println("Found multi part content"); tAnnotationList = pReq.getInputStream(); + // Create a factory for disk-based file items + DiskFileItemFactory factory = new DiskFileItemFactory(); + + // Configure a repository (to ensure a secure temp location is used) + ServletContext servletContext = this.getServletConfig().getServletContext(); + File repository = (File)servletContext.getAttribute("javax.servlet.context.tempdir"); + factory.setRepository(repository); + + // Create a new file upload handler + ServletFileUpload upload = new ServletFileUpload(factory); + + // Parse the request + List items = null; + try { + items = upload.parseRequest(pReq); + } catch (FileUploadException tExcpt) { + tExcpt.printStackTrace(); + pRes.setStatus(HttpServletResponse.SC_BAD_REQUEST); + pRes.setContentType("text/plain"); + pRes.getOutputStream().println("Failed to load annotation list due to: " + tExcpt.toString()); + return; + } + System.out.println("Items: " + items); + // Process the uploaded items + Iterator iter = items.iterator(); + while (iter.hasNext()) { + FileItem item = iter.next(); + System.out.println("Found " + item); + if (!item.isFormField()) { + List> tAnnotationListJSON = _annotationUtils.readAnnotationList(item.getInputStream(), StoreConfig.getConfig().getBaseURI(pReq) + "/annotation"); //annotaiton list + addAnnoList(tAnnotationListJSON, pReq, pRes); + } + } } - List> tAnnotationListJSON = _annotationUtils.readAnnotationList(tAnnotationList, StoreConfig.getConfig().getBaseURI(pReq) + "/annotation"); //annotaiton list - _logger.debug("JSON in:"); - _logger.debug(JsonUtils.toPrettyString(tAnnotationListJSON)); + } + + protected void addAnnoList(final List> pAnnotationListJSON, final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + _logger.debug("JSON in:"); + _logger.debug(JsonUtils.toPrettyString(pAnnotationListJSON)); try { - _store.addAnnotationList(new AnnotationList(tAnnotationListJSON)); + AnnotationList tList = new AnnotationList(pAnnotationListJSON); + tList.setCreator(new UserService(pReq).getUser()); + _store.addAnnotationList(tList); pRes.setStatus(HttpServletResponse.SC_CREATED); pRes.setContentType("text/plain"); @@ -77,5 +119,6 @@ public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes pRes.setContentType("text/plain"); pRes.getOutputStream().println("Falied to load annotation as it was badly informed: " + tExcpt.toString()); } - } + + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/StoreConfig.java b/src/main/java/uk/org/llgc/annotation/store/StoreConfig.java index 53c759d6..0ccb1541 100644 --- a/src/main/java/uk/org/llgc/annotation/store/StoreConfig.java +++ b/src/main/java/uk/org/llgc/annotation/store/StoreConfig.java @@ -7,6 +7,8 @@ import java.util.Map; import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; import java.util.Enumeration; import java.util.Properties; @@ -17,6 +19,7 @@ import uk.org.llgc.annotation.store.adapters.elastic.ElasticStore; import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.AnnotationUtils; +import uk.org.llgc.annotation.store.data.login.OAuthTarget; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -32,11 +35,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.github.jsonldjava.utils.JsonUtils; + public class StoreConfig extends HttpServlet { protected static Logger _logger = LogManager.getLogger(StoreConfig.class.getName()); protected Map _props = null; - public final String[] ALLOWED_PROPS = {"baseURI","encoder","store","data_dir","store","repo_url","solr_connection","elastic_connection"}; + public final String[] ALLOWED_PROPS = {"baseURI","encoder","store","data_dir","store","repo_url","solr_connection","elastic_connection", "public_collections", "default_collection_name", "admin"}; protected AnnotationUtils _annotationUtils = null; + protected List _authTargets = null; public StoreConfig() { _props = null; @@ -73,8 +79,53 @@ public void init(final ServletConfig pConfig) throws ServletException { this.overloadConfigFromEnviroment(tProps); _annotationUtils = new AnnotationUtils(this.getRealPath("/contexts"), getEncoder()); + try { + this.loadAuthConfig(this.getServletContext().getResourceAsStream("/WEB-INF/auth.json")); + } catch (IOException tExcpt) { + tExcpt.printStackTrace(); + throw new ServletException("Failed to load auth config file due to: " + tExcpt.getMessage()); + } + // Create admin user if it doesn't exist initConfig(this); - } + } + + protected void loadAuthConfig(final InputStream pConfigFile) throws IOException { + if (pConfigFile == null) { + _authTargets = null; + } else { + Object tObject = JsonUtils.fromInputStream(pConfigFile); + _authTargets = new ArrayList(); + if (tObject instanceof Map) { + Map tSingleConfig = (Map)tObject; + // Don't add local auth to list of OAuth targets + _authTargets.add(new OAuthTarget(tSingleConfig)); + } else { + List> tConfigs = (List>)tObject; + for (Map tConfig : tConfigs) { + _authTargets.add(new OAuthTarget(tConfig)); + } + } + } + } + + // Is auth setup? + public boolean isAuth() { + return _authTargets != null; + } + + + public List getAuthTargets() { + return _authTargets; + } + + public OAuthTarget getAuthTarget(final String pType) { + for (OAuthTarget tTarget : _authTargets) { + if (tTarget.getId().equals(pType)) { + return tTarget; + } + } + return null; // no target found + } public AnnotationUtils getAnnotationUtils() { return _annotationUtils; @@ -84,6 +135,36 @@ public void setAnnotationUtils(final AnnotationUtils pAnnoUtils) { _annotationUtils = pAnnoUtils; } + /** + * Default to true if config not present + */ + public boolean isPublicCollections() { + if (_props.get("public_collections") == null) { + return true; + } else { + return _props.get("public_collections").equals("true"); + } + } + + public String getDefaultCollectionName() { + if (_props.get("default_collection_name") == null) { + return "Default"; + } else { + return _props.get("default_collection_name"); + } + } + + /** + * Returns null if there is no admin configured + */ + public String getAdminEmail() { + return _props.get("admin"); + } + + public File getDataDir() { + return new File(_props.get("data_dir")); + } + public File getRealPath(final String pPath) { try { return new File(super.getServletContext().getRealPath(pPath)); @@ -128,9 +209,13 @@ public String getBaseURI(final HttpServletRequest pReq) { String tServletName = ""; if (pReq.getServletPath().matches(".*/[a-zA-Z0-9.]*$")) { tServletName = pReq.getServletPath().replaceAll("/[a-zA-Z0-9.]*$","").replaceAll("/",""); + if (tServletName.isEmpty()) { + tServletName = pReq.getServletPath().replaceAll("/",""); + } } else { - tServletName = pReq.getServletPath().replaceAll("/",""); + tServletName = pReq.getServletPath().replaceAll("\\/",""); } + for (int i = tURL.length - 1; i >=0 ; i--) { if (tURL[i].equals(tServletName)) { tBase = i; diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/AbstractStoreAdapter.java b/src/main/java/uk/org/llgc/annotation/store/adapters/AbstractStoreAdapter.java index c7dba86b..ae9d43a8 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/AbstractStoreAdapter.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/AbstractStoreAdapter.java @@ -5,24 +5,35 @@ import uk.org.llgc.annotation.store.data.PageAnnoCount; import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.Collection; import uk.org.llgc.annotation.store.data.Canvas; import uk.org.llgc.annotation.store.data.Target; import uk.org.llgc.annotation.store.data.Annotation; import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.IIIFSearchResults; import uk.org.llgc.annotation.store.data.SearchQuery; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.stats.TopLevel; import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; import uk.org.llgc.annotation.store.AnnotationUtils; +import java.text.SimpleDateFormat; +import java.text.ParseException; + import com.github.jsonldjava.utils.JsonUtils; import org.apache.jena.query.ReadWrite; +import java.net.URISyntaxException; + import java.io.IOException; import java.util.Map; +import java.util.HashMap; import java.util.List; +import java.util.ArrayList; +import java.util.Date; public abstract class AbstractStoreAdapter implements StoreAdapter { protected static Logger _logger = LogManager.getLogger(AbstractStoreAdapter.class.getName()); @@ -82,6 +93,23 @@ public Annotation updateAnnotation(final Annotation pAnno) throws IOException, M return addAnnotationSafe(pAnno); } + protected String formatDate(final Date pDate) { + SimpleDateFormat tDateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + return tDateFormatter.format(pDate); + } + + protected Date parseDate(final String pDate) { + try { + SimpleDateFormat tDateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + return tDateFormatter.parse(pDate); + } catch (ParseException tExcpt) { + tExcpt.printStackTrace(); + System.err.println("Failed to parse date " + pDate); + return null; + } + } + + protected void addWithins(final Annotation pAnno) throws IOException { List tMissingWithins = pAnno.getMissingWithin(); if (tMissingWithins != null && !tMissingWithins.isEmpty()) { @@ -149,7 +177,7 @@ public String indexManifest(Manifest pManifest) throws IOException { } protected String indexManifest(final String pShortId, final Manifest pManifest) throws IOException { - Manifest tExisting = this.getManifest(pShortId); + Manifest tExisting = this.getManifest(pManifest.getURI()); if (tExisting != null) { if (tExisting.getURI().equals(pManifest.getURI())) { return tExisting.getShortId(); // manifest already indexed @@ -172,22 +200,92 @@ protected String indexManifest(final String pShortId, final Manifest pManifest) pManifest.put("@context", tListContext); }*/ - + if (pManifest.getCanvases().isEmpty()) { + throw new IOException("Failed to load manifest " + pManifest.getURI() + " because it had no pages"); + } return this.indexManifestNoCheck(pShortId, pManifest); } + public User retrieveUser(final User pUser) throws IOException { + User tSavedUser = this.getUser(pUser); + if (tSavedUser == null) { + // first try changing from https to http + try { + String tOriginalId = pUser.getId(); + pUser.setId(pUser.getId().replaceAll("https://","http://")); + tSavedUser = this.getUser(pUser); + if (tSavedUser == null) { + pUser.setId(tOriginalId); + return this.saveUser(pUser); + } else { + return tSavedUser; + } + } catch (URISyntaxException tExcpt) { + _logger.error("Failed to replace http uri with https for " + pUser.getId()); + tExcpt.printStackTrace(); + return this.saveUser(pUser); + } + // overwrite saved user if short ID is out of sync + } else if (!pUser.getShortId().equals(tSavedUser.getShortId()) || !pUser.getName().equals(tSavedUser.getName())) { + return this.saveUser(pUser); + } else { + return tSavedUser; + } + } + + public List getUsers(final String pGroup) throws IOException { + List tGroup = new ArrayList(); + if (pGroup.equals("admin")) { + List tAllUsers = this.getUsers(); + for (User tUser : tAllUsers) { + if (tUser.isAdmin()) { + tGroup.add(tUser); + } + } + } + return tGroup; + } + + public void updateCollection(final Collection pCollection) throws IOException { + this.deleteCollection(pCollection); + this.createCollection(pCollection); + } + + public abstract int getTotalAnnotations(final User pUser) throws IOException; + public abstract int getTotalManifests(final User pUser) throws IOException; + public abstract int getTotalAnnoCanvases(final User pUser) throws IOException; + + public Map getTotalAuthMethods() throws IOException { + Map tStats = new HashMap(); + List tUsers = this.getUsers(); + + for (User tUser : tUsers) { + if (tStats.get(tUser.getAuthenticationMethod()) == null) { + tStats.put(tUser.getAuthenticationMethod(), 1); + } else { + int tCurrent = tStats.get(tUser.getAuthenticationMethod()); + tStats.put(tUser.getAuthenticationMethod(), tCurrent + 1); + } + } + + return tStats; + } + public abstract Manifest getManifestForCanvas(final Canvas pCanvas) throws IOException; public abstract Annotation addAnnotationSafe(final Annotation pJson) throws IOException; public abstract IIIFSearchResults search(final SearchQuery pQuery) throws IOException; protected abstract String indexManifestNoCheck(final String pShortID, final Manifest pManifest) throws IOException; public abstract List getManifests() throws IOException; - public abstract List getSkeletonManifests() throws IOException; + public abstract List getSkeletonManifests(final User pUser) throws IOException; public abstract String getManifestId(final String pShortId) throws IOException; - public abstract Manifest getManifest(final String pShortId) throws IOException; + public abstract Manifest getManifest(final String pId) throws IOException; public abstract Canvas resolveCanvas(final String pShortId) throws IOException; public abstract void storeCanvas(final Canvas pCanvas) throws IOException; + public abstract User getUser(final User pUser) throws IOException; + public abstract User saveUser(final User pUser) throws IOException; + public abstract Annotation getAnnotation(final String pId) throws IOException; protected void begin(final ReadWrite pWrite) { diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/StoreAdapter.java b/src/main/java/uk/org/llgc/annotation/store/adapters/StoreAdapter.java index 1f41a6a4..356f4edb 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/StoreAdapter.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/StoreAdapter.java @@ -3,15 +3,18 @@ import uk.org.llgc.annotation.store.data.PageAnnoCount; import uk.org.llgc.annotation.store.data.SearchQuery; import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.Collection; import uk.org.llgc.annotation.store.data.Annotation; import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.IIIFSearchResults; import uk.org.llgc.annotation.store.data.Canvas; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; import uk.org.llgc.annotation.store.AnnotationUtils; import java.util.List; +import java.util.Map; import java.io.IOException; @@ -29,23 +32,57 @@ public interface StoreAdapter { // CRUD manifests public String indexManifest(final Manifest pManifest) throws IOException; + /** + * Check for any canvases that have been annotated before the Manifest was loaded + * Call after index Manifest + */ + public void linkupOrphanCanvas(final Manifest pManifest) throws IOException; + public List getManifests() throws IOException; - public List getSkeletonManifests() throws IOException; - public String getManifestId(final String pShortId) throws IOException; - public Manifest getManifest(final String pShortId) throws IOException; - public Manifest getManifestForCanvas(final Canvas pCanvasId) throws IOException; + public List getSkeletonManifests(final User pUser) throws IOException; + public String getManifestId(final String pShortId) throws IOException; + public Manifest getManifest(final String pId) throws IOException; + public Manifest getManifestForCanvas(final Canvas pCanvasId) throws IOException; // CRUD canvas public Canvas resolveCanvas(final String pShortId) throws IOException; public void storeCanvas(final Canvas pCanvas) throws IOException; // Search - public IIIFSearchResults search(final SearchQuery pQuery) throws IOException; - public AnnotationList getAnnotationsFromPage(final Canvas pPage) throws IOException; + public IIIFSearchResults search(final SearchQuery pQuery) throws IOException; // TODO + public AnnotationList getAnnotationsFromPage(final User pUser, final Canvas pPage) throws IOException; + + // CRUD users + /** + * This will save the user if it doesnt exist + */ + public User retrieveUser(final User pUser) throws IOException; + /** + * This will get the user using the ID + */ + public User getUser(final User pUser) throws IOException; + public User saveUser(final User pUser) throws IOException; + public User deleteUser(final User pUser) throws IOException; + public List getUsers() throws IOException; + public List getUsers(final String pGroup) throws IOException; + + // CRUD Collections + public Collection createCollection(final Collection pCollection) throws IOException; + public List getCollections(final User pUser) throws IOException; + public Collection getCollection(final String pId) throws IOException; + public void deleteCollection(final Collection pCollection) throws IOException; + public void updateCollection(final Collection pCollection) throws IOException; // Used in ListAnnotations can we get rid? public AnnotationList getAllAnnotations() throws IOException; - public List listAnnoPages() throws IOException; // Stats - public List listAnnoPages(final Manifest pManifest) throws IOException; + // If user is null then all user annotations will be returned + public List listAnnoPages(final Manifest pManifest, final User pUser) throws IOException; // TODO + + // Pass in null user to get total annotations + public int getTotalAnnotations(final User pUser) throws IOException; + public int getTotalManifests(final User pUser) throws IOException; + public int getTotalAnnoCanvases(final User pUser) throws IOException; + public Map getTotalAuthMethods() throws IOException; + } diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/elastic/ElasticStore.java b/src/main/java/uk/org/llgc/annotation/store/adapters/elastic/ElasticStore.java index 6c0cbbf1..6693664c 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/elastic/ElasticStore.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/elastic/ElasticStore.java @@ -5,6 +5,7 @@ import java.net.URISyntaxException; import java.net.URI; +import java.net.SocketTimeoutException; import com.github.jsonldjava.utils.JsonUtils; @@ -14,10 +15,13 @@ import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.IIIFSearchResults; import uk.org.llgc.annotation.store.data.Annotation; +import uk.org.llgc.annotation.store.data.Collection; import uk.org.llgc.annotation.store.data.AnnoListNav; import uk.org.llgc.annotation.store.data.Body; import uk.org.llgc.annotation.store.data.Target; import uk.org.llgc.annotation.store.data.SearchQuery; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.users.LocalUser; import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; import uk.org.llgc.annotation.store.adapters.StoreAdapter; @@ -90,7 +94,7 @@ public class ElasticStore extends AbstractStoreAdapter implements StoreAdapter { protected RestHighLevelClient _client = null; protected String _index = ""; - protected RefreshPolicy _policy = RefreshPolicy.NONE; + protected RefreshPolicy _policy = RefreshPolicy.WAIT_UNTIL; // http://host:port public ElasticStore(final String pConnectionURL) throws URISyntaxException, IOException { @@ -132,96 +136,138 @@ public static RestHighLevelClient buildClient(final URI pConnectionURL) { } protected void createIndex() throws IOException { - if (!_client.indices().exists(new GetIndexRequest(_index), RequestOptions.DEFAULT)) { - // Index doesn't exist so create it with mapping - CreateIndexRequest tRequest = new CreateIndexRequest(_index); - - XContentBuilder tMapping = XContentFactory.jsonBuilder() - .startObject() - .startObject("properties") - .startObject("id") - .field("type", "keyword") - .endObject() - .startObject("type") - .field("type", "keyword") - .endObject() - .startObject("created") - .field("type", "date") - .endObject() - .startObject("modified") - .field("type", "date") - .endObject() - .startObject("motivation") - .field("type", "keyword") - .endObject() - .startObject("body") - .field("type", "text") - .endObject() - .startObject("target") - .startObject("properties") - .startObject("id") - .field("type", "keyword") - .endObject() - .startObject("type") - .field("type", "keyword") - .endObject() - .startObject("short_id") - .field("type", "keyword") - .endObject() - .startObject("within") - .startObject("properties") - .startObject("id") - .field("type", "keyword") - .endObject() - .startObject("type") - .field("type", "keyword") - .endObject() - .startObject("label") - .field("type", "text") + try { + if (!_client.indices().exists(new GetIndexRequest(_index), RequestOptions.DEFAULT)) { + // Index doesn't exist so create it with mapping + CreateIndexRequest tRequest = new CreateIndexRequest(_index); + + XContentBuilder tMapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("properties") + .startObject("id") + .field("type", "keyword") + .endObject() + .startObject("type") + .field("type", "keyword") + .endObject() + .startObject("creator") + .field("type", "keyword") + .endObject() + .startObject("created") + .field("type", "date") + .endObject() + .startObject("modified") + .field("type", "date") + .endObject() + .startObject("motivation") + .field("type", "keyword") + .endObject() + .startObject("body") + .field("type", "text") + .endObject() + .startObject("target") + .startObject("properties") + .startObject("id") + .field("type", "keyword") + .endObject() + .startObject("type") + .field("type", "keyword") + .endObject() + .startObject("short_id") + .field("type", "keyword") + .endObject() + .startObject("within") + .startObject("properties") + .startObject("id") + .field("type", "keyword") + .endObject() + .startObject("type") + .field("type", "keyword") + .endObject() + .startObject("label") + .field("type", "text") + .endObject() .endObject() .endObject() .endObject() .endObject() - .endObject() - // Manifest - .startObject("json") - .field("type", "object") - .field("enabled", "false") - .endObject() - .startObject("short_id") - .field("type", "keyword") - .endObject() - .startObject("label") - .field("type", "text") - .endObject() - .startObject("canvases") - .field("type", "object") - .startObject("properties") - .startObject("id") - .field("type", "keyword") - .endObject() - .startObject("type") - .field("type", "keyword") - .endObject() - .startObject("short_id") - .field("type", "keyword") + // Manifest + .startObject("json") + .field("type", "object") + .field("enabled", "false") + .endObject() + .startObject("short_id") + .field("type", "keyword") + .endObject() + .startObject("label") + .field("type", "text") + .endObject() + .startObject("canvases") + .field("type", "object") + .startObject("properties") + .startObject("id") + .field("type", "keyword") + .endObject() + .startObject("type") + .field("type", "keyword") + .endObject() + .startObject("short_id") + .field("type", "keyword") + .endObject() + .startObject("label") + .field("type", "text") + .endObject() .endObject() - .startObject("label") - .field("type", "text") + .endObject() + // User + .startObject("name") + .field("type", "text") + .endObject() + .startObject("email") + .field("type", "text") + .endObject() + .startObject("picture") + .field("type", "keyword") + .endObject() + .startObject("password") + .field("type", "keyword") + .endObject() + .startObject("group") + .field("type", "keyword") + .endObject() + .startObject("authenticationMethod") + .field("type", "keyword") + .endObject() + .startObject("members") + .field("type", "object") + .startObject("properties") + .startObject("id") + .field("type", "keyword") + .endObject() + .startObject("type") + .field("type", "keyword") + .endObject() + .startObject("label") + .field("type", "text") + .endObject() .endObject() .endObject() .endObject() - .endObject() - .endObject(); - tRequest.mapping(tMapping); + .endObject(); + tRequest.mapping(tMapping); - _client.indices().create(tRequest, RequestOptions.DEFAULT); + _client.indices().create(tRequest, RequestOptions.DEFAULT); + } + } catch (SocketTimeoutException tExcpt) { + _logger.error("Failed to connect to Elastic search instance: " + _index); + throw new IOException("Failed to connect to Elastic search instance: " + _index); } } // id, motivation, body, target, selector, within, data, short_id, label public Annotation addAnnotationSafe(final Annotation pAnno) throws IOException { + _logger.debug("addAnnotationSafe"); IndexRequest tIndex = new IndexRequest(_index); tIndex.id(pAnno.getId()); @@ -242,6 +288,9 @@ protected Map anno2json(final Annotation pAnno) { tJson.put("created", pAnno.getCreated()); tJson.put("modified", pAnno.getModified()); tJson.put("motivation", pAnno.getMotivations()); + if (pAnno.getCreator() != null && !pAnno.getCreator().isAdmin()) { + tJson.put("creator", pAnno.getCreator().getId()); + } List tBodies = new ArrayList(); for (Body tBody : pAnno.getBodies()) { @@ -276,6 +325,7 @@ protected Map anno2json(final Annotation pAnno) { } public void deleteAnnotation(final String pAnnoId) throws IOException { + _logger.debug("deleteAnnotation"); DeleteRequest tDelete = new DeleteRequest(_index); tDelete.id(pAnnoId); @@ -283,11 +333,19 @@ public void deleteAnnotation(final String pAnnoId) throws IOException { _client.delete(tDelete, RequestOptions.DEFAULT); } - public AnnotationList getAnnotationsFromPage(final Canvas pPage) throws IOException { + public AnnotationList getAnnotationsFromPage(final User pUser, final Canvas pPage) throws IOException { + _logger.debug("getAnnotationsFromPage"); + BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); + tBuilder.must(QueryBuilders.termQuery("target.id", pPage.getId())); + + if (!pUser.isAdmin()) { + tBuilder.must(QueryBuilders.termQuery("creator", pUser.getId())); + } + AnnotationList tList = new AnnotationList(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.size(10000); - searchSourceBuilder.query(QueryBuilders.termQuery("target.id", pPage.getId())); + searchSourceBuilder.query(tBuilder); SearchRequest searchRequest = new SearchRequest(_index); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); @@ -298,6 +356,7 @@ public AnnotationList getAnnotationsFromPage(final Canvas pPage) throws IOExcept } public Annotation getAnnotation(final String pId) throws IOException { + _logger.debug("getAnnotation"); GetRequest tRequest = new GetRequest(_index, pId); GetResponse tResponse = _client.get(tRequest, RequestOptions.DEFAULT); if (tResponse != null && tResponse.getSource() != null) { @@ -309,6 +368,7 @@ public Annotation getAnnotation(final String pId) throws IOException { public AnnotationList getAllAnnotations() throws IOException { + _logger.debug("getAllAnnotations"); AnnotationList tList = new AnnotationList(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery("type", "oa:Annotation")); @@ -325,16 +385,18 @@ public AnnotationList getAllAnnotations() throws IOException { } public void linkupOrphanCanvas(final Manifest pManifest) throws IOException { + _logger.debug("linkupOrphanCanvas"); for (Canvas tCanvas : pManifest.getCanvases()) { BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); tBuilder.mustNot(QueryBuilders.existsQuery("target.within.id")); - tBuilder.must(QueryBuilders.matchQuery("target.id", tCanvas.getId())); + tBuilder.must(QueryBuilders.termQuery("target.id", tCanvas.getId())); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(tBuilder); searchSourceBuilder.size(10000); SearchRequest searchRequest = new SearchRequest(_index); searchRequest.source(searchSourceBuilder); + _logger.debug("callingSearch"); SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); SearchHits hits = searchResponse.getHits(); SearchHit[] searchHits = hits.getHits(); @@ -359,42 +421,62 @@ public void linkupOrphanCanvas(final Manifest pManifest) throws IOException { } protected String indexManifestNoCheck(final String pShortId, final Manifest pManifest) throws IOException { + _logger.debug("indexManifestNoCheck"); pManifest.setShortId(pShortId); IndexRequest tIndex = new IndexRequest(_index); tIndex.id(pShortId); tIndex.source(manifest2Json(pManifest)); + RefreshPolicy tGeneralPolicy = _policy; + _policy = RefreshPolicy.NONE; - tIndex.setRefreshPolicy(_policy); - _client.index(tIndex, RequestOptions.DEFAULT); - this.linkupOrphanCanvas(pManifest); + //this.linkupOrphanCanvas(pManifest); for (Canvas tCanvas : pManifest.getCanvases()) { this.storeCanvas(tCanvas); } + + _policy = tGeneralPolicy; + tIndex.setRefreshPolicy(_policy); + _client.index(tIndex, RequestOptions.DEFAULT); return pShortId; } public String getManifestId(final String pShortId) throws IOException { - Manifest tManifest = this.getManifest(pShortId); - if (tManifest != null) { + _logger.debug("getManifestId"); + GetRequest tRequest = new GetRequest(_index, pShortId); + GetResponse tResponse = _client.get(tRequest, RequestOptions.DEFAULT); + + if (tResponse != null && tResponse.isExists()) { + Manifest tManifest = json2Manifest(tResponse.getSourceAsMap()); return tManifest.getURI(); } else { return null; - } + } } - public Manifest getManifest(final String pShortId) throws IOException { - GetRequest tRequest = new GetRequest(_index, pShortId); - GetResponse tResponse = _client.get(tRequest, RequestOptions.DEFAULT); - - if (tResponse != null && tResponse.isExists()) { - return json2Manifest(tResponse.getSourceAsMap()); + public Manifest getManifest(final String pId) throws IOException { + _logger.debug("getManifest"); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.termQuery("id", pId)); + searchSourceBuilder.size(1); + SearchRequest searchRequest = new SearchRequest(_index); + searchRequest.source(searchSourceBuilder); + SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); + SearchHits hits = searchResponse.getHits(); + SearchHit[] searchHits = hits.getHits(); + if (searchHits != null && searchHits.length > 0) { + Manifest tManifest = null; + for (SearchHit hit : searchHits) { + tManifest = json2Manifest(hit.getSourceAsMap()); + } + return tManifest; } else { return null; } } public Manifest getManifestForCanvas(final Canvas pCanvas) throws IOException { + _logger.debug("getManifestCanvas"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery("canvases.id", pCanvas.getId())); SearchRequest searchRequest = new SearchRequest(_index); @@ -410,6 +492,7 @@ public Manifest getManifestForCanvas(final Canvas pCanvas) throws IOException { } public List getManifests() throws IOException { + _logger.debug("getManifests"); List tManifests = new ArrayList(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.size(10000); @@ -427,9 +510,13 @@ public List getManifests() throws IOException { } // TODO note this will return indexed manifests as well as non indexed.. - public List getSkeletonManifests() throws IOException { + public List getSkeletonManifests(final User pUser) throws IOException { + _logger.debug("getSkeletonManifests"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.aggregation(AggregationBuilders.terms("manifests").field("target.within.id").size(10000)); + if (!pUser.isAdmin()) { + searchSourceBuilder.query(QueryBuilders.termQuery("creator", pUser.getId())); + } searchSourceBuilder.size(0); SearchRequest searchRequest = new SearchRequest(_index); searchRequest.source(searchSourceBuilder); @@ -446,6 +533,7 @@ public List getSkeletonManifests() throws IOException { } public void storeCanvas(final Canvas pCanvas) throws IOException { + _logger.debug("storeCanvas: " + pCanvas.getId()); IndexRequest tIndex = new IndexRequest(_index); tIndex.id(pCanvas.getShortId()); Map tJson = pCanvas.toJson(); @@ -453,11 +541,14 @@ public void storeCanvas(final Canvas pCanvas) throws IOException { tIndex.source(tJson); tIndex.setRefreshPolicy(_policy); + _logger.debug("Index"); _client.index(tIndex, RequestOptions.DEFAULT); + _logger.debug("done"); } public Canvas resolveCanvas(final String pShortId) throws IOException { + _logger.debug("resolveCanvas"); GetRequest tRequest = new GetRequest(_index, pShortId); GetResponse tResponse = _client.get(tRequest, RequestOptions.DEFAULT); @@ -495,9 +586,24 @@ protected Manifest json2Manifest(final Map pJson) throws IOExcep } public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { + _logger.debug("search"); BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); if (pQuery.getMotivations() != null && !pQuery.getMotivations().isEmpty()) { tBuilder.must(QueryBuilders.termsQuery("motivation", pQuery.getMotivations())); + } + if (pQuery.getUsers() != null && !pQuery.getUsers().isEmpty()) { + List tUserIds = new ArrayList(); + boolean tFoundAdmin = false; + for (User tUser : pQuery.getUsers()) { + tUserIds.add(tUser.getId()); + if (tUser.isAdmin()) { + tFoundAdmin = true; + } + } + if (!tFoundAdmin) { + tBuilder.must(QueryBuilders.termsQuery("creator", tUserIds)); + // if we found an admin then they can access all results + } } tBuilder.must(QueryBuilders.termQuery("target.within.id", pQuery.getScope())); if (pQuery.getQuery() != null && !pQuery.getQuery().isEmpty()) { @@ -515,8 +621,7 @@ public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { SearchHits hits = tResponse.getHits(); try { - IIIFSearchResults tAnnoList = new IIIFSearchResults(); - tAnnoList.setId(pQuery.toURI().toString()); + IIIFSearchResults tAnnoList = new IIIFSearchResults(pQuery.toURI()); long tResultNo = hits.getTotalHits().value; int tNumberOfPages = (int)(tResultNo / pQuery.getResultsPerPage()); @@ -565,10 +670,15 @@ public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { } } - - public List listAnnoPages(final Manifest pManifest) throws IOException { + public List listAnnoPages(final Manifest pManifest, final User pUser) throws IOException { + _logger.debug("listAnnoPages"); + BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); + tBuilder.must(QueryBuilders.termQuery("target.within.id", pManifest.getURI())); + if (pUser != null) { + tBuilder.must(QueryBuilders.termQuery("creator", pUser.getId())); + } SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.termQuery("target.within.id", pManifest.getURI())); + searchSourceBuilder.query(tBuilder); searchSourceBuilder.aggregation(AggregationBuilders.terms("pages").field("target.id").size(10000)); searchSourceBuilder.size(0); SearchRequest searchRequest = new SearchRequest(_index); @@ -582,29 +692,325 @@ public List listAnnoPages(final Manifest pManifest) throws IOExce tLabel = pManifest.getCanvas(tFacet.getKeyAsString()).getLabel(); } Canvas tCanvas = new Canvas(tFacet.getKeyAsString(), tLabel); - this.storeCanvas(tCanvas); + //this.storeCanvas(tCanvas); tAnnoPageCount.add(new PageAnnoCount(tCanvas, (int)tFacet.getDocCount(), pManifest)); } return tAnnoPageCount; } - public List listAnnoPages() throws IOException { + public List getUsers() throws IOException { + _logger.debug("getUsers"); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.aggregation(AggregationBuilders.terms("pages").field("target.id").size(10000)); - searchSourceBuilder.size(0); + searchSourceBuilder.query(QueryBuilders.termQuery("type", "User")); + searchSourceBuilder.size(10000); SearchRequest searchRequest = new SearchRequest(_index); searchRequest.source(searchSourceBuilder); SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); - Terms tFacets = searchResponse.getAggregations().get("pages"); - List tAnnoPageCount = new ArrayList(); - for (Bucket tFacet : tFacets.getBuckets()) { - String tLabel = ""; - Canvas tCanvas = new Canvas(tFacet.getKeyAsString(), tLabel); - this.storeCanvas(tCanvas); - tAnnoPageCount.add(new PageAnnoCount(tCanvas, (int)tFacet.getDocCount(), null)); + SearchHits hits = searchResponse.getHits(); + SearchHit[] searchHits = hits.getHits(); + + List tUsers = new ArrayList(); + for (SearchHit tHit : searchHits) { + tUsers.add(this.json2user((Map)tHit.getSourceAsMap())); } + return tUsers; + } - return tAnnoPageCount; - } + public User getUser(final User pUser) throws IOException { + _logger.debug("getUser"); + GetRequest tRequest = new GetRequest(_index, pUser.getId()); + GetResponse tResponse = _client.get(tRequest, RequestOptions.DEFAULT); + + if (tResponse != null && tResponse.getSource() != null) { + User tSavedUser = this.json2user((Map)tResponse.getSourceAsMap()); + tSavedUser.setToken(pUser.getToken()); + + return tSavedUser; + } else { + return null; + } + } + public User saveUser(final User pUser) throws IOException { + _logger.debug("saveUser"); + User tSavedUser = getUser(pUser); + if (tSavedUser != null) { + // This is an update + pUser.setCreated(tSavedUser.getCreated()); + pUser.updateLastModified(); + } + IndexRequest tIndex = new IndexRequest(_index); + tIndex.id(pUser.getId()); + Map tJson = this.user2json(pUser); + tIndex.source(tJson); + + tIndex.setRefreshPolicy(_policy); + _client.index(tIndex, RequestOptions.DEFAULT); + + return pUser; + } + + public User deleteUser(final User pUser) throws IOException { + _logger.debug("deleteUser"); + DeleteRequest tDelete = new DeleteRequest(_index); + tDelete.id(pUser.getId()); + + tDelete.setRefreshPolicy(_policy); + _client.delete(tDelete, RequestOptions.DEFAULT); + return pUser; + } + + protected User json2user(final Map tUserJson) throws IOException { + User tSavedUser = new User(); + if (tUserJson.get("authenticationMethod").equals(LocalUser.AUTH_METHOD)) { + tSavedUser = new LocalUser(); + if (tUserJson.get("password") != null && !((String)tUserJson.get("password")).isEmpty()) { + ((LocalUser)tSavedUser).setPassword((String)tUserJson.get("password"), false); + } + } + try { + tSavedUser.setId((String)tUserJson.get("id")); + } catch (URISyntaxException tExcpt) { + throw new IOException("Unable to create user as ID was not a URI: " + tExcpt); + } + tSavedUser.setShortId((String)tUserJson.get("short_id")); + tSavedUser.setName((String)tUserJson.get("name")); + tSavedUser.setEmail((String)tUserJson.get("email")); + if (tUserJson.get("created") != null) { + tSavedUser.setCreated(super.parseDate((String)tUserJson.get("created"))); + } + tSavedUser.setLastModified(super.parseDate((String)tUserJson.get("modified"))); + if (tUserJson.get("created") == null && tUserJson.get("modified") != null) { + tSavedUser.setCreated(tSavedUser.getLastModified()); + } + if (tUserJson.get("picture") != null) { + tSavedUser.setPicture((String)tUserJson.get("picture")); + } + if (tUserJson.get("group") != null && tUserJson.get("group").toString().equals("admin")) { + tSavedUser.setAdmin(true); + } + tSavedUser.setAuthenticationMethod((String)tUserJson.get("authenticationMethod")); + + return tSavedUser; + } + + protected Map user2json(final User pUser) { + Map tJson = new HashMap(); + tJson.put("id", pUser.getId()); + tJson.put("type", "User"); + tJson.put("short_id", pUser.getShortId()); + tJson.put("name", pUser.getName()); + tJson.put("email", pUser.getEmail()); + // Elastic search could handle this but better to be explicit on the format + tJson.put("created", super.formatDate(pUser.getCreated())); + tJson.put("modified", super.formatDate(pUser.getLastModified())); + if (pUser instanceof LocalUser) { + tJson.put("password", ((LocalUser)pUser).getPassword()); + } + if (pUser.getPicture() != null && !pUser.getPicture().isEmpty()) { + tJson.put("picture", pUser.getPicture()); + } + if (pUser.isAdmin()) { + tJson.put("group", "admin"); + } + tJson.put("authenticationMethod", pUser.getAuthenticationMethod()); + + return tJson; + } + + protected Map object2json(final Collection pCollection) { + Map tJson = new HashMap(); + tJson.put("id", pCollection.getId()); + tJson.put("type", "Collection"); + tJson.put("short_id", pCollection.getShortId()); + tJson.put("label", pCollection.getLabel()); + tJson.put("creator", pCollection.getUser().getId()); + + List> tMembers = new ArrayList>(); + for (Manifest tManifest : pCollection.getManifests()) { + Map tManifestJson = new HashMap(); + tManifestJson.put("id", tManifest.getURI()); + tManifestJson.put("type", "Manifest"); + tManifestJson.put("label", tManifest.getLabel()); + + tMembers.add(tManifestJson); + } + tJson.put("members", tMembers); + return tJson; + } + + protected Collection collectionFromMap(final Map pJson) throws IOException { + Collection tCollection = new Collection(); + + tCollection.setId((String)pJson.get("id")); + tCollection.setShortId((String)pJson.get("short_id")); + tCollection.setLabel((String)pJson.get("label")); + + User tUser = new User(); + try { + tUser.setId((String)pJson.get("creator")); + } catch (URISyntaxException tExcpt) { + throw new IOException("Unable to create user as the ID is not a valid URI: " + pJson.get("creator")); + } + tCollection.setUser(tUser); + List> tManifests = (List>)pJson.get("members"); + for (Map tManifestJson : tManifests) { + Manifest tManifest = new Manifest(); + tManifest.setURI((String)tManifestJson.get("id")); + tManifest.setLabel((String)tManifestJson.get("label")); + tCollection.add(tManifest); + } + + return tCollection; + } + + public Collection createCollection(final Collection pCollection) throws IOException { + _logger.debug("createCollection"); + IndexRequest tIndex = new IndexRequest(_index); + tIndex.id(pCollection.getId()); + Map tJson = this.object2json(pCollection); + tIndex.source(tJson); + + tIndex.setRefreshPolicy(_policy); + _client.index(tIndex, RequestOptions.DEFAULT); + + return pCollection; + } + + public List getCollections(final User pUser) throws IOException { + _logger.debug("getCollections(pUser)"); + BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); + tBuilder.must(QueryBuilders.termQuery("type", "Collection")); + tBuilder.must(QueryBuilders.termQuery("creator", pUser.getId())); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(tBuilder); + searchSourceBuilder.size(10000); + + List tCollections = this.getCollections(searchSourceBuilder); + if (tCollections == null) { + tCollections = new ArrayList(); + } else { + // Make sure the fully populated user is added rather than just the ID + for (Collection tCollection : tCollections) { + if (!tCollection.getUser().getId().equals(pUser.getId())) { + throw new IOException("The collections search returned collections not owned by this user. You may need to recreate your index"); + } + tCollection.setUser(pUser); + } + } + return tCollections; + } + + public List getCollections(final SearchSourceBuilder tQuery) throws IOException { + _logger.debug("getCollections(SearchSourceBuilder)"); + SearchRequest searchRequest = new SearchRequest(_index); + searchRequest.source(tQuery); + SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); + SearchHits hits = searchResponse.getHits(); + SearchHit[] searchHits = hits.getHits(); + List tCollections = new ArrayList(); + if (searchHits.length == 0) { + return null; // No user saved + } else { + for (int i = 0; i < searchHits.length; i++) { + tCollections.add(this.collectionFromMap(searchHits[i].getSourceAsMap())); + } + } + + return tCollections; + } + + public Collection getCollection(final String pId) throws IOException { + _logger.debug("getCollection(id)"); + GetRequest tRequest = new GetRequest(_index, pId); + GetResponse tResponse = _client.get(tRequest, RequestOptions.DEFAULT); + + if (tResponse != null && tResponse.getSource() != null && tResponse.getSourceAsMap() != null && tResponse.getSourceAsMap() != null) { + return this.collectionFromMap((Map)tResponse.getSourceAsMap()); + } else { + return null; + } + } + + public void deleteCollection(final Collection pCollection) throws IOException { + _logger.debug("deleteCollection()"); + DeleteRequest tDelete = new DeleteRequest(_index); + tDelete.id(pCollection.getId()); + + tDelete.setRefreshPolicy(_policy); + _client.delete(tDelete, RequestOptions.DEFAULT); + } + + public int getTotalAnnotations(final User pUser) throws IOException { + _logger.debug("getTotalAnnotations()"); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + if (pUser == null) { + searchSourceBuilder.query(QueryBuilders.termQuery("type", "oa:Annotation")); + } else { + BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); + tBuilder.must(QueryBuilders.termQuery("type", "oa:Annotation")); + tBuilder.must(QueryBuilders.termQuery("creator", pUser.getId())); + searchSourceBuilder.query(tBuilder); + } + SearchRequest searchRequest = new SearchRequest(_index); + searchRequest.source(searchSourceBuilder); + SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); + + return Math.toIntExact(searchResponse.getHits().getTotalHits().value); + } + + public int getTotalManifests(final User pUser) throws IOException { + _logger.debug("getTotalManifests()"); + if (pUser == null) { + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.termQuery("type", "sc:Manifest")); + + SearchRequest searchRequest = new SearchRequest(_index); + searchRequest.source(searchSourceBuilder); + SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); + + return Math.toIntExact(searchResponse.getHits().getTotalHits().value); + } else { + List tCollections = this.getCollections(pUser); + List tManifests = new ArrayList(); + for (Collection tColl : tCollections) { + for (Manifest tManifest : tColl.getManifests()) { + if (!tManifests.contains(tManifest)) { + tManifests.add(tManifest.getURI()); + } + } + } + return tManifests.size(); + } + } + + public int getTotalAnnoCanvases(final User pUser) throws IOException { + _logger.debug("getTotalAnnoCanvases()"); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + if (pUser == null) { + searchSourceBuilder.query(QueryBuilders.termQuery("type", "oa:Annotation")); + } else { + BoolQueryBuilder tBuilder = QueryBuilders.boolQuery(); + tBuilder.must(QueryBuilders.termQuery("type", "oa:Annotation")); + tBuilder.must(QueryBuilders.termQuery("creator", pUser.getId())); + searchSourceBuilder.query(tBuilder); + } + SearchRequest searchRequest = new SearchRequest(_index); + searchRequest.source(searchSourceBuilder); + SearchResponse searchResponse = _client.search(searchRequest, RequestOptions.DEFAULT); + + List tCanvases = new ArrayList(); + for (SearchHit tHit : searchResponse.getHits().getHits()) { + List> tTargetCanvas = (List>)tHit.getSourceAsMap().get("target"); + for (Map tCanvasMap : tTargetCanvas) { + String tCanvas = (String)tCanvasMap.get("id"); + if (!tCanvases.contains(tCanvas)) { + tCanvases.add(tCanvas); + } + } + } + + return tCanvases.size(); + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/AbstractRDFStore.java b/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/AbstractRDFStore.java index 9f31c201..3d163e9c 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/AbstractRDFStore.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/AbstractRDFStore.java @@ -4,6 +4,9 @@ import java.util.Map; import java.util.ArrayList; import java.util.HashMap; +import java.util.Date; +import java.text.SimpleDateFormat; +import java.text.ParseException; import java.io.IOException; @@ -19,9 +22,12 @@ import uk.org.llgc.annotation.store.data.Manifest; import uk.org.llgc.annotation.store.data.Canvas; import uk.org.llgc.annotation.store.data.Annotation; +import uk.org.llgc.annotation.store.data.Collection; import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.IIIFSearchResults; import uk.org.llgc.annotation.store.data.AnnoListNav; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.users.LocalUser; import uk.org.llgc.annotation.store.AnnotationUtils; import uk.org.llgc.annotation.store.data.rdf.RDFManifest; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; @@ -31,12 +37,16 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Statement; import org.apache.jena.rdf.model.Literal; +import org.apache.jena.rdf.model.StmtIterator; import org.apache.jena.vocabulary.DCTerms; import org.apache.jena.vocabulary.DC; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; +import org.apache.jena.sparql.vocabulary.FOAF; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.riot.Lang; @@ -52,16 +62,44 @@ import org.apache.jena.query.QuerySolution; import org.apache.jena.query.ResultSet; +import java.net.URISyntaxException; + public abstract class AbstractRDFStore extends AbstractStoreAdapter { protected static Logger _logger = LogManager.getLogger(AbstractRDFStore.class.getName()); + private static final Model m = ModelFactory.createDefaultModel(); + private static final String NS = "http://com.gdmrdigital.sas/#"; + private static final Property PASSWORD = m.createProperty(NS+"password"); + protected AnnotationUtils _annoUtils = null; public AbstractRDFStore(final AnnotationUtils pUtils) { _annoUtils = pUtils; } public Annotation addAnnotationSafe(final Annotation pAnno) throws IOException { - Model tAnno = addAnnotationSafe(pAnno.toJson()); + // As creator isn't in OA add something to the context to handle it correctly. + Map tCreatorContext = new HashMap(); + Map tCreatorType = new HashMap(); + tCreatorType.put("@type", "@id"); + tCreatorContext.put("dcterms", "http://purl.org/dc/terms/"); + tCreatorContext.put("dcterms:creator", tCreatorType); + + Map tAnnoJson = pAnno.toJson(); + if (tAnnoJson.get("@context") instanceof List) { + List tContext = (List)tAnnoJson.get("@context"); + tContext.add(tCreatorContext); + } else { + Object tOrigContext = tAnnoJson.get("@context"); + List tContext = new ArrayList(); + tContext.add(tCreatorContext); + tContext.add(tOrigContext); + + tAnnoJson.put("@context", tContext); + } + + Model tAnno = addAnnotationSafe(tAnnoJson); + //RDFDataMgr.write(System.out, tAnno, Lang.TURTLE); Annotation tAfter = this.convertModel(tAnno); + //System.out.println("Anno after read back " + JsonUtils.toPrettyString(tAfter.toJson())); return tAfter; } @@ -75,19 +113,33 @@ public Annotation getAnnotation(final String pId) throws IOException { protected Annotation convertModel(final Model pModel) throws IOException { Map tJson = _annoUtils.frameAnnotation(pModel, false); + if (tJson.get("http://purl.org/dc/terms/creator") != null) { + tJson.put("dcterms:creator", tJson.get("http://purl.org/dc/terms/creator")); + tJson.remove("http://purl.org/dc/terms/creator"); + } + if (tJson.get("dcterms:creator") instanceof Map) { + Map tCreator = (Map)tJson.get("dcterms:creator"); + tJson.put("dcterms:creator", tCreator.get("@id")); + } + return new Annotation(tJson); } protected abstract Model getNamedModel(final String pContext) throws IOException; protected abstract Model addAnnotationSafe(final Map pJson) throws IOException; - public AnnotationList getAnnotationsFromPage(final Canvas pPage) throws IOException { + public AnnotationList getAnnotationsFromPage(final User pUser, final Canvas pPage) throws IOException { + String tUserTest = ""; + if (!pUser.isAdmin()) { + tUserTest = " ?annoId <" + pUser.getId() + "> ."; + } String tQueryString = "select ?annoId ?graph where {" + " GRAPH ?graph { ?on <" + pPage.getId() + "> ." + + tUserTest + " ?annoId ?on } " + "}"; - // _logger.debug("Query " + tQueryString); + _logger.debug("Query " + tQueryString); QueryExecution tExec = this.getQueryExe(tQueryString); this.begin(ReadWrite.READ); @@ -145,10 +197,16 @@ public List getManifests() throws IOException { return tManifests; } - public List getSkeletonManifests() throws IOException { + public List getSkeletonManifests(final User pUser) throws IOException { + String tUserTest = ""; + if (!pUser.isAdmin()) { + tUserTest = " ?annoId <" + pUser.getId() + "> . "; + } + String tQueryString = "select ?manifestId ?manifestLabel where {" + "GRAPH ?graph { ?on ?pageId ." + " ?annoId ?target . " + + tUserTest + " OPTIONAL { ?target ?manifestId } ." + " OPTIONAL { ?manifestId ?manifestLabel }" + "}" + @@ -218,27 +276,33 @@ public String getManifestId(final String pShortId) throws IOException { } - public Manifest getManifest(final String pShortId) throws IOException { - String tManifestURI = this.getManifestId(pShortId); - if (tManifestURI == null || tManifestURI.trim().length() == 0) { - _logger.debug("Manifest URI not found for short id " + pShortId); - return null; - } - Model tModel = this.getNamedModel(tManifestURI); - Manifest tManifest = new RDFManifest(tModel); - tManifest.setURI(tManifestURI); - tManifest.setShortId(pShortId); + public Manifest getManifest(final String pId) throws IOException { + Model tModel = this.getNamedModel(pId); + if (tModel == null) { + return null; + } else { + Manifest tManifest = new RDFManifest(tModel); + tManifest.setURI(pId); - return tManifest; + return tManifest; + } } public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { + String tUserTest = ""; + if (pQuery.getUsers() != null) { + User tUser = pQuery.getUsers().get(0); + if (!tUser.isAdmin()) { + tUserTest = " ?annoId <" + tUser.getId() + "> ."; + } + } String tQueryString = "PREFIX oa: " + "PREFIX cnt: " + "PREFIX dcterms: " + "select ?anno ?content ?graph where { " + " GRAPH ?graph { ?anno oa:hasTarget ?target . " + " ?anno oa:hasBody ?body . " + + tUserTest + " ?target dcterms:isPartOf <" + pQuery.getScope() + "> ." + " ?body <" + Annotation.FULL_TEXT_PROPERTY + "> ?content ." + " FILTER regex(str(?content), \".*" + pQuery.getQuery() + ".*\")" @@ -247,12 +311,14 @@ public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { QueryExecution tExec = this.getQueryExe(tQueryString); - IIIFSearchResults tAnnotationList = new IIIFSearchResults(); this.begin(ReadWrite.READ); List tResults = ResultSetFormatter.toList(tExec.execSelect()); this.end(); + IIIFSearchResults tAnnotationList = null; try { + tAnnotationList = new IIIFSearchResults(pQuery.toURI()); + if (tResults != null) { int tStart = pQuery.getIndex(); int tEnd = tStart + pQuery.getResultsPerPage(); @@ -407,7 +473,7 @@ public Canvas resolveCanvas(final String pShortId) throws IOException { return tCanvas; } - protected abstract void storeCanvas(final String pGraphName, final Model pModel) throws IOException; + protected abstract void storeModel(final String pGraphName, final Model pModel) throws IOException; public void storeCanvas(final Canvas pCanvas) throws IOException { Model tModel = ModelFactory.createDefaultModel(); @@ -418,33 +484,286 @@ public void storeCanvas(final Canvas pCanvas) throws IOException { tModel.add(tModel.createStatement(tCanvasURI, RDFS.label, pCanvas.getLabel())); } - storeCanvas(pCanvas.getId(), tModel); + storeModel(pCanvas.getId(), tModel); } - public List listAnnoPages() { - String tQueryString = "select ?pageId ?manifestId ?manifestLabel ?shortId ?canvasLabel ?canvasShortId (count(?annoId) as ?count) where {" + - "GRAPH ?graph { ?on ?pageId ." + - " ?annoId ?target . " + - " OPTIONAL { ?target ?manifestId }" + + public List getUsers() throws IOException { + // Could optimise this to get all users and their details instead of calling getUser + String tQueryString = "select ?user where {" + + " GRAPH ?user { " + + " ?user_id <" + FOAF.Person + ">" + + " } " + + "}"; + QueryExecution tExec = this.getQueryExe(tQueryString); + this.begin(ReadWrite.READ); + ResultSet results = tExec.execSelect(); // Requires Java 1.7 + this.end(); + List tUsers = new ArrayList(); + if (results != null && results.hasNext()) { + while (results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + Resource tUserResource = soln.getResource("user"); + + // Now get full details of user: + User tUser = new User(); + try { + tUser.setId(tUserResource.getURI()); + } catch (URISyntaxException tExcpt) { + // This shouldn't happen in here + System.err.println("Failed to get user ID: " + tUserResource.getURI() + " as a URI"); + } + tUsers.add(this.getUser(tUser)); + } + } + return tUsers; + } + + public User getUser(final User pUser) throws IOException { + Model tUserModel = getNamedModel(pUser.getId()); + if (tUserModel == null) { + return null; + } else { + this.begin(ReadWrite.READ); + User tSavedUser = new User(); + if (tUserModel.getProperty((Resource)null, FOAF.accountName).getObject().toString().equals(LocalUser.AUTH_METHOD)) { + tSavedUser = new LocalUser(); + } + tSavedUser.setToken(pUser.getToken()); + StmtIterator tStatements = tUserModel.listStatements(); + while (tStatements.hasNext()) { + Statement tStatement = tStatements.nextStatement(); + //System.out.println("User statement " + tStatement.toString()); + if (tStatement.getPredicate().equals(RDF.type) && tStatement.getObject().equals(FOAF.Person)) { + try { + tSavedUser.setId(tStatement.getSubject().getURI()); + } catch (URISyntaxException tExcpt) { + // This shouldn't happen in here + System.err.println("Failed to get user ID as a URI"); + } + } + if (tStatement.getPredicate().equals(DC.identifier)) { + tSavedUser.setShortId(tStatement.getObject().toString()); + } + if (tStatement.getPredicate().equals(FOAF.name)) { + tSavedUser.setName(tStatement.getObject().toString()); + } + if (tStatement.getPredicate().equals(FOAF.mbox)) { + tSavedUser.setEmail(tStatement.getObject().toString()); + } + if (tSavedUser instanceof LocalUser && tStatement.getPredicate().equals(PASSWORD)) { + ((LocalUser)tSavedUser).setPassword(tStatement.getObject().toString(), false); + } + if (tStatement.getPredicate().equals(FOAF.accountName)) { + tSavedUser.setAuthenticationMethod(tStatement.getObject().toString()); + } + if (tStatement.getPredicate().equals(FOAF.img)) { + tSavedUser.setPicture(tStatement.getObject().toString()); + } + if (tStatement.getPredicate().equals(FOAF.member) && tStatement.getSubject().getURI().equals("sas.permissions.admin")) { + tSavedUser.setAdmin(true); + } + if (tStatement.getPredicate().equals(DCTerms.created)) { + tSavedUser.setCreated(parseDate(tStatement.getObject().toString())); + } + if (tStatement.getPredicate().equals(DCTerms.modified)) { + tSavedUser.setLastModified(parseDate(tStatement.getObject().toString())); + } + } + this.end(); + return tSavedUser; + } + } + + protected String formatDate(final Date pDate) { + SimpleDateFormat tDateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + return tDateFormatter.format(pDate); + } + + protected Date parseDate(final String pDate) { + try { + SimpleDateFormat tDateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + return tDateFormatter.parse(pDate); + } catch (ParseException tExcpt) { + tExcpt.printStackTrace(); + System.err.println("Failed to parse date " + pDate); + return null; + } + } + + public User saveUser(final User pUser) throws IOException { + Model tSavedUser = getNamedModel(pUser.getId()); + this.begin(ReadWrite.READ); + if (tSavedUser != null) { + // This is an update + pUser.updateLastModified(); + /*System.out.println("**** Updateding created ******"); + RDFDataMgr.write(System.out, tSavedUser, Lang.TRIG) ; + System.out.println("**** Saved user ******" + tSavedUser.listStatements().toList());*/ + Statement tDateStatement = tSavedUser.getProperty(tSavedUser.createResource(pUser.getId()), DCTerms.created); + if (tDateStatement != null && tDateStatement.getObject() != null) { + pUser.setCreated(parseDate(tDateStatement.getObject().toString())); + } + this.end(); + this.deleteAnnotation(pUser.getId()); + this.begin(ReadWrite.READ); + } + this.end(); + Model tModel = ModelFactory.createDefaultModel(); + Resource tPersonURI = tModel.createResource(pUser.getId()); + tModel.add(tModel.createStatement(tPersonURI, RDF.type, FOAF.Person)); + tModel.add(tModel.createStatement(tPersonURI, DC.identifier, pUser.getShortId())); + tModel.add(tModel.createStatement(tPersonURI, FOAF.name, pUser.getName())); + tModel.add(tModel.createStatement(tPersonURI, FOAF.mbox, pUser.getEmail())); + tModel.add(tModel.createStatement(tPersonURI, DCTerms.created, formatDate(pUser.getCreated()))); + tModel.add(tModel.createStatement(tPersonURI, DCTerms.modified, formatDate(pUser.getLastModified()))); + if (pUser.getPicture() != null && !pUser.getPicture().isEmpty()) { + tModel.add(tModel.createStatement(tPersonURI, FOAF.img, pUser.getPicture())); + } + if (pUser instanceof LocalUser && ((LocalUser)pUser).hasPassword()) { + tModel.add(tModel.createStatement(tPersonURI, PASSWORD, ((LocalUser)pUser).getPassword())); + } + + Resource tAccount = tModel.createResource(); + tModel.add(tModel.createStatement(tPersonURI, FOAF.account, tAccount)); + tModel.add(tModel.createStatement(tAccount, RDF.type, FOAF.OnlineAccount)); + tModel.add(tModel.createStatement(tAccount, FOAF.accountName, pUser.getAuthenticationMethod())); + + if (pUser.isAdmin()) { + Resource tAdminGroup = tModel.createResource("sas.permissions.admin"); + tModel.add(tModel.createStatement(tAdminGroup, FOAF.member, tPersonURI)); + tModel.add(tModel.createStatement(tAdminGroup, RDF.type, FOAF.Group)); + } + + this.storeModel(pUser.getId(), tModel); + + return pUser; + } + + public User deleteUser(final User pUser) throws IOException { + this.deleteAnnotation(pUser.getId()); + return pUser; + } + + public Collection createCollection(final Collection pCollection) throws IOException { + while (true) { + if (this.getNamedModel(pCollection.getId()) != null){ + pCollection.setId(pCollection.getId() + "1"); + System.out.println("Found model trying " + pCollection.getId()); + } else { + break; // Id is unique + } + } + Model tResult = addAnnotationSafe(pCollection.toJson()); + return new Collection(_annoUtils.frameCollection(tResult)); + } + + + public List getCollections(final User pUser) throws IOException { + String tQueryString = "select ?collectionId where {" + + "GRAPH ?collectionId { " + + " ?coll . " + + " ?coll <" + pUser.getId() + "> " + "}" + - "OPTIONAL {GRAPH ?manifestId {" + - " ?manifestId ?manifestLabel ." + - " ?manifestId ?shortId ." + - " ?pageId ?canvasLabel " + - " }}" + - "OPTIONAL { GRAPH ?{ " + - " ?canvas ?canvasShortId ." + - " ?canvas " + - " }}" + - "}group by ?pageId ?manifestId ?manifestLabel ?shortId ?canvasLabel ?canvasShortId order by ?pageId"; + "}"; QueryExecution tExec = this.getQueryExe(tQueryString); - return listAnnoPagesQuery(tExec, null); + this.begin(ReadWrite.READ); + ResultSet results = tExec.execSelect(); // Requires Java 1.7 + this.end(); + List tCollections = new ArrayList(); + if (results != null) { + while (results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + Resource tCollectionResource = soln.getResource("collectionId"); + tCollections.add(this.getCollection(tCollectionResource.getURI())); + } + } + return tCollections; } - public List listAnnoPages(final Manifest pManifest) { + protected void walkList(final Model pModel, final Resource pKey, final List pResults) { + org.apache.jena.rdf.model.RDFNode tNode = null; + StmtIterator tStatements = pModel.listStatements(pKey, null, tNode); + while (tStatements.hasNext()) { + Statement tStatement = tStatements.nextStatement(); + if (tStatement.getPredicate().equals(RDF.first)) { + System.out.println("Adding " + tStatement.toString()); + if (!pResults.contains(tStatement.getObject().toString())) { + pResults.add(tStatement.getObject().toString()); + } + } + if (tStatement.getPredicate().equals(RDF.rest)) { + System.out.println("Looping on " + tStatement.toString()); + walkList(pModel, tStatement.getObject().asResource(),pResults); + } + } + } + + public Collection getCollection(final String pId) throws IOException { + Model tCollModel = this.getNamedModel(pId); + if (tCollModel != null) { + this.begin(ReadWrite.READ); + org.apache.jena.rdf.model.RDFNode tNode = null; + StmtIterator tStatements = tCollModel.listStatements(null, tCollModel.createProperty("http://iiif.io/api/presentation/2#", "hasManifests"), tNode); + List tManifests = new ArrayList(); + while (tStatements.hasNext()) { + tManifests.add(tStatements.nextStatement()); + } + this.end(); + Collection tCollection = null; + if (tManifests.size() > 1) { + System.out.println("***************************************************"); + System.out.println("*** somehow got multiple hasManifests ***"); + System.out.println("***************************************************"); + // If we've reached here then somehow we have two sets of hasManifests which means the framing will break + List tManifestsIds = new ArrayList(); + for (Statement tStatement : tManifests) { + walkList(tCollModel, tStatement.getObject().asResource(), tManifestsIds); + } + + this.begin(ReadWrite.WRITE); + tCollModel.removeAll(null, tCollModel.createProperty("http://iiif.io/api/presentation/2#", "hasManifests"), tNode); + tCollModel.removeAll(null, tCollModel.createProperty("http://iiif.io/api/presentation/2#", "hasParts"), tNode); + tCollModel.commit(); + + tCollection = new Collection(_annoUtils.frameCollection(tCollModel)); + for (String tURI : tManifestsIds) { + Manifest tManifest = new Manifest(); + tManifest.setURI(tURI); + + StmtIterator tStatementsIter = tCollModel.listStatements(tCollModel.createResource(tURI), RDFS.label, tNode); + if (tStatementsIter != null && tStatementsIter.hasNext()) { + tManifest.setLabel(tStatementsIter.next().getObject().toString()); + } + + tCollection.add(tManifest); + } + this.updateCollection(tCollection); + return tCollection; + } else { + tCollection = new Collection(_annoUtils.frameCollection(tCollModel)); + } + + return tCollection; + + } else { + return null; + } + } + + public void deleteCollection(final Collection pCollection) throws IOException { + this.deleteAnnotation(pCollection.getId()); + } + + public List listAnnoPages(final Manifest pManifest, final User pUser) { + String tUserTest = ""; + if (pUser != null) { + tUserTest = " ?annoId <" + pUser.getId() + "> ."; + } + String tQueryString = "select ?pageId ?canvasLabel ?canvasShortId (count(?annoId) as ?count) where {" + "GRAPH ?graph { ?on ?pageId ." + " ?annoId ?target . " + + tUserTest + " ?target <" + pManifest.getURI() + "> " + "}" + "OPTIONAL {GRAPH <" + pManifest.getURI() + "> {" + @@ -557,7 +876,12 @@ protected String indexManifestNoCheck(final String pShortId, Manifest pManifest) pManifest.setShortId(pShortId); pManifest.toJson().put(DC.identifier.getURI(), pShortId); String tShortId = this.indexManifestOnly(pShortId, pManifest.toJson()); - // Now update any annotations which don't contain a link to this manifest. + + return tShortId; + } + + public void linkupOrphanCanvas(final Manifest pManifest) throws IOException { + // Now update any annotations which don't contain a link to this manifest. String tQueryString = "PREFIX oa: " + "PREFIX sc: " + "PREFIX rdf: " + @@ -614,6 +938,121 @@ protected String indexManifestNoCheck(final String pShortId, Manifest pManifest) // found no annotations that weren't linked to this manifest } - return tShortId; - } + } + + public int getTotalAnnotations(final User pUser) { + StringBuffer sparql = new StringBuffer("PREFIX rdf: \n"); + sparql.append("select (count(distinct ?anno) as ?count) where { \n"); + sparql.append("\tGRAPH ?graph {\n"); + sparql.append("\t\t?anno rdf:type .\n"); + if (pUser != null) { + sparql.append("\t\t?anno <"); + sparql.append(pUser.getId()); + sparql.append("> .\n"); + } + sparql.append("\t\tFILTER NOT EXISTS {?canvas rdf:first ?anno}"); + sparql.append("\t}"); + sparql.append("}"); + + QueryExecution tExec = this.getQueryExe(sparql.toString().replaceAll("[\\n\\t]", "")); + this.begin(ReadWrite.READ); + ResultSet results = tExec.execSelect(); // Requires Java 1.7 + this.end(); + if (results != null && results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + + return soln.getLiteral("count").getInt(); + } + return 0; + } + + public int getTotalManifests(final User pUser) { + StringBuffer sparql = new StringBuffer("PREFIX rdf: \n"); + if (pUser == null) { + sparql.append("select (count(distinct ?manifest) as ?count) where { \n"); + sparql.append("\tGRAPH ?graph {\n"); + sparql.append("\t\t?manifest rdf:type .\n"); + sparql.append("\t}"); + sparql.append("}"); + } else { + sparql.append("select (count(distinct ?manifest) as ?count) where {\n"); + sparql.append("\tGRAPH ?collection {\n"); + sparql.append("\t\t?coll .\n"); + sparql.append("\t\t?coll <"); + sparql.append(pUser.getId()); + sparql.append("> .\n"); + sparql.append("\t\t?coll ?list .\n"); + sparql.append("\t\t?list rdf:rest*/rdf:first ?manifest \n"); + sparql.append("\t}"); + sparql.append("}"); + } + + QueryExecution tExec = this.getQueryExe(sparql.toString().replaceAll("[\\n\\t]", "")); + this.begin(ReadWrite.READ); + ResultSet results = tExec.execSelect(); // Requires Java 1.7 + this.end(); + if (results != null && results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + + return soln.getLiteral("count").getInt(); + } + return 0; + } + + public int getTotalAnnoCanvases(final User pUser) { + StringBuffer sparql = new StringBuffer("PREFIX rdf: \n"); + sparql.append("select (count(distinct ?source) as ?count) where { \n"); + sparql.append("\tGRAPH ?graph {\n"); + sparql.append("\t\t?anno rdf:type .\n"); + if (pUser != null) { + sparql.append("\t\t?anno <"); + sparql.append(pUser.getId()); + sparql.append("> .\n"); + } + sparql.append("\t\t?anno ?target .\n"); + sparql.append("\t\t?target ?source\n"); + sparql.append("\t}"); + sparql.append("}"); + + + QueryExecution tExec = this.getQueryExe(sparql.toString().replaceAll("[\\n\\t]", "")); + this.begin(ReadWrite.READ); + ResultSet results = tExec.execSelect(); // Requires Java 1.7 + this.end(); + if (results != null && results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + + return soln.getLiteral("count").getInt(); + } + return 0; + + } + public Map getTotalAuthMethods() { + StringBuffer sparql = new StringBuffer("PREFIX rdf: \n"); + sparql.append("select ?accountType (count(distinct ?accountType) as ?count) where {\n"); + sparql.append("\tGRAPH ?graph {\n"); + sparql.append("\t\t?person rdf:type .\n"); + sparql.append("\t\t?person ?account .\n"); + sparql.append("\t\t?account ?accountType\n"); + sparql.append("\t}"); + sparql.append("} group by ?accountType"); + + + QueryExecution tExec = this.getQueryExe(sparql.toString().replaceAll("[\\n\\t]", "")); + this.begin(ReadWrite.READ); + ResultSet results = tExec.execSelect(); // Requires Java 1.7 + this.end(); + + Map tStats = new HashMap(); + if (results != null) { + while (results.hasNext()) { + QuerySolution soln = results.nextSolution() ; + String tType = soln.getLiteral("accountType").getString(); + int tCount = soln.getLiteral("count").getInt(); + + tStats.put(tType, tCount); + } + } + return tStats; + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/jena/JenaStore.java b/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/jena/JenaStore.java index 90fa5d51..40000bf0 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/jena/JenaStore.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/jena/JenaStore.java @@ -8,6 +8,7 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.JenaTransactionException; import org.apache.jena.riot.RDFDataMgr; import org.apache.jena.riot.Lang; @@ -43,7 +44,7 @@ public JenaStore(final AnnotationUtils pUtils, final String pDataDir) { protected Model addAnnotationSafe(final Map pJson) throws IOException { String tJson = JsonUtils.toString(pJson); - _logger.debug("Converting: " + tJson); + _logger.debug("Converting: " + JsonUtils.toPrettyString(pJson)); Model tJsonLDModel = ModelFactory.createDefaultModel(); RDFDataMgr.read(tJsonLDModel, new ByteArrayInputStream(tJson.getBytes(Charset.forName("UTF-8"))), Lang.JSONLD); @@ -56,7 +57,7 @@ protected Model addAnnotationSafe(final Map pJson) throws IOExcep return tJsonLDModel; } - protected void storeCanvas(final String pGraphName, final Model pModel) throws IOException { + protected void storeModel(final String pGraphName, final Model pModel) throws IOException { _dataset.begin(ReadWrite.WRITE) ; _dataset.addNamedModel(pGraphName, pModel); _dataset.commit(); @@ -87,7 +88,14 @@ protected Model getNamedModel(final String pContext) throws IOException { } protected void begin(final ReadWrite pWrite) { - _dataset.begin(pWrite); + try { + _dataset.begin(pWrite); + } catch (JenaTransactionException tExcpt) { + System.err.println("In transaction so going to try and close it before re-openning"); + tExcpt.printStackTrace(); + this.end(); + _dataset.begin(pWrite); + } } protected void end() { _dataset.end(); diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/sesame/SesameStore.java b/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/sesame/SesameStore.java index 946d7f37..13950cf3 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/sesame/SesameStore.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/rdf/sesame/SesameStore.java @@ -75,7 +75,7 @@ public SesameStore(final AnnotationUtils pUtils, final String pRepositoryURL) { } - protected void storeCanvas(final String pGraphName, final Model pModel) throws IOException { + protected void storeModel(final String pGraphName, final Model pModel) throws IOException { // TODO } diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrManifestStore.java b/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrManifestStore.java index b1cd627f..c769434c 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrManifestStore.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrManifestStore.java @@ -2,6 +2,7 @@ import uk.org.llgc.annotation.store.data.Manifest; import uk.org.llgc.annotation.store.data.Canvas; +import uk.org.llgc.annotation.store.data.users.User; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrDocument; @@ -39,10 +40,10 @@ protected SolrQuery getManifestQuery() { return tQuery; } - public Manifest getManifest(final String pShortId) throws IOException { + public Manifest getManifest(final String pId) throws IOException { SolrQuery tQuery = this.getManifestQuery(); - tQuery.set("q", "short_id:\"" + pShortId + "\""); + tQuery.set("q", "id:" + _utils.escapeChars(pId)); try { QueryResponse tResponse = _solrClient.query(tQuery); @@ -64,7 +65,7 @@ public Manifest getManifest(final String pShortId) throws IOException { } else if (tResponse.getResults().size() == 0) { return null; // no annotation found with supplied id } else { - throw new IOException("Found " + tResponse.getResults().size() + " manifests with ID " + pShortId); + throw new IOException("Found " + tResponse.getResults().size() + " manifests with ID " + pId); } } catch (SolrServerException tException) { throw new IOException("Failed to run solr query due to " + tException.toString()); @@ -116,9 +117,13 @@ public List getManifests() throws IOException { return tManifests; } - public List getSkeletonManifests() throws IOException { + public List getSkeletonManifests(final User pUser) throws IOException { SolrQuery tQuery = this.getManifestQuery(); - tQuery.set("q", "within:*"); + String tUserQuery = ""; + if (!pUser.isAdmin()) { + tUserQuery = " AND creator:\"" + pUser.getId() + "\""; + } + tQuery.set("q", "within:*" + tUserQuery); tQuery.setFacet(true); tQuery.addFacetField("within"); @@ -127,9 +132,11 @@ public List getSkeletonManifests() throws IOException { QueryResponse tResponse = _solrClient.query(tQuery); FacetField tFacetCounts = tResponse.getFacetField("within"); for (FacetField.Count tFacetValue : tFacetCounts.getValues()) { - Manifest tManifest = new Manifest(); - tManifest.setURI(tFacetValue.getName()); - tManifests.add(tManifest); + if (tFacetValue.getCount() > 0) { + Manifest tManifest = new Manifest(); + tManifest.setURI(tFacetValue.getName()); + tManifests.add(tManifest); + } } } catch (SolrServerException tExcpt) { tExcpt.printStackTrace(); diff --git a/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrStore.java b/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrStore.java index 7b06e39f..8de17593 100644 --- a/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrStore.java +++ b/src/main/java/uk/org/llgc/annotation/store/adapters/solr/SolrStore.java @@ -28,10 +28,13 @@ import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.IIIFSearchResults; import uk.org.llgc.annotation.store.data.Annotation; +import uk.org.llgc.annotation.store.data.Collection; import uk.org.llgc.annotation.store.data.AnnoListNav; import uk.org.llgc.annotation.store.data.Body; import uk.org.llgc.annotation.store.data.Target; import uk.org.llgc.annotation.store.data.SearchQuery; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.users.LocalUser; import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; import uk.org.llgc.annotation.store.adapters.StoreAdapter; @@ -95,6 +98,9 @@ public Annotation addAnnotationSafe(final Annotation pAnno) throws IOException { _utils.addMultiple(tDoc, "motivation", pAnno.getMotivations()); tDoc.addField("created", pAnno.getCreated()); tDoc.addField("modified", pAnno.getModified()); + if (pAnno.getCreator() != null) { + tDoc.addField("creator", pAnno.getCreator().getId()); + } for (Body tBody : pAnno.getBodies()) { tDoc.addField("body", tBody.getIndexableContent()); @@ -191,14 +197,13 @@ public List getManifests() throws IOException { return _manifestStore.getManifests(); } - public List getSkeletonManifests() throws IOException { - return _manifestStore.getSkeletonManifests(); + public List getSkeletonManifests(final User pUser) throws IOException { + return _manifestStore.getSkeletonManifests(pUser); } protected String indexManifestNoCheck(final String pShortId, final Manifest pManifest) throws IOException { pManifest.setShortId(pShortId); _manifestStore.indexManifestNoCheck(pManifest); - this.linkupOrphanCanvas(pManifest); return pManifest.getShortId(); } @@ -206,8 +211,8 @@ public String getManifestId(final String pShortId) throws IOException { return _manifestStore.getManifestId(pShortId); } - public Manifest getManifest(final String pShortId) throws IOException { - return _manifestStore.getManifest(pShortId); + public Manifest getManifest(final String pId) throws IOException { + return _manifestStore.getManifest(pId); } public Canvas resolveCanvas(final String pShortId) throws IOException { @@ -284,6 +289,30 @@ public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { tSolrQuery.append(")"); } } + if (pQuery.getUsers() != null && !pQuery.getUsers().isEmpty()) { + StringBuffer tUserQuery = new StringBuffer(); + tUserQuery.append(" AND creator:"); + boolean tFoundAdmin = false; + if (pQuery.getUsers().size() == 1) { + tUserQuery.append("\""); + tUserQuery.append(pQuery.getUsers().get(0).getId()); + tUserQuery.append("\""); + tFoundAdmin = pQuery.getUsers().get(0).isAdmin(); + } else { + tUserQuery.append("("); + for (User tUser : pQuery.getUsers()) { + tUserQuery.append(" "); + tUserQuery.append(tUser.getId()); + if (tUser.isAdmin()) { + tFoundAdmin = true; + } + } + tUserQuery.append(")"); + } + if (!tFoundAdmin) { + tSolrQuery.append(tUserQuery); + } + } tSolrQuery.append(" AND within:\""); tSolrQuery.append(pQuery.getScope()); @@ -296,9 +325,9 @@ public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { tQuery.setHighlight(true); tQuery.addHighlightField("text"); - IIIFSearchResults tAnnoList = new IIIFSearchResults(); + IIIFSearchResults tAnnoList = null; try { - tAnnoList.setId(pQuery.toURI().toString()); + tAnnoList = new IIIFSearchResults(pQuery.toURI()); QueryResponse tResponse = _solrClient.query(tQuery); long tResultNo = tResponse.getResults().getNumFound(); int tNumberOfPages = (int)(tResultNo / pQuery.getResultsPerPage()); @@ -335,9 +364,13 @@ public IIIFSearchResults search(final SearchQuery pQuery) throws IOException { return tAnnoList; } - public AnnotationList getAnnotationsFromPage(final Canvas pPage) throws IOException { + public AnnotationList getAnnotationsFromPage(final User pUser, final Canvas pPage) throws IOException { SolrQuery tQuery = _utils.getQuery(); - tQuery.set("q", "target:" + _utils.escapeChars(pPage.getId())); + String tUserQuery = ""; + if (!pUser.isAdmin()) { + tUserQuery = " AND creator:\"" + pUser.getId() + "\""; + } + tQuery.set("q", "target:" + _utils.escapeChars(pPage.getId()) + tUserQuery); AnnotationList tAnnoList = new AnnotationList(); try { @@ -380,7 +413,12 @@ public Annotation getAnnotation(final String pId) throws IOException { } } - public List listAnnoPages(final Manifest pManifest) throws IOException { + public List listAnnoPages(final Manifest pManifest, final User pUser) throws IOException { + String tUserQuery = ""; + if (pUser != null && !pUser.isAdmin()) { + tUserQuery = " AND creator:\"" + pUser.getId() + "\""; + } + SolrQuery tQuery = new SolrQuery(); tQuery.setRows(0); tQuery.setFacet(true); @@ -388,7 +426,7 @@ public List listAnnoPages(final Manifest pManifest) throws IOExce tQuery.setFacetLimit(-1); tQuery.setFacetMinCount(1); tQuery.setFacetSort("index"); - tQuery.set("q", "type:\"oa:Annotation\" AND within:\"" + pManifest.getURI() + "\""); + tQuery.set("q", "type:\"oa:Annotation\" AND within:\"" + pManifest.getURI() + "\"" + tUserQuery); try { QueryResponse tResponse = _solrClient.query(tQuery); long tTotalAnnos = tResponse.getResults().getNumFound(); @@ -400,7 +438,7 @@ public List listAnnoPages(final Manifest pManifest) throws IOExce tLabel = pManifest.getCanvas(tFacetValue.getName()).getLabel(); } Canvas tCanvas = new Canvas(tFacetValue.getName(), tLabel); - this.storeCanvas(tCanvas); + //this.storeCanvas(tCanvas); tAnnoPageCount.add(new PageAnnoCount(tCanvas, (int)tFacetValue.getCount(), pManifest)); } return tAnnoPageCount; @@ -410,30 +448,6 @@ public List listAnnoPages(final Manifest pManifest) throws IOExce } } - public List listAnnoPages() throws IOException { - SolrQuery tQuery = new SolrQuery(); - tQuery.setRows(0); - tQuery.setFacet(true); - tQuery.addFacetField("target"); - tQuery.setFacetLimit(-1); - tQuery.setFacetSort("index"); - tQuery.set("q", "type:oa\\:Annotation"); - try { - QueryResponse tResponse = _solrClient.query(tQuery); - long tTotalAnnos = tResponse.getResults().getNumFound(); - FacetField tFacetCounts = tResponse.getFacetField("target"); - List tAnnoPageCount = new ArrayList(); - for (FacetField.Count tFacetValue : tFacetCounts.getValues()) { - Canvas tCanvas = new Canvas(tFacetValue.getName(), "");// TODO add manifest and canvas label - tAnnoPageCount.add(new PageAnnoCount(tCanvas, (int)tFacetValue.getCount(), null)); - } - return tAnnoPageCount; - } catch (SolrServerException tExcept) { - tExcept.printStackTrace(); - throw new IOException("Failed to run page count query due to " + tExcept.getMessage()); - } - } - public Annotation buildAnnotation(final SolrDocument pDoc, final boolean pCollapseOn) throws IOException { Map tAnnotation = (Map)JsonUtils.fromString(new String(Base64.getDecoder().decode((String)pDoc.get("data")))); @@ -460,5 +474,300 @@ public List buildAnnotationList(final QueryResponse pResponse, final return tResults; } + public List getUsers() throws IOException { + SolrQuery tQuery = new SolrQuery(); + tQuery.setFields("id", "type", "short_id", "name", "email", "password", "picture", "group", "authenticationMethod", "created", "modified"); + tQuery.setRows(1000); + + tQuery.set("q", "type:\"User\""); + + List tUsers = new ArrayList(); + + try { + QueryResponse tResponse = _solrClient.query(tQuery); + for (SolrDocument pDoc : tResponse.getResults()) { + tUsers.add(json2user(pDoc)); + } + } catch (SolrServerException tException) { + throw new IOException("Failed to run solr query due to " + tException.toString()); + } + return tUsers; + } + + public User getUser(final User pUser) throws IOException { + User tUser = new User(); + + SolrQuery tQuery = new SolrQuery(); + tQuery.setFields("id", "type", "short_id", "name", "email", "password", "picture", "group", "authenticationMethod", "created", "modified"); + tQuery.setRows(1000); + + tQuery.set("q", "type:\"User\" AND id:\"" + pUser.getId() + "\""); + + try { + QueryResponse tResponse = _solrClient.query(tQuery); + + if (tResponse.getResults().size() == 1) { + tUser = json2user(tResponse.getResults().get(0)); + } else if (tResponse.getResults().size() == 0) { + return null; // no annotation found with supplied id + } else { + throw new IOException("Found " + tResponse.getResults().size() + " Users with ID " + pUser.getShortId()); + } + } catch (SolrServerException tException) { + throw new IOException("Failed to run solr query due to " + tException.toString()); + } + tUser.setToken(pUser.getToken()); + return tUser; + } + + public User saveUser(final User pUser) throws IOException { + User tSavedUser = this.getUser(pUser); + if (tSavedUser != null) { + // this is an update + pUser.setCreated(tSavedUser.getCreated()); + pUser.updateLastModified(); + } + SolrInputDocument tDoc = new SolrInputDocument(); + tDoc.addField("id", pUser.getId()); + tDoc.addField("short_id", pUser.getShortId()); + tDoc.addField("name", pUser.getName()); + tDoc.addField("type", "User"); + tDoc.addField("email", pUser.getEmail()); + tDoc.addField("created", pUser.getCreated()); + tDoc.addField("modified", pUser.getLastModified()); + if (pUser instanceof LocalUser) { + tDoc.addField("password", ((LocalUser)pUser).getPassword()); + } + if (pUser.getPicture() != null && !pUser.getPicture().isEmpty()) { + tDoc.addField("picture", pUser.getPicture()); + } + if (pUser.isAdmin()) { + tDoc.addField("group", "admin"); + } + tDoc.addField("authenticationMethod", pUser.getAuthenticationMethod()); + + _utils.addDoc(tDoc, true); + return pUser; + } + + public User deleteUser(final User pUser) throws IOException { + List tOldIds = new ArrayList(); + tOldIds.add(pUser.getId()); + try { + _solrClient.deleteById(tOldIds); + _solrClient.commit(); + } catch(SolrServerException tException) { + tException.printStackTrace(); + throw new IOException("Failed to remove user due to " + tException); + } + return pUser; + } + + protected User json2user(final SolrDocument pDoc) throws IOException { + User tUser = new User(); + if (pDoc.get("authenticationMethod").equals(LocalUser.AUTH_METHOD)) { + tUser = new LocalUser(); + ((LocalUser)tUser).setPassword((String)pDoc.get("password"),false); + } + + try { + tUser.setId((String)pDoc.get("id")); + } catch (URISyntaxException tExcpt) { + throw new IOException("Id is not a URI: " + pDoc.get("id") + "\"" + tExcpt); + } + tUser.setShortId((String)pDoc.get("short_id")); + tUser.setName((String)pDoc.get("name")); + tUser.setEmail(((List)pDoc.get("email")).get(0)); + tUser.setCreated((Date)pDoc.get("created")); + tUser.setLastModified((Date)pDoc.get("modified")); + if (pDoc.get("picture") != null && !((List)pDoc.get("picture")).isEmpty()) { + tUser.setPicture(((List)pDoc.get("picture")).get(0)); + } + if (pDoc.get("group") != null) { + for (String tGroup : (List)pDoc.get("group")) { + if (tGroup.equals("admin")) { + tUser.setAdmin(true); + } + } + } + tUser.setAuthenticationMethod((String)pDoc.get("authenticationMethod")); + + return tUser; + } + + + public Collection createCollection(final Collection pCollection) throws IOException { + SolrInputDocument tDoc = new SolrInputDocument(); + tDoc.addField("id", pCollection.getId()); + tDoc.addField("short_id", pCollection.getShortId()); + tDoc.addField("label", pCollection.getLabel()); + tDoc.addField("type", "Collection"); + tDoc.addField("creator", pCollection.getUser().getId()); + + for (Manifest tManifest: pCollection.getManifests()) { + tDoc.addField("members", tManifest.getURI()); + tDoc.addField("members", tManifest.getLabel()); + } + + _utils.addDoc(tDoc, true); + return pCollection; + } + + public List getCollections(final User pUser) throws IOException { + SolrQuery tQuery = new SolrQuery(); + tQuery.setFields("id", "type", "short_id", "label", "creator", "members"); + tQuery.setRows(1000); + + tQuery.set("q", "type:\"Collection\" AND creator:\"" + pUser.getId() + "\""); + + List tCollections = new ArrayList(); + try { + QueryResponse tResponse = _solrClient.query(tQuery); + + for (SolrDocument pDoc : tResponse.getResults()) { + Collection tCollection = buildCollection(pDoc); + tCollection.setUser(pUser); + tCollections.add(tCollection); + } + } catch (SolrServerException tException) { + throw new IOException("Failed to run create collections query due to " + tException.toString()); + } + + return tCollections; + } + + protected Collection buildCollection(final SolrDocument pCollectionData) throws IOException { + Collection tCollection = new Collection(); + tCollection.setId((String)pCollectionData.get("id")); + tCollection.setShortId((String)pCollectionData.get("short_id")); + tCollection.setLabel(((List)pCollectionData.get("label")).get(0)); + + User tUser = new User(); + try { + tUser.setId(((List)pCollectionData.get("creator")).get(0)); + } catch (URISyntaxException tExcpt) { + throw new IOException("Failed to create user because id wasn't a URI: " + pCollectionData.get("creator")); + } + tCollection.setUser(tUser); + + List tManifestList = ((List)pCollectionData.get("members")); + if (tManifestList != null) { + for (int i = 0; i < tManifestList.size(); i +=2) { + Manifest tManifest = new Manifest(); + tManifest.setURI(tManifestList.get(i)); + tManifest.setLabel(tManifestList.get(i + 1)); + + tCollection.add(tManifest); + } + } + + return tCollection; + } + + public Collection getCollection(final String pId) throws IOException { + SolrQuery tQuery = new SolrQuery(); + tQuery.setFields("id", "type", "short_id", "label", "creator", "members"); + tQuery.setRows(1000); + + tQuery.set("q", "type:\"Collection\" AND id:\"" + pId + "\""); + + try { + QueryResponse tResponse = _solrClient.query(tQuery); + + if (tResponse.getResults().size() == 1) { + return buildCollection(tResponse.getResults().get(0)); + } + } catch (SolrServerException tException) { + throw new IOException("Failed to run create collections query due to " + tException.toString()); + } + + return null; + } + + public void deleteCollection(final Collection pCollection) throws IOException { + List tOldIds = new ArrayList(); + tOldIds.add(pCollection.getId()); + try { + _solrClient.deleteById(tOldIds); + _solrClient.commit(); + } catch(SolrServerException tException) { + tException.printStackTrace(); + throw new IOException("Failed to remove collection due to " + tException); + } + } + + public int getTotalAnnotations(final User pUser) throws IOException { + SolrQuery tQuery = _utils.getQuery(); + StringBuffer tQueryStr = new StringBuffer("type:oa\\:Annotation"); + + if (pUser != null) { + tQueryStr.append(" AND creator:"); + tQueryStr.append(pUser.getId()); + } + tQuery.set("q", tQueryStr.toString()); + + try { + QueryResponse tResponse = _solrClient.query(tQuery); + return Math.toIntExact(tResponse.getResults().getNumFound()); + } catch (SolrServerException tExcpt) { + tExcpt.printStackTrace(); + throw new IOException(tExcpt.getMessage()); + } + } + + public int getTotalManifests(final User pUser) throws IOException { + SolrQuery tQuery = _utils.getQuery(); + if (pUser == null) { + StringBuffer tQueryStr = new StringBuffer(""); + tQuery.set("q", "type:sc\\:Manifest"); + try { + QueryResponse tResponse = _solrClient.query(tQuery); + return Math.toIntExact(tResponse.getResults().getNumFound()); + } catch (SolrServerException tExcpt) { + tExcpt.printStackTrace(); + throw new IOException(tExcpt.getMessage()); + } + } else { + List tCollections = this.getCollections(pUser); + List tManifests = new ArrayList(); + for (Collection tColl : tCollections) { + for (Manifest tManifest : tColl.getManifests()) { + if (!tManifests.contains(tManifest)) { + tManifests.add(tManifest.getURI()); + } + } + } + return tManifests.size(); + } + } + + public int getTotalAnnoCanvases(final User pUser) throws IOException { + SolrQuery tQuery = _utils.getQuery(); + StringBuffer tQueryStr = new StringBuffer("type:oa\\:Annotation"); + + if (pUser != null) { + tQueryStr.append(" AND creator:"); + tQueryStr.append(pUser.getId()); + } + tQuery.set("q", tQueryStr.toString()); + + List tCanvases = new ArrayList(); + try { + QueryResponse tResponse = _solrClient.query(tQuery); + // Now collect unique canvas + for (SolrDocument pDoc : tResponse.getResults()) { + String tCanvas = (String)pDoc.get("target"); + if (!tCanvases.contains(tCanvas)) { + tCanvases.add(tCanvas); + } + } + + } catch (SolrServerException tExcpt) { + tExcpt.printStackTrace(); + throw new IOException(tExcpt.getMessage()); + } + + return tCanvases.size(); + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/contollers/StoreService.java b/src/main/java/uk/org/llgc/annotation/store/contollers/StoreService.java deleted file mode 100644 index ce1fc9b9..00000000 --- a/src/main/java/uk/org/llgc/annotation/store/contollers/StoreService.java +++ /dev/null @@ -1,98 +0,0 @@ -package uk.org.llgc.annotation.store.contollers; - -import javax.faces.bean.ManagedBean; -import javax.faces.bean.ApplicationScoped; -import javax.annotation.PostConstruct; - -import uk.org.llgc.annotation.store.data.PageAnnoCount; -import uk.org.llgc.annotation.store.data.Manifest; -import uk.org.llgc.annotation.store.data.Canvas; -import uk.org.llgc.annotation.store.adapters.StoreAdapter; -import uk.org.llgc.annotation.store.StoreConfig; - -import java.util.Base64; -import java.util.zip.Deflater; - -import java.util.List; -import java.util.ArrayList; - -import java.io.IOException; - -@ApplicationScoped -@ManagedBean -public class StoreService { - protected StoreAdapter _store = null; - - - @PostConstruct - public void init() { - _store = StoreConfig.getConfig().getStore(); - } - - public List getListAnnoPages() { - try { - return _store.listAnnoPages(); - } catch (IOException tExcpt) { - return new ArrayList(); - } - } - - public List listAnnoPages(final String pURI) { - Manifest tManifest = new Manifest(); - tManifest.setURI(pURI); - - return this.listAnnoPages(tManifest); - } - - public List listAnnoPages(final Manifest pManifest) { - try { - List tAnnos = _store.listAnnoPages(pManifest); - return tAnnos; - } catch (IOException tExcpt) { - return new ArrayList(); - } - } - - public String shorternCanvas(final String pCanvasId) throws IOException { - byte[] output = new byte[pCanvasId.getBytes("UTF-8").length]; - Deflater compresser = new Deflater(); - compresser.setInput(pCanvasId.getBytes("UTF-8")); - compresser.finish(); - int compressedDataLength = compresser.deflate(output); - compresser.end(); - return Base64.getUrlEncoder().encodeToString(output); - } - - public Manifest getManifestFromAnnotations(final List pAnnoCounts) { - if (pAnnoCounts.isEmpty()) { - return null; - } else { - return pAnnoCounts.get(0).getManifest(); - } - } - - public List getManifests() { - try { - return _store.getManifests(); - } catch (IOException tExcpt) { - return new ArrayList(); - } - } - - public List getAnnoManifests() { - try { - return _store.getSkeletonManifests(); - } catch (IOException tExcpt) { - return new ArrayList(); - } - } - - - public Manifest getManifest(final String pShortId) { - try { - return _store.getManifest(pShortId); - } catch (IOException tExcpt) { - return new Manifest(); - } - } -} diff --git a/src/main/java/uk/org/llgc/annotation/store/controllers/AuthorisationController.java b/src/main/java/uk/org/llgc/annotation/store/controllers/AuthorisationController.java new file mode 100644 index 00000000..f42b52dd --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/controllers/AuthorisationController.java @@ -0,0 +1,141 @@ +package uk.org.llgc.annotation.store.controllers; + +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; +import javax.annotation.PostConstruct; + +import javax.faces.context.FacesContext; + +import javax.servlet.http.HttpServletRequest; + +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.login.OAuthTarget; +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.adapters.StoreAdapter; +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.data.Annotation; +import uk.org.llgc.annotation.store.data.Collection; +import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.Canvas; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import java.util.List; + +import java.io.IOException; + +@RequestScoped +@ManagedBean +public class AuthorisationController { + protected UserService _users = null; + + public AuthorisationController() { + _users = new UserService(); + init(); + } + + public AuthorisationController(final HttpServletRequest pRequest) { + _users = new UserService(pRequest); + init(); + } + public AuthorisationController(final UserService pService) { + _users = pService; + init(); + } + + @PostConstruct + public void init() { + } + + private User getUser() { + if (tUser == null) { + return _users.getUser(); + } else { + return tUser; + } + } + private User tUser = null; + // Only for use with unit testing! + public void setUser(final User pUser) { + tUser = pUser; + } + + /** + * Allow change if logged in user is the same as the one being edited + * or user is admin + */ + public boolean changeUserDetails(final User pUserToChange) { + User tLoggedInUser = this.getUser(); + return tLoggedInUser.getId().equals(pUserToChange.getId()) || tLoggedInUser.isAdmin(); + } + + public boolean allowUpdate(final Annotation pSavedAnno, final Annotation pNewAnno) { + User tLoggedInUser = this.getUser(); + return (pSavedAnno.getCreator() != null && pSavedAnno.getCreator().getId().equals(tLoggedInUser.getId()) || tLoggedInUser.isAdmin()); + } + + public boolean allowDelete(final Annotation pSavedAnno) { + User tLoggedInUser = this.getUser(); + return (pSavedAnno.getCreator() != null && pSavedAnno.getCreator().getId().equals(tLoggedInUser.getId()) || tLoggedInUser.isAdmin()); + } + + public boolean allowDeleteCollection(final Collection pCollection) { + User tLoggedInUser = this.getUser(); + return (pCollection.getUser() != null && pCollection.getUser().getId().equals(tLoggedInUser.getId())) || tLoggedInUser.isAdmin(); + } + + public boolean allowCollectionEdit(final Collection pCollection) { + User tLoggedInUser = this.getUser(); + System.out.println("Logged in user " + tLoggedInUser); + System.out.println("Collection " + pCollection); + return (pCollection.getUser() != null && pCollection.getUser().getId().equals(tLoggedInUser.getId())) || tLoggedInUser.isAdmin(); + } + + public boolean allowViewCollection(final Collection pCollection) { + if (StoreConfig.getConfig().isPublicCollections() && !pCollection.getId().endsWith("all.json")) { + return true; + } else { + User tLoggedInUser = this.getUser(); + return (pCollection.getUser() != null && pCollection.getUser().getId().equals(tLoggedInUser.getId())) || tLoggedInUser.isAdmin(); + } + } + + // currently just allow but this could be made more complicated + public boolean allowReadManifest(final Manifest pManifest, final User pRequestedUser) { + return true; + } + + public boolean allowSearchManifest(final Manifest pManifest, final User pRequestedUser) { + return true; + } + public boolean allowReadAnnotations(final Canvas pCanvas, final User pRequestedUser) { + return true; + } + + public boolean allowExportAllAnnotations() { + User tLoggedInUser = this.getUser(); + return tLoggedInUser.isAdmin(); // Only admin can do this + } + + public boolean allowThrough(final HttpServletRequest pRequest) { + if (pRequest.getRequestURI().contains("/collection/")) { + if (StoreConfig.getConfig().isPublicCollections()) { + return pRequest.getMethod().equals("GET") && !pRequest.getRequestURI().endsWith("all.json"); + } + } + return false; + } + + public boolean deleteUser(final User pAdmin, final User pTarget) { + return pAdmin.isAdmin(); + } + + public boolean allowReadSomeoneElseAnnos(final User pAnnoOwner, final User pRequester) { + if (pAnnoOwner.getId().equals(pRequester.getId())) { + return true; + } else { + return pRequester.isAdmin(); + } + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/contollers/StatsService.java b/src/main/java/uk/org/llgc/annotation/store/controllers/StatsService.java similarity index 60% rename from src/main/java/uk/org/llgc/annotation/store/contollers/StatsService.java rename to src/main/java/uk/org/llgc/annotation/store/controllers/StatsService.java index 3b9a7af1..247f27a2 100644 --- a/src/main/java/uk/org/llgc/annotation/store/contollers/StatsService.java +++ b/src/main/java/uk/org/llgc/annotation/store/controllers/StatsService.java @@ -1,4 +1,4 @@ -package uk.org.llgc.annotation.store.contollers; +package uk.org.llgc.annotation.store.controllers; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; @@ -22,8 +22,10 @@ import java.util.HashMap; import uk.org.llgc.annotation.store.adapters.StoreAdapter; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.data.Manifest; import uk.org.llgc.annotation.store.data.PageAnnoCount; +import uk.org.llgc.annotation.store.data.stats.TopLevel; import uk.org.llgc.annotation.store.StoreConfig; import uk.org.llgc.annotation.store.AnnotationUtils; @@ -47,35 +49,46 @@ public void init(final AnnotationUtils pUtils) { } - protected Manifest getManifestFromId(final String pShortId) throws IOException { - if (_manifests.containsKey(pShortId)) { - return _manifests.get(pShortId); + public Manifest getManifest(final String pId) throws IOException { + if (_manifests.containsKey(pId)) { + return _manifests.get(pId); } else { - Manifest tManifest = _store.getManifest(pShortId); + Manifest tManifest = _store.getManifest(pId); - _manifests.put(pShortId, tManifest); + _manifests.put(pId, tManifest); return tManifest; } - } - public List getManifestAnnoCount(final Manifest pManifest) throws IOException { - if (_manifestAnnoCount.containsKey(pManifest.getShortId())) { - return _manifestAnnoCount.get(pManifest.getShortId()); + public List getAnnoCountData(final Manifest pManifest, final User pUser) throws IOException { + String tKey = "anno-count-" + pManifest.getShortId(); + if (pUser != null) { + tKey += "-" + pUser.getShortId(); + } + if (_manifestAnnoCount.containsKey(tKey)) { + return _manifestAnnoCount.get(tKey); } else { - List tPageCounts = _store.listAnnoPages(pManifest); - _manifestAnnoCount.put(pManifest.getShortId(), tPageCounts); + List tPageCounts = _store.listAnnoPages(pManifest, pUser); + _manifestAnnoCount.put(tKey, tPageCounts); return tPageCounts; } } - public PieChartModel getPercentAnnotated(final String pShortId) { + public PieChartModel getPercentAnnotated(final String pId) throws IOException { + Manifest tManifest = this.getManifest(pId); + return getPercentAnnotated(tManifest); + } + + public PieChartModel getPercentAnnotated(final Manifest pManifest) { + UserService tUserService = new UserService(); + return this.getPercentAnnotated(pManifest, tUserService.getUser()); + } + + public PieChartModel getPercentAnnotated(final Manifest pManifest, final User pUser) { PieChartModel tModel = new PieChartModel(); try { - Manifest tManifest = this.getManifestFromId(pShortId); - - int tTranscribedTotal = this.getManifestAnnoCount(tManifest).size(); - int tCanvasTotal = tManifest.getCanvases().size(); + int tTranscribedTotal = this.getAnnoCountData(pManifest, pUser).size(); + int tCanvasTotal = pManifest.getCanvases().size(); int tToDoTotal = tCanvasTotal - tTranscribedTotal; tModel.set("Canvases with annotations: " + (int)(((double)tTranscribedTotal / tCanvasTotal) * 100) + "%", tTranscribedTotal); tModel.set("Canvases without annotations: " + (int)(((double)tToDoTotal / tCanvasTotal) * 100) + "%", tToDoTotal); @@ -89,13 +102,23 @@ public PieChartModel getPercentAnnotated(final String pShortId) { return tModel; } - public BarChartModel getManifestAnnoCount(final String pShortId) { + public BarChartModel getManifestAnnoCount(final String pURI) throws IOException { + Manifest tManifest = this.getManifest(pURI); + return getManifestAnnoCount(tManifest); + } + + public BarChartModel getManifestAnnoCount(final Manifest pManifest) { + UserService tUserService = new UserService(); + return getManifestAnnoCount(pManifest, tUserService.getUser()); + } + + public BarChartModel getManifestAnnoCount(final Manifest pManifest, final User pUser) { BarChartModel model = new BarChartModel(); try { - Manifest tManifest = this.getManifestFromId(pShortId); // Get list of all annotations - List tPageCounts = this.getManifestAnnoCount(tManifest); + List tPageCounts = this.getAnnoCountData(pManifest, pUser); + System.out.println(tPageCounts); ChartSeries annoCounts = new ChartSeries(); annoCounts.setLabel("Number of annotations"); @@ -123,7 +146,7 @@ public BarChartModel getManifestAnnoCount(final String pShortId) { model.setDataRenderMode("value"); model.setDatatipEditor("tooltip"); - model.setTitle("Annotations per Canvas for " + tManifest.getLabel()); + model.setTitle("Annotations per Canvas for " + pManifest.getLabel()); model.setLegendPosition("n"); model.setLegendPlacement(LegendPlacement.OUTSIDEGRID); model.setShowPointLabels(true); @@ -142,4 +165,26 @@ public BarChartModel getManifestAnnoCount(final String pShortId) { } return model; } + + public TopLevel getTopLevelStats() { + TopLevel tStats = new TopLevel(); + try { + tStats.setTotalAnnotations(_store.getTotalAnnotations(null)); + tStats.setTotalManifests(_store.getTotalManifests(null)); + tStats.setTotalAnnoCanvases(_store.getTotalAnnoCanvases(null)); + } catch (IOException tExcpt) { + tExcpt.printStackTrace(); + } + + return tStats; + } + + public Map getAuthMethodStats() { + try { + return _store.getTotalAuthMethods(); + } catch (IOException tExcpt) { + tExcpt.printStackTrace(); + } + return new HashMap(); + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/controllers/StoreService.java b/src/main/java/uk/org/llgc/annotation/store/controllers/StoreService.java new file mode 100644 index 00000000..de616ab2 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/controllers/StoreService.java @@ -0,0 +1,405 @@ +package uk.org.llgc.annotation.store.controllers; + +import javax.faces.bean.ManagedBean; +import javax.faces.bean.ApplicationScoped; +import javax.annotation.PostConstruct; + +import uk.org.llgc.annotation.store.data.PageAnnoCount; +import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.AnnotationList; +import uk.org.llgc.annotation.store.data.Canvas; +import uk.org.llgc.annotation.store.data.Collection; +import uk.org.llgc.annotation.store.data.Annotation; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.adapters.StoreAdapter; +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.exceptions.PermissionDenied; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; + +import java.util.Base64; +import java.util.zip.Deflater; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.faces.context.FacesContext; + +import com.github.jsonldjava.utils.JsonUtils; + +import java.net.URL; + +import java.io.IOException; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.File; +import java.io.FileInputStream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@ApplicationScoped +@ManagedBean +public class StoreService { + protected static Logger _logger = LogManager.getLogger(StoreService.class.getName()); + protected StoreAdapter _store = null; + protected HttpServletRequest _request = null; + + public StoreService() { + } + + public StoreService(final HttpServletRequest pRequest) { + _request = pRequest; + this.init(); + } + + @PostConstruct + public void init() { + _store = StoreConfig.getConfig().getStore(); + } + + protected User getCurrentUser() { + UserService tService = new UserService(); + return tService.getUser(); + } + + protected AuthorisationController getAuth() { + return new AuthorisationController(); + } + + protected User getCurrentUser(final HttpServletRequest pRequest) { + UserService tService = new UserService(pRequest); + return tService.getUser(); + } + + public List listAnnoPages(final String pURI) throws PermissionDenied { + return listAnnoPages(pURI, this.getCurrentUser()); + } + + public List listAnnoPages(final String pURI, final User pUser) throws PermissionDenied { + if (this.getAuth().allowReadSomeoneElseAnnos(pUser, this.getCurrentUser())) { + Manifest tManifest = new Manifest(); + tManifest.setURI(pURI); + + try { + return _store.listAnnoPages(tManifest, pUser); + } catch (IOException tExcpt) { + System.err.println("Failed to retrieve stats for " + pURI); + tExcpt.printStackTrace(); + } + return new ArrayList(); + } else { + throw new PermissionDenied("User " + this.getCurrentUser().getName() + "(" + this.getCurrentUser().getId() + ") cannot access " + pUser.getName() + "(" + pUser.getId() + ") annotations as user is not an admin"); + } + } + + public Map countAnnotations(final Manifest pManifest) throws PermissionDenied { + return countAnnotations(pManifest, this.getCurrentUser()); + } + + public Map countAnnotations(final Manifest pManifest, final User pUser) throws PermissionDenied { + String tKey = "stats_" + pManifest.getShortId(); + HttpServletRequest tRequest = this.getRequest(); + if (tRequest.getAttribute(tKey) != null) { + return (Map)tRequest.getAttribute(tKey); + } + + Map tStats = new HashMap(); + tStats.put("canvas_count", 0); + tStats.put("total_annos", 0); + List tCount = this.listAnnoPages(pManifest.getURI(), pUser); + tStats.put("canvas_count", tCount.size()); + + int tTotalAnnos = 0; + for (PageAnnoCount tPageCount : tCount) { + tTotalAnnos += tPageCount.getCount(); + } + tStats.put("total_annos", tTotalAnnos); + + if (tRequest.getAttribute(tKey) == null) { + tRequest.setAttribute(tKey, tStats); + } + return tStats; + } + + protected HttpServletRequest getRequest() { + if (_request == null) { + FacesContext facesContext = FacesContext.getCurrentInstance(); + return (HttpServletRequest)facesContext.getExternalContext().getRequest(); + } else { + return _request; + } + } + + public Manifest getEnhancedManifest(final String pManifestURI) throws IOException { + Manifest tManifest = this.getManifestId(pManifestURI); + return this.getEnhancedManifest(this.getCurrentUser(), tManifest, false); + } + + public Manifest getEnhancedManifest(final User pUser, final Manifest pManifest, final boolean regenerate) throws IOException { + File tManifestPath = new File(StoreConfig.getConfig().getDataDir(),"manifests"); + File tUserDir = new File(tManifestPath, pUser.getShortId()); + File tManifestFile = new File(tUserDir, pManifest.getShortId() + ".json"); + + if (tManifestFile.exists() && !regenerate) { + Manifest tManifest = new Manifest(); + tManifest.setJson((Map)JsonUtils.fromInputStream(new FileInputStream(tManifestFile))); + + return tManifest; + } else { + tManifestFile.getParentFile().mkdirs(); + Manifest tManifest = this.generateEnhancedManifest(pUser, pManifest); + StringBuffer tURL = new StringBuffer(StoreConfig.getConfig().getBaseURI(this.getRequest())); + if (!tURL.toString().endsWith("/")) { + tURL.append("/"); + } + tURL.append("manifests/"); + tURL.append(pUser.getShortId()); + tURL.append("/"); + tURL.append(pManifest.getShortId()); + tURL.append(".json"); + tManifest.setURI(tURL.toString()); + + JsonUtils.writePrettyPrint(new BufferedWriter(new FileWriter(tManifestFile)), tManifest.getJson()); + return tManifest; + } + } + + public Manifest generateEnhancedManifest(final User pUser, final Manifest pManifest) throws IOException { + Manifest tSourceManifest = new Manifest(); + tSourceManifest.setJson((Map)JsonUtils.fromInputStream(new URL(pManifest.getURI()).openStream())); + + tSourceManifest.addSearchService(StoreConfig.getConfig().getBaseURI(this.getRequest()), pUser); + + tSourceManifest.addAnnotationLists(StoreConfig.getConfig().getBaseURI(this.getRequest()), pUser); + + return tSourceManifest; + } + + public List listAnnoPages(final Manifest pManifest) { + HttpServletRequest tRequest = this.getRequest(); + if (tRequest.getAttribute(pManifest.getURI()) != null) { + return (List)tRequest.getAttribute(pManifest.getURI()); + } + try { + //new PageAnnoCount(final Canvas pCanvas, final int pCount, final Manifest pManifest) + List tAnnosCount = _store.listAnnoPages(pManifest, this.getCurrentUser()); + List tFullCanvasList = new ArrayList(); + for (Canvas tCanvas : pManifest.getCanvases()) { + PageAnnoCount tCanvasCount = new PageAnnoCount(tCanvas, 0, pManifest); + if (tAnnosCount.contains(tCanvasCount)) { + tCanvasCount = tAnnosCount.get(tAnnosCount.indexOf(tCanvasCount)); + } + tFullCanvasList.add(tCanvasCount); + } + if (tRequest.getAttribute(pManifest.getURI()) == null) { + tRequest.setAttribute(pManifest.getURI(), tFullCanvasList); + } + return tFullCanvasList; + } catch (IOException tExcpt) { + return new ArrayList(); + } + } + + public String shorternCanvas(final String pCanvasId) throws IOException { + byte[] output = new byte[pCanvasId.getBytes("UTF-8").length]; + Deflater compresser = new Deflater(); + compresser.setInput(pCanvasId.getBytes("UTF-8")); + compresser.finish(); + int compressedDataLength = compresser.deflate(output); + compresser.end(); + return Base64.getUrlEncoder().encodeToString(output); + } + + public Manifest getManifestFromAnnotations(final List pAnnoCounts) { + if (pAnnoCounts.isEmpty()) { + return null; + } else { + return pAnnoCounts.get(0).getManifest(); + } + } + + public Canvas getCanvasId(final String pId) { + try { + Canvas tCanvas = new Canvas(pId, ""); + tCanvas = _store.resolveCanvas(tCanvas.getShortId()); + return tCanvas; + } catch (IOException tExcpt) { + return null; + } + } + + public List getManifests() { + try { + return _store.getManifests(); + } catch (IOException tExcpt) { + return new ArrayList(); + } + } + + public List getAnnoManifests() { + try { + return _store.getSkeletonManifests(this.getCurrentUser()); + } catch (IOException tExcpt) { + return new ArrayList(); + } + } + + public Manifest getManifestFromCanvas(final String pCanvasURI) { + try { + Manifest tSkeleton = _store.getManifestForCanvas(new Canvas(pCanvasURI, "")); + return _store.getManifest(tSkeleton.getURI()); + } catch (IOException tExcpt) { + return new Manifest(); + } + } + + public AnnotationList getAnnotations(final String pCanvasURI) { + return this.getAnnotations(pCanvasURI, this.getCurrentUser()); + } + + public AnnotationList getAnnotations(final String pCanvasURI, final User pUser) { + HttpServletRequest tRequest = this.getRequest(); + String tStoreKey = "al_" + pCanvasURI; + if (tRequest.getAttribute(tStoreKey) != null) { + _logger.debug("Getting annotations from cache"); + return (AnnotationList)tRequest.getAttribute(tStoreKey); + } + + try { + AnnotationList tAnnos = _store.getAnnotationsFromPage(pUser, new Canvas(pCanvasURI, "")); + Collections.sort(tAnnos.getAnnotations(), new Comparator() { + public int compare(Object o1, Object o2) { + Annotation tAnno1 = (Annotation)o1; + Annotation tAnno2 = (Annotation)o2; + + try { + return new Integer(lastPartOfID(tAnno1)).compareTo(new Integer(lastPartOfID(tAnno2))); + } catch (NumberFormatException tExcpt) { + // do string compare instead + return lastPartOfID(tAnno1).compareTo(lastPartOfID(tAnno2)); + } + } + + protected String lastPartOfID(final Annotation pAnno) { + return pAnno.getId().substring(pAnno.getId().lastIndexOf("/")); + } + } + ); + + // sort and store in request + tRequest.setAttribute(tStoreKey, tAnnos); + + _logger.debug("Getting annotations from db"); + return tAnnos; + } catch (IOException tExcpt) { + return new AnnotationList(); + } + } + + public Manifest getManifest(final String pId) { + try { + return _store.getManifest(pId); + } catch (IOException tExcpt) { + return new Manifest(); + } + } + + public Manifest getManifestId(final String pURI) { + try { + return _store.getManifest(pURI); + } catch (IOException tExcpt) { + return new Manifest(); + } + } + + public List getCollections(final HttpServletRequest pRequest) throws IOException { + User tUser = this.getCurrentUser(pRequest); + String tKey = "get_collections_from_request_" + tUser.getShortId(); + if (this.isCached(tKey)) { + return (List)this.getCacheObject(tKey); + } + _logger.debug("getCollections(pRequest)"); + List tCollections = _store.getCollections(tUser); + // if empty create the default collection + if (tCollections.isEmpty()) { + Collection tDefaultCollection = new Collection(); + tDefaultCollection.setUser(tUser); + tDefaultCollection.setLabel(StoreConfig.getConfig().getDefaultCollectionName()); + tDefaultCollection.createDefaultId(StoreConfig.getConfig().getBaseURI(pRequest)); + tDefaultCollection = _store.createCollection(tDefaultCollection); + tCollections.add(tDefaultCollection); + } + + Collections.sort(tCollections); + this.putCacheObject(tKey, tCollections); + return tCollections; + } + + protected void putCacheObject(final String pKey, final Object pObject) { + HttpServletRequest tRequest = this.getRequest(); + tRequest.setAttribute(pKey, pObject); + } + + protected Object getCacheObject(final String pKey) { + HttpServletRequest tRequest = this.getRequest(); + return tRequest.getAttribute(pKey); + } + + protected boolean isCached(final String pKey) { + HttpServletRequest tRequest = this.getRequest(); + return tRequest.getAttribute(pKey) != null; + } + + public List getCollections(final User pUser) throws IOException { + _logger.debug("getCollections(pUser)"); + String tKey = "collections_for_user_" + pUser.getShortId(); + + if (this.isCached(tKey)) { + return (List)this.getCacheObject(tKey); + } + + List tCollections = new ArrayList(); + + User tUser = this.getCurrentUser(); + if (tUser.isAdmin()) { + tCollections = _store.getCollections(pUser); + + this.putCacheObject(tKey, tCollections); + } + + return tCollections; + } + + public Collection getCollection(final String pID, final HttpServletRequest pRequest) throws IOException { + String tCollectionKey = "collection_"; + if (pID == null || pID.length() == 0) { + User tUser = this.getCurrentUser(pRequest); + + List tCollections = this.getCollections(pRequest); + for (Collection tCollection : tCollections) { + if (tCollection.isDefaultCollection()) { + return tCollection; + } + } + // Create default collection + Collection tDefaultCollection = new Collection(); + tDefaultCollection.setUser(tUser); + tDefaultCollection.createDefaultId(StoreConfig.getConfig().getBaseURI(pRequest)); + Collection tResponse = _store.getCollection(tDefaultCollection.getId()); + this.putCacheObject(tCollectionKey + tResponse.getId(), tResponse); + return tResponse; + } else { + if (this.isCached(tCollectionKey + pID)) { + return (Collection)this.getCacheObject(tCollectionKey + pID); + } else { + Collection tResponse = _store.getCollection(pID); + this.putCacheObject(tCollectionKey + tResponse.getId(), tResponse); + return tResponse; + } + } + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/controllers/UserService.java b/src/main/java/uk/org/llgc/annotation/store/controllers/UserService.java new file mode 100644 index 00000000..cb36bd3c --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/controllers/UserService.java @@ -0,0 +1,211 @@ +package uk.org.llgc.annotation.store.controllers; + +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; +import javax.annotation.PostConstruct; + +import javax.faces.context.FacesContext; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpServletRequest; + +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.users.LocalUser; +import uk.org.llgc.annotation.store.data.login.OAuthTarget; +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.adapters.StoreAdapter; +import uk.org.llgc.annotation.store.StoreConfig; + +import java.util.List; +import java.util.ArrayList; + +import java.io.IOException; +import java.net.URISyntaxException; + +@RequestScoped +@ManagedBean +public class UserService { + protected StoreAdapter _store = null; + protected HttpSession _session = null; + protected HttpServletRequest _request = null; + + public UserService() { + init(); + } + + public UserService(final HttpServletRequest pRequest) { + _request = pRequest; + _session = pRequest.getSession(); + init(); + } + + @PostConstruct + public void init() { + _store = StoreConfig.getConfig().getStore(); + } + + protected HttpSession getSession() { + if (_session == null) { + FacesContext facesContext = FacesContext.getCurrentInstance(); + HttpSession tSession = (HttpSession)facesContext.getExternalContext().getSession(true); + return tSession; + } else { + return _session; + } + } + + protected HttpServletRequest getRequest() { + if (_request == null) { + FacesContext facesContext = FacesContext.getCurrentInstance(); + return (HttpServletRequest)facesContext.getExternalContext().getRequest(); + } else { + return _request; + } + } + + public void setUser(final User pUser) throws IOException { + // Try to get from Store to enhance this logged in user + // If not present then this method should also add the user to the database + User tEnhancedUser = _store.retrieveUser(pUser); + this.getSession().setAttribute("user", tEnhancedUser); + } + + public List getUsers() throws IOException { + List tUsers = new ArrayList(); + // check if admin then return users + User tUser = this.getUser(); + if (tUser.isAdmin()) { + tUsers = _store.getUsers(); + } else { + System.out.println("User not admin so returning current user"); + tUsers.add(tUser); + } + + return tUsers; + } + + public boolean isAdminSetup() { + try { + // Is there at least one Admin with a password? + List tAdminUsers = _store.getUsers("admin"); + + for (User tUser : tAdminUsers) { + if (tUser instanceof LocalUser && ((LocalUser)tUser).hasPassword()) { + return true; + } + } + return false; + } catch (IOException pExcpt) { + System.err.println("Failed to get Admin users due to:"); + pExcpt.printStackTrace(); + return false; + } + } + + public User getUser(final String pID) { + User tUser = this.getUser(); + if (tUser.isAdmin()) { + try { + User tSearchUser = User.createUserFromID(pID); + System.out.println("Search user " + tSearchUser); + System.out.println("User " + _store.getUser(tSearchUser)); + return _store.getUser(tSearchUser); + } catch (IOException tExcpt) { + System.err.println("Failed to get user due to: " + tExcpt); + return null; + } catch (URISyntaxException tExcpt) { + System.err.println("Failed to get user due to: " + tExcpt); + return null; + } + } else { + return null; + } + } + + public boolean isLocal(final User pUser) { + return pUser instanceof LocalUser; + } + + public User getUser() { + HttpSession tSession = this.getSession(); + if (tSession.getAttribute("user") != null) { + return (User)tSession.getAttribute("user"); + } else { + return null; + } + } + + public LocalUser getLocalUser(final String pEmail) { + try { + List tUsers = _store.getUsers("admin"); + for (User tUser : tUsers) { + if (tUser instanceof LocalUser) { + LocalUser tAdminUser = (LocalUser)tUser; + if (tAdminUser.getEmail().equals(pEmail)) { + return tAdminUser; + } + } + } + // if there is a admin set but not present in the DB create it. + if (StoreConfig.getConfig().getAdminEmail() != null && pEmail.equals(StoreConfig.getConfig().getAdminEmail())) { + LocalUser tAdmin = new LocalUser(); + try { + + tAdmin.setId(User.createUserFromShortID(StoreConfig.getConfig().getBaseURI(this.getRequest()), "admin").getId()); + tAdmin.setShortId("admin"); + tAdmin.setEmail(StoreConfig.getConfig().getAdminEmail()); + tAdmin.setPassword("", false); + tAdmin.setName("Admin User"); + tAdmin.setAdmin(true); + tAdmin.setPicture("/images/AdminIcon.svg"); + + User tSavedUser = _store.saveUser(tAdmin); + return (LocalUser)tSavedUser; + } catch (URISyntaxException tExcpt) { + System.err.println("Failed to create admin user due to an issue with the ID"); + tExcpt.printStackTrace(); + } + } + } catch (IOException pExcpt) { + System.err.println("Failed to get local user by email due to:"); + pExcpt.printStackTrace(); + } + return null; + } + + public String getRelativeId() { + User tUser = this.getUser(); + return tUser.getId().substring(tUser.getId().lastIndexOf("user/")); + } + + public boolean isAuthenticated() { + return this.getSession().getAttribute("user") != null; + } + + public boolean getAuthenticated() { + return isAuthenticated(); + } + + public List getConfig() { + return StoreConfig.getConfig().getAuthTargets(); + } + + public String getAuthMethodLogo(final String pMethod) { + for (OAuthTarget tTarget : this.getConfig()) { + if (tTarget.getId().equals(pMethod)) { + return tTarget.getButton().getLogo(); + } + } + return ""; + } + + public boolean isAdmin() { + HttpSession tSession = this.getSession(); + if (tSession.getAttribute("user") == null) { + // No one is logged in + return false; + } + User tUser = (User)tSession.getAttribute("user"); + return tUser.isAdmin(); + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/Annotation.java b/src/main/java/uk/org/llgc/annotation/store/data/Annotation.java index 994d135c..35d9717d 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/Annotation.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/Annotation.java @@ -15,6 +15,7 @@ import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.data.users.User; import org.apache.jena.vocabulary.DCTerms; @@ -35,6 +36,7 @@ public class Annotation { protected Map _annotation = null; protected List _bodies = null; protected List _targets = null; + protected User _creator = null; public Annotation(final Map pAnno) { this(pAnno, null); @@ -58,6 +60,24 @@ public void setEncoder(final Encoder pEncoder) { _encoder = pEncoder; } + public void setCreator(final User pUser) { + _annotation.put("dcterms:creator", pUser.getId()); + _creator = pUser; + } + + public User getCreator() { + if (_creator == null && _annotation.get("dcterms:creator") != null) { + _creator = new User(); + try { + _creator.setId((String)_annotation.get("dcterms:creator")); + } catch(URISyntaxException tExcept) { + System.err.println("Failed to load user to annotation as the ID was no a URI: " + _annotation.get("dcterms:creator")); + return null; + } + } + return _creator; + } + public void setJson(final Map pJson) { _annotation = pJson; this.init(); @@ -92,6 +112,8 @@ protected void init() { _targets.add(new Target(_annotation.get("on"))); } } + // Ensure motivation is an array + this.getMotivations(); } protected void standaiseAnno() { @@ -111,6 +133,35 @@ protected void standaiseAnno() { _annotation.put("resource", tList); } + + + // Ensure on is an array + if (_annotation.get("on") != null && !(_annotation.get("on") instanceof String)) { + List> tOns = new ArrayList>(); + if (_annotation.get("on") instanceof List) { + tOns = (List>)_annotation.get("on"); + } else { + tOns.add((Map)_annotation.get("on")); + } + // Ensure selector.item is an object not an array + for (Map tOn: tOns) { + if (tOn.get("@type") != null && tOn.get("@type").equals("oa:SpecificResource") && tOn.get("selector") != null) { + Map tSelector = (Map)tOn.get("selector"); + if (tSelector.get("item") != null && tSelector.get("item") instanceof List) { + List> tItems = (List>)tSelector.get("item"); + if (tItems.size() > 1) { + System.err.println("I think this is an invalid annotation as there are multiple items and I expected only one: " + this.getId()); + try { + System.out.println(JsonUtils.toPrettyString(_annotation)); + } catch (IOException tExcpt) { + System.err.println("Failed to print annotation due to " + tExcpt); + } + } + tSelector.put("item", tItems.get(0)); + } + } + } + } } public String getId() { @@ -138,6 +189,7 @@ public List getMotivations() { List tMotivations = new ArrayList(); if (_annotation.get("motivation") instanceof String) { tMotivations.add((String)_annotation.get("motivation")); + _annotation.put("motivation", tMotivations); } else { tMotivations = (List)_annotation.get("motivation"); } diff --git a/src/main/java/uk/org/llgc/annotation/store/data/AnnotationList.java b/src/main/java/uk/org/llgc/annotation/store/data/AnnotationList.java index 67645a17..b5e489eb 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/AnnotationList.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/AnnotationList.java @@ -9,7 +9,10 @@ import org.apache.jena.rdf.model.Model; +import com.github.jsonldjava.utils.JsonUtils; + import uk.org.llgc.annotation.store.AnnotationUtils; +import uk.org.llgc.annotation.store.data.users.User; public class AnnotationList { protected List _annotations = null; @@ -65,6 +68,10 @@ public Annotation get(final String pAnnoId) { return null; } + public Annotation get(final int pIndex) { + return _annotations.get(pIndex); + } + public int size() { return _annotations.size(); } @@ -77,6 +84,12 @@ public void setAnnotations(final List pAnnos) { _annotations = pAnnos; } + public void setCreator(final User pUser) { + for (Annotation tAnno : _annotations) { + tAnno.setCreator(pUser); + } + } + public Map toJson() throws IOException { Map tJson = new HashMap(); tJson.put("@context", "http://iiif.io/api/presentation/2/context.json"); @@ -93,4 +106,13 @@ public Map toJson() throws IOException { return tJson; } + public String toString() { + try { + Map tJson = this.toJson(); + + return JsonUtils.toPrettyString(tJson); + } catch (Exception tExcpt) { + return "Failed to convert to JSON due to: " + tExcpt; + } + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/data/Collection.java b/src/main/java/uk/org/llgc/annotation/store/data/Collection.java new file mode 100644 index 00000000..b03d713e --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/Collection.java @@ -0,0 +1,269 @@ +package uk.org.llgc.annotation.store.data; + +import com.github.jsonldjava.utils.JsonUtils; + +import java.io.IOException; + +import java.net.URL; +import java.net.URISyntaxException; + +import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.AnnotationUtils; + +public class Collection implements Comparable { + protected String _id = ""; + protected String _shortId = ""; + protected String _label = ""; + protected Map _json = null; + protected User _user = null; + protected List _manifests = new ArrayList(); + + public Collection() { + _json = new HashMap(); + } + + public Collection(final String pId, final User pUser, final String pLabel) { + _json = new HashMap(); + this.setId(pId); + this.setUser(pUser); + this.setLabel(pLabel); + } + + public Collection(final Map pJson) throws IOException { + this.setJson(pJson); + } + + public Map toJson() throws IOException { + _json.remove("manifests"); // this will just return null if its not present + _json.remove("members"); + if (!_manifests.isEmpty()) { + List> tMembers = new ArrayList>(); + _json.put("members", tMembers); + _json.put("manifests", tMembers); + for (Manifest tManifest : _manifests) { + Map tManJson = new HashMap(); + tManJson.put("@id", tManifest.getURI()); + tManJson.put("@type", "sc:Manifest"); + tManJson.put("label", tManifest.getLabel()); + + tMembers.add(tManJson); + } + } + _json.put("@id", this.getId()); + _json.put("@type", "sc:Collection"); + _json.put("dcterms:creator", _user.getId()); + _json.put("label", _label); + _json.put("dc:identifier", this.getShortId()); + + _json.put("@context", JsonUtils.fromString(new StringBuilder().append("[") + .append("{") + .append("\"dcterms\" : \"http://purl.org/dc/terms/\",") + .append("\"dcterms:creator\" : {") + .append("\"@type\" : \"@id\",") + .append("\"@id\" : \"dcterms:creator\"") + .append("}") + .append(" },") + .append("\"http://iiif.io/api/presentation/2/context.json\" ]").toString())); + return _json; + } + + public void setJson(final Map pJson) throws IOException { + _json = pJson; + this.setId((String)_json.get("@id")); + this.setLabel((String)_json.get("label")); // will fail if there is a multilingual string + this.setShortId((String)_json.get("dc:identifier")); + String tUserId = (String)_json.get("dcterms:creator"); + User tUser = new User(); + try { + tUser.setId(tUserId); + } catch (URISyntaxException tExcpt) { + throw new IOException("Failed to add user because Id wasn't a URI" + tExcpt); + } + this.setUser(tUser); + + _manifests = new ArrayList(); + if (_json.get("manifests") != null && _json.get("manifests") instanceof List) { + for (Map tManifest: (List>)_json.get("manifests")) { + this.addManifest(tManifest); + } + } + if (_json.get("members") != null && _json.get("members") instanceof List) { + for (Map tManifest: (List>)_json.get("members")) { + if (tManifest.get("@type").equals("sc:Manifest")) { + this.addManifest(tManifest); + } + } + } + } + + protected void addManifest(final Map pManifestJson) throws IOException { + try { + Manifest tManifest = new Manifest(pManifestJson); + if (!_manifests.contains(tManifest)) { + _manifests.add(tManifest); + } + } catch (IOException tExcpt) { + System.err.println("Failed to add Manifest due to " + tExcpt); + System.err.println(JsonUtils.toPrettyString(pManifestJson)); + } + } + + public String getType() { + return "sc:Collection"; + } + + public Manifest getManifest(final String pId) { + for (Manifest tManifest : _manifests) { + if (tManifest.getURI().equals(pId)) { + return tManifest; + } + } + return null; + } + + public boolean isDefaultCollection() { + return _id.endsWith("inbox.json"); + } + + public boolean remove(final Manifest pManifest) { + return _manifests.remove(pManifest); + } + + public void add(final Manifest pManifest) { + _manifests.add(pManifest); + } + + public boolean contains(final Manifest pManifest) { + return this.getManifest(pManifest.getURI()) != null; + } + + public String createDefaultId(final String pBaseURL) { + StringBuffer tIdentifier = new StringBuffer(pBaseURL); + if (!pBaseURL.endsWith("/")) { + tIdentifier.append("/"); + } + tIdentifier.append("collection/"); + tIdentifier.append(_user.getShortId()); + tIdentifier.append("/inbox.json"); + _id = tIdentifier.toString(); + return _id; + } + + public String createId(final String pBaseURL) { + _id = pBaseURL + _user.getShortId() + "/" + new Date().getTime() + ".json"; + return _id; + } + /** + * Get URI. + * + * @return URI as String. + */ + public String getId() { + return _id; + } + + /** + * Set URI. + * + * @param URI the value to set. + */ + public void setId(final String pId) { + _id = pId; + } + + /** + * Get shortId. + * + * @return shortId as String. + */ + public String getShortId() { + if (_shortId == null || _shortId.isEmpty()) { + try { + _shortId = AnnotationUtils.getHash(_id, "md5"); + } catch (IOException tExcpt) { + tExcpt.printStackTrace(); + } + } + return _shortId; + } + + /** + * Set shortId. + * + * @param shortId the value to set. + */ + public void setShortId(final String pShortId) { + _shortId = pShortId; + } + + /** + * Get label. + * + * @return label as String. + */ + public String getLabel() { + return _label; + } + + public User getUser() { + return _user; + } + + public void setUser(final User pUser) { + _user = pUser; + } + + /** + * Set label. + * + * @param label the value to set. + */ + public void setLabel(final String pLabel) { + _label = pLabel; + } + + + public List getManifests() { + return _manifests; + } + + public boolean equals(Object pOther) { + if (pOther instanceof Collection) { + return _id.equals(((Collection)pOther).getId()); + } else { + return false; + } + } + + public int compareTo(final Object pOther) { + Collection tOther = (Collection)pOther; + if (_id.equals(tOther.getId())) { + return 0; // objects are equal + } + if (_id.endsWith("inbox.json")) { + return -1; + } + if (tOther.getId().endsWith("inbox.json")) { + return 1; + } + long tTime1 = this.getTimestamp(_id); + long tTime2 = this.getTimestamp(tOther.getId()); + + return (int)(tTime1 - tTime2); + } + + protected long getTimestamp(final String pID) { + return Long.parseLong(pID.substring(pID.lastIndexOf("/")+ 1).split("\\.")[0]); + } + + public String toString() { + return "Id: " + _id + "\nShortId: " + _shortId + "\nLabel: " + _label + "\nManifests: " + _manifests.size(); + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/IIIFSearchResults.java b/src/main/java/uk/org/llgc/annotation/store/data/IIIFSearchResults.java index 72f9171d..9f7c9e5e 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/IIIFSearchResults.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/IIIFSearchResults.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.ArrayList; +import java.net.URI; + import java.io.IOException; public class IIIFSearchResults extends AnnotationList { @@ -12,8 +14,9 @@ public class IIIFSearchResults extends AnnotationList { protected int _startIndex = -1; protected String _next = ""; - public IIIFSearchResults() { + public IIIFSearchResults(final URI pID) { super(); + super.setId(pID.toString()); } public Map toJson() throws IOException { diff --git a/src/main/java/uk/org/llgc/annotation/store/data/Manifest.java b/src/main/java/uk/org/llgc/annotation/store/data/Manifest.java index 654b29c0..88ae14bf 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/Manifest.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/Manifest.java @@ -5,12 +5,15 @@ import java.io.IOException; import java.net.URL; +import java.net.MalformedURLException; import java.util.Map; +import java.util.HashMap; import java.util.List; import java.util.ArrayList; import uk.org.llgc.annotation.store.data.Canvas; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.AnnotationUtils; public class Manifest { @@ -32,13 +35,18 @@ public Manifest(final Map pJson) throws IOException { } public Manifest(final Map pJson, final String pShortId) throws IOException { - this.setJson(pJson); - if (!this.getType().equals("sc:Manifest")) { - throw new IOException("Can't create manifest as type was incorrect. Expected sc:Manifest but got: " + this.getType()); - } - if (this.getCanvases().isEmpty()) { - throw new IOException("Can't load manifest as it has no pages."); + if (pJson.get("@type") == null) { + if (pJson.get("type") != null) { + throw new IOException("SAS Currently only works with IIIF version 2.0 manifests"); + } else { + throw new IOException("Failed to process manifest as it has no @type property."); + } + } else { + if (!pJson.get("@type").equals("sc:Manifest")) { + throw new IOException("Can't create manifest as type was incorrect. Expected sc:Manifest but got: " + this.getType()); + } } + this.setJson(pJson); this.setShortId(pShortId); this.setURI((String)pJson.get("@id")); } @@ -50,7 +58,37 @@ public Map toJson() { public void setJson(final Map pJson) { _json = pJson; this.setURI((String)_json.get("@id")); - this.setLabel((String)_json.get("label")); // will fail if there is a multilingual string + if (_json.get("label") != null) { + String tLabel = ""; + if (_json.get("label") instanceof String) { + tLabel = (String)_json.get("label"); + } else if (_json.get("label") instanceof Map) { + Map tLabelMap = (Map)_json.get("label"); + if (tLabelMap.get("@value") != null && tLabelMap.get("@value") instanceof String) { + tLabel = (String)tLabelMap.get("@value"); + } + } else { + // Label is an array of one or more language strings or an array of strings + List tLabels = (List)_json.get("label"); + if (!tLabels.isEmpty()) { + if (tLabels.get(0) instanceof String) { + tLabel = (String)tLabels.get(0); + } else if (tLabels.get(0) instanceof Map){ + // Lang map just select first + Map tLabelMap = (Map)tLabels.get(0); + if (tLabelMap.get("@value") != null && tLabelMap.get("@value") instanceof String) { + tLabel = (String)tLabelMap.get("@value"); + } + + } + } + } + + this.setLabel(tLabel); + } + if (this.getLabel().isEmpty()) { + this.setLabel("Missing Manifest label"); + } Map tSequence = null; if (_json.get("sequences") instanceof List ) { @@ -69,11 +107,124 @@ public void setJson(final Map pJson) { } } + public String getAnnoListURL(final Canvas pCanvas, final String pBaseURI, final User pUser) { + StringBuffer tAnnoListURL = new StringBuffer(pBaseURI); + if (!pBaseURI.endsWith("/")) { + tAnnoListURL.append("/"); + } + + tAnnoListURL.append("annotations/"); + tAnnoListURL.append(pUser.getShortId()); + tAnnoListURL.append("/"); + tAnnoListURL.append(pCanvas.getShortId()); + tAnnoListURL.append(".json"); + + return tAnnoListURL.toString(); + } + + public void addAnnotationLists(final String pBaseURI, final User pUser) { + Map tSequence = null; + if (_json.get("sequences") instanceof List ) { + if (!((List)_json.get("sequences")).isEmpty()) { + tSequence = ((List>)_json.get("sequences")).get(0); + } + } else { + tSequence = (Map)_json.get("sequences"); + } + + if (tSequence != null) { + StringBuffer tAnnoListURL = new StringBuffer(pBaseURI); + if (!pBaseURI.endsWith("/")) { + tAnnoListURL.append("/"); + } + + tAnnoListURL.append("annotations/"); + tAnnoListURL.append(pUser.getShortId()); + tAnnoListURL.append("/##CANVAS_SHORT##.json"); + + for (Map tCanvasJson : (List>)tSequence.get("canvases")) { + /* "otherContent": [ { + "@id": "http://localhost:8888/annotation/list/41eef9939cd722b17ef4c177c2afa12d.json", + "@type": "sc:AnnotationList", + "label": "My fantastic annotations" + }]*/ + Canvas tCanvas = new Canvas((String)tCanvasJson.get("@id"), (String)tCanvasJson.get("label")); + + Map tOtherContent = new HashMap(); + tOtherContent.put("@id", tAnnoListURL.toString().replace("##CANVAS_SHORT##", tCanvas.getShortId())); + tOtherContent.put("@type", "sc:AnnotationList"); + tOtherContent.put("label", "Annotations for canvas " + tCanvas.getLabel()); + + this.addKey(tCanvasJson, "otherContent", tOtherContent, true); + } + } + } + + /** + * Add key but if it already exists add it to a list + * @param alwaysList if true then always set data as a list. + */ + protected void addKey(final Map pParent, final String pKey, final Map pData, final boolean alwaysList) { + List tValueList = new ArrayList(); + + // If service already exists then add to it. + if (pParent.containsKey(pKey)) { + if (pParent.get(pKey) instanceof Map) { + tValueList.add(pParent.get(pKey)); + pParent.put(pKey, tValueList); + } + + ((List)pParent.get(pKey)).add(pData); + } else { + if (alwaysList) { + tValueList.add(pData); + pParent.put(pKey, tValueList); + } else { + pParent.put(pKey, pData); + } + } + } + + public URL getSearchURL(final String pBaseURI, final User pUser) { + StringBuffer tSearchURL = new StringBuffer(pBaseURI); + if (!pBaseURI.endsWith("/")) { + tSearchURL.append("/"); + } + + tSearchURL.append("search-api/"); + tSearchURL.append(pUser.getShortId()); + tSearchURL.append("/"); + tSearchURL.append(this.getShortId()); + tSearchURL.append("/search"); + try { + return new URL(tSearchURL.toString()); + } catch (MalformedURLException tExcpt) { + System.out.println("Unable to create URL from " + tSearchURL.toString()); + tExcpt.printStackTrace(); + } + return null; + } + + public void addSearchService(final String pBaseURI, final User pUser) { + /* "service": { + "@context": "http://iiif.io/api/search/0/context.json" + "@id": "http://localhost:8888/search-api/5bbada360fbe7c8f72a8153896686398/search", + "profile": "http://iiif.io/api/search/0/search", + },*/ + + Map tService = new HashMap(); + tService.put("@context", "http://iiif.io/api/search/0/context.json"); + tService.put("@id", this.getSearchURL(pBaseURI, pUser).toString()); + tService.put("profile", "http://iiif.io/api/search/0/search"); + + this.addKey(_json, "service", tService, false); + } + public String getType() { try { return (String)getJson().get("@type"); } catch (IOException tExcpt) { - tExcpt.printStackTrace(); + //tExcpt.printStackTrace(); return null; } } @@ -113,6 +264,9 @@ public String getURI() { */ public void setURI(final String pURI) { _URI = pURI; + if (_json != null) { + _json.put("@id", pURI); + } } /** @@ -122,16 +276,19 @@ public void setURI(final String pURI) { */ public String getShortId() { if (_shortId == null || _shortId.isEmpty()) { - if (_URI.endsWith("manifest.json")) { + // Its no longer safe to use this NLW shortcut + // as it fails the workbench: + // https://glenrobson.github.io/workbench/manifests/projectmanifest.json" + /*if (_URI.endsWith("manifest.json")) { String[] tURI = _URI.split("/"); _shortId = tURI[tURI.length - 2]; - } else { - try { - _shortId = AnnotationUtils.getHash(_URI, "md5"); - } catch (IOException tExcpt) { - tExcpt.printStackTrace(); - } + } else {*/ + try { + _shortId = AnnotationUtils.getHash(_URI, "md5"); + } catch (IOException tExcpt) { + tExcpt.printStackTrace(); } + //} } return _shortId; } @@ -163,6 +320,37 @@ public void setLabel(final String pLabel) { _label = pLabel; } + public String getLogo() { + String tLogo = null; + + if (_json != null && _json.containsKey("logo")) { + if (_json.get("logo") instanceof Map && ((Map)_json.get("logo")).containsKey("@id")) { + tLogo = (String)((Map)_json.get("logo")).get("@id"); + } else if (_json.get("logo") instanceof String) { + tLogo = (String)_json.get("logo"); + } + } + + return tLogo; + } + + public String getDescription(){ + String tDesc = null; + if (_json != null && _json.containsKey("description")) { + tDesc = (String)_json.get("description"); + } + + return tDesc; + } + + public String getAttribution() { + String tAtt = null; + if (_json != null && _json.containsKey("attribution")) { + tAtt = (String)_json.get("attribution"); + } + + return tAtt; + } public List getCanvases() { return _canvases; diff --git a/src/main/java/uk/org/llgc/annotation/store/data/PageAnnoCount.java b/src/main/java/uk/org/llgc/annotation/store/data/PageAnnoCount.java index c4e7da62..d2aa7d42 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/PageAnnoCount.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/PageAnnoCount.java @@ -4,11 +4,7 @@ public class PageAnnoCount { protected int _count = 0; - // These three properties should really be a canvas object... protected Canvas _canvas = null; - protected String _pageId = ""; - protected String _label = ""; - protected String _shortId = ""; protected Manifest _manifest = null; public PageAnnoCount(final Canvas pCanvas, final int pCount, final Manifest pManifest) { @@ -62,6 +58,15 @@ public void setCanvas(final Canvas pCanvas) { _canvas = pCanvas; } + public boolean equals(final Object pOtherObj) { + if (pOtherObj instanceof PageAnnoCount) { + PageAnnoCount tOther = (PageAnnoCount)pOtherObj; + return _canvas.getId().equals(tOther.getCanvas().getId()); + } else { + return false; + } + } + public String toString() { return "Canvas=>" + _canvas.toString() + "\tCount=>" + _count; } diff --git a/src/main/java/uk/org/llgc/annotation/store/data/SearchQuery.java b/src/main/java/uk/org/llgc/annotation/store/data/SearchQuery.java index fd13af53..39a8bcc9 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/SearchQuery.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/SearchQuery.java @@ -18,11 +18,13 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; +import uk.org.llgc.annotation.store.data.users.User; + public class SearchQuery { protected String _query = ""; protected List _motivations = null; protected List _dates = null; - protected List _users = null; + protected List _users = null; protected int _resultsPerPage = 1000; protected int _page = 0; protected String _scope = ""; @@ -32,7 +34,7 @@ public SearchQuery(final String pQuery) { this.setQuery(pQuery); } - public SearchQuery(final URI pURI) throws ParseException { + public SearchQuery(final URI pURI) throws ParseException, URISyntaxException { this.setBaseURI(pURI); List tParamsList = URLEncodedUtils.parse(pURI, Charset.forName("UTF-8")); @@ -57,16 +59,18 @@ public SearchQuery(final URI pURI) throws ParseException { } } - protected String convertListToString(final String pKey, final List pList) { + protected String convertListToString(final String pKey, final List pList) throws UnsupportedEncodingException { if (pList != null && !pList.isEmpty()) { - StringBuffer tBuff = new StringBuffer("&"); - tBuff.append(pKey); - tBuff.append("="); + StringBuffer tBuff = new StringBuffer(); for (Object tChild: pList) { - tBuff.append(tChild); + if (tChild instanceof User) { + tBuff.append(((User)tChild).getId()); + } else { + tBuff.append(tChild); + } tBuff.append(" "); } - return tBuff.toString().trim(); + return "&" + pKey + "=" + URLEncoder.encode(tBuff.toString().trim(), "UTF-8"); } else { return ""; } @@ -81,20 +85,17 @@ public String toQueryString() { try { tBuff.append(URLEncoder.encode(_query, "UTF-8")); if (_motivations != null) { - tBuff.append("&"); - tBuff.append(URLEncoder.encode(this.convertListToString("motivation", _motivations), "UTF-8")); + tBuff.append(this.convertListToString("motivation", _motivations)); } if (_dates != null) { - tBuff.append("&"); tBuff.append(this.convertListToString("date", _dates)); } if (_users != null) { - tBuff.append("&"); - tBuff.append(URLEncoder.encode(this.convertListToString("user", _users), "UTF-8")); + tBuff.append(this.convertListToString("user", _users)); } if (_page != 0) { - tBuff.append("&"); - tBuff.append("page=" + _page); + tBuff.append("&page="); + tBuff.append(_page); } } catch (UnsupportedEncodingException tExcpt) { // shouldn't happen as UTF-8 should be supported. @@ -181,15 +182,32 @@ public List getDateRanges() { return _dates; } - public void setUsers(final String pUsers) { + public void setUsers(final String pUsers) throws URISyntaxException { StringTokenizer tTokenizer = new StringTokenizer(pUsers); - _users = new ArrayList(); + _users = new ArrayList(); while (tTokenizer.hasMoreTokens()) { - _users.add(tTokenizer.nextToken()); + User tUser = new User(); + tUser.setId(tTokenizer.nextToken()); + _users.add(tUser); } } - - public List getUsers() { + public void addUser(final User pUser) { + if (_users == null) { + _users = new ArrayList(); + } + boolean tFoundUser = false; + for (User tSearchUser : _users) { + if(tSearchUser.getId().equals(pUser.getId())) { + tFoundUser = true; + break; + } + } + if (!tFoundUser) { + _users.add(pUser); + } + } + + public List getUsers() { return _users; } } diff --git a/src/main/java/uk/org/llgc/annotation/store/data/Target.java b/src/main/java/uk/org/llgc/annotation/store/data/Target.java index b01d642a..89193aa4 100644 --- a/src/main/java/uk/org/llgc/annotation/store/data/Target.java +++ b/src/main/java/uk/org/llgc/annotation/store/data/Target.java @@ -44,6 +44,9 @@ public String getRegion() { if (tSelector.get("@type") != null && tSelector.get("@type").equals("oa:FragmentSelector") && tSelector.get("value") != null) { tRegion = (String)tSelector.get("value"); } + if (tSelector.get("@type") != null && tSelector.get("@type").equals("oa:Choice") && tSelector.get("default") != null && ((Map)tSelector.get("default")).get("@type").equals("oa:FragmentSelector")) { + tRegion = ((Map)tSelector.get("default")).get("value"); + } } return tRegion; } diff --git a/src/main/java/uk/org/llgc/annotation/store/data/login/Button.java b/src/main/java/uk/org/llgc/annotation/store/data/login/Button.java new file mode 100644 index 00000000..f50f5972 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/login/Button.java @@ -0,0 +1,21 @@ +package uk.org.llgc.annotation.store.data.login; + +import java.util.Map; + +public class Button { + protected String _logo = ""; + protected String _text = ""; + + public Button(final Map pConfig) { + _logo = pConfig.get("logo"); + _text = pConfig.get("text"); + } + + public String getLogo() { + return _logo; + } + + public String getText() { + return _text; + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/login/GenericOAuth.java b/src/main/java/uk/org/llgc/annotation/store/data/login/GenericOAuth.java new file mode 100644 index 00000000..cff86947 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/login/GenericOAuth.java @@ -0,0 +1,46 @@ +package uk.org.llgc.annotation.store.data.login; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; +import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; + +import java.util.Map; + +public class GenericOAuth extends DefaultApi20 { + + protected String _accessTokenEndpoint = ""; + protected Verb _accessTokenVerb = null; + protected String _authBaseUrl = ""; + + public GenericOAuth(final Map pConfig) { + _accessTokenEndpoint = pConfig.get("accessTokenEndpoint"); + if (pConfig.get("accessTokenVerb").toUpperCase().equals("POST")) { + _accessTokenVerb = Verb.POST; + } else { + _accessTokenVerb = Verb.GET; + } + _authBaseUrl = pConfig.get("authorizationBaseUrl"); + } + + @Override + public Verb getAccessTokenVerb() { + return _accessTokenVerb; + } + + // From: https://wp-oauth.com/docs/general/main-concepts/ + @Override + public String getAccessTokenEndpoint() { + return _accessTokenEndpoint; + } + + @Override + protected String getAuthorizationBaseUrl() { + return _authBaseUrl; + } + + @Override + public BearerSignature getBearerSignature() { + return BearerSignatureURIQueryParameter.instance(); + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/login/OAuthTarget.java b/src/main/java/uk/org/llgc/annotation/store/data/login/OAuthTarget.java new file mode 100644 index 00000000..baaafac7 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/login/OAuthTarget.java @@ -0,0 +1,162 @@ +package uk.org.llgc.annotation.store.data.login; + +import java.lang.reflect.Method; + +import java.util.Map; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.model.Verb; + +import java.io.IOException; + +public class OAuthTarget { + protected String _id = ""; + protected DefaultApi20 _endpoints = null; + protected String _clientId = ""; + protected String _clientSecret = ""; + protected String _scope = null; + protected Map _additionalParams = null; + protected Button _button = null; + protected UserMapping _mapping = null; + + public OAuthTarget(final Map pConfig) throws IOException { + this.setId(pConfig); + this.setEndpoints(pConfig); + this.setClientId(pConfig); + this.setClientSecret(pConfig); + this.setScopes(pConfig); + this.setAdditionalParams(pConfig); + this.setButton(pConfig); + this.setMapping(this.getId(), pConfig); + } + + + /** + * Get id. + * + * @return id as String. + */ + public String getId() { + return _id; + } + + protected void setId(final Map pConfig) { + _id = (String)pConfig.get("id"); + } + + /** + * Get endpoints. + * + * @return endpoints as DefaultApi20. + */ + public DefaultApi20 getEndpoints() { + return _endpoints; + } + + protected void setEndpoints(final Map pConfig) throws IOException { + if (((String)pConfig.get("class")).equals("uk.org.llgc.annotation.store.data.login.GenericOAuth")) { + _endpoints = new GenericOAuth((Map)pConfig.get("endpoints")); + } else { + Class tClass = null; + try { + tClass = Class.forName((String)pConfig.get("class")); + } catch (ClassNotFoundException tExcpt) { + throw new IOException("Failed to load auth class " + (String)pConfig.get("class") + " due to " + tExcpt); + } catch (LinkageError tExcpt) { + throw new IOException("Failed to load auth class " + (String)pConfig.get("class") + " due to " + tExcpt); + } + Method tMethod = null; + try { + tMethod = tClass.getMethod("instance", null); + } catch (NoSuchMethodException tExcpt) { + throw new IOException("Failed to load auth class " + (String)pConfig.get("class") + " due to problem loading the instance() method: " + tExcpt); + } + + try { + _endpoints = (DefaultApi20)tMethod.invoke(null); + } catch (ExceptionInInitializerError tExcpt) { + throw new IOException("Failed to load auth class " + (String)pConfig.get("class") + " due to problem running the instance() method: " + tExcpt); + } catch (ReflectiveOperationException tExcpt) { + throw new IOException("Failed to load auth class " + (String)pConfig.get("class") + " due to problem running the instance() method: " + tExcpt); + } + } + } + + /** + * Get clientId. + * + * @return clientId as String. + */ + public String getClientId() { + return _clientId; + } + + protected void setClientId(final Map pConfig) { + _clientId = (String)pConfig.get("clientId"); + } + + /** + * Get clientSecret. + * + * @return clientSecret as String. + */ + public String getClientSecret() { + return _clientSecret; + } + + protected void setClientSecret(final Map pConfig) { + _clientSecret = (String)pConfig.get("clientSecret"); + } + + public String getScopes() { + return _scope; + } + + public void setScopes(final Map pConfig) { + if (pConfig.get("scope") != null) { + _scope = (String)pConfig.get("scope"); + } else { + _scope = null; + } + } + + public Map getAdditionalParams() { + return _additionalParams; + } + + public void setAdditionalParams(final Map pConfig) { + if (pConfig.get("additionalParam") != null) { + _additionalParams = (Map)pConfig.get("additionalParam"); + } else { + _additionalParams = null; // Optional config + } + } + + + /** + * Get button. + * + * @return button as Button. + */ + public Button getButton() { + return _button; + } + + protected void setButton(final Map pConfig) { + _button = new Button((Map)pConfig.get("button")); + } + + /** + * Get mapping. + * + * @return mapping as UserMapping. + */ + public UserMapping getMapping() { + return _mapping; + } + + protected void setMapping(final String pType, final Map pConfig) { + _mapping = new UserMapping(pType, (Map)pConfig.get("userMapping")); + } + +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/login/UserMapping.java b/src/main/java/uk/org/llgc/annotation/store/data/login/UserMapping.java new file mode 100644 index 00000000..9bc4c265 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/login/UserMapping.java @@ -0,0 +1,58 @@ +package uk.org.llgc.annotation.store.data.login; + +import java.util.Map; + +import java.net.URISyntaxException; + +import uk.org.llgc.annotation.store.data.users.User; + +public class UserMapping { + protected String _type = ""; + protected String _endpoint = ""; + protected Map _mapping = null; + public UserMapping(final String pType, final Map pConfig) { + _type = pType; + _endpoint = (String)pConfig.get("endpoint"); + + _mapping = (Map)pConfig.get("responseKeys"); + } + + public String getEndpoint() { + return _endpoint; + } + + public User createUser(final String pBaseURL, final Map pResponse) { + User tUser = null; + try { + + tUser = User.createUserFromShortID(pBaseURL, Math.abs(_type.hashCode()) + "/" + this.getKey("id", pResponse)); + } catch (URISyntaxException tExcpt) { + System.err.println("Failed to create user URI but failed due to " + tExcpt); + tExcpt.printStackTrace(); + return null; + } + if (this.isIn("name", pResponse)) { + tUser.setName(this.getKey("name", pResponse)); + } + if (this.isIn("email", pResponse)) { + tUser.setEmail(this.getKey("email", pResponse)); + } + if (this.isIn("pic", pResponse)) { + tUser.setPicture(this.getKey("pic", pResponse)); + } + + return tUser; + } + + protected boolean isIn(final String pKey, final Map pMap) { + if (_mapping.get(pKey) != null) { + return pMap.get(_mapping.get(pKey)) != null; + } else { + return false; + } + } + + protected String getKey(final String pKey, final Map pMap) { + return pMap.get(_mapping.get(pKey)).toString(); + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/stats/TopLevel.java b/src/main/java/uk/org/llgc/annotation/store/data/stats/TopLevel.java new file mode 100644 index 00000000..de57d7f7 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/stats/TopLevel.java @@ -0,0 +1,64 @@ +package uk.org.llgc.annotation.store.data.stats; + +public class TopLevel { + protected int _totalAnnotations = 0; + protected int _totalManifests = 0; + protected int _totalAnnoCanvases = 0; + + public TopLevel() { + } + + /** + * Get totalAnnotations. + * + * @return totalAnnotations as int. + */ + public int getTotalAnnotations() { + return _totalAnnotations; + } + + /** + * Set totalAnnotations. + * + * @param totalAnnotations the value to set. + */ + public void setTotalAnnotations(final int pTotalAnnotations) { + _totalAnnotations = pTotalAnnotations; + } + + /** + * Get totalManifests. + * + * @return totalManifests as int. + */ + public int getTotalManifests() { + return _totalManifests; + } + + /** + * Set totalManifests. + * + * @param totalManifests the value to set. + */ + public void setTotalManifests(final int pTotalManifests) { + _totalManifests = pTotalManifests; + } + + /** + * Get totalAnnoCanvases. + * + * @return totalAnnoCanvases as int. + */ + public int getTotalAnnoCanvases() { + return _totalAnnoCanvases; + } + + /** + * Set totalAnnoCanvases. + * + * @param totalAnnoCanvases the value to set. + */ + public void setTotalAnnoCanvases(final int pTotalAnnoCanvases) { + _totalAnnoCanvases = pTotalAnnoCanvases; + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/users/LocalUser.java b/src/main/java/uk/org/llgc/annotation/store/data/users/LocalUser.java new file mode 100644 index 00000000..b598cf2e --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/users/LocalUser.java @@ -0,0 +1,62 @@ +package uk.org.llgc.annotation.store.data.users; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class LocalUser extends User { + public final static String AUTH_METHOD = "local SAS authentication"; + + // bcrypt hash of password + protected String _password = null; + + public LocalUser() { + super(); + } + + public PasswordEncoder getEncoder() { + return new BCryptPasswordEncoder(); + } + + public boolean hasPassword() { + return _password != null && _password.trim().length() != 0; + } + + public void setPassword(final String pPassword) { + this.setPassword(pPassword, true); + } + + public void setPassword(final String pPassword, final boolean pEncode) { + if (pEncode) { + PasswordEncoder tEncoder = this.getEncoder(); + _password = this.getEncoder().encode(pPassword); + } else { + _password = pPassword; + } + } + + public String getPassword() { + return _password; + } + + public boolean authenticate(final String pPassword) { + return this.getEncoder().matches(pPassword, _password); + } + + public void setAuthenticationMethod() { + } + + public String getAuthenticationMethod() { + return AUTH_METHOD; + } + + public String toString() { + StringBuffer tBuffer = new StringBuffer("Local User:\n"); + + tBuffer.append("Has password: "); + tBuffer.append(this.hasPassword()); + tBuffer.append("\n"); + tBuffer.append(super.toString()); + + return tBuffer.toString(); + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/data/users/User.java b/src/main/java/uk/org/llgc/annotation/store/data/users/User.java new file mode 100644 index 00000000..de0f3aa2 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/data/users/User.java @@ -0,0 +1,256 @@ +package uk.org.llgc.annotation.store.data.users; + +import com.github.scribejava.core.model.OAuth2AccessToken; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.util.Date; + +public class User { + protected static Logger _logger = LogManager.getLogger(User.class.getName()); + + protected String _id = ""; + protected String _shortId = ""; + protected String _name = ""; + protected String _email = ""; + protected String _pic = ""; + protected boolean _isAdmin = false; + protected OAuth2AccessToken _token = null; + protected String _authenticationMethod = ""; + protected Date _created = null; + protected Date _lastModified = null; + + public User() { + _created = new Date(); + _lastModified = _created; + } + + public Date getCreated() { + return _created; + } + + public void setCreated(final Date pDate) { + _created = pDate; + } + + public Date getLastModified() { + return _lastModified; + } + + public void setLastModified(final Date pDate) { + _lastModified = pDate; + } + + public Date updateLastModified() { + _lastModified = new Date(); + return _lastModified; + } + + public boolean isAdmin() { + return _isAdmin; + } + + public void setAdmin(final boolean pValue) { + _isAdmin = pValue; + } + + public String getAuthenticationMethod() { + return _authenticationMethod; + } + + public void setAuthenticationMethod(final String pAuthMethod) { + _authenticationMethod = pAuthMethod; + } + + public String getAvatar() { + /* String[] tNames = _name.split(" "); + if (tNames.length > 1) { + return tNames[0].substring(0,1).toUpperCase() + tNames[1].substring(0,1).toUpperCase(); + } else { */ + return _name.substring(0,1).toUpperCase(); + // } + } + + public static User createUserFromShortID(final String pBaseURI, final String pShortID) throws URISyntaxException { + User tUser = new User(); + tUser.setId(pBaseURI + "/user/" + pShortID); + tUser.setShortId(pShortID); + return tUser; + } + + public static User createUserFromID(final String pURI) throws URISyntaxException { + User tUser = new User(); + tUser.setId(pURI); + tUser.setShortId(pURI.substring(pURI.lastIndexOf("/user/") + 6)); + + return tUser; + } + + public String toString() { + StringBuffer tBuffer = new StringBuffer("Id: "); + tBuffer.append(_id); + tBuffer.append("\nShortid: "); + tBuffer.append(_shortId); + tBuffer.append("\nName: "); + tBuffer.append(_name); + tBuffer.append("\nEmail: "); + tBuffer.append(_email); + tBuffer.append("\nPic: "); + tBuffer.append(_pic); + tBuffer.append("\nCreated: "); + if (_created != null) { + tBuffer.append(_created.getTime()); + } else { + tBuffer.append(" is null"); + } + tBuffer.append("\nAdmin: "); + tBuffer.append(_isAdmin); + tBuffer.append("\nAuth method: "); + tBuffer.append(_authenticationMethod); + + return tBuffer.toString(); + } + + /** + * Get id. + * + * @return id as String. + */ + public String getId() { + return _id; + } + + /** + * Set id. + * + * @param id the value to set. + */ + public void setId(final String pId) throws URISyntaxException { + new URI(pId); + _id = pId; + } + + public String getShortId() { + return _shortId; + } + + public void setShortId(final String pShortId) { + _shortId = pShortId; + } + + /** + * Get name. + * + * @return name as String. + */ + public String getName() { + return _name; + } + + /** + * Set name. + * + * @param name the value to set. + */ + public void setName(final String pName) { + _name = pName; + } + + /** + * Get email. + * + * @return email as String. + */ + public String getEmail() { + return _email; + } + + /** + * Set email. + * + * @param email the value to set. + */ + public void setEmail(final String pEmail) { + _email = pEmail; + } + + /** + * Get pic. + * + * @return pic as String. + */ + public String getPicture() { + return _pic; + } + + /** + * Set pic. + * + * @param pic the value to set. + */ + public void setPicture(final String pPic) { + _pic = pPic; + } + + /** + * Get token. + * + * @return token as OAuth2AccessToken. + */ + public OAuth2AccessToken getToken() { + return _token; + } + + /** + * Set token. + * + * @param token the value to set. + */ + public void setToken(final OAuth2AccessToken pToken) { + _token = pToken; + } + + public boolean equals(final Object pOther) { + if (!(pOther instanceof User)) { + return false; + } + User pOtherUser = (User)pOther; + if (_token != null && pOtherUser.getToken() != null) { + if (!_token.getAccessToken().equals(pOtherUser.getToken().getAccessToken())) { + _logger.debug("Token different"); + return false; + } + } else { + if (!(_token == null && pOtherUser.getToken() == null)) { + _logger.debug("Token different one was null"); + return false; + } + } + if (_pic == null) { + if (pOtherUser.getPicture() != null) { + _logger.debug("Pic different"); + return false; + } + } else if (!_pic.equals(pOtherUser.getPicture())) { + _logger.debug("Pic different"); + return false; + } + _logger.debug("ID " + _id.equals(pOtherUser.getId())); + _logger.debug("shortid " + _shortId.equals(pOtherUser.getShortId())); + _logger.debug("name " + _name.equals(pOtherUser.getName())); + _logger.debug("email " + _email.equals(pOtherUser.getEmail())); + _logger.debug("admin " + (_isAdmin == pOtherUser.isAdmin())); + _logger.debug("authMethod " + _authenticationMethod.equals(pOtherUser.getAuthenticationMethod())); + return _id.equals(pOtherUser.getId()) + && _shortId.equals(pOtherUser.getShortId()) + && _name.equals(pOtherUser.getName()) + && _email.equals(pOtherUser.getEmail()) + && _isAdmin == pOtherUser.isAdmin() + && _created.equals(pOtherUser.getCreated()) + && _authenticationMethod.equals(pOtherUser.getAuthenticationMethod()); + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/exceptions/PermissionDenied.java b/src/main/java/uk/org/llgc/annotation/store/exceptions/PermissionDenied.java new file mode 100644 index 00000000..7ad79cd1 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/exceptions/PermissionDenied.java @@ -0,0 +1,13 @@ +package uk.org.llgc.annotation.store.exceptions; + +public class PermissionDenied extends Exception { + public PermissionDenied() { + super(); + } + + public PermissionDenied(final String pMessage) { + super(pMessage); + } +} + + diff --git a/src/main/java/uk/org/llgc/annotation/store/filters/AdminFilter.java b/src/main/java/uk/org/llgc/annotation/store/filters/AdminFilter.java new file mode 100644 index 00000000..1f92a352 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/filters/AdminFilter.java @@ -0,0 +1,64 @@ +package uk.org.llgc.annotation.store.filters; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; + +import java.io.IOException; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.apis.GoogleApi20; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +import java.util.Random; +import java.util.Map; +import java.util.HashMap; + +import uk.org.llgc.annotation.store.servlets.login.LoginCallback; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; +import uk.org.llgc.annotation.store.StoreConfig; + +public class AdminFilter implements Filter { + protected String _loginPage = "/admin.xhtml"; + + public void init(final FilterConfig pConfig) { + } + + public void doFilter(final ServletRequest pRequest, final ServletResponse pRes, final FilterChain pChain) throws IOException, ServletException { + HttpServletRequest pReq = (HttpServletRequest)pRequest; + if (StoreConfig.getConfig().isAuth()) { + HttpSession tSession = pReq.getSession(); + UserService tUsers = new UserService(pReq); + AuthorisationController tAuth = new AuthorisationController(tUsers); + if (!tUsers.isAuthenticated() && !tAuth.allowThrough((HttpServletRequest)pRequest)) { + String tCallingURL = pReq.getRequestURI(); + if (pReq.getQueryString() != null) { + tCallingURL += "?" + pReq.getQueryString(); + } + + tSession.setAttribute("oauth_url", tCallingURL); + + ((HttpServletResponse)pRes).sendRedirect(_loginPage); + //} + return; + /*} else { + System.out.println("Found logged in user: " + tSession.getAttribute("user"));*/ + } + } + pChain.doFilter(pReq, pRes); + } + + public void destroy() { + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/filters/CorsFilter.java b/src/main/java/uk/org/llgc/annotation/store/filters/CorsFilter.java index 41106957..92ff5aff 100644 --- a/src/main/java/uk/org/llgc/annotation/store/filters/CorsFilter.java +++ b/src/main/java/uk/org/llgc/annotation/store/filters/CorsFilter.java @@ -7,6 +7,7 @@ import javax.servlet.ServletResponse; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; @@ -16,10 +17,17 @@ public void init(final FilterConfig filterConfig) { } public void doFilter(final ServletRequest pReq, final ServletResponse pRes, final FilterChain pChain) throws IOException, ServletException { + HttpServletResponse pResponse = (HttpServletResponse)pRes; + HttpServletRequest pRequest = (HttpServletRequest)pReq; // Never seems to returng from doFilter so setting CORS headers early. - ((HttpServletResponse)pRes).addHeader("Access-Control-Allow-Origin", "*"); - ((HttpServletResponse)pRes).addHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type"); - ((HttpServletResponse)pRes).addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); + pResponse.addHeader("Access-Control-Allow-Origin", "*"); + pResponse.addHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type"); + pResponse.addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); + + if (pRequest.getRequestURI().endsWith("xhtml")) { + // Don't cache xhtml files + pResponse.addHeader("Cache-Control", "no-store, max-age=0"); + } pChain.doFilter(pReq, pRes); } diff --git a/src/main/java/uk/org/llgc/annotation/store/filters/OAuthFilter.java b/src/main/java/uk/org/llgc/annotation/store/filters/OAuthFilter.java new file mode 100644 index 00000000..b788c6e5 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/filters/OAuthFilter.java @@ -0,0 +1,75 @@ +package uk.org.llgc.annotation.store.filters; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; + +import java.io.IOException; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.apis.GoogleApi20; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +import java.util.Random; +import java.util.Map; +import java.util.HashMap; + +import uk.org.llgc.annotation.store.servlets.login.LoginCallback; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; +import uk.org.llgc.annotation.store.StoreConfig; + +public class OAuthFilter implements Filter { + protected String _loginPage = "/login.xhtml"; + + public void init(final FilterConfig pConfig) { + if (pConfig.getInitParameter("LOGIN_PAGE") != null) { + // Allow default login page to be overwritten + _loginPage = pConfig.getInitParameter("LOGIN_PAGE"); + } + } + + public void doFilter(final ServletRequest pRequest, final ServletResponse pRes, final FilterChain pChain) throws IOException, ServletException { + HttpServletRequest pReq = (HttpServletRequest)pRequest; + if (StoreConfig.getConfig().isAuth()) { + HttpSession tSession = pReq.getSession(); + UserService tUsers = new UserService(pReq); + AuthorisationController tAuth = new AuthorisationController(tUsers); + if (!tUsers.isAuthenticated() && !tAuth.allowThrough((HttpServletRequest)pRequest)) { + String tCallingURL = pReq.getRequestURI(); + if (pReq.getQueryString() != null) { + tCallingURL += "?" + pReq.getQueryString(); + } + + tSession.setAttribute("oauth_url", tCallingURL); + /* + This auto login page is too confusing especially if you've logged out as it + logs you straight back in... + if (StoreConfig.getConfig().getAuthTargets().size() == 1) { + // if there is only 1 target forward straight onto the oauth process + ((HttpServletResponse)pRes).sendRedirect("/login?type=" + StoreConfig.getConfig().getAuthTargets().get(0).getId()); + } else {*/ + // Otherwise ask the user how they want to authenticate + ((HttpServletResponse)pRes).sendRedirect(_loginPage); + //} + return; + /*} else { + System.out.println("Found logged in user: " + tSession.getAttribute("user"));*/ + } + } + pChain.doFilter(pReq, pRes); + } + + public void destroy() { + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/AnnotationListServlet.java b/src/main/java/uk/org/llgc/annotation/store/servlets/AnnotationListServlet.java index e6d10ad1..02470b2f 100644 --- a/src/main/java/uk/org/llgc/annotation/store/servlets/AnnotationListServlet.java +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/AnnotationListServlet.java @@ -11,13 +11,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Map; +import java.util.HashMap; + import java.io.IOException; +import java.net.URISyntaxException; + import uk.org.llgc.annotation.store.adapters.StoreAdapter; import uk.org.llgc.annotation.store.StoreConfig; import uk.org.llgc.annotation.store.AnnotationUtils; import uk.org.llgc.annotation.store.data.Canvas; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.data.AnnotationList; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; public class AnnotationListServlet extends HttpServlet { protected static Logger _logger = LogManager.getLogger(AnnotationList.class.getName()); @@ -30,16 +38,42 @@ public void init(final ServletConfig pConfig) throws ServletException { } public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { - String[] tRequestURI = pReq.getRequestURI().split("/"); - String tCanvasShortID = tRequestURI[tRequestURI.length -1].replace(".json",""); + String relativeId = pReq.getRequestURI().substring(pReq.getRequestURI().lastIndexOf("/annotations/") + "/annotations/".length()); + int tLastSlash = relativeId.lastIndexOf("/"); + String tFilename = relativeId.substring(tLastSlash + 1); + User tUser = null; + try { + tUser = _store.getUser(User.createUserFromShortID(StoreConfig.getConfig().getBaseURI(pReq), relativeId.substring(0, tLastSlash))); + } catch (URISyntaxException tExcpt) { + throw new IOException("Unable to create user due to " + tExcpt); + } - Canvas tCanvas = _store.resolveCanvas(tCanvasShortID); + Canvas tCanvas = _store.resolveCanvas(tFilename.split("\\.")[0]); - AnnotationList tList = _store.getAnnotationsFromPage(tCanvas); - tList.setId(StoreConfig.getConfig().getBaseURI(pReq) + "/annotation/list/" + tCanvasShortID + ".json"); + AuthorisationController tAuth = new AuthorisationController(pReq); + if (tAuth.allowReadAnnotations(tCanvas, tUser)) { + AnnotationList tList = new AnnotationList(); + String tAnnoId = StoreConfig.getConfig().getBaseURI(pReq) + "/annotations/" + relativeId; + if (tUser != null && tCanvas != null) { + tList = _store.getAnnotationsFromPage(tUser, tCanvas); + tList.setId(tAnnoId); + } + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + pRes.getWriter().println(JsonUtils.toPrettyString(tList.toJson())); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You are not allowed to view this users annotations"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } - pRes.setContentType("application/ld+json; charset=UTF-8"); + protected void sendJson(final HttpServletResponse pRes, final int pCode, final Map pPayload) throws IOException { + pRes.setStatus(pCode); + pRes.setContentType("application/json"); pRes.setCharacterEncoding("UTF-8"); - pRes.getWriter().println(JsonUtils.toPrettyString(tList.toJson())); + JsonUtils.writePrettyPrint(pRes.getWriter(), pPayload); } + } diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/CollectionServlet.java b/src/main/java/uk/org/llgc/annotation/store/servlets/CollectionServlet.java new file mode 100644 index 00000000..028743df --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/CollectionServlet.java @@ -0,0 +1,265 @@ +package uk.org.llgc.annotation.store.servlets; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import com.github.jsonldjava.utils.JsonUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.BufferedReader; +import java.io.UnsupportedEncodingException; +import java.io.InputStreamReader; + +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.adapters.StoreAdapter; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.data.Collection; +import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.users.User; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Scanner; + +import java.net.URLDecoder; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class CollectionServlet extends HttpServlet { + protected static Logger _logger = LogManager.getLogger(CollectionServlet.class.getName()); + protected StoreAdapter _store = null; + + public void init(final ServletConfig pConfig) throws ServletException { + super.init(pConfig); + _store = StoreConfig.getConfig().getStore(); + } + + public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + User tUser = new UserService(pReq).getUser(); + if (pReq.getRequestURI().endsWith("collection/all.json")) { + // get list of Collections + // TODO get from helper!!! + List tCollections = _store.getCollections(tUser); + // if empty create the default collection + if (tCollections.isEmpty()) { + Collection tDefaultCollection = new Collection(); + tDefaultCollection.setUser(tUser); + tDefaultCollection.setLabel(StoreConfig.getConfig().getDefaultCollectionName()); + tDefaultCollection.createDefaultId(StoreConfig.getConfig().getBaseURI(pReq)); + tDefaultCollection = _store.createCollection(tDefaultCollection); + tCollections.add(tDefaultCollection); + } + + Collections.sort(tCollections); + + Map tCollection = new HashMap(); + + tCollection.put("@context", "http://iiif.io/api/presentation/2/context.json"); + tCollection.put("@id", StoreConfig.getConfig().getBaseURI(pReq) + "/collection/all.json"); + tCollection.put("@type", "sc:Collection"); + tCollection.put("label","Collection of all collections for this user"); + + List> tMembers = new ArrayList>(); + for (Collection tCollectionObj : tCollections) { + tMembers.add(tCollectionObj.toJson()); + } + + //tCollection.put("members", tMembers); + tCollection.put("collections", tMembers); + + pRes.setStatus(HttpServletResponse.SC_OK); + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.write(pRes.getWriter(), tCollection); + } else { + String relativeId = pReq.getRequestURI().substring(pReq.getRequestURI().lastIndexOf("/collection/")); + String tCollectionId = StoreConfig.getConfig().getBaseURI(pReq) + relativeId; + + Collection tCollection = null; + //try { + tCollection = _store.getCollection(tCollectionId); + /*} catch (NullPointerException tExcpt) { + // Failed to get collection so delete it useful during development + tCollection = new Collection(); + tCollection.setId(tCollectionId); + + _store.deleteCollection(tCollection); + tCollection = null; + }*/ + if (tCollection != null) { + AuthorisationController tAuth = new AuthorisationController(pReq); + if (tAuth.allowViewCollection(tCollection)) { + pRes.setStatus(HttpServletResponse.SC_OK); + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.write(pRes.getWriter(), tCollection.toJson()); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only view your own collections"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_NOT_FOUND); + tResponse.put("message", "Collection not found."); + this.sendJson(pRes, pRes.SC_NOT_FOUND, tResponse); + } + } + } + + public void doDelete(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + User tUser = new UserService(pReq).getUser(); + + String relativeId = pReq.getRequestURI().substring(pReq.getRequestURI().lastIndexOf("/collection/")); + if (relativeId.endsWith("inbox.json")) { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_FORBIDDEN); + tResponse.put("message", "You can't remove your default collection."); + this.sendJson(pRes, pRes.SC_FORBIDDEN, tResponse); + } else { + String tCollectionId = StoreConfig.getConfig().getBaseURI(pReq) + relativeId; + Collection tExistingCollection = _store.getCollection(tCollectionId); + if (tExistingCollection == null) { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_NOT_FOUND); + tResponse.put("message", "Collection with URI " + tCollectionId + " not found"); + this.sendJson(pRes, pRes.SC_NOT_FOUND, tResponse); + } else { + AuthorisationController tAuth = new AuthorisationController(pReq); + if (tAuth.allowDeleteCollection(tExistingCollection)) { + _store.deleteCollection(tExistingCollection); + pRes.setStatus(HttpServletResponse.SC_OK); + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.write(pRes.getWriter(), tExistingCollection.toJson()); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own collections unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } + } + } + + + public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + // create collection usually empty just with id and label + User tUser = new UserService(pReq).getUser(); + + Collection tCollection = new Collection(); + tCollection.setUser(tUser); + tCollection.setLabel(pReq.getParameter("name")); + tCollection.createId(StoreConfig.getConfig().getBaseURI(pReq) + "/collection/"); + tCollection = _store.createCollection(tCollection); + + pRes.setStatus(HttpServletResponse.SC_OK); + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.write(pRes.getWriter(), tCollection.toJson()); + } + + public void doPut(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + AuthorisationController tAuth = new AuthorisationController(pReq); + + Map tParams = (Map)JsonUtils.fromInputStream(pReq.getInputStream()); + System.out.println("Params " + tParams); + + if (tParams.get("name") != null && tParams.get("rename_id") != null) { + // This is a rename collection request + Collection tCollection = _store.getCollection(tParams.get("rename_id")); + if (tAuth.allowCollectionEdit(tCollection)) { + tCollection.setLabel(tParams.get("name")); + + _store.updateCollection(tCollection); + + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_OK); + tResponse.put("message", "Succesfully renamed collection"); + tResponse.put("@id", tCollection.getId()); + this.sendJson(pRes, pRes.SC_OK, tResponse); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own collections unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } else { + System.out.println("From: '" + tParams.get("from") + "' To: '" + tParams.get("to") + "' Manifest: '" + tParams.get("manifest") + "'"); + // Update collection typically moving manifests + User tUser = new UserService(pReq).getUser(); + Collection tFrom = _store.getCollection(tParams.get("from")); + if (tParams.get("to") != null) { + // this is a move request + Collection tTo = _store.getCollection(tParams.get("to")); + + if (tAuth.allowCollectionEdit(tFrom) && tAuth.allowCollectionEdit(tTo)) { + Manifest tManifest = new Manifest(); + tManifest.setURI(tParams.get("manifest")); + Manifest tFullManifest = _store.getManifest(tManifest.getURI()); + if (tFullManifest != null) { + tManifest = tFullManifest; + } + + tFrom.remove(tManifest); + _store.updateCollection(tFrom); + + tTo.add(tManifest); + _store.updateCollection(tTo); + + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_OK); + tResponse.put("message", "Succesfully moved manifest to new collection"); + this.sendJson(pRes, pRes.SC_OK, tResponse); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own collections unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } else { + // This is a remove manifest from collection request + if (tAuth.allowCollectionEdit(tFrom)) { + Manifest tManifest = new Manifest(); + tManifest.setURI(tParams.get("manifest")); + Manifest tFullManifest = _store.getManifest(tManifest.getURI()); + if (tFullManifest != null) { + tManifest = tFullManifest; + } + + tFrom.remove(tManifest); + _store.updateCollection(tFrom); + + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_OK); + tResponse.put("message", "Succesfully removed manifest from collection"); + this.sendJson(pRes, pRes.SC_OK, tResponse); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own collections unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } + } + } + + protected void sendJson(final HttpServletResponse pRes, final int pCode, final Map pPayload) throws IOException { + pRes.setStatus(pCode); + pRes.setContentType("application/json"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.writePrettyPrint(pRes.getWriter(), pPayload); + } + +} diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/ManifestUpload.java b/src/main/java/uk/org/llgc/annotation/store/servlets/ManifestUpload.java index c5af2c03..ddad8cab 100644 --- a/src/main/java/uk/org/llgc/annotation/store/servlets/ManifestUpload.java +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/ManifestUpload.java @@ -16,8 +16,10 @@ import java.io.FileReader; import java.io.FilenameFilter; import java.io.BufferedReader; +import java.io.InputStreamReader; import java.net.URL; +import java.net.URISyntaxException; import java.util.Map; import java.util.List; @@ -25,6 +27,7 @@ import java.util.ArrayList; import com.github.jsonldjava.utils.JsonUtils; +import com.fasterxml.jackson.core.JsonParseException; import org.apache.jena.rdf.model.Model; @@ -33,8 +36,14 @@ import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.data.ManifestProcessor; import uk.org.llgc.annotation.store.data.Manifest; +import uk.org.llgc.annotation.store.data.Collection; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.AnnotationUtils; import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.StoreService; + public class ManifestUpload extends HttpServlet { protected static Logger _logger = LogManager.getLogger(ManifestUpload.class.getName()); @@ -58,66 +67,181 @@ public void init(final ServletConfig pConfig) throws ServletException { } public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { - String tID = ""; - Map tManifestJson = null; - if (pReq.getParameter("uri") != null) { - tID = pReq.getParameter("uri"); - tManifestJson = (Map)JsonUtils.fromInputStream(new URL(tID).openStream()); - } else { - InputStream tManifestStream = pReq.getInputStream(); - /*java.io.BufferedReader tReader = new java.io.BufferedReader(new java.io.InputStreamReader(tManifestStream)); - String tLine = tReader.readLine(); - while (tLine != null) { - System.out.println(tLine); - tLine = tReader.readLine(); - }*/ - - tManifestJson = (Map)JsonUtils.fromInputStream(tManifestStream); - } - - Manifest tManifest = new Manifest(tManifestJson, null); - String tShortId = _store.indexManifest(tManifest); - Map tJson = new HashMap(); - Map tLinks = new HashMap(); - tJson.put("loaded", tLinks); - tLinks.put("uri", tManifest.getURI()); - tLinks.put("short_id", tManifest.getShortId()); + try { + User tUser = new UserService(pReq).getUser(); + String tID = ""; + Map tManifestJson = null; + String tCollectionId = ""; + if (pReq.getParameter("uri") != null) { + tID = pReq.getParameter("uri"); + tManifestJson = (Map)JsonUtils.fromInputStream(new URL(tID).openStream()); + + if (pReq.getParameter("collection") != null) { + tCollectionId = pReq.getParameter("collection"); + } + } else { + StringBuffer tBuff = new StringBuffer(); + BufferedReader tReader = null; + if (pReq.getCharacterEncoding() != null) { + tReader = new BufferedReader(new InputStreamReader(pReq.getInputStream(), pReq.getCharacterEncoding())); + } else { + tReader = new BufferedReader(new InputStreamReader(pReq.getInputStream(), "UTF-8")); + } + String tLine = ""; + while ((tLine = tReader.readLine()) != null) { + tBuff.append(tLine); + } + + String tManifestStr = tBuff.toString(); + + if (tManifestStr.isEmpty()) { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_NOT_FOUND); + tResponse.put("message", "Manifest not found POST was empty"); + sendJson(pRes, HttpServletResponse.SC_NOT_FOUND, tResponse); + return; + } + + try { + // This calls the fromReader anyway so probably more efficient to pass the reader above. + tManifestJson = (Map)JsonUtils.fromString(tManifestStr); + } catch (JsonParseException tExcpt) { + System.out.println("Failed to load the following manifest: "); + System.out.println(tManifestStr); + throw tExcpt; + } + + if (tManifestJson.get("within") != null) { + tCollectionId = (String)tManifestJson.get("within"); + tManifestJson.remove("within"); + } + } + if (tCollectionId.isEmpty()) { + Collection tTmpCollection = new Collection(); + tTmpCollection.setUser(tUser); + tCollectionId = tTmpCollection.createDefaultId(StoreConfig.getConfig().getBaseURI(pReq)); + } + + Collection tCollection = _store.getCollection(tCollectionId); + if (tCollection == null) { + tCollection = _store.getCollection(StoreConfig.getConfig().getBaseURI(pReq) + "/collection/" + tUser.getShortId() + "/inbox.json"); + } + AuthorisationController tAuth = new AuthorisationController(pReq); + if (tAuth.allowCollectionEdit(tCollection)) { + Manifest tManifest = new Manifest(tManifestJson, null); + if (!tCollection.getManifests().contains(tManifest)) { + System.out.println("Adding Manifest " + tManifest); + String tShortId = _store.indexManifest(tManifest); + + System.out.println("To collection " + tCollection); + tCollection.getManifests().add(tManifest); + _store.updateCollection(tCollection); + } + + Map tJson = new HashMap(); + Map tLinks = new HashMap(); + tJson.put("loaded", tLinks); + tLinks.put("uri", tManifest.getURI()); + tLinks.put("short_id", tManifest.getShortId()); + + this.sendJson(pRes, pRes.SC_OK, tJson); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own collections unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } catch (IOException tExcpt) { + tExcpt.printStackTrace(); + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_INTERNAL_SERVER_ERROR); + tResponse.put("message", "Failed to process manifest due to: " + tExcpt.getMessage()); + this.sendJson(pRes, pRes.SC_INTERNAL_SERVER_ERROR, tResponse); + } catch (Exception tExcpt) { + tExcpt.printStackTrace(); + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_INTERNAL_SERVER_ERROR); + tResponse.put("message", "Failed to process manifest due to: " + tExcpt.getMessage()); + this.sendJson(pRes, pRes.SC_INTERNAL_SERVER_ERROR, tResponse); + } + } + protected void sendJson(final HttpServletResponse pRes, final int pCode, final Map pPayload) throws IOException { + pRes.setStatus(pCode); pRes.setContentType("application/json"); pRes.setCharacterEncoding("UTF-8"); - JsonUtils.write(pRes.getWriter(), tJson); - } + JsonUtils.writePrettyPrint(pRes.getWriter(), pPayload); + } // if asked for without path then return collection of manifests that are loaded public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { - String tRequestURI = pReq.getRequestURI(); - String[] tSplitURI = tRequestURI.split("/"); - - // Return collection - List tManifests = _store.getManifests(); - - Map tCollection = new HashMap(); - - tCollection.put("@context", "http://iiif.io/api/presentation/2/context.json"); - tCollection.put("@id", StoreConfig.getConfig().getBaseURI(pReq) + "/annotation//collection/managed.json"); - tCollection.put("@type", "sc:Collection"); - tCollection.put("label","Collection of all manifests known by this annotation server"); - - List> tMembers = new ArrayList>(); - for (Manifest tManifest : tManifests) { - Map tManifestJson = new HashMap(); - tManifestJson.put("@id", tManifest.getURI()); - tManifestJson.put("@type", "sc:Manifest"); - - tMembers.add(tManifestJson); - } - - tCollection.put("members", tMembers); - tCollection.put("manifests", tMembers); - - pRes.setStatus(HttpServletResponse.SC_CREATED); - pRes.setContentType("application/ld+json; charset=UTF-8"); - pRes.setCharacterEncoding("UTF-8"); - JsonUtils.write(pRes.getWriter(), tCollection); - } + String tCollectionId = pReq.getParameter("collection"); + String tManifestId = pReq.getParameter("manifest"); + AuthorisationController tAuth = new AuthorisationController(pReq); + + if (tManifestId != null) { + Manifest tManifest = _store.getManifest(tManifestId); + if (tManifest == null) { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_NOT_FOUND); + tResponse.put("message", "Manifest is not loaded"); + sendJson(pRes, HttpServletResponse.SC_NOT_FOUND, tResponse); + } else { + if (tCollectionId != null) { + Collection tCollection = _store.getCollection(tCollectionId); + if (tCollection != null && !tCollection.contains(tManifest)) { + if (tAuth.allowCollectionEdit(tCollection)) { + tCollection.add(tManifest); + _store.updateCollection(tCollection); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own collections unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + return; + } + } + } + Map tJson = new HashMap(); + Map tLinks = new HashMap(); + tJson.put("loaded", tLinks); + tLinks.put("uri", tManifest.getURI()); + tLinks.put("short_id", tManifest.getShortId()); + + this.sendJson(pRes, pRes.SC_OK, tJson); + } + } else { + // Get enriched manifest + boolean regenerate = pReq.getParameter("regenerate") != null && pReq.getParameter("regenerate").equals("true"); + String relativeId = pReq.getRequestURI().substring(pReq.getRequestURI().lastIndexOf("/manifests/") + "/manifests/".length()); + // 1245635613/1969268/5bbada360fbe7c8f72a8153896686398.json + int tLastSlash = relativeId.lastIndexOf("/"); + User tUser = new User(); + String tFilename = relativeId.substring(tLastSlash + 1); + try { + tUser = User.createUserFromShortID(StoreConfig.getConfig().getBaseURI(pReq), relativeId.substring(0, tLastSlash)); + } catch (URISyntaxException tExcpt) { + throw new IOException("Unable to create user due to " + tExcpt); + } + tUser = _store.getUser(tUser); + + String tURI = _store.getManifestId(tFilename.split("\\.json")[0]); + Manifest tManifest = new Manifest(); + tManifest.setURI(tURI); + + if (tAuth.allowReadManifest(tManifest, tUser)) { + StoreService tService = new StoreService(pReq); + + Manifest tFullManifest = tService.getEnhancedManifest(tUser, tManifest, regenerate); + + this.sendJson(pRes, pRes.SC_OK, tFullManifest.toJson()); + } else { + Map tResponse = new HashMap(); + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You are not allowed to see this users annotations"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + return; + } + } + } } diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/UserServlet.java b/src/main/java/uk/org/llgc/annotation/store/servlets/UserServlet.java new file mode 100644 index 00000000..0da096ff --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/UserServlet.java @@ -0,0 +1,116 @@ +package uk.org.llgc.annotation.store.servlets; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import com.github.jsonldjava.utils.JsonUtils; + +import java.io.IOException; + +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.adapters.StoreAdapter; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.users.LocalUser; +import uk.org.llgc.annotation.store.controllers.UserService; + +import java.util.Map; +import java.util.HashMap; + +import java.net.URISyntaxException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class UserServlet extends HttpServlet { + protected static Logger _logger = LogManager.getLogger(UserServlet.class.getName()); + protected StoreAdapter _store = null; + + public void init(final ServletConfig pConfig) throws ServletException { + super.init(pConfig); + _store = StoreConfig.getConfig().getStore(); + } + + public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + } + + public void doDelete(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + AuthorisationController tAuth = new AuthorisationController(pReq); + User tLoggedInUser = new UserService(pReq).getUser(); + + User tUser = new User(); + try { + tUser.setId(pReq.getParameter("uri")); + } catch (URISyntaxException tExcpt) { + _logger.debug("failed to delete " + tLoggedInUser + "due to: " + tLoggedInUser); + } + + Map tResponse = new HashMap(); + if (tAuth.deleteUser(tLoggedInUser, tUser)) { + _store.deleteUser(tUser); + tResponse.put("code", pRes.SC_OK); + tResponse.put("message", "User deleted"); + this.sendJson(pRes, pRes.SC_OK, tResponse); + } else { + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only delete users if you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } + + public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + AuthorisationController tAuth = new AuthorisationController(pReq); + User tLoggedInUser = new UserService(pReq).getUser(); + User tUser = getUser(pReq); + System.out.println("Saved user " + tUser); + Map tResponse = new HashMap(); + if (tAuth.changeUserDetails(tUser)) { + tUser.setName(pReq.getParameter("name")); + tUser.setEmail(pReq.getParameter("email")); + if (pReq.getParameter("password") != null && pReq.getParameter("password").trim().length() != 0) { + ((LocalUser)tUser).setPassword(pReq.getParameter("password")); + } + + System.out.println("Updated User " + tUser); + + User tUpdated = _store.saveUser(tUser); + if (tLoggedInUser.getId().equals(tUser.getId())) { + // If editing the logged in user copy across info and update session + tUser.setToken(tLoggedInUser.getToken()); + pReq.getSession().setAttribute("user", tUser); + } + tResponse.put("code", pRes.SC_OK); + tResponse.put("message", "Profile updated."); + this.sendJson(pRes, pRes.SC_OK, tResponse); + } else { + tResponse.put("code", pRes.SC_UNAUTHORIZED); + tResponse.put("message", "You can only edit your own details unless you are Admin"); + this.sendJson(pRes, pRes.SC_UNAUTHORIZED, tResponse); + } + } + + protected void sendJson(final HttpServletResponse pRes, final int pCode, final Map pPayload) throws IOException { + pRes.setStatus(pCode); + pRes.setContentType("application/json"); + pRes.setCharacterEncoding("UTF-8"); + JsonUtils.writePrettyPrint(pRes.getWriter(), pPayload); + } + + protected User getUser(final HttpServletRequest pReq) throws IOException { + String relativeId = pReq.getRequestURI().substring(pReq.getRequestURI().lastIndexOf("/user/")); + String tPersonURI = StoreConfig.getConfig().getBaseURI(pReq) + relativeId; + + User tSkeletonUser = new User(); + try { + tSkeletonUser.setId(tPersonURI); + tSkeletonUser.setShortId(relativeId.substring("/user/".length())); + } catch (URISyntaxException tExcpt) { + throw new IOException("Unable to add user because " + tPersonURI + " is not a valid URI"); + } + User tFullUser = _store.getUser(tSkeletonUser); + return tFullUser; + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/login/LoginCallback.java b/src/main/java/uk/org/llgc/annotation/store/servlets/login/LoginCallback.java new file mode 100644 index 00000000..edb370f1 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/login/LoginCallback.java @@ -0,0 +1,130 @@ +package uk.org.llgc.annotation.store.servlets.login; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.apis.GoogleApi20; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.net.URISyntaxException; + +import java.util.concurrent.ExecutionException; + +import java.io.IOException; + +import java.util.Collections; +import java.util.Random; +import java.util.Map; + +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.data.users.LocalUser; +import uk.org.llgc.annotation.store.data.login.OAuthTarget; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.StoreConfig; + +import com.github.jsonldjava.utils.JsonUtils; + +public class LoginCallback extends HttpServlet { + protected static Logger _logger = LogManager.getLogger(LoginCallback.class.getName()); + + public void init(final ServletConfig pConfig) throws ServletException { + super.init(pConfig); + } + + public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + HttpSession tSession = pReq.getSession(); + try { + OAuthTarget tTarget = (OAuthTarget)tSession.getAttribute("oauth_target"); + final OAuth20Service service = new ServiceBuilder(tTarget.getClientId()) + .apiSecret(tTarget.getClientSecret()) + .callback(StoreConfig.getConfig().getBaseURI(pReq) + "/login-callback") + .build(tTarget.getEndpoints()); + + OAuth2AccessToken accessToken = service.getAccessToken(pReq.getParameter("code")); + + final OAuthRequest request = new OAuthRequest(Verb.GET, tTarget.getMapping().getEndpoint()); + service.signRequest(accessToken, request); + Response tResponse = service.execute(request); + User tUser = tTarget.getMapping().createUser(StoreConfig.getConfig().getBaseURI(pReq),(Map)JsonUtils.fromString(tResponse.getBody())); + tUser.setToken(accessToken); + tUser.setAuthenticationMethod(tTarget.getId()); + + UserService tUsers = new UserService(pReq); + tUsers.setUser(tUser); + if (tSession.getAttribute("oauth_url") != null) { + pRes.sendRedirect((String)tSession.getAttribute("oauth_url")); + } else { + pRes.sendRedirect("collections.xhtml"); + } + } catch (InterruptedException tExcpt) { + tExcpt.printStackTrace(); + } catch (ExecutionException tExcpt) { + tExcpt.printStackTrace(); + // should redirect to login fail page + } + } + + // For local login + public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + final String tEmail = pReq.getParameter("email"); + final String tPassword = pReq.getParameter("password"); + HttpSession tSession = pReq.getSession(); + UserService tUsers = new UserService(pReq); + + LocalUser tUser = tUsers.getLocalUser(tEmail); + // If user not set by email is registered as a user + // then set password + if (tUser != null && !tUser.hasPassword()) { + tUsers.setUser(tUser); + pRes.sendRedirect("/profile.xhtml"); + } else if (tUser != null && tUser.authenticate(tPassword)) { + tUsers.setUser(tUser); + if (tSession.getAttribute("oauth_url") != null) { + pRes.sendRedirect((String)tSession.getAttribute("oauth_url")); + } else { + if (tUser.isAdmin()) { + pRes.sendRedirect("/admin/users.xhtml"); + } else { + pRes.sendRedirect("/collections.xhtml"); + } + } + + } else { + //throw 401 + pRes.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Failed to authenticate " + tEmail); + } + + /*LocalAuth tAuth = StoreConfig.getConfig().getLocalAuth(); + if (tAuth.authenticate(tEmail, tPassword)) { + try { + UserService tUsers = new UserService(tSession); + tUsers.setUser(tAuth.getUser(tEmail, StoreConfig.getConfig().getBaseURI(pReq))); + if (tSession.getAttribute("oauth_url") != null) { + pRes.sendRedirect((String)tSession.getAttribute("oauth_url")); + } else { + pRes.sendRedirect("/admin/users.xhtml"); + } + } catch (URISyntaxException tExcpt) { + String tMessage = "Config error in users config file. User " + tEmail + " has an invalid value for id"; + System.err.println(tMessage); + tExcpt.printStackTrace(); + pRes.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, tMessage); + } + } else { + //throw 401 + pRes.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Failed to authenticate " + tEmail); + }*/ + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/login/Logout.java b/src/main/java/uk/org/llgc/annotation/store/servlets/login/Logout.java new file mode 100644 index 00000000..d2d2e768 --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/login/Logout.java @@ -0,0 +1,35 @@ + + + +package uk.org.llgc.annotation.store.servlets.login; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Logout extends HttpServlet { + protected static Logger _logger = LogManager.getLogger(Logout.class.getName()); + protected String _redirectURL = "index.html"; + + public void init(final ServletConfig pConfig) throws ServletException { + super.init(pConfig); + if (pConfig.getInitParameter("post_logout_url") != null) { + _redirectURL = pConfig.getInitParameter("post_logout_url"); + } + } + + public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + HttpSession tSession = pReq.getSession(); + tSession.invalidate(); + pRes.sendRedirect(_redirectURL); + return; + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/login/OAuth.java b/src/main/java/uk/org/llgc/annotation/store/servlets/login/OAuth.java new file mode 100644 index 00000000..b221c9ed --- /dev/null +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/login/OAuth.java @@ -0,0 +1,66 @@ +package uk.org.llgc.annotation.store.servlets.login; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.oauth.OAuth20Service; + +import java.util.Random; +import java.util.Map; +import java.util.HashMap; + +import java.io.IOException; + +import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.data.login.OAuthTarget; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class OAuth extends HttpServlet { + protected static Logger _logger = LogManager.getLogger(OAuth.class.getName()); + + public void init(final ServletConfig pConfig) throws ServletException { + super.init(pConfig); + } + + public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + HttpSession tSession = pReq.getSession(); + if (pReq.getParameter("type") != null) { + OAuthTarget tTarget = StoreConfig.getConfig().getAuthTarget(pReq.getParameter("type")); + if (tTarget != null) { + final String secretState = tTarget.getId() + new Random().nextInt(999_999); + final OAuth20Service service = new ServiceBuilder(tTarget.getClientId()) + .apiSecret(tTarget.getClientSecret()) + .defaultScope(tTarget.getScopes()) // replace with desired scope + .callback(StoreConfig.getConfig().getBaseURI(pReq) + "/login-callback") + .build(tTarget.getEndpoints()); + _logger.debug("Sending callback " + StoreConfig.getConfig().getBaseURI(pReq) + "/login-callback"); + System.out.println("Sending callback " + StoreConfig.getConfig().getBaseURI(pReq) + "/login-callback with client_id " + tTarget.getClientId()); + + Map additionalParams = new HashMap<>(); + if (tTarget.getAdditionalParams() != null) { + additionalParams = tTarget.getAdditionalParams(); + } + //force to re-get refresh token (if user are asked not the first time) + //additionalParams.put("prompt", "consent"); + final String authorizationUrl = service.createAuthorizationUrlBuilder() + .state(secretState) + .additionalParams(additionalParams) + .build(); + tSession.setAttribute("oauth_state", secretState); + tSession.setAttribute("oauth_target", tTarget); + pRes.sendRedirect(authorizationUrl); + } else { + // Type parameter unrecognised + } + } else { + // No type parameter sent + } + } +} diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/CanvasAnnotations.java b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/CanvasAnnotations.java index 62c54408..6e83a6b7 100644 --- a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/CanvasAnnotations.java +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/CanvasAnnotations.java @@ -23,8 +23,11 @@ import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.Canvas; +import uk.org.llgc.annotation.store.data.Annotation; import uk.org.llgc.annotation.store.AnnotationUtils; import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; @@ -42,17 +45,38 @@ public void init(final ServletConfig pConfig) throws ServletException { } public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + UserService tUserService = new UserService(pReq); + _logger.debug("Annotations for page: " + pReq.getParameter("uri")); _logger.debug("media " + pReq.getParameter("media")); _logger.debug("limit " + pReq.getParameter("limit")); if (pReq.getParameter("uri") == null || pReq.getParameter("uri").trim().length() == 0) { return; // for some reason Mirador is sending blank uri requests } - AnnotationList tAnnoList = _store.getAnnotationsFromPage(new Canvas(pReq.getParameter("uri"), "")); + AnnotationList tAnnoList = _store.getAnnotationsFromPage(tUserService.getUser(), new Canvas(pReq.getParameter("uri"), "")); pRes.setContentType("application/ld+json; charset=UTF-8"); pRes.setCharacterEncoding("UTF-8"); /**/_logger.debug(JsonUtils.toPrettyString(tAnnoList.toJson().get("resources"))); pRes.getWriter().println(JsonUtils.toPrettyString(tAnnoList.toJson().get("resources"))); } + + public void doDelete(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + UserService tUserService = new UserService(pReq); + AuthorisationController tAuth = new AuthorisationController(tUserService); + + Canvas tCanvas = new Canvas(pReq.getParameter("canvas"), ""); + AnnotationList tList = _store.getAnnotationsFromPage(tUserService.getUser(), tCanvas); + for (Annotation tAnno : tList.getAnnotations()) { + if (tAuth.allowDelete(tAnno)) { + _store.deleteAnnotation(tAnno.getId()); + } else { + pRes.sendError(pRes.SC_FORBIDDEN, "You must be the owner of the annotation to delete it."); + return; + } + } + + pRes.setStatus(pRes.SC_NO_CONTENT, "Deleted all annotations from canvas " + tCanvas.getId()); + } + } diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Create.java b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Create.java index e80777c8..d5d1f2e2 100644 --- a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Create.java +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Create.java @@ -20,9 +20,11 @@ import uk.org.llgc.annotation.store.AnnotationUtils; import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.controllers.UserService; import uk.org.llgc.annotation.store.adapters.StoreAdapter; import uk.org.llgc.annotation.store.data.AnnotationList; import uk.org.llgc.annotation.store.data.Annotation; +import uk.org.llgc.annotation.store.data.users.User; import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.exceptions.IDConflictException; import uk.org.llgc.annotation.store.exceptions.MalformedAnnotation; @@ -43,14 +45,12 @@ public void init(final ServletConfig pConfig) throws ServletException { public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { Map tAnnotationJSON = _annotationUtils.readAnnotaion(pReq.getInputStream(), StoreConfig.getConfig().getBaseURI(pReq) + "/annotation"); - if (tAnnotationJSON.get("@context") instanceof String) { - Map tJsonContext = (Map)JsonUtils.fromInputStream(super.getServletContext().getResourceAsStream("/contexts/iiif-2.0.json")); - tAnnotationJSON.put("@context",tJsonContext.get("@context"));//"http://localhost:8080/bor/contexts/iiif-2.0.json"); // must have a remote context for a remote repo - } - _logger.debug("JSON in:"); + _logger.debug("JSON in:"); _logger.debug(JsonUtils.toPrettyString(tAnnotationJSON)); try { - Annotation tAnno = _store.addAnnotation(new Annotation(tAnnotationJSON)); + Annotation tAnno = new Annotation(tAnnotationJSON); + tAnno.setCreator(new UserService(pReq).getUser()); + tAnno = _store.addAnnotation(tAnno); pRes.setStatus(HttpServletResponse.SC_CREATED); pRes.setContentType("application/ld+json; charset=UTF-8"); diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Delete.java b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Delete.java index 3446610e..eb0c726d 100644 --- a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Delete.java +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Delete.java @@ -17,7 +17,11 @@ import uk.org.llgc.annotation.store.adapters.StoreAdapter; import uk.org.llgc.annotation.store.encoders.Encoder; import uk.org.llgc.annotation.store.AnnotationUtils; +import uk.org.llgc.annotation.store.data.Annotation; import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; public class Delete extends HttpServlet { protected static Logger _logger = LogManager.getLogger(Delete.class.getName()); @@ -32,9 +36,21 @@ public void init(final ServletConfig pConfig) throws ServletException { } public void doDelete(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { + UserService tUserService = new UserService(pReq); + AuthorisationController tAuth = new AuthorisationController(tUserService); _logger.debug("uri " + pReq.getParameter("uri")); String tURI = pReq.getParameter("uri"); - _store.deleteAnnotation(tURI); - pRes.setStatus(pRes.SC_NO_CONTENT); + + Annotation tSavedAnno = _store.getAnnotation(tURI); + if (tSavedAnno != null) { + if (tAuth.allowDelete(tSavedAnno)) { + _store.deleteAnnotation(tURI); + pRes.setStatus(pRes.SC_NO_CONTENT); + } else { + pRes.sendError(pRes.SC_FORBIDDEN, "You must be the owner of the annotation to delete it."); + } + } else { + pRes.setStatus(pRes.SC_NO_CONTENT, "Partly succeeded in that the annoation is no longer present but.. it wasn't there in the first place."); + } } } diff --git a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Update.java b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Update.java index bbf38313..0dc9dadc 100644 --- a/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Update.java +++ b/src/main/java/uk/org/llgc/annotation/store/servlets/oa/Update.java @@ -23,6 +23,9 @@ import uk.org.llgc.annotation.store.data.Annotation; import uk.org.llgc.annotation.store.AnnotationUtils; import uk.org.llgc.annotation.store.StoreConfig; +import uk.org.llgc.annotation.store.data.users.User; +import uk.org.llgc.annotation.store.controllers.UserService; +import uk.org.llgc.annotation.store.controllers.AuthorisationController; public class Update extends HttpServlet { protected static Logger _logger = LogManager.getLogger(Update.class.getName()); @@ -42,22 +45,29 @@ public void doGet(final HttpServletRequest pReq, final HttpServletResponse pRes) public void doPost(final HttpServletRequest pReq, final HttpServletResponse pRes) throws IOException { try { + UserService tUserService = new UserService(pReq); + AuthorisationController tAuth = new AuthorisationController(tUserService); + Map tAnnotationJSON = _annotationUtils.readAnnotaion(pReq.getInputStream(), StoreConfig.getConfig().getBaseURI(pReq) + "/annotation"); - if (tAnnotationJSON.get("@context") instanceof String) { - Map tJsonContext = (Map)JsonUtils.fromInputStream(super.getServletContext().getResourceAsStream("/contexts/iiif-2.0.json")); - tAnnotationJSON.put("@context",tJsonContext.get("@context"));//"http://localhost:8080/bor/contexts/iiif-2.0.json"); // must have a remote context for a remote repo - } + _logger.debug("JSON in:"); _logger.debug(JsonUtils.toPrettyString(tAnnotationJSON)); Annotation tUpdate = new Annotation(tAnnotationJSON); - Annotation tSavedAnno = _store.updateAnnotation(tUpdate); + tUpdate.setCreator(tUserService.getUser()); + + Annotation tSavedAnno = _store.getAnnotation(tUpdate.getId()); + if (tAuth.allowUpdate(tSavedAnno, tUpdate)) { + tSavedAnno = _store.updateAnnotation(tUpdate); - pRes.setStatus(HttpServletResponse.SC_CREATED); - pRes.setContentType("application/ld+json; charset=UTF-8"); - pRes.setCharacterEncoding("UTF-8"); - _logger.debug("JSON out:"); - _logger.debug(JsonUtils.toPrettyString(tSavedAnno.toJson())); - pRes.getWriter().println(JsonUtils.toPrettyString(tSavedAnno.toJson())); + pRes.setStatus(HttpServletResponse.SC_CREATED); + pRes.setContentType("application/ld+json; charset=UTF-8"); + pRes.setCharacterEncoding("UTF-8"); + _logger.debug("JSON out:"); + _logger.debug(JsonUtils.toPrettyString(tSavedAnno.toJson())); + pRes.getWriter().println(JsonUtils.toPrettyString(tSavedAnno.toJson())); + } else { + pRes.sendError(pRes.SC_FORBIDDEN, "You must be the owner of the annotation to edit it."); + } } catch (IOException tException) { System.err.println("Exception occured trying to add annotation:"); tException.printStackTrace(); diff --git a/src/main/resources/solr/conf/schema.xml b/src/main/resources/solr/conf/schema.xml index dbb0a147..47b54471 100644 --- a/src/main/resources/solr/conf/schema.xml +++ b/src/main/resources/solr/conf/schema.xml @@ -144,7 +144,6 @@ - @@ -156,6 +155,14 @@ + + + + + + + + diff --git a/src/main/webapp/WEB-INF/sas.properties b/src/main/webapp/WEB-INF/sas.properties index 2ba970f8..74438695 100644 --- a/src/main/webapp/WEB-INF/sas.properties +++ b/src/main/webapp/WEB-INF/sas.properties @@ -35,3 +35,10 @@ data_dir=data # Uncoment the following if you want to use ElasticSearch as a backend: #store=elastic #elastic_connection=http://localhost:9200/annotations + + +# Allow or not collections to be public? Default to yes +#public_collections=true + +# Default collection name. Inbox by default +# default_collection_name=Inbox diff --git a/src/main/webapp/WEB-INF/templates/collectionBarLayout.xhtml b/src/main/webapp/WEB-INF/templates/collectionBarLayout.xhtml new file mode 100644 index 00000000..4fc50696 --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/collectionBarLayout.xhtml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + +
    + +
    + +
    + + + +
    +
    + diff --git a/src/main/webapp/WEB-INF/templates/content-state.xhtml b/src/main/webapp/WEB-INF/templates/content-state.xhtml new file mode 100644 index 00000000..ed7ef6ab --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/content-state.xhtml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/addManifest.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/addManifest.xhtml new file mode 100644 index 00000000..f618a53a --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/addManifest.xhtml @@ -0,0 +1,26 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/confirm.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/confirm.xhtml new file mode 100644 index 00000000..38c3100c --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/confirm.xhtml @@ -0,0 +1,23 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/copy.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/copy.xhtml new file mode 100644 index 00000000..01dee61c --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/copy.xhtml @@ -0,0 +1,18 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/createCollection.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/createCollection.xhtml new file mode 100644 index 00000000..f22675ab --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/createCollection.xhtml @@ -0,0 +1,25 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/editAnnotation.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/editAnnotation.xhtml new file mode 100644 index 00000000..e335c8dd --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/editAnnotation.xhtml @@ -0,0 +1,50 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/importAnnotations.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/importAnnotations.xhtml new file mode 100644 index 00000000..b5aed21b --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/importAnnotations.xhtml @@ -0,0 +1,41 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/moveManifest.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/moveManifest.xhtml new file mode 100644 index 00000000..5265931f --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/moveManifest.xhtml @@ -0,0 +1,36 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/regenerate.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/regenerate.xhtml new file mode 100644 index 00000000..7ecd5e22 --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/regenerate.xhtml @@ -0,0 +1,32 @@ + diff --git a/src/main/webapp/WEB-INF/templates/dialogues/renameCollection.xhtml b/src/main/webapp/WEB-INF/templates/dialogues/renameCollection.xhtml new file mode 100644 index 00000000..47051e42 --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/dialogues/renameCollection.xhtml @@ -0,0 +1,30 @@ + diff --git a/src/main/webapp/WEB-INF/templates/head.xhtml b/src/main/webapp/WEB-INF/templates/head.xhtml index 1c0d96e6..fc3daa6a 100644 --- a/src/main/webapp/WEB-INF/templates/head.xhtml +++ b/src/main/webapp/WEB-INF/templates/head.xhtml @@ -1,13 +1,26 @@ + xmlns:h="http://java.sun.com/jsf/html" + xmlns:c="http://java.sun.com/jsp/jstl/core"> + #{title} - + + + + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/templates/help/collections.xhtml b/src/main/webapp/WEB-INF/templates/help/collections.xhtml new file mode 100644 index 00000000..9c5967ea --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/help/collections.xhtml @@ -0,0 +1,27 @@ + diff --git a/src/main/webapp/WEB-INF/templates/layout.xhtml b/src/main/webapp/WEB-INF/templates/layout.xhtml index e73551d1..58effe55 100644 --- a/src/main/webapp/WEB-INF/templates/layout.xhtml +++ b/src/main/webapp/WEB-INF/templates/layout.xhtml @@ -1,5 +1,4 @@ - + + diff --git a/src/main/webapp/WEB-INF/templates/menu.xhtml b/src/main/webapp/WEB-INF/templates/menu.xhtml index ec94fdf4..1a96eddb 100644 --- a/src/main/webapp/WEB-INF/templates/menu.xhtml +++ b/src/main/webapp/WEB-INF/templates/menu.xhtml @@ -6,34 +6,66 @@ xmlns:ui="http://xmlns.jcp.org/jsf/facelets"> -