diff --git a/.github/workflows/build-and-test-postgres.yml b/.github/workflows/build-and-test-postgres.yml
new file mode 100644
index 00000000..e5f17b93
--- /dev/null
+++ b/.github/workflows/build-and-test-postgres.yml
@@ -0,0 +1,117 @@
+# Copyright 2020 SkillTree
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Test against PostgreSQL
+
+#on:
+# push:
+# paths-ignore:
+# - 'README.md'
+# pull_request:
+# paths-ignore:
+# - 'README.md'
+on:
+ schedule:
+ - cron: '0 5 * * *'
+
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ # Docker Hub image
+ image: postgres
+ # Provide the password for postgres
+ env:
+ POSTGRES_PASSWORD: skillsPassword
+ POSTGRES_DB: skills
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ # Maps tcp port 5432 on service container to the host
+ - 5432:5432
+
+ steps:
+ - name: Install Emoji Support
+ run: sudo apt-get install fonts-noto-color-emoji
+
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Build skills-service
+ run: mvn --batch-mode install -Dspring.datasource.url=jdbc:postgresql://localhost:5432/skills -Dspring.datasource.username=postgres -Dspring.datasource.password=skillsPassword
+
+ - name: Start Services for Cypress tests
+ run: |
+ cd e2e-tests
+ npm install
+ npm run backend:dropAllDBTables:postgres
+ npm run cyServices:start:skills-service:postgresql
+ npm run cyServices:start:client-display
+ cd ..
+
+ - name: Run Cypress tests
+ uses: cypress-io/github-action@v2
+ with:
+ working-directory: e2e-tests
+ record: true
+ parallel: false
+ group: 'skills-service against postgres'
+ env: db=postgres
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: upload result artifacts
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: CI result artifacts
+ path: |
+ ./service/target/*.jar
+ ./service/target/*.log
+ ./e2e-tests/logs
diff --git a/.github/workflows/build-and-test-rabbitmq.yml b/.github/workflows/build-and-test-rabbitmq.yml
new file mode 100644
index 00000000..9cbe5776
--- /dev/null
+++ b/.github/workflows/build-and-test-rabbitmq.yml
@@ -0,0 +1,106 @@
+# Copyright 2020 SkillTree
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Test Web Sockets over STOMP using RabbitMQ
+
+#on:
+# push:
+# paths-ignore:
+# - 'README.md'
+# pull_request:
+# paths-ignore:
+# - 'README.md'
+on:
+ schedule:
+ - cron: '0 6 * * *'
+
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+
+ services:
+ rabbitmq:
+ image: skilltree/skills-stomp-broker:1.0.0
+ ports:
+ # Maps port 15672 and 61613 on service container to the host
+ - 15672:15672
+ - 61613:61613
+
+ steps:
+ - name: Install Emoji Support
+ run: sudo apt-get install fonts-noto-color-emoji
+
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Build skills-service
+ run: mvn --batch-mode install -DskipTests
+
+ - name: Start services for Cypress tests
+ run: |
+ cd e2e-tests
+ npm install
+ npm run cyServices:start:skills-service:rabbitmq
+ npm run cyServices:start:client-display
+ cd ..
+
+ - name: Run Cypress tests
+ uses: cypress-io/github-action@v2
+ with:
+ working-directory: e2e-tests
+ record: true
+ parallel: false
+ group: 'skills-service with STOMP using RabbitMQ'
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+
+ - name: upload result artifacts
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: CI result artifacts
+ path: |
+ ./service/target/*.jar
+ ./service/target/*.log
+ ./e2e-tests/logs
diff --git a/.github/workflows/build-and-test-redis.yml b/.github/workflows/build-and-test-redis.yml
new file mode 100644
index 00000000..db5286d4
--- /dev/null
+++ b/.github/workflows/build-and-test-redis.yml
@@ -0,0 +1,110 @@
+# Copyright 2020 SkillTree
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Test storing HttpSession in Redis
+
+#on:
+# push:
+# paths-ignore:
+# - 'README.md'
+# pull_request:
+# paths-ignore:
+# - 'README.md'
+on:
+ schedule:
+ - cron: '0 6 * * *'
+
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+
+ services:
+ redis:
+ image: redis
+ # Set health checks to wait until redis has started
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ # Maps port 6379 on service container to the host
+ - 6379:6379
+
+ steps:
+ - name: Install Emoji Support
+ run: sudo apt-get install fonts-noto-color-emoji
+
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Build skills-service
+ run: mvn --batch-mode install -DskipTests
+
+ - name: Start services for Cypress tests
+ run: |
+ cd e2e-tests
+ npm install
+ npm run cyServices:start:skills-service:redis
+ npm run cyServices:start:client-display
+ cd ..
+
+ - name: Run Cypress tests
+ uses: cypress-io/github-action@v2
+ with:
+ working-directory: e2e-tests
+ record: true
+ parallel: false
+ group: 'skills-service with Redis'
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: upload result artifacts
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: CI result artifacts
+ path: |
+ ./service/target/*.jar
+ ./service/target/*.log
+ ./e2e-tests/logs
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644
index 00000000..1a05ca8c
--- /dev/null
+++ b/.github/workflows/build-and-test.yml
@@ -0,0 +1,268 @@
+# Copyright 2020 SkillTree
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Continuous Integration
+
+on:
+ push:
+ paths-ignore:
+ - 'README.md'
+ - '.github/workflows/build-and-test-postgres.yml'
+ - '.github/workflows/build-and-test-redis.yml'
+ - '.gitlab-ci.yml'
+ pull_request:
+ paths-ignore:
+ - 'README.md'
+ - '.github/workflows/build-and-test-postgres.yml'
+ - '.github/workflows/build-and-test-redis.yml'
+ - '.gitlab-ci.yml'
+
+jobs:
+ service-tests-against-h2:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Build skills-service
+ run: mvn --batch-mode install
+
+ - name: upload result artifacts
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: CI result artifacts
+ path: |
+ ./service/target/*.log
+
+ build-skills-service-for-ui-tests:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Build skills-service
+ run: mvn --batch-mode install -DskipTests
+
+ - name: upload service jar
+ uses: actions/upload-artifact@v2
+ with:
+ name: service jar
+ path: ./service/target/*.jar
+
+ ui-tests-against-h2:
+ runs-on: ubuntu-latest
+ needs: [build-skills-service-for-ui-tests]
+ strategy:
+ # when one test fails, DO NOT cancel the other
+ # containers, because this will kill Cypress processes
+ # leaving the Dashboard hanging ...
+ # https://github.com/cypress-io/github-action/issues/48
+ fail-fast: false
+ matrix:
+ # run 3 copies of the current job in parallel
+ containers: [1, 2, 3]
+
+ steps:
+ - name: Install Emoji Support
+ run: sudo apt-get install fonts-noto-color-emoji
+
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - uses: actions/download-artifact@v2
+ with:
+ name: service jar
+ path: ./service/target/
+
+ - name: Prep Services for Cypress tests
+ run: |
+ cd e2e-tests
+ npm install
+ npm run cyServices:start
+ cd ..
+
+ - name: Run Cypress tests
+ uses: cypress-io/github-action@v2
+ with:
+ working-directory: e2e-tests
+ record: true
+ parallel: true
+ group: 'skills-service tests'
+ env:
+ # pass the Dashboard record key as an environment variable
+ CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass GitHub token to allow accurately detecting a build vs a re-run build
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: upload result artifacts
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: CI result artifacts ${{ matrix.container }}
+ path: |
+ ./service/target/*.log
+ ./e2e-tests/logs
+
+ service-against-postgresql:
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres
+ # Provide the password for postgres
+ env:
+ POSTGRES_PASSWORD: skillsPassword
+ POSTGRES_DB: skills
+ # Set health checks to wait until postgres has started
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ # Maps tcp port 5432 on service container to the host
+ - 5432:5432
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2-beta
+ with:
+ node-version: '12'
+
+ - name: Set up Maven
+ uses: stCarolas/setup-maven@v3
+ with:
+ maven-version: 3.6.3
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11.X.X' # The JDK version to make available on the path.
+
+ - name: Print Versions
+ run: |
+ mvn --version
+ java -version
+
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Build skills-service
+ run: mvn --batch-mode test -Dspring.datasource.url=jdbc:postgresql://localhost:5432/skills -Dspring.datasource.username=postgres -Dspring.datasource.password=skillsPassword
+
+ - name: upload result artifacts
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: CI result artifacts
+ path: |
+ ./service/target/*.log
+ ./e2e-tests/logs
+
+ publish-snapshot-docker-image:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ needs: [ui-tests-against-h2, service-against-postgresql]
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions/download-artifact@v2
+ with:
+ name: service jar
+ path: ./service/target/
+
+ - name: Build image
+ env:
+ docker_username: ${{ secrets.DOCKERUSERNAME }}
+ docker_password: ${{ secrets.DOCKERPASSWORD }}
+ run: |
+ cd docker
+ bash build-and-push.sh "skilltree/skills-service-ci"
+
+
diff --git a/.gitignore b/.gitignore
index 2dc5545d..afc5f3e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,11 @@ target
rebel.xml
rebel.xml.lock
package-lock.json
-backend/src/main/resources/public/**
+service/src/main/resources/public/**
.vscode
+.history
+/e2e-tests/cypress/screenshots/
+/e2e-tests/cypress/snapshots/**/__diff_output__
+
+/dashboard/build/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 1b07cd2e..00000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,154 +0,0 @@
-stages:
- - build
- - testCypress
- - pushToNexus
- - deploy
-
-
-build:
- except:
- variables:
- - $TYPE == "postgrestest"
- image: maven:3.6.0-jdk-11
- stage: build
- before_script:
- - ./ci/installNode.sh
- - ./ci/setupRepos.sh
- script:
- - mvn --batch-mode install
- artifacts:
- paths:
- - backend/pom.xml
- - backend/target/backend-*.jar
- - backend/target/skills-service-tests.log
-
-postgres-test:on-schedule:
- only:
- variables:
- - $TYPE == "postgrestest"
- image: maven:3.6.0-jdk-11
- stage: build
- services:
- - postgres:latest
- variables:
- POSTGRES_PASSWORD: skillsPassword
- POSTGRES_DB: skills
- before_script:
- - ./ci/installNode.sh
- - ./ci/setupRepos.sh
- script:
- - mvn --batch-mode test -Dspring.datasource.url=jdbc:postgresql://postgres:5432/skills -Dspring.datasource.username=postgres -Dspring.datasource.password=skillsPassword
- artifacts:
- paths:
- - backend/target/backend-*.jar
- - backend/target/skills-service-tests.log
-
-# all jobs that actually run tests can use the same definition
-cypress_test:
- except:
- variables:
- - $TYPE == "postgrestest"
- image: amazonlinux:2
- stage: testCypress
- before_script:
- - cat /etc/os-release
- - uname -a
- # install packaged fonts
- - yum install -y fontconfig
- - cp -r e2e-tests/cypress/fonts/* /usr/share/fonts/
- - fc-cache -fv
- - fc-list | wc
- - fc-list
-# - yum install -y gtk2.x86_64
- - yum install -y which
- - yum update -y
- - yum install -y gtk3
- - yum install -y libnotify-devel
- - yum install -y GConf2
- - yum install -y libXScrnSaver
-# - yum install -y libnss3
-# - yum install -y libxss1
- - yum install -y nss
-# - yum install -y libasound2
- - yum install -y xorg-x11-server-Xvfb
- - amazon-linux-extras install -y java-openjdk11
- - java --version
- - yum install -y gcc-c++ make
- - curl -sL https://rpm.nodesource.com/setup_12.x | bash -
- - yum install -y nodejs
- - node -v
- - npm -v
- - echo "@skills:registry=http://$NEXUS_SERVER/repository/skills-registry/" > ~/.npmrc
- - cat ~/.npmrc
- script:
- # start the server in the background
- - cd e2e-tests
- - npm run backend:start:ci &
- - cd ../client-display
- - npm install
- - npm run serve &
- - cd ../e2e-tests
- - npm install
- - npm run backend:waitToStart
- - npx wait-on -t 40000 http://localhost:8083
- # run tests
- - npm run cy:run
- artifacts:
- when: always
- paths:
- - e2e-tests/cypress/videos/*.mp4
- - e2e-tests/cypress/videos/**/*.mp4
- - e2e-tests/cypress/screenshots/*.png
- - e2e-tests/cypress/screenshots/**/*.png
- - e2e-tests/cypress/snapshots/**/*
- - e2e-tests/cypress/snapshots/client-display/**/*
- - e2e-tests/cypress/snapshots/client-display/**/__diff_output__/*
- expire_in: 1 day
-
-pushToNexus:
- except:
- variables:
- - $TYPE == "postgrestest"
- image: maven:3.6.0-jdk-11
- stage: pushToNexus
- script:
- - echo "@skills:registry=http://$NEXUS_SERVER/repository/skills-registry/" > ~/.npmrc
- - cat ~/.npmrc
- - echo "nexus-snapshotsadmin$NEXUS_PASSnexus-releasesadmin$NEXUS_PASScentralcentralhttp://$NEXUS_SERVER/repository/maven-public/*" > ~/.m2/settings.xml
- - cat ~/.m2/settings.xml
- - backendJar=$(ls backend/target/backend-*.jar)
- - echo $backendJar
- - mvn --batch-mode deploy:deploy-file -DpomFile=backend/pom.xml -Dfile=${backendJar} -Durl=http://ip-10-113-80-244.evoforge.org/repository/maven-snapshots/ -DrepositoryId=nexus-snapshots
- artifacts:
- paths:
- - backend/target/backend-*.jar
- - backend/target/skills-service-tests.log
- only:
- refs:
- - branches
- variables:
- - $BRANCH_TO_DEPLOY_SKILLS_SERVICE == $CI_COMMIT_REF_NAME
-
-deploy:
- except:
- variables:
- - $TYPE == "postgrestest"
- image: alpine:latest
- stage: deploy
- before_script:
- - apk --update --no-cache add sshpass openssh git
- script:
- - git clone https://${GITLAB_DEPLOY_USERNAME}:${GITLAB_DEPLOY_PASSWORD}@gitlab.evoforge.org/skills/skills-deploy.git
- - TIMESTAMP=`date +%s`
- - TMP_DIR="deploy_${TIMESTAMP}"
- - DEST_PATH="/home/${CI_USERNAME}/$TMP_DIR"
- - sshpass -p $CI_PASSWORD ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no $CI_USERNAME@$CI_IP "rm -rf /home/${CI_USERNAME}/deploy_* && mkdir -p ${DEST_PATH}"
- - cp backend/target/backend-*.jar backend.jar
- - sshpass -p $CI_PASSWORD scp -r skills-deploy ${CI_USERNAME}@${CI_IP}:${DEST_PATH}
- - sshpass -p $CI_PASSWORD scp -r backend.jar ${CI_USERNAME}@${CI_IP}:${DEST_PATH}/skills-deploy
- - sshpass -p $CI_PASSWORD ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no $CI_USERNAME@$CI_IP "cd ${DEST_PATH}/skills-deploy && ./runDeploy.sh"
- only:
- refs:
- - branches
- variables:
- - $BRANCH_TO_DEPLOY_SKILLS_SERVICE == $CI_COMMIT_REF_NAME
diff --git a/README.md b/README.md
index e69de29b..4140b61b 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,18 @@
+# SkillTree Service, Dashboard and Client Display
+
+SkillTree is an innovative approach to implementing application training.
+
+To learn about the SkillTree platform please visit our [Official Documentation](https://code.nsa.gov/skills-docs/).
+These pages provide in-depth guidance on the installation, usage and contribution.
+
+
+# Workflows Status
+
+[](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Continuous+Integration%22)
+
+
+[](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Test+against+PostgreSQL%22)
+
+[](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Test+storing+HttpSession+in+Redis%22)
+
+[](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Test+Web+Sockets+over+STOMP+using+RabbitMQ%22)
diff --git a/backend/src/main/java/skills/WebSocketConfig.groovy b/backend/src/main/java/skills/WebSocketConfig.groovy
deleted file mode 100644
index aa183008..00000000
--- a/backend/src/main/java/skills/WebSocketConfig.groovy
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills
-
-import groovy.util.logging.Slf4j
-import org.springframework.beans.factory.annotation.Value
-import org.springframework.context.annotation.Configuration
-import org.springframework.core.annotation.Order
-import org.springframework.messaging.Message
-import org.springframework.messaging.MessageChannel
-import org.springframework.messaging.simp.config.ChannelRegistration
-import org.springframework.messaging.simp.config.MessageBrokerRegistry
-import org.springframework.messaging.simp.stomp.StompCommand
-import org.springframework.messaging.simp.stomp.StompHeaderAccessor
-import org.springframework.messaging.support.ChannelInterceptor
-import org.springframework.messaging.support.MessageHeaderAccessor
-import org.springframework.security.authentication.AbstractAuthenticationToken
-import org.springframework.security.authentication.AuthenticationDetailsSource
-import org.springframework.security.core.Authentication
-import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor
-import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails
-import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetailsSource
-import org.springframework.security.oauth2.provider.authentication.TokenExtractor
-import org.springframework.security.web.UnsupportedOperationExceptionInvocationHandler
-import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
-import org.springframework.web.socket.config.annotation.StompEndpointRegistry
-import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
-import skills.auth.AuthMode
-import skills.auth.form.oauth2.SkillsOAuth2AuthenticationManager
-
-import javax.servlet.http.HttpServletRequest
-import javax.servlet.http.HttpServletRequestWrapper
-import javax.servlet.http.HttpSession
-import java.lang.reflect.Proxy
-
-@Configuration
-@Slf4j
-@Order(-2147483549) // Ordered.HIGHEST_PRECEDENCE + 99 (see https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/websocket.adoc#token-authentication)
-@EnableWebSocketMessageBroker
-class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-
- static final String AUTHORIZATION = 'Authorization'
-
- @Value('#{"${skills.websocket.enableStompBrokerRelay:false}"}')
- Boolean enableStompBrokerRelay
-
- @Value('#{"${skills.websocket.relayHost:skills-stomp-broker}"}')
- String relayHost
-
- @Value('#{"${skills.websocket.relayPort:61613}"}')
- Integer relayPort
-
- // injected by the SkillsOAuth2AuthenticationManager itself (only when using SecurityMode.FormAuth)
- SkillsOAuth2AuthenticationManager oAuth2AuthenticationManager
-
- @Value('${skills.authorization.authMode:#{T(skills.auth.AuthMode).DEFAULT_AUTH_MODE}}')
- AuthMode authMode
-
- @Override
- void configureMessageBroker(MessageBrokerRegistry registry) {
- if (enableStompBrokerRelay) {
- registry.enableStompBrokerRelay('/topic', '/queue')
- .setRelayHost(relayHost)
- .setRelayPort(relayPort)
- .setUserRegistryBroadcast('/topic/registry')
- .setUserDestinationBroadcast('/topic/unresolved-user-destination')
- } else {
- registry.enableSimpleBroker('/topic', '/queue')
- }
- registry.setApplicationDestinationPrefixes('/app')
- }
-
- @Override
- void registerStompEndpoints(StompEndpointRegistry registry) {
- registry.addEndpoint('/skills-websocket')
- .setAllowedOrigins("*")
- .withSockJS()
- }
-
- @Override
- void configureClientInboundChannel(ChannelRegistration registration) {
- if (authMode == AuthMode.FORM) { // only injected when using SecurityMode.FormAuth
- log.info('Initializing websocket registration interceptor.')
- registration.interceptors(new ChannelInterceptor() {
- TokenExtractor tokenExtractor = new BearerTokenExtractor()
- AuthenticationDetailsSource authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
-
- @Override
- Message> preSend(Message> message, MessageChannel channel) {
- StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class)
- if (StompCommand.CONNECT == accessor.getCommand()) {
- List authHeaders = accessor.getNativeHeader(AUTHORIZATION)
- if (authHeaders) {
- log.debug("Found Authorization headers on websocket connection: [{}]", authHeaders)
- WebSocketHttpServletRequest request = new WebSocketHttpServletRequest(headers: [(AUTHORIZATION): Collections.enumeration(authHeaders)])
- Authentication authentication = tokenExtractor.extract(request)
- if (authentication) {
- request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
- if (authentication instanceof AbstractAuthenticationToken) {
- AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
- needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
- }
- Authentication authResult = oAuth2AuthenticationManager.authenticate(authentication);
- if (authResult.authenticated) {
- log.debug("Setting OAuth user [{}] on websocket connection", authResult)
- accessor.setUser(authResult)
- }
- }
- }
- }
- return message
- }
- })
- }
- }
-
- static class WebSocketHttpServletRequest extends HttpServletRequestWrapper {
- private static final HttpServletRequest UNSUPPORTED_REQUEST = (HttpServletRequest) Proxy
- .newProxyInstance(WebSocketHttpServletRequest.class.getClassLoader(),
- [ HttpServletRequest.class ] as Class>[],
- new UnsupportedOperationExceptionInvocationHandler())
-
- String remoteAddr
- Map attributes = [:]
- Map> headers = [:]
-
- WebSocketHttpServletRequest() { super(UNSUPPORTED_REQUEST) }
-
- Object getAttribute(String attributeName) { return attributes.get(attributeName) }
- void setAttribute(String name, Object o) { attributes.put(name, o) }
-
- Enumeration getHeaders(String name) { return headers.get(name) }
-
- HttpSession getSession(boolean create) { return null }
- }
-}
diff --git a/backend/src/main/java/skills/auth/PortalWebSecurityHelper.groovy b/backend/src/main/java/skills/auth/PortalWebSecurityHelper.groovy
deleted file mode 100644
index d65c094f..00000000
--- a/backend/src/main/java/skills/auth/PortalWebSecurityHelper.groovy
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills.auth
-
-import org.springframework.security.config.annotation.web.builders.HttpSecurity
-import org.springframework.stereotype.Component
-import skills.storage.model.auth.RoleName
-
-@Component
-class PortalWebSecurityHelper {
- HttpSecurity configureHttpSecurity(HttpSecurity http) {
- http
- .csrf().disable()
- .authorizeRequests()
- .antMatchers("/", "/favicon.ico", "/icons/**", "/static/**", "/skills.ico", "/skills.jpeg", "/error", "/oauth/**", "/app/oAuthProviders", "/login*", "/bootstrap/**", "/performLogin", "/createAccount", "/createRootAccount", '/grantFirstRoot', '/userExists/**', "/app/userInfo", "/app/users/validExistingDashboardUserId/*", "/app/oAuthProviders", "index.html", "/public/**", "/skills-websocket/**").permitAll()
- .antMatchers('/admin/**').hasRole('PROJECT_ADMIN')
- .antMatchers('/supervisor/**').hasAnyAuthority(RoleName.ROLE_SUPERVISOR.name(), RoleName.ROLE_SUPER_DUPER_USER.name())
- .antMatchers('/root/isRoot').hasAnyAuthority(RoleName.values().collect {it.name()}.toArray(new String[0]))
- .antMatchers('/root/**').hasRole('SUPER_DUPER_USER')
- .anyRequest().authenticated()
- http.headers().frameOptions().disable()
-
- return http
- }
-}
diff --git a/backend/src/main/java/skills/controller/PublicConfigController.groovy b/backend/src/main/java/skills/controller/PublicConfigController.groovy
deleted file mode 100644
index ba4e0ab8..00000000
--- a/backend/src/main/java/skills/controller/PublicConfigController.groovy
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills.controller
-
-import groovy.util.logging.Slf4j
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.web.bind.annotation.CrossOrigin
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestMethod
-import org.springframework.web.bind.annotation.ResponseBody
-import org.springframework.web.bind.annotation.RestController
-import skills.HealthChecker
-import skills.UIConfigProperties
-import skills.profile.EnableCallStackProf
-
-@RestController
-@RequestMapping("/public")
-@Slf4j
-@EnableCallStackProf
-class PublicConfigController {
-
- @Autowired
- HealthChecker healthChecker
-
- @Autowired
- UIConfigProperties uiConfigProperties
-
- @RequestMapping(value = "/config", method = RequestMethod.GET, produces = "application/json")
- @ResponseBody
- Map getConfig(){
- return uiConfigProperties.ui
- }
-
- final private static Map statusRes = [
- status: "OK",
- ]
-
- @CrossOrigin
- @RequestMapping(value = "/status", method = RequestMethod.GET, produces = "application/json")
- @ResponseBody
- def status() {
- healthChecker.checkRequiredServices()
- return statusRes
- }
-}
diff --git a/backend/src/main/java/skills/controller/UserTokenController.groovy b/backend/src/main/java/skills/controller/UserTokenController.groovy
deleted file mode 100644
index 90ec9079..00000000
--- a/backend/src/main/java/skills/controller/UserTokenController.groovy
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills.controller
-
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.context.annotation.Conditional
-import org.springframework.http.MediaType
-import org.springframework.http.ResponseEntity
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
-import org.springframework.security.oauth2.common.OAuth2AccessToken
-import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint
-import org.springframework.web.bind.annotation.PathVariable
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestMethod
-import org.springframework.web.bind.annotation.ResponseBody
-import org.springframework.web.bind.annotation.RestController
-import skills.auth.SecurityMode
-import skills.services.InceptionProjectService
-
-@Conditional(SecurityMode.FormAuth)
-@RestController
-@skills.profile.EnableCallStackProf
-class UserTokenController {
-
- @Autowired
- TokenEndpoint tokenEndpoint
-
- /**
- * token for inception
- * @param userId
- * @return
- */
- @RequestMapping(value = "/app/projects/Inception/users/{userId}/token", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
- @ResponseBody
- ResponseEntity getUserToken(@PathVariable("userId") String userId) {
- return createToken(InceptionProjectService.inceptionProjectId, userId)
- }
-
-
- /**
- * utilized by client-display within a project that previews that project's points
- * @param projectId
- * @param userId
- * @return
- */
- @RequestMapping(value = "/admin/projects/{projectId}/token/{userId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
- @ResponseBody
- ResponseEntity getUserToken(@PathVariable("projectId") String projectId, @PathVariable("userId") String userId) {
- return createToken(projectId, userId)
- }
-
- private ResponseEntity createToken(String projectId, String userId) {
- skills.controller.exceptions.SkillsValidator.isNotBlank(projectId, "Project Id")
- skills.controller.exceptions.SkillsValidator.isNotBlank(userId, "User Id")
-
- UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken(projectId, null, [])
- Map parameters = [grant_type: 'client_credentials', proxy_user: userId]
- return tokenEndpoint.postAccessToken(principal, parameters)
- }
-}
diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy b/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy
deleted file mode 100644
index 84c0813f..00000000
--- a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy
+++ /dev/null
@@ -1,221 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills.services.events.pointsAndAchievements
-
-import callStack.profiler.Profile
-import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
-import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.stereotype.Component
-import skills.services.LevelDefinitionStorageService
-import skills.services.events.CompletionItem
-import skills.services.events.CompletionTypeUtil
-import skills.services.events.SkillEventResult
-import skills.storage.model.*
-import skills.storage.repos.SkillEventsSupportRepo
-import skills.storage.repos.SkillRelDefRepo
-import skills.storage.repos.UserAchievedLevelRepo
-import skills.storage.repos.UserPointsRepo
-
-@Component
-@Slf4j
-@CompileStatic
-class PointsAndAchievementsHandler {
-
- @Autowired
- UserPointsRepo userPointsRepo
-
- @Autowired
- UserAchievedLevelRepo userAchievedLevelRepo
-
- @Autowired
- UserAchievedLevelRepo achievedLevelRepo
-
- @Autowired
- SkillRelDefRepo skillRelDefRepo
-
- @Autowired
- LevelDefinitionStorageService levelDefService
-
- @Autowired
- SkillEventsSupportRepo skillEventsSupportRepo
-
- @Autowired
- PointsAndAchievementsSaver saver
-
- @Autowired
- PointsAndAchievementsDataLoader dataLoader
-
- @Profile
- List updatePointsAndAchievements(String userId, SkillEventsSupportRepo.SkillDefMin skillDef, Date incomingSkillDate){
- LoadedData loadedData = dataLoader.loadData(skillDef.projectId, userId, incomingSkillDate, skillDef)
-
- PointsAndAchievementsBuilder builder = new PointsAndAchievementsBuilder(
- userId: userId,
- projectId: skillDef.projectId,
- skillId: skillDef.skillId,
- skillRefId: skillDef.id,
- loadedData: loadedData,
- pointIncrement: skillDef.pointIncrement,
- incomingSkillDate: incomingSkillDate,
- levelDefService: levelDefService,
- )
- PointsAndAchievementsBuilder.PointsAndAchievementsResult result = builder.build()
- saver.save(result.dataToSave)
- return result.completionItems
- }
-
-
-
- @Profile
- void checkParentGraph(Date incomingSkillDate, SkillEventResult res, String userId, SkillEventsSupportRepo.SkillDefMin skillDef, boolean decrement) {
- updateByTraversingUpSkillDefs(incomingSkillDate, res, skillDef, skillDef, userId, decrement)
-
- // updated project level
- UserPoints totalPoints = updateUserPoints(userId, skillDef, incomingSkillDate, null, decrement)
- if (!decrement) {
- List levelDefs = skillEventsSupportRepo.findLevelsByProjectId(skillDef.projectId)
- SkillEventsSupportRepo.TinyProjectDef totalProjectPoints = skillEventsSupportRepo.getTinyProjectDef(skillDef.projectId)
- LevelDefinitionStorageService.LevelInfo levelInfo = levelDefService.getLevelInfo(skillDef.projectId, levelDefs, totalProjectPoints.totalPoints, totalPoints.points)
- CompletionItem completionItem = calculateLevels(levelInfo, totalPoints, null, userId, "OVERALL", decrement)
- if (completionItem?.level && completionItem?.level > 0) {
- res.completed.add(completionItem)
- }
- }
- }
-
- /**
- * @param skillId if null then will document it at overall project level
- */
- UserPoints updateUserPoints(String userId, SkillEventsSupportRepo.SkillDefMin requestedSkill, Date incomingSkillDate, String skillId = null, boolean decrement) {
- doUpdateUserPoints(requestedSkill, userId, incomingSkillDate, skillId, decrement)
- UserPoints res = doUpdateUserPoints(requestedSkill, userId, null, skillId, decrement)
- return res
- }
-
- @Profile
- private UserPoints doUpdateUserPoints(SkillEventsSupportRepo.SkillDefMin requestedSkill, String userId, Date incomingSkillDate, String skillId, boolean decrement) {
- Date day = incomingSkillDate ? new Date(incomingSkillDate.time).clearTime() : null
- UserPoints userPoints = getUserPoints(requestedSkill, userId, skillId, day)
- if (!userPoints) {
- assert !decrement
- userPoints = new UserPoints(userId: userId?.toLowerCase(), projectId: requestedSkill.projectId,
- skillId: skillId,
- skillRefId: skillId ? requestedSkill.id : null,
- points: requestedSkill.pointIncrement, day: day)
- } else {
- if (decrement) {
- userPoints.points -= requestedSkill.pointIncrement
- } else {
- userPoints.points += requestedSkill.pointIncrement
- }
- }
-
- UserPoints res
- if (decrement && userPoints.points <= 0) {
- userPointsRepo.delete(userPoints)
- res = new UserPoints(userId: userId?.toLowerCase(), projectId: requestedSkill.projectId,
- skillId: skillId,
- skillRefId: skillId ? requestedSkill.id : null,
- points: 0, day: day)
- } else {
- res = saveUserPoints(userPoints)
- log.debug("Updated points [{}]", res)
- }
- res
- }
-
- @Profile
- private UserPoints saveUserPoints(UserPoints subjectPoints) {
- userPointsRepo.save(subjectPoints)
- }
-
- @Profile
- private UserPoints getUserPoints(SkillEventsSupportRepo.SkillDefMin requestedSkill, String userId, String skillId, Date day) {
- userPointsRepo.findByProjectIdAndUserIdAndSkillIdAndDay(requestedSkill.projectId, userId, skillId, day)
- }
-
- @Profile
- private void updateByTraversingUpSkillDefs(Date incomingSkillDate, SkillEventResult res,
- SkillEventsSupportRepo.SkillDefMin currentDef,
- SkillEventsSupportRepo.SkillDefMin requesterDef,
- String userId, boolean decrement) {
- if (shouldEvaluateForAchievement(currentDef)) {
- UserPoints updatedPoints = updateUserPoints(userId, requesterDef, incomingSkillDate, currentDef.skillId, decrement)
-
- List levelDefs = skillEventsSupportRepo.findLevelsBySkillId(currentDef.id)
- if (!levelDefs) {
- if (!decrement && updatedPoints.points >= currentDef.totalPoints) {
- UserAchievement groupAchievement = new UserAchievement(userId: userId.toLowerCase(), projectId: currentDef.projectId, skillId: currentDef.skillId, skillRefId: currentDef?.id,
- pointsWhenAchieved: updatedPoints.points)
- achievedLevelRepo.save(groupAchievement)
-
- res.completed.add(new CompletionItem(type: CompletionTypeUtil.getCompletionType(currentDef.type), id: currentDef.skillId, name: currentDef.name))
- } else if (decrement && updatedPoints.points <= currentDef.totalPoints) {
- // we are decrementing, there are no levels defined and points are less that total points so we need
- // to delete previously added achievement if it exists
- achievedLevelRepo.deleteByProjectIdAndSkillIdAndUserIdAndLevel(currentDef.projectId, currentDef.skillId, userId, null)
- }
- } else {
- int currentScore = decrement ? updatedPoints.points + requesterDef.pointIncrement : updatedPoints.points
- LevelDefinitionStorageService.LevelInfo levelInfo = levelDefService.getLevelInfo(currentDef.projectId, levelDefs, currentDef.totalPoints, currentScore)
- CompletionItem completionItem = calculateLevels(levelInfo, updatedPoints, currentDef, userId, currentDef.name, decrement)
- if (!decrement && completionItem?.level && completionItem?.level > 0) {
- res.completed.add(completionItem)
- }
- }
- }
-
- List parentsRels = skillEventsSupportRepo.findParentSkillsByChildIdAndType(currentDef.id, SkillRelDef.RelationshipType.RuleSetDefinition)
- parentsRels?.each {
- updateByTraversingUpSkillDefs(incomingSkillDate, res, it, requesterDef, userId, decrement)
- }
- }
-
- private boolean shouldEvaluateForAchievement(SkillEventsSupportRepo.SkillDefMin skillDef) {
- skillDef.type == SkillDef.ContainerType.Subject
- }
-
- @Profile
- private CompletionItem calculateLevels(LevelDefinitionStorageService.LevelInfo levelInfo, UserPoints userPts, SkillEventsSupportRepo.SkillDefMin skillDef, String userId, String name, boolean decrement) {
- CompletionItem res
-
- List userAchievedLevels = achievedLevelRepo.findAllByUserIdAndProjectIdAndSkillId(userId, userPts.projectId, userPts.skillId)
- boolean levelAlreadyAchieved = userAchievedLevels?.find { it.level == levelInfo.level }
- if (!levelAlreadyAchieved && !decrement) {
- UserAchievement newLevel = new UserAchievement(userId: userId.toLowerCase(), projectId: userPts.projectId, skillId: userPts.skillId, skillRefId: skillDef?.id,
- level: levelInfo.level, pointsWhenAchieved: userPts.points)
- achievedLevelRepo.save(newLevel)
- log.debug("Achieved new level [{}]", newLevel)
-
- res = new CompletionItem(
- level: newLevel.level, name: name,
- id: userPts.skillId ?: "OVERALL",
- type: userPts.skillId ? CompletionItem.CompletionItemType.Subject : CompletionItem.CompletionItemType.Overall)
- } else if (decrement) {
- // we are decrementing, so we need to remove any level that is greater than the current level (there should only be one)
- List levelsToRemove = userAchievedLevels?.findAll { it.level >= levelInfo.level }
- if (levelsToRemove) {
- assert levelsToRemove.size() == 1, "we are decrementing a single skill so we should not be remove multiple (${levelsToRemove.size()} levels)"
- achievedLevelRepo.delete(levelsToRemove.first())
- }
- }
-
- return res
- }
-
-
-}
diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_BadgeSkillsSpecs.groovy b/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_BadgeSkillsSpecs.groovy
deleted file mode 100644
index 01ef467a..00000000
--- a/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_BadgeSkillsSpecs.groovy
+++ /dev/null
@@ -1,177 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills.intTests.reportSkills
-
-import skills.intTests.utils.DefaultIntSpec
-import skills.intTests.utils.SkillsClientException
-import skills.intTests.utils.SkillsFactory
-
-class ReportSkills_BadgeSkillsSpecs extends DefaultIntSpec {
-
- String projId = SkillsFactory.defaultProjId
-
- def setup(){
- skillsService.deleteProjectIfExist(projId)
- }
-
- def "give credit if all dependencies were fulfilled"(){
- String subj = "testSubj"
-
- Map skill1 = [projectId: projId, subjectId: subj, skillId: "skill1", name : "Test Skill 1", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill2 = [projectId: projId, subjectId: subj, skillId: "skill2", name : "Test Skill 2", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill3 = [projectId: projId, subjectId: subj, skillId: "skill3", name : "Test Skill 3", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill4 = [projectId: projId, subjectId: subj, skillId: "skill4", name : "Test Skill 4", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1*60, numMaxOccurrencesIncrementInterval: 1, dependentSkillsIds: [skill1.skillId, skill2.skillId, skill3.skillId]]
-
- Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1']
- List requiredSkillsIds = [skill1.skillId, skill2.skillId, skill3.skillId, skill4.skillId]
-
-
- when:
- skillsService.createProject([projectId: projId, name: "Test Project"])
- skillsService.createSubject([projectId: projId, subjectId: subj, name: "Test Subject"])
- skillsService.createSkill(skill1)
- skillsService.createSkill(skill2)
- skillsService.createSkill(skill3)
- skillsService.createSkill(skill4)
- skillsService.createBadge(badge)
- requiredSkillsIds.each { skillId ->
- skillsService.assignSkillToBadge(projectId: projId, badgeId: badge.badgeId, skillId: skillId)
- }
-
- def resSkill1 = skillsService.addSkill([projectId: projId, skillId: skill1.skillId]).body
- def resSkill3 = skillsService.addSkill([projectId: projId, skillId: skill3.skillId]).body
- def resSkill2 = skillsService.addSkill([projectId: projId, skillId: skill2.skillId]).body
- def resSkill4 = skillsService.addSkill([projectId: projId, skillId: skill4.skillId]).body
-
- then:
- resSkill1.skillApplied && !resSkill1.completed.find { it.id == 'badge1'}
- resSkill2.skillApplied && !resSkill2.completed.find { it.id == 'badge1'}
- resSkill3.skillApplied && !resSkill3.completed.find { it.id == 'badge1'}
- resSkill4.skillApplied && resSkill4.completed.find { it.id == 'badge1'}
- }
-
- def "give credit if all dependencies were fulfilled, but the badge/gem is active"(){
- String subj = "testSubj"
-
- Map skill1 = [projectId: projId, subjectId: subj, skillId: "skill1", name : "Test Skill 1", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill2 = [projectId: projId, subjectId: subj, skillId: "skill2", name : "Test Skill 2", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill3 = [projectId: projId, subjectId: subj, skillId: "skill3", name : "Test Skill 3", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill4 = [projectId: projId, subjectId: subj, skillId: "skill4", name : "Test Skill 4", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1, dependentSkillsIds: [skill1.skillId, skill2.skillId, skill3.skillId]]
-
- Date tomorrow = new Date()+1
- Date twoWeeksAgo = new Date()-14
- Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1', startDate: twoWeeksAgo, endDate: tomorrow]
- List requiredSkillsIds = [skill1.skillId, skill2.skillId, skill3.skillId, skill4.skillId]
-
- when:
- skillsService.createProject([projectId: projId, name: "Test Project"])
- skillsService.createSubject([projectId: projId, subjectId: subj, name: "Test Subject"])
- skillsService.createSkill(skill1)
- skillsService.createSkill(skill2)
- skillsService.createSkill(skill3)
- skillsService.createSkill(skill4)
- skillsService.createBadge(badge)
- requiredSkillsIds.each { skillId ->
- skillsService.assignSkillToBadge(projectId: projId, badgeId: badge.badgeId, skillId: skillId)
- }
-
-
- def resSkill1 = skillsService.addSkill([projectId: projId, skillId: skill1.skillId]).body
- def resSkill3 = skillsService.addSkill([projectId: projId, skillId: skill3.skillId]).body
- def resSkill2 = skillsService.addSkill([projectId: projId, skillId: skill2.skillId]).body
- def resSkill4 = skillsService.addSkill([projectId: projId, skillId: skill4.skillId]).body
-
- then:
- resSkill1.skillApplied && !resSkill1.completed.find { it.id == 'badge1'}
- resSkill2.skillApplied && !resSkill2.completed.find { it.id == 'badge1'}
- resSkill3.skillApplied && !resSkill3.completed.find { it.id == 'badge1'}
- resSkill4.skillApplied && resSkill4.completed.find { it.id == 'badge1'}
- }
-
- def "do not give credit if all dependencies were fulfilled, but the badge/gem is not active"(){
- String subj = "testSubj"
-
- Map skill1 = [projectId: projId, subjectId: subj, skillId: "skill1", name : "Test Skill 1", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill2 = [projectId: projId, subjectId: subj, skillId: "skill2", name : "Test Skill 2", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill3 = [projectId: projId, subjectId: subj, skillId: "skill3", name : "Test Skill 3", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill4 = [projectId: projId, subjectId: subj, skillId: "skill4", name : "Test Skill 4", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1, dependentSkillsIds: [skill1.skillId, skill2.skillId, skill3.skillId]]
-
- Date oneWeekAgo = new Date()-7
- Date twoWeeksAgo = new Date()-14
- Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1', startDate: twoWeeksAgo, endDate: oneWeekAgo]
-
- when:
- skillsService.createProject([projectId: projId, name: "Test Project"])
- skillsService.createSubject([projectId: projId, subjectId: subj, name: "Test Subject"])
- skillsService.createSkill(skill1)
- skillsService.createSkill(skill2)
- skillsService.createSkill(skill3)
- skillsService.createSkill(skill4)
- skillsService.createBadge(badge)
-
- def resSkill1 = skillsService.addSkill([projectId: projId, skillId: skill1.skillId]).body
- def resSkill3 = skillsService.addSkill([projectId: projId, skillId: skill3.skillId]).body
- def resSkill2 = skillsService.addSkill([projectId: projId, skillId: skill2.skillId]).body
- def resSkill4 = skillsService.addSkill([projectId: projId, skillId: skill4.skillId]).body
-
- List requiredSkillsIds = [skill1.skillId, skill2.skillId, skill3.skillId, skill4.skillId]
- requiredSkillsIds.each { String skillId ->
- skillsService.assignSkillToBadge(projectId: projId, badgeId: badge.badgeId, skillId: skillId)
- }
-
- then:
- resSkill1.skillApplied && !resSkill1.completed.find { it.id == 'badge1'}
- resSkill2.skillApplied && !resSkill2.completed.find { it.id == 'badge1'}
- resSkill3.skillApplied && !resSkill3.completed.find { it.id == 'badge1'}
- resSkill4.skillApplied && !resSkill4.completed.find { it.id == 'badge1'}
- }
-
- def 'validate that if one gem date is provided both dates need to be provided - start provided'() {
- when:
- skillsService.createProject([projectId: projId, name: "Test Project"])
- Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1', startDate: new Date()]
- skillsService.createBadge(badge)
-
- then:
- SkillsClientException e = thrown()
- e.message.contains("explanation:If one date is provided then both start and end dates must be provided")
- e.message.contains("errorCode:BadParam")
- }
-
- def 'validate that if one gem date is provided both dates need to be provided - end provided'() {
- when:
- skillsService.createProject([projectId: projId, name: "Test Project"])
- Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1', startDate: new Date()]
- skillsService.createBadge(badge)
-
- then:
- SkillsClientException e = thrown()
- e.message.contains("explanation:If one date is provided then both start and end dates must be provided")
- e.message.contains("errorCode:BadParam")
- }
-}
diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_GlobalBadgeSkillsSpecs.groovy b/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_GlobalBadgeSkillsSpecs.groovy
deleted file mode 100644
index f0a1f458..00000000
--- a/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_GlobalBadgeSkillsSpecs.groovy
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package skills.intTests.reportSkills
-
-import org.joda.time.DateTime
-import skills.intTests.utils.DefaultIntSpec
-import skills.intTests.utils.SkillsFactory
-import skills.intTests.utils.SkillsService
-
-class ReportSkills_GlobalBadgeSkillsSpecs extends DefaultIntSpec {
-
- String projId = SkillsFactory.defaultProjId
- String badgeId = 'GlobalBadge1'
-
- String ultimateRoot = 'jh@dojo.com'
- SkillsService rootSkillsService
- String nonRootUserId = 'foo@bar.com'
- SkillsService nonSupervisorSkillsService
-
- def setup(){
- skillsService.deleteProjectIfExist(projId)
- rootSkillsService = createService(ultimateRoot, 'aaaaaaaa')
- nonSupervisorSkillsService = createService(nonRootUserId)
-
- if (!rootSkillsService.isRoot()) {
- rootSkillsService.grantRoot()
- }
- rootSkillsService.grantSupervisorRole(skillsService.wsHelper.username)
- }
-
- def cleanup() {
- rootSkillsService?.removeSupervisorRole(skillsService.wsHelper.username)
- }
-
- def "give credit if all dependencies were fulfilled"(){
- String subj = "testSubj"
-
- Map skill1 = [projectId: projId, subjectId: subj, skillId: "skill1", name : "Test Skill 1", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill2 = [projectId: projId, subjectId: subj, skillId: "skill2", name : "Test Skill 2", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill3 = [projectId: projId, subjectId: subj, skillId: "skill3", name : "Test Skill 3", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1]
- Map skill4 = [projectId: projId, subjectId: subj, skillId: "skill4", name : "Test Skill 4", type: "Skill",
- pointIncrement: 25, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1*60, numMaxOccurrencesIncrementInterval: 1, dependentSkillsIds: [skill1.skillId, skill2.skillId, skill3.skillId]]
-
- Map badge = [badgeId: badgeId, name: 'Test Global Badge 1']
- List requiredSkillsIds = [skill1.skillId, skill2.skillId, skill3.skillId, skill4.skillId]
-
- when:
- skillsService.createProject([projectId: projId, name: "Test Project"])
- skillsService.createSubject([projectId: projId, subjectId: subj, name: "Test Subject"])
- skillsService.createSkill(skill1)
- skillsService.createSkill(skill2)
- skillsService.createSkill(skill3)
- skillsService.createSkill(skill4)
- skillsService.createGlobalBadge(badge)
- skillsService.assignProjectLevelToGlobalBadge(projectId: projId, badgeId: badge.badgeId, level: "3")
- requiredSkillsIds.each { skillId ->
- skillsService.assignSkillToGlobalBadge(projectId: projId, badgeId: badge.badgeId, skillId: skillId)
- }
-
- DateTime dt = new DateTime().minusDays(4)
-
- def resSkill1 = skillsService.addSkill([projectId: projId, skillId: skill1.skillId], "user1", dt.toDate()).body
- def resSkill3 = skillsService.addSkill([projectId: projId, skillId: skill3.skillId], "user1", dt.plusDays(1).toDate()).body
- def resSkill2 = skillsService.addSkill([projectId: projId, skillId: skill2.skillId], "user1", dt.plusDays(2).toDate()).body
- def resSkill4 = skillsService.addSkill([projectId: projId, skillId: skill4.skillId], "user1", dt.plusDays(3).toDate()).body
-
- then:
- resSkill1.skillApplied && !resSkill1.completed.find { it.id == badgeId}
- resSkill2.skillApplied && !resSkill2.completed.find { it.id == badgeId}
- resSkill3.skillApplied && !resSkill3.completed.find { it.id == badgeId}
- resSkill4.skillApplied && resSkill4.completed.find { it.id == badgeId}
-
- cleanup:
- skillsService.deleteGlobalBadge(badgeId)
- }
-
-
-}
diff --git a/call-stack-profiler/LICENSE.txt b/call-stack-profiler/LICENSE.txt
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/call-stack-profiler/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/call-stack-profiler/README.md b/call-stack-profiler/README.md
new file mode 100644
index 00000000..52a7b076
--- /dev/null
+++ b/call-stack-profiler/README.md
@@ -0,0 +1,371 @@
+# Call Stack Profiler for Groovy
+
+> Profile your code with negligible performance and memory overhead.
+
+This profiler keeps track of the method calls and outputs method call hierarchy, allowing developers
+to quickly comprehend execution time breakdown.
+- The profiler is **fast** and is appropriate to have track and output enabled in a production system
+- Use ``@Profile`` to easily annotate Groovy methods OR wrap logic in a closure OR manually start/stop events
+- Naturally fits into a service based architecture
+- Provides support for delegating concurrent tasks to a Thread Pool
+
+Consider the following class where methods are annotated with the ``@Profile`` annotation:
+```groovy
+class Example {
+ @Profile
+ void m1() {
+ m2()
+ }
+
+ @Profile
+ void m2() {
+ 5.times {
+ m3()
+ }
+ }
+
+ @Profile
+ void m3() {
+ Thread.sleep(100)
+ }
+}
+```
+
+and then
+
+```groovy
+static void main(String[] args) {
+ new Example().m1()
+ println CProf.prettyPrint()
+}
+```
+
+then the output is:
+```
+|-> Example.m1 (1) : 501ms [000ms]
+| |-> Example.m2 (1) : 501ms [000ms]
+| | |-> Example.m3 (5) : 501ms
+```
+
+The output provides method call hierarchy as well as the following information:
+- Total method execution time: number in ms, seconds and/or minutes
+- ``(N)``: number of times method was called, m2() was called once and m3() called 5 times
+- ``[N ms]``: execution time which was not accounted for by child methods/logic; this happens when either not all of the child methods/logic is profiled OR there is a GC or JVM overhead
+
+## Features
+
+### Custom Profile Name
+
+When using the ``@Profile`` annotation, by default, profile names are derived from the method name and its parameters.
+You can supply a custom name by setting the ``name`` attribute on the ``@Profile`` annotation:
+
+```groovy
+class Example {
+ @Profile(name = 'veryCustomName')
+ void m1() {
+ m2()
+ }
+
+ @Profile
+ void m2() {
+ Thread.sleep(20)
+ }
+}
+```
+
+Then the output is:
+
+```
+|-> veryCustomName (1) : 020ms [000ms]
+| |-> Example.m2 (1) : 020ms
+```
+
+### Closure based Profiling
+
+You can easily profile (and name) any bit of code by wrapping it in a closure:
+
+```groovy
+class Example {
+ @Profile
+ void m1() {
+ m2()
+ CProf.prof("Another Long Action") {
+ // great logic
+ Thread.sleep(1000)
+ }
+ }
+
+ @Profile
+ void m2() {
+ Thread.sleep(20)
+ }
+}
+```
+
+Then the output is:
+```
+|-> Example.m1 (1) : 1s 020ms [000ms]
+| |-> Example.m2 (1) : 020ms
+| |-> Another Long Action (1) : 1s
+```
+
+### Manually start/stop events
+
+Start and stop profiling events can be managed manually:
+
+```groovy
+class Example {
+ @Profile
+ void m1() {
+ m2()
+ String name = "Another Long Action"
+ CProf.start(name)
+ try {
+ // great logic
+ Thread.sleep(1000)
+ } finally {
+ CProf.stop(name)
+ }
+ }
+
+ @Profile
+ void m2() {
+ Thread.sleep(20)
+ }
+}
+```
+
+Then the output is:
+```
+|-> Example.m1 (1) : 1s 020ms [000ms]
+| |-> Example.m2 (1) : 020ms
+| |-> Another Long Action (1) : 1s
+```
+
+If you select to manually manage start/stop events then please:
+- always wrap logic in a ``try/catch`` block to ensure the event is closed
+- verify that the same name is used to start and end the event
+
+### Delegate concurrent tasks to a Thread Pool
+
+Call Stack Profiler supplies a thread pool implementation ``ProfThreadPool``
+which makes it seamless to execute and profile concurrent tasks.
+
+Below is an example of executing methods ``m1()`` and ``(m2)`` concurrently:
+
+```groovy
+class Example {
+ @Profile
+ void runConcurrent() {
+ ProfThreadPool threadPool = new ProfThreadPool("Threads", 2, 2)
+ threadPool.warnIfFull = false
+ List> callables = [
+ ThreadPoolUtils.callable {
+ m1()
+ },
+ ThreadPoolUtils.callable {
+ m2()
+ },
+ ]
+ List res = threadPool.asyncExec(callables)
+ println "Result: ${res}"
+ }
+
+ @Profile
+ int m1() {
+ 5.times { m2() }
+ return 10
+ }
+
+ @Profile()
+ int m2() {
+ Thread.sleep(20)
+ return 5
+ }
+}
+```
+
+Then the output is:
+```
+Result: [10, 5]
+|-> Example.runConcurrent (1) : 104ms [003ms]
+| ||-> Example.m1-Threads-1 (1) : 101ms [000ms]
+| || |-> Example.m2 (5) : 101ms
+| ||-> Example.m2-Threads-2 (1) : 020ms
+```
+
+``||`` depicts that the code is being executed concurrently
+
+### Each Call as its own event
+
+If you are calling a method within a loop AND the loop has a reasonable (for display purposes) number of elements,
+then you may want to opt for displaying each method call as its own profiling event.
+
+Set attribute ``aggregateIntoSingleEvent = false`` for the ``@Profile`` annotation, for example:
+
+```groovy
+class Example {
+ @Profile
+ void m1() {
+ 5.times {
+ m2()
+ }
+ }
+
+ @Profile(aggregateIntoSingleEvent = false)
+ void m2() {
+ Thread.sleep(20)
+ }
+}
+```
+
+Then the output is:
+```
+|-> Example.m1 (1) : 102ms [000ms]
+| |-> Example.m20_24 (1) : 021ms
+| |-> Example.m20_23 (1) : 020ms
+| |-> Example.m20_22 (1) : 020ms
+| |-> Example.m20_21 (1) : 021ms
+| |-> Example.m20_20 (1) : 020ms
+```
+
+### Exceptions
+
+Exceptions are propagated as expected. For example:
+
+```groovy
+class Example {
+ @Profile
+ int m1() {
+ 5.times { m2() }
+ return 10
+ }
+
+ @Profile()
+ int m2() {
+ throw new RuntimeException("It's fun to fail!")
+ }
+}
+```
+
+Then the output is:
+```
+Exception in thread "main" java.lang.RuntimeException: It's fun to fail!
+ at callStack.profiler.examples.Example.m2(Example.groovy:15)
+...
+...
+```
+
+### Entry method
+
+At runtime, profiling starts when the very first profiling artifact is encountered, which can be one of these:
+- ``@Profile`` annotation
+- ``Cprof.prof`` method
+- ``CProf.start`` method
+
+If the same entry point is encountered again then the profiling restarts/resets (there can only be one entry point).
+Please consider:
+
+```groovy
+class Example {
+ @Profile
+ int entryPoint() {
+ 5.times { m2() }
+ return 10
+ }
+
+ @Profile()
+ int m2() {
+ Thread.sleep(200)
+ return 5
+ }
+}
+```
+and then:
+```groovy
+class ForDocs {
+ static void main(String[] args) {
+ 5.times {
+ new Example().entryPoint()
+ }
+ println CProf.prettyPrint()
+ }
+}
+```
+
+The output is then:
+```
+|-> Example.entryPoint (1) : 1s 001ms [000ms]
+| |-> Example.m2 (5) : 1s 001ms
+```
+
+``entryPoint()`` is the first time a profiling event is discovered, so each time the profiler encounters the entry point method it resets its profiling stack.
+Let's move ``CProf.prettyPrint()`` into the loop:
+```groovy
+class ForDocs {
+ static void main(String[] args) {
+ 5.times {
+ new Example().entryPoint()
+ println CProf.prettyPrint()
+ }
+ }
+}
+```
+Now the output is:
+```
+|-> Example.entryPoint (1) : 1s 011ms [001ms]
+| |-> Example.m2 (5) : 1s 010ms
+|-> Example.entryPoint (1) : 1s 001ms [000ms]
+| |-> Example.m2 (5) : 1s 001ms
+|-> Example.entryPoint (1) : 1s 001ms [000ms]
+| |-> Example.m2 (5) : 1s 001ms
+|-> Example.entryPoint (1) : 1s 003ms [001ms]
+| |-> Example.m2 (5) : 1s 002ms
+|-> Example.entryPoint (1) : 1s 001ms [000ms]
+| |-> Example.m2 (5) : 1s 001ms
+```
+
+### Access Profile Stack Programmatically
+
+Instead of using ``CProf.prettyPrint()`` you can get a hold of the entry event programmatically via ``CProf.rootEvent`` and then store the results anywhere you want.
+For example:
+```groovy
+ProfileEvent entryEvent = CProf.rootEvent
+// grab child events
+entryEvent.children.each {
+ // use these accessors
+ it.getName()
+ it.getNumOfInvocations()
+ it.getRuntimeInMillis()
+ it.isConcurrent()
+ it.isRemote()
+}
+```
+
+## How does it work?
+
+Call Stack profiler utilizes Groovy's (Abstract Syntax Tree) AST Transformation to inject profiling code into the annotated methods.
+Profiling code is injected during the compilation phase so there is no introspection at runtime which accounts for the minimal overhead.
+
+For example take the following code:
+
+```groovy
+ @Profile()
+ int m2() {
+ return 5
+ }
+```
+
+will be compiled into something like this:
+
+```groovy
+ int m2() {
+ String profName = "m2"
+ CProf.start(profName)
+ try {
+ return 5
+ } finally {
+ CProf.stop(profName)
+ }
+ }
+```
diff --git a/call-stack-profiler/pom.xml b/call-stack-profiler/pom.xml
new file mode 100644
index 00000000..8cbd59b6
--- /dev/null
+++ b/call-stack-profiler/pom.xml
@@ -0,0 +1,314 @@
+
+
+ 4.0.0
+
+
+ skills-service-parent
+ skill-tree
+ 1.3.0-SNAPSHOT
+
+
+ call-stack-profiler
+
+
+
+ Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 3.0.5
+ 2.0-M3-groovy-3.0
+
+ 3.6.0-03
+
+ 3.0.5-01
+
+ 3.8.1
+ 3.11
+ 2.10.6
+ 2.13.3
+
+ 2.7
+ 1.11.2
+
+
+
+
+ org.codehaus.groovy
+ groovy-all
+ ${groovy.version}
+ provided
+ pom
+
+
+ org.codehaus.groovy
+ groovy-testng
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons.lang3.version}
+
+
+ joda-time
+ joda-time
+ ${joda.time.version}
+
+
+
+ org.apache.logging.log4j
+ log4j-bom
+ ${log4j.version}
+ pom
+ import
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ ${log4j.version}
+
+
+
+ org.spockframework
+ spock-core
+ ${spock.myVersion}
+ test
+
+
+
+ org.codehaus.groovy
+ groovy
+ ${groovy.version}
+ provided
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 2.8.2
+
+
+
+
+
+
+
+ maven-compiler-plugin
+ ${maven.compiler.plugin.version}
+
+ 11
+ 11
+ groovy-eclipse-compiler
+
+
+ config.groovy
+
+
+
+
+ org.codehaus.groovy
+ groovy-eclipse-compiler
+ ${groovy.eclipse.compiler.version}
+
+
+ org.codehaus.groovy
+ groovy-eclipse-batch
+ ${groovy.eclipse.batch.version}
+
+
+ org.codehaus.groovy
+ groovy-all
+ ${groovy.version}
+ pom
+
+
+ org.codehaus.groovy
+ groovy-testng
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ versions-maven-plugin
+ ${versions.maven.plugin}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M3
+
+
+ **/*Spec*.java
+ **/*Test.java
+ **/*Tests.java
+ **/*Spec*.groovy
+ **/*Test.groovy
+ **/*Tests.groovy
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+
+
+
+ org.codehaus.mojo
+ license-maven-plugin
+ 2.0.0
+
+
+ default-cli
+ package
+
+ add-third-party
+
+
+
+ true
+
+
+ true
+ test,provided
+
+
+ true
+ true
+
+ The Apache Software License, Version 2.0
+ MIT License
+ Eclipse Public License - Version 1.0
+ Eclipse Public License - Version 2.0
+ The 3-Clause BSD License
+
+
+ The Apache Software License, Version 2.0|Apache License, Version 2.0|The Apache License, Version 2.0
+ Eclipse Public License - Version 1.0|Eclipse Public License 1.0
+ Eclipse Public License - Version 2.0|Eclipse Public License v2.0
+ The 3-Clause BSD License|New BSD License|BSD Licence 3|BSD License 3
+
+
+
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 3.0
+
+ ${basedir}/src/license/LICENSE-HEADER.txt
+
+ **/*.xml
+ **/*.jks
+ **/*.ftl
+ src/main/resources/public/**
+ **/license/*.properties
+ LICENSE.txt
+
+
+
+
+ package
+
+ check
+
+
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
+
+
+ nexus-releases
+ Release Repository
+
+ ${nexusServer}/repository/maven-releases/
+
+
+
+ nexus-snapshots
+ Snapshot Repository
+
+ ${nexusServer}/repository/maven-snapshots/
+
+
+
+
+
diff --git a/call-stack-profiler/src/license/LICENSE-HEADER.txt b/call-stack-profiler/src/license/LICENSE-HEADER.txt
new file mode 100644
index 00000000..641df477
--- /dev/null
+++ b/call-stack-profiler/src/license/LICENSE-HEADER.txt
@@ -0,0 +1,13 @@
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/AsyncProcess.groovy b/call-stack-profiler/src/main/java/callStack/profiler/AsyncProcess.groovy
new file mode 100644
index 00000000..80b84975
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/AsyncProcess.groovy
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import groovy.util.logging.Slf4j
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.BlockingQueue
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
+@Slf4j
+class AsyncProcess {
+
+ int secondsToPoll = 2
+ int queueSize = 1000
+ boolean dropIfFull = false
+
+ BlockingQueue toProcess
+ Thread thread
+ AtomicBoolean stopped = new AtomicBoolean(false)
+
+ AsyncProcess start(){
+ assert !toProcess, "already started"
+
+ toProcess = new ArrayBlockingQueue(queueSize)
+ log.info("Starting with queue size of [{}]", queueSize)
+ thread = Thread.start("${this.class.simpleName}(${this.hashCode()})") {
+ while(!stopped.get()){
+ Closure closure = toProcess.poll(secondsToPoll, TimeUnit.SECONDS)
+ try {
+ if (closure != null) {
+ closure.call()
+ }
+ } catch (Throwable t){
+ log.error("Failed to process async task", t)
+ }
+ }
+ }
+ return this
+ }
+
+ boolean async(Closure executeMe ){
+ assert toProcess != null
+ boolean res = true
+ if(dropIfFull){
+ res = toProcess.offer(executeMe)
+ if(!res){
+ log.warn("Async queue is full!!!! \n" +
+ " Investigate why internal thread isn't servicing requests in a timely manner.\n" +
+ " Dropping incoming request for class [{}]", executeMe.class)
+ }
+ } else {
+ toProcess.add(executeMe)
+ }
+
+ return res
+ }
+
+ void stop() {
+ stopped.set(true)
+ }
+}
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/CProf.java b/call-stack-profiler/src/main/java/callStack/profiler/CProf.java
new file mode 100644
index 00000000..0e061de8
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/CProf.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler;
+
+import groovy.lang.Closure;
+import groovy.transform.CompileStatic;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.apache.commons.lang3.Validate.notNull;
+
+@CompileStatic
+public class CProf {
+ public static final AtomicBoolean turnTreeProfilingOff = new AtomicBoolean(false);
+
+ final static ThreadLocal> profileEventStack = new ThreadLocal>();
+ final static ThreadLocal rootEventThreadLocal = new ThreadLocal();
+ final static AtomicLong counter = new AtomicLong(0);
+ final static ProfileEvent EMPTY = new ProfileEvent();
+ static {
+ EMPTY.setName("treeProfilingDisabled");
+ }
+
+ public static void clear(){
+ rootEventThreadLocal.set(null);
+ profileEventStack.set(null);
+ counter.set(0);
+ }
+
+ public static void start(String name) {
+ notNull(name);
+
+ Deque stack = profileEventStack.get();
+ if (stack == null) {
+ stack = new ArrayDeque();
+ profileEventStack.set(stack);
+ }
+
+ ProfileEvent event = null;
+ ProfileEvent parent = getParent();
+ if (parent != null && turnTreeProfilingOff.get()) {
+ //if tree profiling is disabled, don't start any new ProfileEvents if there
+ //is already a parent/root event
+ return;
+ }else if (parent != null) {
+ event = parent.getEvent(name);
+ }
+
+ if (event == null) {
+ event = new ProfileEvent();
+ event.setName(name);
+ if (parent != null) {
+ parent.addChild(event);
+ }
+ }
+
+ // if stack is empty then consider this to be an entry point
+ if (stack.isEmpty()) {
+ rootEventThreadLocal.set(event);
+ }
+ stack.push(event);
+ event.startEvent();
+ }
+
+ public static ProfileEvent getParent() {
+ ProfileEvent parent = null;
+ Deque stack = profileEventStack.get();
+ if (stack != null) {
+ parent = stack.peek();
+ }
+ return parent;
+ }
+
+ public static ProfileEvent stop(String name) {
+ return stop(name, true);
+ }
+
+ public static ProfileEvent stop(String name, boolean aggregate) {
+ notNull(name);
+ ProfileEvent rootEvent = getRootEvent();
+ boolean stoppingRoot = rootEvent != null && rootEvent.getName().equals(name);
+ if(turnTreeProfilingOff.get() && !stoppingRoot){
+ //if tree profiling is turned off and the call isn't to stop the rootEvent, return null
+
+ //if disable gets set in between a start and stop call
+ //we'll end up with invalid elements in the event stack, we need to clear those out
+ Deque stack = profileEventStack.get();
+ while (stack.size() > 1) {
+ //remove any ProfilingEvents that were started in between tree profiling being disabled and enabled
+ ProfileEvent pe = stack.pop();
+ }
+ return EMPTY;
+ }
+ Deque stack = profileEventStack.get();
+
+ if(!stoppingRoot && stack.size() == 1){
+ //tree profiling must have been re-enabled in between start and stop call
+ //we can't stop this event as it was never started, return EMPTY results rather than throwing an exception.
+ return EMPTY;
+ }
+
+ if (stack == null) {
+ notNull(stack, "Must call start prior calling stop. Name [" + name + "]");
+ }
+ ProfileEvent event = stack.pop();
+ notNull(event, "Must call start prior calling stop. Name=$name");
+ if(!event.getName().equals(name)){
+ throw new IllegalArgumentException("Current event's name=["+event.getName()+"] but stop name=["+name+"]");
+ }
+ event.endEvent();
+ if (!aggregate) {
+ String previousName = event.getName();
+ event.setName(event.getName() + "_" + counter.getAndIncrement());
+ if (event.getParent()!=null) {
+ event.getParent().replaceChild(previousName, event);
+ }
+ }
+ return event;
+ }
+
+ public static ProfileEvent prof(String name, Closure profileMe){
+ return prof(name, true, profileMe);
+ }
+
+ public static ProfileEvent prof(String name, boolean aggregate, Closure profileMe) {
+ if (!aggregate) {
+ name = name + "_" + counter.getAndIncrement();
+ }
+
+ ProfileEvent res = null;
+ start(name);
+ boolean hadExcetpion = false;
+ try {
+ profileMe.call();
+ } catch (Throwable t){
+ hadExcetpion = true;
+ throw t;
+ } finally {
+ try {
+ res = stop(name);
+ } catch (Throwable stopT){
+
+ // the stack is officially broken since we couldn't stop a profiling event
+ // the only thing to do is clear the stack so next profiling session is correct
+ clear();
+
+ // if stop method itself throws an exception it will hide the original exception
+ if(!hadExcetpion) {
+ throw stopT;
+ }
+ }
+ }
+ return res;
+ }
+
+ public static String prettyPrint() {
+ ProfileEvent profileEvent = getRootEvent();
+ if(profileEvent!= null){
+ return profileEvent.prettyPrint();
+ }
+ return "No profiling events";
+ }
+
+ public static ProfileEvent getRootEvent() {
+ return rootEventThreadLocal.get();
+ }
+
+ public static void initRootEvent(ProfileEvent rootEvent) {
+ ProfileEvent existing = getRootEvent();
+ if(existing != null ){
+ throw new IllegalArgumentException("Root event is already set. Event name is [" + existing.getName() + "]" );
+ }
+ rootEventThreadLocal.set(rootEvent);
+ ArrayDeque stack = new ArrayDeque();
+ stack.push(rootEvent);
+ profileEventStack.set(stack);
+ }
+}
\ No newline at end of file
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/ProfAsyncResult.groovy b/call-stack-profiler/src/main/java/callStack/profiler/ProfAsyncResult.groovy
new file mode 100644
index 00000000..30725190
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/ProfAsyncResult.groovy
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+class ProfAsyncResult {
+ T res
+ ProfileEvent profileEvent
+}
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/ProfFuture.groovy b/call-stack-profiler/src/main/java/callStack/profiler/ProfFuture.groovy
new file mode 100644
index 00000000..0e4f5381
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/ProfFuture.groovy
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import groovy.transform.CompileStatic
+
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+@CompileStatic
+class ProfFuture implements Future {
+
+ Future> underlyingFuture
+
+ @Override
+ boolean cancel(boolean mayInterruptIfRunning) {
+ return underlyingFuture.cancel(mayInterruptIfRunning)
+ }
+
+ @Override
+ boolean isCancelled() {
+ return underlyingFuture.cancelled
+ }
+
+ @Override
+ boolean isDone() {
+ return underlyingFuture.done
+ }
+
+ @Override
+ T get() throws InterruptedException, ExecutionException {
+ ProfAsyncResult profAsyncResult = underlyingFuture.get()
+ documentProfiling(profAsyncResult)
+ return profAsyncResult.res
+ }
+
+ @Override
+ T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ ProfAsyncResult profAsyncResult = underlyingFuture.get(timeout,unit)
+ documentProfiling(profAsyncResult)
+ return profAsyncResult.res
+ }
+
+ private void documentProfiling(ProfAsyncResult profAsyncResult) {
+ profAsyncResult.profileEvent.concurrent = true
+ if (CProf?.parent) {
+ CProf?.parent.addChild(profAsyncResult.profileEvent)
+ }
+ }
+}
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/ProfThreadPool.groovy b/call-stack-profiler/src/main/java/callStack/profiler/ProfThreadPool.groovy
new file mode 100644
index 00000000..7179370e
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/ProfThreadPool.groovy
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import callStack.utils.CachedThreadPool
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import java.util.concurrent.Callable
+import java.util.concurrent.Future
+
+@Slf4j
+@CompileStatic
+class ProfThreadPool {
+
+ CachedThreadPool cachedThreadPool
+ boolean assignUniqueNameToEachRootEvent = false
+ final String poolName
+ boolean warnIfFull = true
+
+ public ProfThreadPool(String name) {
+ cachedThreadPool = new CachedThreadPool(name)
+ this.poolName = name
+ }
+
+ public ProfThreadPool(final String name, int numThreads) {
+ this(name, numThreads, numThreads)
+ }
+
+ public ProfThreadPool(final String name, int minNumOfThreads, int maxNumOfThreads) {
+ cachedThreadPool = new CachedThreadPool(name, minNumOfThreads, maxNumOfThreads)
+ this.poolName = name
+ }
+
+ static class ProfCallable implements Callable {
+ Callable callable
+ boolean uniqueName = false
+
+ @Override
+ ProfAsyncResult call() throws Exception {
+ CProf.clear()
+ final String threadName = Thread.currentThread().name
+ String rootEventName = threadName
+ if (uniqueName) {
+ rootEventName = "${rootEventName}-${UUID.randomUUID().toString()}"
+ }
+ Object o
+ ProfileEvent rootEvent = CProf.prof(rootEventName) {
+ o = callable.call()
+ }
+
+ // use the interinal impl root event if there is one defined
+ if (rootEvent && rootEvent?.children.size() == 1) {
+ rootEvent = rootEvent.children.first()
+ rootEvent.name = rootEvent.name + "-" + threadName
+ }
+ return new ProfAsyncResult(res: o, profileEvent: rootEvent)
+ }
+ }
+
+ public List asyncExec(List> listToSubmit) {
+ assert listToSubmit
+
+ warnIfFull(listToSubmit.size())
+ List> profCallables = []
+ listToSubmit.each {
+ profCallables.add((Callable) new ProfCallable(callable: it, uniqueName: assignUniqueNameToEachRootEvent))
+ }
+
+ List profAsyncResults = (List) cachedThreadPool.submitAndGetResults(profCallables)
+ List res = []
+ profAsyncResults.each {
+ res.add((T) it.res)
+ it.profileEvent.concurrent = true
+ if (CProf?.parent) {
+ CProf?.parent.addChild(it.profileEvent)
+ }
+ }
+
+ return res
+ }
+
+ private void warnIfFull(int numToSubmit) {
+ if (warnIfFull) {
+ double currentPoolSize = (double) (cachedThreadPool.activePoolSize + numToSubmit)
+ double percentFull = currentPoolSize / cachedThreadPool.maximumPoolSize
+ if (percentFull > 0.9) {
+ log.warn("[{}] pool is > 90% full, [{}] current threads", poolName, ((int) currentPoolSize - 1))
+ }
+ }
+ }
+
+ public Future submit(Callable callable) {
+ warnIfFull(1)
+ List futures = cachedThreadPool.submit(new ProfCallable(callable: callable, uniqueName: assignUniqueNameToEachRootEvent), 1)
+ return new ProfFuture(underlyingFuture: futures.first())
+ }
+
+ int getMaximumPoolSize() {
+ return cachedThreadPool.maximumPoolSize
+ }
+
+ int getCurrentPoolSize() {
+ return cachedThreadPool.currentPoolSize
+ }
+
+ int getActivePoolSize() {
+ return cachedThreadPool.activePoolSize
+ }
+
+ void shutdown() {
+ cachedThreadPool.shutdown()
+ }
+}
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/Profile.java b/call-stack-profiler/src/main/java/callStack/profiler/Profile.java
new file mode 100644
index 00000000..77f21acc
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/Profile.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD, ElementType.TYPE})
+@GroovyASTTransformationClass("callStack.profiler.ProfileASTTransformation")
+public @interface Profile {
+ String name() default "";
+ boolean aggregateIntoSingleEvent() default true;
+}
+
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/ProfileASTTransformation.groovy b/call-stack-profiler/src/main/java/callStack/profiler/ProfileASTTransformation.groovy
new file mode 100644
index 00000000..068e0c36
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/ProfileASTTransformation.groovy
@@ -0,0 +1,138 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import groovy.util.logging.Slf4j
+import org.codehaus.groovy.ast.*
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.stmt.BlockStatement
+import org.codehaus.groovy.ast.stmt.ExpressionStatement
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.ast.stmt.TryCatchStatement
+import org.codehaus.groovy.control.CompilePhase
+import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.transform.AbstractASTTransformation
+import org.codehaus.groovy.transform.GroovyASTTransformation
+
+import java.util.concurrent.atomic.AtomicLong
+
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+@Slf4j
+public class ProfileASTTransformation extends AbstractASTTransformation {
+ final static AtomicLong counter = new AtomicLong(0)
+ public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
+ if (!nodes) return
+ if (!nodes[0]) return
+ if (!nodes[1]) return
+ if (!nodes[0] instanceof AnnotatedNode) return
+ if (!nodes[1] instanceof MethodNode) return
+
+ MethodNode annotatedMethod = nodes[1]
+ List annotationNodeList = annotatedMethod.getAnnotations(new ClassNode(Profile))
+ if (!annotationNodeList) {
+ return
+ }
+ String profileKey = getMemberStringValue(annotationNodeList, "name")
+ boolean aggregateIntoSingleEvent = getMemberBooleanValue(annotationNodeList, "aggregateIntoSingleEvent", true)
+
+ if (!profileKey) {
+ ClassNode declaringClass = annotatedMethod.declaringClass
+ profileKey = declaringClass.nameWithoutPackage + "." + annotatedMethod.name
+
+ // add the parameter types to the profile key if more than one method exists with the same name
+ if (declaringClass.getMethods(annotatedMethod.name).size() > 1) {
+ for (Parameter parameter : annotatedMethod.parameters) {
+ profileKey += '_' + parameter.type.nameWithoutPackage
+ }
+ }
+ }
+ if(!aggregateIntoSingleEvent){
+ profileKey = profileKey+counter.getAndIncrement()
+ }
+
+ log.info('profile key is {}', profileKey)
+ Statement startMessage = createProfileCallAst("start", profileKey)
+ Statement endMessage = createProfileCallAst("stop", profileKey, aggregateIntoSingleEvent)
+ wrapWithTryFinally(annotatedMethod, startMessage, endMessage)
+ }
+
+ private static void wrapWithTryFinally(MethodNode methodNode, Statement startProf, Statement stopProf) {
+ BlockStatement code = (BlockStatement) methodNode.getCode()
+ BlockStatement newCode = new BlockStatement()
+ newCode.addStatement(startProf)
+
+ TryCatchStatement tryCatchStatement = new TryCatchStatement(code, new BlockStatement())
+ newCode.addStatement(tryCatchStatement)
+ methodNode.setCode(newCode)
+ tryCatchStatement.setFinallyStatement(stopProf)
+ }
+
+ private Statement createProfileCallAst(String method, String message) {
+ return new ExpressionStatement(
+ new StaticMethodCallExpression(
+ ClassHelper.make(CProf),
+ method,
+ new ArgumentListExpression(
+ new ConstantExpression(message)
+ )
+ )
+ )
+ }
+ private Statement createProfileCallAst(String method, String message, boolean aggregateIntoSingleEvent) {
+ return new ExpressionStatement(
+ new StaticMethodCallExpression(
+ ClassHelper.make(CProf),
+ method,
+ new ArgumentListExpression(
+ new ConstantExpression(message),
+ new ConstantExpression(aggregateIntoSingleEvent)
+ )
+ )
+ )
+ }
+
+ protected String getMemberStringValue(List annotationNodeList, String name){
+ annotationLoop: for (AnnotationNode annotationNode : annotationNodeList) {
+ String res = getMemberStringValue(annotationNode, name)
+ if(res){
+ return res
+ }
+ }
+ return null
+ }
+
+ protected Boolean getMemberBooleanValue(List annotationNodeList, String name, boolean defaultVal){
+ annotationLoop: for (AnnotationNode annotationNode : annotationNodeList) {
+ Boolean res = getMemberBooleanValue(annotationNode, name)
+ if(res!=null){
+ return res
+ }
+ }
+ defaultVal
+ }
+
+ protected Boolean getMemberBooleanValue(AnnotationNode node, String name) {
+ final Expression member = node.getMember(name);
+ if (member != null && member instanceof ConstantExpression) {
+ Object result = ((ConstantExpression) member).getValue();
+ if (result != null) return (boolean)result;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/call-stack-profiler/src/main/java/callStack/profiler/ProfileEvent.java b/call-stack-profiler/src/main/java/callStack/profiler/ProfileEvent.java
new file mode 100644
index 00000000..b2fa43d4
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/profiler/ProfileEvent.java
@@ -0,0 +1,262 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler;
+
+import org.apache.commons.lang3.Validate;
+import org.joda.time.Period;
+import org.joda.time.format.PeriodFormatter;
+import org.joda.time.format.PeriodFormatterBuilder;
+
+import java.io.Serializable;
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static org.apache.commons.lang3.Validate.notNull;
+
+public class ProfileEvent implements Serializable {
+ static final long serialVersionUID = 1l;
+
+ long runtimeInMillis = 0;
+ int numOfInvocations = 0;
+ String name;
+
+ // interal use only please
+ long start = -1;
+ // these are display only
+ boolean isConcurrent = false;
+ boolean isRemote = false;
+ ProfileEvent parent;
+ // ---------------------------------
+ Map childrenAsMap;
+
+ public Collection getChildren() {
+ if (childrenAsMap == null) {
+ return Collections.emptyList();
+ }
+ return childrenAsMap.values();
+ }
+
+ public synchronized void addChild(ProfileEvent child) {
+ notNull(child);
+ notNull(child.getName());
+
+ if (childrenAsMap == null) {
+ childrenAsMap = new ConcurrentHashMap();
+ }
+ childrenAsMap.put(child.getName(), child);
+ child.setParent(this);
+ }
+
+ synchronized void replaceChild(String previousName, ProfileEvent child) {
+ childrenAsMap.remove(previousName);
+ childrenAsMap.put(child.getName(), child);
+ }
+
+
+ public ProfileEvent getEvent(String str) {
+ notNull(str);
+ ProfileEvent res = null;
+ if (childrenAsMap != null) {
+ res = childrenAsMap.get(str);
+ }
+ return res;
+ }
+
+ public void startEvent() {
+ if (start != -1) {
+ Validate.isTrue(start == -1, "Can not start event twice. Event [" + name + "] has already been started");
+ }
+
+ start = System.currentTimeMillis();
+ }
+
+ public void endEvent() {
+ if (start == -1) {
+ throw new IllegalArgumentException("Must call startEvent first");
+ }
+ numOfInvocations++;
+ runtimeInMillis = runtimeInMillis + (System.currentTimeMillis() - start);
+ start = -1;
+ }
+
+ public String prettyPrint() {
+ StringBuilder res = new StringBuilder();
+ buildPrettyString(res, this, "");
+ return res.toString();
+ }
+
+ private final static NumberFormat NUMBER_FORMAT = NumberFormat.getInstance();
+
+ private void buildPrettyString(StringBuilder res, ProfileEvent node, String pre) {
+ if (res.length() > 0) {
+ res.append("\n");
+ }
+ StringBuilder preBuilder = new StringBuilder(pre);
+ if (node.isConcurrent) {
+ preBuilder.append("|");
+ }
+ if (node.isRemote) {
+ preBuilder.append("||");
+ }
+ preBuilder.append("|");
+ res.append(preBuilder.toString());
+
+ res.append("-> ");
+ res.append(node.getName());
+ res.append(" (");
+ res.append(NUMBER_FORMAT.format(node.getNumOfInvocations()));
+ res.append(") : ");
+ addRuntime(res, node.getRuntimeInMillis());
+
+ boolean hasChildren = node != null && !isEmpty(node.getChildrenAsMap());
+ if (hasChildren) {
+ handleUnaccountedTime(res, node);
+ }
+ if (hasChildren) {
+ preBuilder.append(" ");
+ for (ProfileEvent profileEvent : node.getChildrenAsMap().values()) {
+ buildPrettyString(res, profileEvent, preBuilder.toString());
+ }
+ }
+ }
+
+ private static boolean isEmpty(Map map) {
+ return map == null || map.isEmpty();
+ }
+
+ private void handleUnaccountedTime(StringBuilder res, ProfileEvent node) {
+ Collection values = node.getChildrenAsMap().values();
+
+ long childrenSum = 0;
+ List syncEvents = values.stream().filter(p-> !isConcurrent(p)).collect(Collectors.toList());
+ if(!syncEvents.isEmpty()){
+ childrenSum += syncEvents.stream().mapToLong(ProfileEvent::getRuntimeInMillis).sum();
+ }
+ List asyncEvents = values.stream().filter(p-> isConcurrent(p)).collect(Collectors.toList());
+ if(!asyncEvents.isEmpty()){
+ childrenSum += asyncEvents.stream().mapToLong(ProfileEvent::getRuntimeInMillis).max().getAsLong();
+ }
+
+ long diff = node.getRuntimeInMillis() - childrenSum;
+ res.append(" [");
+ res.append(periodFormatter.print(new Period(diff)));
+ res.append("]");
+ }
+ private boolean isConcurrent(ProfileEvent p){
+ return p.isRemote() || p.isConcurrent();
+ }
+
+ private final static long SECOND = 1000;
+ private final static long MINUTE = 60 * SECOND;
+ private final static long HOUR = 60 * MINUTE;
+
+
+ private static final PeriodFormatter periodFormatter = new PeriodFormatterBuilder()
+ .appendHours()
+ .appendSuffix("h")
+ .appendSeparatorIfFieldsBefore(" ")
+ .appendMinutes()
+ .appendSuffix("m")
+ .appendSeparatorIfFieldsBefore(" ")
+ .appendSeconds()
+ .appendSuffix("s")
+ .appendSeparatorIfFieldsBefore(" ")
+ .appendMillis3Digit()
+ .appendSuffix("ms").toFormatter();
+
+ private void addRuntime(StringBuilder res, long runtime) {
+ res.append(periodFormatter.print(new Period(runtime)));
+ }
+
+ public boolean isEnded() {
+ return start == -1;
+ }
+
+ @Override
+ public String toString() {
+ return prettyPrint();
+ }
+
+ public long getRuntimeInMillis() {
+ return runtimeInMillis;
+ }
+
+ public void setRuntimeInMillis(long runtimeInMillis) {
+ this.runtimeInMillis = runtimeInMillis;
+ }
+
+ public int getNumOfInvocations() {
+ return numOfInvocations;
+ }
+
+ public void setNumOfInvocations(int numOfInvocations) {
+ this.numOfInvocations = numOfInvocations;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public boolean isConcurrent() {
+ return isConcurrent;
+ }
+
+ public void setConcurrent(boolean concurrent) {
+ isConcurrent = concurrent;
+ }
+
+ public boolean isRemote() {
+ return isRemote;
+ }
+
+ public void setRemote(boolean remote) {
+ isRemote = remote;
+ }
+
+ public ProfileEvent getParent() {
+ return parent;
+ }
+
+ public void setParent(ProfileEvent parent) {
+ this.parent = parent;
+ }
+
+ public Map getChildrenAsMap() {
+ return childrenAsMap;
+ }
+
+ public void setChildrenAsMap(Map childrenAsMap) {
+ this.childrenAsMap = childrenAsMap;
+ }
+}
+
diff --git a/call-stack-profiler/src/main/java/callStack/utils/AbstractThreadPool.java b/call-stack-profiler/src/main/java/callStack/utils/AbstractThreadPool.java
new file mode 100644
index 00000000..537f4789
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/utils/AbstractThreadPool.java
@@ -0,0 +1,301 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.utils;
+
+import org.apache.commons.lang3.Validate;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public abstract class AbstractThreadPool implements ThreadPoolStats {
+
+ protected ExecutorService m_pool = null;
+
+ public List submitAndGetExceptions(Callable> submitMe,
+ int numOfSubmissions) {
+ List allExceptions = submitAndGetExceptions(m_pool, submitMe,
+ numOfSubmissions);
+ return allExceptions;
+ }
+
+ public List submitAndGetExceptions(List> submitList) {
+ List allExceptions = submitAndGetExceptions(m_pool, submitList);
+ return allExceptions;
+ }
+
+ public List> submit(Callable submitMe, int numOfSubmissions) {
+ return submit(m_pool, submitMe, numOfSubmissions);
+ }
+
+ public List> submit(List> listToSubmit) {
+ return submitAll(m_pool, listToSubmit);
+ }
+
+ public List submitAndGetResults(List> listToSubmit) {
+ List> futures = submitAll(m_pool, listToSubmit);
+
+ List results = pullOutResults(futures);
+ return results;
+ }
+
+ private List pullOutResults(List> futures) {
+ List results = new ArrayList();
+ for (Future future : futures) {
+ try {
+ T result = future.get();
+ if (result != null) {
+ results.add(result);
+ }
+ } catch (Throwable e) {
+ throw new RuntimeException("Failed to execute callable.", e);
+ }
+ }
+ return results;
+ }
+
+ public List submitAndGetResults(Callable callable,
+ int numOfSubmissions) {
+ Validate.notNull(callable);
+ Validate.isTrue(numOfSubmissions > 0,
+ "Must at submit at least 1 callable");
+
+ List> futures = submit(callable, numOfSubmissions);
+ return pullOutResults(futures);
+ }
+
+ public void shutdown() {
+ m_pool.shutdown();
+ }
+
+ /**
+ * The default thread factory
+ *
+ * Code borrowed from JDK, named was added to the constructor
+ */
+ static class NamedThreadFactory implements ThreadFactory {
+ final ThreadGroup group;
+
+ final AtomicInteger threadNumber = new AtomicInteger(1);
+
+ final String namePrefix;
+
+ NamedThreadFactory(String name) {
+ SecurityManager s = System.getSecurityManager();
+ group = (s != null) ? s.getThreadGroup() : Thread.currentThread()
+ .getThreadGroup();
+ namePrefix = name + "-";
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(group, r, namePrefix
+ + threadNumber.getAndIncrement(), 0);
+ if (t.isDaemon())
+ t.setDaemon(false);
+ if (t.getPriority() != Thread.NORM_PRIORITY)
+ t.setPriority(Thread.NORM_PRIORITY);
+ return t;
+ }
+ }
+
+ /**
+ * Submit provided {@link Callable} numOfSubmissions times, wait for ALL the
+ * threads to finish and return a list of the exceptions generated by the
+ * threads. If there are no exceptions then an empty list is returned.
+ *
+ * @param
+ * @param executor
+ * - executor service to use
+ * @param callable
+ * - {@link Callable} to submit numOfSubmissions times
+ * @param numOfSubmissions
+ * number of times to submit the provided callable
+ * @return
+ */
+ private List submitAndGetExceptions(
+ final ExecutorService executor, final Callable callable,
+ final int numOfSubmissions) {
+ List> futures = submit(executor, callable, numOfSubmissions);
+ return getExceptionsInList(futures);
+ }
+
+ /**
+ * Submit provided {@link Callable} numOfSubmissions times and return the
+ * futures
+ *
+ * @param
+ * @param executor
+ * - executor service to use
+ * @param callable
+ * - {@link Callable} to submit numOfSubmissions times
+ * @param numOfSubmissions
+ * number of times to submit the provided callable
+ * @return a list of futures
+ */
+ private List> submit(
+ final ExecutorService executor, final Callable callable,
+ final int numOfSubmissions) {
+ List> futures = new ArrayList>(numOfSubmissions);
+ for (int i = 0; i < numOfSubmissions; i++) {
+ futures.add(executor.submit(callable));
+ }
+
+ return futures;
+ }
+
+ /**
+ * Retrieves all Exceptions from the List of Future objects and puts them in
+ * a list of Strings. Note: will halt the calling thread till all the
+ * futures/threads are completed.
+ *
+ * @param
+ * @param futures
+ * @return the List of Strings, one element for each Future's Exception
+ */
+ private List getExceptionsInList(
+ final List> futures) {
+ List exceptions = new ArrayList(futures.size());
+
+ for (Future future : futures) {
+ try {
+ future.get();
+ } catch (Throwable append) {
+ exceptions.add(getStackTraceFromThrowable(append));
+ }
+ }
+
+ return exceptions;
+ }
+
+ private String getStackTraceFromThrowable(Throwable t) {
+ if (t != null) {
+ StringWriter writer = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(writer);
+ t.printStackTrace(printWriter);
+ String stackTrace = writer.toString();
+ printWriter.close();
+ return stackTrace;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Submit a list of {@link Callable}s, wait for them to execute and return
+ * exceptions
+ *
+ * @param
+ * @param executor
+ * - executor service to use
+ * @param callables
+ * - a list o {@link Callable}s to submit
+ * @return
+ */
+ private List submitAndGetExceptions(
+ final ExecutorService executor, final List> callables) {
+ List> futures = submitAll(executor, callables);
+ return getExceptionsInList(futures);
+ }
+
+ /**
+ * Submits all {@link Callable} tasks in the list using the provided
+ * ExecutorService
+ *
+ * @param
+ * @param executor
+ * @param callables
+ * @return a list of Future objects, one for each submitted task
+ */
+ private List> submitAll(
+ final ExecutorService executor, final List> callables) {
+ List> futures = new ArrayList>(callables.size());
+ for (Callable callable : callables) {
+ futures.add(executor.submit(callable));
+ }
+ return futures;
+ }
+
+ /**
+ * Retrieves all Exceptions from the List of Future objects and appends them
+ * to a String. Note: will halt the calling thread till all the
+ * futures/threads are completed.
+ *
+ * @param
+ * @param futures
+ * @return a String containing all Exceptions thrown from the Future tasks
+ */
+ public String getExceptions(final List> futures) {
+ StringBuilder builder = new StringBuilder();
+ for (Future future : futures) {
+ try {
+ future.get();
+ } catch (Throwable append) {
+ builder.append(getStackTraceFromThrowable(append));
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * (U) Returns the maximum size of this thread pool. Some implementations of
+ * the underlying Executor may not expose the thread pool sizes. If this
+ * occurs, zero will be returned.
+ *
+ * @return The maximum size of this thread pool.
+ */
+ public int getMaximumPoolSize() {
+ if (m_pool instanceof ThreadPoolExecutor) {
+ return ((ThreadPoolExecutor) m_pool).getMaximumPoolSize();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * (U) Returns the current size of this thread pool. Some implementations of
+ * the underlying Executor may not expose the thread pool sizes. If this
+ * occurs, zero will be returned.
+ *
+ * @return The current size of this thread pool.
+ */
+ public int getCurrentPoolSize() {
+ if (m_pool instanceof ThreadPoolExecutor) {
+ return ((ThreadPoolExecutor) m_pool).getPoolSize();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * (U) Returns the number of actively running threads in this thread pool.
+ * Some implementations of the underlying Executor may not expose the thread
+ * pool sizes. If this occurs, zero will be returned.
+ *
+ * @return The active size of this thread pool.
+ */
+ public int getActivePoolSize() {
+ if (m_pool instanceof ThreadPoolExecutor) {
+ return ((ThreadPoolExecutor) m_pool).getActiveCount();
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/call-stack-profiler/src/main/java/callStack/utils/CachedThreadPool.java b/call-stack-profiler/src/main/java/callStack/utils/CachedThreadPool.java
new file mode 100644
index 00000000..f9978ecd
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/utils/CachedThreadPool.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.utils;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class CachedThreadPool extends AbstractThreadPool {
+
+ public CachedThreadPool(String name) {
+ m_pool = Executors.newCachedThreadPool(new NamedThreadFactory(name));
+ }
+
+ public CachedThreadPool(final String name, int minNumOfThreads, int maxNumOfThreads) {
+ m_pool = new ThreadPoolExecutor(minNumOfThreads, maxNumOfThreads,
+ 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue(),
+ new NamedThreadFactory(name));
+ }
+}
diff --git a/call-stack-profiler/src/main/java/callStack/utils/ClosureCallable.groovy b/call-stack-profiler/src/main/java/callStack/utils/ClosureCallable.groovy
new file mode 100644
index 00000000..b9515290
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/utils/ClosureCallable.groovy
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.utils
+
+import java.util.concurrent.Callable
+
+class ClosureCallable implements Callable{
+ Closure closure
+ @Override
+ T call() throws Exception {
+ return closure.call()
+ }
+}
diff --git a/call-stack-profiler/src/main/java/callStack/utils/ThreadPoolStats.java b/call-stack-profiler/src/main/java/callStack/utils/ThreadPoolStats.java
new file mode 100644
index 00000000..d18dbe7b
--- /dev/null
+++ b/call-stack-profiler/src/main/java/callStack/utils/ThreadPoolStats.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.utils;
+
+public interface ThreadPoolStats {
+
+ public int getMaximumPoolSize();
+
+ public int getCurrentPoolSize();
+
+ public int getActivePoolSize();
+}
diff --git a/backend/src/test/java/skills/intTests/PublicConfigSpecs.groovy b/call-stack-profiler/src/main/java/callStack/utils/ThreadPoolUtils.groovy
similarity index 67%
rename from backend/src/test/java/skills/intTests/PublicConfigSpecs.groovy
rename to call-stack-profiler/src/main/java/callStack/utils/ThreadPoolUtils.groovy
index 99f7ffd3..0545283d 100644
--- a/backend/src/test/java/skills/intTests/PublicConfigSpecs.groovy
+++ b/call-stack-profiler/src/main/java/callStack/utils/ThreadPoolUtils.groovy
@@ -13,17 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package skills.intTests
+package callStack.utils
-import skills.intTests.utils.DefaultIntSpec
+import java.util.concurrent.Callable
-class PublicConfigSpecs extends DefaultIntSpec {
+class ThreadPoolUtils {
- def "retrieve public configs"() {
- when:
- def config = skillsService.getPublicConfigs()
- then:
- config
- config.descriptionMaxLength == "2000"
+ static Callable callable(Closure closure) {
+ return new ClosureCallable(closure: closure)
}
}
diff --git a/call-stack-profiler/src/test/java/callStack/profiler/AsyncProcessSpecification.groovy b/call-stack-profiler/src/test/java/callStack/profiler/AsyncProcessSpecification.groovy
new file mode 100644
index 00000000..92a14dfa
--- /dev/null
+++ b/call-stack-profiler/src/test/java/callStack/profiler/AsyncProcessSpecification.groovy
@@ -0,0 +1,154 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import callStack.profiler.AsyncProcess
+import groovy.util.logging.Slf4j
+import spock.lang.Specification
+
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+
+@Slf4j
+class AsyncProcessSpecification extends Specification{
+
+ AsyncProcess asyncProcess
+ void cleanup(){
+ asyncProcess.stop()
+ }
+
+ def "Able to async process closure code"(){
+ AtomicInteger count = new AtomicInteger()
+
+ asyncProcess = new AsyncProcess()
+ asyncProcess.start()
+ when:
+ asyncProcess.async {
+ count.andIncrement
+ }
+ asyncProcess.async {
+ count.andIncrement
+ }
+ asyncProcess.async {
+ count.andIncrement
+ }
+ waitFor(count, 3)
+
+ then:
+ count.get() == 3
+ }
+
+ private void waitFor(AtomicInteger count, int numToWait) {
+ int num = 0
+ while (count.get() != numToWait && num < 10) {
+ Thread.sleep(200)
+ num++
+ }
+ }
+
+ def "Async code may throw exceptions"(){
+ AtomicInteger count = new AtomicInteger()
+
+ asyncProcess = new AsyncProcess()
+ asyncProcess.start()
+ when:
+ asyncProcess.async {
+ if(true){throw new IllegalArgumentException("fail")}
+ count.andIncrement
+ }
+ asyncProcess.async {
+ count.andIncrement
+ }
+ asyncProcess.async {
+ count.andIncrement
+ }
+ waitFor(count, 2)
+
+ then:
+ count.get() == 2
+ }
+
+ def "Execute real slow closure"(){
+ AtomicInteger count = new AtomicInteger(0)
+
+ asyncProcess = new AsyncProcess()
+ asyncProcess.start()
+ when:
+ long start = System.currentTimeMillis()
+ asyncProcess.async {
+ Thread.sleep(5000)
+ count.getAndIncrement()
+ }
+
+ long diff = System.currentTimeMillis()-start
+ int num = 0
+ while (count.get() != 1 && num < 10) {
+ Thread.sleep(1000)
+ num++
+ }
+ then:
+ diff < 1000
+ count.get() == 1
+ }
+
+
+ def "throw an exception if async queue is full"(){
+
+ asyncProcess = new AsyncProcess(queueSize:2)
+ asyncProcess.start()
+
+ asyncProcess.async { Thread.sleep(50000) }
+ asyncProcess.async { Thread.sleep(1) }
+
+ Throwable t
+ when:
+ try {
+ asyncProcess.async { Thread.sleep(50000) }
+ asyncProcess.async { Thread.sleep(50000) }
+ } catch (IllegalStateException e){
+ e.printStackTrace()
+ t = e
+ }
+
+ then:
+ asyncProcess.stop()
+ t.message == "Queue full"
+ }
+
+
+ def "support drop-if-full option"(){
+ AtomicInteger count = new AtomicInteger(0)
+
+ asyncProcess = new AsyncProcess(queueSize:1, dropIfFull:true)
+ asyncProcess.start()
+
+ int numAttempts = 100
+ when:
+ List res = (1..numAttempts).collect {
+ asyncProcess.async {
+ count.incrementAndGet()
+ }
+ }
+
+ // we need to sleep so async tasks execute
+ TimeUnit.SECONDS.sleep(5)
+ then:
+ count.get().intValue() < numAttempts
+ res.findAll ( { it.equals(false) }).size() > 0
+ }
+
+}
diff --git a/call-stack-profiler/src/test/java/callStack/profiler/CProfSpecification.groovy b/call-stack-profiler/src/test/java/callStack/profiler/CProfSpecification.groovy
new file mode 100644
index 00000000..9283f93f
--- /dev/null
+++ b/call-stack-profiler/src/test/java/callStack/profiler/CProfSpecification.groovy
@@ -0,0 +1,392 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import callStack.profiler.CProf
+import callStack.profiler.Profile
+import callStack.profiler.ProfileEvent
+import org.apache.commons.lang3.time.StopWatch
+import spock.lang.Specification
+
+class CProfSpecification extends Specification {
+
+
+ def setup() {
+ CProf.clear()
+ }
+
+
+ class C1 {
+ C2 c2 = new C2()
+
+ void callC2(boolean disableTree) {
+ CProf.prof("callC2") {
+ c2.callC3(disableTree)
+ }
+ }
+ }
+
+ class C2 {
+ C3 c3 = new C3()
+
+ void callC3(boolean disableTree) {
+ CProf.prof("callC3") {
+ Thread.sleep(100)
+ c3.some(disableTree)
+ c3.m2(disableTree)
+ }
+ }
+ }
+
+ class C3 {
+ void some(boolean disableTree) {
+ CProf.prof("some") {
+ Thread.sleep(150)
+ }
+
+ CProf.prof("anotherSome") {
+ Thread.sleep(200)
+ }
+ }
+
+ void m2(boolean disableTree) {
+ CProf.prof("m2") {
+ Thread.sleep(50)
+ CProf.turnTreeProfilingOff.set(disableTree)
+ }
+
+ CProf.prof("m3") {
+ Thread.sleep(350)
+ }
+
+ (0..5).each {
+ CProf.prof("forLoop") {
+ Thread.sleep(50)
+ }
+ }
+ }
+ }
+
+ class C8 {
+
+ @Profile
+ void m2() {
+
+ Thread.sleep(50)
+ m2("blah")
+
+ }
+ @Profile
+ void m2(String s) {
+
+ Thread.sleep(50)
+
+ }
+
+ }
+
+ def "call stack should propagate exceptions"() {
+ when:
+ new ThrowExceptionClass().profAndThrow()
+ then:
+ thrown(RuntimeException)
+ }
+
+ def "Test Simple Hierarchy Profiling"() {
+ setup:
+ C1 c1 = new C1()
+ ProfileEvent event
+ when:
+ c1.callC2(false)
+ then:
+ CProf.rootEvent.name == "callC2"
+ CProf.rootEvent.runtimeInMillis >= 1000
+ CProf.rootEvent.children.size() == 1
+ CProf.rootEvent.children.first().name == "callC3"
+ CProf.rootEvent.children.first().runtimeInMillis >= 1000
+ CProf.rootEvent.children.first().children.size() == 5
+ Map> eventsByName = CProf.rootEvent.children.first().children.groupBy { it.name }
+ eventsByName["some"].first().runtimeInMillis >= 150
+ eventsByName["anotherSome"].first().runtimeInMillis >= 200
+ eventsByName["m2"].first().runtimeInMillis >= 50
+ eventsByName["m3"].first().runtimeInMillis >= 350
+ eventsByName["forLoop"].first().runtimeInMillis >= (50 * 5)
+ eventsByName["forLoop"].first().numOfInvocations == 6
+ }
+
+ def "Test Simple Hierarchy Profiling - tree profiling disabled mid profiling"() {
+ setup:
+ C1 c1 = new C1()
+ ProfileEvent event
+ when:
+ c1.callC2(true)
+ CProf.turnTreeProfilingOff.set(false)
+ then:
+ CProf.rootEvent.name == "callC2"
+// println CProf.prettyPrint()
+ }
+
+ def "Test Simple Hierarchy Profiling - tree profiling disabled"() {
+ setup:
+ C4 c4 = new C4()
+ ProfileEvent event
+ when:
+ CProf.turnTreeProfilingOff.set(true)
+ c4.m()
+ CProf.turnTreeProfilingOff.set(false)
+ then:
+ CProf.rootEvent.name == "root"
+ CProf.rootEvent.children.size() == 0
+// println CProf.prettyPrint()
+ }
+
+
+ class C4 {
+ void m() {
+ CProf.prof("root") {
+ (0..5).each {
+ CProf.prof("call", false) {
+ Thread.sleep(50)
+ }
+ }
+ }
+ }
+ }
+
+ def "Allow for events to not be aggregated on the same hierarchy level with the same name"() {
+ setup:
+ C4 c4 = new C4()
+ ProfileEvent event
+ when:
+ c4.m()
+// println CProf.prettyPrint()
+ then:
+ CProf.rootEvent.name == "root"
+ CProf.rootEvent.runtimeInMillis >= 300
+ CProf.rootEvent.children.size() == 6
+ }
+
+
+ class C5 {
+ void m() {
+ CProf.prof("root") {
+ (0..5).each {
+ CProf.start("call")
+ Thread.sleep(50)
+ CProf.stop("call", false)
+ }
+ }
+ }
+ }
+
+ def "Allow for events to not be aggregated on the same hierarchy level with the same name - use stopProf Method"() {
+ setup:
+ C5 c5 = new C5()
+ ProfileEvent event
+ when:
+ c5.m()
+// println CProf.prettyPrint()
+ then:
+ CProf.rootEvent.name == "root"
+ CProf.rootEvent.runtimeInMillis >= 300
+ CProf.rootEvent.children.size() == 6
+ CProf.rootEvent.children.first().name.startsWith("call")
+ }
+
+ def "Print out should look good for large hierarchy"() {
+ setup:
+ when:
+ CProf.prof("l1") {
+ CProf.prof("l2") {
+ CProf.prof("l3") {
+ CProf.prof("l4") {
+ CProf.prof("l5") {
+ CProf.prof("l6") {
+ CProf.prof("l7") {
+ CProf.prof("l8") {
+ (0..10).each {
+ CProf.prof("l9-${it}") {
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ CProf.prof("l2-1") {
+ (0..10).each {
+ CProf.prof("l3-${it}") {
+ CProf.prof("l4") {
+
+ }
+ }
+ }
+ }
+ CProf.prof("l2-3") {
+ CProf.prof("l3") {
+ CProf.prof("l4") {
+
+ }
+ }
+ }
+ }
+ then:
+ CProf.prettyPrint()
+// println CProf.prettyPrint()
+ }
+
+
+ def "Profiling must be fast in a for-loop"() {
+ when:
+ CProf.prof("load") {}
+ StopWatch watch = new StopWatch()
+ watch.start()
+ CProf.prof("top") {
+ (0..1000).each {
+ CProf.prof("forLoop") {
+ }
+ }
+ }
+ watch.stop()
+
+// println watch.time
+// println CProf.prettyPrint()
+ then:
+ watch.time < 1000
+ }
+
+ def "Pretty print large numbers"() {
+
+ when:
+ CProf.prof("pretty") {}
+ CProf.rootEvent.numOfInvocations = 10000
+ String prettyPrint = CProf.prettyPrint()
+ then:
+ prettyPrint.startsWith("|-> pretty (10,000) :")
+ }
+
+
+ def "Pretty print should not be too slow"() {
+ setup:
+ when:
+ CProf.prof("l1") {
+ CProf.prof("l2") {
+ CProf.prof("l3") {
+ CProf.prof("l4") {
+ CProf.prof("l5") {
+ CProf.prof("l6") {
+ CProf.prof("l7") {
+ CProf.prof("l8") {
+ (0..10).each {
+ CProf.prof("l9-${it}") {
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ CProf.prof("l2-1") {
+ (0..10).each {
+ CProf.prof("l3-${it}") {
+ CProf.prof("l4") {
+
+ }
+ }
+ }
+ }
+ CProf.prof("l2-3") {
+ CProf.prof("l3") {
+ CProf.prof("l4") {
+
+ }
+ }
+ }
+ }
+ StopWatch stopWatch = new StopWatch()
+ stopWatch.start()
+ CProf.prettyPrint()
+ stopWatch.stop()
+
+// println stopWatch.time
+ then:
+ CProf.prettyPrint()
+// println CProf.prettyPrint()
+ }
+
+ def "Call stack profiler should properly propagate exceptions"() {
+
+ when:
+ CProf.prof("with exception"){
+ throw new IllegalArgumentException("fail")
+ }
+ then: thrown (IllegalArgumentException)
+
+ }
+
+ def "Should be able to handle overloaded methods when profiling is off"() {
+
+ setup:
+ CProf?.turnTreeProfilingOff.set(true)
+ when:
+ new C8().m2()
+ then:
+ CProf?.turnTreeProfilingOff.set(false)
+
+ }
+
+ static class ThrowExceptionClass {
+ void profAndThrow() {
+ CProf.prof("m1") {
+ Thread.sleep(50)
+ throwException("blah")
+ }
+
+ }
+
+ void throwException(String s) {
+ CProf.start("m2")
+ Thread.sleep(50)
+ throw new RuntimeException("Exception")
+ }
+
+ }
+
+ def "ability to preserve root ProfileEvent object for later utilization"(){
+ List events = []
+
+ when:
+ 2.times {
+ CProf.prof("prof"){
+ Thread.sleep(50)
+ }
+ events.add(CProf.rootEvent)
+ }
+ then:
+ events.size() == 2
+ events.first().hashCode() != events.last().hashCode()
+ }
+
+
+}
diff --git a/call-stack-profiler/src/test/java/callStack/profiler/ProfileAnnotationSpecification.groovy b/call-stack-profiler/src/test/java/callStack/profiler/ProfileAnnotationSpecification.groovy
new file mode 100644
index 00000000..07411369
--- /dev/null
+++ b/call-stack-profiler/src/test/java/callStack/profiler/ProfileAnnotationSpecification.groovy
@@ -0,0 +1,277 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import callStack.profiler.CProf
+import callStack.profiler.Profile
+import spock.lang.Specification
+
+
+class ProfileAnnotationSpecification extends Specification {
+ class C1 {
+ @Profile
+ void m1() {
+ Thread.sleep(50)
+ new C2().m2()
+ }
+
+ @Profile
+ void willThrow(){
+ new C2().willThrow()
+ }
+
+ @Profile
+ void willThrowAsWell(){
+ new C2().willThrowAsWell()
+ }
+ }
+ class C2 {
+ @Profile
+ void m2() {
+ Thread.sleep(50)
+ }
+
+ @Profile
+ void willThrow(){
+ new C3().m4()
+ }
+
+ @Profile
+ void willThrowAsWell(){
+ new C3().willThrowAsWell()
+ }
+ }
+
+ def "Profile annotation must add profiling to the annotation method"() {
+ setup:
+
+ when:
+ new C1().m1()
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C1.m1"
+ CProf.rootEvent.runtimeInMillis > 0
+
+ CProf.rootEvent.children
+ CProf.rootEvent.children.size() == 1
+ CProf.rootEvent.children.first().name == "ProfileAnnotationSpecification\$C2.m2"
+ CProf.rootEvent.children.first().runtimeInMillis > 0
+ CProf.rootEvent.ended
+ }
+
+
+ class C3 {
+ @Profile
+ String m3() {
+ Thread.sleep(200)
+ return "string"
+ }
+
+ @Profile
+ String m4() {
+ Thread.sleep(200)
+ throw new IllegalArgumentException("aljaljfljaljf")
+ return "string"
+ }
+
+ @Profile
+ String willThrowAsWell() {
+ Thread.sleep(200)
+ try {
+ (0..10).each {
+ List list = [new WithAttr(attr: "blja"), ["aljl", "lajlakj"]]
+ // this should throw an exception
+ def groupBy = list.groupBy { it.attr }
+ }
+ return "groupBy"
+ } catch (Throwable throwable) {
+// throwable.printStackTrace()
+ throw throwable
+ }
+ }
+ }
+
+ class WithAttr{
+ String attr
+ }
+
+ def "Profile method's return must be properly propagated"() {
+ String res
+ when:
+ res = new C3().m3()
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C3.m3"
+ CProf.rootEvent.runtimeInMillis >= 0
+ CProf.rootEvent.ended
+ res == "string"
+ }
+
+ def "Thrown exception does NOT stop profiling from completing"() {
+ String res
+ Exception thrownE
+ when:
+ try {
+ res = new C3().m4()
+ fail "should never get here"
+ } catch (Exception e) { thrownE = e}
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C3.m4"
+ CProf.rootEvent.runtimeInMillis >= 200
+ CProf.rootEvent.ended
+ !res
+ thrownE instanceof IllegalArgumentException
+ thrownE.message == "aljaljfljaljf"
+ }
+
+ def "Nested exception does NOT stop profiling from completing"() {
+ String res
+ Exception thrownE
+ when:
+ try {
+ res = new C1().willThrow()
+ fail "should never get here"
+ } catch (Exception e) { thrownE = e}
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C1.willThrow"
+ CProf.rootEvent.runtimeInMillis >= 200
+ CProf.rootEvent.ended
+ !res
+ thrownE instanceof IllegalArgumentException
+ thrownE.message == "aljaljfljaljf"
+ }
+
+ def "Nested odd exception does NOT stop profiling from completing"() {
+ String res
+ Exception thrownE
+ when:
+ try {
+ res = new C1().willThrowAsWell()
+ fail "should never get here"
+ } catch (Exception e) { thrownE = e}
+
+ thrownE.printStackTrace()
+
+ println CProf.prettyPrint()
+ then:
+// CProf.rootEvent
+// CProf.rootEvent.name == "ProfileAnnotationSpecification\$C1.willThrow"
+// CProf.rootEvent.runtimeInMillis >= 200
+// CProf.rootEvent.ended
+ !res
+// thrownE instanceof IllegalArgumentException
+// thrownE.message == "aljaljfljaljf"
+ }
+
+
+ def "Can the entry method multiple times and time should be properly re-set on each call"() {
+ when:
+ (0..10).each {
+ new C3().m3()
+ }
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C3.m3"
+ CProf.rootEvent.runtimeInMillis >= 200
+ CProf.rootEvent.runtimeInMillis < 1000
+ CProf.rootEvent.ended
+ }
+
+ class C4 {
+ @Profile(name = "CustomName")
+ void m1() {
+ Thread.sleep(50)
+ }
+ }
+
+ def "Annotation allows to change the name of the profile event"() {
+ when:
+ new C4().m1()
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "CustomName"
+ CProf.rootEvent.ended
+ CProf.rootEvent.runtimeInMillis >= 50
+ }
+
+
+ class C5 {
+ @Profile
+ void empty() {
+ }
+ }
+
+ def "Should be able to profile empty method, for some odd reason"() {
+ when:
+ new C5().empty()
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C5.empty"
+ CProf.rootEvent.ended
+ }
+
+ class C6 {
+ boolean b = true
+
+ @Profile(name = "multi-return")
+ void multiReturn() {
+ if (b) {
+ Thread.sleep(50)
+ return
+ }
+ Thread.sleep(10)
+ return
+ }
+ }
+
+ def "Profile methods that have multiple returns"() {
+ when:
+ new C6().multiReturn()
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "multi-return"
+ CProf.rootEvent.ended
+ CProf.rootEvent.runtimeInMillis >= 50
+ }
+
+ class C7 {
+ boolean b = true
+ @Profile
+ void callMethod() {
+ dontAggregate()
+ dontAggregate()
+ dontAggregate()
+ }
+
+ @Profile(aggregateIntoSingleEvent = false)
+ void dontAggregate() {
+ Thread.sleep(50)
+ }
+ }
+
+ def "Allow each call to be a separate profile event"() {
+ when:
+ new C7().callMethod()
+// println CProf.rootEvent.prettyPrint()
+ then:
+ CProf.rootEvent
+ CProf.rootEvent.name == "ProfileAnnotationSpecification\$C7.callMethod"
+ CProf.rootEvent.children.size() == 3
+ CProf.rootEvent.runtimeInMillis >= 150
+ }
+}
diff --git a/call-stack-profiler/src/test/java/callStack/profiler/ProfileEventSpecification.groovy b/call-stack-profiler/src/test/java/callStack/profiler/ProfileEventSpecification.groovy
new file mode 100644
index 00000000..94df36e5
--- /dev/null
+++ b/call-stack-profiler/src/test/java/callStack/profiler/ProfileEventSpecification.groovy
@@ -0,0 +1,187 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.profiler
+
+import callStack.profiler.ProfileEvent
+import spock.lang.Specification
+
+class ProfileEventSpecification extends Specification{
+// def "Must be able to Kryo serDer"(){
+//
+// ProfileEvent child1 = new ProfileEvent(name: "test", runtimeInMillis: 10)
+// ProfileEvent child2 = new ProfileEvent(name: "test", runtimeInMillis: 10)
+// ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+// event.addChild(child1)
+// event.addChild(child2)
+//
+// ProfileEvent res
+// when:
+// byte [] ser = KryoSerializer.instance.serialize(event)
+// res = KryoSerializer.instance.deserialize(ser)
+//
+// then:
+// res
+// res.name == event.name
+// }
+
+ def "toString call with hierarchy should not throw exceptions :) "(){
+ ProfileEvent child1 = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ ProfileEvent child2 = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ event.addChild(child1)
+ event.addChild(child2)
+
+
+ when:
+ event.toString()
+ then:
+ event
+ }
+
+ def "Pretty print long running events - 1,000 ms"() {
+ expect:
+ profileEvent.prettyPrint() == result
+
+ where:
+ profileEvent | result
+ new ProfileEvent(name: "p", runtimeInMillis: 1000, numOfInvocations: 1) | "|-> p (1) : 1s "
+ new ProfileEvent(name: "p", runtimeInMillis: 10*1000, numOfInvocations: 1) | "|-> p (1) : 10s "
+ new ProfileEvent(name: "p", runtimeInMillis: 29999, numOfInvocations: 1) | "|-> p (1) : 29s 999ms"
+ new ProfileEvent(name: "p", runtimeInMillis: 30*1000, numOfInvocations: 1) | "|-> p (1) : 30s "
+ new ProfileEvent(name: "p", runtimeInMillis: 60*1000 + 1000, numOfInvocations: 1) | "|-> p (1) : 1m 1s "
+ new ProfileEvent(name: "p", runtimeInMillis: 59*60*1000 + 59*1000, numOfInvocations: 1) | "|-> p (1) : 59m 59s "
+ new ProfileEvent(name: "p", runtimeInMillis: 62*60*1000 + 59*1000, numOfInvocations: 1) | "|-> p (1) : 1h 2m 59s "
+ new ProfileEvent(name: "p", runtimeInMillis: 143*60*1000 + 199, numOfInvocations: 1) | "|-> p (1) : 2h 23m 199ms"
+ }
+
+ def "serialize should not throw exceptions :) "(){
+ ProfileEvent child1 = new ProfileEvent(name: "child1", runtimeInMillis: 10)
+ ProfileEvent child2 = new ProfileEvent(name: "child2", runtimeInMillis: 10)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ event.addChild(child1)
+ event.addChild(child2)
+
+ ProfileEvent serialized
+
+ when:
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()
+ ObjectOutputStream oos = new ObjectOutputStream(baos)
+ oos.writeObject(event)
+ oos.close()
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())
+ ObjectInputStream ois = new ObjectInputStream(bais)
+ serialized = ois.readObject()
+ ois.close()
+
+ then:
+ serialized
+ }
+
+ def "demonstrate how much time is unaccounted for within its children"() {
+ ProfileEvent child1 = new ProfileEvent(name: "child1", runtimeInMillis: 4)
+ ProfileEvent child2 = new ProfileEvent(name: "child2", runtimeInMillis: 5)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ event.addChild(child1)
+ event.addChild(child2)
+
+ when:
+ String pretty = event.prettyPrint()
+ then:
+ pretty.toString().trim() == '''
+|-> test (0) : 010ms [001ms]
+| |-> child2 (0) : 005ms
+| |-> child1 (0) : 004ms'''.toString().trim()
+ }
+
+ def "demonstrate how much time is unaccounted for within its concurrent children"() {
+ ProfileEvent child1 = new ProfileEvent(name: "child1", runtimeInMillis: 4, isConcurrent: true)
+ ProfileEvent child2 = new ProfileEvent(name: "child2", runtimeInMillis: 5, isConcurrent: true)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ event.addChild(child1)
+ event.addChild(child2)
+
+ when:
+ String pretty = event.prettyPrint()
+ then:
+ pretty
+ pretty.toString().trim() == '''
+|-> test (0) : 010ms [005ms]
+| ||-> child2 (0) : 005ms
+| ||-> child1 (0) : 004ms'''.toString().trim()
+ }
+
+ def "demonstrate how much time is unaccounted for within its concurrent children and synchronous children"() {
+ ProfileEvent child1 = new ProfileEvent(name: "child1", runtimeInMillis: 4, isConcurrent: false)
+ ProfileEvent child2 = new ProfileEvent(name: "child2", runtimeInMillis: 5, isConcurrent: true)
+ ProfileEvent child3 = new ProfileEvent(name: "child3", runtimeInMillis: 5, isConcurrent: true)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ event.addChild(child1)
+ event.addChild(child2)
+ event.addChild(child3)
+
+ when:
+ String pretty = event.prettyPrint()
+ then:
+ pretty
+ pretty.toString().trim() == '''
+|-> test (0) : 010ms [001ms]
+| ||-> child2 (0) : 005ms
+| ||-> child3 (0) : 005ms
+| |-> child1 (0) : 004ms'''.toString().trim()
+ }
+
+ def "demonstrate how much time is unaccounted for within its concurrent children and several synchronous children"() {
+ ProfileEvent child1 = new ProfileEvent(name: "child1", runtimeInMillis: 4, isConcurrent: true)
+ ProfileEvent child2 = new ProfileEvent(name: "child2", runtimeInMillis: 5, isConcurrent: false)
+ ProfileEvent child3 = new ProfileEvent(name: "child3", runtimeInMillis: 6, isConcurrent: true)
+ ProfileEvent child4 = new ProfileEvent(name: "child4", runtimeInMillis: 7, isConcurrent: false)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 20)
+ event.addChild(child1)
+ event.addChild(child2)
+ event.addChild(child3)
+ event.addChild(child4)
+
+ when:
+ String pretty = event.prettyPrint()
+ then:
+ pretty
+ pretty.toString().trim() == '''
+|-> test (0) : 020ms [002ms]
+| |-> child4 (0) : 007ms
+| |-> child2 (0) : 005ms
+| ||-> child3 (0) : 006ms
+| ||-> child1 (0) : 004ms'''.toString().trim()
+ }
+
+
+ def "demonstrate how much time is unaccounted for within its remote children"() {
+ ProfileEvent child1 = new ProfileEvent(name: "child1", runtimeInMillis: 4, isRemote: true)
+ ProfileEvent child2 = new ProfileEvent(name: "child2", runtimeInMillis: 5, isRemote: true)
+ ProfileEvent event = new ProfileEvent(name: "test", runtimeInMillis: 10)
+ event.addChild(child1)
+ event.addChild(child2)
+
+ when:
+ String pretty = event.prettyPrint()
+ then:
+ pretty
+ pretty.toString().trim() == '''
+|-> test (0) : 010ms [005ms]
+| |||-> child2 (0) : 005ms
+| |||-> child1 (0) : 004ms'''.toString().trim()
+ }
+}
diff --git a/call-stack-profiler/src/test/java/callStack/utils/ProfThreadPoolSpecification.groovy b/call-stack-profiler/src/test/java/callStack/utils/ProfThreadPoolSpecification.groovy
new file mode 100644
index 00000000..43ce60a3
--- /dev/null
+++ b/call-stack-profiler/src/test/java/callStack/utils/ProfThreadPoolSpecification.groovy
@@ -0,0 +1,264 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package callStack.utils
+
+import callStack.profiler.CProf
+import callStack.profiler.ProfThreadPool
+import callStack.utils.ThreadPoolUtils
+import spock.lang.Specification
+
+import java.util.concurrent.Future
+
+class ProfThreadPoolSpecification extends Specification {
+
+
+ String poolName = "pool"
+ ProfThreadPool profThreadPool = new ProfThreadPool(poolName, 2, 5)
+ def setup() {
+ CProf.clear()
+ }
+
+ def "Thread pool usage where underlying exec does NOT name events"() {
+ List res
+ when:
+ CProf.prof("l1") {
+ res = profThreadPool.asyncExec([
+ ThreadPoolUtils.callable { return "1" },
+ ThreadPoolUtils.callable { return "2" }
+ ])
+ }
+
+ then:
+ res
+ res.size() == 2
+ res.contains("1")
+ res.contains("2")
+ CProf.rootEvent.name == "l1"
+ CProf.rootEvent.children.size() == 2
+ CProf.rootEvent.children.collect({it.name}).sort() == ["$poolName-1", "$poolName-2"]
+ }
+
+
+
+ def "Thread pool allows to assign unique name to each event"() {
+ ProfThreadPool profThreadPool = new ProfThreadPool(poolName, 2, 5)
+ profThreadPool.assignUniqueNameToEachRootEvent = true
+
+ List res
+ when:
+ CProf.prof("l1") {
+ res = profThreadPool.asyncExec([
+ ThreadPoolUtils.callable { return "1" },
+ ThreadPoolUtils.callable { return "2" }
+ ])
+ }
+
+ println CProf.prettyPrint()
+
+ then:
+ res
+ res.size() == 2
+ res.contains("1")
+ res.contains("2")
+ CProf.rootEvent.name == "l1"
+ CProf.rootEvent.children.size() == 2
+ List names = CProf.rootEvent.children.collect({it.name}).sort()
+ names.first().startsWith("$poolName-1")
+ names.last().startsWith("$poolName-2")
+ }
+
+
+ def "Threadpool shouldn't fail if there is not profile event on the parent thread"() {
+ List res
+ when:
+ res = profThreadPool.asyncExec([
+ ThreadPoolUtils.callable { return "1" },
+ ThreadPoolUtils.callable { return "2" }
+ ])
+ then:
+ res
+ res.size() == 2
+ res.contains("1")
+ res.contains("2")
+ !CProf.rootEvent
+ }
+
+
+ def "Thread pool usage where underlying exec name events"() {
+ List res
+ when:
+ CProf.prof("l1") {
+ res = profThreadPool.asyncExec([
+ ThreadPoolUtils.callable { CProf.prof("1") {}; return "1" },
+ ThreadPoolUtils.callable { CProf.prof("2") {}; return "2" }
+ ])
+ }
+ then:
+ res
+ res.size() == 2
+ res.contains("1")
+ res.contains("2")
+// println CProf.rootEvent.prettyPrint()
+ CProf.rootEvent.name == "l1"
+ CProf.rootEvent.children.size() == 2
+ List names = CProf.rootEvent.children.collect({ it.name }).sort()
+ names.find({ it.startsWith("1-pool") })
+ names.find({ it.startsWith("2-pool") })
+ }
+
+ def "Thread pool usage in multi-level call stack"() {
+ List res
+ when:
+ CProf.prof("l1") {
+ CProf.prof("l2") {
+ res = profThreadPool.asyncExec([
+ ThreadPoolUtils.callable { CProf.prof("l3") { CProf.prof("l4") { CProf.prof("l5") {} } }; return "1" },
+ ThreadPoolUtils.callable { CProf.prof("l3") { CProf.prof("l4") {} }; return "2" }
+ ])
+ }
+ }
+ then:
+// println CProf.rootEvent.prettyPrint()
+ res
+ res.size() == 2
+ res.contains("1")
+ res.contains("2")
+
+ CProf.rootEvent.name == "l1"
+ CProf.rootEvent.children.size() == 1
+ CProf.rootEvent.children.first().name == "l2"
+ CProf.rootEvent.children.first().children.size() == 2
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" })
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.size() == 1
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().name == "l4"
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().children.size() == 1
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().children.first().name == "l5"
+ !CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().children.first().children
+
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" })
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" }).children.size() == 1
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" }).children.first().name == "l4"
+ !CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" }).children.first().children
+ }
+
+ def "can use futures with the pool"() {
+ List res = []
+ when:
+ CProf.prof("l1") {
+ Future futureRes = profThreadPool.submit(ThreadPoolUtils.callable { return "1" })
+ Future futureRes1 = profThreadPool.submit(ThreadPoolUtils.callable { return "2" })
+
+ res.add(futureRes.get())
+ res.add(futureRes1.get())
+ }
+ println CProf.rootEvent.prettyPrint()
+ then:
+ res
+ res.size() == 2
+ res.sort() == ["1", "2"]
+ CProf.rootEvent.name == "l1"
+ CProf.rootEvent.children.size() == 2
+ }
+
+ def "Thread pool usage in multi-level call stack via futures"() {
+ List res = []
+ when:
+ CProf.prof("l1") {
+ CProf.prof("l2") {
+ Future f1 = profThreadPool.submit(ThreadPoolUtils.callable { CProf.prof("l3") { CProf.prof("l4") { CProf.prof("l5") {} } }; return "1" })
+ Future f2 = profThreadPool.submit( ThreadPoolUtils.callable { CProf.prof("l3") { CProf.prof("l4") {} }; return "2" })
+ res.add(f1.get())
+ res.add(f2.get())
+ }
+ }
+ then:
+ println CProf.rootEvent.prettyPrint()
+ res
+ res.size() == 2
+ res.contains("1")
+ res.contains("2")
+
+ CProf.rootEvent.name == "l1"
+ CProf.rootEvent.children.size() == 1
+ CProf.rootEvent.children.first().name == "l2"
+ CProf.rootEvent.children.first().children.size() == 2
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" })
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.size() == 1
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().name == "l4"
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().children.size() == 1
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().children.first().name == "l5"
+ !CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-1" }).children.first().children.first().children
+
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" })
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" }).children.size() == 1
+ CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" }).children.first().name == "l4"
+ !CProf.rootEvent.children.first().children.find({ it.name == "l3-pool-2" }).children.first().children
+ }
+
+
+ def "Warn when usage reaches high percentage"() {
+ ProfThreadPool threadPool = new ProfThreadPool(poolName, 5, 5)
+ List futures = []
+ when:
+ (0..4).each {
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ }
+
+
+ (0..2).each {
+ threadPool.asyncExec([ThreadPoolUtils.callable { Thread.sleep(500) }])
+ }
+
+ futures.each {
+ it.get()
+ }
+ then:
+ true
+
+ // nothing go validate, look for warn messages
+ }
+
+
+ def "Do not warn after the pool went below threshold"() {
+ ProfThreadPool threadPool = new ProfThreadPool(poolName, 5)
+ List futures = []
+ when:
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+
+ futures.each {
+ it.get()
+ }
+ futures.clear()
+
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.add(threadPool.submit(ThreadPoolUtils.callable { (0..2).each{Thread.sleep(500)} }))
+ futures.each {
+ it.get()
+ }
+
+ then:
+ true
+
+ // nothing go validate, look for warn messages
+ }
+
+}
+
diff --git a/call-stack-profiler/src/test/resources/logback.xml b/call-stack-profiler/src/test/resources/logback.xml
new file mode 100644
index 00000000..9df180f4
--- /dev/null
+++ b/call-stack-profiler/src/test/resources/logback.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
diff --git a/ci/installNode.sh b/ci/installNode.sh
deleted file mode 100755
index 444e913a..00000000
--- a/ci/installNode.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright 2020 SkillTree
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#!/usr/bin/env bash
-
-cat /etc/os-release
-apt-get update
-apt-get install -y build-essential
-curl -sL https://deb.nodesource.com/setup_12.x | bash -
-apt-get install -y nodejs
-nodejs -v
-npm -v
-
diff --git a/ci/setupRepos.sh b/ci/setupRepos.sh
deleted file mode 100755
index 9705916a..00000000
--- a/ci/setupRepos.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright 2020 SkillTree
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#!/usr/bin/env bash
-
-echo "@skills:registry=http://$NEXUS_SERVER/repository/skills-registry/" > ~/.npmrc
-cat ~/.npmrc
-echo "centralcentralhttp://$NEXUS_SERVER/repository/maven-public/*" > ~/.m2/settings.xml
-cat ~/.m2/settings.xml
-
diff --git a/client-display/.eslintrc.js b/client-display/.eslintrc.js
index 71318378..33872dfa 100644
--- a/client-display/.eslintrc.js
+++ b/client-display/.eslintrc.js
@@ -1,30 +1,91 @@
+// https://eslint.org/docs/user-guide/configuring
+
module.exports = {
root: true,
+
+ parserOptions: {
+ parser: 'babel-eslint',
+ },
+
env: {
+ browser: true,
node: true,
},
- extends: [
- 'plugin:vue/essential',
- '@vue/airbnb',
+
+ // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
+ // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
+ extends: ['plugin:vue/essential', '@vue/airbnb'],
+
+ // required to lint *.vue files
+ plugins: [
+ 'vue',
],
+
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: require.resolve('@vue/cli-service/webpack.config.js'),
+ },
+ },
+ },
+
+ // check if imports actually resolve
+ // add your custom rules here
rules: {
- 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ 'import/extensions': [
+ 'error',
+ 'always',
+ {
+ js: 'never',
+ vue: 'never',
+ },
+ ],
+ 'no-param-reassign': [
+ 'error',
+ {
+ props: true,
+ ignorePropertyModificationsFor: [
+ 'state',
+ 'acc',
+ 'e',
+ ],
+ },
+ ],
+ 'import/no-extraneous-dependencies': [
+ 'error',
+ {
+ optionalDependencies: [
+ 'test/unit/index.js',
+ ],
+ },
+ ],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
- 'indent': ['error', 2],
- 'max-len': ['error', { 'code': 300 }],
- 'no-underscore-dangle': 0,
- 'no-new': 0,
- 'vue/script-indent': 0, // I have to disable this for now. It makes your newline then blocks look real dumb
- },
- 'overrides': [
- {
- 'files': ['*.vue'],
- 'rules': {
- 'indent': 'off'
- }
- }
- ],
- parserOptions: {
- parser: 'babel-eslint',
+ indent: 'off',
+ 'vue/script-indent': [
+ 'error',
+ 2,
+ {
+ baseIndent: 1,
+ switchCase: 0,
+ ignores: [],
+ },
+ ],
+ 'vue/max-attributes-per-line': [
+ 2,
+ {
+ singleline: 5,
+ multiline: {
+ max: 5,
+ allowFirstLine: true,
+ },
+ },
+ ],
+ 'max-len': [
+ 'error',
+ {
+ code: 300,
+ },
+ ],
+ 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
};
diff --git a/client-display/package.json b/client-display/package.json
index 829fb72c..2a47d041 100644
--- a/client-display/package.json
+++ b/client-display/package.json
@@ -1,7 +1,7 @@
{
- "name": "@skills/user-skills",
+ "name": "@skilltree/skills-client-display",
"main": "./dist/userSkills.common.js",
- "version": "0.0.6",
+ "version": "1.0.0",
"license": "Apache-2.0",
"description": "SkillTree Client Display UI",
"author": "SkillTree Team",
@@ -11,56 +11,58 @@
"licenseHeaderCheck": "license-check-and-add check -f '../license-add/license-add-config.json'",
"licenseHeaderAdd": "license-check-and-add add -f ../license-add/license-add-config.json",
"licenseCheck": "npm run licenseHeaderCheck && npm run licenseDepCheck",
- "build": "npm run licenseCheck && vue-cli-service build",
+ "build": "npm run licenseCheck && npm run test:unit && vue-cli-service build",
"build-lib": "NODE_ENV='production' && rm -rfv dist && vue-cli-service build --target lib --name userSkills './src/index.js'",
"deploy": "npm run build && rm -rf ../backend/src/main/resources/public/static/clientPortal && cp -rT dist ../backend/src/main/resources/public/static/clientPortal && cp -rT dist ../backend/target/classes/public/static/clientPortal ",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
- "@fortawesome/fontawesome-free": "5.11.2",
- "animate.css": "3.7.2",
- "apexcharts": "3.8.6",
- "axios": "0.19.0",
- "axios-auth-refresh": "1.0.7",
- "bootstrap": "4.3.1",
- "lodash": "4.17.15",
- "marked": "0.7.0",
+ "@fortawesome/fontawesome-free": "5.14.0",
+ "animate.css": "4.1.1",
+ "apexcharts": "3.20.2",
+ "axios": "0.20.0",
+ "axios-auth-refresh": "3.0.0",
+ "bootstrap": "4.5.2",
+ "dompurify": "2.0.15",
+ "lodash": "4.17.20",
+ "marked": "1.1.1",
"material-icons": "0.3.1",
+ "node-emoji": "1.10.0",
"numeral": "2.0.6",
- "postmate": "1.5.1",
+ "postmate": "1.5.2",
"q": "1.5.1",
"tinycolor2": "1.4.1",
- "url-search-params-polyfill": "7.0.0",
+ "url-search-params-polyfill": "8.1.0",
"vis": "4.21.0",
- "vue": "2.6.10",
- "vue-apexcharts": "1.5.0",
- "vue-js-toggle-button": "1.3.2",
- "vue-moment": "4.0.0",
- "vue-radial-progress": "0.2.10",
- "vue-router": "3.0.6",
- "vue-simple-progress": "1.1.0",
- "vue-simple-spinner": "1.2.8",
- "vuex": "3.1.1",
- "dompurify": "2.0.3"
+ "vue": "2.6.12",
+ "vue-apexcharts": "1.6.0",
+ "vue-js-toggle-button": "1.3.3",
+ "vue-moment": "4.1.0",
+ "vue-radial-progress": "0.3.2",
+ "vue-router": "3.4.3",
+ "vue-simple-progress": "1.1.1",
+ "vue-simple-spinner": "1.2.10",
+ "vuex": "3.5.1"
},
"devDependencies": {
- "@vue/cli-plugin-babel": "4.1.0",
- "@vue/cli-plugin-eslint": "4.1.0",
- "@vue/cli-plugin-unit-jest": "4.1.0",
- "@vue/cli-service": "4.1.0",
- "@vue/eslint-config-airbnb": "4.0.1",
- "@vue/test-utils": "1.0.0-beta.29",
- "babel-core": "7.0.0-bridge.0",
- "babel-eslint": "10.0.3",
- "babel-jest": "24.9.0",
- "eslint": "5.16.0",
- "eslint-plugin-vue": "5.0.0",
+ "@babel/core": "7.11.6",
+ "@vue/cli-plugin-babel": "4.5.6",
+ "@vue/cli-plugin-eslint": "4.5.6",
+ "@vue/cli-plugin-unit-jest": "4.5.6",
+ "@vue/cli-service": "4.5.6",
+ "@vue/eslint-config-airbnb": "5.1.0",
+ "@vue/test-utils": "1.1.0",
+ "babel-eslint": "10.1.0",
+ "babel-jest": "26.3.0",
+ "eslint": "7.8.1",
+ "eslint-plugin-vue": "6.2.2",
"license-check-and-add": "3.0.4",
"license-checker": "25.0.1",
- "node-sass": "4.12.0",
- "sass-loader": "8.0.0",
- "vue-template-compiler": "2.6.10"
+ "moment-timezone": "0.5.31",
+ "node-sass": "4.14.1",
+ "sass-loader": "10.0.2",
+ "vue-template-compiler": "2.6.12"
},
"peerDependencies": {
"font-awesome": "4.7.0"
diff --git a/client-display/pom.xml b/client-display/pom.xml
index ae5d5fba..46806899 100644
--- a/client-display/pom.xml
+++ b/client-display/pom.xml
@@ -3,9 +3,9 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- skills-service
- skills
- 1.1.4-SNAPSHOT
+ skills-service-parent
+ skill-tree
+ 1.3.0-SNAPSHOT4.0.0
diff --git a/client-display/src/App.vue b/client-display/src/App.vue
index 4c396995..088c9cd8 100644
--- a/client-display/src/App.vue
+++ b/client-display/src/App.vue
@@ -31,8 +31,8 @@ limitations under the License.
import UserSkillsService from '@/userSkills/service/UserSkillsService';
import store from '@/store';
- import NewSoftwareVersionComponent from '@/common/softwareVersion/NewSoftwareVersion.vue';
- import DevModeMixin from '@/dev/DevModeMixin.vue';
+ import NewSoftwareVersionComponent from '@/common/softwareVersion/NewSoftwareVersion';
+ import DevModeMixin from '@/dev/DevModeMixin';
import ThemeHelper from './common/theme/ThemeHelper';
const getDocumentHeight = () => {
diff --git a/client-display/src/SkillsEntry.vue b/client-display/src/SkillsEntry.vue
index bb43b195..2fca49b8 100644
--- a/client-display/src/SkillsEntry.vue
+++ b/client-display/src/SkillsEntry.vue
@@ -23,7 +23,7 @@ limitations under the License.
diff --git a/client-display/src/common/utilities/NoDataYet.vue b/client-display/src/common/utilities/NoDataYet.vue
index d4703b86..58749b35 100644
--- a/client-display/src/common/utilities/NoDataYet.vue
+++ b/client-display/src/common/utilities/NoDataYet.vue
@@ -34,13 +34,13 @@ limitations under the License.
-
-
diff --git a/client-display/src/userSkills/SkillDisplayDataLoadingMixin.vue b/client-display/src/userSkills/SkillDisplayDataLoadingMixin.vue
index 5ca2911e..b4f14272 100644
--- a/client-display/src/userSkills/SkillDisplayDataLoadingMixin.vue
+++ b/client-display/src/userSkills/SkillDisplayDataLoadingMixin.vue
@@ -14,61 +14,52 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
diff --git a/client-display/src/userSkills/badge/ProjectLevelRow.vue b/client-display/src/userSkills/badge/ProjectLevelRow.vue
index 83dcc812..50cec845 100644
--- a/client-display/src/userSkills/badge/ProjectLevelRow.vue
+++ b/client-display/src/userSkills/badge/ProjectLevelRow.vue
@@ -49,7 +49,7 @@ limitations under the License.
diff --git a/client-display/src/userSkills/pointProgress/PointProgressChart.vue b/client-display/src/userSkills/pointProgress/PointProgressChart.vue
new file mode 100644
index 00000000..df281046
--- /dev/null
+++ b/client-display/src/userSkills/pointProgress/PointProgressChart.vue
@@ -0,0 +1,263 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
+
+
Locked
+ *** 2 days of usage will unlock this chart! ***
+
+
+
+
+
+
Point History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-display/src/userSkills/pointProgress/PointProgressHelper.js b/client-display/src/userSkills/pointProgress/PointProgressHelper.js
new file mode 100644
index 00000000..d54d821a
--- /dev/null
+++ b/client-display/src/userSkills/pointProgress/PointProgressHelper.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default {
+ calculateXAxisMaxTimestamp(histResult) {
+ // only perform this calculation if there are at least 2 months of points
+ if (!histResult || !histResult.pointsHistory || histResult.pointsHistory.length < 60) {
+ return null;
+ }
+ const pointHistory = histResult.pointsHistory;
+ const maxAchievedTimeStamp = this.getMaxAchievedTimestamp(histResult);
+
+ // start at the end
+ let resPosition = pointHistory.length - 1;
+ const maxPoints = pointHistory[resPosition].points;
+
+ // tolerate 2% of point gained
+ const pointsThreshold = Math.trunc(maxPoints * 0.98);
+ for (let i = resPosition; i > 30; i -= 1) {
+ resPosition = i;
+ const pointsToCompare = pointHistory[i].points;
+ if (pointsThreshold > pointsToCompare) {
+ break;
+ }
+ }
+ let maxTimestampRes = new Date(pointHistory[resPosition].dayPerformed).getTime();
+ if (maxAchievedTimeStamp) {
+ maxTimestampRes = Math.max(maxTimestampRes, maxAchievedTimeStamp);
+ }
+ return maxTimestampRes;
+ },
+ getMaxAchievedTimestamp(histResult) {
+ let maxAchievedTimeStamp = -1;
+ if (histResult.achievements && histResult.achievements.length > 0) {
+ const timestamps = histResult.achievements.map((item) => new Date(item.achievedOn).getTime());
+ maxAchievedTimeStamp = Math.max(...timestamps);
+ }
+ return maxAchievedTimeStamp;
+ },
+};
diff --git a/client-display/src/userSkills/service/TokenReauthorizer.js b/client-display/src/userSkills/service/TokenReauthorizer.js
index c78e8e13..08da262a 100644
--- a/client-display/src/userSkills/service/TokenReauthorizer.js
+++ b/client-display/src/userSkills/service/TokenReauthorizer.js
@@ -21,7 +21,6 @@ import store from '@/store';
// eslint-disable-next-line
let service = {};
-
const refreshAuthorization = (failedRequest) => {
if (store.state.authToken === 'pki') {
router.push({
@@ -66,7 +65,7 @@ const getErrorMsg = (errorResponse) => {
return response;
};
-axios.interceptors.response.use(response => response, (error) => {
+axios.interceptors.response.use((response) => response, (error) => {
if (!error || !error.response || (error.response && error.response.status !== 401)) {
const errorMessage = getErrorMsg(error);
router.push({
diff --git a/client-display/src/userSkills/service/UserSkillsService.js b/client-display/src/userSkills/service/UserSkillsService.js
index 18b8cb05..a103209d 100644
--- a/client-display/src/userSkills/service/UserSkillsService.js
+++ b/client-display/src/userSkills/service/UserSkillsService.js
@@ -53,34 +53,34 @@ export default {
let response = null;
response = axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/summary`, {
params: this.getUserIdAndVersionParams(),
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
getCustomIconCss() {
let response = null;
response = axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/customIconCss`, {
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
getCustomGlobalIconCss() {
let response = null;
response = axios.get(`${store.state.serviceUrl}/api/icons/customIconCss`, {
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
getSubjectSummary(subjectId) {
return axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/subjects/${subjectId}/summary`, {
params: this.getUserIdAndVersionParams(),
- }).then(result => result.data);
+ }).then((result) => result.data);
},
getSkillDependencies(skillId) {
return axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/skills/${skillId}/dependencies`, {
params: this.getUserIdParams(),
- }).then(result => result.data);
+ }).then((result) => result.data);
},
getSkillSummary(skillId, optionalCrossProjectId) {
@@ -91,7 +91,7 @@ export default {
return axios.get(url, {
params: this.getUserIdParams(),
withCredentials: true,
- }).then(result => result.data);
+ }).then((result) => result.data);
},
getBadgeSkills(badgeId, global) {
@@ -99,13 +99,13 @@ export default {
requestParams.global = global;
return axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/badges/${badgeId}/summary`, {
params: requestParams,
- }).then(result => result.data);
+ }).then((result) => result.data);
},
getBadgeSummaries() {
return axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/badges/summary`, {
params: this.getUserIdAndVersionParams(),
- }).then(result => result.data);
+ }).then((result) => result.data);
},
getPointsHistory(subjectId) {
@@ -116,7 +116,7 @@ export default {
}
response = axios.get(url, {
params: this.getUserIdAndVersionParams(),
- }).then(result => result.data.pointsHistory);
+ }).then((result) => result.data);
return response;
},
@@ -124,7 +124,7 @@ export default {
let response = null;
response = axios.get(`${store.state.serviceUrl}${this.getServicePath()}/${store.state.projectId}/addSkill/${userSkillId}`, {
params: this.getUserIdParams(),
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
@@ -136,7 +136,7 @@ export default {
}
response = axios.get(url, {
params: this.getUserIdParams(),
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
@@ -150,7 +150,7 @@ export default {
requestParams.subjectId = subjectId;
response = axios.get(url, {
params: requestParams,
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
@@ -164,7 +164,7 @@ export default {
params: {
subjectId,
},
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
@@ -178,7 +178,7 @@ export default {
version: this.version,
global: type === 'global-badge',
},
- }).then(result => result.data);
+ }).then((result) => result.data);
return response;
},
diff --git a/client-display/src/userSkills/skill/AchievementDate.vue b/client-display/src/userSkills/skill/AchievementDate.vue
new file mode 100644
index 00000000..872420fc
--- /dev/null
+++ b/client-display/src/userSkills/skill/AchievementDate.vue
@@ -0,0 +1,39 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
Achieved on {{ date | moment("MMMM Do YYYY") }}
+
{{ date | moment("from", "now") }}
+
+
+
+
+
+
+
+
+
diff --git a/client-display/src/userSkills/skill/PartialPointsAlert.vue b/client-display/src/userSkills/skill/PartialPointsAlert.vue
new file mode 100644
index 00000000..2963d434
--- /dev/null
+++ b/client-display/src/userSkills/skill/PartialPointsAlert.vue
@@ -0,0 +1,54 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+ You were able to earn partial points before the dependencies were added. Don't worry you get to keep the points!!!
+
+
+
Accomplish all of the dependencies to unlock the rest of the skill's points!
+
+
+
+
+ Congrats! You completed this skill before the dependencies were added. Don't worry you get to keep the points!!!
+
+
+
+
+
+
+
+
diff --git a/client-display/src/userSkills/skill/SkillDetails.vue b/client-display/src/userSkills/skill/SkillDetails.vue
index 22f33603..449479d3 100644
--- a/client-display/src/userSkills/skill/SkillDetails.vue
+++ b/client-display/src/userSkills/skill/SkillDetails.vue
@@ -28,69 +28,69 @@ limitations under the License.
diff --git a/frontend/babel.config.js b/dashboard/babel.config.js
similarity index 100%
rename from frontend/babel.config.js
rename to dashboard/babel.config.js
diff --git a/dashboard/package.json b/dashboard/package.json
new file mode 100644
index 00000000..89711a91
--- /dev/null
+++ b/dashboard/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "dashboard",
+ "version": "1.0.0",
+ "license": "Apache-2.0",
+ "description": "SkillTree Dashboard UI",
+ "author": "SkillTree Team",
+ "scripts": {
+ "serve": "vue-cli-service serve",
+ "licenseDepCheck": "license-checker --production --onlyAllow='MIT;ISC;Apache-2.0;BSD-2-Clause;BSD-3-Clause;Unlicense;Custom: https://travis-ci.org/component/emitter.png' --summary",
+ "licenseHeaderCheck": "license-check-and-add check -f '../license-add/license-add-config.json'",
+ "licenseHeaderAdd": "license-check-and-add add -f ../license-add/license-add-config.json",
+ "licenseCheck": "npm run licenseHeaderCheck && npm run licenseDepCheck",
+ "build": "npm run licenseCheck && vue-cli-service build",
+ "deploy": "npm run build && rm -rf ../skills-service/src/main/resources/public/static/js && cp -rT dist ../skills-service/src/main/resources/public/ && cp -rT dist ../skills-service/target/classes/public/",
+ "lint": "vue-cli-service lint",
+ "test:e2e": "vue-cli-service test:e2e",
+ "test:unit": "vue-cli-service test:unit"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "5.14.0",
+ "@skilltree/skills-client-vue": "3.0.1",
+ "animate.css": "4.1.1",
+ "apexcharts": "3.20.2",
+ "axios": "0.20.0",
+ "babel-polyfill": "6.26.0",
+ "bootstrap": "4.5.2",
+ "bootstrap-vue": "2.16.0",
+ "core-js": "3.6.5",
+ "dompurify": "2.0.15",
+ "enquire.js": "2.1.6",
+ "font-awesome-picker": "2.0.0",
+ "lodash.debounce": "4.0.8",
+ "marked": "1.1.1",
+ "matchmedia-polyfill": "0.3.2",
+ "material-icons": "0.3.1",
+ "moment": "2.27.0",
+ "node-emoji": "1.10.0",
+ "numeral": "2.0.6",
+ "sockjs-client": "1.5.0",
+ "vee-validate": "3.4.2",
+ "vis": "4.21.0",
+ "vue": "2.6.12",
+ "vue-apexcharts": "1.6.0",
+ "vue-multiselect": "2.1.6",
+ "vue-router": "3.4.3",
+ "vue-scrollto": "2.18.2",
+ "vue-tables-2": "1.5.46",
+ "vue-virtual-scroll-list": "2.3.1",
+ "vuejs-datepicker": "1.6.2",
+ "vuex": "3.5.1",
+ "webstomp-client": "1.2.6"
+ },
+ "devDependencies": {
+ "@vue/cli-plugin-babel": "4.5.6",
+ "@vue/cli-plugin-e2e-nightwatch": "4.5.6",
+ "@vue/cli-plugin-eslint": "4.5.6",
+ "@vue/cli-plugin-unit-jest": "4.5.6",
+ "@vue/cli-service": "4.5.6",
+ "@vue/eslint-config-airbnb": "5.1.0",
+ "@vue/test-utils": "1.1.0",
+ "@babel/core": "7.11.6",
+ "babel-eslint": "10.1.0",
+ "babel-jest": "26.3.0",
+ "eslint": "7.8.1",
+ "eslint-plugin-vue": "6.2.2",
+ "license-check-and-add": "3.0.4",
+ "license-checker": "25.0.1",
+ "node-sass": "4.14.1",
+ "sass-loader": "10.0.2",
+ "vue-template-compiler": "2.6.12"
+ }
+}
diff --git a/frontend/pom.xml b/dashboard/pom.xml
similarity index 94%
rename from frontend/pom.xml
rename to dashboard/pom.xml
index bb4be5ad..33cd45da 100644
--- a/frontend/pom.xml
+++ b/dashboard/pom.xml
@@ -3,13 +3,13 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- skills-service
- skills
- 1.1.4-SNAPSHOT
+ skills-service-parent
+ skill-tree
+ 1.3.0-SNAPSHOT4.0.0
- frontend
+ dashboardUTF-8
diff --git a/frontend/postcss.config.js b/dashboard/postcss.config.js
similarity index 100%
rename from frontend/postcss.config.js
rename to dashboard/postcss.config.js
diff --git a/frontend/public/index.html b/dashboard/public/index.html
similarity index 96%
rename from frontend/public/index.html
rename to dashboard/public/index.html
index 9375b55a..8a7e53a4 100644
--- a/frontend/public/index.html
+++ b/dashboard/public/index.html
@@ -18,7 +18,7 @@
- User Skills
+ SkillTree Dashboard
diff --git a/frontend/src/components/access/AccessService.js b/dashboard/src/components/access/AccessService.js
similarity index 67%
rename from frontend/src/components/access/AccessService.js
rename to dashboard/src/components/access/AccessService.js
index 224a8cef..52a866c5 100644
--- a/frontend/src/components/access/AccessService.js
+++ b/dashboard/src/components/access/AccessService.js
@@ -19,11 +19,11 @@ export default {
getUserRoles(projectId, roleName) {
if (projectId) {
return axios.get(`/admin/projects/${projectId}/userRoles`)
- .then(response => response.data);
+ .then((response) => response.data);
}
if (roleName === 'ROLE_SUPER_DUPER_USER' || roleName === 'ROLE_SUPERVISOR') {
return axios.get(`/root/users/roles/${roleName}`)
- .then(response => response.data);
+ .then((response) => response.data);
}
throw new Error(`unexpected user role [${roleName}]`);
},
@@ -36,14 +36,14 @@ export default {
userId = userKey;
}
if (projectId) {
- return axios.put(`/admin/projects/${projectId}/users/${userKey}/roles/${roleName}`, null, { headers: { 'x-handleError': false } })
+ return axios.put(`/admin/projects/${projectId}/users/${userKey}/roles/${roleName}`, null, { handleError: false })
.then(() => axios.get(`/admin/projects/${projectId}/users/${userId}/roles`)
- .then(response => response.data.find(element => element.roleName === roleName)));
+ .then((response) => response.data.find((element) => element.roleName === roleName)));
}
if (roleName === 'ROLE_SUPER_DUPER_USER' || roleName === 'ROLE_SUPERVISOR') {
- return axios.put(`/root/users/${userKey}/roles/${roleName}`, null, { headers: { 'x-handleError': false } })
+ return axios.put(`/root/users/${userKey}/roles/${roleName}`, null, { handleError: false })
.then(() => axios.get(`/root/users/roles/${roleName}`)
- .then(response => response.data.find(element => element.userIdForDisplay.toLowerCase() === origUserId.toLowerCase())));
+ .then((response) => response.data.find((element) => element.userIdForDisplay.toLowerCase() === origUserId.toLowerCase())));
}
throw new Error(`unexpected user role [${roleName}]`);
},
@@ -52,13 +52,13 @@ export default {
return axios.delete(`/admin/projects/${projectId}/users/${userId}/roles/${encodeURIComponent(roleName)}`);
}
if (roleName === 'ROLE_SUPER_DUPER_USER' || roleName === 'ROLE_SUPERVISOR') {
- return axios.delete(`/root/users/${userId}/roles/${roleName}`).then(response => response.data);
+ return axios.delete(`/root/users/${userId}/roles/${roleName}`).then((response) => response.data);
}
throw new Error(`unexpected user role [${roleName}]`);
},
getOAuthProviders() {
return axios.get('/app/oAuthProviders')
- .then(response => response.data);
+ .then((response) => response.data);
},
resetClientSecret(projectId) {
return axios.put(`/admin/projects/${projectId}/resetClientSecret`)
@@ -66,12 +66,23 @@ export default {
},
getClientSecret(projectId) {
return axios.get(`/admin/projects/${projectId}/clientSecret`)
- .then(response => response.data);
+ .then((response) => response.data);
},
userWithEmailExists(email) {
- return axios.get(`/userExists/${email}`).then(response => !response.data);
+ return axios.get(`/userExists/${email}`).then((response) => !response.data);
},
hasRole(roleName) {
- return axios.get(`/app/userInfo/hasRole/${roleName}`).then(response => response.data);
+ return axios.get(`/app/userInfo/hasRole/${roleName}`).then((response) => response.data);
+ },
+ requestPasswordReset(userId) {
+ const formData = new FormData();
+ formData.append('userId', userId);
+ return axios.post('/resetPassword', formData, { handleError: false }).then((response) => response.data);
+ },
+ resetPassword(reset) {
+ return axios.post('/performPasswordReset', reset, { handleError: false }).then((response) => response.data);
+ },
+ isResetSupported() {
+ return axios.get('/public/isFeatureSupported?feature=passwordreset').then((response) => response.data);
},
};
diff --git a/frontend/src/components/access/AccessSettings.vue b/dashboard/src/components/access/AccessSettings.vue
similarity index 100%
rename from frontend/src/components/access/AccessSettings.vue
rename to dashboard/src/components/access/AccessSettings.vue
diff --git a/dashboard/src/components/access/BootstrapService.js b/dashboard/src/components/access/BootstrapService.js
new file mode 100644
index 00000000..9ff9326a
--- /dev/null
+++ b/dashboard/src/components/access/BootstrapService.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import axios from 'axios';
+
+export default {
+ grantRoot() {
+ return axios.post('/grantFirstRoot').then((response) => response.data);
+ },
+};
diff --git a/dashboard/src/components/access/Login.vue b/dashboard/src/components/access/Login.vue
new file mode 100644
index 00000000..7779a15b
--- /dev/null
+++ b/dashboard/src/components/access/Login.vue
@@ -0,0 +1,201 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
Sign in to SkillTree Dashboard
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/PkiAppBootstrap.vue b/dashboard/src/components/access/PkiAppBootstrap.vue
new file mode 100644
index 00000000..9f44e243
--- /dev/null
+++ b/dashboard/src/components/access/PkiAppBootstrap.vue
@@ -0,0 +1,69 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/RequestAccess.vue b/dashboard/src/components/access/RequestAccess.vue
new file mode 100644
index 00000000..cc37354c
--- /dev/null
+++ b/dashboard/src/components/access/RequestAccess.vue
@@ -0,0 +1,154 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
+ New SkillTree Root Account
+ New SkillTree Account
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/RequestPasswordReset.vue b/dashboard/src/components/access/RequestPasswordReset.vue
new file mode 100644
index 00000000..c8740996
--- /dev/null
+++ b/dashboard/src/components/access/RequestPasswordReset.vue
@@ -0,0 +1,140 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
Reset Password For SkillTree Dashboard
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/RequestResetConfirmation.vue b/dashboard/src/components/access/RequestResetConfirmation.vue
new file mode 100644
index 00000000..fd498bf2
--- /dev/null
+++ b/dashboard/src/components/access/RequestResetConfirmation.vue
@@ -0,0 +1,61 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
Password Reset Sent!
+
+
+
+ A password reset link has been sent to {{ email }}. You will be forwarded to the login page in {{ timer }} seconds.
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/ResetConfirmation.vue b/dashboard/src/components/access/ResetConfirmation.vue
new file mode 100644
index 00000000..91c9c75e
--- /dev/null
+++ b/dashboard/src/components/access/ResetConfirmation.vue
@@ -0,0 +1,60 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
Password Successfully Reset!
+
+
+
+ Your password has been successfully reset! You will be forwarded to the login page in {{ timer }} seconds.
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/ResetNotSupportedPage.vue b/dashboard/src/components/access/ResetNotSupportedPage.vue
new file mode 100644
index 00000000..b0ec84a1
--- /dev/null
+++ b/dashboard/src/components/access/ResetNotSupportedPage.vue
@@ -0,0 +1,37 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
Password Reset not currently enabled
+
+
+
+ Password Reset is not currently enabled on this system. Please contact your SkillTree administrator. Return to the login page?
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/access/ResetPassword.vue b/dashboard/src/components/access/ResetPassword.vue
new file mode 100644
index 00000000..d2acebba
--- /dev/null
+++ b/dashboard/src/components/access/ResetPassword.vue
@@ -0,0 +1,148 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
+ Reset Account Password
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/access/RoleManager.vue b/dashboard/src/components/access/RoleManager.vue
similarity index 93%
rename from frontend/src/components/access/RoleManager.vue
rename to dashboard/src/components/access/RoleManager.vue
index e5718b4f..afb7b5b7 100644
--- a/frontend/src/components/access/RoleManager.vue
+++ b/dashboard/src/components/access/RoleManager.vue
@@ -21,14 +21,15 @@ limitations under the License.
v-model="selectedUser"/>
-
Add
-
+ Error! Request could not be completed! {{
errNotification.msg }}
@@ -79,7 +80,7 @@ limitations under the License.
role: {
type: String,
default: 'ROLE_PROJECT_ADMIN',
- validator: value => ([ROLE_APP_USER, ROLE_PROJECT_ADMIN, ROLE_SUPERVISOR, ROLE_SUPER_DUPER_USER].indexOf(value) >= 0),
+ validator: (value) => ([ROLE_APP_USER, ROLE_PROJECT_ADMIN, ROLE_SUPERVISOR, ROLE_SUPER_DUPER_USER].indexOf(value) >= 0),
},
roleDescription: {
type: String,
@@ -145,8 +146,8 @@ limitations under the License.
deleteUserRole(row) {
AccessService.deleteUserRole(row.projectId, row.userId, row.roleName)
.then(() => {
- this.data = this.data.filter(item => item.userId !== row.userId);
- this.userIds = this.userIds.filter(userId => userId !== row.userIdForDisplay);
+ this.data = this.data.filter((item) => item.userId !== row.userId);
+ this.userIds = this.userIds.filter((userId) => userId !== row.userIdForDisplay);
this.$emit('role-deleted', { userId: row.userId, role: row.roleName });
});
},
diff --git a/frontend/src/components/access/TrustedClientProps.vue b/dashboard/src/components/access/TrustedClientProps.vue
similarity index 100%
rename from frontend/src/components/access/TrustedClientProps.vue
rename to dashboard/src/components/access/TrustedClientProps.vue
diff --git a/frontend/src/components/badges/Badge.vue b/dashboard/src/components/badges/Badge.vue
similarity index 64%
rename from frontend/src/components/badges/Badge.vue
rename to dashboard/src/components/badges/Badge.vue
index 7d595c85..b91f8ea4 100644
--- a/frontend/src/components/badges/Badge.vue
+++ b/dashboard/src/components/badges/Badge.vue
@@ -23,10 +23,22 @@ limitations under the License.
-
- Manage
-
+
+
+ Manage
+
+
+
+
+
+ Status:
+ Disabled | Go Live
+
+
+ Status: Live
+
+
@@ -54,11 +66,16 @@ limitations under the License.
data() {
return {
isLoading: false,
- badgeInternal: Object.assign({}, this.badge),
+ badgeInternal: { ...this.badge },
cardOptions: {},
showEditBadge: false,
};
},
+ computed: {
+ live() {
+ return this.badgeInternal.enabled !== 'false';
+ },
+ },
watch: {
badge: function badgeWatch(newBadge, oldBadge) {
if (oldBadge) {
@@ -91,6 +108,8 @@ limitations under the License.
icon: this.badgeInternal.iconClass,
title: this.badgeInternal.name,
subTitle: `ID: ${this.badgeInternal.badgeId}`,
+ warn: this.badgeInternal.enabled === 'false',
+ warnMsg: this.badgeInternal.enabled === 'false' ? 'This badge cannot be achieved until it is live' : '',
stats,
};
},
@@ -125,11 +144,37 @@ limitations under the License.
moveDown() {
this.$emit('move-badge-down', this.badgeInternal);
},
+ handlePublish() {
+ const msg = `While this Badge is disabled, user's cannot see the Badge or achieve it. Once the Badge is live, it will be visible to users.
+ Please note that once the badge is live, it cannot be disabled.`;
+ this.msgConfirm(msg, 'Please Confirm!', 'Yes, Go Live!')
+ .then((res) => {
+ if (res) {
+ this.badgeInternal.enabled = 'true';
+ const toSave = { ...this.badgeInternal };
+ if (!toSave.originalBadgeId) {
+ toSave.originalBadgeId = toSave.badgeId;
+ }
+ toSave.startDate = this.toDate(toSave.startDate);
+ toSave.endDate = this.toDate(toSave.endDate);
+ this.badgeEdited(toSave);
+ }
+ });
+ },
+ toDate(value) {
+ let dateVal = value;
+ if (value && !(value instanceof Date)) {
+ dateVal = new Date(Date.parse(value.replace(/-/g, '/')));
+ }
+ return dateVal;
+ },
},
};
-
diff --git a/frontend/src/components/badges/BadgePage.vue b/dashboard/src/components/badges/BadgePage.vue
similarity index 99%
rename from frontend/src/components/badges/BadgePage.vue
rename to dashboard/src/components/badges/BadgePage.vue
index 6c4b50ca..6d738ebc 100644
--- a/frontend/src/components/badges/BadgePage.vue
+++ b/dashboard/src/components/badges/BadgePage.vue
@@ -21,7 +21,6 @@ limitations under the License.
-
+
-
@@ -42,7 +41,7 @@ limitations under the License.
- {{ errors[0] }}
+ {{ errors[0] }}
@@ -52,37 +51,37 @@ limitations under the License.
msg="If project level 'Root Help Url' is specified then this path will be relative to 'Root Help Url'"/>
- {{ errors.first('helpUrl')}}
@@ -91,23 +90,25 @@ limitations under the License.
-
-
-
-
- Save
-
-
- Cancel
-
+
+
+
+ Save
+
+
+ Cancel
+
+
-
-
+
+
diff --git a/frontend/src/components/customization/CustomizableHeader.vue b/dashboard/src/components/customization/CustomizableHeader.vue
similarity index 71%
rename from frontend/src/components/customization/CustomizableHeader.vue
rename to dashboard/src/components/customization/CustomizableHeader.vue
index 53065069..336ff7bc 100644
--- a/frontend/src/components/customization/CustomizableHeader.vue
+++ b/dashboard/src/components/customization/CustomizableHeader.vue
@@ -15,12 +15,21 @@ limitations under the License.
*/
-
+
+
diff --git a/dashboard/src/components/customization/DynamicVariableReplacementMixin.vue b/dashboard/src/components/customization/DynamicVariableReplacementMixin.vue
new file mode 100644
index 00000000..8fe64528
--- /dev/null
+++ b/dashboard/src/components/customization/DynamicVariableReplacementMixin.vue
@@ -0,0 +1,39 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
diff --git a/frontend/src/components/header/Breadcrumb.vue b/dashboard/src/components/header/Breadcrumb.vue
similarity index 97%
rename from frontend/src/components/header/Breadcrumb.vue
rename to dashboard/src/components/header/Breadcrumb.vue
index d4477240..7a545436 100644
--- a/frontend/src/components/header/Breadcrumb.vue
+++ b/dashboard/src/components/header/Breadcrumb.vue
@@ -123,7 +123,7 @@ limitations under the License.
return value.charAt(0).toUpperCase() + value.slice(1);
},
shouldExclude(item) {
- return this.idsToExcludeFromPath.some(searchForMe => item.toUpperCase() === searchForMe.toUpperCase());
+ return this.idsToExcludeFromPath.some((searchForMe) => item.toUpperCase() === searchForMe.toUpperCase());
},
},
};
diff --git a/frontend/src/components/header/Header.vue b/dashboard/src/components/header/Header.vue
similarity index 94%
rename from frontend/src/components/header/Header.vue
rename to dashboard/src/components/header/Header.vue
index ad9d5a7d..b3e5ca9d 100644
--- a/frontend/src/components/header/Header.vue
+++ b/dashboard/src/components/header/Header.vue
@@ -17,7 +17,7 @@ limitations under the License.
- User Skills
+ SkillTree Dashboard
diff --git a/frontend/src/components/header/HelpButton.vue b/dashboard/src/components/header/HelpButton.vue
similarity index 99%
rename from frontend/src/components/header/HelpButton.vue
rename to dashboard/src/components/header/HelpButton.vue
index 0835274e..ab69f737 100644
--- a/frontend/src/components/header/HelpButton.vue
+++ b/dashboard/src/components/header/HelpButton.vue
@@ -43,5 +43,4 @@ limitations under the License.
diff --git a/frontend/src/components/header/NewSoftwareVersion.vue b/dashboard/src/components/header/NewSoftwareVersion.vue
similarity index 93%
rename from frontend/src/components/header/NewSoftwareVersion.vue
rename to dashboard/src/components/header/NewSoftwareVersion.vue
index f13420ca..65fcef33 100644
--- a/frontend/src/components/header/NewSoftwareVersion.vue
+++ b/dashboard/src/components/header/NewSoftwareVersion.vue
@@ -45,7 +45,9 @@ limitations under the License.
},
watch: {
libVersion() {
- if (localStorage.skillsDashboardLibVersion !== undefined && this.libVersion.localeCompare(localStorage.skillsDashboardLibVersion) > 0) {
+ if (localStorage.skillsDashboardLibVersion !== undefined
+ && this.libVersion !== undefined
+ && this.libVersion.localeCompare(localStorage.skillsDashboardLibVersion) > 0) {
this.showNewVersionAlert = true;
}
this.updateStorageIfNeeded();
diff --git a/frontend/src/components/header/SettingsButton.vue b/dashboard/src/components/header/SettingsButton.vue
similarity index 84%
rename from frontend/src/components/header/SettingsButton.vue
rename to dashboard/src/components/header/SettingsButton.vue
index 2c70af3d..6a384714 100644
--- a/frontend/src/components/header/SettingsButton.vue
+++ b/dashboard/src/components/header/SettingsButton.vue
@@ -16,8 +16,8 @@ limitations under the License.
-
- {{ displayName }}
+
+ {{ displayName }} Settings
@@ -67,6 +67,21 @@ limitations under the License.
diff --git a/frontend/src/components/icons/CustomIconService.js b/dashboard/src/components/icons/CustomIconService.js
similarity index 90%
rename from frontend/src/components/icons/CustomIconService.js
rename to dashboard/src/components/icons/CustomIconService.js
index c318cc55..6460a7fe 100644
--- a/frontend/src/components/icons/CustomIconService.js
+++ b/dashboard/src/components/icons/CustomIconService.js
@@ -26,8 +26,8 @@ export default {
if (url) {
return axios
.get(url)
- .then(response => response.data);
+ .then((response) => response.data);
}
- return new Promise(resolve => resolve(null));
+ return new Promise((resolve) => resolve(null));
},
};
diff --git a/frontend/src/components/inception/InceptionButton.vue b/dashboard/src/components/inception/InceptionButton.vue
similarity index 93%
rename from frontend/src/components/inception/InceptionButton.vue
rename to dashboard/src/components/inception/InceptionButton.vue
index 0f5d93b9..245d3f8e 100644
--- a/frontend/src/components/inception/InceptionButton.vue
+++ b/dashboard/src/components/inception/InceptionButton.vue
@@ -20,7 +20,7 @@ limitations under the License.
+
+
diff --git a/frontend/src/components/levels/global/GlobalBadgeLevels.vue b/dashboard/src/components/levels/global/GlobalBadgeLevels.vue
similarity index 97%
rename from frontend/src/components/levels/global/GlobalBadgeLevels.vue
rename to dashboard/src/components/levels/global/GlobalBadgeLevels.vue
index b4d8a5b7..b9fa9224 100644
--- a/frontend/src/components/levels/global/GlobalBadgeLevels.vue
+++ b/dashboard/src/components/levels/global/GlobalBadgeLevels.vue
@@ -143,7 +143,7 @@ limitations under the License.
levelDeleted(deletedItem) {
GlobalBadgeService.removeProjectLevelFromBadge(this.badgeId, deletedItem.projectId, deletedItem.level)
.then(() => {
- this.badgeLevels = this.badgeLevels.filter(item => `${item.projectId}${item.level}` !== `${deletedItem.projectId}${deletedItem.level}`);
+ this.badgeLevels = this.badgeLevels.filter((item) => `${item.projectId}${item.level}` !== `${deletedItem.projectId}${deletedItem.level}`);
this.loadGlobalBadgeDetailsState({ badgeId: this.badgeId });
this.$refs.projectSelectorRef.loadProjectsForBadge();
this.$emit('global-badge-levels-changed', deletedItem);
diff --git a/frontend/src/components/levels/global/LevelSelector.vue b/dashboard/src/components/levels/global/LevelSelector.vue
similarity index 97%
rename from frontend/src/components/levels/global/LevelSelector.vue
rename to dashboard/src/components/levels/global/LevelSelector.vue
index 77ecf3c2..6e4fe3eb 100644
--- a/frontend/src/components/levels/global/LevelSelector.vue
+++ b/dashboard/src/components/levels/global/LevelSelector.vue
@@ -68,7 +68,7 @@ limitations under the License.
this.isLoading = true;
GlobalBadgeService.getProjectLevels(projectId)
.then((response) => {
- this.projectLevels = response.map(entry => entry.level);
+ this.projectLevels = response.map((entry) => entry.level);
}).finally(() => {
this.isLoading = false;
});
diff --git a/frontend/src/components/levels/global/ProjectSelector.vue b/dashboard/src/components/levels/global/ProjectSelector.vue
similarity index 96%
rename from frontend/src/components/levels/global/ProjectSelector.vue
rename to dashboard/src/components/levels/global/ProjectSelector.vue
index 8668f342..0b738daf 100644
--- a/frontend/src/components/levels/global/ProjectSelector.vue
+++ b/dashboard/src/components/levels/global/ProjectSelector.vue
@@ -76,7 +76,7 @@ limitations under the License.
methods: {
setSelectedInternal() {
if (this.value) {
- this.selectedInternal = Object.assign({}, this.value);
+ this.selectedInternal = { ...this.value };
} else {
this.selectedInternal = null;
}
@@ -95,7 +95,7 @@ limitations under the License.
GlobalBadgeService.getAllProjectsForBadge(this.badgeId)
.then((response) => {
this.isLoading = false;
- this.projects = response.map(entry => entry);
+ this.projects = response.map((entry) => entry);
});
},
},
diff --git a/frontend/src/components/levels/global/SimpleLevelsTable.vue b/dashboard/src/components/levels/global/SimpleLevelsTable.vue
similarity index 99%
rename from frontend/src/components/levels/global/SimpleLevelsTable.vue
rename to dashboard/src/components/levels/global/SimpleLevelsTable.vue
index 3443d8fd..71ecb58c 100644
--- a/frontend/src/components/levels/global/SimpleLevelsTable.vue
+++ b/dashboard/src/components/levels/global/SimpleLevelsTable.vue
@@ -93,7 +93,6 @@ limitations under the License.
}
}
-
#simple-skills-table .notactive {
cursor: not-allowed;
pointer-events: none;
diff --git a/frontend/src/components/metrics/MetricsCard.vue b/dashboard/src/components/metrics/MetricsCard.vue
similarity index 100%
rename from frontend/src/components/metrics/MetricsCard.vue
rename to dashboard/src/components/metrics/MetricsCard.vue
diff --git a/frontend/src/components/metrics/MetricsService.js b/dashboard/src/components/metrics/MetricsService.js
similarity index 89%
rename from frontend/src/components/metrics/MetricsService.js
rename to dashboard/src/components/metrics/MetricsService.js
index d4e8b1ea..dcb6277a 100644
--- a/frontend/src/components/metrics/MetricsService.js
+++ b/dashboard/src/components/metrics/MetricsService.js
@@ -19,30 +19,30 @@ export default {
getChartsForSection(sectionParams) {
const url = `/admin/projects/${sectionParams.projectId}/${sectionParams.section}/${sectionParams.sectionIdParam}/metrics?numDays=${sectionParams.numDays}&numMonths=${sectionParams.numMonths}&loadDataForFirst=${sectionParams.loadDataForFirst}`;
return axios.get(url)
- .then(response => Promise.resolve(this.buildCharts(response.data)));
+ .then((response) => Promise.resolve(this.buildCharts(response.data)));
},
getGlobalChartsForSection(sectionParams) {
const url = `/metrics/${sectionParams.section}?numDays=${sectionParams.numDays}&numMonths=${sectionParams.numMonths}&loadDataForFirst=${sectionParams.loadDataForFirst}`;
const self = this;
return axios.get(url)
- .then(response => Promise.resolve(self.buildCharts(response.data)));
+ .then((response) => Promise.resolve(self.buildCharts(response.data)));
},
getGlobalChartForSection(sectionParams) {
const url = `/metrics/${sectionParams.section}/${sectionParams.sectionIdParam}/metric/${sectionParams.chartBuilderId}?numDays=${sectionParams.numDays}&numMonths=${sectionParams.numMonths}&loadDataForFirst=${sectionParams.loadDataForFirst}`;
return axios.get(url)
- .then(response => Promise.resolve(this.buildCharts(response.data)));
+ .then((response) => Promise.resolve(this.buildCharts(response.data)));
},
getChartForSection(sectionParams) {
const url = `/admin/projects/${sectionParams.projectId}/${sectionParams.section}/${sectionParams.sectionIdParam}/metrics/${sectionParams.chartBuilderId}?numDays=${sectionParams.numDays}&numMonths=${sectionParams.numMonths}`;
return axios.get(url)
- .then(response => Promise.resolve(this.buildChart(response.data)));
+ .then((response) => Promise.resolve(this.buildChart(response.data)));
},
buildCharts(data) {
- return data.map(item => this.buildChart(item));
+ return data.map((item) => this.buildChart(item));
},
buildChart(chartData) {
@@ -66,11 +66,11 @@ export default {
let seriesData = null;
if (chartData.chartType.toLowerCase() === 'pie') {
- seriesData = chartData.dataItems.map(dataItem => dataItem.count);
+ seriesData = chartData.dataItems.map((dataItem) => dataItem.count);
return seriesData;
}
- seriesData = chartData.dataItems.map(dataItem => ({ x: dataItem.value, y: dataItem.count }));
+ seriesData = chartData.dataItems.map((dataItem) => ({ x: dataItem.value, y: dataItem.count }));
const sortAsc = (a, b) => a.y - b.y;
const sortDsc = (a, b) => b.y - a.y;
diff --git a/frontend/src/components/metrics/SectionHelper.js b/dashboard/src/components/metrics/SectionHelper.js
similarity index 99%
rename from frontend/src/components/metrics/SectionHelper.js
rename to dashboard/src/components/metrics/SectionHelper.js
index fe704ac2..8c52825b 100644
--- a/frontend/src/components/metrics/SectionHelper.js
+++ b/dashboard/src/components/metrics/SectionHelper.js
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+/* eslint-disable */
export const SECTION = {
GLOBAL: 'global',
PROJECTS: 'projects',
diff --git a/frontend/src/components/metrics/SectionMetrics.vue b/dashboard/src/components/metrics/SectionMetrics.vue
similarity index 95%
rename from frontend/src/components/metrics/SectionMetrics.vue
rename to dashboard/src/components/metrics/SectionMetrics.vue
index 65738ee9..46d051e6 100644
--- a/frontend/src/components/metrics/SectionMetrics.vue
+++ b/dashboard/src/components/metrics/SectionMetrics.vue
@@ -120,10 +120,10 @@ limitations under the License.
},
computed: {
loadedCharts() {
- return this.charts.filter(chart => chart.dataLoaded);
+ return this.charts.filter((chart) => chart.dataLoaded);
},
loadableCharts() {
- return this.charts.filter(chart => !chart.dataLoaded);
+ return this.charts.filter((chart) => !chart.dataLoaded);
},
canDisplayCharts() {
return this.loadedCharts && this.loadedCharts.length > 0;
@@ -180,8 +180,8 @@ limitations under the License.
}
promise.then((response) => {
- this.charts.splice(this.charts.findIndex(it => it.chartMeta.chartBuilderId === chartBuilderId), 1);
- this.charts.push(Object.assign({ scrollIntoView: true }, response));
+ this.charts.splice(this.charts.findIndex((it) => it.chartMeta.chartBuilderId === chartBuilderId), 1);
+ this.charts.push({ scrollIntoView: true, ...response });
})
.finally(() => {
this.isLoading = false;
diff --git a/frontend/src/components/metrics/SkillsChart.vue b/dashboard/src/components/metrics/SkillsChart.vue
similarity index 100%
rename from frontend/src/components/metrics/SkillsChart.vue
rename to dashboard/src/components/metrics/SkillsChart.vue
diff --git a/frontend/src/components/projects/EditProject.vue b/dashboard/src/components/projects/EditProject.vue
similarity index 59%
rename from frontend/src/components/projects/EditProject.vue
rename to dashboard/src/components/projects/EditProject.vue
index e16d147f..6415a38c 100644
--- a/frontend/src/components/projects/EditProject.vue
+++ b/dashboard/src/components/projects/EditProject.vue
@@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-
+
-
-
-
-
- {{ errors[0] }}
+
+
+
+ {{ errors[0] }}
@@ -38,34 +42,35 @@ limitations under the License.
***{{ overallErrMsg }}***
-
-
-
-
- Save
-
-
- Cancel
-
-
-
+
+
+ Save
+
+
+ Cancel
+
+
+
+
-
diff --git a/frontend/src/components/projects/MyProjects.vue b/dashboard/src/components/projects/MyProjects.vue
similarity index 84%
rename from frontend/src/components/projects/MyProjects.vue
rename to dashboard/src/components/projects/MyProjects.vue
index d7882b3b..9d63e512 100644
--- a/frontend/src/components/projects/MyProjects.vue
+++ b/dashboard/src/components/projects/MyProjects.vue
@@ -20,6 +20,8 @@ limitations under the License.
:disabled="addProjectDisabled" :disabled-msg="addProjectsDisabledMsg"/>
+
@@ -37,7 +39,7 @@ limitations under the License.
+
+
diff --git a/frontend/src/components/settings/EmailSettings.vue b/dashboard/src/components/settings/EmailSettings.vue
similarity index 100%
rename from frontend/src/components/settings/EmailSettings.vue
rename to dashboard/src/components/settings/EmailSettings.vue
diff --git a/frontend/src/components/settings/GeneralSettings.vue b/dashboard/src/components/settings/GeneralSettings.vue
similarity index 59%
rename from frontend/src/components/settings/GeneralSettings.vue
rename to dashboard/src/components/settings/GeneralSettings.vue
index 86445301..27cd4516 100644
--- a/frontend/src/components/settings/GeneralSettings.vue
+++ b/dashboard/src/components/settings/GeneralSettings.vue
@@ -15,64 +15,57 @@ limitations under the License.
*/
-
+
-
-
-
-
-
-
-
-
{{ errors.first('first')}}
+
+
+
+
+
+
+
+
+
+
{{ errors[0]}}
+
-
-
-
+
+
+
+
+
+
{{ errors[0]}}
+
-
{{ errors.first('last')}}
-
-
-
-
-
-
{{ errors.first('nickname')}}
+
+
+
+
+
+
{{ errors[0]}}
+
-
-
+
+
+
-
+
+
+
diff --git a/frontend/src/components/skills/AddSkillEvent.vue b/dashboard/src/components/skills/AddSkillEvent.vue
similarity index 75%
rename from frontend/src/components/skills/AddSkillEvent.vue
rename to dashboard/src/components/skills/AddSkillEvent.vue
index 2c06b414..9d76beca 100644
--- a/frontend/src/components/skills/AddSkillEvent.vue
+++ b/dashboard/src/components/skills/AddSkillEvent.vue
@@ -17,25 +17,31 @@ limitations under the License.
-
-
-
- {{ errors.first('User Id')}}
-
-
-
-
-
-
-
- Add
-
-
+
+
+
+
+
+ {{ errors[0]}}
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
-
+
@@ -57,7 +63,6 @@ limitations under the License.
+
+
diff --git a/frontend/src/components/skills/SearchAllSkillsCheckbox.vue b/dashboard/src/components/skills/SearchAllSkillsCheckbox.vue
similarity index 100%
rename from frontend/src/components/skills/SearchAllSkillsCheckbox.vue
rename to dashboard/src/components/skills/SearchAllSkillsCheckbox.vue
diff --git a/frontend/src/components/skills/SimpleSkillsTable.vue b/dashboard/src/components/skills/SimpleSkillsTable.vue
similarity index 99%
rename from frontend/src/components/skills/SimpleSkillsTable.vue
rename to dashboard/src/components/skills/SimpleSkillsTable.vue
index 8a02ff8e..6832076c 100644
--- a/frontend/src/components/skills/SimpleSkillsTable.vue
+++ b/dashboard/src/components/skills/SimpleSkillsTable.vue
@@ -31,7 +31,6 @@ limitations under the License.
-
@@ -128,7 +127,6 @@ limitations under the License.
}
}
-
#simple-skills-table .notactive {
cursor: not-allowed;
pointer-events: none;
diff --git a/frontend/src/components/skills/SkillOverview.vue b/dashboard/src/components/skills/SkillOverview.vue
similarity index 100%
rename from frontend/src/components/skills/SkillOverview.vue
rename to dashboard/src/components/skills/SkillOverview.vue
diff --git a/frontend/src/components/skills/SkillPage.vue b/dashboard/src/components/skills/SkillPage.vue
similarity index 94%
rename from frontend/src/components/skills/SkillPage.vue
rename to dashboard/src/components/skills/SkillPage.vue
index 14b41156..9f0ee5e6 100644
--- a/frontend/src/components/skills/SkillPage.vue
+++ b/dashboard/src/components/skills/SkillPage.vue
@@ -85,16 +85,17 @@ limitations under the License.
]),
loadData() {
this.isLoading = true;
+ const { projectId, subjectId } = this.$route.params;
SkillsService.getSkillDetails(this.$route.params.projectId, this.$route.params.subjectId, this.$route.params.skillId)
.then((response) => {
- this.skill = Object.assign(response, { subjectId: this.$route.params.subjectId });
+ this.skill = Object.assign(response, { subjectId });
this.headerOptions = this.buildHeaderOptions(this.skill);
if (this.subject) {
this.isLoading = false;
} else {
this.loadSubjectDetailsState({
- projectId: this.$route.params.projectId,
- subjectId: this.$route.params.subjectId,
+ projectId,
+ subjectId,
}).then(() => {
this.isLoading = false;
});
diff --git a/frontend/src/components/skills/Skills.vue b/dashboard/src/components/skills/Skills.vue
similarity index 97%
rename from frontend/src/components/skills/Skills.vue
rename to dashboard/src/components/skills/Skills.vue
index 1f18be41..51653e54 100644
--- a/frontend/src/components/skills/Skills.vue
+++ b/dashboard/src/components/skills/Skills.vue
@@ -54,7 +54,7 @@ limitations under the License.
.then((skills) => {
const loadedSkills = skills;
this.skills = loadedSkills.map((loadedSkill) => {
- const copy = Object.assign({}, loadedSkill);
+ const copy = { ...loadedSkill };
copy.created = window.moment(loadedSkill.created);
return copy;
});
diff --git a/frontend/src/components/skills/SkillsSelector2.vue b/dashboard/src/components/skills/SkillsSelector2.vue
similarity index 94%
rename from frontend/src/components/skills/SkillsSelector2.vue
rename to dashboard/src/components/skills/SkillsSelector2.vue
index 0f9ec33e..04ec2e12 100644
--- a/frontend/src/components/skills/SkillsSelector2.vue
+++ b/dashboard/src/components/skills/SkillsSelector2.vue
@@ -102,12 +102,12 @@ limitations under the License.
methods: {
setSelectedInternal() {
if (this.selected) {
- this.selectedInternal = this.selected.map(entry => Object.assign({ entryId: `${entry.projectId}_${entry.skillId}` }, entry));
+ this.selectedInternal = this.selected.map((entry) => ({ entryId: `${entry.projectId}_${entry.skillId}`, ...entry }));
}
},
setOptionsInternal() {
if (this.options) {
- this.optionsInternal = this.options.map(entry => Object.assign({ entryId: `${entry.projectId}_${entry.skillId}` }, entry));
+ this.optionsInternal = this.options.map((entry) => ({ entryId: `${entry.projectId}_${entry.skillId}`, ...entry }));
}
},
considerRemoval(removedItem, removeMethod) {
@@ -148,7 +148,6 @@ limitations under the License.
cursor: pointer;
}
-
diff --git a/frontend/src/components/skills/crossProjects/CrossProjectsSkills.vue b/dashboard/src/components/skills/crossProjects/CrossProjectsSkills.vue
similarity index 100%
rename from frontend/src/components/skills/crossProjects/CrossProjectsSkills.vue
rename to dashboard/src/components/skills/crossProjects/CrossProjectsSkills.vue
diff --git a/frontend/src/components/skills/crossProjects/ProjectSelector.vue b/dashboard/src/components/skills/crossProjects/ProjectSelector.vue
similarity index 100%
rename from frontend/src/components/skills/crossProjects/ProjectSelector.vue
rename to dashboard/src/components/skills/crossProjects/ProjectSelector.vue
diff --git a/frontend/src/components/skills/crossProjects/ShareSkillsWithOtherProjects.vue b/dashboard/src/components/skills/crossProjects/ShareSkillsWithOtherProjects.vue
similarity index 97%
rename from frontend/src/components/skills/crossProjects/ShareSkillsWithOtherProjects.vue
rename to dashboard/src/components/skills/crossProjects/ShareSkillsWithOtherProjects.vue
index bfb9d8fd..52b44728 100644
--- a/frontend/src/components/skills/crossProjects/ShareSkillsWithOtherProjects.vue
+++ b/dashboard/src/components/skills/crossProjects/ShareSkillsWithOtherProjects.vue
@@ -157,7 +157,7 @@ limitations under the License.
},
doesShareAlreadyExist() {
const selectedSkill = this.selectedSkills[0];
- const alreadyExist = this.sharedSkills.find(entry => entry.skillId === selectedSkill.skillId && (!entry.projectId || this.shareWithAllProjects || entry.projectId === this.selectedProject.projectId));
+ const alreadyExist = this.sharedSkills.find((entry) => entry.skillId === selectedSkill.skillId && (!entry.projectId || this.shareWithAllProjects || entry.projectId === this.selectedProject.projectId));
if (alreadyExist) {
if (alreadyExist.sharedWithAllProjects) {
this.errorMessage = `Skill [${selectedSkill.name}] is already shared to [All Projects].`;
diff --git a/frontend/src/components/skills/crossProjects/SharedSkillsFromOtherProjects.vue b/dashboard/src/components/skills/crossProjects/SharedSkillsFromOtherProjects.vue
similarity index 100%
rename from frontend/src/components/skills/crossProjects/SharedSkillsFromOtherProjects.vue
rename to dashboard/src/components/skills/crossProjects/SharedSkillsFromOtherProjects.vue
diff --git a/frontend/src/components/skills/crossProjects/SharedSkillsTable.vue b/dashboard/src/components/skills/crossProjects/SharedSkillsTable.vue
similarity index 100%
rename from frontend/src/components/skills/crossProjects/SharedSkillsTable.vue
rename to dashboard/src/components/skills/crossProjects/SharedSkillsTable.vue
diff --git a/frontend/src/components/skills/crossProjects/SkillsShareService.js b/dashboard/src/components/skills/crossProjects/SkillsShareService.js
similarity index 93%
rename from frontend/src/components/skills/crossProjects/SkillsShareService.js
rename to dashboard/src/components/skills/crossProjects/SkillsShareService.js
index 6418acb6..9efa2324 100644
--- a/frontend/src/components/skills/crossProjects/SkillsShareService.js
+++ b/dashboard/src/components/skills/crossProjects/SkillsShareService.js
@@ -24,11 +24,11 @@ export default {
},
getSharedSkills(projectId) {
return axios.get(`/admin/projects/${projectId}/shared`)
- .then(response => response.data);
+ .then((response) => response.data);
},
getSharedWithmeSkills(projectId) {
return axios.get(`/admin/projects/${projectId}/sharedWithMe`)
- .then(response => response.data);
+ .then((response) => response.data);
},
};
diff --git a/frontend/src/components/skills/dependencies/DependantsGraph.vue b/dashboard/src/components/skills/dependencies/DependantsGraph.vue
similarity index 94%
rename from frontend/src/components/skills/dependencies/DependantsGraph.vue
rename to dashboard/src/components/skills/dependencies/DependantsGraph.vue
index b9b5d3fa..23536ab4 100644
--- a/frontend/src/components/skills/dependencies/DependantsGraph.vue
+++ b/dashboard/src/components/skills/dependencies/DependantsGraph.vue
@@ -124,17 +124,17 @@ limitations under the License.
},
methods: {
updateNodes() {
- const newItems = this.dependentSkills.filter(item => !this.nodes.get().find(item1 => item1.id === item.id));
+ const newItems = this.dependentSkills.filter((item) => !this.nodes.get().find((item1) => item1.id === item.id));
newItems.forEach((newItem) => {
const nodeEdgeData = this.buildNodeEdgeData(newItem);
this.edges.add(nodeEdgeData.edge);
this.nodes.add(nodeEdgeData.node);
});
- const removeItems = this.nodes.get().filter(item => !this.dependentSkills.find(item1 => item1.id === item.id) && item.id !== this.skill.id);
+ const removeItems = this.nodes.get().filter((item) => !this.dependentSkills.find((item1) => item1.id === item.id) && item.id !== this.skill.id);
removeItems.forEach((item) => {
this.nodes.remove(item.id);
- const edgeToRemove = this.edges.get().find(edgeItem => edgeItem.to === item.id);
+ const edgeToRemove = this.edges.get().find((edgeItem) => edgeItem.to === item.id);
this.edges.remove(edgeToRemove);
});
},
@@ -172,7 +172,7 @@ limitations under the License.
background: 'lightgreen',
};
// newNode.shape = 'circle';
- } else if (!this.dependentSkills.find(elem => elem.id === newNode.id)) {
+ } else if (!this.dependentSkills.find((elem) => elem.id === newNode.id)) {
newNode.color = {
border: 'darkgray',
background: 'lightgray',
diff --git a/frontend/src/components/skills/dependencies/FullDependencyGraph.vue b/dashboard/src/components/skills/dependencies/FullDependencyGraph.vue
similarity index 99%
rename from frontend/src/components/skills/dependencies/FullDependencyGraph.vue
rename to dashboard/src/components/skills/dependencies/FullDependencyGraph.vue
index d0fc697a..b6b0acdb 100644
--- a/frontend/src/components/skills/dependencies/FullDependencyGraph.vue
+++ b/dashboard/src/components/skills/dependencies/FullDependencyGraph.vue
@@ -37,7 +37,6 @@ limitations under the License.
-
-
diff --git a/dashboard/src/components/utils/MarkdownText.vue b/dashboard/src/components/utils/MarkdownText.vue
new file mode 100644
index 00000000..5d302b1a
--- /dev/null
+++ b/dashboard/src/components/utils/MarkdownText.vue
@@ -0,0 +1,63 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/utils/Navigation.vue b/dashboard/src/components/utils/Navigation.vue
similarity index 96%
rename from frontend/src/components/utils/Navigation.vue
rename to dashboard/src/components/utils/Navigation.vue
index ccde9f05..0d0fc711 100644
--- a/frontend/src/components/utils/Navigation.vue
+++ b/dashboard/src/components/utils/Navigation.vue
@@ -28,6 +28,7 @@ limitations under the License.
@@ -87,7 +88,7 @@ limitations under the License.
buildNewMenuMapWhenPropsChange(navigationItems) {
const routeName = this.$route.name;
if (navigationItems && navigationItems.length > 0) {
- const navItem = navigationItems.find(item => item.page === routeName);
+ const navItem = navigationItems.find((item) => item.page === routeName);
this.menuSelections = this.buildNewMenuMap(navItem ? navItem.name : navigationItems[0].name);
}
},
diff --git a/frontend/src/components/utils/NoContent2.vue b/dashboard/src/components/utils/NoContent2.vue
similarity index 100%
rename from frontend/src/components/utils/NoContent2.vue
rename to dashboard/src/components/utils/NoContent2.vue
diff --git a/frontend/src/components/utils/NoContent3.vue b/dashboard/src/components/utils/NoContent3.vue
similarity index 100%
rename from frontend/src/components/utils/NoContent3.vue
rename to dashboard/src/components/utils/NoContent3.vue
diff --git a/frontend/src/components/utils/NotAuthorizedPage.vue b/dashboard/src/components/utils/NotAuthorizedPage.vue
similarity index 88%
rename from frontend/src/components/utils/NotAuthorizedPage.vue
rename to dashboard/src/components/utils/NotAuthorizedPage.vue
index 53c1397d..bf40bda8 100644
--- a/frontend/src/components/utils/NotAuthorizedPage.vue
+++ b/dashboard/src/components/utils/NotAuthorizedPage.vue
@@ -26,8 +26,11 @@ limitations under the License.
-
-
+
+
+ {{ explanation }}
+
+
You are not authorized to view this resource.
@@ -40,6 +43,9 @@ limitations under the License.
export default {
name: 'NotAuthorized',
+ props: {
+ explanation: String,
+ },
};
diff --git a/dashboard/src/components/utils/NotFoundPage.vue b/dashboard/src/components/utils/NotFoundPage.vue
new file mode 100644
index 00000000..08f09e89
--- /dev/null
+++ b/dashboard/src/components/utils/NotFoundPage.vue
@@ -0,0 +1,75 @@
+/*
+Copyright 2020 SkillTree
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+
+
+
+
+
+
+
+
Resource Not Found
+
+
+
+
+
+ {{ explanation }}
+
+
+ The resource you requested cannot be located.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/utils/RequestOrderMixin.vue b/dashboard/src/components/utils/RequestOrderMixin.vue
similarity index 95%
rename from frontend/src/components/utils/RequestOrderMixin.vue
rename to dashboard/src/components/utils/RequestOrderMixin.vue
index 1b40acdd..7d094f32 100644
--- a/frontend/src/components/utils/RequestOrderMixin.vue
+++ b/dashboard/src/components/utils/RequestOrderMixin.vue
@@ -23,7 +23,7 @@ limitations under the License.
},
methods: {
getRequestId() {
- this.requestId = this.requestId + 1;
+ this.requestId += 1;
return this.requestId;
},
ensureOrderlyResultHandling(requestId, closure) {
diff --git a/frontend/src/components/utils/ServerTableLoadingMask.vue b/dashboard/src/components/utils/ServerTableLoadingMask.vue
similarity index 100%
rename from frontend/src/components/utils/ServerTableLoadingMask.vue
rename to dashboard/src/components/utils/ServerTableLoadingMask.vue
diff --git a/frontend/src/components/utils/SkillsSpinner.vue b/dashboard/src/components/utils/SkillsSpinner.vue
similarity index 100%
rename from frontend/src/components/utils/SkillsSpinner.vue
rename to dashboard/src/components/utils/SkillsSpinner.vue
diff --git a/frontend/src/components/utils/ToastSupport.vue b/dashboard/src/components/utils/ToastSupport.vue
similarity index 100%
rename from frontend/src/components/utils/ToastSupport.vue
rename to dashboard/src/components/utils/ToastSupport.vue
diff --git a/frontend/src/components/utils/cards/MediaInfoCard.vue b/dashboard/src/components/utils/cards/MediaInfoCard.vue
similarity index 100%
rename from frontend/src/components/utils/cards/MediaInfoCard.vue
rename to dashboard/src/components/utils/cards/MediaInfoCard.vue
diff --git a/frontend/src/components/utils/cards/SimpleCard.vue b/dashboard/src/components/utils/cards/SimpleCard.vue
similarity index 100%
rename from frontend/src/components/utils/cards/SimpleCard.vue
rename to dashboard/src/components/utils/cards/SimpleCard.vue
diff --git a/dashboard/src/components/utils/iconPicker/GroupedIcons.js b/dashboard/src/components/utils/iconPicker/GroupedIcons.js
new file mode 100644
index 00000000..3e792a1d
--- /dev/null
+++ b/dashboard/src/components/utils/iconPicker/GroupedIcons.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default class GroupedIcons {
+ row = [];
+
+ constructor(row) {
+ this.row = row;
+ }
+
+ get id() {
+ return this.row.reduce((accumulator, currentVal) => accumulator + currentVal.cssClass, '');
+ }
+}
diff --git a/frontend/src/components/utils/iconPicker/IconManager.vue b/dashboard/src/components/utils/iconPicker/IconManager.vue
similarity index 70%
rename from frontend/src/components/utils/iconPicker/IconManager.vue
rename to dashboard/src/components/utils/iconPicker/IconManager.vue
index b2f209a9..71363896 100644
--- a/frontend/src/components/utils/iconPicker/IconManager.vue
+++ b/dashboard/src/components/utils/iconPicker/IconManager.vue
@@ -15,7 +15,8 @@ limitations under the License.
*/
-
+
@@ -23,21 +24,15 @@ limitations under the License.
{{ fontAwesomeIcons.iconPack }}
No icons matched your search
-
-
+
+
+
+
+
diff --git a/frontend/src/components/utils/iconPicker/font-awesome-index.js b/dashboard/src/components/utils/iconPicker/font-awesome-index.js
similarity index 100%
rename from frontend/src/components/utils/iconPicker/font-awesome-index.js
rename to dashboard/src/components/utils/iconPicker/font-awesome-index.js
diff --git a/frontend/src/components/utils/iconPicker/material-index.js b/dashboard/src/components/utils/iconPicker/material-index.js
similarity index 100%
rename from frontend/src/components/utils/iconPicker/material-index.js
rename to dashboard/src/components/utils/iconPicker/material-index.js
diff --git a/frontend/src/components/utils/inputForm/FormTextInput.vue b/dashboard/src/components/utils/inputForm/FormTextInput.vue
similarity index 100%
rename from frontend/src/components/utils/inputForm/FormTextInput.vue
rename to dashboard/src/components/utils/inputForm/FormTextInput.vue
diff --git a/frontend/src/components/utils/inputForm/IdInput.vue b/dashboard/src/components/utils/inputForm/IdInput.vue
similarity index 73%
rename from frontend/src/components/utils/inputForm/IdInput.vue
rename to dashboard/src/components/utils/inputForm/IdInput.vue
index d0a8e23f..015f936f 100644
--- a/frontend/src/components/utils/inputForm/IdInput.vue
+++ b/dashboard/src/components/utils/inputForm/IdInput.vue
@@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-
+
diff --git a/frontend/src/components/utils/modal/MsgBoxMixin.vue b/dashboard/src/components/utils/modal/MsgBoxMixin.vue
similarity index 100%
rename from frontend/src/components/utils/modal/MsgBoxMixin.vue
rename to dashboard/src/components/utils/modal/MsgBoxMixin.vue
diff --git a/frontend/src/components/utils/pages/PageHeader.vue b/dashboard/src/components/utils/pages/PageHeader.vue
similarity index 100%
rename from frontend/src/components/utils/pages/PageHeader.vue
rename to dashboard/src/components/utils/pages/PageHeader.vue
diff --git a/frontend/src/components/utils/pages/PagePreviewCard.vue b/dashboard/src/components/utils/pages/PagePreviewCard.vue
similarity index 90%
rename from frontend/src/components/utils/pages/PagePreviewCard.vue
rename to dashboard/src/components/utils/pages/PagePreviewCard.vue
index f85cb951..6ebadbfc 100644
--- a/frontend/src/components/utils/pages/PagePreviewCard.vue
+++ b/dashboard/src/components/utils/pages/PagePreviewCard.vue
@@ -23,7 +23,9 @@ limitations under the License.
-
{{ options.title }}
+
{{ options.title }}
+
+
{{ options.subTitle }}
@@ -58,6 +60,8 @@ limitations under the License.
options: {
icon: String,
title: String,
+ warn: Boolean,
+ warnMsg: String,
subTitle: String,
stats: [],
},
diff --git a/frontend/src/components/utils/pages/SubPageHeader.vue b/dashboard/src/components/utils/pages/SubPageHeader.vue
similarity index 100%
rename from frontend/src/components/utils/pages/SubPageHeader.vue
rename to dashboard/src/components/utils/pages/SubPageHeader.vue
diff --git a/frontend/src/components/utils/upload/FileUpload.vue b/dashboard/src/components/utils/upload/FileUpload.vue
similarity index 94%
rename from frontend/src/components/utils/upload/FileUpload.vue
rename to dashboard/src/components/utils/upload/FileUpload.vue
index 84272a18..53928d22 100644
--- a/frontend/src/components/utils/upload/FileUpload.vue
+++ b/dashboard/src/components/utils/upload/FileUpload.vue
@@ -35,14 +35,6 @@ limitations under the License.
const DEFAULT_STATUS_MSG = 'Drag your file here to upload or click to browse';
export default {
- $_veeValidate: {
- value() {
- return this.getFormData();
- },
- name() {
- return this.name;
- },
- },
name: 'FileUpload',
props: ['name', 'accept'],
data() {
diff --git a/frontend/src/components/utils/upload/FileUploadService.js b/dashboard/src/components/utils/upload/FileUploadService.js
similarity index 91%
rename from frontend/src/components/utils/upload/FileUploadService.js
rename to dashboard/src/components/utils/upload/FileUploadService.js
index 2c457c1b..19a8f0dc 100644
--- a/frontend/src/components/utils/upload/FileUploadService.js
+++ b/dashboard/src/components/utils/upload/FileUploadService.js
@@ -17,7 +17,7 @@ import axios from 'axios';
export default {
upload(url, formData, success, failure) {
- axios.post(url, formData, { headers: { 'x-handleError': false } })
+ axios.post(url, formData, { handleError: false })
.then(success)
.catch(failure);
},
diff --git a/frontend/src/directives/FocusDirective.js b/dashboard/src/directives/FocusDirective.js
similarity index 100%
rename from frontend/src/directives/FocusDirective.js
rename to dashboard/src/directives/FocusDirective.js
diff --git a/frontend/src/directives/SkillsOnMountDirective.js b/dashboard/src/directives/SkillsOnMountDirective.js
similarity index 92%
rename from frontend/src/directives/SkillsOnMountDirective.js
rename to dashboard/src/directives/SkillsOnMountDirective.js
index 77895b38..a8379340 100644
--- a/frontend/src/directives/SkillsOnMountDirective.js
+++ b/dashboard/src/directives/SkillsOnMountDirective.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import Vue from 'vue';
-import { SkillsReporter } from '@skills/skills-client-vue';
+import { SkillsReporter } from '@skilltree/skills-client-vue';
// Register a global custom directive
Vue.directive('skills-onMount', {
diff --git a/frontend/src/filters/DateFilter.js b/dashboard/src/filters/DateFilter.js
similarity index 92%
rename from frontend/src/filters/DateFilter.js
rename to dashboard/src/filters/DateFilter.js
index 6d167896..56606c0a 100644
--- a/frontend/src/filters/DateFilter.js
+++ b/dashboard/src/filters/DateFilter.js
@@ -16,10 +16,9 @@
import Vue from 'vue';
import moment from 'moment';
-const dateFormatter = value => moment(value).format('YYYY-MM-DD HH:mm');
+const dateFormatter = (value) => moment(value).format('YYYY-MM-DD HH:mm');
Vue.filter('date', dateFormatter);
-
// this allows to call this function from an js code; to learn more about that read about javascript modules
// import DateFilter from 'src/DateFilter.js'
// DateFilter(dateStrVAlue)
diff --git a/frontend/src/filters/NumberFilter.js b/dashboard/src/filters/NumberFilter.js
similarity index 93%
rename from frontend/src/filters/NumberFilter.js
rename to dashboard/src/filters/NumberFilter.js
index f6de0eac..ec808991 100644
--- a/frontend/src/filters/NumberFilter.js
+++ b/dashboard/src/filters/NumberFilter.js
@@ -16,10 +16,9 @@
import Vue from 'vue';
import numeral from 'numeral';
-const numberFormatter = value => numeral(value).format('0,0');
+const numberFormatter = (value) => numeral(value).format('0,0');
Vue.filter('number', numberFormatter);
-
// this allows to call this function from an js code; to learn more about that read about javascript modules
// import NumberFilter from 'src/NumberFilter.js'
// NumberFilter(myNumber)
diff --git a/frontend/src/filters/TruncateFilter.js b/dashboard/src/filters/TruncateFilter.js
similarity index 99%
rename from frontend/src/filters/TruncateFilter.js
rename to dashboard/src/filters/TruncateFilter.js
index b99d5d6a..499a1fa4 100644
--- a/frontend/src/filters/TruncateFilter.js
+++ b/dashboard/src/filters/TruncateFilter.js
@@ -33,7 +33,6 @@ const truncateFormatter = (strValue, truncateTo = 30) => {
};
Vue.filter('truncate', truncateFormatter);
-
// this allows to call this function from an js code; to learn more about that read about javascript modules
// import NumberFilter from 'src/NumberFilter.js'
// NumberFilter(myNumber)
diff --git a/frontend/src/interceptors/clientVersionInterceptor.js b/dashboard/src/interceptors/clientVersionInterceptor.js
similarity index 78%
rename from frontend/src/interceptors/clientVersionInterceptor.js
rename to dashboard/src/interceptors/clientVersionInterceptor.js
index 0304fe71..90f09484 100644
--- a/frontend/src/interceptors/clientVersionInterceptor.js
+++ b/dashboard/src/interceptors/clientVersionInterceptor.js
@@ -17,8 +17,10 @@ import axios from 'axios';
import store from '../store/store';
function handleFunction(config) {
- const incomingVersion = config.headers['skills-client-lib-version'];
- store.dispatch('updateLibVersionIfDifferent', incomingVersion);
+ if (config && config.headers && config.headers['skills-client-lib-version']) {
+ const incomingVersion = config.headers['skills-client-lib-version'];
+ store.dispatch('updateLibVersionIfDifferent', incomingVersion);
+ }
}
// apply interceptor on response
diff --git a/frontend/src/interceptors/errorHandler.js b/dashboard/src/interceptors/errorHandler.js
similarity index 70%
rename from frontend/src/interceptors/errorHandler.js
rename to dashboard/src/interceptors/errorHandler.js
index 5b0829ee..102a4c22 100644
--- a/frontend/src/interceptors/errorHandler.js
+++ b/dashboard/src/interceptors/errorHandler.js
@@ -17,13 +17,9 @@ import axios from 'axios';
import router from '../router';
import store from '../store/store';
-
function errorResponseHandler(error) {
// check if the caller wants to handle the error with displaying the errorPage/dialog
- if ((Object.prototype.hasOwnProperty.call(error.config, 'handleError') && error.config.handleError === false)
- || (error.config && error.config.headers && error.config.headers['x-handleError'] === false)) {
- // config.handleError does not appear to be propagated here regardless of whether or not it's set on the axios call
- // only properties defined on AxiosRequestConfig make it here
+ if (Object.prototype.hasOwnProperty.call(error.config, 'handleError') && error.config.handleError === false) {
return Promise.reject(error);
}
@@ -39,16 +35,26 @@ function errorResponseHandler(error) {
router.push(loginRoute);
}
} else if (errorCode === 403) {
- router.push({ name: 'NotAuthorizedPage' });
+ let explanation;
+ if (error.response && error.response.data && error.response.data.explanation) {
+ ({ explanation } = error.response.data);
+ }
+ router.push({ name: 'NotAuthorizedPage', params: { explanation } });
+ } else if (errorCode === 404) {
+ let explanation;
+ if (error.response && error.response.data && error.response.data.explanation) {
+ ({ explanation } = error.response.data);
+ }
+ router.push({ name: 'NotFoundPage', params: { explanation } });
} else {
router.push({ name: 'ErrorPage' });
}
- return Promise.reject(error);
+ return Promise.resolve({ data: {} });
}
// apply interceptor on response
axios.interceptors.response.use(
- response => response,
+ (response) => response,
errorResponseHandler,
);
diff --git a/frontend/src/main.js b/dashboard/src/main.js
similarity index 54%
rename from frontend/src/main.js
rename to dashboard/src/main.js
index a8103b17..e1339989 100644
--- a/frontend/src/main.js
+++ b/dashboard/src/main.js
@@ -18,8 +18,11 @@
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import { ClientTable, ServerTable } from 'vue-tables-2';
-import { SkillsDirective } from '@skills/skills-client-vue';
-import VeeValidate from 'vee-validate';
+import { SkillsConfiguration, SkillsDirective, SkillsReporter } from '@skilltree/skills-client-vue';
+import {
+ localize, ValidationProvider, ValidationObserver, setInteractionMode,
+} from 'vee-validate';
+import en from 'vee-validate/dist/locale/en.json';
import VueApexCharts from 'vue-apexcharts';
import Vuex from 'vuex';
import InceptionConfigurer from './InceptionConfigurer';
@@ -38,13 +41,18 @@ import store from './store/store';
Vue.use(ClientTable, {}, false, 'bootstrap4', 'default');
Vue.use(ServerTable, {}, false, 'bootstrap4', 'default');
-Vue.use(VeeValidate);
+Vue.component('ValidationProvider', ValidationProvider);
+Vue.component('ValidationObserver', ValidationObserver);
Vue.use(Vuex);
Vue.use(VueApexCharts);
Vue.use(BootstrapVue);
Vue.use(SkillsDirective);
-VeeValidate.setMode('betterEager', () => ({ on: ['input'], debounce: 500 }));
+localize({
+ en,
+});
+
+setInteractionMode('custom', () => ({ on: ['input', 'change'] }));
Vue.component('apexchart', VueApexCharts);
@@ -58,6 +66,51 @@ require('./interceptors/clientVersionInterceptor');
require('vue-multiselect/dist/vue-multiselect.min.css');
+const isActiveProjectIdChange = (to, from) => to.params.projectId !== from.params.projectId;
+const isLoggedIn = () => store.getters.isAuthenticated;
+const isPki = () => store.getters.isPkiAuthenticated;
+
+router.beforeEach((to, from, next) => {
+ const requestAccountPath = '/request-root-account';
+ if (!isPki() && !isLoggedIn() && to.path !== requestAccountPath && store.getters.config.needToBootstrap) {
+ next({ path: requestAccountPath });
+ } else if (!isPki() && to.path === requestAccountPath && !store.getters.config.needToBootstrap) {
+ next({ path: '/' });
+ } else {
+ if (from.path !== '/error') {
+ store.commit('previousUrl', from.fullPath);
+ }
+ if (isActiveProjectIdChange(to, from)) {
+ store.commit('currentProjectId', to.params.projectId);
+ }
+ if (to.matched.some((record) => record.meta.requiresAuth)) {
+ // this route requires auth, check if logged in if not, redirect to login page.
+ if (!isLoggedIn()) {
+ const newRoute = { query: { redirect: to.fullPath } };
+ if (isPki()) {
+ newRoute.name = 'HomePage';
+ } else {
+ newRoute.name = 'Login';
+ }
+ next(newRoute);
+ } else {
+ next();
+ }
+ } else {
+ next();
+ }
+ }
+});
+
+router.afterEach((to) => {
+ if (to.meta.reportSkillId) {
+ SkillsConfiguration.afterConfigure()
+ .then(() => {
+ SkillsReporter.reportSkill(to.meta.reportSkillId);
+ });
+ }
+});
+
store.dispatch('loadConfigState').finally(() => {
RegisterValidators.init();
store.dispatch('restoreSessionIfAvailable').finally(() => {
diff --git a/frontend/src/router/index.js b/dashboard/src/router/index.js
similarity index 85%
rename from frontend/src/router/index.js
rename to dashboard/src/router/index.js
index 534fff0e..8701aa1c 100644
--- a/frontend/src/router/index.js
+++ b/dashboard/src/router/index.js
@@ -15,7 +15,6 @@
*/
import Vue from 'vue';
import Router from 'vue-router';
-import { SkillsReporter, SkillsConfiguration } from '@skills/skills-client-vue';
import HomePage from '@/components/HomePage';
import MyProjects from '@/components/projects/MyProjects';
import LoginForm from '@/components/access/Login';
@@ -23,12 +22,12 @@ import RequestAccountForm from '@/components/access/RequestAccess';
import ProjectPage from '@/components/projects/ProjectPage';
import ErrorPage from '@/components/utils/ErrorPage';
import NotAuthorizedPage from '@/components/utils/NotAuthorizedPage';
+import NotFoundPage from '@/components/utils/NotFoundPage';
import SubjectPage from '@/components/subjects/SubjectPage';
import BadgePage from '@/components/badges/BadgePage';
import GlobalBadgePage from '@/components/badges/global/GlobalBadgePage';
import SkillPage from '@/components/skills/SkillPage';
import UserPage from '@/components/users/UserPage';
-import store from '@/store/store';
import GlobalSettings from '@/components/settings/GlobalSettings';
import GFMDescription from '@//components/utils/GFMDescription';
import InceptionSkills from '@//components/inception/InceptionSkills';
@@ -54,7 +53,13 @@ import UserSkillsPerformed from '@//components/users/UserSkillsPerformed';
import GeneralSettings from '@//components/settings/GeneralSettings';
import SecuritySettings from '@//components/settings/SecuritySettings';
import EmailSettings from '@//components/settings/EmailSettings';
+import SystemSettings from '@//components/settings/SystemSettings';
import { SECTION } from '@//components/metrics/SectionHelper';
+import ResetPassword from '@//components/access/ResetPassword';
+import RequestPasswordReset from '@//components/access/RequestPasswordReset';
+import RequestResetConfirmation from '@//components/access/RequestResetConfirmation';
+import ResetConfirmation from '@//components/access/ResetConfirmation';
+import ResetNotSupportedPage from '@//components/access/ResetNotSupportedPage';
Vue.use(Router);
@@ -98,6 +103,58 @@ const router = new Router({
requiresAuth: false,
},
},
+ {
+ path: '/forgot-password',
+ name: 'ForgotPassword',
+ component: RequestPasswordReset,
+ meta: {
+ requiresAuth: false,
+ },
+ },
+ {
+ path: '/reset-password/:resetToken',
+ name: 'ResetPassword',
+ component: ResetPassword,
+ props: true,
+ meta: {
+ requiresAuth: false,
+ },
+ },
+ {
+ path: '/forgot-password-confirmation',
+ name: 'RequestResetConfirmation',
+ component: RequestResetConfirmation,
+ props: true,
+ meta: {
+ requiresAuth: false,
+ },
+ },
+ {
+ path: '/reset-password-confirmation',
+ name: 'ResetConfirmation',
+ component: ResetConfirmation,
+ props: true,
+ meta: {
+ requiresAuth: false,
+ },
+ },
+ {
+ path: '/reset-not-supported',
+ name: 'ResetNotSupportedPage',
+ component: ResetNotSupportedPage,
+ meta: {
+ requiresAuth: false,
+ },
+ },
+ {
+ path: '/request-root-account',
+ name: 'RequestRootAccount',
+ component: RequestAccountForm,
+ props: { isRootAccount: true },
+ meta: {
+ requiresAuth: false,
+ },
+ },
{
path: '/error',
name: 'ErrorPage',
@@ -110,6 +167,16 @@ const router = new Router({
path: '/not-authorized',
name: 'NotAuthorizedPage',
component: NotAuthorizedPage,
+ props: true,
+ meta: {
+ requiresAuth: false,
+ },
+ },
+ {
+ path: '/not-found',
+ name: 'NotFoundPage',
+ component: NotFoundPage,
+ props: true,
meta: {
requiresAuth: false,
},
@@ -289,6 +356,11 @@ const router = new Router({
path: 'email',
component: EmailSettings,
meta: { requiresAuth: true },
+ }, {
+ name: 'SystemSettings',
+ path: 'system',
+ component: SystemSettings,
+ meta: { requiresAuth: true },
}],
},
{
@@ -341,41 +413,4 @@ const router = new Router({
],
});
-const isActiveProjectIdChange = (to, from) => to.params.projectId !== from.params.projectId;
-const isLoggedIn = () => store.getters.isAuthenticated;
-
-router.beforeEach((to, from, next) => {
- if (from.path !== '/error') {
- store.commit('previousUrl', from.fullPath);
- }
- if (isActiveProjectIdChange(to, from)) {
- store.commit('currentProjectId', to.params.projectId);
- }
- if (to.matched.some(record => record.meta.requiresAuth)) {
- // this route requires auth, check if logged in if not, redirect to login page.
- if (!isLoggedIn()) {
- const newRoute = { query: { redirect: to.fullPath } };
- if (store.getters.isPkiAuthenticated) {
- newRoute.name = 'HomePage';
- } else {
- newRoute.name = 'Login';
- }
- next(newRoute);
- } else {
- next();
- }
- } else {
- next();
- }
-});
-
-router.afterEach((to) => {
- if (to.meta.reportSkillId) {
- SkillsConfiguration.afterConfigure()
- .then(() => {
- SkillsReporter.reportSkill(to.meta.reportSkillId);
- });
- }
-});
-
export default router;
diff --git a/frontend/src/store/modules/access.js b/dashboard/src/store/modules/access.js
similarity index 74%
rename from frontend/src/store/modules/access.js
rename to dashboard/src/store/modules/access.js
index 47094b2c..91810f32 100644
--- a/frontend/src/store/modules/access.js
+++ b/dashboard/src/store/modules/access.js
@@ -21,7 +21,15 @@ const actions = {
AccessService.hasRole('ROLE_SUPERVISOR').then((result) => {
commit('supervisor', result);
resolve(result);
- }).catch(error => reject(error));
+ }).catch((error) => reject(error));
+ });
+ },
+ isRoot({ commit }) {
+ return new Promise((resolve, reject) => {
+ AccessService.hasRole('ROLE_SUPER_DUPER_USER').then((result) => {
+ commit('root', result);
+ resolve(result);
+ }).catch((error) => reject(error));
});
},
};
@@ -30,16 +38,23 @@ const mutations = {
supervisor(state, value) {
state.isSupervisor = value;
},
+ root(state, value) {
+ state.isRoot = value;
+ },
};
const getters = {
isSupervisor(state) {
return state.isSupervisor;
},
+ isRoot(state) {
+ return state.isRoot;
+ },
};
const state = {
isSupervisor: null,
+ isRoot: null,
};
export default {
diff --git a/frontend/src/store/modules/auth.js b/dashboard/src/store/modules/auth.js
similarity index 76%
rename from frontend/src/store/modules/auth.js
rename to dashboard/src/store/modules/auth.js
index 2a0797cf..99c15f40 100644
--- a/frontend/src/store/modules/auth.js
+++ b/dashboard/src/store/modules/auth.js
@@ -14,24 +14,21 @@
* limitations under the License.
*/
import axios from 'axios';
-import { SkillsConfiguration } from '@skills/skills-client-vue';
+import { SkillsConfiguration } from '@skilltree/skills-client-vue';
import router from '../../router';
const getters = {
userInfo(state) {
return state.userInfo;
},
- isAuthenticated(state) {
+ isAuthenticated(state, gettersParam) {
return (
state.token !== null
- || state.pkiAuth
+ || gettersParam.isPkiAuthenticated
|| state.localAuth
|| state.oAuthAuth
) && state.userInfo !== null;
},
- isPkiAuthenticated(state) {
- return state.pkiAuth;
- },
};
const mutations = {
@@ -68,57 +65,69 @@ const mutations = {
},
};
+const handleLogin = (commit, dispatch, result) => {
+ const token = result.headers.authorization;
+ let expirationDate;
+ // special handling for oAuth
+ if (result.headers.tokenexpirationtimestamp) {
+ expirationDate = new Date(Number(result.headers.tokenexpirationtimestamp));
+ dispatch('setLogoutTimer', expirationDate);
+ }
+ commit('authUser', {
+ token,
+ expirationDate,
+ });
+};
+
const actions = {
signup({ commit, dispatch }, authData) {
return new Promise((resolve, reject) => {
- axios.put('/createAccount', authData)
+ const url = authData.isRootAccount ? '/createRootAccount' : '/createAccount';
+ axios.put(url, authData)
.then((result) => {
if (result) {
- const token = result.headers.authorization;
- let expirationDate;
- if (result.headers.tokenexpirationtimestamp) {
- expirationDate = new Date(Number(result.headers.tokenexpirationtimestamp));
- dispatch('setLogoutTimer', expirationDate);
- }
- commit('authUser', { token, expirationDate });
+ handleLogin(commit, dispatch, result);
dispatch('fetchUser')
.then(() => {
- resolve(result);
+ if (authData.isRootAccount) {
+ // when creating root account for the first time, reload the config state
+ // at a minimum it will update the flag indicating whether root user needs to be created
+ dispatch('loadConfigState')
+ .then(() => {
+ resolve(result);
+ });
+ } else {
+ resolve(result);
+ }
});
}
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
login({ commit, dispatch }, authData) {
return new Promise((resolve, reject) => {
axios.post('/performLogin', authData, { handleError: false })
.then((result) => {
- const token = result.headers.authorization;
- let expirationDate;
- if (result.headers.tokenexpirationtimestamp) {
- expirationDate = new Date(Number(result.headers.tokenexpirationtimestamp));
- dispatch('setLogoutTimer', expirationDate);
- }
- commit('authUser', { token, expirationDate });
+ handleLogin(commit, dispatch, result);
dispatch('fetchUser')
.then(() => {
resolve(result);
});
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
oAuth2Login({ commit }, oAuthId) {
commit('oAuth2AuthUser');
window.location = `/oauth2/authorization/${oAuthId}`;
},
- restoreSessionIfAvailable({ commit, dispatch, state }) {
+ restoreSessionIfAvailable({
+ commit, dispatch, state, getters: gettersParam,
+ }) {
return new Promise((resolve, reject) => {
let reAuthenticated = false;
const token = localStorage.getItem('token');
- const localAuth = (localStorage.getItem('localAuth') === 'true');
- const oAuthAuth = (localStorage.getItem('oAuthAuth') === 'true');
if (token) {
let tokenExpired = true;
let expirationDate = localStorage.getItem('expirationDate');
@@ -145,17 +154,13 @@ const actions = {
dispatch('fetchUser', false).then(() => {
if (state.userInfo) {
reAuthenticated = true;
- if (!localAuth && !oAuthAuth) {
- state.pkiAuth = true;
- } else {
- state.localAuth = true;
- }
+ state.localAuth = !gettersParam.isPkiAuthenticated;
} else {
// cannot obtain userInfo, so clear any other lingering auth data
commit('clearAuthData');
}
resolve(reAuthenticated);
- }).catch(error => reject(error));
+ }).catch((error) => reject(error));
}
});
},
@@ -180,7 +185,7 @@ const actions = {
}
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
};
@@ -188,7 +193,6 @@ const actions = {
const state = {
token: null,
userInfo: null,
- pkiAuth: false,
localAuth: false,
oAuthAuth: false,
};
diff --git a/frontend/src/store/modules/badges.js b/dashboard/src/store/modules/badges.js
similarity index 94%
rename from frontend/src/store/modules/badges.js
rename to dashboard/src/store/modules/badges.js
index e3cf2d52..ecc56b71 100644
--- a/frontend/src/store/modules/badges.js
+++ b/dashboard/src/store/modules/badges.js
@@ -36,7 +36,7 @@ const actions = {
commit('setBadge', response);
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
loadGlobalBadgeDetailsState({ commit }, payload) {
@@ -46,7 +46,7 @@ const actions = {
commit('setBadge', response);
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
};
diff --git a/frontend/src/store/modules/config.js b/dashboard/src/store/modules/config.js
similarity index 90%
rename from frontend/src/store/modules/config.js
rename to dashboard/src/store/modules/config.js
index 5eb4c176..a9145cc6 100644
--- a/frontend/src/store/modules/config.js
+++ b/dashboard/src/store/modules/config.js
@@ -19,6 +19,9 @@ const getters = {
config(state) {
return state.config;
},
+ isPkiAuthenticated(state) {
+ return state.config.authMode === 'PKI';
+ },
};
const mutations = {
@@ -35,7 +38,7 @@ const actions = {
commit('setConfig', response);
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
};
diff --git a/frontend/src/store/modules/libVersion.js b/dashboard/src/store/modules/libVersion.js
similarity index 100%
rename from frontend/src/store/modules/libVersion.js
rename to dashboard/src/store/modules/libVersion.js
diff --git a/frontend/src/store/modules/projects.js b/dashboard/src/store/modules/projects.js
similarity index 96%
rename from frontend/src/store/modules/projects.js
rename to dashboard/src/store/modules/projects.js
index 76aea86b..788e7185 100644
--- a/frontend/src/store/modules/projects.js
+++ b/dashboard/src/store/modules/projects.js
@@ -35,7 +35,7 @@ const actions = {
commit('setProject', response);
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
};
diff --git a/frontend/src/store/modules/subjects.js b/dashboard/src/store/modules/subjects.js
similarity index 96%
rename from frontend/src/store/modules/subjects.js
rename to dashboard/src/store/modules/subjects.js
index fd35002e..278e20ea 100644
--- a/frontend/src/store/modules/subjects.js
+++ b/dashboard/src/store/modules/subjects.js
@@ -35,7 +35,7 @@ const actions = {
commit('setSubject', response);
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
};
diff --git a/frontend/src/store/modules/users.js b/dashboard/src/store/modules/users.js
similarity index 97%
rename from frontend/src/store/modules/users.js
rename to dashboard/src/store/modules/users.js
index 25b193d4..b1fa8852 100644
--- a/frontend/src/store/modules/users.js
+++ b/dashboard/src/store/modules/users.js
@@ -54,7 +54,7 @@ const actions = {
commit('setUserTotalPoints', response.userTotalPoints);
resolve(response);
})
- .catch(error => reject(error));
+ .catch((error) => reject(error));
});
},
};
diff --git a/frontend/src/store/store.js b/dashboard/src/store/store.js
similarity index 92%
rename from frontend/src/store/store.js
rename to dashboard/src/store/store.js
index 0cb6c9a1..227b6937 100644
--- a/frontend/src/store/store.js
+++ b/dashboard/src/store/store.js
@@ -30,6 +30,7 @@ export default new Vuex.Store({
state: {
projectId: '',
previousUrl: '',
+ projectSearch: '',
},
mutations: {
currentProjectId(state, projectId) {
@@ -38,6 +39,9 @@ export default new Vuex.Store({
previousUrl(state, previousUrl) {
state.previousUrl = previousUrl;
},
+ projectSearch(state, projectSearch) {
+ state.projectSearch = projectSearch;
+ },
},
modules: {
auth,
diff --git a/frontend/src/styles/palette.scss b/dashboard/src/styles/palette.scss
similarity index 100%
rename from frontend/src/styles/palette.scss
rename to dashboard/src/styles/palette.scss
diff --git a/frontend/src/styles/utils.css b/dashboard/src/styles/utils.css
similarity index 100%
rename from frontend/src/styles/utils.css
rename to dashboard/src/styles/utils.css
diff --git a/frontend/src/validators/CustomDescriptionValidator.js b/dashboard/src/validators/CustomDescriptionValidator.js
similarity index 78%
rename from frontend/src/validators/CustomDescriptionValidator.js
rename to dashboard/src/validators/CustomDescriptionValidator.js
index c1845d3d..ecf5a339 100644
--- a/frontend/src/validators/CustomDescriptionValidator.js
+++ b/dashboard/src/validators/CustomDescriptionValidator.js
@@ -13,23 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import VeeValidate from 'vee-validate';
+import { extend } from 'vee-validate';
import store from '../store/store';
import CustomValidatorService from './CustomValidatorsService';
const validator = {
- getMessage: field => `${field} - ${store.getters.config.paragraphValidationMessage}.`,
+ message: (field) => `${field} - ${store.getters.config.paragraphValidationMessage}.`,
validate(value) {
if (!store.getters.config.paragraphValidationRegex) {
return true;
}
- return CustomValidatorService.validateDescription(value).then(result => result.valid);
+ return CustomValidatorService.validateDescription(value).then((result) => result.valid);
},
};
-VeeValidate.Validator.extend('customDescriptionValidator', validator, {
- immediate: false,
-});
+extend('customDescriptionValidator', validator);
export default validator;
diff --git a/frontend/src/validators/CustomNameValidator.js b/dashboard/src/validators/CustomNameValidator.js
similarity index 74%
rename from frontend/src/validators/CustomNameValidator.js
rename to dashboard/src/validators/CustomNameValidator.js
index d2667d7c..7f8ed211 100644
--- a/frontend/src/validators/CustomNameValidator.js
+++ b/dashboard/src/validators/CustomNameValidator.js
@@ -13,23 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import VeeValidate from 'vee-validate';
+import { extend } from 'vee-validate';
import store from '../store/store';
import CustomValidatorService from './CustomValidatorsService';
const validator = {
- getMessage: field => `${field} - ${store.getters.config.nameValidationMessage}.`,
+ message: (field) => `${field} - ${store.getters.config.nameValidationMessage}.`,
validate(value) {
if (!store.getters.config.nameValidationRegex) {
return true;
}
- return CustomValidatorService.validateName(value).then(result => result.valid);
+ return CustomValidatorService.validateName(value).then((result) => result.valid);
},
};
-VeeValidate.Validator.extend('customNameValidator', validator, {
- immediate: false,
-});
+extend('customNameValidator', validator);
export default validator;
diff --git a/frontend/src/validators/CustomValidatorsService.js b/dashboard/src/validators/CustomValidatorsService.js
similarity index 88%
rename from frontend/src/validators/CustomValidatorsService.js
rename to dashboard/src/validators/CustomValidatorsService.js
index f45c070c..fa15a0bf 100644
--- a/frontend/src/validators/CustomValidatorsService.js
+++ b/dashboard/src/validators/CustomValidatorsService.js
@@ -20,12 +20,12 @@ export default {
const body = {
value: description,
};
- return axios.post('/app/validation/description', body).then(result => result.data);
+ return axios.post('/app/validation/description', body).then((result) => result.data);
},
validateName(name) {
const body = {
value: name,
};
- return axios.post('/app/validation/name', body).then(result => result.data);
+ return axios.post('/app/validation/name', body).then((result) => result.data);
},
};
diff --git a/frontend/src/validators/NumConvertUtil.js b/dashboard/src/validators/NumConvertUtil.js
similarity index 100%
rename from frontend/src/validators/NumConvertUtil.js
rename to dashboard/src/validators/NumConvertUtil.js
diff --git a/frontend/src/validators/OptionalNumericValidator.js b/dashboard/src/validators/OptionalNumericValidator.js
similarity index 82%
rename from frontend/src/validators/OptionalNumericValidator.js
rename to dashboard/src/validators/OptionalNumericValidator.js
index 4eaaf41c..c695d1d2 100644
--- a/frontend/src/validators/OptionalNumericValidator.js
+++ b/dashboard/src/validators/OptionalNumericValidator.js
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import VeeValidate from 'vee-validate';
+import { extend } from 'vee-validate';
const numericRegex = /^[0-9]+$/;
const validator = {
- getMessage: field => `The ${field} field may only contain numeric characters.`,
+ message: (field) => `${field} may only contain numeric characters.`,
validate(value) {
const testValue = (val) => {
const strValue = String(val);
@@ -37,8 +37,6 @@ const validator = {
},
};
-VeeValidate.Validator.extend('optionalNumeric', validator, {
- immediate: false,
-});
+extend('optionalNumeric', validator);
export default validator;
diff --git a/dashboard/src/validators/RegisterValidators.js b/dashboard/src/validators/RegisterValidators.js
new file mode 100644
index 00000000..f8d8bc05
--- /dev/null
+++ b/dashboard/src/validators/RegisterValidators.js
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { extend, localize } from 'vee-validate';
+import { required } from 'vee-validate/dist/rules';
+import './OptionalNumericValidator';
+import './CustomDescriptionValidator';
+import './CustomNameValidator';
+import ValidatorFactory from './ValidatorFactory';
+import store from '../store/store';
+
+export default {
+ init() {
+ extend('maxDescriptionLength', ValidatorFactory.newCharLengthValidator(store.getters.config.descriptionMaxLength));
+ extend('maxFirstNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxFirstNameLength));
+ extend('maxLastNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxLastNameLength));
+ extend('maxNicknameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxNicknameLength));
+
+ extend('minUsernameLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minUsernameLength));
+
+ extend('minPasswordLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minPasswordLength));
+ extend('maxPasswordLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxPasswordLength));
+
+ extend('minIdLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minIdLength));
+ extend('maxIdLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxIdLength));
+
+ extend('minNameLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minNameLength));
+
+ extend('maxBadgeNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxBadgeNameLength));
+ extend('maxProjectNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxProjectNameLength));
+ extend('maxSkillNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxSkillNameLength));
+ extend('maxSubjectNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxSubjectNameLength));
+ extend('maxLevelNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxLevelNameLength));
+
+ extend('maxSkillVersion', ValidatorFactory.newMaxNumValidator(store.getters.config.maxSkillVersion));
+ extend('maxPointIncrement', ValidatorFactory.newMaxNumValidator(store.getters.config.maxPointIncrement));
+ extend('maxNumPerformToCompletion', ValidatorFactory.newMaxNumValidator(store.getters.config.maxNumPerformToCompletion));
+ extend('maxNumPointIncrementMaxOccurrences', ValidatorFactory.newMaxNumValidator(store.getters.config.maxNumPointIncrementMaxOccurrences));
+
+ extend('userNoSpaceInUserIdInNonPkiMode', ValidatorFactory.newUserObjNoSpacesValidatorInNonPkiMode(store.getters.isPkiAuthenticated));
+
+ extend('required', required);
+
+ localize({
+ en: {
+ messages: {
+ alpha: '{_field_} may only contain alphabetic characters',
+ alpha_num: '{_field_} may only contain alpha-numeric characters',
+ alpha_dash: '{_field_} may contain alpha-numeric characters as well as dashes and underscores',
+ alpha_spaces: '{_field_} may only contain alphabetic characters as well as spaces',
+ between: '{_field_} must be between {min} and {max}',
+ confirmed: '{_field_} confirmation does not match',
+ digits: '{_field_} must be numeric and exactly contain {length} digits',
+ dimensions: '{_field_} must be {width} pixels by {height} pixels',
+ email: '{_field_} must be a valid email',
+ excluded: '{_field_} is not a valid value',
+ ext: '{_field_} is not a valid file',
+ image: '{_field_} must be an image',
+ integer: '{_field_} must be an integer',
+ length: '{_field_} must be {length} long',
+ max_value: '{_field_} must be {max} or less',
+ max: '{_field_} may not be greater than {length} characters',
+ mimes: '{_field_} must have a valid file type',
+ min_value: '{_field_} must be {min} or more',
+ min: '{_field_} must be at least {length} characters',
+ numeric: '{_field_} may only contain numeric characters',
+ oneOf: '{_field_} is not a valid value',
+ regex: '{_field_} format is invalid',
+ required_if: '{_field_} is required',
+ required: '{_field_} is required',
+ size: '{_field_} size must be less than {size}KB',
+ },
+ },
+ });
+ },
+};
diff --git a/frontend/src/validators/ValidatorFactory.js b/dashboard/src/validators/ValidatorFactory.js
similarity index 80%
rename from frontend/src/validators/ValidatorFactory.js
rename to dashboard/src/validators/ValidatorFactory.js
index e687b0c0..2846e84c 100644
--- a/frontend/src/validators/ValidatorFactory.js
+++ b/dashboard/src/validators/ValidatorFactory.js
@@ -18,7 +18,7 @@ import NumConvertUtil from './NumConvertUtil';
export default {
newCharLengthValidator(maxLength) {
return {
- getMessage: field => `${field} cannot exceed ${maxLength} characters.`,
+ message: (field) => `${field} cannot exceed ${maxLength} characters.`,
validate(value) {
if (value.length > NumConvertUtil.toInt(maxLength)) {
return false;
@@ -29,7 +29,7 @@ export default {
},
newCharMinLengthValidator(maxLength) {
return {
- getMessage: field => `${field} cannot be less than ${maxLength} characters.`,
+ message: (field) => `${field} cannot be less than ${maxLength} characters.`,
validate(value) {
if (value.length < NumConvertUtil.toInt(maxLength)) {
return false;
@@ -40,7 +40,7 @@ export default {
},
newMaxNumValidator(maxNum) {
return {
- getMessage: field => `${field} cannot exceed ${maxNum}.`,
+ message: (field) => `${field} cannot exceed ${maxNum}.`,
validate(value) {
if (NumConvertUtil.toInt(value) > NumConvertUtil.toInt(maxNum)) {
return false;
@@ -51,13 +51,11 @@ export default {
},
newUserObjNoSpacesValidatorInNonPkiMode(isPkiMode) {
return {
- getMessage: field => `The ${field} field may not contain spaces`,
+ message: (field) => `${field} may not contain spaces`,
validate(value) {
if (isPkiMode || !value.userId) {
return true;
}
- // const isValid = !value.userId.match(/^[0-9a-zA-Z]+$/);
- // return !isValid;
const hasSpaces = value.userId.indexOf(' ') >= 0;
return !hasSpaces;
},
diff --git a/frontend/test/e2e/custom-assertions/elementCount.js b/dashboard/test/e2e/custom-assertions/elementCount.js
similarity index 100%
rename from frontend/test/e2e/custom-assertions/elementCount.js
rename to dashboard/test/e2e/custom-assertions/elementCount.js
diff --git a/frontend/test/e2e/nightwatch.conf.js b/dashboard/test/e2e/nightwatch.conf.js
similarity index 100%
rename from frontend/test/e2e/nightwatch.conf.js
rename to dashboard/test/e2e/nightwatch.conf.js
diff --git a/frontend/test/e2e/runner.js b/dashboard/test/e2e/runner.js
similarity index 100%
rename from frontend/test/e2e/runner.js
rename to dashboard/test/e2e/runner.js
diff --git a/frontend/test/e2e/specs/test.js b/dashboard/test/e2e/specs/test.js
similarity index 100%
rename from frontend/test/e2e/specs/test.js
rename to dashboard/test/e2e/specs/test.js
diff --git a/frontend/test/unit/.eslintrc b/dashboard/test/unit/.eslintrc
similarity index 100%
rename from frontend/test/unit/.eslintrc
rename to dashboard/test/unit/.eslintrc
diff --git a/frontend/test/unit/jest.conf.js b/dashboard/test/unit/jest.conf.js
similarity index 100%
rename from frontend/test/unit/jest.conf.js
rename to dashboard/test/unit/jest.conf.js
diff --git a/frontend/test/unit/setup.js b/dashboard/test/unit/setup.js
similarity index 100%
rename from frontend/test/unit/setup.js
rename to dashboard/test/unit/setup.js
diff --git a/frontend/test/unit/specs/HelloWorld.spec.js b/dashboard/test/unit/specs/HelloWorld.spec.js
similarity index 100%
rename from frontend/test/unit/specs/HelloWorld.spec.js
rename to dashboard/test/unit/specs/HelloWorld.spec.js
diff --git a/frontend/vue.config.js b/dashboard/vue.config.js
similarity index 95%
rename from frontend/vue.config.js
rename to dashboard/vue.config.js
index 9fab002e..89a00578 100644
--- a/frontend/vue.config.js
+++ b/dashboard/vue.config.js
@@ -66,6 +66,7 @@ module.exports = {
'/logout': proxyConf,
'/createAccount': proxyConf,
'/grantFirstRoot': proxyConf,
+ '/createRootAccount': proxyConf,
'/oauth2': proxyConf,
'/login': proxyConf,
'/static': proxyConf,
@@ -75,6 +76,9 @@ module.exports = {
'/public': proxyConf,
'/metrics' : proxyConf,
'/skills-websocket' : proxyConf,
+ '/resetPassword' : proxyConf,
+ '/performPasswordReset' : proxyConf,
+ '/isFeatureSupported' : proxyConf,
},
},
configureWebpack: {
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 00000000..872f2ebf
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,43 @@
+FROM openjdk:14.0.2-slim-buster
+
+# install netcat so start script can use nc command to wait for DB to come up
+RUN apt-get update
+RUN apt-get -y install netcat-openbsd
+
+ARG BUILD_DATE
+ARG VERSION
+ARG VCS_REF=unspecified
+
+LABEL org.label-schema.build-date=$BUILD_DATE
+LABEL org.label-schema.license=Apache-2.0
+LABEL org.label-schema.name=SkillTree
+LABEL org.label-schema.schema-version=$VERSION
+LABEL org.label-schema.url=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.label-schema.usage=https://code.nsa.gov/skills-docs/
+LABEL org.label-schema.vcs-ref=$VCS_REF
+LABEL org.label-schema.vendor=SkillTree
+LABEL org.label-schema.vcs-url=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.label-schema.vendor=SkillTree
+LABEL org.label-schema.version=7.8.0
+
+LABEL org.opencontainers.image.created=$BUILD_DATE
+LABEL org.opencontainers.image.documentation=https://code.nsa.gov/skills-docs/
+LABEL org.opencontainers.image.licenses=Apache-2.0
+LABEL org.opencontainers.image.revision=$VCS_REF
+LABEL org.opencontainers.image.source=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.opencontainers.image.title=SkillTree
+LABEL org.opencontainers.image.url=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.opencontainers.image.vendor=SkillTree
+LABEL org.opencontainers.image.version=$VERSION
+
+VOLUME /tmp
+
+EXPOSE 80
+EXPOSE 8443
+
+RUN mkdir /data
+
+COPY skills-service.jar skills.jar
+COPY startup.sh startup.sh
+
+ENTRYPOINT ["sh", "/startup.sh"]
diff --git a/docker/build-and-push.sh b/docker/build-and-push.sh
new file mode 100755
index 00000000..4b8b5ebc
--- /dev/null
+++ b/docker/build-and-push.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# exit if a command returns non-zero exit code
+set -e
+
+IMG_NAME=${1:-"skilltree/skills-service"}
+DOCKER_USER=${2:-"${docker_username}"}
+DOCKER_PASS=${3:-"${docker_password}"}
+
+./build-docker-image.sh $IMG_NAME
+
+docker login --username "${DOCKER_USER}" --password "${DOCKER_PASS}"
+docker push $IMG_NAME
diff --git a/docker/build-docker-image.sh b/docker/build-docker-image.sh
new file mode 100755
index 00000000..5f09e312
--- /dev/null
+++ b/docker/build-docker-image.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+# exit if a command returns non-zero exit code
+set -e
+echo "Building docker image..."
+
+BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
+BUILD_DATE_TAG=$(date -u +'%Y%m%dT%H%M%SZ')
+VERSION=$(cd ../ && mvn org.apache.maven.plugins:maven-help-plugin:3.1.0:evaluate -Dexpression=project.version -q -DforceStdout)
+JAR_LOC=$(ls ../service/target/*.jar)
+IMG_NAME=${1:-"skilltree/skills-service"}
+if [ "$JAR_LOC" = "" ]
+then
+ echo "Failed to find jar"
+ exit
+fi
+rm -f skills-service.jar
+cp $JAR_LOC skills-service.jar
+
+IMAGE_TAG="${VERSION}"
+if [[ "$VERSION" == *SNAPSHOT ]]
+then
+ IMAGE_TAG="${VERSION}_${BUILD_DATE_TAG}"
+fi
+
+echo "-------------------"
+echo "BUILD_DATE=[$BUILD_DATE]"
+echo "VERSION=[$VERSION]"
+echo "JAR_LOC=[$JAR_LOC]"
+echo "VCS_REF=[$VCS_REF]"
+echo "IMG_NAME=[$IMG_NAME]"
+echo "IMAGE_TAG=[$IMAGE_TAG]"
+echo "-------------------"
+
+docker build --no-cache=true --build-arg BUILD_DATE=$BUILD_DATE --build-arg VERSION=$VERSION --build-arg VCS_REF=$GITHUB_SHA -t $IMG_NAME -t "${IMG_NAME}:${IMAGE_TAG}" .
diff --git a/docker/skills-stomp-broker/Dockerfile b/docker/skills-stomp-broker/Dockerfile
new file mode 100644
index 00000000..aefbace8
--- /dev/null
+++ b/docker/skills-stomp-broker/Dockerfile
@@ -0,0 +1,26 @@
+FROM rabbitmq:3.8-management-alpine
+
+ADD join.sh /
+COPY enabled_plugins /etc/rabbitmq/enabled_plugins
+
+RUN apk add --no-cache bind-tools
+
+RUN sed -i 's/exec "$@"/\
+ sh -c "while ! nc -z localhost 15672; do sleep 0.1; done; sleep 3; .\/join.sh" \&\
+ \nexec "$@"/' /usr/local/bin/docker-entrypoint.sh
+
+LABEL org.label-schema.license=Apache-2.0
+LABEL org.label-schema.name=SkillTree
+LABEL org.label-schema.url=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.label-schema.usage=https://code.nsa.gov/skills-docs/
+LABEL org.label-schema.vendor=SkillTree
+LABEL org.label-schema.vcs-url=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.label-schema.vendor=SkillTree
+LABEL org.label-schema.version=7.8.0
+
+LABEL org.opencontainers.image.documentation=https://code.nsa.gov/skills-docs/
+LABEL org.opencontainers.image.licenses=Apache-2.0
+LABEL org.opencontainers.image.source=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.opencontainers.image.title=SkillTree
+LABEL org.opencontainers.image.url=https://github.com/NationalSecurityAgency/skills-service
+LABEL org.opencontainers.image.vendor=SkillTree
\ No newline at end of file
diff --git a/docker/skills-stomp-broker/enabled_plugins b/docker/skills-stomp-broker/enabled_plugins
new file mode 100644
index 00000000..33953aef
--- /dev/null
+++ b/docker/skills-stomp-broker/enabled_plugins
@@ -0,0 +1 @@
+[rabbitmq_federation_management,rabbitmq_management,rabbitmq_stomp].
diff --git a/docker/skills-stomp-broker/join.sh b/docker/skills-stomp-broker/join.sh
new file mode 100755
index 00000000..2f1f1d34
--- /dev/null
+++ b/docker/skills-stomp-broker/join.sh
@@ -0,0 +1,112 @@
+#!/bin/bash
+#
+# Script to join rabbitmq cluster.
+#
+# This script is call in background once the mangement ui of this node is
+# running (see Dockerfile).
+#
+#
+
+# Wait a random amount of seconds (between 1 and 10 seconds) to get in
+# parallel started instances a littel bit out of sync.
+echo "Wait random duration..."
+sleep $[ ( $RANDOM % 10 ) + 10 ]s
+
+echo "Try to join rabbitmq cluster..."
+
+# busybox 'nslookup' required
+# Try to determine all hosts of this service (given by env var `SERVICE_NAME`).
+# Sometimes wrong hostnames are returned, hence the retry functionality.
+for i in `seq 5`
+do
+ if [[ "$i" > "5" ]]
+ then
+ echo "Retry count exceeded"
+ exit 1
+ fi
+ retry=false
+# nodes=`nslookup tasks.$SERVICE_NAME 2>/dev/null | grep -v $(hostname) | grep Address | awk '{print $4}' | cut -d. -f1-3`
+ nodes=`dig +short tasks.$SERVICE_NAME | xargs -r -n 1 dig +short -x | grep -v $(hostname) | cut -d. -f1-3`
+ for node in $nodes
+ do
+ if [[ "$node" != $SERVICE_NAME* ]]
+ then
+ retry=true
+ break
+ fi
+ done
+ if ! $retry; then break; fi
+done
+
+# If the service is configured with just one replica this rabbitmq instance is
+# running in standalone mode and no further cluster joining arithmetic is
+# required. If there are multiple nodes configured stop the app to start
+# setting up a cluster.
+echo
+if [[ ${#nodes} > 0 ]]
+then
+ echo "Found nodes of cluster:"
+ echo $nodes
+ rabbitmqctl stop_app
+ rabbitmqctl reset
+else
+ echo "Found standalone setup."
+ exit 0
+fi
+echo
+
+# Join cluster by trying one node after each other. If successfully joined, start the rabbitmq
+# app again
+while true
+do
+ for node in $nodes
+ do
+ # manually force a start by setting the env variable FORCE_START
+ if [[ -f /tmp/FORCE_START ]]
+ then
+ echo "Startup forced manually."
+ echo
+ rabbitmqctl start_app
+ exit 0
+ fi
+ # of peer is reachable try to join the cluster of that host
+ echo "Try to reach $node"
+ if nc -z "$node" 15672
+ then
+ rabbitmqctl join_cluster rabbit@$node
+ if [[ $? == "0" ]]
+ then
+ echo
+ echo "Start app after joining cluster"
+
+ rabbitmqctl start_app
+
+ echo
+ echo "Try to cleanup old nodes of same slot..."
+ for n in `rabbitmqctl cluster_status | awk '/disc/,/]}]}/' | grep -o "$SERVICE_NAME[^']*"`
+ do
+ if [[ $n == "$SERVICE_NAME.$SLOT."* && $n != $HOSTNAME ]]
+ then
+ echo
+ echo "removing node $n from cluster"
+ rabbitmqctl forget_cluster_node rabbit@$n
+ fi
+ done
+ echo
+ echo "Successfully joined cluster"
+ exit 0
+ fi
+ elif [[ "$SLOT" == "$MASTER_SLOT" ]]
+ then
+ echo "Startup due to claimed master role on slot $MASTER_SLOT."
+ echo
+ rabbitmqctl start_app
+ exit 0
+ fi
+ done
+
+ sleep 10
+done
+
+echo "Failed to join cluster."
+exit 1
diff --git a/docker/startup.sh b/docker/startup.sh
new file mode 100755
index 00000000..feb34ac6
--- /dev/null
+++ b/docker/startup.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+echo "Starting Skills Service"
+JAVA_OPTS="${JAVA_OPTS} -Dlogging.file=/logs/webapp.log"
+
+if [ ! -z "${EXTRA_JAVA_OPTS}" ]
+then
+ JAVA_OPTS="${EXTRA_JAVA_OPTS} ${JAVA_OPTS}"
+ echo "Added EXTRA_JAVA_OPTS to JAVA_OPTS = [$EXTRA_JAVA_OPTS]"
+fi
+
+echo "JAVA_OPTS=${JAVA_OPTS}"
+echo -e "SPRING_PROPS=${SPRING_PROPS}"
+
+# support both \n and , as a prop separator
+echo -e $SPRING_PROPS | sed -r 's$([^\])[,]\s?$\1\n$g; s$\\,$,$g' >> application.properties
+
+pid=0
+term_handler() {
+ echo "SIGTERM handler was called"
+ if [ $pid -ne 0 ]; then
+ echo "exec: kill -SIGTERM $pid"
+ kill -SIGTERM "$pid"
+ echo "exec: wait $pid"
+ wait "$pid"
+ fi
+ exit 143; # 128 + 15 -- SIGTERM
+}
+trap term_handler SIGTERM
+
+java ${DEBUG_OPTS} ${JAVA_OPTS} -jar skills.jar &
+pid="$!"
+
+# wait forever
+while true
+do
+ tail -f /dev/null & wait ${!}
+done
diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore
index 806acc7f..9caf9567 100644
--- a/e2e-tests/.gitignore
+++ b/e2e-tests/.gitignore
@@ -1,6 +1,7 @@
.DS_Store
/node_modules/
/dist/
+/logs/
npm-debug.log*
yarn-debug.log*
yarn-errpr.log*
@@ -11,6 +12,7 @@ selenium-debug.log
# Editor directories and files
.idea
.vscode
+.history
*.suo
*.ntvs*
*.njsproj
diff --git a/e2e-tests/cypress.json b/e2e-tests/cypress.json
index f302dd39..d6abff94 100644
--- a/e2e-tests/cypress.json
+++ b/e2e-tests/cypress.json
@@ -1,4 +1,10 @@
{
- "projectId": "skillstests1",
- "baseUrl": "http://localhost:8080"
+ "projectId": "7kivjf",
+ "baseUrl": "http://localhost:8080",
+ "requestTimeout": 10000,
+ "defaultCommandTimeout": 10000,
+ "retries": {
+ "runMode": 2,
+ "openMode": 0
+ }
}
diff --git a/e2e-tests/cypress/db/clear.sql b/e2e-tests/cypress/db/clear.sql
new file mode 100644
index 00000000..aa06cdfc
--- /dev/null
+++ b/e2e-tests/cypress/db/clear.sql
@@ -0,0 +1,21 @@
+-- Copyright 2020 SkillTree
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+-- creating Inception project is expensive so lets not delete it
+delete from PROJECT_DEFINITION;
+delete from USER_ATTRS;
+delete from USER_ROLES;
+delete from USER_ROLES;
+delete from GLOBAL_BADGE_LEVEL_DEFINITION;
+delete from SKILL_DEFINITION;
+delete from SETTINGS;
diff --git a/e2e-tests/cypress/db/dropTables.sql b/e2e-tests/cypress/db/dropTables.sql
new file mode 100644
index 00000000..3f6f1061
--- /dev/null
+++ b/e2e-tests/cypress/db/dropTables.sql
@@ -0,0 +1,49 @@
+-- Copyright 2020 SkillTree
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+-- creating Inception project is expensive so lets not delete it
+drop table databasechangeloglock;
+
+drop table databasechangelog;
+
+drop table skills_db_locks;
+
+drop table skill_relationship_definition;
+
+drop table skill_share_definition;
+
+drop table user_roles;
+
+drop table user_performed_skill;
+
+drop table user_points;
+
+drop table user_achievement;
+
+drop table settings;
+
+drop table global_badge_level_definition;
+
+drop table level_definition;
+
+drop table skill_definition;
+
+drop table custom_icons;
+
+drop table project_definition;
+
+drop table password_reset_token;
+
+drop table users;
+
+drop table user_attrs;
diff --git a/e2e-tests/cypress/db/reset.sql b/e2e-tests/cypress/db/reset.sql
index 9329e447..7c033c79 100644
--- a/e2e-tests/cypress/db/reset.sql
+++ b/e2e-tests/cypress/db/reset.sql
@@ -18,3 +18,4 @@ delete from USER_ROLES where USER_ID = 'skills@skills.org' and ROLE_NAME = 'ROLE
delete from USER_ROLES where USER_ID = 'root@skills.org' and ROLE_NAME = 'ROLE_SUPERVISOR';
delete from GLOBAL_BADGE_LEVEL_DEFINITION;
delete from SKILL_DEFINITION where PROJECT_ID is null;
+delete from SETTINGS;
diff --git a/e2e-tests/cypress/fonts/Type1/.uuid b/e2e-tests/cypress/fonts/Type1/.uuid
new file mode 100644
index 00000000..003633d4
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/.uuid
@@ -0,0 +1 @@
+8fbe70ce-396c-462a-88a5-77af70944b2e
\ No newline at end of file
diff --git a/e2e-tests/cypress/fonts/Type1/UTBI____.afm b/e2e-tests/cypress/fonts/Type1/UTBI____.afm
new file mode 100644
index 00000000..4af327dc
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTBI____.afm
@@ -0,0 +1,1017 @@
+StartFontMetrics 2.0
+Comment Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.
+Comment Creation Date: Wed Oct 2 18:46:03 1991
+Comment UniqueID 36546
+Comment VMusage 34429 41321
+FontName Utopia-BoldItalic
+FullName Utopia Bold Italic
+FamilyName Utopia
+Weight Bold
+ItalicAngle -13
+IsFixedPitch false
+FontBBox -141 -250 1297 916
+UnderlinePosition -100
+UnderlineThickness 50
+Version 001.001
+Notice Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.
+EncodingScheme AdobeStandardEncoding
+CapHeight 692
+XHeight 502
+Ascender 742
+Descender -242
+StartCharMetrics 228
+C 32 ; WX 210 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 285 ; N exclam ; B 70 -12 371 707 ;
+C 34 ; WX 455 ; N quotedbl ; B 177 407 531 707 ;
+C 35 ; WX 560 ; N numbersign ; B 72 0 641 668 ;
+C 36 ; WX 560 ; N dollar ; B 67 -104 623 748 ;
+C 37 ; WX 896 ; N percent ; B 141 -31 896 702 ;
+C 38 ; WX 752 ; N ampersand ; B 97 -12 771 680 ;
+C 39 ; WX 246 ; N quoteright ; B 130 387 329 707 ;
+C 40 ; WX 350 ; N parenleft ; B 122 -135 473 699 ;
+C 41 ; WX 350 ; N parenright ; B 3 -135 354 699 ;
+C 42 ; WX 500 ; N asterisk ; B 156 315 563 707 ;
+C 43 ; WX 600 ; N plus ; B 118 0 602 490 ;
+C 44 ; WX 280 ; N comma ; B 26 -167 242 180 ;
+C 45 ; WX 392 ; N hyphen ; B 106 203 389 298 ;
+C 46 ; WX 280 ; N period ; B 67 -12 247 166 ;
+C 47 ; WX 260 ; N slash ; B 19 -15 405 707 ;
+C 48 ; WX 560 ; N zero ; B 92 -12 618 680 ;
+C 49 ; WX 560 ; N one ; B 107 0 505 680 ;
+C 50 ; WX 560 ; N two ; B 39 0 613 680 ;
+C 51 ; WX 560 ; N three ; B 56 -12 602 680 ;
+C 52 ; WX 560 ; N four ; B 63 0 592 668 ;
+C 53 ; WX 560 ; N five ; B 58 -12 628 668 ;
+C 54 ; WX 560 ; N six ; B 91 -12 621 680 ;
+C 55 ; WX 560 ; N seven ; B 147 -12 667 668 ;
+C 56 ; WX 560 ; N eight ; B 72 -12 619 680 ;
+C 57 ; WX 560 ; N nine ; B 83 -12 605 680 ;
+C 58 ; WX 280 ; N colon ; B 67 -12 315 490 ;
+C 59 ; WX 280 ; N semicolon ; B 26 -167 315 490 ;
+C 60 ; WX 600 ; N less ; B 101 5 579 495 ;
+C 61 ; WX 600 ; N equal ; B 118 103 602 397 ;
+C 62 ; WX 600 ; N greater ; B 121 5 599 495 ;
+C 63 ; WX 454 ; N question ; B 150 -12 550 707 ;
+C 64 ; WX 828 ; N at ; B 125 -15 877 707 ;
+C 65 ; WX 634 ; N A ; B -24 0 674 692 ;
+C 66 ; WX 680 ; N B ; B 40 0 724 692 ;
+C 67 ; WX 672 ; N C ; B 111 -15 777 707 ;
+C 68 ; WX 774 ; N D ; B 40 0 819 692 ;
+C 69 ; WX 622 ; N E ; B 40 0 722 692 ;
+C 70 ; WX 585 ; N F ; B 40 0 718 692 ;
+C 71 ; WX 726 ; N G ; B 111 -15 791 707 ;
+C 72 ; WX 800 ; N H ; B 40 0 915 692 ;
+C 73 ; WX 386 ; N I ; B 40 0 501 692 ;
+C 74 ; WX 388 ; N J ; B -15 -114 512 692 ;
+C 75 ; WX 688 ; N K ; B 40 -6 858 692 ;
+C 76 ; WX 586 ; N L ; B 40 0 626 692 ;
+C 77 ; WX 921 ; N M ; B 35 0 1033 692 ;
+C 78 ; WX 741 ; N N ; B 30 0 873 692 ;
+C 79 ; WX 761 ; N O ; B 113 -15 803 707 ;
+C 80 ; WX 660 ; N P ; B 40 0 729 692 ;
+C 81 ; WX 761 ; N Q ; B 113 -193 803 707 ;
+C 82 ; WX 681 ; N R ; B 40 0 731 692 ;
+C 83 ; WX 551 ; N S ; B 66 -15 605 707 ;
+C 84 ; WX 616 ; N T ; B 126 0 757 692 ;
+C 85 ; WX 776 ; N U ; B 150 -15 902 692 ;
+C 86 ; WX 630 ; N V ; B 127 0 818 692 ;
+C 87 ; WX 920 ; N W ; B 115 0 1097 692 ;
+C 88 ; WX 630 ; N X ; B -21 0 779 692 ;
+C 89 ; WX 622 ; N Y ; B 127 0 800 692 ;
+C 90 ; WX 618 ; N Z ; B 5 0 749 692 ;
+C 91 ; WX 350 ; N bracketleft ; B 91 -128 463 692 ;
+C 92 ; WX 460 ; N backslash ; B 149 -15 460 707 ;
+C 93 ; WX 350 ; N bracketright ; B 13 -128 385 692 ;
+C 94 ; WX 600 ; N asciicircum ; B 114 215 602 668 ;
+C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ;
+C 96 ; WX 246 ; N quoteleft ; B 149 399 348 719 ;
+C 97 ; WX 596 ; N a ; B 61 -12 647 502 ;
+C 98 ; WX 586 ; N b ; B 69 -12 627 742 ;
+C 99 ; WX 456 ; N c ; B 73 -12 533 502 ;
+C 100 ; WX 609 ; N d ; B 64 -12 686 742 ;
+C 101 ; WX 476 ; N e ; B 73 -12 532 502 ;
+C 102 ; WX 348 ; N f ; B -94 -242 588 742 ; L i fi ; L l fl ;
+C 103 ; WX 522 ; N g ; B 21 -242 644 512 ;
+C 104 ; WX 629 ; N h ; B 79 -12 666 742 ;
+C 105 ; WX 339 ; N i ; B 101 -12 392 720 ;
+C 106 ; WX 333 ; N j ; B -85 -242 399 720 ;
+C 107 ; WX 570 ; N k ; B 74 -12 639 742 ;
+C 108 ; WX 327 ; N l ; B 97 -12 395 742 ;
+C 109 ; WX 914 ; N m ; B 81 -12 952 502 ;
+C 110 ; WX 635 ; N n ; B 80 -12 674 502 ;
+C 111 ; WX 562 ; N o ; B 77 -12 591 502 ;
+C 112 ; WX 606 ; N p ; B 35 -242 648 502 ;
+C 113 ; WX 584 ; N q ; B 64 -242 639 513 ;
+C 114 ; WX 440 ; N r ; B 86 -12 532 502 ;
+C 115 ; WX 417 ; N s ; B 45 -12 467 502 ;
+C 116 ; WX 359 ; N t ; B 103 -12 463 641 ;
+C 117 ; WX 634 ; N u ; B 106 -12 678 502 ;
+C 118 ; WX 518 ; N v ; B 103 -12 582 502 ;
+C 119 ; WX 795 ; N w ; B 105 -12 861 502 ;
+C 120 ; WX 516 ; N x ; B 9 -12 581 502 ;
+C 121 ; WX 489 ; N y ; B -14 -242 567 502 ;
+C 122 ; WX 466 ; N z ; B 18 -12 541 490 ;
+C 123 ; WX 340 ; N braceleft ; B 125 -128 474 692 ;
+C 124 ; WX 265 ; N bar ; B 152 -250 256 750 ;
+C 125 ; WX 340 ; N braceright ; B -7 -128 342 692 ;
+C 126 ; WX 600 ; N asciitilde ; B 105 157 606 338 ;
+C 161 ; WX 285 ; N exclamdown ; B 22 -217 323 502 ;
+C 162 ; WX 560 ; N cent ; B 115 -21 646 668 ;
+C 163 ; WX 560 ; N sterling ; B 31 0 618 679 ;
+C 164 ; WX 100 ; N fraction ; B -141 -27 405 695 ;
+C 165 ; WX 560 ; N yen ; B 100 0 711 668 ;
+C 166 ; WX 560 ; N florin ; B 19 -135 670 691 ;
+C 167 ; WX 568 ; N section ; B 99 -115 594 707 ;
+C 168 ; WX 560 ; N currency ; B 95 73 613 596 ;
+C 169 ; WX 246 ; N quotesingle ; B 169 376 320 707 ;
+C 170 ; WX 455 ; N quotedblleft ; B 149 399 557 719 ;
+C 171 ; WX 560 ; N guillemotleft ; B 125 37 568 464 ;
+C 172 ; WX 360 ; N guilsinglleft ; B 125 37 368 464 ;
+C 173 ; WX 360 ; N guilsinglright ; B 93 37 336 464 ;
+C 174 ; WX 651 ; N fi ; B -94 -242 690 742 ;
+C 175 ; WX 652 ; N fl ; B -94 -242 720 742 ;
+C 177 ; WX 500 ; N endash ; B 47 209 566 292 ;
+C 178 ; WX 514 ; N dagger ; B 136 -125 580 707 ;
+C 179 ; WX 490 ; N daggerdbl ; B 67 -119 563 707 ;
+C 180 ; WX 280 ; N periodcentered ; B 102 161 282 339 ;
+C 182 ; WX 580 ; N paragraph ; B 145 -101 688 692 ;
+C 183 ; WX 465 ; N bullet ; B 134 174 489 529 ;
+C 184 ; WX 246 ; N quotesinglbase ; B 18 -153 217 167 ;
+C 185 ; WX 455 ; N quotedblbase ; B 18 -153 426 167 ;
+C 186 ; WX 455 ; N quotedblright ; B 130 387 538 707 ;
+C 187 ; WX 560 ; N guillemotright ; B 93 37 537 464 ;
+C 188 ; WX 1000 ; N ellipsis ; B 120 -12 966 166 ;
+C 189 ; WX 1297 ; N perthousand ; B 141 -31 1297 702 ;
+C 191 ; WX 454 ; N questiondown ; B 25 -217 426 502 ;
+C 193 ; WX 400 ; N grave ; B 144 511 416 740 ;
+C 194 ; WX 400 ; N acute ; B 221 511 493 740 ;
+C 195 ; WX 400 ; N circumflex ; B 128 520 506 747 ;
+C 196 ; WX 400 ; N tilde ; B 129 549 537 697 ;
+C 197 ; WX 400 ; N macron ; B 168 592 494 664 ;
+C 198 ; WX 400 ; N breve ; B 181 556 504 714 ;
+C 199 ; WX 402 ; N dotaccent ; B 255 561 413 710 ;
+C 200 ; WX 400 ; N dieresis ; B 141 561 539 710 ;
+C 202 ; WX 400 ; N ring ; B 201 529 458 762 ;
+C 203 ; WX 400 ; N cedilla ; B 120 -246 327 0 ;
+C 205 ; WX 400 ; N hungarumlaut ; B 193 546 517 750 ;
+C 206 ; WX 350 ; N ogonek ; B 73 -246 288 0 ;
+C 207 ; WX 400 ; N caron ; B 165 520 543 747 ;
+C 208 ; WX 1000 ; N emdash ; B 47 209 1066 292 ;
+C 225 ; WX 890 ; N AE ; B -72 0 993 692 ;
+C 227 ; WX 444 ; N ordfeminine ; B 97 265 517 590 ;
+C 232 ; WX 592 ; N Lslash ; B 46 0 632 692 ;
+C 233 ; WX 761 ; N Oslash ; B 112 -51 804 734 ;
+C 234 ; WX 1016 ; N OE ; B 111 0 1119 692 ;
+C 235 ; WX 412 ; N ordmasculine ; B 121 265 481 590 ;
+C 241 ; WX 789 ; N ae ; B 61 -12 845 509 ;
+C 245 ; WX 339 ; N dotlessi ; B 101 -12 378 502 ;
+C 248 ; WX 339 ; N lslash ; B 53 -12 455 742 ;
+C 249 ; WX 562 ; N oslash ; B 77 -69 591 549 ;
+C 250 ; WX 811 ; N oe ; B 77 -12 867 502 ;
+C 251 ; WX 628 ; N germandbls ; B -94 -242 727 742 ;
+C -1 ; WX 402 ; N onesuperior ; B 119 272 396 680 ;
+C -1 ; WX 600 ; N minus ; B 118 210 602 290 ;
+C -1 ; WX 375 ; N degree ; B 128 360 460 680 ;
+C -1 ; WX 562 ; N oacute ; B 77 -12 591 740 ;
+C -1 ; WX 761 ; N Odieresis ; B 113 -15 803 881 ;
+C -1 ; WX 562 ; N odieresis ; B 77 -12 620 710 ;
+C -1 ; WX 622 ; N Eacute ; B 40 0 722 904 ;
+C -1 ; WX 634 ; N ucircumflex ; B 106 -12 678 747 ;
+C -1 ; WX 940 ; N onequarter ; B 139 -27 884 695 ;
+C -1 ; WX 600 ; N logicalnot ; B 118 95 602 397 ;
+C -1 ; WX 622 ; N Ecircumflex ; B 40 0 722 905 ;
+C -1 ; WX 940 ; N onehalf ; B 125 -27 933 695 ;
+C -1 ; WX 761 ; N Otilde ; B 113 -15 803 876 ;
+C -1 ; WX 634 ; N uacute ; B 106 -12 678 740 ;
+C -1 ; WX 476 ; N eacute ; B 73 -12 566 740 ;
+C -1 ; WX 339 ; N iacute ; B 101 -12 443 740 ;
+C -1 ; WX 622 ; N Egrave ; B 40 0 722 904 ;
+C -1 ; WX 339 ; N icircumflex ; B 73 -12 451 747 ;
+C -1 ; WX 634 ; N mu ; B 32 -230 678 502 ;
+C -1 ; WX 265 ; N brokenbar ; B 152 -175 256 675 ;
+C -1 ; WX 600 ; N thorn ; B 29 -242 642 700 ;
+C -1 ; WX 634 ; N Aring ; B -24 0 674 879 ;
+C -1 ; WX 489 ; N yacute ; B -14 -242 567 740 ;
+C -1 ; WX 622 ; N Ydieresis ; B 127 0 800 881 ;
+C -1 ; WX 1100 ; N trademark ; B 138 277 1128 692 ;
+C -1 ; WX 824 ; N registered ; B 126 -15 854 707 ;
+C -1 ; WX 562 ; N ocircumflex ; B 77 -12 591 747 ;
+C -1 ; WX 634 ; N Agrave ; B -24 0 674 904 ;
+C -1 ; WX 551 ; N Scaron ; B 66 -15 647 916 ;
+C -1 ; WX 776 ; N Ugrave ; B 150 -15 902 904 ;
+C -1 ; WX 622 ; N Edieresis ; B 40 0 722 881 ;
+C -1 ; WX 776 ; N Uacute ; B 150 -15 902 904 ;
+C -1 ; WX 562 ; N otilde ; B 77 -12 618 697 ;
+C -1 ; WX 635 ; N ntilde ; B 80 -12 674 697 ;
+C -1 ; WX 489 ; N ydieresis ; B -14 -242 567 710 ;
+C -1 ; WX 634 ; N Aacute ; B -24 0 674 904 ;
+C -1 ; WX 562 ; N eth ; B 77 -12 593 742 ;
+C -1 ; WX 596 ; N acircumflex ; B 61 -12 647 747 ;
+C -1 ; WX 596 ; N aring ; B 61 -12 647 762 ;
+C -1 ; WX 761 ; N Ograve ; B 113 -15 803 904 ;
+C -1 ; WX 456 ; N ccedilla ; B 73 -246 533 502 ;
+C -1 ; WX 600 ; N multiply ; B 145 22 595 478 ;
+C -1 ; WX 600 ; N divide ; B 98 7 582 493 ;
+C -1 ; WX 402 ; N twosuperior ; B 64 272 458 680 ;
+C -1 ; WX 741 ; N Ntilde ; B 30 0 873 876 ;
+C -1 ; WX 634 ; N ugrave ; B 106 -12 678 740 ;
+C -1 ; WX 776 ; N Ucircumflex ; B 150 -15 902 905 ;
+C -1 ; WX 634 ; N Atilde ; B -24 0 697 876 ;
+C -1 ; WX 466 ; N zcaron ; B 18 -12 561 747 ;
+C -1 ; WX 339 ; N idieresis ; B 81 -12 479 710 ;
+C -1 ; WX 634 ; N Acircumflex ; B -24 0 674 905 ;
+C -1 ; WX 386 ; N Icircumflex ; B 40 0 541 905 ;
+C -1 ; WX 622 ; N Yacute ; B 127 0 800 904 ;
+C -1 ; WX 761 ; N Oacute ; B 113 -15 803 904 ;
+C -1 ; WX 634 ; N Adieresis ; B -24 0 687 881 ;
+C -1 ; WX 618 ; N Zcaron ; B 5 0 749 916 ;
+C -1 ; WX 596 ; N agrave ; B 61 -12 647 740 ;
+C -1 ; WX 402 ; N threesuperior ; B 94 265 456 680 ;
+C -1 ; WX 562 ; N ograve ; B 77 -12 591 740 ;
+C -1 ; WX 940 ; N threequarters ; B 130 -27 911 695 ;
+C -1 ; WX 780 ; N Eth ; B 46 0 825 692 ;
+C -1 ; WX 600 ; N plusminus ; B 118 0 602 549 ;
+C -1 ; WX 634 ; N udieresis ; B 106 -12 678 710 ;
+C -1 ; WX 476 ; N edieresis ; B 73 -12 577 710 ;
+C -1 ; WX 596 ; N aacute ; B 61 -12 647 740 ;
+C -1 ; WX 339 ; N igrave ; B 101 -12 378 740 ;
+C -1 ; WX 386 ; N Idieresis ; B 40 0 568 881 ;
+C -1 ; WX 596 ; N adieresis ; B 61 -12 647 710 ;
+C -1 ; WX 386 ; N Iacute ; B 40 0 534 904 ;
+C -1 ; WX 824 ; N copyright ; B 126 -15 854 707 ;
+C -1 ; WX 386 ; N Igrave ; B 40 0 501 904 ;
+C -1 ; WX 672 ; N Ccedilla ; B 111 -246 777 707 ;
+C -1 ; WX 417 ; N scaron ; B 45 -12 557 747 ;
+C -1 ; WX 476 ; N egrave ; B 73 -12 532 740 ;
+C -1 ; WX 761 ; N Ocircumflex ; B 113 -15 803 905 ;
+C -1 ; WX 629 ; N Thorn ; B 40 0 695 692 ;
+C -1 ; WX 596 ; N atilde ; B 61 -12 647 697 ;
+C -1 ; WX 776 ; N Udieresis ; B 150 -15 902 881 ;
+C -1 ; WX 476 ; N ecircumflex ; B 73 -12 559 747 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 697
+
+KPX A z 18
+KPX A y -40
+KPX A x 16
+KPX A w -30
+KPX A v -30
+KPX A u -18
+KPX A t -6
+KPX A s 6
+KPX A r -6
+KPX A quoteright -92
+KPX A quotedblright -92
+KPX A p -6
+KPX A o -18
+KPX A n -12
+KPX A m -12
+KPX A l -18
+KPX A h -6
+KPX A d 4
+KPX A c -6
+KPX A b -6
+KPX A a 10
+KPX A Y -56
+KPX A X -8
+KPX A W -46
+KPX A V -75
+KPX A U -50
+KPX A T -60
+KPX A Q -30
+KPX A O -30
+KPX A G -30
+KPX A C -30
+
+KPX B y -6
+KPX B u -12
+KPX B r -6
+KPX B quoteright -20
+KPX B quotedblright -32
+KPX B o 6
+KPX B l -20
+KPX B k -10
+KPX B i -12
+KPX B h -15
+KPX B e 4
+KPX B a 10
+KPX B W -30
+KPX B V -45
+KPX B U -30
+KPX B T -20
+
+KPX C z -6
+KPX C y -18
+KPX C u -12
+KPX C r -12
+KPX C quoteright 12
+KPX C quotedblright 20
+KPX C i -6
+KPX C e -6
+KPX C a -6
+KPX C Q -12
+KPX C O -12
+KPX C G -12
+KPX C C -12
+
+KPX D y 18
+KPX D quoteright -20
+KPX D quotedblright -20
+KPX D period -20
+KPX D o 6
+KPX D h -15
+KPX D e 6
+KPX D comma -20
+KPX D a 6
+KPX D Y -80
+KPX D W -40
+KPX D V -65
+
+KPX E z -6
+KPX E y -24
+KPX E x 15
+KPX E w -30
+KPX E v -18
+KPX E u -24
+KPX E t -18
+KPX E s -6
+KPX E r -6
+KPX E quoteright 10
+KPX E q 10
+KPX E period 15
+KPX E p -12
+KPX E n -12
+KPX E m -12
+KPX E l -6
+KPX E j -6
+KPX E i -12
+KPX E g -12
+KPX E d 10
+KPX E comma 15
+KPX E a 10
+
+KPX F y -12
+KPX F u -24
+KPX F r -12
+KPX F quoteright 40
+KPX F quotedblright 35
+KPX F period -120
+KPX F o -24
+KPX F i -6
+KPX F e -24
+KPX F comma -110
+KPX F a -30
+KPX F A -45
+
+KPX G y -25
+KPX G u -22
+KPX G r -22
+KPX G quoteright -30
+KPX G quotedblright -30
+KPX G n -22
+KPX G l -24
+KPX G i -12
+KPX G h -18
+KPX G e 5
+
+KPX H y -18
+KPX H u -30
+KPX H o -25
+KPX H i -25
+KPX H e -25
+KPX H a -25
+
+KPX I z -20
+KPX I y -6
+KPX I x -6
+KPX I w -30
+KPX I v -30
+KPX I u -30
+KPX I t -18
+KPX I s -18
+KPX I r -12
+KPX I p -18
+KPX I o -25
+KPX I n -18
+KPX I m -18
+KPX I l -6
+KPX I k -6
+KPX I j -20
+KPX I i -10
+KPX I g -24
+KPX I f -6
+KPX I e -25
+KPX I d -15
+KPX I c -25
+KPX I b -6
+KPX I a -15
+
+KPX J y -12
+KPX J u -32
+KPX J quoteright 6
+KPX J quotedblright 6
+KPX J o -36
+KPX J i -30
+KPX J e -30
+KPX J braceright 15
+KPX J a -36
+
+KPX K y -70
+KPX K w -36
+KPX K v -30
+KPX K u -30
+KPX K r -24
+KPX K quoteright 36
+KPX K quotedblright 36
+KPX K o -30
+KPX K n -24
+KPX K l 10
+KPX K i -12
+KPX K h 15
+KPX K e -30
+KPX K a -12
+KPX K Q -50
+KPX K O -50
+KPX K G -50
+KPX K C -50
+KPX K A 15
+
+KPX L y -70
+KPX L w -30
+KPX L u -18
+KPX L quoteright -110
+KPX L quotedblright -110
+KPX L l -16
+KPX L j -18
+KPX L i -18
+KPX L Y -80
+KPX L W -78
+KPX L V -110
+KPX L U -42
+KPX L T -100
+KPX L Q -48
+KPX L O -48
+KPX L G -48
+KPX L C -48
+KPX L A 40
+
+KPX M y -18
+KPX M u -24
+KPX M quoteright 6
+KPX M quotedblright 6
+KPX M o -25
+KPX M n -20
+KPX M j -35
+KPX M i -20
+KPX M e -25
+KPX M d -20
+KPX M c -25
+KPX M a -20
+
+KPX N y -18
+KPX N u -24
+KPX N o -18
+KPX N i -12
+KPX N e -16
+KPX N a -22
+
+KPX O z -6
+KPX O y 12
+KPX O u -6
+KPX O t -6
+KPX O s -6
+KPX O r -6
+KPX O quoteright -20
+KPX O quotedblright -20
+KPX O q 6
+KPX O period -10
+KPX O p -6
+KPX O n -6
+KPX O m -6
+KPX O l -15
+KPX O k -10
+KPX O j -6
+KPX O h -10
+KPX O g -6
+KPX O e 6
+KPX O d 6
+KPX O comma -10
+KPX O a 6
+KPX O Y -70
+KPX O X -30
+KPX O W -35
+KPX O V -50
+KPX O T -42
+KPX O A -8
+
+KPX P y 6
+KPX P u -18
+KPX P t -6
+KPX P s -24
+KPX P r -6
+KPX P quoteright -12
+KPX P period -170
+KPX P o -24
+KPX P n -12
+KPX P l -20
+KPX P h -20
+KPX P e -24
+KPX P comma -170
+KPX P a -40
+KPX P I -45
+KPX P H -45
+KPX P E -45
+KPX P A -70
+
+KPX Q u -6
+KPX Q quoteright -20
+KPX Q quotedblright -38
+KPX Q a -6
+KPX Q Y -70
+KPX Q X -12
+KPX Q W -35
+KPX Q V -50
+KPX Q U -30
+KPX Q T -36
+KPX Q A -18
+
+KPX R y -6
+KPX R u -12
+KPX R quoteright -22
+KPX R quotedblright -22
+KPX R o -20
+KPX R e -12
+KPX R Y -45
+KPX R X 15
+KPX R W -25
+KPX R V -35
+KPX R U -40
+KPX R T -18
+KPX R Q -8
+KPX R O -8
+KPX R G -8
+KPX R C -8
+KPX R A 15
+
+KPX S y -30
+KPX S w -30
+KPX S v -20
+KPX S u -18
+KPX S t -18
+KPX S r -20
+KPX S quoteright -38
+KPX S quotedblright -50
+KPX S p -18
+KPX S n -24
+KPX S m -24
+KPX S l -20
+KPX S k -18
+KPX S j -25
+KPX S i -20
+KPX S h -12
+KPX S e -6
+
+KPX T z -48
+KPX T y -52
+KPX T w -54
+KPX T u -54
+KPX T semicolon -6
+KPX T s -60
+KPX T r -54
+KPX T quoteright 36
+KPX T quotedblright 36
+KPX T period -70
+KPX T parenright 25
+KPX T o -78
+KPX T m -54
+KPX T i -22
+KPX T hyphen -100
+KPX T h 6
+KPX T endash -40
+KPX T emdash -40
+KPX T e -78
+KPX T comma -90
+KPX T bracketright 20
+KPX T braceright 30
+KPX T a -78
+KPX T Y 12
+KPX T X 18
+KPX T W 30
+KPX T V 20
+KPX T T 40
+KPX T Q -6
+KPX T O -6
+KPX T G -6
+KPX T C -6
+KPX T A -40
+
+KPX U z -18
+KPX U x -30
+KPX U v -20
+KPX U t -24
+KPX U s -40
+KPX U r -30
+KPX U p -30
+KPX U n -30
+KPX U m -30
+KPX U l -12
+KPX U k -12
+KPX U i -24
+KPX U h -6
+KPX U g -30
+KPX U f -10
+KPX U d -30
+KPX U c -30
+KPX U b -6
+KPX U a -30
+KPX U A -40
+
+KPX V y -34
+KPX V u -42
+KPX V semicolon -45
+KPX V r -55
+KPX V quoteright 46
+KPX V quotedblright 60
+KPX V period -110
+KPX V parenright 64
+KPX V o -55
+KPX V i 15
+KPX V hyphen -60
+KPX V endash -20
+KPX V emdash -20
+KPX V e -55
+KPX V comma -110
+KPX V colon -18
+KPX V bracketright 64
+KPX V braceright 64
+KPX V a -80
+KPX V T 12
+KPX V A -70
+
+KPX W y -36
+KPX W u -30
+KPX W t -10
+KPX W semicolon -12
+KPX W r -30
+KPX W quoteright 42
+KPX W quotedblright 55
+KPX W period -80
+KPX W parenright 55
+KPX W o -55
+KPX W m -30
+KPX W i 5
+KPX W hyphen -40
+KPX W h 16
+KPX W e -55
+KPX W d -60
+KPX W comma -80
+KPX W colon -12
+KPX W bracketright 64
+KPX W braceright 64
+KPX W a -60
+KPX W T 30
+KPX W Q -5
+KPX W O -5
+KPX W G -5
+KPX W C -5
+KPX W A -45
+
+KPX X y -40
+KPX X u -30
+KPX X r -6
+KPX X quoteright 24
+KPX X quotedblright 40
+KPX X i -6
+KPX X e -18
+KPX X a -6
+KPX X Y -6
+KPX X W -6
+KPX X Q -45
+KPX X O -45
+KPX X G -45
+KPX X C -45
+
+KPX Y v -60
+KPX Y u -70
+KPX Y t -32
+KPX Y semicolon -20
+KPX Y quoteright 56
+KPX Y quotedblright 70
+KPX Y q -100
+KPX Y period -80
+KPX Y parenright 5
+KPX Y o -95
+KPX Y l 15
+KPX Y i 15
+KPX Y hyphen -110
+KPX Y endash -40
+KPX Y emdash -40
+KPX Y e -95
+KPX Y d -85
+KPX Y comma -80
+KPX Y colon -20
+KPX Y bracketright 64
+KPX Y braceright 64
+KPX Y a -85
+KPX Y Y 12
+KPX Y X 12
+KPX Y W 12
+KPX Y V 6
+KPX Y T 30
+KPX Y Q -25
+KPX Y O -25
+KPX Y G -25
+KPX Y C -25
+KPX Y A -40
+
+KPX Z y -36
+KPX Z w -36
+KPX Z u -12
+KPX Z quoteright 18
+KPX Z quotedblright 18
+KPX Z o -6
+KPX Z i -12
+KPX Z e -6
+KPX Z a -6
+KPX Z Q -20
+KPX Z O -20
+KPX Z G -20
+KPX Z C -20
+KPX Z A 30
+
+KPX a quoteright -54
+KPX a quotedblright -54
+
+KPX b y -6
+KPX b w -5
+KPX b v -5
+KPX b quoteright -30
+KPX b quotedblright -30
+KPX b period -15
+KPX b comma -15
+
+KPX braceleft Y 64
+KPX braceleft W 64
+KPX braceleft V 64
+KPX braceleft T 40
+KPX braceleft J 60
+
+KPX bracketleft Y 60
+KPX bracketleft W 64
+KPX bracketleft V 64
+KPX bracketleft T 35
+KPX bracketleft J 30
+
+KPX c quoteright 5
+KPX c quotedblright 5
+
+KPX colon space -30
+
+KPX comma space -40
+KPX comma quoteright -100
+KPX comma quotedblright -100
+
+KPX d quoteright -12
+KPX d quotedblright -12
+KPX d period 15
+KPX d comma 15
+
+KPX e y 6
+KPX e x -10
+KPX e w -10
+KPX e v -10
+KPX e quoteright -25
+KPX e quotedblright -25
+
+KPX f quoteright 120
+KPX f quotedblright 120
+KPX f period -30
+KPX f parenright 100
+KPX f comma -30
+KPX f bracketright 110
+KPX f braceright 110
+
+KPX g y 50
+KPX g quotedblright -20
+KPX g p 30
+KPX g f 42
+KPX g comma 20
+
+KPX h quoteright -78
+KPX h quotedblright -78
+
+KPX i quoteright -20
+KPX i quotedblright -20
+
+KPX j quoteright -20
+KPX j quotedblright -20
+KPX j period -20
+KPX j comma -20
+
+KPX k quoteright -38
+KPX k quotedblright -38
+
+KPX l quoteright -12
+KPX l quotedblright -12
+
+KPX m quoteright -78
+KPX m quotedblright -78
+
+KPX n quoteright -88
+KPX n quotedblright -88
+
+KPX o y -12
+KPX o x -20
+KPX o w -25
+KPX o v -25
+KPX o quoteright -50
+KPX o quotedblright -50
+KPX o period -10
+KPX o comma -10
+
+KPX p w -6
+KPX p quoteright -30
+KPX p quotedblright -52
+KPX p period -15
+KPX p comma -15
+
+KPX parenleft Y 64
+KPX parenleft W 64
+KPX parenleft V 64
+KPX parenleft T 30
+KPX parenleft J 50
+
+KPX period space -40
+KPX period quoteright -100
+KPX period quotedblright -100
+
+KPX q quoteright -40
+KPX q quotedblright -40
+KPX q period -10
+KPX q comma -5
+
+KPX quotedblleft z -30
+KPX quotedblleft x -60
+KPX quotedblleft w -12
+KPX quotedblleft v -12
+KPX quotedblleft u -12
+KPX quotedblleft t 5
+KPX quotedblleft s -30
+KPX quotedblleft r -12
+KPX quotedblleft q -50
+KPX quotedblleft p -12
+KPX quotedblleft o -30
+KPX quotedblleft n -12
+KPX quotedblleft m -12
+KPX quotedblleft l 10
+KPX quotedblleft k 10
+KPX quotedblleft h 10
+KPX quotedblleft g -30
+KPX quotedblleft e -30
+KPX quotedblleft d -50
+KPX quotedblleft c -30
+KPX quotedblleft b 24
+KPX quotedblleft a -50
+KPX quotedblleft Y 30
+KPX quotedblleft X 45
+KPX quotedblleft W 55
+KPX quotedblleft V 40
+KPX quotedblleft T 36
+KPX quotedblleft A -100
+
+KPX quotedblright space -50
+KPX quotedblright period -200
+KPX quotedblright comma -200
+
+KPX quoteleft z -30
+KPX quoteleft y 30
+KPX quoteleft x -10
+KPX quoteleft w -12
+KPX quoteleft u -12
+KPX quoteleft t -30
+KPX quoteleft s -30
+KPX quoteleft r -12
+KPX quoteleft q -30
+KPX quoteleft p -12
+KPX quoteleft o -30
+KPX quoteleft n -12
+KPX quoteleft m -12
+KPX quoteleft l 10
+KPX quoteleft k 10
+KPX quoteleft h 10
+KPX quoteleft g -30
+KPX quoteleft e -30
+KPX quoteleft d -30
+KPX quoteleft c -30
+KPX quoteleft b 24
+KPX quoteleft a -30
+KPX quoteleft Y 12
+KPX quoteleft X 46
+KPX quoteleft W 46
+KPX quoteleft V 28
+KPX quoteleft T 36
+KPX quoteleft A -100
+
+KPX quoteright v -20
+KPX quoteright space -50
+KPX quoteright s -45
+KPX quoteright r -12
+KPX quoteright period -140
+KPX quoteright m -12
+KPX quoteright l -12
+KPX quoteright d -65
+KPX quoteright comma -140
+
+KPX r z 20
+KPX r y 18
+KPX r x 12
+KPX r w 6
+KPX r v 6
+KPX r t 8
+KPX r semicolon 20
+KPX r quoteright -6
+KPX r quotedblright -6
+KPX r q -24
+KPX r period -100
+KPX r o -6
+KPX r l -12
+KPX r k -12
+KPX r hyphen -40
+KPX r h -10
+KPX r f 8
+KPX r endash -20
+KPX r e -26
+KPX r d -25
+KPX r comma -100
+KPX r colon 20
+KPX r c -12
+KPX r a -25
+
+KPX s quoteright -25
+KPX s quotedblright -30
+
+KPX semicolon space -30
+
+KPX space quotesinglbase -60
+KPX space quoteleft -60
+KPX space quotedblleft -60
+KPX space quotedblbase -60
+KPX space Y -70
+KPX space W -50
+KPX space V -70
+KPX space T -50
+KPX space A -50
+
+KPX t quoteright 15
+KPX t quotedblright 15
+KPX t period 15
+KPX t comma 15
+
+KPX u quoteright -65
+KPX u quotedblright -78
+KPX u period 20
+KPX u comma 20
+
+KPX v quoteright -10
+KPX v quotedblright -10
+KPX v q -6
+KPX v period -62
+KPX v o -6
+KPX v e -6
+KPX v d -6
+KPX v comma -62
+KPX v c -6
+KPX v a -6
+
+KPX w quoteright -10
+KPX w quotedblright -10
+KPX w period -40
+KPX w comma -50
+
+KPX x y 12
+KPX x w -6
+KPX x quoteright -30
+KPX x quotedblright -30
+KPX x q -6
+KPX x o -6
+KPX x e -6
+KPX x d -6
+KPX x c -6
+
+KPX y quoteright -10
+KPX y quotedblright -10
+KPX y q -10
+KPX y period -56
+KPX y d -10
+KPX y comma -56
+
+KPX z quoteright -40
+KPX z quotedblright -40
+KPX z o -6
+KPX z e -6
+KPX z d -6
+KPX z c -6
+EndKernPairs
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/UTBI____.pfa b/e2e-tests/cypress/fonts/Type1/UTBI____.pfa
new file mode 100644
index 00000000..a4fda2e9
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTBI____.pfa
@@ -0,0 +1,1172 @@
+%!PS-AdobeFont-1.0: Utopia-BoldItalic 001.001
+%%CreationDate: Wed Oct 2 18:45:57 1991
+%%VMusage: 34429 41321
+%% Utopia is a registered trademark of Adobe Systems Incorporated.
+11 dict begin
+/FontInfo 10 dict dup begin
+/version (001.001) readonly def
+/Notice (Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.) readonly def
+/FullName (Utopia Bold Italic) readonly def
+/FamilyName (Utopia) readonly def
+/Weight (Bold) readonly def
+/ItalicAngle -13 def
+/isFixedPitch false def
+/UnderlinePosition -100 def
+/UnderlineThickness 50 def
+end readonly def
+/FontName /Utopia-BoldItalic def
+/Encoding StandardEncoding def
+/PaintType 0 def
+/FontType 1 def
+/FontMatrix [0.001 0 0 0.001 0 0] readonly def
+/UniqueID 36546 def
+/FontBBox{-141 -250 1297 916}readonly def
+currentdict end
+currentfile eexec
+f9d13ed4c538ee56ca0c8979e615439db5863a5292e578086555752cf04323b5
+761364ec433e576f108b1bff6c56d0f56331bc1243c31feb9985de983b0b3300
+e35eaa63cb5ef5a522d2e2731f37eef0cc62a540af58c4780559b4a0499bd168
+7642677ef167669aa264d68a8ef6da7425b771d48260108ab87a077560b2ebc7
+dafcace10cee472c35746fc11d5304e441fa0678decc0652588a56cef7c28ba5
+5cbd94a7acac9a06942a82d6abc1ed45ef21f4f3f5f61d0002340725ba15b9b3
+0b9992741770f020ec318f514874b6abdb27c3f0dee979cce17199b9f3ecb0f0
+709736e55bc48c55ebc8f7259d9e0ba13b46c2983c6e8265633d11358057019a
+9bc40f9ef8830b5cf7022bc19c01160d17df4ab51d113cd49bde4d1e3b5f8ba2
+7d179464b8fa28e12092267d5c70cc341eb3ed0eec08b343175d5458311a7404
+a9463d728eda1642be73366ec57a705501aa054b60b04983e30b9987bdbd1c69
+95ef18d327beb2f2086a2245602495ccb7851bcc5eb302b980ada64a262c562f
+6b2748b8f7dc4a0a06c69a7231fd40568bb236b33f33ac04f1d2f91573f60470
+599ca51abce802711c7fbd95247ec33fba3a393195a5cd54622434e668d1e63a
+a39a51699962bfcc4774228ebdb0b01f29b430434d8023331bd1b6b2c76e43df
+b3c68753c55075856e9ea0f5c7d60f510bc115bb70e0a8eb9998d3f32a0b72d3
+ab524841ef36b198e3ae58129ca64e005f776a0676dcef57e7961dd2b948c2da
+f392cba950f1101a7bba99952251c82ab5d0eb5ecd8af07973eeafcb3e0b5f2a
+3b6c8852ea072b9787fb945cb7cc7a2acebe92a5458e7dab5ea2551a61a55068
+d7023b6193c750e32502fe1e1d8e8d497453fff493caaed83c410eef050c30c6
+e38339831517101b07fc15d5f63470a9863bbfee0c0002695669900aa70a18f7
+a87f1b0b83494c39c3c90a8313060204d19553e2d66419e7b002b25108ba00ff
+a7e60c68b73c53f306e112ecba3b64d35caf59f11a41ae76aa2fb4882ffffb96
+400c2b6b0051c90d7279ffd0df3a6ba63daf802888bb6b98db281b89734b6e2c
+9ebd473352b36e11b5c3f3c7ecb22095b7aae220fbc89cf6036fa963530b8a01
+1cb06875ceaa143eacdbf1015631e8858bfe5917e01737ec6c840fa7097a42f1
+15884772664b9665e5575dc3003914643d34f329b9cd12b1012e93133a27d139
+ebcf7f9b68dc21575f3ef070ef94d4f3f9ea0d3ab4a75e6ec849a4630f4f13f5
+0883dd86a9903613ecf36ed6ab7294afdc167e0eda4b1944870a2f99ae8a22c8
+1b711d1eeefd489009b86f08078f4510c6846b17ae72c87dc51d0433ed9cb40f
+197db7e3c2ef41f077481b3d194aa3284d5400681001e0531348b91cb957c396
+5611aab14b92dbb110db950073dcaf69d6454588bf63c477112c38ac578b0ddb
+18fbf7f0d44138c4721743fc05cbef7cc3c684328df7c4bbc17afd9084bafec9
+99b4b8304cfd2d604e31d96c466466cebb6ef3a2236498e31b3c8dd8f15c1ec1
+c67d2b6472c3dce8f613c8a206e5dbca88b03ea931fdbb7e2d19625d4aa0d800
+b2eb953de5ae22b2478432710ad8efc57558171c332f6b27a2cac9145badf751
+9cf4987b853ed30d3a726039a83158c03a0f457dfc9a81e78d909f070e8d47e0
+252b72d03d049ed6349c752a01b92072a2c8f6c50ae85d6a5469eedab2e0a5f2
+6e502cf72a76706ecaaf7a590105feb6f0c1f52a820369fe3b903b08ec602ab6
+882e1ea21007443e2460e29de7fc07fa72bf89c145b04be55df48c0ae5ba1bcc
+c84bbf0a2de650ded51dbb240dadc2faf3a2fd0fa8749f0b1e372c579f905148
+706e910cd5578d06c8018e3ff8b88af6002c549b87a9fe2b1f804e95921ef83a
+2810e015e315d55496eec2bd16e0b71e4d7004dfbaab8a5d651c4446e343e382
+8204f6e7b4cababa6aed624942902c65899615d858ca24fe6c91a41d087fd647
+e50c52850049c81a5dd9447761b3a308df7403fa4f8669df366d3e114628858d
+17ba68a92fed47c199d1f7d415c8315d30b394177b161fa487225c38e0b9d9c7
+ab236e90e926614f9fbdb3bc081e948319ec3871fd37193f0889ecf2c47841c0
+666b272328bbd281f719181b4e509c01ce80afe08f6244ecbfc136f8edaccf79
+36399e63e5982d27fbdaffab086e1e064c84cd97d4eef959aa3a452e065618f6
+b549e3fe7207bcdf85ec1917612c098236b9124d6f1c037bd0656777dfee995d
+5d125d678fa7ac5c995515e952194f9620579316eb0f0ad55930ac82fa773159
+c1ac7f4b072f06baef03ee07c23fcb4ad8072503469a8e4eda4cf143c7a68d27
+bbf01668c0abf4950277c195fa1cba7297a2e863299e195c6684f2a076427e5a
+a2f308fcfb1dc11d14bcc8a4a59611eb8bfcbfcd73e963d69c82fcd95e989ff5
+ed486f9809d3f9e1a0ddfb573b37d1d3d57e0fa4594f66ac56fe16168a472552
+eca179c7ad26be24e9afb1a759b73de53e9f7075b7ce0ab8bc8e30ec6d68aa42
+a84b36f2c4e4afa9e59c6dfa9af97a93b2cbd9ba786fff8d8b60305b608bfc36
+83c2c94dac72000fa51ff15739975e9ec0185f3b7cd805e67b5ff91fabfa648d
+81e0beddecb47dbbad28a99acc3074f1c248b2231be3fb01b331e90a1bc723e2
+8a0b2e99f2defe00a0b33c13f0a6a37717089fb28b7c58daf478e877e4bcb73d
+54b358c509a630d73938bca5126547c94f88c97cedd1c65a3d514b3ab86a489c
+317554f67e1aecc75e3c71cd37f268999f17fb649cc52ce4ada26d2d4ff1c248
+e9e4f6f5ccb85aa64e90f371cb94da1ecf5b995ded0fa6771e3f874ef449186b
+4b78c298d5c24e5127532b67c9479f70c3703ab7800ba8ecbca511703d6a1733
+7f2edbaa61171f00f01debc6eba2094465e248cc8262bd35cab622443a1be5c2
+8e6318e4f3b7131045b8f676e2086f0432f51983c74fea25cd5aad0797a4cb5c
+6503695292d798c755170f0ae137674d25759bbd89cf4fe0917e32999b4a04a3
+655ea871b30e22b0ee39f29f7915fa3dbac0d01c55a454728869501335f97e3d
+24b17ab7ea28784b6f8c9e0b72b38f64e0fdb578eecaa5590c20a1d17ebd4ec4
+1632b40553aa80fced6d88079eee8097a410a29b549a0bb3b6fec526caa745a7
+202eabbe2cf3f1262db3e6e309e0afc3d9c808fced17b05c320945fdf0a05459
+ea7ba64fc4112fa68eb4887bc845e63a598e2d31df0ab0d64155e631e3ab4621
+744da7c3da19a2e66ed2bf759505e0cfb47a76a81ed7242cc29c7383989afc30
+df86c4b38be0a5c8b360f586f463e53d6684584a3a5fc75495200fc707cd1981
+1a0fbcc0fa784b02140158ef0913170979c4fe96f3fdeb414cad01b38d2b5934
+0b21cf14a28c24a583472ae3cd667ab094e56744c6b35ab7870ee5b0aa22a70d
+dc68afd34b44e45275ca8d12747dc6fef76b826f1dcd70a5a30537e096935f7d
+ba37414aa1f0111cf7a1844c5e2b3f63764458ce9e113d06cc844bead4d22109
+069b5174e0ba5aabef78dea7e384369de2e4142bfe6521efadfe67aa41fd7bf5
+52ff016ad8d6314b4285e129ebbdbfd3c6e3383906248f0ce045ad370b0f90f2
+a832944cbd59bc7cf15a9c17ee0fa0bbe12f2304fd6a9f56cbd65e7cc91ed860
+7d09e58934079a74d02c1b25b958567133657dc028148e9f13a0ff6db812422b
+6c7cfe064a171b4fc5e1764000d4a761d1c476aeb59a45f7b64ba1c8623b1423
+91d79d124b8506eaf94ef8fd28b5fa6ca6aa5677ffe979dd3d75f540d577ea8f
+9cc95f5b41942294df812c4d4aa4ee701f654be2c00440ba575c5512bb5c196b
+3ad24366514f2b4ea4ab0c35927517bfe166c261c0c7f9b73ab0c52ed0da3696
+628a1f230e48cdaf2ae53000dbb2ee3fe17b9785faf74baec7caadb23fe6e329
+b2b908bae996af74097cdb6e280b5d5c2783b33ba106d6f458179822af89ee24
+f99514605cb4e232b62d92014636841a991b3790ae7478d020d6513e70244034
+dec5f2106a1820acac4b7e091dce521896f25dcfaf58a521ecb5d26ae3b8bed6
+c61c3c25d09d159d5ef58e253dc0f9817fbb469e44eed683ffecb08689e66108
+ce6f93327a5ade948640d0e9347b113d846846fae982687d95b66d6a75ffe876
+a2c50ce60e3f7953d9dd5a9f5d65b1105a7849d734336804de32576e81abd201
+4aff1a36aae15c725df295daaf26238b21c6d288450aa4d94e26ce6a1de7f096
+5838c195a97ddeb019500e8fb95e8b48190b020cbf0582a99df66b7192b67448
+c1695882808efb39a3c54fa42c183f65492f9d0cccd832b5590a6c4cf18e8d2a
+e9f50100f197818f192c39d4204b6dab711c2f8cb53e5ef002719755cfee11e4
+f1ca77783034901fa2da7f82254d1184f2f724e36be731fe923c99d444407539
+3000ceb19548d5c11d7aab1e13917ea4391ac626e02bd7a1960bcdf24e048f46
+76fc3cb4031811d94f83a0c23b309cc9a537b599c174dc862429bba8b94e7e56
+56682c13bc4deb402af2334993c66d6ef48cebcf6dcc924a2a1615ee92edb7bd
+221fd3204d9dfc6b2abe84d5b47a9d03227a4c015204af7dc5f937adc8c60de9
+feb1c1c722a911b2a5f186478ad7540ff28f7cbc82d85e461c740e8aa51c16d8
+8f939075b78f8a097f647de080dadd87dd5a4107ae7cbc25a7964e5e8e0aff10
+1f17185e95a7c432c76f5fba2138ee76b7ec76c691df228099c0de7107aa0f5b
+523209576c023ed23e9ea9047d1def8175e515bd1ee133167f0f92fc87732091
+4be8e6a04a796e5affd91f2e0bfb1b47d960228b970cae0824b32f602504cb12
+9f29f933e538a01fc231eedadd0bea5523ce4acd3ca5d512aeffd4bd91adc088
+47e520f07aa173dc6d1dcda5c7a20a1cbb399d7a11694bd2c959d2546d5cd1b9
+b7afba473e7c9f0492c2d4b4ed1204d6f633a431d83b813b5ec5ecbe41b6c8ac
+003fbe65fee1581fb80c2f4c691f9e8a33630940b198deba7491d83421017409
+6dd46e914bf82841e2377e57cf81c0f03db26133519f638d0b6422585af7f7f2
+589aed2dfd15c02f83bb6fbac42b4fe85e14256c5b5f6e7a287953a4f96a0c6c
+562b87ff8a8ddf922571aedf6898faf7f9327f92a6f736546c5699aefc4cd1ba
+b403a43f8464b215993ef53d2d9186ab1218f7e1f2ccf589ee0a52096a153ac5
+e328654465f3f734fb590ed232399f9f6dcfdfe7f702b4ed829e444ad685b1e0
+d0764217ae144e4af82554faf6d013be83816ab94a6b175905916bea3c053fdf
+8d033e48ce7e6a7338fdebdf9cd047624cfcf3be1aa133690eed083e28939707
+778274ae874e906f972080d9dd453ff5039cd351776a1ed52d3e4a8fbc749f9c
+588478826593e0e8625b013ff1469bf76aef8ae06a63ebbda3176d517a7cc32d
+ad93a096258b227102b2b748359ff2e70a438709eb5f1b60be92f7a6c8f92372
+c3b9a58ee0934083f2300c6423250c00ecc2cfc67a6be83450eeaeca993fd695
+939e47e7c7d28f9db30e2a82ad4895092a1a5a734f1727b4734a14743ee1c9dc
+851c9463451042f28a519e8c532e16c6a7f65a11286b85d48ee3bd6f09caf327
+cd87df6f40cc9ed5784744408da6a2e65cdbc6838812b2da414fcf7a33ebe676
+d381f7cb0bc1d84273b03240fd3eacfab2693d04abe2a89f6d884fc1ba2c0c6b
+183916096f98f062bbe990f87a9d74d275f18d5d97e9eff897b99fd5e5f2353e
+16fe417e644364017a1f014cd0ea62ad0a684fcb1f7a4969bb2d0119d3021ea6
+91ad90564b7cd59c03662d4809943ac3c0316c45b3e6576a9cac3f549f04a9cf
+b21aa641eeced2af9c0e9944bcd355444f1ef2dd626d94d65c1e00b208f06fd7
+526f73917bfebde0538984edc704fb56a92f429f2b3ccd2324926605ffb9e06c
+e3f9435778a9972bc1d9511b1c9e013c9232cfa8fa03b8076de78144e24583da
+ce13c107390a4b4a66fcf41d02c7ea3889fcf648bd9b202a77ee0debb98745c9
+b424aa89f8028129fea8293f44efdfea96ca75d1462715087baceb18506124ad
+843bf5b5eb3044665921c1e711ae61d7ea772773f99f7aee6de71fcd8c5c2c9a
+4e5d5a1f02ba9ad0e31ac0673594882b491d8dcc9b7c4d8bf806845cb7e7b054
+e073b9379f6f7b756786174fdf25b080adf0cb4645dd8fe1b4cb5377195bbbc8
+88e09bf98c82e523794bc350fa0fa12413dbffc006a66c47c402b78164fd9234
+b9fae67432f7a9423ee7569d3e6bb293cc0a095f3c5a0abb6a2c52a0c4dccddf
+54dccede5914afbb9bce717409060d5772bf6c19ec65700c6395884b3da5bd6c
+d81daba48977e3af9653875148fe7ddab905a0cf6c5352a468c36ad2bd09d897
+2d54f62bf9af9688c1454b67f24b5cbe39e9c069d6b5429ab9e8b2117c0b09eb
+7f877d667af2af4c06339230c2e2d082f389d7006f8bacf00bcef8b652a8f1a7
+e210e7facbe69f8b0cebc21b6cb559ce9f6d8e1b0546870d773f6335426b6978
+60bfe9e2c6cfd1206ff1fa27cadbdd58c4c2368e95e17bda712c4feb1e89c63c
+45c0cc72dac845d10847408dfc7dd390ea0c50fbf3012939372707be42c88a8b
+025a77150be73e08e73fa1e15fb39d985d5e6666d2a4f77f947eb1b2228ef0e4
+bdb7f767621b7edf5b863d44c09a02b08a41a3feeb239ec9e022d0d8c2fbe939
+fc242a923b082a3035a23c0ae48dae76b4c2147a7f8126ab66dfbb8c892d858a
+48f8b65eb85d3216b8c156ee4005045697a11f0f349d05620b1c0d41575c11bd
+e8165d443d4e836d4b4b2fc028e3ee7eb995115de8bb5b58d7575a260c8ebb93
+cc5d43766f56ce454dff93669f41be40953c42ffcc98a89e9f6ab320f7688083
+32a48d4ff8eb88c84c7e0cbb9e031ac71c53873da074cd4112dc30b4f27c163c
+0d6371b35d0571dbc1b80f5101c90d4745e978091c4c8b2bfe8aa23db4979a9f
+143dba6c041772bfdda2077985fe5da829d9de5d8c5c9d02175f65e23495756e
+2ba67a1f46d862bb76a4f330c66adc2df5303b34ca0e705f28a97e2782105359
+3ade715c3f7eea39e401af757229bd314b886dda55f418b3a67657d199cdc8b4
+d05a5b7aaf37b53e1c5707fe5454d512c954b6887731d2626499cccd6e80b1cf
+9b3116814c0f7705d53b402fc4e9b6307b22941daa12d9c66849165e8de01e0b
+20c33b1b503c6ac7995d4bda83da72dee3b177389d500061e16c6decd05bd391
+1e2d1727b5436aa550e1bd505e91ce0efcea62d9fcaebc098880180caa252f6a
+667c75a3185f62caa0c28763104f57e93fba3a396d7015b3a55161401970668c
+7e6ed168f6e0f1029216c27c33544cc77111b2ef2b1538f1e89397a2e78f0319
+28e948640198d7d2b5684f9566b5e70988c2516a81623a6fba82c1554f3a8a85
+98c1efd626eea0ea586cea3b1f6b62f97b1ea416332ae107f0f174ebde1f84b7
+4af56a9495913e0e84ed0d8702ebe1c699f8e8f056de6e7728023e6fe0dc68cc
+aa76c3e2aac6eb5f5ee502c1098db8033c4432fd0c6f62aadb989954d81e4247
+6efe3ddfef52cb066d3da1811e1bf13a9da68d77b3d88ae4b5e0ec252a992b2b
+dd6b487458694df4bb1f300f88f6891f2033dfd4ed6eb147315ea84efef20c16
+3b2acdbd1a6812ebfa51f69413d1750c60d374839d46400b9563daea410e4bb6
+4231cc49a6db6f9acad1bd930faa27bc25e636deef091d07ee8c405eacf00d37
+a67027a49443231fb38cf74b4afe723353838866c9d56c2218507344cfe23e85
+264ec426283982e0e299b8557f37010c921d4b1e92b1997498426d3690408685
+5e0ab86ffb4c74192269197dad437aafdfc98809cc865a72f397901a02e22f97
+f035deb4063864798cbbdca6adfdd2e1f644c299d5acaa8b0ebf0102cdc1e478
+77179fd1e5c69595485ce83dd18e3a154e8f6bf6aad3752f209ad3e561cb2400
+0959f964e853ec9cde9deea9278c5988db3f026a6120e0020438f44066d270f4
+af05c4d6568fa3b288c0443be3cc406b6ea555ba97b9fae56c2b7e75263397c4
+f247bee72066fc445c158297eab5151852dc2c32306794b7be0a5f23c22f76f8
+4c80909a303d6d29d808a606b8c0739e69d715778ae843c033bcc8065ce83795
+f3d5b1a403aecd1ce8db79b148946d9f96673b51840fe073065a7da8cf8a8e98
+0e70984d88ef691e21a7f3b52c2a455286d80e3831ef4b7119459970a77e2bfb
+1b2e12a6e2cd4e02d80cdf90fc00a1fbcb42817660487d5fadefde0278d4fcad
+98fe39b3827b681d4427fe992adae296946720129777c73d954e1f91dfe80130
+362fd97c8a7f693b73ebe45155434eb078fc66adde8f9217583680f5e0c9f80e
+c413226de65a1e06fd6bb4a4870e8702d2d77eeafc0ea5634272f19f31a6ea21
+b51de45449e89807b18588c9a43524713c31f27be41941c32da7c5cb636c5e1d
+0ef69e2ef2c18ab0746cb2d19980fbdcf9691c1d74606830522219310193b32e
+f886e8f200135b55e178d832f0416de4b86e2fb4eb8731337efccf833c044446
+1ab477118d9b6d53ebf5c42e46ed8e5d0181eef7a15145d5a51d7a1b8f82189a
+f6ccde81e2f1da5a0e419838b90adc945a7e74a2132aa57807fe6107caff8296
+aca45bdc7366cf3b738cf402674c2c2ef0954ccdf7c7df0377641e9ffa12a165
+725cc7bca1a0d38dbabd6982b2f1c432454919fb58876f7854ec713ebf05e0e7
+01d1135f89b31ffa94413f30f72527de872418559c07c8a87556fb558bbd7726
+aa9cfb99d41a7ddbc8d5e4f8aaf20619b2dfdc18ed319b958b5812867e5f4442
+fc670f55f78be8ec1c3ed00fa36cbd52c5c15d45a5c77746de5aaadbce70eb29
+05c3972ddd7e2c220ba4137a5a6aa178af65fd65f225393a08197057d5dd407e
+9c238702425991496f7c7c91754399ff3be95c95704f26de0cf99aabc5786994
+9b727f5cd72f4428908b95f1b879c2366ebc061ad4ef68ccd4f957cc76594d88
+dbea7f48b814a94be79e2cebf4de7d2f6b2fa680baab5196b132c5970d035801
+e021704e10823478c68b4f791e63cf7d9725088a6fc2279e1d09a68b04f31901
+7bfdf33caf58ef856a1aaa0793d327c91a2224e7239c211f1210ab11cab44b93
+0ac2b75868db9a0faca784a130fdf9f47cc09d44f9bc9f697f698d2da440441c
+1adf05c77b768535ec2dd30b9897c52b1e28d081f1dde26d5e8438c405277c2c
+2291b1911cbd914a714159a957e0b436583b1d90cd36c7ba1b6e1041c8eb3996
+dd2715fc01e9a82bd1590cb4ad8fbbdac9410beda818b81260a3a8742c137f6d
+175fa8bf26d648c991fe684e0e1469900ed0a53b7d44e35e63c7e1fd0b5aacbf
+5e9b854f302f3ee9c7620f5c9e2a5446f7653446f0bdc192a34e23654830e5d1
+52bf36fc02c220ac72569f80795ee9271fa89b438a8fa4de0a3fef6de60310e0
+77bb99f86ae3e0824c593cb2d0387faf97b3612131a2b0edbbc79a4444acd77a
+53ec23d447610ad92120f7702df3c0955e5870e1ede213b5edcfe5c0405538bd
+6d1a06aa55a8afa73fb82a29c6eba9adb721473d0756c31e77a4012d10dab6e5
+80c90caae60c3ea5a846f74536079bd790979059936c7e879eb79905e3b67b3f
+ace6bd0a20a8afbf1034dff455f130347f54b6f7862a772f0afd46f3601349e8
+9d1caf04d0598df4cdb164190614aaa872036855c02c3c92374a3baf5453baec
+0c0d2d89d79fd0452babb562e925422d17e888a9350fa50d9b34bd119618a1a3
+50f9059f36ffc5adb1f29923af100fadb5d32aa948ae738d53ba04c44c3afb82
+e935f0ccf3a7903ee5f939f10d3668c672b75e90777354d1429453010e4f72ac
+23bb5cd7ddb1712a6851c033bfc10470f8ae87308777dc1d69a1b95e0ab1a655
+e46f843d3c4bbf2714177d4c83747f918884d366dad7705e4668087eb0d13548
+2d3c35939ad5402b94d3fd7f2bdceb0ec55ec0fde7fe7a555ab921dac698917a
+fc4bd079834d26e0b1d4020bb50588e961658935079e8b542f927ec7a7e0ccfc
+d03a7bff24ddedd6d7df33c3507c449067e548b0db1a42baa9e3b1d0528298ce
+981f8e35b10b00f69c92390e4aa06d2b3374e868b4ee0cf0ee5f59f1c734550f
+640bd5eeeddf30a340558a831b9e39bff15b35649a22b23b8f39d0d473a07be8
+0c757d16f7738a0190178de0e5509ad9fe624499d7464c43833515c9a5841c1b
+e704c88e2df4508101dbb7330ab3dd863f544294887e6ad56bdf15800ae7a0d8
+e523cc1bd1f9424576492b11b41ccbdf5daaf31e3278b3d1c9160c9310f604be
+1e869ce18ea776bd3ad33ad41dbc5b3a0acce72033b37aaa5fd6854d0f2faf59
+d2e674cf97ed09c3ec2f7a02657bc036f78c29e25284c7caa6e480beec4772ff
+09b02feed2cf374a02b43f653c1d095d21cccefd30d2049c50327fc38bf44e61
+1e5afc9d29d37fc5c22f62b927fd2c5720a8025194734d8d361bd63745781d3c
+996f448dd51f345442f1738ef8915a69bced678b6dc75ce6d77c44464d4e3c23
+0f90600074b5d31a870ccb24ccca94e8597177d51f85f9079d3de3ea0978a8fe
+f94346f14cc5dca7c6476dd60ee8a7f402e498d91f4c3b8071f5238bf16e8909
+0c0d2b246fd6ac9bdff3ec3633c6d90dc5ae76e353293823b20abdaae12a66f5
+19637c1e69ddfd24df8a76807c441732f3bc7d334bdf5cd5eca5b3ef88798b59
+6f0eee2d147dd35f57c4a776a9c89eb781acc56491e6adf40b94df9e8953df54
+ebff8faa7bc3128a375ee91a17ef4e6ac5fbd17f84db3bcb2ac0468efcf8575f
+b51865ed827e1dae4a3b601239519fc552e9959875285d9f688b1f4da4a62ffb
+b166645fcc7e587fa82d02f8ea43049f9c589a3b570c16aa3d37f34f999c4f13
+ef852c19789834269ecdc91433b6351f1936b24fda2c6614a467855679c50162
+fb62daa72b9b971511e81c8dc7305e90cfad4950bc2dcfee8f1ad6f30c2133cf
+c117dacb41836efe6d5ddb09b6948475c21a7c7e0b843820564b7f4375b43c09
+a07632390ba4fb796e2ff6b45a0584f763873e6a36768ef958c306fdb23607ba
+73cfc5d53a282585b9c7c97e8b59ceeb43360107efd16c2d54f825fc77e4eeed
+50c040e210a2be8034648a0cd886e2720b8d939ef7c94df36bec7ceb983e7157
+5d8c7d688fdbea4c23b8bdca5e5719ad6bbd1384032235ab03d05af8e86b2e01
+2f6ba96703f6d2e4a014c3de80067fd3fd665ed1c64c3362f254e3c7fba06800
+389cbf12c7b229b66b2df82a823765b41235e31a1e348cf57da0e304fca82b44
+0b2af48040f04750d7df18ebf95da0101b418c3820e161be149a3459e1c39758
+e8063dd0fce331702e9f7be3d10da99033bf2c90a1a24ebc48cc276d78f61cc2
+a6721f3ec1d48d86b54f8eb88b9ef248ee50896901be472b8196f52289869d9a
+a656932d4872bab54cfde6eb67537c70078bb808bdd52f4d4b99f4d3a3d7fa4a
+ee72b2b5202983641496ab3cd44434d44062db39d6c3e8b60ac6f73ab9b101a3
+2d687ab24b84e27043126bf4eee162f2cbea040586f49fd91b5d4069704bb9d6
+9ea28a6c20643c3f92f648b38273abb8bd944e7edabfd4e3085056f4b29fbe1f
+ff5ec131de60fa61812a8ab0d3673272309e36f6a1bd1552a3c41af2a2228f85
+f3809d222aec8eedcb4baf373f37d2a5de8ecc62ea37b335b81005f8e92f27fa
+6b2397952bbbe44fe9a3213c33e368d9dc37465e84f538d969f97d55b31956ae
+7d8522a5ee6d6fdcef2cb6ecb636de6ee1b2e7b44530166f7b2eb278a0892504
+30cd9e39ad1d38748ccef3f201330fc43f12f6581162775064b6275c6959ad21
+b5486b450d8581de96291f870ed3768da5833520cc36c14b31a90232d1d1cb03
+de7f8f0a506c979b765205d82b518a8ddf6e3e494b4217e47c5dd770c51e47f8
+106f23e7e9c0c4c9b2bdbba64bef04c5acbcf437db53d1c47fcde3bf88d58f23
+fa8a8c482b5d45c78985e7e850b676d773a4ad42bea32e3090403c01c1532a7f
+ec6e008ca9911f15319bb2708a57499e19b3482537ac0bcfe8a2dd398b0ad1a3
+a4f9812a66e9b82b095da83a9925c4e9128ad159dfb674cef7090c06626853c9
+0f5d64f662ce69cb135a4ed6eab6f69df2f376f1e3ea47a1903d066441d157f8
+858848b89aa4634011799b26842adc6981686381638e9d7e2c6c8ca22e8cb9ba
+d86986aa0300a224d2d0bc6fd227fd557de7da66c301e4cac1aae538c62697b0
+b2258394e0af0333ef56007b98b436dde0d40937923c9c72ad3e3fc7b39d1c70
+2851b4dde84e697f7983d5f71089c3f94abe8ea47392800d7300f4576e9498a4
+20b537ff69231fe3643209c59c485471b2cfa89aec05e50b2ab081eefc80e1f2
+b08f772631c8c649497745610363246af135e2c3231b64be0d5db3789aa6a000
+f27ede24cbc19346557465818d4bf4c4286e08840f030dddf30bb175e81482a6
+bdbdd5c83a361a28b8ea5654a9066b5c8cfc8f5d4380cae506d6a61cf0ad481e
+832e08f17bd9bf87749ef53d858b2a9eeab8d7be7e259bf110c4e3313d6ea87c
+1ae3664089e4af8160d334fb3c3005de0d5037211f888ac0d959dad0a586b61c
+0dddb0be9c5110479f5b0674d00f0e54ca7f642b62b32593779591b4dc119a25
+1e8fb9c40b68ecc9e5a08e63bae86aa52235dea3902f3c355fc1ae6b8c79644c
+01a54c68fcbcf46ebfdf0a54eedeafd5d1120bf228ef499913b4426ba7dba017
+75d6e240dcb7d41352b5cb624bafd11fa37fde8b4d4fed2c106a34d8325fc31a
+b86701cc72fc1e8db68a1508337947352c46d46d4393384cfef2c9e9e9d6d877
+f13780441ed70e86aee02516ee87bf567857f6af21aeb625ac3e30fb1b873bb2
+862a5ba6c7a6f30dba14fea378a41d38116214c999ab4666e48ce091751ce700
+62bd9e47351e9458e355e08bdf9f38ce3f52993df8d6893bdfa29813fafcefd2
+217b481c1100b8db69b9f3a2169c2a66300af2099a9acecbc542f140ad5d3a64
+6a1d818b0cfad8cd8b4b8a3f8e8ffddc03ba62a624e72df170b65fafed325883
+feb1bd45e3865043bb008d3055c08a3e4c95734db50f983f3ce6b6fb9367104e
+c44b96c80b2fff43285967667a3edfd341ce587b212e12b2fcf156f03b0848b9
+3545bf50fe4ba75c13529ee16857d317ae98a934a369b83db5f6039b72bc09be
+6d14c88a6e2c77543796939789252b70f51fa37a69b08551be5416fe8c785110
+30264f52592909c8e7f81f54f1b37c2b488f088e85994336c7a075779b168464
+fb37e8f7085f8fbeee68fb68ca9fb2986a53878454d72a3ead0b5b2ed507528a
+ff6aa53b238380d6f0e59e991f4092d68e2001c0e6d945b810141d3d1bbb7405
+46d378af9ba3fefd4d44f6caf78b74450a0e230431a5c46e79c222c3767c4132
+90cb9f75baea5d1bbd25a718e8cbbe551441b642f10173e90aaa9b2cd6301332
+0ced79e66e38e8dd290bf36fe33244548216916fad18f20da7b73e31838930c7
+f4a9c44b3207eeb6c103f633f3fc8b2b74fb04a072fbc156089ee3f7351ef196
+f365dd43088f4d4a280be7e81fe73293912000e3e5f493dc4bfc275b5b8a8796
+bdbd266090733ba17e45239f7c0dfb4153be451a8560de6551202f700efafa6d
+2bdaac1ba9c8f6e084e4b273fb8c501f336b005cab01ffd38cb6d27941096f75
+db1721eeaec315842dc7938dfa1b9e530b285604ec2c6982389e6c22fbb126c1
+e6a21916e06ee778b51fd72db2dcc11d878f21276a00bae3cf3ad0999c64d007
+f909d0ee79251810c64c4d05a2a759ef0837cf5563a8765817003dac3aa1bd1e
+7e3b868307e2d1ccc93b1a5a0a4cc32b4eb6961f71750644fde26dc7b01a66dc
+667005b396bb597d1eaeaa55b46506d447449497d29872485b31207e460df2b8
+c5b181459bc68b686182f1b6e164a825199c3ee24c1bd6a1dca450ca1a3fde70
+2d2e89bf6d8b98e4c4f25284b667cec77677be9046ede444a1b93b3b03db6aea
+f89aa0759cc8aa7f10a2c4b36f63487f59bfd6e3a2025f26c346847fc257741b
+629a3f662e3bda8c3c010c5dc1838afd26f875a56fd6042aebe1dbf119cfd387
+477d713cdda669f61ec1070705382520548a94749711144d8311df5db319ec0a
+033f2d5c4c639b5af42549b1bcfd95afa55994389ca8a66b64c28520f821c808
+4ae9dcc9d6020aa6b1f93de064318e5e464e8808bdf0ac519653752e612c9149
+6e3c8e61434d012a5e4537465cb678b6e9a9005f5039e415dd2872ff0985439d
+12d524a4f34c520aeecdc4d96383e1edd0059943d59d47880f51729e54a68a98
+a03fd6ba4ecb567c47422e04966c4655f9bb6f74f2b82d4d3ded42a42e90157c
+6639f18ecae1cdc636bd616b60218d1b6f8294ecd09a88fc5928c37c9304d7b9
+5bc609286a3524fcfb9e406f636bf1b40573225e658543c29be863964556b622
+da308dcbb6516c88155f6d9b8ae4fe34b17374d46dc560809fccb19e87f7fede
+beaece1ec0f41102b99ba6c4de9dd105dc5d90b6d3b392040273e0703b204d26
+89d1a340d0d32d6e201b91244e07ebbaae8b4c877a213ceb2f1b1d3ca07880ec
+3f4817c00d48a06102352ef4de3440699cfd69ffaddfd1b90edb42034e5b8e3b
+c42fa37c38d563554f8d5b96956879b63866e552fb931c6462b934dd0454dd9f
+965055bb3e969b0063dfd2bc299df5552d8edb68b6530673fc08f3a0bb96557f
+13b068d62dd8b42060e67d2584b4e6fb97e9dc7439f5c1d23c6a324e72062395
+6305ab4be3a430141d8e615b9f59c3a3eb2b1267ede3987cefcc1419a9f6de7d
+d73ff35749b44018da9e79aadebafd0495ee9e29ae4fc7e420d86572f735134a
+f54e8f853b764c6e0319f720e8350110c1b300290c8bf8fa4cd3be47533482d1
+53553691ebcdfb46c6879347e3573c318195bc13ea1c732d8d0e7567eb655b22
+1960e8476188532af8af85f7ed7d399ce2ced204894be3a592ffbccbd831f027
+7ae604851702b50c4ef6482c78c77c7959869e83001f7f8f93bb3b8be70d587d
+9e5cae3aeccfb455463adef4038ccb6eb8447a7e1992929f5ff7f8e6f37d4a11
+d5815c2ae35f90d3708f3e2a166953d9823846d87d2e7a7419abc47f4044c8bb
+a9129cbda3c7fef7725b481fbfab1ec2b3700b3b3780ce5e711663557e873531
+72c3bf0ea8ba03a66c5033602f12cd8aeb3c5cd0e0abd65c936cd7d973e8d3d7
+c5d09bec259f41005187115317afe5f6ef895eac769459c0ad7ab039febed7c1
+dadaf14d015a7a3356982c242713e27eccc7c4d7237052b004e8e5f15008a826
+ed515dd699e6bf60e3a2aba08c07b8c8d9df393094069e781c7d75b090445c7b
+1741949aeddd01691102b001781598b87f493ed956b500b2d813d50ba749ea1e
+bdceede30f45e0fac0aae683ae087a635ca344411c26a2dcd04a084b5d1774fe
+c1f77079af1f9d8c75f383b3755f3ea120e2bef4375dccdd221e1811b4b775a4
+f45b142ae17f1654a53a938eab4c2b2e71293cdc8e2d18ebb8b502af594de4f5
+00795c3712a58bb65b8287aadf183e508d907290cdb9ad20e578ee485ca6aedd
+432f01a550980bf7ffdd0a61834748fa4ddd4d2f561b9aabab279e8c131f4d32
+68d89b5f8f542d881286b3c8913a0a71036058755e2f47994157f645ff086729
+67ae4c8461191607b1cd8c4c83298a4bcb397fa5eeef83c5955b301fa270c293
+88d0fa7d58b00f5f79cdb0326172364a0f4f91e189101a2bbbbd448b576c275e
+ae6b320f61d6536649b0b0f9340d25ed5ef2746f385bec9526a2d989271e9a38
+302c84169518891d336882d85871ab751c147d1c8563e2d7bf5c3a660c0b0ae4
+9c0981e96dcd4ec45c166f7a34270018a852d5108219486b706f84ebbe9ede96
+6dabe61fcaa62658db716919af23f384948b4ed54ed369dd3e6e9a4f0422f35c
+afac114819e7b1b9a09bfd1b985e18e6dde975e610f48d752eebea00ede16f3a
+0b090103100000d0c213c4425a5b0ff3193941e27eeb978c7a42dc6927bf301d
+1f0f366a5d0ed75b8a969ba68c8106466db17cdf5b516a0ab63f3772dc7ffcf0
+4ff5a952d0798b68273dcefc30c3324ab824690821875bb8580543ada7b15b50
+aa04cefb22aedd548ca17e0c334493245c2eb4fa7549fbf0833f142967753111
+96dfd986a2536834b4dbee2945c2ed76205a05b9fdef35c9b4e41a808220e6d6
+e0349659c3ac8c3f86462866807ee5179d6994dca75b8d162763e8af6107b272
+bb10c72d297aa93c0055125a426ca0ef9a2c79a4b73060ff6b45719c3dfe2924
+8ee4430acc1c9c28f8b93f554f668cf14f39a728d7b2cf00523248d5877c718d
+28aa6d2b25e1b0ef7fbca7bd87a1bba4ea760b7db91c7d8f0811840654860fcb
+ef679163d03dab048f05194924b68433b76330a6ac806034e954b80b5a7c8652
+719aa83b70fd434ede624963a0ffe25bb1f72b71f3351164d1124712d58a79f2
+5c60a3fdecccb25a5658ad50961a756880b81698c2f2915d13f02ec6cbd28edf
+587457d825544af042a800e4387e004d0701b12b8281bdc76197de64309c5a33
+463bb7d317e7646ea2e7bdf7c0a38e82b0142d51a055bb90689f11bfb8cc5065
+b98cb98bcac7c740d9b1676a103af5a3501e6949ebd9705131b7f30f10323ac8
+0e69159b8da52acc1bbcccb8096d378536ecb498900fcf1d21a92dfcadcca90f
+cceeb56d38916c2fb64a9c9df5ceada68553e18d5c5d184c8c23ec78dd5fbc1e
+f685737e77767afdf33fc585e5d4e9fa1d85446b1b209ed0f18e529dc1487bef
+57c45e10759705297476b4d9b323b45c9d7b364df726b9108f4efdf1c080c151
+5d379adb369724746185abfb9567978b090f9159317be68fefd735a62d686ff5
+0b08e19c87f668aebde88f66fbba5c614415c7ba4085b35b3b086662064ad401
+abe40b7c4d0541d69b745f7e4a9ff3657e74c797ee656e721088409226aeee8a
+be944ef94ccc9d0d7f4f059eeba35ad008e961a252119950277e7f1028305b7c
+a09a5f4c99a3620001d53d8df889589c9aca04f525a39a67fe2f776fc650b3f2
+ec5a895911171356df2bef1fb6302d4e9ec698af4e2c4320821f37533e1cae94
+a5e14eb18f98a127c498ea7e70b1c8599d4ca425272bca3aad3f499130c386a3
+ec3632c8ce2d714d309b52a90ae662ffd310e335a3690bdd831f52ac88279a31
+c3441d8e0c6a741f615c78ef08502af34d81cb1ebd03af02a3fcc76b6648efa4
+d8e550d995bbfa02d5a1d08d0625432267ed55aa34fecd3078c75e1d8e4185f0
+390abfacbf6dc83e8e6bd75094a54a28ac5c44ba886c3b586759e192beaa88ad
+b1f1e7b59703ccc55d0c4383875f8d9fc1e24d7574828f8db01ed10919e3abdf
+f062f0350da3414719c765b600a281ac245f4f7a33efdaf6ea3cefcf081158e3
+1451b8bef0e8ac74d0ecd0ea1958e171fe2a1f23b8557556927b4db67a09a077
+fca48bfd110420bd160c931c07799fddc47df1d5a7050bf1221bf3c5335f994e
+c002a6621068cf4630fc508cfbb7a89ed86593370fe36d559e4fad3853aa596f
+50a4d4fa7c28ddb348986fd5e41c12aa6612e1b8ae69723decfecf062e7108d9
+f0b6f92c3d9251d7d061911fa6d35ed466763f82f669b3ff5dc1cdc5ecd21b23
+6424453dc9fd542578983cecdccd279cd52a2461499ff3709ca9b52e7d8ca177
+b59ebb7aac2e5a65dad9f3d07694e4f161b6c78c37f85cb9d21f2a4908c523e6
+5faf1a391066e68344a9f15467991f809d421af2cbdb9692e4ad9e64d0175f59
+729db92384af1d1c9d058b4c5c9808404e72db14c434929a2ee56772a623811c
+4bd21e254fc99e23ed1f8b2997ab725f809b5b4a6a9019d61e82b151a3a03c24
+4189ad819e0c0852ece01cdb4829fdb205a3f704480c57235cef0b9960134ab7
+4b3bdc8ed7376980e75d48829dfa6b1f6b3300e2ed170771b9c450ff104ea759
+666461d62a16674950d54d804ae25dbe2540896e7a9fe57006efa21a6efc9630
+4836db559319fe389cf960f041a4d9a0366121a1ead35032afd4b699db7e6252
+abc9a67d9bbd956f5e99de22a0ca48ae978d2c8e018bff87406a9f0c437eb552
+e19d2455b56d4ef37b47577d009d14b2ce56b87966b8d2de7a7c423f89a22ca2
+60c837d76b2ba0d5c4817e9f67ea9348d65975fa356851a9e56c19bce0ef8aaf
+3905b73c16eae90cbcd8c1eb4c8e9d840cb7158f145120e3dd6398e3a2034fd9
+95f34006745ccac381cf8c36a13bad6cc4a5420ebe75ee9aa608437fa831d5d3
+3113083cad048dc0730667388bdbed24b33d2c39f67f501f948fd810b8111903
+63e2fe20f823845f9a876e679120f93ba686cf408bd7d69237538e3751c20e84
+4b48e15f86188b8b1002be79cbbc75f226bdf458193c024629aa680f650cc8c8
+71f4a6983ae5a1598821f3f4d8a53aeca3fb8b3bb4266c4aca496110962a8de4
+bf4d133bef58ce2dda36222249d0532cb935804e1ab3fa15066bd90ce643ef8f
+4446bf23d64a9212f5386f4cec60ac632c0e9d7404eb70303f35c50b3ec8dd5a
+99d309b6b7364cdf5fbc1ef653b661442b260764c902e8f4cf6f2ff2252472b7
+53fdea2300d5d6e1f77e46494d6dc0533a77fc007e723591769fea95906c9a0a
+c65f04a8fab8fae134b1227406bb3e44fa20a8379c91318d21315f730b208763
+654d279af7d123b98f2ae67bd5b2f7920187ca936b45b7e3d1da116953f1416c
+46a41d922df8d5941e1a860879c052020ee0ebb78eab5e0936b1c8d02eb4061c
+11c2cd1098e566a180e63cbc8ccf70a0298f45adbfd0db942abb2470bc1b9c3c
+98f61b3ca787d0f823c90231439ac4c5f8ffc7057469b2955b9bea13dcd9835d
+8d020f593e21b87cc8becab62522a1b1dcde669760e830be5c3bb7b12decca2d
+785e1eafc5d6bb3ddb447c2e69215f7647d4e725a2c647746b7bc7f5df9d3189
+d6239ee2a2bfded2745418befb8def463433405f4b4c278295c8c2ad9ee43917
+11353ecb9ae9685f17550f208bb3eb7179a1fa69882ee2691d280ead58e93f37
+70ca26ac429ee5b8e0dbbb7dc1876f11ebe6f1a0dc330cdbb24b9cd1439b3044
+8cb62e50b33ff18099602c620af45674a3ec0c93fde12ebde8b1543c1f9c5d0e
+0a12916ed048ea3244f07060235145cd2a366581ea658b85272d59cd429fc509
+8f62c9937dfa11d29ef22c74e5115f6383d5aea6b2de0ca4c5366800a2bd802f
+162c3e3774b12b016914e4c4e008d81b0278c336d764dad0cbbda4652abd4a8c
+213c7da9c9fd6ff7270575c6c15bf622b6f6f4802ff6f7991d371a7f06755ff3
+cc7b344749f239036a68803d367d121a24241a559e8d64b6658a8363371cd9e9
+b79231afa2f6284a1ba5b99f18bded77db058469932ebe7d405cc7fad3224536
+fa5fa3f7c643637960e88b0e50014a391c65f22424f7edada3a7dd72a3bbe5cf
+47a7dbc05a7955d77fd115db666f4858df46260425575b8ad8ad44912540ff7d
+1cc336c3f9056df50f5a0a2c34f12830f81a332b982db204d218593b804661df
+5adf14c6ff71504cf9a6faf948601e3e1319c806301ba2915fcb59208ca378e7
+afb5adee01e4167cac25e8a1da13034162c5b1d648bf608174a38205ef838067
+c4943118642ac1d5723eccb0d5e365226ff4c34ab97cec6c9ccb7b73c01ca8c5
+85ec906334753fe353bdee090a79bd56905deeb8e4314620e509a83867ea1c19
+a16599dcf0f2d2f3eae58d8bcd8da926a1af99b4057ce29d6ffdec24a7db1a14
+4ed9feaedaa325b6eb583ed87901c43f9f31bb97563afe8630df00acced9d95d
+4b8c1d0f47e02a76730b960e95fb78a4a11284923e9c91a2a5804c14ffb3e352
+943cefbcb1dc6ac888c3a332528a89ffe36fd2824c48eaca69919838665a3367
+7d528ce020fbf68a32d10f0e0708e16611e734a0b20d18aa6dcbf8c450e2b5b5
+f09d32c94c115e9ea5e1a634a163cec01c3a2f3c7826ef7e9d5e8d19d4c4866c
+84666db73ec62d9c5b2508b05145522c3da50a31871ef2b6ea0f48187c191e06
+e0d8160390c05376f0eb931bc07007fb313af86bc0934bcab824073d6b30aed6
+5bcb843f80cfd4c3ca11926c604512bb9c14ddc64bed2eeb5522a4ff3120fa47
+d454352891efe9fac5fa88660011a8c0ec0570e0430b19b698160d4ddebf7051
+079809d76d8aecd01ead398d4e2fb15423fb38a6dbec48fbad2098a9ed738f81
+f6330165622f646fdb5b148ae89168c3a1fbadedd5d316246ae37e45bc896d9b
+aee9dff0f5de45b6121e9a092ac8b4c69eb61138505ec66b367f59b0c6f18fec
+c0010864ccc7a0dfb650115242f1664ec78b1861b93a82a793ad55f1d27d646e
+90cb1534ffc6b9f5e982db9070fc65c9559b67285696acb8c7d680e7f109890f
+4df338c15cc2359fdc3d52bf64c5f1f04d2507e80b4f3c51e68c2fc099e56fcb
+89d0d3bc15657fce79a2633352e4b6ef022f23f17c9b99006de8f42b6c09f5c2
+c82d88b0bf013d7500e10a838d34098b657ce411d22916acfa09121a3a84fce2
+a6ebf229b12763e2f5777c99234ec8a966a7858330ef3330a3e3d0fed0c8969c
+1d5099164cc9f1dc0e398cd9d30bcaf6a4b02b148705de166306633ae538f9cc
+e791641864e73441382828e68f406a673cae2853224428921ffec808630e495c
+64ab4c0754cd85327bfd7fc9ef957642a6d30258b4b9d29bc059392b7e3fd339
+5a6e5a0eb060f9dbd09d110fadbd8a061b1ee8880c61e01fce52c661e1d1b5f7
+cc45b9efab2880a9bea2bd64e8ec183632b1a7a996baa7eff23b18ccaac6c88b
+1835eb751974d2930c1f354f5173c9266918872eb7c70d90f64113f12b86e6f2
+eca0e29c4d4c20fa26df487b9be3323ef6de8dfad3f81c7a23a4930fd3a1c6ff
+ae0a952f8e074f0f034d4dc16e0f3c11463a57ce0c14fba845733568ae0e8f46
+1a00d55508e00a80be53d818d38acc4f7212c628cb2047f18bcaef075e7fb3d9
+6d090ea9a95a21ca3e7f797aa2934653176bd7ae0e72c52dc19196142e571338
+572a939790b6fab6592f56f8a876c99bc0f9d04afb37da897e2dd8a3818f9134
+ff6dfc66c7142738167ac5688b4da11dbbf343aeab2fddc82631fc7f5e3e5bae
+c857ff3757de3c01f097db34436e6d9ce0736734acc91c447277baf1df07c5ab
+ae7bed411bb203043f0c16155e38f62da67dff78f94bff4e8ee86e0261628487
+0290e8b302f9941c60a04973aaebd7ff2c4fd0c5fad6e85625921e641b73bd91
+8ab85ecc505552442e1d1802460024a9d3f523fde2d1139733fe54e74dc1322c
+0d67261490354ed262e2601098689d583f0feabd9b90fe030812442be5645817
+5fcb9cc603f45146004f4f06d3b6325adb329b6c0686403678ee2901165cd44a
+f85d7977c6e3c84a0c5f5d7ac40be097e91259eb0928f84166b2bea549d82ffb
+e2187555d160a406a8f273d18778b42f37ec7045734e579b9a7141f48559d8a1
+2a491e774eb885449bf57ed3737c42ebfeff262bff74eed2d0a2a8915006950d
+24b1f2ed4bccaaa7a37f126759e52034dc7264eedad951a69dfaa509af59549a
+f2de1eb4a74f18abd83d7e8bf00ebc4755ca342836ecfdd0c11221aa26c187ea
+95700568592036fa3a03b7378633984b7340e8080f82355c2a572579c06d2c86
+b3602943102236aa99d971cd3fab42db95dcedf44d04c8f05a2a1381c6e9c58a
+a526db2ab99b4033ba0d339ab2e1d93b57e691e66efbb79e627f2d0eba005080
+7ed4a60011be87ceae68fade61f969daec01accbaa640cca88a550c7de3b13c0
+ee86bcc1b3d11cb069bb2c205c205b35da97c554e0eac66dd8585bbfcb18c8a0
+cb3941d9eef3a493a2f7dc187a480d21a7b4e5285de82b81e04d6e239f7807a6
+6a334b6c7bf6fafe27c1a81c3957ac1590de2f5d74a0c9b8a990e2dfbde91298
+30c494a28e29557f3cb2ae8a6637fa3d83eb11331f8a3554a1419d8c5e84c1e3
+35f4325506a08bc8ae49a113a5c3a10f6d4b5446fca9a287d02d8db0bd53d706
+00137e83ed703f3e72e6f5f3f2fb14b6dc3e61c1bf7d910f03610666f5579a81
+644c5eca8ba56df6790561ecc91db4fbb7872fa49d2b7f10ba2aa28fb9ac85a3
+4d571ad54e98993f94b7c551e237b314536681cf8e42a26144ea39d9f9a9b295
+9aff80e5b965d2b3127d89e27ab1c8804c68ff4a1ab678ad2f1bfd5b2df38985
+bce719099c9c7a128e29ca4ba9187ef9462d4059034d7379a04aba778756f48a
+2373c32c4b29b769c6e9fb458bacc4bf10bf25320d757b76bacf1745c0593e2c
+fd20432e32bade5a213d53c77d56d34d67f436c1566395e6143c36c16796cb27
+bdbdad5cfe3dd3eae67b9474edd492c85879169324cd9fdef636d5b9c3abaf36
+edbb4ac14a98893e36dfbe7316c3b9dd3c072327b0bee8bf09fed0a0262f0e5c
+08bb6dc0474ac2b428a4c1ee5b624bb1833be8b2f4b14c254d7129a2162308e6
+48035a579d1a7c236ec5bf6255630be94bf6129b96989b933f9ce2745f69140a
+515da1fdf975588afbd4c39201ab3ae8b016635c2bbbe6aa27bdca6215617e2e
+b9d4c0e1fdcffa313d847929c37ac7b07f96b0180fbbb67f46e9b6cfcae1a66d
+4be0afa99fa7787e7f8be5a04e723d4f30abd5a25f080f51de36130b7862b025
+eb56872f2744686fe5959fd51b40e6d11876f61bd9e3eaecd57b40441b5bf86b
+378c83c4deaddddb6ea17f6997549eccdcee9a7a48a24466d9c55ed17d7733fb
+f323871fd7bcb8414609729c27575ba9fd429553e3d63ecdf78851b72dabe4eb
+9ac67e961a4dc4cabf70946cfecc144a87dbd9c73259455199412faeeaa3644c
+2022489a3136253e1497bb3458be497afd4c9049c4a615a102829c85e9a4576e
+350e14d837372b4b1663d415cbae6a3a954880114b13a1c214b61ea6594127e5
+742b98849778893631b67469aef59b15919872a7cfd255910f17c53576f03571
+0819d59e2c1a46d0dea64fa7e03a63256a8e65047a1ffe4fe36497d2281b6aa8
+9ce1ec831cce61de63c1da6c8a5672c25e5a5a7bf20a7f933bfdc45cf7bbe921
+42d8625b3aac773bafdcc614425fadc62699de088714b72691a1447506e9a37a
+093a960287e418f2e530c537c08a3e19ec7becfa4ca9320b05f27974d4c22ddf
+61a0ccb6b866d8961f04f9f9b85809bc57d358cd918dfdc9603b32fcbea81d3b
+e193034bc458941b7a413947a1594e46b4ac9bdfe9416c275807a4302694f7b9
+2a226b8c96361a389a9bf92a47b28e501a06d91d6cd54fd2d2707ce52af2c279
+24c6e95948a936b4e0cea35c6bc62edb1f545a28ab762ebc3c2ab4c4c3a670fd
+14cd631fb5fbc26cb51ded32461d69a2b82c586c40cd1841fa3ba780dbad789a
+cca61d286389dae51295141ed27217db047eea3247388390c153c22f949642b8
+5f87f9193421abdcb6d4d3882578f9e70b28ad6238d1fc25877f42e3899ed0eb
+4cd9f13eb898e67a053d41ec485931655c9575370aa8f4ff30e8d3e7a26bf27e
+f32f2ad0bd244272f84af019bcb2bc6332142b4d0bb50fcfe13b9c4e9883f0cd
+c4773686b3ebc08b1dbd2ee12fc3f18a28be0610c4bea2df1543ae37dabd861e
+2c1a65515d8bddd314f8949b5941aca77220073b541c9ee457e24c862781f8b7
+56fa69719cfcea364a0d983989ced39ba2c910fcc25a00601b0340d8d6083f54
+73d54c577012ff8895d006922198cbd4718e32d5fdfc56143818901a670d67e1
+0971b6754390848db25f059764a99801ecb89fd0f38d46c8da9e01011bcd32ef
+c3c445769122d22409c8492bd716782f411aef4b4e0f1234a936e71b9e8a6237
+9b18088464e5e2934c748c64d69b3f2a92d81a1e0ff81a3150013c3764ca0194
+110f7691c3a2b8809900ab262825cef744812fd2f5566894ba1f8d2250d98b89
+a8683f736d4b5a59f51f2e332c8ed0751089e9c51b91b1c54d9b260e312959f0
+c692d4a760932264214ed92c3ded8f45ed1f40dbde9f20fa7a658b6a36e121dd
+cd1bde6e0b8942715b5080fc04a7e6db224faedd8c5778219966177e476de4a5
+a22be484107a0c11d6eaded4ebb020651777add24537d83b6d61b407df0f60cd
+34ee532a9343f5b4231937839a5b330df795293ffb2b27521d142065edeeefc4
+8fc9f90558cae326dc18fa6b759e4d0db1decbe33bf3c1fae3e3ab33c9392d62
+429506196d1050a0e2da447161f2e323103da806d05c40618288fbeff828d053
+d3c6e0b0718f5c9afd1581f0098eb9b97c35aad2cd590e97f4a139733cfcc7dc
+acaa5814d3b63efee7c764c0d0a8ef392e3dc3f02c8e89f64fae8ee4ca05f115
+4314eb98e6a0e032886fdf29692dfed62802e84b48e4705cc0c158eaccf4d7db
+dc47b91aa3c5a49ab600679c13a2177c145759d8bdd337e35c77717a15c55caa
+45339aadbda46e71a8a018d7782b9299a2681c927888408328699da5b8cbc45a
+b2705038b818fc0bdca4d52896057e105c8b859a52676735022208a76abe3f53
+3b012a22daadacfdf29aefd86f67ca840a1473dcd650f03c315f23bc6ed8285a
+9d1796862a88051a4ec70fab8697b54c53260eb12edcb9e533eba6f9c1668f5c
+9709dc308f35b927c974fb1d0c58757086770e1774485e757f1b777db15a6857
+1c084f48b3da6102cc7b38dc89f7a8e33411d7e35434750ebc98dab8439fce99
+37f7b35ebdef734e2b44faf3dc68fd5d36e8fa82face91146ca6bfe1b1710cfa
+60a6178c88ef1137074956c67cf81e0052d7109debf10409d752e3f770933111
+a069a639005c0e043d42f622e96fab8a4a203857a788b32d3b2b5867c7fcc075
+9236e5dea77a574f4cf964d0cc2cc786c6f0068049f20015dda844a3f409b4dd
+bc95f22266529f58e88eb9568ba44cc414535075b383937d1b33135f77c87a92
+9e77f95cdb0d71a1807f1193ef20ecc1c497df8e08732505288fd2ccc73b77dc
+cd88fa4db43b9410af1524b03864642f44ded44466b5caf39429e2c18e634834
+11bda54bfb6787cdf3810a3f2e15141d7dbf09aec5b80675d65aec2c66e80177
+4bd3e428b6d111e50d6f2e6b5d4d9140689598d147473b57a71ab08080d37d2a
+7f2b89ae2f5948eb11f3808542b8af7ca2173bd33eb513c67f28bbeb87627ad0
+09fff3b7ab0c2deb1668fb922649bec4bde78448bf7070e07b101055584d002e
+93826a781916f1e2ac7d272f1a31611835737d54671d9fe6e5b9460b15e39fa9
+af198819f2073d6dae3b3763abc240732d719b0b1c8a37e5f9f76103e770c0fc
+9343289d8cf3cb969bb4053cd377413a610084e68c255a6faed405bed1e4437f
+f93b34eae37559b78cac42761c985e280b92c57c0641c244f3c480bf57fdbf6f
+8749fa7a45d44a40f690d7cbf8b6d9d014a533b8fc62717eac7e069cbe6e3a2d
+62e0472bdd23f0edf4202a643c8fe7dcf37f4401d94abf44c749ccd26c2d8fd7
+1ebf9ecfcf534280e759959c243f3592622d799747efa7d5b75964090e0a3dbe
+dd08b6106631bdfcc890bd5eac60e6fe316e1780b0073eb4a25dd38ea62793c2
+e3e73f9c4ec3d85693dfe038e322c2c383193b789d13c4be802852b7c877cf29
+4b58bf6ab55f9d6669bae41771d926ca28aa6a58c2601f13fd1aa9b0d7b481f6
+2be12d72c3ee24851851bbaca53045b24823e02581988e4432a8ec843866efbe
+5f4511418efec41146309c7a57d665d8ff4d97dd85578dece104a6589ab5de28
+c7d3e653f01ca8492ae98a2dc7381be07abdbb5ca3ef9241ad0e74561f9f5a15
+2ae8cd680fe804aeb1f73eba506da5aab780d3fb24f617957f7835c406c2f017
+1bcedf43715e7f01450f3bdd30c91d91c64e55ea52a8fe5423d54965aebf4574
+b1e9c8e46c78b76fb87df3491440a7bedc74af6a963a2d34486b81daabdf4ff0
+37529951e2bac3dc85ae2aa8a2a0543a0093d2ad7a872369458db38188a346bf
+43d572f9e2bc2f780ec70a5b65d94aad411f888b5592894da1e4486a0d800895
+c08accb0c9b8e0f18c0ded81be332ce12989c95f451c661496fae73ed7f0eced
+8a9a8754bf7778ff0b4cb4ba16c67c9a905e781ddbe16bfa57462ccb6044d891
+83d761ce58941024a403ab718764a1855d0673d39975a286ac8b32f043176f85
+f2efab1f0a0fa4e32ff159be4f53292d5e393262e9f6404b6ed6349ee4ebb62a
+53dfe5dbd2f6b63960cb2bbbfa89d7c66a44b2cf5969ee98854cea3e04b27c19
+f4c79157311318bc975bdcac70548ace6dcce4d71e506e4fcf2eb115309a4444
+f76b6c271017a02953d0ea0a2448aa832e89ab0895bcccc314fb02968cf754ed
+3307f1c59306eb773d7f63f7b853f0ae71093da716b6c0b8ab469c2371c26c85
+193ef171b6c1f48c559146d1989af1700d9d08e3d60ffdf548d5cf7a8712c75d
+24ad4dc445f310b625c4a00e0879b95a2699189cbf88fa7d2c3fd191c963164b
+d363998c215e8121fc3c44a6fd4e428b244a8f2c08b3b058220bd3a83ecc4b12
+24b192d8972430693d9692a045df6f8cc8e04829a1ad2a690cb876c8c72ef8b7
+d04a119430d9882aa7b4994581ea9d846970e80f29c4e4386e95cc74e60d1aa0
+e0eb8d5a9b018dcb3bdb9fca1605efebb39bab3abdeed7585fb989f8149db2b1
+22bff8090404c44bf2da1492a697454804b85f7f282f78ed0df62d59d911107d
+524f6db56e00f897c58015f399bc82d4340a62025c7867c38f7063dc4012a795
+016ee4d90f14c9568d5e324e73f6b7c57ee5f5375907bc403584eb44a4e33c4a
+88435a399d72307c46d232ae24b3ffd31514ad891b7af711263d5778f1412db7
+4b8302b179fa8810340f07fc9b0acc27c06c915f1b45dd1d4d238e00796d937b
+4539a8386a42d6d73f66bea6d8719d057d54cfcaa282c4f06336c05285f53667
+8feeae08253eb094aa8290c40ad00bcb7e1ec20cc99a0ceb80f08f8f356657da
+ba56829d869fde7c4645559f9349d3371efa44f58bc36aa493d9a0624b5ed4fa
+935703ee695de66e687f27e1f88228261eb8c5a42a6c9b30eb45227874f42fa6
+aeb66ec01d1b140af535b1e28d625b1849d2914cba6545727125f739bcd1da1b
+7d1fa4b2b0e6db67c549e0e2e52953cd3a240d3eacbf623043d28b119193923c
+94a15f4773e0c3c974325f608d0ac0015cd450c02118fef94cd39341b8b1144d
+9ed9f3764f3c7796bae60881f6d9d84e76fc18ed060cc82a1beba6123f05ea92
+9a3f3eb82e2ac1eb99e5c6ec65a9cf2a4e0b3c9d55816779edf45c429b49f553
+f0ef1f1c49966a822920230d1a4dd6c05e2c676780ad7a6cd325422d3fb97cfb
+1594f9a31f0730c1c811cd0533671940b3dca7a70fe1c1a79cb4d22194a7eb83
+20323cea558fa1ab48573b4d56fadc9ffc911c8e94680985b0f527be4d218233
+890a06787f21b60b459b8a712eda9797f9dfefea8bdadd077e56ae6035fd5ce1
+a405b543538c790b0bac9c7586d9677feb4518aa590e4b4499902f8afba48386
+908d301b8ed6b87ee1f1ddfe80c37eb0affdc1406617d37613b9a3e82694f7b1
+60b2d43483a09811ea5f50cb559022034d9726c0f9f80b5efbbae8d1dff34e34
+ad6a03f4fe2396b50560fb8a9e9edb2c33f359728f2ff393da760f9a50a04320
+916a583228be053cf17a11246d97e2adf68891b73cf607a1c2705f9066408137
+39c09fe101bbf9d6f0be4fea0b469c47ce6bc2ab1df3c5123f3693ecd1e29ce4
+20f76b77f99d428254f8bc54f0cb95ec02872f83677bdb3aa42e6ac247df1a65
+9c53f63c779b4a2d8a720818033395d47798594ae67a5d84c4505697991830a1
+b244d86dbdcb1b57499332ce0c7c2b69275d043e5be094d835ed3058fef037d1
+2771c26dcbb2da45f3cbf68f219f102b5926200726ae55a214c5267b0ea374de
+41c46376cfb6bc7c005fc5cc803822982133c604261f3a53c7f100fb43638195
+5f4d544d99e3296fec2d65b9f3c40636cbf6f6225e62498320a0148dea171546
+6ec4a4a6626c2d69ade49abc9856a5df5d5854aeec16070750dd7de29b8b6a30
+63aeb9bd772f232e80080f9eec514e8578ff7a51f2d7c083695c50c679130a83
+9708e9e7111c6d8abeb055502ab14e0e4310676140a79bc88a0cd652eaf497df
+e92d43ac0bc724892d7b44926852030ba229527177d42f648f829a77942f1181
+2aa4688d4953c18b231b0e016441022450b6a4efa08a10b550734c2e7794bf06
+567fec60c62d0c159025dec3805ee2ff0e85280442f9cb14b5e6d2cbf2035153
+b49ba7836a372a2c4a4173e2b63fa839a368ac7896cfe0e3c8b131cc4385377c
+b4ce47bb4fc50e84a24af35342cd3356b1299d421ef7d172679816d7bc8ec23e
+e778c76e59cff801a01bacea63f46568ce5fdb81e5fd21f9f43661a9f8815df2
+58e2e9de4b22fb8b2588cdb3037b5725ebdd76a50ebf6afe36eaaa072aabbebd
+0c6adb74468395b34b84567cfa423b19deb4bc67dcfc86345cda9a9f511ee834
+a17ea4349584ece87b4fb9ea12718c600a1122c1096326e06e193da1b32f9945
+3a40f5f8b37245c99e5225c6b75e1bf9af0fb5d0b522fad58eeb2d6dd9d63ec3
+d9dc272a5a80ee7626651f9181ce39fd8717c870dd9e84ce9e942e1713c11c6c
+87acd1b3415c15ac0b34c24dcdfc59acdfa56d5980efbeba111a4e5693c341f8
+613d83b3a0828d48ea00487e55a27ee4be7b93c30bee530ebd2bd0c41e280f60
+eaf6ea444894e24af55e04c7892dc8860d19dbd60f157537e2808d2f561bf869
+143efcd8a7e3e52911c58f2148fc2485e3b37ddb23e933b074638c6459c09e2c
+29244a49f8e7fc86546210e7bd68b966e67333b2b527ae20180f8af858a1e2d9
+8f21bda6e98e02f310ad7960411eb89bdd97e79751ce5cad665ecd7e7995cc6b
+18969d13e902252b163eb346857d6b091c1f8e5f7ac76284be6eec4748e92c68
+4e31a333726ff7d5c1fe6d9867814781ccbdcbd1d270cc2bf014d8d1cce6a8dc
+b184f18651a2cc94975385906e22128aadf419672cc862aafe5110fd9cb4a028
+4f7413e4e244638660a20968a81ad923eaf27bcbb2aae3106ebc5050a69a92d3
+483e3ce8e5ce69f06edfe2c6a1ead8522184f4beff28313a8109731fda4228ff
+e3ca429f4cfd68035d2f73667bf0510135d968fff85cf8c516a09cf60bddecea
+8f889b3e63bbb6ebd2ef7ccb0db00ef5a92794b162c82846f7fcf3e69bc6956c
+5e63776482c6043bf73c986f1f18c6abe32c71f1a4a74f29e9fddada315f5ec7
+e4b38e1225ae92ba3dfd4746ce74c1004a7c2007bc3a95d0bca6eedaacc05078
+ec3a6e32fe37b6b3edc1ba46da5eb8bb981867b64404805dcc91b4b5e0b527b0
+be53e79596a7db573edd7f8f913082e0ba15e74d44ba59f89b0c9d46fb34c2f1
+25ce2d967c903debbdf67040af8bdae68ee0b1e0e89d79e9bec185394ce531ec
+8ff639e1ad00caefc9fab3880fccf55dce1e3937860b25a47aab8fbe4376f854
+f0c983ae950bfa5b53ac225557d28dbef67a89ea7571ddc8a710756833bdb5c4
+30ce1ddcb86623a82cb4e1be4ffe6b777a0a8409813580b75a89f6a8c65e81c0
+168dc486916be4f40cafa4cc6b1e22d553963b2449e00796e459c57b94e34ed9
+5bd4f1eb9798dac04521d031bb8eef565c6b0740dbe50f0b23fb1d799fdc2f61
+3f46ccdd6ff1ae778909d8998598fb796e9921069e4766ed35a4422782f62338
+a03acbc0a8f42c0dbba5917d7fd0bd2481e1835c660ceb5f2334acd57d4427d1
+97a82e44b0b172ee769b69666776597e018d05db7161e3662e42e0a1d5906c96
+607ae2ed6447cf857ce0e58984d8c363a3a6516bf4713c3b23246232b5e512d3
+9d2c3e32c33408c03262cbf5c517772ace92982b59db582ff69a3ff6c0497405
+8dac147089c03c44bff3ac70e685939ed4c1097a0cb6bb9fb399c5c0950f6689
+5dd9f05c7d262061813eca80e87e2c15a266f5be420796d1493ff3547db70f66
+26543b1ae2d6fa431c5623191327118c5eb8b331ccaa0f198f062c51cfbb981a
+dbc442937f63c8a0c179c2c42d572564bb872a2e6dde8e7d680372b157e840f6
+6fef984b009c671bbec94d3446e85579806b11586cdfefd7e2bffce117f15e11
+f9f47bcc9e390595b06f7207f6457cf1f775f781af79b5b825dbbc98e3858056
+aec61f1d9210769e2761f216f01038b24422e67b5c2386182a4e9382d1f345c2
+c6ee6299e4e3a784a4166183be9c2628afcc27c45ca0391975591961cdd718a4
+3c3d1b85b793b5f3a8b6d7590daa27eb6e73ed7726d9719b43aa13e05ea7ade5
+ccef5bf1333eb7880311869e055391412a9f09b676ec67c994783d0be74cca70
+76424bad7f2c80f031c2dcfefb6f2c8e352d2f41e010dff7d37dd6b03cfd207c
+6308d43432d33a1ffc94b74248b5db43f044764a865cf2a6671bb1f99a7e0df9
+5ac614da6c4a8d2f8fafef2fb5358abeb8cf307e0a710a4d3670de75e0f19410
+3062037291a143c03375e6c1fcca2711de12aafacdd5c071adf22c86dc240ce2
+d5182701ade04a8a1c73144012e2b88d4d5621e76a26dd5418f171f8edcd1dee
+3bbe3d6a357b75a217a40e79545113fa2ee15ac0ea2a94b8abdc82e7645596b0
+d8de0966693b23183fc34324b6b7c3d58dcd3b9a6fee2df7b599ec03fddc0aac
+632305776c092c7d45c8e64d9f4c4d9342fa55f9777ebe941ac71abcbaa94a0f
+91ecd9ba159dbaa6ab418c1bdc3580728705e782b6259478a036df71d56cd481
+1e5679ec3fe59432d6491749a9653ea2f22a16cb93a0762e7f2aae8fcfdfb43c
+fee053fd488746b2af285ac480e7830a0707adc87161595a5cc5a81feeaae5e7
+64a73af7c1d7e8c1dcca93374c6dcd57d0d31c479b3e89ff4135e66352290dac
+86d663c53d9b2f4479662c8a335b00ff194e0a29931552a641510bb9b08dc991
+78a63331f41b7a813ac7640ba77d7a82d9f9823f75c01175d49d2d357b2be420
+b8137c3821f260f3dac79818a7d0b4d86d257af7bec5fed35f157024f89795fb
+6723d13bdc0f41a1d16cb2a232d457376acc0e332fbe5b36344fbdb22ca8d7e5
+660a86f3902f04ad8e1685998888baa9b4e0a2c064d2b63cbbde79914f3d8f52
+059ba32a5f541fed91ab69b69ad53d045755270345d3ce9916488f75bc83e7ce
+041b96eded251aaf6abc9b68b16da135f86c74556394cf9042b54cb38784c548
+6491c83043598564ca6c64ac7e11fa09a3c3887fe20d9314c21ad36136e6bc05
+561c0c1e9be70079846939b84eafbb0030f4a9b0cb303cbcfcf16ddbf44cf977
+7f8cfd027bad28fb461b29ec10abf2655341f0dd3950a508026ad4031f3b139c
+1e16220521c36ed2f7a0e11780dd8982747af44a62bf13bba9eec406077238b6
+a03acfc88709d33589cbf6341eb2d1f3b096e515efbca9b6c1328d97cd840dec
+e341741bac9d0ac07f5608652af7caec833adff0e09eb728588a6da3e3fa8038
+b8c1f4105a5fd50dabc7609007ed1ae39a0b75f11772a452ca7d0bf856e0c9f4
+fa0e98d21df2f47924528b6edaf372b937d3fc9e418b6e5c18058f93d5bb0e28
+a2b59dfb5b5924cb20ba6aa4a5e5a1239e864c9de629aa43543ac2eacf89f6fc
+cac316215a3295ff05f42bc41cd8504f33326a3f1596c63f15272c2484386544
+9ce9e479d5df3214c91b9ee0fa94da29036912a6b1f2b8e240a9123a6854d84a
+08f647bad14829aeb65a3a6fc8b7cc53723b992db9a94d8057c33c369b401ad7
+1ddbf2078bd8ab303b399002600d5eea4dc469535562e1e6e50366728441fd2d
+35cd771495ffa169ed4cdac01f988e7ce5c7d28f9dd0093bb95aa97ffb30bc6e
+dbe2f71fd3e70aa1e2de7cdbe96181e92326d26a7558b1c1fd0edb58c63c51f8
+b1d741513d98e06d3769d64e1a11fae44088583ee7584e867b9492e66e9c7114
+3e165b672204d444aad59893d4212860f074d1c6079a261f6b3e7383c57ef17a
+bb600552eb518698dff54a48ef83ac82ad43534c07dce6e9e32078b5d60240f8
+b10d07d496f7b7de76fda6a20db434f88428dda2c5320a38836ab84de94645f1
+c511080a50d8254c0cfb67bc3ac2101c401541a34959107898bfacc611bb2962
+f4b3d1ba8006c28103c2a55286bcdba75a3537f81e7cd0d02a9a3f574cb6389f
+332983f4cce98c6d53089588532402a4b8a1d1427d550c7015e5ef584272160f
+a044a92664299ddfc990ab0c885666c09b6f2d6240db9f7dd3f1a53a6e312a80
+963080a78e62169cfc1a1a7a329ea2998568d9be427b7c69a504e56a7920246b
+1d12fc87e7f797798b01aa76fb34594749fca31041373c2a3e1bdf066c839925
+fd89b479bc991372dedcd1c4588f1edc25ba46b7f48c93d13ee470727c9f9f27
+dc942e55051d4f73af8ff183bcd232980fa907def8f9192c3397552070d0792a
+0e13f3dce6f14c904992940dc66abcf4de5d31be037a8d913fbf6f0a71c8a9b3
+ae5962d0e70397aef6d42d2959bf2f4058f4aa2226afdaf9a24f326e37d3757c
+56d590dfc41a5a2313ef3a460932e71c16229a152a36bfda828d9d68a411d64b
+24e4854cf677d412d22db866c7f70ce1514684a9a09203572a0b5088ce3d2652
+b014a181389279bc373f982b2db5d6cd9e6f17af6dc1ffa3b05787c50ff2b9c5
+21509b9ff68384217c6c8027b93478e31083e71b91ac4e8de22352ac08aff22f
+e7821600af0d1a269bc56a658bf7650187545a1ff2b85d73c969199277a76dbc
+5f1246a6a7066720b62310321411be865fabd80cba515d52a7767fd70d3bd0c3
+7512150200b4d5e8d92548bd3f392c1f16a55ae1a110fa2bc6ebacc5276e6983
+7dedca629ff9d85218522e59566991ac22a584af50b6155873012e623e64c0dd
+9b029546c73368bd1a91a1365a3082b715738d2d470b4c9cc0798a9d09756157
+74b8b5fe0fd88864260e3af1b2cb01f3cb6123834a468fcf1de1f1ecf945b6fb
+6e2eac258b0e67cbae19250eb2ec927bacde291d4424d8a5537bafdc2bea300e
+3fb2eb614a952717fa35749dc90a683359dc426d901b4f407710ca529f6d100a
+143cc3264586dcf7aeb6ae1e0a38ce7efa0bcc2eaba937bc1d924c61fc4e1214
+f421cab4a51d223cb75fb13258fe0cf3001760df8081e5d8796e329b2219c773
+4ba75cd625e873f19d07dca484886174c089c1a50482edce0b33e297980eca19
+7e80f95612562f655958510bee41fc43eb6f3a6dcfac20af5492449994016664
+9f17f649844b0061a2c7bdfdc3d2d8bbc0decf248382e29b34ec934c3be95aa4
+4d7ad210d2f47c986bb74b162a8b646246e7399dbf42e744dda2aa4814f184be
+9e5b40762dc25102e036833f22f02ac701bcd27adfd7233ecf8178c4406372a9
+44b2afe64739bec4f3fb782643245a7c76a541997da6ab9273d73b506775db1c
+d1009aab2289e1bec86bbb3079e20b0814a0dc046c557d8d5d9a04fcd8deb4ac
+76bc3daaeaa95012770da4d96120e153595c2cfde91499ec1d3371e4f662cd2b
+4b3becdaf3d3cef3c93e786bead46fc8b7dcafddf067e43571cfac08e2c2a94b
+33c824176159bd0612cbad0ab9c87e86ecc7e6fce0e09ac6b42aa81cf1d216ca
+5fa78b5e5df6ea279a8466822c15e9c570c782dbedfd2a2b472dd555551d7e06
+6c582844169e4ff992f0632f3a5d84d0ad97e5e0c87471714f87fd02f7470af0
+f64820292af14a29e600293cd53e9a0d3c2e4a10386913a08d3aeb81830745be
+be651d33c4dc3f9a362b295a9feeaed0a2691659f86846dbf96fa73d14c69fb6
+2934e3e881ed6ac09db98bc4d485ce9b190d1aab30e1bb2c6fac242505c68e89
+d796cafde8195ca0922fe315295e9644d4aed6b4f2a0219784b3933b7814ef25
+5e37a309edb100a6a9484778f195f1b89620e7a46a719953fbf11ea504575842
+9deed1c4d9d9eeffc4988e1db72107ae09873586256168e00f74efc803245f90
+7191058b70b823f06a5728d4bb2cc329b16b1622efe45898da9c3eeb82a0faaa
+ceb67820fc60ed2f8673e089ede97024fd7e7a4c34fe53882f09aaa8fa14d5a8
+ae172b4a20774dba7e98bcd626b6f45ff15742fbb2586c581a44ec7197019e2a
+2a53038e15af2acd5e33fea4f76617d0bdfa83fb7a8ce070abbf357a394c166b
+7991ccfb463e4178d4d4e92629e993639b9f2e80d90d0e040e12c60b563426cf
+0c5444224aee42c44f7dedc06aa93cee86fe7407f2f8cb11d1ecd71852eabc6c
+bf25ce66d4776da60f4daeebfcafa240620e9b261b922ffb670d53007ad659ad
+f25e33c2635201b15afa6ccd81b225d9d59ef78e6f809d03f39b6b968deaf8e3
+b52bcc1c9dc6ff4de50d039d98d0b36773cc230153fa438f8fb4de8fe1f0a3d9
+548ef3078abac9aa74bb499fb06fecc78db02bfe48dc441510a393f6805613f5
+a762368142cc9b1de1163bee652ff1427f0b81836d42bc0d50e70d29846115f7
+3299bec648eaa5afa960b13740716767c92e4217eeabf4f42f7773cf08eede70
+c092e17c4be3e18a0a18bed80c2538f53dbc7410e9cb346df72070dceea70d68
+fcf91f821da4fac781a50ace3f2fa9a4d1f843e07d24b906ac9eee025ba98f5c
+29de05050c88a0c5e645bb6abb68ac9b6e1b376e32d0e6e312bf8115479bdc62
+c34a9d7dbd3b7232e96e326352c6957e316c4b68befaaec0d8c72835a4833ee4
+e745985eacb33c712ac6639f86959c11d6d22c562de124dc83613e86e9359c07
+a1517830eadd1286aa48f699258e9d85678cfeb425680f630c5090d9528ee401
+26b02bac723e01b5613ef12dc9dc22f5c3061f5c39a3948bbb46c20fe86f48c2
+408bfc9d8b8d6bbe00b14bb86070df73dd49d24db3e54a11c87aae6741ea6419
+bd9a370f71f23a42d2ab67d8a7b01967416dfec9220c40ec6203e0739034e29d
+ea00e150ad58da630bb057036216c811a26fd1dc945babf9af676f1bc6f67a7a
+f4ff3f67e34677c5c5012de714c8c0fb6ad2cebbccc73f82b0cc4b52eb992756
+51dbf27622056839bb491674f7632c55a7ef67b249e3d695a883d6fc0cdde6b3
+79cdecea565d186d798bbf65b2a420896dd075df38bd54236a93e968b5d9fd75
+cb178a7da86634231332d0382a0e58c800f453e1fc1a83e19d17115037466395
+71987f6a57092bb46e0ae10110f84d1d959c567b67b9603d22b4e5cacdc827e1
+85e5559fed3b702e3f79577c4b4b3ad26fc8e14a326a94e776f4ada16ad544e9
+0b3a19012f9d6e0d3583f2e814648e86c01561fed1bddcf9b3cde825d0a7eec9
+bc7aeb705c5d1476ecb00ae4b8b8a021bf03aed4e7c9c5c6753f04fd87bb1322
+350e6db3ed8da517dd938dec6befce20a854567d0fcf1a339149369235889a1d
+70f31bad6c47c4e52f6b2e785a0eb141359d76a05a37bd1c566e36d262780140
+65a4fd366723c4841bec798e4ce07f542183a01ef9d7f8f1979539e84ec241d6
+6aa2339275f588352fbe3b05bbae7b7bef3485b05641ebb8c8e3a619c48f93a6
+dd301f0b108ae77b36302af00e580cae0174eb6d30813e9ffad6058e7e850338
+1e53adb9f1f03d8524c4aad05c98ab3862d6d7cbbdcd7d2d7bd921a4069392b0
+bc8a531ae000b67c7e8bebd6b94c2c79f5303c53b045a47be451a3f10afafc76
+a28360374d5fda8895b79d1f94555c30966e5ad8a5f58d54b2dc4c4c39e47b1e
+b3b2a57c8f7aed2ae776fdf4f839cb77f1b2be2d23aa4cb8b8d205152fe3f9b0
+d9002b58fdda551caac65569880995267003437b20cd3a72c3ab3166e593f3e2
+c5399da8ee17843e224b01cbe2c1548d1054a8c21304085b93b95caed462679a
+fa228b98a458e5f08435bcc981e58e4cdc310f57c85478e56b186a65cb8ed185
+4e8a1b30ac414152cabcf2773a2a6c296f89fda5f0cb3634288291022eb1e608
+d7d0a8a46f84aaf58929f8dd7097ca83443ce71f56455092d27b22816379eb04
+4930c56449c419e37978806f3b42e97e1279e9e897a3a709dacdd5d87d0d774b
+e14672a4ef12ba4b49915c76d4d574d44f3e94a20d911db728d973356e91604b
+45e6eb25bc0dd3d285311bd2b01a22559f1e14387c996d531898d5fafceba4ba
+0c6b87aaf5f8e82afc71c04d634c782ae7288f397e50ca0cace18eaa1c17cea0
+cf388e326c7ea4ba925331fefbda0606aec1defbee547666002d478a69538c99
+28106f509a53bdc2167903c9e1d54c41b3696e25106a6062c10d945602e341c9
+b50f1df6630a886c8c6f27383f29817e1b2aa326cc85e43afb136fa58ec24dd7
+03c43b4694b403a1471053cff14fbf9b34567b4eaa0b96adea3a5d428bdf8b9f
+c0159e4acb57deac8d87597b4ee450e93f682bca0712f2136f8216945ea482ec
+8ca99a112e447aab44850a576b69a254a9cfaf86d7cd1c130dc6c1181515b79a
+d703f0903faea4c8e76375951b03b924c1079df5ac55802b53cb34113ffdc513
+8201c1fb93767ede80e76cfbea2f07ac0f95f250697ff0267c206bb7048a862b
+7715741877e4c27b7a88bae1414be335d97ddc235deb4b95a5c51089f50996d6
+a812c4e9cefbcbdfe18388e75df88d04df66eb53cf3e7eac5fea15ff041ac3c8
+834356278f31517ef1d30d53e0dbadab04bd70f2ac190c35e7273d45a0259363
+40dc1e0b0017a7ef36c4c8669f29f21591e43541f149c4a3b1412a5057c5348d
+33b8aee4f452db49b54d4faac7ec9c4eff03bc3c783d901a896fb7533b51097b
+81783a016b20dacb75f52bf78dde107172803db8d3a24300aaff61863ac87c27
+f62e44bafd260a687a8cb47daddc2bdb6570669fd3b63b9609c57caad4fc29b2
+01082b752a679423dde1a8c86836c44dba5d1e3ab96676707df07bb47f730363
+cf78ceff36ff2ca976c969cb74abb3f72f693396518e9f1ec0fab6b53fa334ec
+741b043045e31ace728698c097d458dc733fa74a63d61f53fd4f45b9ac1880d0
+ade4bad2e774af7ee6549e2824168f0aadebcc069efea4a90d4f2d327a242042
+9557284971dc1f1ecef59b92550929dcc42a3168d7f5df5d18e53837ca6bd01e
+003a02c3700842fac98946f6d516ef503be66d4897cf2f138286ee3aa79f771b
+4af8541855c21d583f747c294bf1d1016d3e4313b5df8fd2c3172820bdf19792
+428072fae237ea94b4e28daf257d41cbcbbf9c56dc2d964ad26aab3df4f912db
+71546ec1fb9dbaef67a4a8771f859e0e78a9f6972724df27f79ded61b92ff3df
+4de0f497c50f569891279b968d0567d17c99cacf6c07da376b986299423adc90
+84d4f8e93dead2970857b2c562244f898a59995b5ba464087cb3e2318eff4975
+5079a9ec897cc4dd5bed02e820619bc1edcb56385e0fde4c2ba27fb51a3e80be
+9a2116bf27a5a42e2a70487177d1ab878bfd89347513c54eb92abbc87d50ce70
+3e2d237187cc8abeda00f686bca95cfb404053b6261d4795e558f3886c523282
+06adbea3cb36212dd839d26755ac16632a723754890ac8d9a6599d6f2663f2c9
+b920d92da7c773aecbe0cc7b950e619ba5eb1abc667cd354d417995d6eee1b0e
+e43f5bc9061b897e0779b023eaf9aed24ea5cec2082050c92e0f30e23ba713f6
+2fe14d9957dace391f05e8370b9551423dd76bb50dc218d7fddbbf235753e1fd
+f35e73d4b81e2f8bce061451a2c53e95015a7be71df27847b628bf8d3b682b4f
+0ea381f5635fdd2b8e49e534a9c4d50e2e3991a7d5d418c9922774246f8e9e62
+9096c4c4bbbdb0b7fa78877883fe2f47f4570b4188d540163bfbd5d10975a80e
+c4a21c1ecd43e211ecfbcdc391869a27b06a47bc1540a708a4e58c2fce67bdde
+94fbf0310a60fc2baab83cd16b1c0c3b05a6fe84367b3e46f93a409633ce202b
+fef120b0c791a48d376e3707689a186c73135f034ec6bc28870ff0f9ca5c2dab
+2412d8292fdca20113abfff496ca34eb7829f43d6e1b51eb79ec9d331d6aad28
+2046c1eac346953e6db62225770dda2d02150601fc66f53f94e9d58b4d3499ff
+22494b4ce45de3739f3ed286e421febeaf9e125168babd9b705b51ea733e31f1
+ce295f4d144d86a26bc8aabcd042a4953ac9d61b90e2d4feabab18648ba16ea3
+ae99f53b971f48ce1985c0a02703f1c609b7cc77d551fca1a6dbae9ad7d2dc7b
+a61a5e6f8b729bd7623d9c73d33fe9ae249a3b776634e10dd49222cbc4705af6
+4e262a75bf5e60b249acf6d40358f5ead7fc3096342ea25de703909be907e364
+0a1315a1589df358b7eadf394d4b355571809b1e860d96f4d1386b9d8276b85d
+6ff051e2883e2fb05c809d189e66bce5465fbe830282a23f1bdf0fe3bbf77780
+8b2c93fe7b69d6156f6ec93cae83bbe9c54638bba8d6df08da9bafcb3d080d3c
+775c0347a472a23da6a22ca855ace9385893fa7d851cace8768fda1b2716e6cb
+fe875530ed94c49e8c67922ee003911c08f2165d97d8a18f62d040e64b35b544
+d740bd40d8715005dd5469d106a4bd8040fa53cef750287560433965c6656508
+9cdc19875262c227b8ca384cd97047d279224e13bc8cb280f18e163d1aceaadf
+d4986a6db5a6bdad04d188a927b6b7a8c21bd67bdd4210eca16dfdef107c8228
+0d722d8a3bb5102c0dceba5e75a5d8834e8f253bc5b9938d5099873e1848b0d2
+76040a020d677b2b5b4f7e029df54650b0d1408bb81824f003232c5e8fa4cc52
+8da2f15b4cfa2fd8770e0ef75a7a82d92fe20af3d5132d00c136a8740d88b7bd
+5004d69fc459d373672b2b72f3ca1fbe58a3c2dbe5708b57bb3c2489f28a9e3a
+81d61c0a661b8b039b1b7677438906399a6b8fabedf9b25dc024a038574d7719
+cc4686a43725c624d07a0760aac63f477123f7f4af8caf41874046fb963eaf4d
+eb027b0651dfe68f224a5631eec6342258f71bae4053cb491c6c37bd9c4c0915
+2a643ca05150b84e2342205415be8556ce7b2f22ebceea680873da60f1a1efe6
+97e3e9e43c1756a243d3a9757f1219ae739579d393d46d747bf36bbea11dc549
+df94f5b12386c770d6927ca29b80eb728d68b7c20438ba3932429e5e7cb190e0
+3213cfe74fe392ba93dedd3b1779fc44c85d9ad678d92bc25827807b607aacc2
+414b2d0d59147ab8e0be0d32f76b95b27fddb2ff275c870a5c441612faa47330
+5ff3ea6caa6cfd284c2349ac9e5d185beb1eedbbe07b8c2a3ad4766142c02434
+042568172b0d3781bfe703b149eaa84fd09e8d488b99aa38eb577f7b21c740e4
+6088dbc4e17d9333a3e387531f97b4ca1ea179952eb33878bcaa465333e1de7c
+83616e413ee6e4c5bcc1fde1987a0cc78d279468b9b1511081f6e3bc802e8d6d
+78b19dc59990579eddcfaad310e0f360632da25ed5f6f16d560ae7d0175e7b2b
+01c3dbd8c98f55f494bfc52b49f3227ce12dc4f458de34b2ad36b3b00168ac80
+85e35caa9ffb7ff60098e5027e32be43790a0dbdeeb8223b692aedb3d6c6130b
+b6089c764fa7897b9a36207c9fbf50ac0c481a37096a57da8beadd05bd16deeb
+d93fd56fe54a6097c61c1d647f2f52553609c3f936831f4f981862526139cb29
+48a02146d1815d3d078bc3dbd7c32f2167a70edc28b30025ef761cd43bcecf7a
+a8ea6619cecdeb83147467c3970c772db2989301cc8337350be1f55c8566d14d
+8d963234133bc7ad37397be014950d325d2b67d70232a50fc6a4a064dab0a970
+e7d4e1949f7a0e50ea970934a621330fd477107843bcc7713dd08075127c19d1
+5497f2a13f917c3b7a8917a5e7be2009dba70ea513b405b8926631562a0a8ffb
+4bee56250fd7c147f5b515a8f1032564b6baf2569e913e7660098d72eaf586a0
+078af231d4595d67cc0c73faaf4463fe6ca8f94a8aa8f9132106da58d45bbfb8
+3e574f76201cd5ef05d47daa02001a89452b5a8bcef2cdb714b0bf5c36c76557
+7e2eb7aa71d085de5a4f5fefd4b22e25854c21a8e9e8460a2686da7f4dd2926d
+3533f3fd3ef0d7cf276fabccf0c1dd01eaf32d788aa6e01d810713bf2d236a83
+4c67f4c69fa9281451c98307d7e69afafe851f8502421c4276061bd6c3790029
+5f72f1ac5d78564cc221302cd97889400b3238c65a2cb820bf23006208c65d23
+9b428ba1a5f9696665bfdf9057d645615993d7f572c6646d710482b9ed62f0cd
+4dd6515b31ddc3a154beac3d89c81c8628daf456ac62abbe9de7a5b6062cf650
+4060c23ae920cc4967798b6eefbc752e3baf2492d8327d295f710814eb11b167
+629c496c555117f76309ea311c03ed7fc7f248f7d1492587d0cd5c5ae5e037a9
+57e68d48f4b8128c31dd014ad5230ea69ce33ba6c0c4e6aa1e1a7b3eb2245445
+979a59d7796a059f54c3a1332e06906cd565d21c0acef799d9d8aadfa6f5aa2a
+309b296c1bd84c1fe08bae310f5f22e60c9ec7096490bbee14ca80cafee768f5
+7ca0d7a51894b2d52ffe875b3762d20f0a1500adda6a401dfc1adace27677890
+ec2794c07529ee08ff575ebcce496c16e6b36f9a9beb924f95c4566ac092be59
+1694ca2a76062112ac62b1360cccda1c5020fcb4f400de7e87b26391d4177341
+c91c4633b878ff121715bdf6eaef905841cb096f66547d03aea8d8c66dc7b603
+9b354bf14f3bdb74184393f00dd5840e23b08653d045f18155098fcaefaaf8b6
+2c548f4a756994f8427598ac8f68d7304ee37107738c74c0b1c911396c7de26a
+3f33dc50809c971c394d2846208999bdb9bbe252ce67cd7df343a4f0e108623a
+837a18e3f19a9689b823ede76f782b46d3635d60e4fb66d7a672b973d3d14262
+dc6e05092fc79544b2d82b03a089e8314c4ad8a64498f1bc699f58983c1c5087
+98aa563860b833bcc7c490b0eb852d2ec7ee9d9a02c91ae1e94a4f5a94688492
+05bc1df3e49c6368463303c81ebd9ec0742a8374ddbc39da1ba8e51d1e074e05
+1b3311fab5daab09de62bc9580ba9dd337f8c72fe557ae91caf5bb3c1932b544
+04eb6a55d79892f70331e72acb7093b38e7327c336fb6b4a6e6cfaf17ea978f0
+04b58b0c8a04b0c8806741abd1684835fd5a1a7c0345d5a2387f22ddd0b93269
+8559f7a4b1170e98ca957fc9c903cffd18fe9b92b4b32c508e53efb68883e042
+7f8b646afc1d1c597942e79111d571be9cec5221383f5f6b03193e0c8eeedd50
+261b9f57e594b795c3c7f493296e9a8ba1f4506f868b8502649d7784503fd253
+c5e99f3041f2b4cd9db715728f1e692ae507e234e0e8a3a8f6c732413b90176d
+48e910abc918ccfd30a33c872f9efe34a8944f692f5f770c027da9bad3f65866
+2abeeb55a57dc13c71bfb71cef2886453b43d9961172723556639642ae281980
+50fb7ee850651738a68be6420eefae78d1a20817ed96dff413656bb4a968a70c
+b9fdd1e9bdb2c0e1dd5d32ea1b50cc487de1f55407eadbf2b3fb9c03b64c15c9
+5aeb67cbb5ecba0889ea4708141e82d2ace7a841896c77ab00dfdbccfafa1f64
+61abe7803872d15961c1c31a6b1ad50f120d555471579bf02399f815988958ff
+97d0132e6429769ecfe39b270a07ffd1469246229cbc1450251e71b1beb00886
+35399527233576ead74ef01d7bd7bad02d951e93b2e71dd1a82c523abba753e0
+95af0c249ad1f5d59f5c95da00d4eafeede3a093e4cb6e108469c6cc80fdb8b4
+5116c29b0a9a02a48f6f77115649b583ba4474900f69e9472ff178e41a33842e
+78b476a924c14814191a61620e398ee3b48ebaff1b3635edb48ad16d497b3362
+ca9fe38284e1261344b34dfa8d46e43324b3d3b21f31146ef95c2c7c08029e20
+776474fa622d5f145ee66aef74e4d42b40e981fd5ca36c6697c2655b2183c655
+9901a8f632beaee841515da5956eb82b341d4683c37050ce998df19480662d7e
+2d09b430b0d79441295b2d599bcb59a3ccbbf3333745e509903400c74b30dfc7
+7da9616b21b268b07f65a4a32d878feca9852a78d43e341e893238a2b2c0cf1e
+3dfd75592bb9f15c4e8653dd6e8aee0b772961a9e54696a266506ae7146947f0
+ea378bd1ed22b391795bbe5737994bf9756ca3019f42a032435f9429eedf2d41
+5ff27dfa5aac62a418667843ad3995684018c1b1de4ea68848e3762818e5b80e
+0d5bbd9dd866753537ed7ba8ec145d49df30fc8d293d69e3083209754a4b425b
+99e39da47bf27152049231f60dc3a849ad6a21b86771e945877cb2ecf7451261
+b5943f02252618cc734cd0ddcdb312ab729f35826c89fa9420fb1ad70647a74b
+70474ddd8ae200fca9317e1c50f9cb68de8379399bfa7424e66311c9d7574746
+e19d44413a0c6149f28a340062946288f9f78b1c2b6f5233885e6a19cacb78f2
+31534202b24aa659211adc6420b95b17b20226e74e9ebe01948582b8ea2d3ab1
+78d5b4ed4c778041fc17d11157d5262c90571ff706099c5b06130412490833b9
+c5267a7163ff7e1d45c89050d6497d097826f5b26be150ddf1ee486d232e687e
+453116f0b6b7c93064207213540cbbd88d549b74dbea59176bb6d7f7712dd598
+5256a9273aa32d1701a6b2faa0da6d21356be95968dfc03221b0ac65b2b22287
+90c3cb64ab2a65909824dbee9b5399b0c651bf5efd603621b94feae2fde41963
+c266e40755ca4266496ece2fe93c4397d907cf8d413a230701e7045cb9da1d17
+f1a00e00ea64b45dbca755a454a9fb232a8b4b80d6032650231b9fde78ad9664
+71232d9076520306940852e529fc46d83fee4d6fea15056cf42fafba1e8482db
+6870f41187c0906484e8996738b52a946aa34dae017d19faa08c86845a85a543
+20dad4f4002f6625b84a1b4b80467d102c678823b676df24c9ca802d568ccccd
+615858fe6517ae56d631eba96da6b71e0e0e7b71ad9a9483c61574e23c47bfc9
+a0394b4bd85fe41c3a8bb60ee8c6f788017ea90ede0a70cf82f14056b24a6c3f
+58849d613f08ec3ee21d990dc4fe661dcf4b3afc8ed34ec2ddd279cfc2cee1b7
+f1929ecb72d10752ea012342923450110ec1868292babcf755d0b393768de27f
+702fb68daf1154e8855a89bc32bc378580ad405a05cd12b35826a9d88a3cfb65
+987f364b89c30340a2b1e25ca29cec4988afd940c1321d12cbd50eb411a29a65
+657b2e7fe642bd8c90cce146b9d216b7f87aa135ffb9ccf0fc8da1146bb6f03c
+1cea3f7d532fb572bae2db81b7f15c14de973f6bacaadde5cd0333e71a9a539a
+8356038f876ece85f7e6b2d057ee8d48626483ba054f8a662991cd52fc317738
+2d029b04dc44da269de8be9b025f7929a34ccb6efeef271232894ba8c837698a
+fe6cefeb4c1ca0180215c2adb029986953af902ba21c7531850ca7ef798ce22b
+3496988dd07347e36f26cd55db9caaa73c5ac260256432c9e0f6e3bdcebd81a6
+59e822ba4188ef395f10c448c4c09270daa19a856c34f5cf0f91a74ea962b4a9
+5459408633966d6730b85c54e2de6ae317409287414079072d9422ff38db6c93
+d37994e7ef1a0e65c88019e729528b51ad23bffad9ec9954de8678f03a307c43
+0c498a7c16b3b348947e25a896bdab2168bf3119b296a327c2378904045e322b
+9cb321d23d74f470fafc828404612288252f1701df6c743de571e2e876cc2672
+23f2aff00d26f1155d6d41a3b08322cf8eab002fe47f5265bfb67a405d4ed77e
+89cbb4a61b54e796b8f3cdf388d5418b96615c1312ab7d4e7deb1aba8984e3a6
+76dccad1bb953569931f8c26f7af460c5689412648cdb7803f1c93dd452a8ec1
+6c8e3e6190d558919b855b7b5db34bc42b992ea155615cab202009a96fe13363
+4f649e77e4268cf651a74b8c8c645aae6dff800ad17b194c0b883867151c6230
+dddfaf596c2639a46e6746fc3ccd1593d432063ba27bf2476bb3ee610fd7d92c
+c09b70c657a8255cbe2d8ac281892280d5f19fc27b5f0e912eed9c38007e748b
+079ce05131c3956dd078c0846963268fb4dfb5eda5ee8894a89920088f721d1e
+b725870aa43e92c565d10d5d796cc04a114b5c32e90c9c7f5a9b5d03d894f96b
+9ef0b123b9f90d1692be30515757e54c2f9e073c16c04587363c1e9bd4f834df
+4134a10fffc0b5960a83c4f75979a14bbbbd52d4d4b5eb75901fd20a36613826
+0d39c035f3144381fb6d4eaa99953736614d1a46fc05f800ee66ca63b1c6ef10
+673e4faec2288afbf5cae62b7f6ab8c80e96c8cbc7b666ebe78e68d184d85c4c
+d8b92adb253aef17ada1dcb3eb1895047b93163050a6f3c1a69a7935a12244ba
+cc164a780f44dba4c432f913f5b649529721b123f4d6816ac8f1029bba12f442
+935e1b03a434297d33e03357e0a4a56a10f0fc2c16dcd060cf858b58f3cdb5b3
+4f571ca4ef8a7a22bf7b54c35b38aeaa6548be8a70027994834ba2d55b4893a3
+c70d9d44cdf6cb9d1d6b92af027a0fa802f207d34c10f8c4aa3a2cb64f8817b5
+78408cd8f98466905ec58a8df6cf060d6ba17235dc6591e2ebf061f5a7f1bf9a
+7690df303dffc583b98e8765977c887e776814899b45feb0f0cac642912d4b81
+e51c5ae48c0cdf9024cb96f421d1b7ae45b5ce9d41d9d35c2b00416c8b9c032f
+b27680c1f8c1e55d9973a698180a786b92fe97c1b0a9017e37b51cab3f388b2f
+42329ec38e452fceaef2152d36471573caa830a5c80eb0ea73c1c7161f7b21b4
+cb7d3e55f29e36dfaa09c8b3592fd8afbb64adde44d8665143ebf1f3afa1a5eb
+a16860b8e8f52eda1a6793be62ee8a682a1dd30b6482a3188b5e6b8dcad4c688
+98e39892691434d85c42922a035c752827474e01cfa507d0c434f746993ed746
+3d32f530ea324bbe0ff4a38581296788fa2dde0db5dec9666d8af2deb947f200
+b617a83e84114e684442fe3360e0a66880f3a87f1030dead88eba805cdceb8fa
+4b110dcd71f720d93de7c0e8122a09081b9cd1152dbba5f0411728ed87028a21
+9cc36c72bccd73e3cd517b9a7210cf1f3c719e7e23511b0f0c272de69951e0cc
+c264387948b95b3fd7d7d849947117e1f14bec10d2d718374eb09734709ca60e
+c7037c7c3c5d1b9797b9e4c539733fb69fe61cc569f76b269780fe9abd497f54
+4d728bea738a4ec3e17aae0ffc36c2bce74cd24bb8a2758b9c8554d92a64413b
+ad5f35aafdb387b28f1ca329a32d8d99db75121207109e6fba304fc86e027bab
+b631f93526be28022f06af5126cf0a69c7de8aec8043aff5a47f6397097b8dd8
+8538bb33987fbdd064558e7634612b8b2c22390f35b8096fb0674fd62084d7fd
+15d98b22fab5dd19f9d222a2c4d2573badb76db8383c74b4996cb068ad4856b3
+a01a8f42bda0501aa7c317c275e050a133df090b6274f9beb1af87f8b48f7b03
+64bb4e937369972839448dc459dbb463e8df91e0fc7fb909f5a423ea9aeb2f23
+0d24b27dbe955c4b6c3a9d22603b42c6c5ebcfd67c74f915a47ed1b762e10738
+b7f181feb8e6a3b2b764336966c6acff5a2e2fe237a4dcd64b7dc7b7dd33669d
+8dea06bc3c1ad351e2305335036b8f109fece9e30cde9ae2f472907780dc1667
+e34cc399da1ae24fb728529cf610c6592df901ec9a7a1d0c52ef3840a99861da
+b28412b3751b07206391aa61c33ca87d4164876b92bd6d27de24d6abe4af0119
+aed63c73e43a696bebc21252a6c39d5ecdf45042d230a583e177b1e41167ff25
+1e633941dd1808f8a4436e663197fd2442e085c596d3fc12b654f94f05453599
+e7957e70a965c87132bdebab6eb857a450c23d36538da886be9785a15ed6a2d1
+64dcce81504e271f58101d36257f8cb1d146c5dca3d00963d245634ab074b95d
+d490b8a999b83ad4e4fba3573858fda34ef4735ece350868c4ab772298e22b6e
+d78e9faa6af70f400df8c6777d27a205fc1b40b26e9ebd72e37a18948d447dd6
+021aefcbc2ab7939928daa51eb03d66172d0d4df7e01b032de40c3ff156e46be
+28fad368241ae811d6914a3cc4ecb640531adc026496999ffa429093a40615db
+81b63de704aeb0ae6363559a0b70d55940bfa22415edb9239b27bc5ef0e4fdf6
+3a91764ca5dbc491df84e07875da4814bcae9b715d3e4dccc55cd83fcfd63696
+c30648600c986bdf4b1f00c1334b1403670a56d0cc8bae15187e1f4dc8082eb6
+7d85d9413fa5bd3215c6af814cdb8c1b7dc579c6f8f2bb11836a5760021c0fe5
+e715080339b24aa017ce4aae8bafa417f889c5d27a285004429383bf4cb8f65a
+6bb72bcb975d903020bc27752c7e40a2d00ea67609a04b125da0ae443cba553e
+d3d7bed8408f3cf9d22176f96382fe832a85fcdc995663c777ba80b9750000a7
+921d3fb174ffc454547a90a74289c2a5b49029b3e7798f5587e9c646ac23f2b9
+254b151cfbab3472eff67ded7ce08db90c0a9a858b3d58e7b185212a0c45c1d2
+ba5d5527e23860ff4a9c985f74efa6c70a749206addbd93278965d3b6ade68ca
+8e2e9ad4974a836fb78836fb95e4fb7dd7d6c2b5423334aed2644b09259367ab
+8a06c57a99803b547e3becb7ccdbba446a7061d624a8ed0ab1f86740351ffa61
+2f55ad009fe1486418119f3b9932014cc92ed1f13ec433c62c3a004f9fa207a8
+da0dc1d5adc59d2268866b143868feabd83a7a27aa68c1d2d31b5a3c5b2dc1b5
+6f65f46b5a3e33c9d2202d901c9eb2b100667903493999f0a23bff58c6dafedb
+7f2914f65e1654579b4a7df69ee950a223b679161f4455cde3d275edb6e8977b
+5067a80d040e349655f4a3a60adffa2a9e56a4c0a3b1d4475c29dbcb177b18bc
+2c755214570988ebdcce1d8f54c59fca0b0e19116e6a2bf951ee7d00b3feca8c
+bbfe7c9914ff0eca51ac6273ce6e8f5915eae72bb2e3b8c67635da114e783e91
+57f1ea86d828fc1aa506e0fd7843bb036aea75714c5638793076f0b87de3f6d8
+8527a8b3adfb5171c076ad8fc47b68f186af94a0de3b7badfa167cf09c7909dc
+d9649484238f6be5c2cea6b58f37e55e6aa1e331d2bfd64e5103841ebd324f3a
+f9b9d7ff7dd9934a1eca605ba4a38c926c654d0c9ed2ef59506fd93f25ee09f9
+ef6383481ff4f29d33098d0b1c3675a256d924c8a344590fde46880cf9c12128
+dbe09c32735b56de14dbab2957fae3ba143a07de4b48c25b399b0e77d537da00
+f0fe4d154f1e9a09b9c4bb74f62ae698fbfe43bd06f038089e8b1876eb07e249
+533f9aca9fe1eba0d89e0bc4a0f98488462a11f95f6d77ee6ff20508293d8e71
+cc81d9bc80450c2c6e6a0fd46b8a24aa263acd703217ff2e19e7c61d7d1200aa
+b8c6234e397ae8f474f7afd9dabb65cb6de25212987a8530dd5ee78aeea4226b
+56fbc58fc736c7fe3eaa7a842907cb4f4589814479ad2a09bd1e3bb0bf575d15
+f0aa886a56e30b5991f7064d4b298c4eeb2c6a33a7307014315e3dae2ff72581
+283e0a607a7d8c62766a23f42c8722590a784b34f5e97b9707bd45fbe3b08630
+1df111d0aba815c03369f7caf6aa950498bcd6bd02736bf70970525b70c023eb
+11ef2901a01abc672153ddeb66264d59a7eca681c602e1b4a362bd4fe09ec788
+2e739d4c3aa9ac9446b2cb7da376c4617cdf9fe436aab026d2e23c755be7ba9c
+fb61013c90dd986c2f90ab7444661c16ac4db3a8402b03450173aba644a61e98
+99d1cafa06a27e428ecd6307ae89b1747de0609ba95f4d882986a904f1c1f3bb
+da550327d637a29fb1b4b23f5e08772889d27bf3cb461144d0f01db6699b2cc4
+8c2a7cb91090432d70fa673b7ade79f862e9b79a94f06405c44c2b0de8f233b4
+8080e440921cfabc9133916a0419613528f04b00275b140d691f4057c5420d49
+a1e53fde0e926a60613d1168d886ba0e2d5de629e212cb9b8b4e2253a7c7b531
+8287a0fa5438528bd414e679d20b92d0ae2ed58d2c6874354ece38b933a1da35
+508e5c4e63e62437a77a443a2a9ad7044d63f36c23b5f3a2ab443c717219e02f
+dcb6a4286f765477eaae1a18bb63c3375fcd109a953a644e168f2ac0e1d85f91
+3d62b34dc51e3a134aa30bfccfd6178cbe7dfde088a435ecd7b310f5f10a5b8d
+9387ad196d7e1b37047670c8ef0397cd1f380415de77419a20ea5349ce908027
+a8f8d1498f5bffa712690e4ccca077a452c902fc4a9b7c7882ad7b9db0d626c6
+7def29713c25d7ca466958350d56aeb8dcd0987ac05fce2205081d301cd3e601
+f386f99eef9857fabc6bf49a32ec3d4a921ed57063e5366cfe51adbcc48b3fc1
+ea1e40205c7fccec4fbe6dc0bcc7af55984f6e49a8a466e144b7e1bbf63506c5
+f60caa2320f9ec1732e9767ff4c9effc5b2f9ba6a9dad549dd30771c53f9a4dd
+24adfd39cc5be74851d51b4fb038b22fb65eca6cecf6c7e902e9391c89b1587c
+509bf28a987cb1c8ad74ebe7557792a9ddad7dbdb1a367c633a8eacb91e3131f
+02000b5d06213700a011011cc1cfc3abe1bac54878cf32145d9b18eea7b59b9e
+19f551170b92472f8922c0838f5d7ab5421d59a325c05372fe2c4c37184361a3
+71572d378c3d0de308e2c1cf3dd7f2c3dfe703de9df84e569dbe2e8cf00c54f5
+15db602bb2d44d1f99cf2aaaf78953b4e838fc1532e2ef4de4f0268b2e6e2c2b
+cdd46138aac78f9f34d6911ae387008d594291c7a57d693386170e7e6d377cf8
+11966b59bfa3cf47fae49324348a91f80399e29991d659b32e0fb0af79763ad0
+93630b21d061cd935351fad564039a2fac55b025a04e9668771118e86f9216bc
+55c4cc8ac588333bbc1b8fbf6b76a08bc14e1f83a683aefed0e20c7bacd2ee51
+96d723daf15279d8e2ebb993847b43d63e4960ad2f4150b07fd081517de48a9f
+6f4ab09802181a2e4e44cb0204200fc112fd99ee2e09cbaa3c6d3cbf97f95bdc
+a4f6d06ad415ebe00d6a78c3ac0ce6cd4dbc06d2829749b7e410e6fbd6f8390a
+b958ce73414a8766576c3b642916778c22cfcaa498afd93aaa19cd506e231a81
+f9994006f5e948a2b20912d70132e6a0382666f35fcf21b6edd947d1e497f66e
+da9cb07d716cabfdb6535e5ebffff6196b495327fa4e5c60778f76b40d46d4f2
+769ea09961f24b8110be2c59f426f16928866233066135ff9e1f15379ed5b11e
+de442dae3895e438ac05889c683687242a4758f1a234ef144510bbcc89e20954
+93272fa2dd7373c6b7cf9170287f4dbe3022b20bfc08e27c3232514c813e93b1
+cb37193b2eb17a832c1833ece9666aa24c12424d6c157d4f279e1950d45b4940
+2c54e2bee3f3eb017af95881e32f009764f5e67b93eeb2172023b510a874a159
+4975ed54f35268d3df9c2cfb65c6316b8178d1945abae3c5566b9da3c8681b4d
+acee486d5084d66e88620af120a44b5e35ab55591ea5283b4a69a31a9f5b212f
+96456ccee559dd42c58b6da34dc181a0ec0a5be41a9b3e0958f399d083fba072
+686a38da9b05355fd47c3f59708c7b7d4c31de833a87a4d8da6abb4b9d2ef519
+7575de9dc5b012e2e3aa51c407afe407001a0714d7ef1184b33afe085754edeb
+3ae4472f53011049af4ce4e0ef299720cf8c10911dc0bcb0f9f81a8caec00855
+91b750dcae4e3b0e0f507c25c1ca59cc38e366f27ea5f13427f2bf6809fcff29
+5d513e03c98272a0a822592dc9682a49d9324dc1c07f5b8b933ea9232ada0108
+27eba8837797d8782f46dadf53cb7404cb6612e71820478bf37f88615ada4989
+00f10078a2385cb83c8e41aa5464148bd90f7f2a18fd0b3f41a78b10471bc33e
+0b2815ed757515a331a6dbf861844ffcbf6b37ef7f348d7d918fefa2248d82d5
+56580cbbc3ff9c079d04b9ab4fccf1682c0b66358cff6840a9808d91374319b3
+09fb2f795569eab9c00e850a50afeb42248a251473156bf4d220f220e8acd814
+2554128f83693005694e19707d0630ddf56bb55f47f21f87c37286015677685d
+d6763e4a3e28de9d9f7292f659ac307ef3e83d84529389f7eb0639c02239861b
+86df711f11038176d9a70b04aad94f14285bd7ba4b264696c383bdb6cde76ea2
+c3870ada2dbd7fa7c70e3fbf92ee9baebf07e97da553aec0c90ea3feb38ff428
+7625706fd41f54aa6d85124920c33c48228cdfb0e426eefbc3732e12813bd368
+01698802e58db4eed2aa7fda4c1e439ce2012457c55115d69eedb9f587e46b5d
+35abf7e7372203b01ffecff24226de4e428217c5313a2e8bb58efd0f465cc612
+c143ccae5d8b6dfe04583b1168259f15c363bd4a7db272642b0816c7e0e5ba0c
+b2bb4690aed8e0455b878e139c15ffa403828e30c079b3c3a26278050ba66ba6
+fce91bad29dc2d980c06678131d961e47363f11856b72f5ca039fc991ba248fc
+f838d935789da19681db4d9119cf55bd29847d2536ef4f69327163c3e262cf63
+695d42a83ab0553b4c72c5eebb9e71c358c5fdbc209ada006f885f262e3d077f
+b9966e8f53ce61e144d5a2d096a2ba15af75790609218bcbf7097a0342e1dacd
+4eecf33fe3c4c7dff8b4c1f501007957f8aaba2e7176a707064c266cb723ba18
+6139e0bd697b37528fbd3a4bc8be5f7c6a50aa6fc65a0a52fd310140e750176c
+698a919512867a1ae998b32043c0e11106f81154aad48fa3f6acc98bb2b38493
+863b2ab05aa77dc16e5b01b328d10dd7af79ec3a5fd38eb7712e9624cfa2a620
+74988bbd4014a7816a4826319490594c0abba1643e85e961870e7125de720ed9
+db31febdcb739b3f8f0a5a2803819f9f7f3fb3ac983e3b6743c7cb3feeebe578
+0b0456f15047a76ade4d764e56093b19ae91d52cd93ca61bab22095711e2dd56
+cdb2db4cb0a1120dba8939720c84f2faadbda3b0cab8ffd83bd77580e0dd59da
+67cebf38ac126e54f6b70b55e7940c6b44d459757958c13f1c6f248eefb5af86
+48d752e91788945fda3da668a887f657de389b123260e3ef4f1b93393c1f484f
+7268e84e78585c0a3695ce84bf217637f8326c98407c1060a00ef8950e498d0b
+79ce31264ab755493d4ec00babe341e431d34c8b7228d0c5297bb776992586c5
+ae9ceaed8020f3ed8c0ff4bb963b9c488f8ca6c667c851861e8410973373fbf7
+56b0355b1a0dea5adf037ccf59bd1d4957c116187c99c558d5827d609dd27cd3
+b2fcede2d281ad3c33c5f9d55cce22bb1b8e9b14d9b6dc140620733a10389574
+eed9fa2caffd5591b8091c8533cc29d600b4a75b738f8dd2951983f06da5cb54
+26c1b1039bdb3550ec6cba9be971034a125f58e74152bf38e59b6365e7c6be2a
+8d5decd0397e0edeb638238e8320f0425c5ba6f18ed9bb32693dfe1b079ffaa0
+3bef302dad880b1b4625865d2e8794793e1e63fdd5b0acc91d1f83d12238427b
+47dc31e3b218ddef4813ba2e15d872b0e6b640905e261405d148de380b062b9b
+a73f115b4f5ef94d4023bbad60fe61939b5c6cfcfacbd85110cc027a194d92b6
+f64b2b2bd0c5648e744da3048486e5d9b77b45f0360aed982351fadce42a42fe
+f2151ec97e2e546940a7d9154cef3b2b8dc94aa1076369d9b1fc54e22d65b6c3
+bdbbc16f315b52e31933acb14929e23f0a1806d7bef12578a6
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+cleartomark
diff --git a/e2e-tests/cypress/fonts/Type1/UTB_____.afm b/e2e-tests/cypress/fonts/Type1/UTB_____.afm
new file mode 100644
index 00000000..c41755d1
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTB_____.afm
@@ -0,0 +1,1005 @@
+StartFontMetrics 2.0
+Comment Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.
+Comment Creation Date: Wed Oct 2 18:25:02 1991
+Comment UniqueID 36543
+Comment VMusage 33079 39971
+FontName Utopia-Bold
+FullName Utopia Bold
+FamilyName Utopia
+Weight Bold
+ItalicAngle 0
+IsFixedPitch false
+FontBBox -155 -250 1249 916
+UnderlinePosition -100
+UnderlineThickness 50
+Version 001.001
+Notice Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.
+EncodingScheme AdobeStandardEncoding
+CapHeight 692
+XHeight 490
+Ascender 742
+Descender -230
+StartCharMetrics 228
+C 32 ; WX 210 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 278 ; N exclam ; B 47 -12 231 707 ;
+C 34 ; WX 473 ; N quotedbl ; B 71 407 402 707 ;
+C 35 ; WX 560 ; N numbersign ; B 14 0 547 668 ;
+C 36 ; WX 560 ; N dollar ; B 38 -104 524 748 ;
+C 37 ; WX 887 ; N percent ; B 40 -31 847 701 ;
+C 38 ; WX 748 ; N ampersand ; B 45 -12 734 680 ;
+C 39 ; WX 252 ; N quoteright ; B 40 387 212 707 ;
+C 40 ; WX 365 ; N parenleft ; B 99 -135 344 699 ;
+C 41 ; WX 365 ; N parenright ; B 21 -135 266 699 ;
+C 42 ; WX 442 ; N asterisk ; B 40 315 402 707 ;
+C 43 ; WX 600 ; N plus ; B 58 0 542 490 ;
+C 44 ; WX 280 ; N comma ; B 40 -167 226 180 ;
+C 45 ; WX 392 ; N hyphen ; B 65 203 328 298 ;
+C 46 ; WX 280 ; N period ; B 48 -12 232 174 ;
+C 47 ; WX 378 ; N slash ; B 34 -15 344 707 ;
+C 48 ; WX 560 ; N zero ; B 31 -12 530 680 ;
+C 49 ; WX 560 ; N one ; B 102 0 459 680 ;
+C 50 ; WX 560 ; N two ; B 30 0 539 680 ;
+C 51 ; WX 560 ; N three ; B 27 -12 519 680 ;
+C 52 ; WX 560 ; N four ; B 19 0 533 668 ;
+C 53 ; WX 560 ; N five ; B 43 -12 519 668 ;
+C 54 ; WX 560 ; N six ; B 30 -12 537 680 ;
+C 55 ; WX 560 ; N seven ; B 34 -12 530 668 ;
+C 56 ; WX 560 ; N eight ; B 27 -12 533 680 ;
+C 57 ; WX 560 ; N nine ; B 34 -12 523 680 ;
+C 58 ; WX 280 ; N colon ; B 48 -12 232 490 ;
+C 59 ; WX 280 ; N semicolon ; B 40 -167 232 490 ;
+C 60 ; WX 600 ; N less ; B 61 5 539 493 ;
+C 61 ; WX 600 ; N equal ; B 58 103 542 397 ;
+C 62 ; WX 600 ; N greater ; B 61 5 539 493 ;
+C 63 ; WX 456 ; N question ; B 20 -12 433 707 ;
+C 64 ; WX 833 ; N at ; B 45 -15 797 707 ;
+C 65 ; WX 644 ; N A ; B -28 0 663 692 ;
+C 66 ; WX 683 ; N B ; B 33 0 645 692 ;
+C 67 ; WX 689 ; N C ; B 42 -15 654 707 ;
+C 68 ; WX 777 ; N D ; B 33 0 735 692 ;
+C 69 ; WX 629 ; N E ; B 33 0 604 692 ;
+C 70 ; WX 593 ; N F ; B 37 0 568 692 ;
+C 71 ; WX 726 ; N G ; B 42 -15 709 707 ;
+C 72 ; WX 807 ; N H ; B 33 0 774 692 ;
+C 73 ; WX 384 ; N I ; B 33 0 351 692 ;
+C 74 ; WX 386 ; N J ; B 6 -114 361 692 ;
+C 75 ; WX 707 ; N K ; B 33 -6 719 692 ;
+C 76 ; WX 585 ; N L ; B 33 0 584 692 ;
+C 77 ; WX 918 ; N M ; B 23 0 885 692 ;
+C 78 ; WX 739 ; N N ; B 25 0 719 692 ;
+C 79 ; WX 768 ; N O ; B 42 -15 726 707 ;
+C 80 ; WX 650 ; N P ; B 33 0 623 692 ;
+C 81 ; WX 768 ; N Q ; B 42 -193 726 707 ;
+C 82 ; WX 684 ; N R ; B 33 0 686 692 ;
+C 83 ; WX 561 ; N S ; B 42 -15 533 707 ;
+C 84 ; WX 624 ; N T ; B 15 0 609 692 ;
+C 85 ; WX 786 ; N U ; B 29 -15 757 692 ;
+C 86 ; WX 645 ; N V ; B -16 0 679 692 ;
+C 87 ; WX 933 ; N W ; B -10 0 960 692 ;
+C 88 ; WX 634 ; N X ; B -19 0 671 692 ;
+C 89 ; WX 617 ; N Y ; B -12 0 655 692 ;
+C 90 ; WX 614 ; N Z ; B 0 0 606 692 ;
+C 91 ; WX 335 ; N bracketleft ; B 123 -128 308 692 ;
+C 92 ; WX 379 ; N backslash ; B 34 -15 345 707 ;
+C 93 ; WX 335 ; N bracketright ; B 27 -128 212 692 ;
+C 94 ; WX 600 ; N asciicircum ; B 56 215 544 668 ;
+C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ;
+C 96 ; WX 252 ; N quoteleft ; B 40 399 212 719 ;
+C 97 ; WX 544 ; N a ; B 41 -12 561 502 ;
+C 98 ; WX 605 ; N b ; B 15 -12 571 742 ;
+C 99 ; WX 494 ; N c ; B 34 -12 484 502 ;
+C 100 ; WX 605 ; N d ; B 34 -12 596 742 ;
+C 101 ; WX 519 ; N e ; B 34 -12 505 502 ;
+C 102 ; WX 342 ; N f ; B 27 0 421 742 ; L i fi ; L l fl ;
+C 103 ; WX 533 ; N g ; B 25 -242 546 512 ;
+C 104 ; WX 631 ; N h ; B 19 0 622 742 ;
+C 105 ; WX 316 ; N i ; B 26 0 307 720 ;
+C 106 ; WX 316 ; N j ; B -12 -232 260 720 ;
+C 107 ; WX 582 ; N k ; B 19 0 595 742 ;
+C 108 ; WX 309 ; N l ; B 19 0 300 742 ;
+C 109 ; WX 948 ; N m ; B 26 0 939 502 ;
+C 110 ; WX 638 ; N n ; B 26 0 629 502 ;
+C 111 ; WX 585 ; N o ; B 34 -12 551 502 ;
+C 112 ; WX 615 ; N p ; B 19 -230 581 502 ;
+C 113 ; WX 597 ; N q ; B 34 -230 596 502 ;
+C 114 ; WX 440 ; N r ; B 26 0 442 502 ;
+C 115 ; WX 446 ; N s ; B 38 -12 425 502 ;
+C 116 ; WX 370 ; N t ; B 32 -12 373 616 ;
+C 117 ; WX 629 ; N u ; B 23 -12 620 502 ;
+C 118 ; WX 520 ; N v ; B -8 0 546 490 ;
+C 119 ; WX 774 ; N w ; B -10 0 802 490 ;
+C 120 ; WX 522 ; N x ; B -15 0 550 490 ;
+C 121 ; WX 524 ; N y ; B -12 -242 557 490 ;
+C 122 ; WX 483 ; N z ; B -1 0 480 490 ;
+C 123 ; WX 365 ; N braceleft ; B 74 -128 325 692 ;
+C 124 ; WX 284 ; N bar ; B 94 -250 190 750 ;
+C 125 ; WX 365 ; N braceright ; B 40 -128 291 692 ;
+C 126 ; WX 600 ; N asciitilde ; B 50 158 551 339 ;
+C 161 ; WX 278 ; N exclamdown ; B 47 -217 231 502 ;
+C 162 ; WX 560 ; N cent ; B 39 -15 546 678 ;
+C 163 ; WX 560 ; N sterling ; B 21 0 555 679 ;
+C 164 ; WX 100 ; N fraction ; B -155 -27 255 695 ;
+C 165 ; WX 560 ; N yen ; B 3 0 562 668 ;
+C 166 ; WX 560 ; N florin ; B -40 -135 562 691 ;
+C 167 ; WX 566 ; N section ; B 35 -115 531 707 ;
+C 168 ; WX 560 ; N currency ; B 21 73 539 596 ;
+C 169 ; WX 252 ; N quotesingle ; B 57 407 196 707 ;
+C 170 ; WX 473 ; N quotedblleft ; B 40 399 433 719 ;
+C 171 ; WX 487 ; N guillemotleft ; B 40 37 452 464 ;
+C 172 ; WX 287 ; N guilsinglleft ; B 40 37 252 464 ;
+C 173 ; WX 287 ; N guilsinglright ; B 35 37 247 464 ;
+C 174 ; WX 639 ; N fi ; B 27 0 630 742 ;
+C 175 ; WX 639 ; N fl ; B 27 0 630 742 ;
+C 177 ; WX 500 ; N endash ; B 0 209 500 292 ;
+C 178 ; WX 510 ; N dagger ; B 35 -125 475 707 ;
+C 179 ; WX 486 ; N daggerdbl ; B 35 -119 451 707 ;
+C 180 ; WX 280 ; N periodcentered ; B 48 156 232 342 ;
+C 182 ; WX 552 ; N paragraph ; B 35 -101 527 692 ;
+C 183 ; WX 455 ; N bullet ; B 50 174 405 529 ;
+C 184 ; WX 252 ; N quotesinglbase ; B 40 -153 212 167 ;
+C 185 ; WX 473 ; N quotedblbase ; B 40 -153 433 167 ;
+C 186 ; WX 473 ; N quotedblright ; B 40 387 433 707 ;
+C 187 ; WX 487 ; N guillemotright ; B 35 37 447 464 ;
+C 188 ; WX 1000 ; N ellipsis ; B 75 -12 925 174 ;
+C 189 ; WX 1289 ; N perthousand ; B 40 -31 1249 701 ;
+C 191 ; WX 456 ; N questiondown ; B 23 -217 436 502 ;
+C 193 ; WX 430 ; N grave ; B 40 511 312 740 ;
+C 194 ; WX 430 ; N acute ; B 119 511 391 740 ;
+C 195 ; WX 430 ; N circumflex ; B 28 520 402 747 ;
+C 196 ; WX 430 ; N tilde ; B 2 553 427 706 ;
+C 197 ; WX 430 ; N macron ; B 60 587 371 674 ;
+C 198 ; WX 430 ; N breve ; B 56 556 375 716 ;
+C 199 ; WX 430 ; N dotaccent ; B 136 561 294 710 ;
+C 200 ; WX 430 ; N dieresis ; B 16 561 414 710 ;
+C 202 ; WX 430 ; N ring ; B 96 540 334 762 ;
+C 203 ; WX 430 ; N cedilla ; B 136 -246 335 0 ;
+C 205 ; WX 430 ; N hungarumlaut ; B 64 521 446 751 ;
+C 206 ; WX 430 ; N ogonek ; B 105 -246 325 0 ;
+C 207 ; WX 430 ; N caron ; B 28 520 402 747 ;
+C 208 ; WX 1000 ; N emdash ; B 0 209 1000 292 ;
+C 225 ; WX 879 ; N AE ; B -77 0 854 692 ;
+C 227 ; WX 405 ; N ordfeminine ; B 28 265 395 590 ;
+C 232 ; WX 591 ; N Lslash ; B 30 0 590 692 ;
+C 233 ; WX 768 ; N Oslash ; B 42 -61 726 747 ;
+C 234 ; WX 1049 ; N OE ; B 42 0 1024 692 ;
+C 235 ; WX 427 ; N ordmasculine ; B 28 265 399 590 ;
+C 241 ; WX 806 ; N ae ; B 41 -12 792 502 ;
+C 245 ; WX 316 ; N dotlessi ; B 26 0 307 502 ;
+C 248 ; WX 321 ; N lslash ; B 16 0 332 742 ;
+C 249 ; WX 585 ; N oslash ; B 34 -51 551 535 ;
+C 250 ; WX 866 ; N oe ; B 34 -12 852 502 ;
+C 251 ; WX 662 ; N germandbls ; B 29 -12 647 742 ;
+C -1 ; WX 402 ; N onesuperior ; B 71 272 324 680 ;
+C -1 ; WX 600 ; N minus ; B 58 210 542 290 ;
+C -1 ; WX 396 ; N degree ; B 35 360 361 680 ;
+C -1 ; WX 585 ; N oacute ; B 34 -12 551 740 ;
+C -1 ; WX 768 ; N Odieresis ; B 42 -15 726 881 ;
+C -1 ; WX 585 ; N odieresis ; B 34 -12 551 710 ;
+C -1 ; WX 629 ; N Eacute ; B 33 0 604 904 ;
+C -1 ; WX 629 ; N ucircumflex ; B 23 -12 620 747 ;
+C -1 ; WX 900 ; N onequarter ; B 73 -27 814 695 ;
+C -1 ; WX 600 ; N logicalnot ; B 58 95 542 397 ;
+C -1 ; WX 629 ; N Ecircumflex ; B 33 0 604 905 ;
+C -1 ; WX 900 ; N onehalf ; B 53 -27 849 695 ;
+C -1 ; WX 768 ; N Otilde ; B 42 -15 726 876 ;
+C -1 ; WX 629 ; N uacute ; B 23 -12 620 740 ;
+C -1 ; WX 519 ; N eacute ; B 34 -12 505 740 ;
+C -1 ; WX 316 ; N iacute ; B 26 0 329 740 ;
+C -1 ; WX 629 ; N Egrave ; B 33 0 604 904 ;
+C -1 ; WX 316 ; N icircumflex ; B -28 0 346 747 ;
+C -1 ; WX 629 ; N mu ; B 23 -242 620 502 ;
+C -1 ; WX 284 ; N brokenbar ; B 94 -175 190 675 ;
+C -1 ; WX 609 ; N thorn ; B 13 -230 575 722 ;
+C -1 ; WX 644 ; N Aring ; B -28 0 663 872 ;
+C -1 ; WX 524 ; N yacute ; B -12 -242 557 740 ;
+C -1 ; WX 617 ; N Ydieresis ; B -12 0 655 881 ;
+C -1 ; WX 1090 ; N trademark ; B 38 277 1028 692 ;
+C -1 ; WX 800 ; N registered ; B 36 -15 764 707 ;
+C -1 ; WX 585 ; N ocircumflex ; B 34 -12 551 747 ;
+C -1 ; WX 644 ; N Agrave ; B -28 0 663 904 ;
+C -1 ; WX 561 ; N Scaron ; B 42 -15 533 916 ;
+C -1 ; WX 786 ; N Ugrave ; B 29 -15 757 904 ;
+C -1 ; WX 629 ; N Edieresis ; B 33 0 604 881 ;
+C -1 ; WX 786 ; N Uacute ; B 29 -15 757 904 ;
+C -1 ; WX 585 ; N otilde ; B 34 -12 551 706 ;
+C -1 ; WX 638 ; N ntilde ; B 26 0 629 706 ;
+C -1 ; WX 524 ; N ydieresis ; B -12 -242 557 710 ;
+C -1 ; WX 644 ; N Aacute ; B -28 0 663 904 ;
+C -1 ; WX 585 ; N eth ; B 34 -12 551 742 ;
+C -1 ; WX 544 ; N acircumflex ; B 41 -12 561 747 ;
+C -1 ; WX 544 ; N aring ; B 41 -12 561 762 ;
+C -1 ; WX 768 ; N Ograve ; B 42 -15 726 904 ;
+C -1 ; WX 494 ; N ccedilla ; B 34 -246 484 502 ;
+C -1 ; WX 600 ; N multiply ; B 75 20 525 476 ;
+C -1 ; WX 600 ; N divide ; B 58 6 542 494 ;
+C -1 ; WX 402 ; N twosuperior ; B 29 272 382 680 ;
+C -1 ; WX 739 ; N Ntilde ; B 25 0 719 876 ;
+C -1 ; WX 629 ; N ugrave ; B 23 -12 620 740 ;
+C -1 ; WX 786 ; N Ucircumflex ; B 29 -15 757 905 ;
+C -1 ; WX 644 ; N Atilde ; B -28 0 663 876 ;
+C -1 ; WX 483 ; N zcaron ; B -1 0 480 747 ;
+C -1 ; WX 316 ; N idieresis ; B -37 0 361 710 ;
+C -1 ; WX 644 ; N Acircumflex ; B -28 0 663 905 ;
+C -1 ; WX 384 ; N Icircumflex ; B 4 0 380 905 ;
+C -1 ; WX 617 ; N Yacute ; B -12 0 655 904 ;
+C -1 ; WX 768 ; N Oacute ; B 42 -15 726 904 ;
+C -1 ; WX 644 ; N Adieresis ; B -28 0 663 881 ;
+C -1 ; WX 614 ; N Zcaron ; B 0 0 606 916 ;
+C -1 ; WX 544 ; N agrave ; B 41 -12 561 740 ;
+C -1 ; WX 402 ; N threesuperior ; B 30 265 368 680 ;
+C -1 ; WX 585 ; N ograve ; B 34 -12 551 740 ;
+C -1 ; WX 900 ; N threequarters ; B 40 -27 842 695 ;
+C -1 ; WX 783 ; N Eth ; B 35 0 741 692 ;
+C -1 ; WX 600 ; N plusminus ; B 58 0 542 549 ;
+C -1 ; WX 629 ; N udieresis ; B 23 -12 620 710 ;
+C -1 ; WX 519 ; N edieresis ; B 34 -12 505 710 ;
+C -1 ; WX 544 ; N aacute ; B 41 -12 561 740 ;
+C -1 ; WX 316 ; N igrave ; B -17 0 307 740 ;
+C -1 ; WX 384 ; N Idieresis ; B -13 0 397 881 ;
+C -1 ; WX 544 ; N adieresis ; B 41 -12 561 710 ;
+C -1 ; WX 384 ; N Iacute ; B 33 0 373 904 ;
+C -1 ; WX 800 ; N copyright ; B 36 -15 764 707 ;
+C -1 ; WX 384 ; N Igrave ; B 9 0 351 904 ;
+C -1 ; WX 689 ; N Ccedilla ; B 42 -246 654 707 ;
+C -1 ; WX 446 ; N scaron ; B 38 -12 425 747 ;
+C -1 ; WX 519 ; N egrave ; B 34 -12 505 740 ;
+C -1 ; WX 768 ; N Ocircumflex ; B 42 -15 726 905 ;
+C -1 ; WX 640 ; N Thorn ; B 33 0 622 692 ;
+C -1 ; WX 544 ; N atilde ; B 41 -12 561 706 ;
+C -1 ; WX 786 ; N Udieresis ; B 29 -15 757 881 ;
+C -1 ; WX 519 ; N ecircumflex ; B 34 -12 505 747 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 685
+
+KPX A z 25
+KPX A y -40
+KPX A w -42
+KPX A v -48
+KPX A u -18
+KPX A t -12
+KPX A s 6
+KPX A quoteright -110
+KPX A quotedblright -80
+KPX A q -6
+KPX A p -18
+KPX A o -12
+KPX A e -6
+KPX A d -12
+KPX A c -12
+KPX A b -12
+KPX A a -6
+KPX A Y -64
+KPX A X -18
+KPX A W -54
+KPX A V -70
+KPX A U -40
+KPX A T -58
+KPX A Q -18
+KPX A O -18
+KPX A G -18
+KPX A C -18
+
+KPX B y -18
+KPX B u -12
+KPX B r -12
+KPX B o -6
+KPX B l -15
+KPX B k -15
+KPX B i -12
+KPX B h -15
+KPX B e -6
+KPX B b -10
+KPX B a -12
+KPX B W -20
+KPX B V -20
+KPX B U -25
+KPX B T -20
+
+KPX C z -5
+KPX C y -24
+KPX C u -18
+KPX C r -6
+KPX C o -12
+KPX C e -12
+KPX C a -16
+KPX C Q -6
+KPX C O -6
+KPX C G -6
+KPX C C -6
+
+KPX D u -12
+KPX D r -12
+KPX D period -40
+KPX D o -5
+KPX D i -12
+KPX D h -18
+KPX D e -5
+KPX D comma -40
+KPX D a -15
+KPX D Y -60
+KPX D W -40
+KPX D V -40
+
+KPX E y -30
+KPX E w -24
+KPX E v -24
+KPX E u -12
+KPX E t -18
+KPX E s -12
+KPX E r -4
+KPX E q -6
+KPX E period 10
+KPX E p -18
+KPX E o -6
+KPX E n -4
+KPX E m -4
+KPX E j -6
+KPX E i -6
+KPX E g -6
+KPX E e -6
+KPX E d -6
+KPX E comma 10
+KPX E c -6
+KPX E b -5
+KPX E a -4
+KPX E Y -6
+KPX E W -6
+KPX E V -6
+
+KPX F y -18
+KPX F u -12
+KPX F r -36
+KPX F quoteright 20
+KPX F quotedblright 20
+KPX F period -150
+KPX F o -36
+KPX F l -12
+KPX F i -22
+KPX F e -36
+KPX F comma -150
+KPX F a -48
+KPX F A -60
+
+KPX G y -12
+KPX G u -12
+KPX G r -18
+KPX G quotedblright -20
+KPX G n -18
+KPX G l -6
+KPX G i -12
+KPX G h -12
+KPX G a -12
+
+KPX H y -24
+KPX H u -26
+KPX H o -30
+KPX H i -18
+KPX H e -30
+KPX H a -25
+
+KPX I z -6
+KPX I y -6
+KPX I x -6
+KPX I w -18
+KPX I v -24
+KPX I u -26
+KPX I t -24
+KPX I s -18
+KPX I r -12
+KPX I p -26
+KPX I o -30
+KPX I n -18
+KPX I m -18
+KPX I l -6
+KPX I k -6
+KPX I h -6
+KPX I g -6
+KPX I f -6
+KPX I e -30
+KPX I d -30
+KPX I c -30
+KPX I b -6
+KPX I a -24
+
+KPX J y -20
+KPX J u -36
+KPX J o -35
+KPX J i -20
+KPX J e -35
+KPX J bracketright 15
+KPX J braceright 15
+KPX J a -36
+
+KPX K y -70
+KPX K w -60
+KPX K v -80
+KPX K u -42
+KPX K o -30
+KPX K l 10
+KPX K i 6
+KPX K h 10
+KPX K e -18
+KPX K a -6
+KPX K Q -36
+KPX K O -36
+KPX K G -36
+KPX K C -36
+KPX K A 20
+
+KPX L y -52
+KPX L w -58
+KPX L u -12
+KPX L quoteright -130
+KPX L quotedblright -130
+KPX L l 6
+KPX L j -6
+KPX L Y -70
+KPX L W -78
+KPX L V -95
+KPX L U -32
+KPX L T -80
+KPX L Q -12
+KPX L O -12
+KPX L G -12
+KPX L C -12
+KPX L A 30
+
+KPX M y -24
+KPX M u -36
+KPX M o -30
+KPX M n -6
+KPX M j -12
+KPX M i -12
+KPX M e -30
+KPX M d -30
+KPX M c -30
+KPX M a -25
+
+KPX N y -24
+KPX N u -30
+KPX N o -30
+KPX N i -24
+KPX N e -30
+KPX N a -30
+
+KPX O z -6
+KPX O u -6
+KPX O t -6
+KPX O s -6
+KPX O r -10
+KPX O q -6
+KPX O period -40
+KPX O p -10
+KPX O o -6
+KPX O n -10
+KPX O m -10
+KPX O l -15
+KPX O k -15
+KPX O i -6
+KPX O h -15
+KPX O g -6
+KPX O e -6
+KPX O d -6
+KPX O comma -40
+KPX O c -6
+KPX O b -15
+KPX O a -12
+KPX O Y -50
+KPX O X -40
+KPX O W -35
+KPX O V -35
+KPX O T -40
+KPX O A -30
+
+KPX P y 10
+KPX P u -18
+KPX P t -6
+KPX P s -30
+KPX P r -12
+KPX P quoteright 20
+KPX P quotedblright 20
+KPX P period -200
+KPX P o -36
+KPX P n -12
+KPX P l -15
+KPX P i -6
+KPX P hyphen -30
+KPX P h -15
+KPX P e -36
+KPX P comma -200
+KPX P a -36
+KPX P I -20
+KPX P H -20
+KPX P E -20
+KPX P A -85
+
+KPX Q u -6
+KPX Q a -18
+KPX Q Y -50
+KPX Q X -40
+KPX Q W -35
+KPX Q V -35
+KPX Q U -25
+KPX Q T -40
+KPX Q A -30
+
+KPX R y -20
+KPX R u -12
+KPX R t -25
+KPX R quoteright -10
+KPX R quotedblright -10
+KPX R o -12
+KPX R e -18
+KPX R a -6
+KPX R Y -32
+KPX R X 20
+KPX R W -18
+KPX R V -26
+KPX R U -30
+KPX R T -20
+KPX R Q -10
+KPX R O -10
+KPX R G -10
+KPX R C -10
+
+KPX S y -35
+KPX S w -30
+KPX S v -40
+KPX S u -24
+KPX S t -24
+KPX S r -10
+KPX S quoteright -15
+KPX S quotedblright -15
+KPX S p -24
+KPX S n -24
+KPX S m -24
+KPX S l -18
+KPX S k -24
+KPX S j -30
+KPX S i -12
+KPX S h -12
+KPX S a -18
+
+KPX T z -64
+KPX T y -74
+KPX T w -72
+KPX T u -74
+KPX T semicolon -50
+KPX T s -82
+KPX T r -74
+KPX T quoteright 24
+KPX T quotedblright 24
+KPX T period -95
+KPX T parenright 40
+KPX T o -90
+KPX T m -72
+KPX T i -28
+KPX T hyphen -110
+KPX T endash -40
+KPX T emdash -60
+KPX T e -80
+KPX T comma -95
+KPX T bracketright 40
+KPX T braceright 30
+KPX T a -90
+KPX T Y 12
+KPX T X 10
+KPX T W 15
+KPX T V 6
+KPX T T 30
+KPX T S -12
+KPX T Q -25
+KPX T O -25
+KPX T G -25
+KPX T C -25
+KPX T A -52
+
+KPX U z -35
+KPX U y -30
+KPX U x -30
+KPX U v -30
+KPX U t -36
+KPX U s -45
+KPX U r -50
+KPX U p -50
+KPX U n -50
+KPX U m -50
+KPX U l -12
+KPX U k -12
+KPX U i -22
+KPX U h -6
+KPX U g -40
+KPX U f -20
+KPX U d -40
+KPX U c -40
+KPX U b -12
+KPX U a -50
+KPX U A -50
+
+KPX V y -36
+KPX V u -50
+KPX V semicolon -45
+KPX V r -75
+KPX V quoteright 50
+KPX V quotedblright 36
+KPX V period -135
+KPX V parenright 80
+KPX V o -70
+KPX V i 20
+KPX V hyphen -90
+KPX V emdash -20
+KPX V e -70
+KPX V comma -135
+KPX V colon -45
+KPX V bracketright 80
+KPX V braceright 80
+KPX V a -70
+KPX V Q -20
+KPX V O -20
+KPX V G -20
+KPX V C -20
+KPX V A -60
+
+KPX W y -50
+KPX W u -46
+KPX W t -30
+KPX W semicolon -40
+KPX W r -50
+KPX W quoteright 40
+KPX W quotedblright 24
+KPX W period -100
+KPX W parenright 80
+KPX W o -60
+KPX W m -50
+KPX W i 5
+KPX W hyphen -70
+KPX W h 20
+KPX W e -60
+KPX W d -60
+KPX W comma -100
+KPX W colon -40
+KPX W bracketright 80
+KPX W braceright 70
+KPX W a -75
+KPX W T 30
+KPX W Q -20
+KPX W O -20
+KPX W G -20
+KPX W C -20
+KPX W A -58
+
+KPX X y -40
+KPX X u -24
+KPX X quoteright 15
+KPX X e -6
+KPX X a -6
+KPX X Q -24
+KPX X O -30
+KPX X G -30
+KPX X C -30
+KPX X A 20
+
+KPX Y v -50
+KPX Y u -65
+KPX Y t -46
+KPX Y semicolon -37
+KPX Y quoteright 50
+KPX Y quotedblright 36
+KPX Y q -100
+KPX Y period -90
+KPX Y parenright 60
+KPX Y o -90
+KPX Y l 25
+KPX Y i 15
+KPX Y hyphen -100
+KPX Y endash -30
+KPX Y emdash -50
+KPX Y e -90
+KPX Y d -90
+KPX Y comma -90
+KPX Y colon -60
+KPX Y bracketright 80
+KPX Y braceright 64
+KPX Y a -80
+KPX Y Y 12
+KPX Y X 12
+KPX Y W 12
+KPX Y V 12
+KPX Y T 30
+KPX Y Q -40
+KPX Y O -40
+KPX Y G -40
+KPX Y C -40
+KPX Y A -55
+
+KPX Z y -36
+KPX Z w -36
+KPX Z u -6
+KPX Z o -12
+KPX Z i -12
+KPX Z e -6
+KPX Z a -6
+KPX Z Q -18
+KPX Z O -18
+KPX Z G -18
+KPX Z C -18
+KPX Z A 25
+
+KPX a quoteright -45
+KPX a quotedblright -40
+
+KPX b y -15
+KPX b w -20
+KPX b v -20
+KPX b quoteright -45
+KPX b quotedblright -40
+KPX b period -10
+KPX b comma -10
+
+KPX braceleft Y 64
+KPX braceleft W 64
+KPX braceleft V 64
+KPX braceleft T 25
+KPX braceleft J 50
+
+KPX bracketleft Y 64
+KPX bracketleft W 64
+KPX bracketleft V 64
+KPX bracketleft T 35
+KPX bracketleft J 60
+
+KPX c quoteright -5
+
+KPX colon space -20
+
+KPX comma space -40
+KPX comma quoteright -100
+KPX comma quotedblright -100
+
+KPX d quoteright -24
+KPX d quotedblright -24
+
+KPX e z -4
+KPX e quoteright -25
+KPX e quotedblright -20
+
+KPX f quotesingle 70
+KPX f quoteright 68
+KPX f quotedblright 68
+KPX f period -10
+KPX f parenright 110
+KPX f comma -20
+KPX f bracketright 100
+KPX f braceright 80
+
+KPX g y 20
+KPX g p 20
+KPX g f 20
+KPX g comma 10
+
+KPX h quoteright -60
+KPX h quotedblright -60
+
+KPX i quoteright -20
+KPX i quotedblright -20
+
+KPX j quoteright -20
+KPX j quotedblright -20
+KPX j period -10
+KPX j comma -10
+
+KPX k quoteright -30
+KPX k quotedblright -30
+
+KPX l quoteright -24
+KPX l quotedblright -24
+
+KPX m quoteright -60
+KPX m quotedblright -60
+
+KPX n quoteright -60
+KPX n quotedblright -60
+
+KPX o z -12
+KPX o y -25
+KPX o x -18
+KPX o w -30
+KPX o v -30
+KPX o quoteright -45
+KPX o quotedblright -40
+KPX o period -10
+KPX o comma -10
+
+KPX p z -10
+KPX p y -15
+KPX p w -15
+KPX p quoteright -45
+KPX p quotedblright -60
+KPX p period -10
+KPX p comma -10
+
+KPX parenleft Y 64
+KPX parenleft W 64
+KPX parenleft V 64
+KPX parenleft T 50
+KPX parenleft J 50
+
+KPX period space -40
+KPX period quoteright -100
+KPX period quotedblright -100
+
+KPX q quoteright -50
+KPX q quotedblright -50
+KPX q period -10
+KPX q comma -10
+
+KPX quotedblleft z -26
+KPX quotedblleft w 10
+KPX quotedblleft u -40
+KPX quotedblleft t -40
+KPX quotedblleft s -32
+KPX quotedblleft r -40
+KPX quotedblleft q -70
+KPX quotedblleft p -40
+KPX quotedblleft o -70
+KPX quotedblleft n -40
+KPX quotedblleft m -40
+KPX quotedblleft g -50
+KPX quotedblleft f -30
+KPX quotedblleft e -70
+KPX quotedblleft d -70
+KPX quotedblleft c -70
+KPX quotedblleft a -60
+KPX quotedblleft Y 30
+KPX quotedblleft X 20
+KPX quotedblleft W 40
+KPX quotedblleft V 40
+KPX quotedblleft T 18
+KPX quotedblleft J -24
+KPX quotedblleft A -122
+
+KPX quotedblright space -40
+KPX quotedblright period -100
+KPX quotedblright comma -100
+
+KPX quoteleft z -26
+KPX quoteleft y -5
+KPX quoteleft x -5
+KPX quoteleft w 5
+KPX quoteleft v -5
+KPX quoteleft u -25
+KPX quoteleft t -25
+KPX quoteleft s -40
+KPX quoteleft r -40
+KPX quoteleft quoteleft -30
+KPX quoteleft q -70
+KPX quoteleft p -40
+KPX quoteleft o -70
+KPX quoteleft n -40
+KPX quoteleft m -40
+KPX quoteleft g -50
+KPX quoteleft f -10
+KPX quoteleft e -70
+KPX quoteleft d -70
+KPX quoteleft c -70
+KPX quoteleft a -60
+KPX quoteleft Y 35
+KPX quoteleft X 30
+KPX quoteleft W 35
+KPX quoteleft V 35
+KPX quoteleft T 35
+KPX quoteleft J -24
+KPX quoteleft A -122
+
+KPX quoteright v -20
+KPX quoteright t -50
+KPX quoteright space -40
+KPX quoteright s -70
+KPX quoteright r -42
+KPX quoteright quoteright -30
+KPX quoteright period -100
+KPX quoteright m -42
+KPX quoteright l -6
+KPX quoteright d -100
+KPX quoteright comma -100
+
+KPX r z 20
+KPX r y 18
+KPX r x 12
+KPX r w 30
+KPX r v 30
+KPX r u 8
+KPX r t 8
+KPX r semicolon 20
+KPX r quoteright -20
+KPX r quotedblright -10
+KPX r q -6
+KPX r period -60
+KPX r o -6
+KPX r n 8
+KPX r m 8
+KPX r l -10
+KPX r k -10
+KPX r i 8
+KPX r hyphen -60
+KPX r h -10
+KPX r g 5
+KPX r f 8
+KPX r emdash -20
+KPX r e -20
+KPX r d -20
+KPX r comma -80
+KPX r colon 20
+KPX r c -20
+
+KPX s quoteright -40
+KPX s quotedblright -40
+
+KPX semicolon space -20
+
+KPX space quotesinglbase -100
+KPX space quoteleft -40
+KPX space quotedblleft -40
+KPX space quotedblbase -100
+KPX space Y -60
+KPX space W -60
+KPX space V -60
+KPX space T -40
+
+KPX t period 15
+KPX t comma 10
+
+KPX u quoteright -60
+KPX u quotedblright -60
+
+KPX v semicolon 20
+KPX v quoteright 5
+KPX v quotedblright 10
+KPX v q -15
+KPX v period -75
+KPX v o -15
+KPX v e -15
+KPX v d -15
+KPX v comma -90
+KPX v colon 20
+KPX v c -15
+KPX v a -15
+
+KPX w semicolon 20
+KPX w quoteright 15
+KPX w quotedblright 20
+KPX w q -10
+KPX w period -60
+KPX w o -10
+KPX w e -10
+KPX w d -10
+KPX w comma -68
+KPX w colon 20
+KPX w c -10
+
+KPX x quoteright -25
+KPX x quotedblright -20
+KPX x q -6
+KPX x o -6
+KPX x e -12
+KPX x d -12
+KPX x c -12
+
+KPX y semicolon 20
+KPX y quoteright 5
+KPX y quotedblright 10
+KPX y period -72
+KPX y hyphen -20
+KPX y comma -72
+KPX y colon 20
+
+KPX z quoteright -20
+KPX z quotedblright -20
+KPX z o -6
+KPX z e -6
+KPX z d -6
+KPX z c -6
+EndKernPairs
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/UTB_____.pfa b/e2e-tests/cypress/fonts/Type1/UTB_____.pfa
new file mode 100644
index 00000000..36ef3395
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTB_____.pfa
@@ -0,0 +1,1134 @@
+%!PS-AdobeFont-1.0: Utopia-Bold 001.001
+%%CreationDate: Wed Oct 2 18:24:56 1991
+%%VMusage: 33079 39971
+%% Utopia is a registered trademark of Adobe Systems Incorporated.
+11 dict begin
+/FontInfo 10 dict dup begin
+/version (001.001) readonly def
+/Notice (Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.) readonly def
+/FullName (Utopia Bold) readonly def
+/FamilyName (Utopia) readonly def
+/Weight (Bold) readonly def
+/ItalicAngle 0 def
+/isFixedPitch false def
+/UnderlinePosition -100 def
+/UnderlineThickness 50 def
+end readonly def
+/FontName /Utopia-Bold def
+/Encoding StandardEncoding def
+/PaintType 0 def
+/FontType 1 def
+/FontMatrix [0.001 0 0 0.001 0 0] readonly def
+/UniqueID 36543 def
+/FontBBox{-155 -250 1249 916}readonly def
+currentdict end
+currentfile eexec
+f9cd86fd4821715265f16a614e3770ef80561e77ba6ecbb4ef4232445eef2839
+94e93e3c8c9b09c09d71542241821e07888ceb54e8cacc8a45802f50c0afeeca
+5e3d9114c1860bcb2ba5fd2a879a0d79953e30c90d8347513f4ca5f05b2e231b
+4973bd1e9db66a39d846a8b3d9e48da72e2de8743ba2e104893167c235719245
+87b43ed3b6552cd85e4bbdeb9f46bd813298d531c74be81995a52ceaad4112c7
+f65773b088bffbc9874f615371c5e7b50ae99d5b6acf9c80d87057bf3a424377
+f4284b7232096b6ea623dacd1c925ce09cfa6515f675aacf815c38e984b4ba40
+0d409bb62cd4ebac75201c8e782e68fd73e8622c46b696b1ba37ca9621400041
+d95b948a6bbc089fe6b230b39bd358228af9b3e68418166a9adc4e7665341088
+c47c074aa4b21b0949fef9929e1a32ffddd2d02145cdb256863cf6067d27ed9a
+f0aba85b1e0cfcbab7e74880e9693b5626ad31e6b5c2d208087d86c116513212
+710c9d6ca50e1503f8d4c2863fa378f8184189af0cf109f4affe7cb74563d513
+2104fb9dc6f3f5992e075633a0baebda8ebe9a07c3ec4b25fef015d9915a26b1
+d401a6722015090672714181580350560caf79bccd6040c0c1651d917c73107c
+2d46aad5d4a370b55e896ce1eed127b6f0aabaf7c4ca62e89003032abb2c95ba
+58c4a0eb0bb1a59a439a6a0c2082e50725b9e8139324af06ff03b4269a697342
+8fab0c1fabd320f1452d9c435e48037eb8cf6df4233cd9c05691604146cc813f
+7ea131f21e9863b7bd1b087a583af14675b9bd31fd364d5dbddbc9b533a6ac38
+c4bdeb28a1ae3d2b374da72405acf67b1b3a83d80726301b3677c067c748d857
+ae1da4f723d4365647de10f90e96dd7e9ab9ef5c7e6866a13b807fd3f78136d5
+72950a10bb0165e61f12c60a813c7e35eeb9a0500725dd912ca2deebb238538e
+790af851b14643f40e5eea13660e57b5c407735079a292a7c162dd11d0a3d342
+67290f1c9261fa5de6b92ca1e7daeb7cb970c766b3e0396ec0ce55dffbef0bdc
+b437fbb4d63634b9abcf5844e76f9313ab2182e1fb3c47a705a1d97b79e93dbc
+5e48ac19b45f9be4b9225e5a7e1d3679dd21001eca3c08d6febd35fa5453290e
+1269c1d195e45338c10aa60fb8ab9e5a5aaf08c82e737f082bb6216d7cc1d5d8
+8906c8b05c4432d04561377438447eb5fade652af84c1ced99551365cf9e6f99
+03d1593de0e450003fb0d5b5588330d6527fdd0c8193fc0cd2293012310a425e
+0ea0f8b901b991648ebbc894cfb7d95da6d35fb7c947e887bd3b6599e673b921
+c96f2a033e30b341a14066076ff0abd5fe77f393b548a15ea2185f99a3545b01
+dada854fce1ddd3448a9aa42ba260a4f85a0d1bdcf08b3de2ff8b6411adff446
+4a2929193ade35f4e26006cfdc45929250bb76ef007043d739d798a4973f350f
+52fa1690423381539e3a4cc458600fed91fa8e16b5ae0aca710dbf318d16e3d9
+16a082a093b59f35ff92a1b235d5621e87ab471310ca26a07a9ecdd3cbdc9bdc
+7506ff13b886b26eda6d4ccc52a312593a0f2845a4a30aef829109e6b76bfcbd
+2f2fe267d9376acc0686c9b1df3a25fc41d9e5390a90249cc26c502e4bcf582e
+ca5054d96b6ebb2bec79ae23125b4c2a13a97570153b83dfcc4d9dec9717679a
+ba8c37c02697d8f47269b9ddb983bd4dc029968d0d1729bcb9f94c899906af40
+0df490eaad13acd5a893663fc90a850533263cf6657632e8e4df31e6e6636521
+78dac783feb9616172d7651e3e4092b409383ccfcf8aa04b330119c334d6c8ab
+bdf97d97fb475c25331044413a117dee0f15d6560cac5ca6c01c1c98b56d2971
+724c135de3c624fb7a1994a8267ead0628af72d5af1dc0ae3177c3b678dc15cf
+9dcf100dd9de9bb71f568661293515002220681b0afd9775464be2f66df08c5c
+7d3995a57ffd2af299564dd57614e3273d8555f705ce2a2cb095cd662acb8163
+dc5c587747e27fbfab667dd449228e7b2f40cea584c997d2b98e78a063edd327
+9e23947ba2577035ff349019293b13953ceea11b49b230e2a29d295b032ebbe1
+dacf823e3587184182d0b910794a1d784f0bdc21c74a65d12b5875cdf0c1cddd
+5995a6e56c9a95dfff55167f82b3de41a2eb653b0d1eb61e9c48654424ce13ec
+8671211b2b8a78839a4569b3cefae520e91c538f09d96f98ebbe905b01ddb080
+f7f6ff00d2f662e02bbe7dc3044b66723956559669000810f706f4ad544b9a29
+b384f554816696eb7b59540b407d438efb6c8bfdcf05324cdb65e9ffb8864a9a
+05adaa3678a341983aac1f6bed28040df2f0e18d27300cab0930958704d900fc
+5e194b149740b089814d1050e11a9d885bb1d835a7cc092ada9f08858e1ac5b1
+84b6fdfdea41d4b1241ae64fa3c47f125f48cc05c5dd8e2c896bb88b0a846379
+4477252f0d7abd31d4bd5703b9f280b09a59bfaec0663dcb2ccce2821e0e382f
+837f4a4fdf0e57d531383155f4fd633bc4b7315e54425a0395bd91f050783685
+eb0b85e15e331166795acc5dcb2ea1239209f8296c2c4cd1eb15b37e3ddfd2ba
+33a25c3761da66c0c08635101534607ca02990447c210689d31e49376d152161
+7696160aa2da75f993535e6cfbce229c657578620d7c8de8cf720f21ccc22ed1
+2be5c92bf7e26cb750f707fa7e2348a04a642ff319cda45fb9884caacb562df1
+365992416084a4d40da9831865b02246bda402f4354e8882e98215c37d3eb3c4
+d2f9ba2b93f190005da1bbb506a5d39e2a24be0ddc50e1bc5235d221a151698b
+803f0f0414bc085a395fbada62a73a9a3d4c66415ff1704226f8437e9bc470cc
+63011be0e1f3dbe6e6ad3ff35541961f6dfaae1425b2020349486880fe7fae78
+5a2db27a586f0daddab8ca62887db0a18462a9a3d821f31923c583bb731d7299
+f051b8db5f33fcf53c37c1e4ac7fa0275dd39ab9abc97cedd1c65a3d514d96df
+8b7a49217fe1af15b5c738e927028d1ec523e8e8575f018e7bb3f7e9afc81ff9
+3de7a4969ba59f7093772e5696bc4f2d6616419b43ff26f2a9da0e183f7bd542
+b31f175724d2e8f18a61ccd010d8c18394f640f905b6832fcfe7d3e9c02e3e03
+911dff14d0a1c73129ba85e2179372aff3c028388f30621d1f8aac757660079c
+63c22cfec96ad9ee4846c0854ec912abbf3f934e293e0f1211786455019a7f7f
+38f9e877f4186a9440c75a4babf3a9fbad1c5fc484082e1be4084ca3018b676f
+0e037cfaa4195a18948d8bb46a6d00b68213c09d5e69673ed4e00155fba9d840
+9e08ca2dffafc93110751b59ebc2b6ed4bf5f535ac9d73c9399d9081eb6b2ba0
+a13aabde56a7f9200749c9e866cac735bc6e2f7859cbda912f0c26e85d3f7b25
+3185ec64546cfeb89d3992b12815e70a548299615c6889379218c0e7dd1cffaa
+ff4ccaf3c35c77b09513b5f9b0d501322a862af4f6df0af50b62e4e1c1b3067a
+f5b24a44190c41e96b659f62921dd6075609f7d7db532298a425c675e4537952
+bf06fa9b1295e32068740c361048301f6ba6386602b8cb54bf8f38fd77669f5d
+791a7b1854b7f3effa193e9e5a435694b1c8578ec78426134c9c56e6c9f08951
+2469a5137bb452be5ec3b3d27c3cbf6e6fee0d58d73dd3d277bb0c1540ad102c
+50b746093832be65d05c102ddc982b08c7ec1fba629dbe27d4604c1476d91392
+801f91ff781aeabec88132162e021741f8f31f06db82b29d17fe66ee04e9c200
+5240d68ba82373c7a567ba33bad4aed1d6b5c570d03c5c394c09e5c5eb9e924d
+9e1c2f161a9d52cbafdad75dd674327ec81ce286cf1abfb5ddd8606de9265784
+5c950413caeca8ef80303793826c3ac4b58d9fd4b02fab612fdbf71c2a1698f9
+048769235eb9dd35823536662ccccc128c74818c8ae48edbe9a4624ece9d1ff5
+39795c7b4808520a08d611af58eec60bfc8d272a944201096c45749bc97f32ce
+a52618a1d6d53267ab4fde671ef1a47b03687a59e47a8e277c02a618af09b78c
+f5be89f5da433ea9d31cb8319bbd220099a8d7d2f879abd99b4a96b06ae3f8ab
+4cb89e72fea1355a010405cd72dedc7bd35b3ae32fac3d40642fe88e7a938579
+f4c10f751174cf042331e7274eeeaa28e5829f0e6e866783412a45e8ce8ce127
+5438c3fedf8cfbc9078e2b6dc439d9a513fb2a74a672b496e19af3c092342811
+983b1a3d6c60981db5aaf03b1fcd375f4ad64a10b57d378f52f29cb89dc77814
+1dd5106934bae98eee32b779eddc6cb5fd13d38ee5b39d4513338af4feafcece
+dda1f3d9b6f0b5cae4e7efa14921578cf07ad6e1eb7e0a3bf7d52bb7e2fc9656
+32c5c642a6ab07c108d79699d56727db2da956cb576b97648ade80eab2ae12bb
+2dedab0672ae666115758614c91108c9e4a4da0bd49f15379cf6716a303accbf
+477ae6756d18a0f6e3801f72eaff179849888104e38e69fb722a385a630e06ad
+e71c5553a14c5955f6189e08f8cce45ba8fbb5c5b6b0bac8ca48d11c0656f21a
+9b9286c4f958db61b111ffd40b3c8d69720b6e2eeac5452ba0837fd6806464f1
+0cf5bd73423b9f065b6ff58954a9090a53e31a16fa9703700fba14f8b6666ee7
+4b6a7bec61fdce1fc10245e2e90e31463bb7d326857e389d87834ddffa9ca581
+5a5abc54a958bd3738753e38b255cb1012cadb7278f8e84d9c649a4a659a8385
+19e128ddbd0985505fc613d9c577fc10c429648ad18771b0da1087ef584c2628
+b91d33da27c403c0214e0b7f984a9bfe6c2c574883f446e4dc1a563c32898bff
+ca2433f1161168565e063ecf045408d619f2a3215ee881cbd13a7751b14189bd
+a0cfafee7ce4d9600fdf993ceddd965c4a608129c89b52b5b59419ed079a6f57
+9efc222187cf25f848d47bf3bdca06ec2573fa8fe20624cfeec3f9ead6ded709
+75cae667bf025ad867944faef14fa9d6617b74cee3804d5edb688d9ed4a0c2bf
+1aede05eaca61e297f03026864c738014d8afd79294e062e04e5fce1a784d850
+b00e58fdf6fb5182d6ff08bcd05d2c7860dde223a37ce53131c09cba9c52e9e5
+76cf7e58bb99f045c8b23d67c43449c6c3f6acdb5b5f35be96d17fb0f8d42685
+cc79fbd421fdbed2fbfbdbac976736a6fb34c62eded4a12ad456d94aea2a2bf5
+f1fe4e1519361177998c8272866d26cc0e1af48fcc5a399edb8550046f3e8b31
+baf20c0ba1210fa2487cc2982e3d39e6a630d11813801bfdf4cc3695384410e6
+067d10d37aed54394448427032c9cce4d00f89418f10a45fda8aa392db636c06
+a26c0b39b25f128f7e5ad81a548f643eb1802117172d9be1fccaa3f1a73b7c1f
+1cb21f7f8f71fd8d063ebf41cf2f3eba32392f0303affcb36e113a7e46e5406d
+20e6c47a9be1e8f8085a8c49b04d219e13082d344c91dda69384c0b16d3fd7e5
+e46f213b789603c5d89257d6510ab618f24df56b1ded2029779af220b40eb86b
+cb9ecc32f2b1b713132ba229c3bfa888b014e12ccc1cc902b28bd5a9d5263ac2
+a47b38f37f0c9b279418b1bb81b288d0312667dfeb0aff86b8072c2bbdc79835
+f6a04d34c0825caef54293348bd6c672ba2ace4d7d3fa604faacba1a42dfe02c
+0150abaf33d0a6582071af6b15929a4857cd57048c9e32b04237ba047a2e2837
+a23da6917bf5912e2b1abaab68fa65891788a6c3b30ced2fb907e603a7654c27
+976d2fffbad1538395c05cf876bb3b64198e0a1b94f40d748c03373736018466
+6a9a9e53e0a159ac7a5890bcc18cab7172dedf4b8fdea3544184b0032edee0f4
+6429cd0451949277013d89dc6bbb23566cb158489d168a341271aa99618de261
+9580e6ebc094ebc5d7122fac0cc95021df41feffbf34ed59dee7556ecad36d75
+12b96ace5f81203e47d9e3b9698163a175767762fe2278648d0823b7a41e8730
+16fea5eaf17effef6b2c537b2727de3ab71987b49f9bed16fd8aaa06e5580643
+c2514bda11e7fb68f310bdded37ff6e6fd67b04c83fb8ef288da520954f0c2e4
+ee463de5ce7bcbdfa5403a4de7c64a7d9807dc7fc21678fcbee60d8b2bfa59bb
+c8303d7f060c0a2182f6779a386fd82b9b44550a03d98cfc0095524a7308cf6a
+e0c2c366b8bba56ccc354f3319a888e1483595b28f38868231631d25f50743f5
+07d08da61232da0353fe9fef9bee08c93e66b4cde93369efa538d03dcae19d88
+1fffcbd05c767b97fdf225685d18d7076b2b9b788404b79715b18655bf816f57
+2d96ea5f0d6ec848b399d0629c5775e5edde2a79a3dc6bb189506fcf65ff104b
+389ccf9387a3d6d243e4c83a5d78e9ee659b8e47b54559c80e15e1d9b3fdf838
+565e0ec14404dbfb5da05ebd0c3cc925f2a0be3ba741b9dfdc0531c2d35985ff
+89268541bbc6e4ecc13a50d937cead6b3a9863f85a8c1870838f4abd35494e28
+92248f65c7d3c452f6f743a345087661c3f5e8f09cc6854e7f2d6b5c9461c0f8
+3170337fb694d91478be1ccefe270f28e1c8ce48ffd25ed2dc83c070289405ce
+533af0bd4aa922d8d3b9220edc5b2fe6fadeeb81dfd270ee469b434f44622223
+307e041823e65f762048d028c2b66c306422102c19e3eb113b6f613759b68d15
+9ed5665ab83a1fd1e8d45f0de9c187db9f0f7d145c3bc457b5ac065a0bf447d4
+2f92b61a915f4dc8c03757eaf6cc115c87cf2aba456e01834a0e283347d487b5
+0e9bf1ef39ff7ce6505d1be0e20f425943cbbb4c188a359348d67c0247da0c3e
+3116f9a961e616894a704f192f550ed25503bf14c828ab8bf3211ce02689ba55
+49b2237fb5861f7fd2003d4c82a15ac10f5e1325928aeff9169fcf585334f62c
+571057f3e5d5bf3ebbbac63110353fcb05a533af4199d0abf922653efecb2177
+5fe358df229a5daac3ebede23bb469173a33ada03cb2afbe309195dbcfe08cf5
+472f9979956828289448a9781ff3b8a4a3f375a57ccc6d1ed120ac150b1b4ad0
+5d76760c330d85616e88801050c93c9e44dde990a8d1867ecdcea7700b6ad389
+010a5f0b846c2fbc7e4c4b2daaf0fc6443004d6993fbf3990f78d69350bdec37
+66b9f8d3659fb44cc7917208072d1cdc3618044e0706804aadbfd6e6823e197e
+e8dcbeac8f6ae996260f5d75279b3d1508ba47b61759dbc1dfe41cc229ff0f5a
+a3ff10b0e1ab34e1b29fc95f245b69d61f8ccd5835937b8cf2afda032bd85635
+9448b70d736946734ecacdfc0652cd5fc7653b3fd367c4c5d9c7f7ae1be27671
+8bf62030e55fed95b073eb8b805088eda02d726e2f9416eb02ef7ed5ee19b57f
+f196b0ce036cee614680585d911a289a4953b5288dbadd44e2e6c1e406836a6f
+cab530c36ddfcef4a58922f96cdb00a41929cc98dc77c52dc6c7e9d83b188a88
+1c8a16de3557f9bd5b23e03d5c9cd00f921a1e7f7c520b6c55205ab0dfc8caa7
+6f6d7aa1e2c4819e4d19af8bd07aebb63e716d0a4fefbd0c9600e5c5a9857ff8
+a637e2da191cc7bb8b1a09f4f4d1ebad7549b7b2d41d74cc991cb0ef99fc1bd7
+b3575dc3003914643d277f88131facc8b58b3bdcc55dcbc372de6e3104a92609
+09d9fe9c16e18ddb44a481438ca22330c7d6b7a4eedda79f01bf19c266ec672f
+99fd1f26d55faa7c485af35609a799918dce99225bc28a60256f5d393d488bc8
+660af7a2651391bfb765a8e7993a8701dec9254c983682fbcd136b4774ee2c41
+bb2e1c64a6371db80c433e8b0c14658ce0ae511bef8da3ab0d10d4aec7d9545b
+90881504302bcb086528834bee3a0c744092067de2205a7d773a5b1882d6094b
+9f32b36d94033259881aaf60c7ea2eb496ff2bf705aeb576a41170461fd95e2c
+0c4a65ba15357e7ef123f8a749b89583e037e1e9e2ea2d8f435bce1627cbe529
+7183f4634de4d321d6b63d939abd38085a1d1ecd916fceb8dccac6798b358449
+ae06c3f7dc948b9f32346a3f492db2b894605f0d33bf2ad98445afcb05b6be5c
+2f38338c78b40cc533b4d7dea2b9760f48a5822ce3776e87a8748f81c84ee7d3
+d43971f8d572904da8b6e1f2f20f95bb2c31a4febdf195973dbb2b8399883d9d
+7edd05e710d4161fa96e30244dea9c2dae7ed30e36058bf077f060067c3c6331
+b26d7e358cf3efbd5dd8820d0f383071fd7c8f29886e7fa098a9521eeb557af5
+d275f95ccd75f593e97aa2a1d7463ed69ca8b87e3ca8359861cdf24ff3a01955
+befe00289476891a2db20b0c808ccd590598c4486d00a53ae66acea05e921f91
+b6b8410bb4708bf580998f9e08df92798f3da75b1ba7e380d4e3a5efac7424a1
+83bd2adcfd45d84a0495cb2cf6f647ed38042d03ebc57cc7fb01c9b6eb809815
+aaff355c0048fd1456bb3b666d69b61e62e5f8764d8fb8cbe568ffab07b2b20a
+dd75e6e8afc57cdd6a368b35903177c16db14c14f861eb4318e158caa7615743
+c847e066be616524248056f1f1323cb75f976f3c45db703df818eb011499049a
+3795c81add7bdb60c942fc7dc78cc0644a6cbffee86744af3127d021b0db7cad
+29745fed8dcc7c814da5085ffb6537dc87b2b10c0d739ec72037060d77d03479
+80b7c1c66f33594dcb8fab64cb6a7595bf4321074e177b613d8ff0d6b3165734
+ef7ba807198398cbfa82b5c22268966bf1c6efcb763bda2771370fe353d5f74e
+39b97d0007c8f07cd4ec408d5fd8e0b91898da51c5995f1bf401201f3d990741
+a413a1873c0630b02461dbe7eefc315718b935819245a3e4ed6f7245205b0260
+2ba4e3fbd10e9e9a6d795b7e5faeeb4bb327f8e2bf6288afa17b33febc30da26
+339646fde4bc2052156bb105b6495ebb1f109772fc0cf4e74d04fc343e59455d
+f66e127b01af76306f1eefb66cdde33513c9cd8cfde5191a30a611fc740c7778
+a8ec6ed6ab7294afdc13c647b5beb50d055f1713d94f8f4c34198de9e7d69140
+4a6ac35aee04add4622ee052dc6e5ac069ca16f44699d6dd1135b167daef2640
+6354b17dd0065b0667167a32152e0a1bfabcc906d6a83f57322b6aaeb665739b
+0a129704c95439feae80813d4e016707e58501284f589bcfefb24f2d4f4a64f6
+8f498156b4b33739c6d709353b297803d5e2be86d22a8537ecd6f22eae4a9a08
+8675186ed6b6f04bf4ebf656f62c93b707b488177b56c671218c0ecfc7c331e5
+ebf4d7d19e24e3874722d2ec41850586bed01c51501cdafb58f62da3bacf1e5f
+fbe991ae47504f4cff536cd07c27ea98c7874d0332184e8e9ec489a8b88cf4da
+3aab2cbf5fce08fce1aac46f7be72bfd13b2e68247c6dbc126e94b6c223b65b8
+e10e023b083a73ba7116097cc30b32619e0337e0234231d125c615edab87a076
+11dedf515e1015422838455580e1ee2332f79b66e0b187045fc80b02a42b5864
+ca8f5ec65bcd6740cac58c1bef50462dab2b0148c1f38ece89fbdc8fa08aa8c2
+6bb7f7c2760f88e4b56e3ead341f6696544fc6735122c136ebab68afa8dadb4f
+54c6fc0166bf56990d9b28d0fa8d843f1c971029a46665c1e5b7bcd2b9d794cd
+ef19e52d4a154e0d537d14c4bf0feb4ee4f289cae0c86cc82fbac3b43102c28c
+b5f6a82948da4c51fcf85d95a91a5a60806815f04c7175f695e1af5270f961ca
+bf7ca7376887489e598d4014b2bcfb9f4a745a14f577ccb2f402be279fda4d02
+3f464004998682716083496d4112ed9ec7351d81bd4729511a96f001d119e714
+7641307abad87eef8cb42bb7ddd62f61eee2a3944a834039a5456d3324c53c4e
+7b44016242baa784eba34a2ca7556dedad425bdefc04f11aab77263b508ac9b4
+4f372af561266420f852b0f288c5a8d08cb682b47bcd0f8c738370c48ff9dc51
+17e8551a946b9b0d0b441e8353e15214cb669f5a696b50a23a4092b65d3fdec6
+f174fd8b2d51b22cefac51cd5fc7653b3fd367dbf0aad2f94172a50a1bc5c236
+103d08e2aca1b465cdbd6441a8b9f05b2e998d330ae453cb4d2ecebd069ea9ea
+10a0652dc518141e6bf674282bd3ac1aaf7361513a945163bbfd249effeba765
+38c41e708a6e765fe6b7fde08d192ae7e1e4a12e61ced834f873f89bd13934db
+828fe9925e412e687fd5b99ce4f0fadd5d20a2d5d2c083b98d7e85655df6dcb7
+f488d3bafc7e1cf88a8ddac3e33b3f38c1da0bca42f052f379bf663f7ddd047f
+227dc4461713cf88d27780480ef4154c1cda2c40ef944a86aa053375d9504107
+4149481e9f6d061b6cf70d533ea7f4df16075c1cb2d84fe52ea967b85adee81d
+976e3b5cc75fa41286cefd804fd3b471c74e973e3961346a3b6473390660492f
+1e602e7f27c38f282c7d398013f133812a6e8d2d20c45f8c31c8dd26e53ac22c
+5581ab149190010f6e3213f77da2662dbe1919d79a23aa976d23183074171102
+9dc5311c96a7dcd36bf4e60780ba68f44292b23c6301bfe39defe3f420306b6f
+c94a224e43638aaa1d3ddcaf6d4976dc33831d413f691ad7867feebf53ff31e9
+05a6f084f525873c6e1a7eaf956f8c1f9728f88303c6cb14b6f79fd730febac7
+1552d5a8e5f4b8e0bcb10efbf8a49ca6f470ef4ba8fc2b5b05d0e2077ff12202
+bf480f21c47ef74592b6f8a25ed1bdec3a87a3afca117fd40ce5fd5700cc0b62
+5f29828d7dc95812d142e1967b748961b343cb96408146a6aee63d8cb82e0984
+1073d3085e5db40b230814bf4a2fe816891d33003db3cd0a585c1dd306042423
+a212af55353f1e915436137cb6ef0823dc1f14746d0de7425e036cfb6722a890
+3e5260dec6a7dafd44c995c215d6a917075a3c8220884be62c0660b833b658a8
+74f9af8d280d67bcaa96429d4882427c81b38570c66d0e309628d8d7cc28c442
+2511210838a12cde39af4eca0423e5b040c52124b5c549774e207401ffe8a771
+08192fcbbfb5a127c24241c8d3e0bd8a70456ae850725fc1dc1b2d5c2d1acb49
+71fcdcd33e027a562c0ab202e73bb3e701d4f675fc4e301a710bfcef00da6ff0
+926112665593322dfac72485b8eddfaf4cc449154c53c4403eed5332f38c7498
+9851e9e056c6b8cf9fb7079b48420f04cd54cc12d505f13f111df1a51672447a
+71defa3128cbd7ccd50afa388170bc80162228c1ab8b8b98ab9bcfb0d49686f9
+2fef95de610905562e92665992fc6fc31c2a0e9ec7fc06bd22addc740e02bb02
+d08cff9cdd111cf41b91a7e4d8f38d88d8cbb7dcd4120fbb76f69a24a773f32e
+a1f63f906a92a16861db77523c55cfbfee0e96b00e1f93dd20d154343ee774e0
+1fdb1be91c43a7d690a557dcd5f75fc2bb0e3e18286dd8e1e23412fd61fd79ad
+602259e90d0e5a2894a29eda1c096289766160e356931015f1ccda0a56f3f2a9
+d9c2e495157514e1b03768d8190619e0bd113afcf91f67de3ec80a09281107b6
+b2af3e22a970ddc8a28c61140b102d990392c923a50e55971f6751287aef256e
+f1c4540af2b1d7754b16fec88e3ecaedb6b713ba39fe835b27cd7fc8c132a740
+46702a4b9737dc04b12310ba7f30ad94de90c4cc2c0e174c5a119d14ba6488a6
+a1134a68ec0ea009c0caef6c0b1edbfcac670162a1b3235757ad4dc4b0ef153e
+a4c396a809c8ad3945607752dc6ecd49348d9100c3029da91cdd15852d80cff2
+dddce3e7e2818294a4113ce0bb2e4d930b1069156c346c2eef80e8441084fa66
+7ed142e5f9414f02a362992ad52055fb82d627dd481405966836efb51f858f8b
+6104fe5d335427b528c2a83512fc056de5f0590945e84d9da75012e2382cc99d
+cd957c861f43582d982411c74d62c0ba1320d0e069cdf1ad193a4afdae8b0b3e
+8bc1179ed8b02e0acf0591edd39971dc36698a884f750694b57bcd28ffa1848b
+621f22b1a5c9b85498caa13e82aa9573695d3e1ddd2c408123494d46e671b50f
+5314e92a073fc52522f6492de090208493f1581017e05d7f8bcda9cd8cb62cf8
+2cfa57390a3e27ea3a15014a00c9b6189bc82996f2823b1f7b6c932ae9d44cd2
+351af96c918294e743f054f1cd71f1287e7d0dd8908e573c3113734c091cd343
+95647aea885cd9df1211028b13f99591f653f69df2bc629297ec1c273ff6c446
+a7d6bc6579118915705baad148ce2d8d0f032ab0ce4608d3f2c19ec255df6b86
+c88700d11dfc2953b60391c5c4eafab97fb6fc87c467f25db92c05ac2aec8ab9
+f4f624b08f92522739d920f7e07eb4d32807990a1f03dd323c5b5d2b4b5af128
+c18cf5270e0cfbb3744fbe8041d7ffebbaea8bbc7b851f6e34e7abe48cfa8b72
+00ba0034a95bec468ca2f49f7f34fb3a441936128fe1d842380aee63867b2ec7
+38963bd51781d14bb5971e156e6fb64b2ac5fd5fb3c1c1569a6081ba0f98d8c7
+7736a52aa2313dece258c0b9e2cc3c8547fcdf0ecaf7c704cb646eb9b00b0d29
+4ff444b03c4d041667206d44adf45890b1672eed8cd460c32b8cb37e21be14c6
+0852be32de9002618df604770ffadf5cfc05857966bccb01edd59906bbf4faca
+52a14c2e1f0857cf039a4a82474f03dafd54b8c163882ba77436820fec657082
+45bb02be9e7fcad3cc9ffbd119ab53982acb49e7c74fa8fcd92b2b0dee92eb87
+fc7f8bde3000b42656b338078c2ae8f58eb616fdf447c06ca70048b2f5ae25c5
+123655dd45b546277bd35b63a1308ca47cf4d8917c4bbde2b8a07855a0628c7e
+eb5bef472819729b3878168096a81bf7dcdd59df236fabfcb0c95c86ad80800f
+c2f69fc835544ea36403ff2cf0f3bc8127bc2eb5abacaee987b76e202c8de7d9
+954fdfb49cad1a5e9dd6e5b943d9b3477acb63cc884168cdda0961b483c20b73
+2f7012e980d1c6a353134e2ab65b8f93b6c6929ba78287c215daf08f480b786c
+45b5ee6df0b2b030fa7a27e39268c1526759aa0e095606d2de48f0e7ead25596
+09eae59437b3621716a4892a241d8fa957bb65db783ce120cbd1e6bd83b8a26f
+579f062654e17f8e35159212f7b1b7788934279c0315b9a603b3008cfb1393d0
+edd53889e22df7108a6cb6e2732f9edb9ea792e00469f192a288ca6fe995c7f9
+e3b321efa32329d3b1f0d467e49aa584000810c5b547149295a7676ff1e4cd89
+d8d6da7e117e8cfd0877e8d7ad0869b15e69da45fe038ba3d62ae756bec8802e
+e27ead377a333d62377718b523658dce2f8669a67a70585fc50549b52ec5289d
+22dd47a8892152cde34423e671a3820089f4f3b4007759dd686bbce52c7c9fde
+61585a41e1e74d33d72e052708b3163d62ca2e89fd981b595b3840c94afc7b5a
+3148be452208a894dab2becf9820d8e7769ec43d9635aa867edc4453e7d03b4d
+f0e4b89b4b6a3297cfbcdb97b158e46503f38c9543c49d81a41665228c07c28c
+33965bb4fbdf8fe213bba119605114bf07c4de83aca1eeaddea3345caec6ef41
+ea7bc93bab21a2948eb3808f467090058fd08f00510ff600658fa971d1f66e54
+7ad1965a174786e4d92e059fb6769e965b1147f4048648fe82ec51b13bc56c71
+229b19829abcc1ffa85a051c413245bacfe48d720fbf8d4496c05d7b5f45572e
+6190fdc37301f6cbdc44031c1e97cf47f9c5b9ebec8f79c8bf8157c5fd861ca6
+4a5d258d12a8e48f7c9ed901fdc289b53641d5cd5e63310a8be495b1104a96ea
+664da883db4d8b8dd8ab1dfcdc10d771d37562edaa793cc9019389d060709efd
+18d78bcb6d3719a0ee0a5316f9f40fa503d9707a6d6abd1cced61dbfccb06be6
+714985a9ca63809cbba77857712c2381c569207cf9db2699dc0aa6b7f5565043
+816bdeb09e29be611cc336c31e84302245c9ed37231a7b92d711742c640d06ce
+e484024b5335dbd0387548b2b71655e83cad37abc1496e1cf9d8fe792fde96a3
+bb684ca9fed1bd75d9bf50b06d58bc2929de047f96f003302fa2872ad8be3e0b
+68873f143abdfa4a73702f481ec4e32018581f19ff862c93fdba310f5914a242
+30a24df9f56ee75d274c9d5cdeeecd61618c14a3c2d36ae5167b04312e7a7797
+0267e2df6837256f0cad7af1b5e29386fb384a8ec2a3a116e026bd2e48fdbc5a
+90d503e2f3c5919c715724b234b71349ccc9dbee51808884d39a8b1ffa448488
+504b67e245eb403221348735723197708a564298581b5d75384b13ba92a7e25a
+5c40792e86d3401f9fab4a41762d95982365f6408f9683877402ce4e98e4a00c
+c3178e104c0857719ac4cfd53d07c219eb60e66d463708e3ed65cb4280285c2e
+723673836995e18aef5a9a503d7e89aa2ce4e17c33a1214df21ef551d68c3066
+64583dfd9664e09acecee772d6a70a23f3840ecdf6a671dd7870e1fa9f261e79
+6048168a73a20c53a971656d47a0ecdf7e7c5cad76073335ccdc14477746c675
+e2628951ee83c402f7a0b581af8ee6a594923a375d18c98a2dd01d3d5eb8f71e
+657bd49e5b0c4f4cc0a62ffbe13f10cbca8bc2b631d21b43827a5c3ee7548ed2
+baee96c94af70cadccd81dd3a48c7190b0630a0c7ce48249b91d92a1bfd06ba3
+123f97326e31c195fd1771fe71abf66fe3fcea8b285d6a1d4cb55b9de4699b0e
+c55bf2f5d6c245f41cd933a4ce3eda258cb90e401c6075a02757e36717c8f547
+4b4e9f3a8c2d172b5d380dbb9e5cf8dbbd8b2d8cfd014a5011b9069cc00b7dba
+21b4d17f44a55ef1e5c49ae01d91ab509c3313f2b0a8461194bb89fec131a396
+4e58fceb5677baa816bfb9e15a5121b3f0649562f920ddc302a949268741a677
+4e8c2fcd11288f6705250232d4de4caa56a8e8219dcb805ed08722a674d94491
+ee9f530d2eb8e07173f86912f0e585e0051cdb190e4d0040f9da037f29890068
+4e4d5577f67a9359e6f86b690ed5e6d3e772f7817edc2867dcd6d11dbd59b9ed
+904a57f2c5dbaedc298dc4f2bffd42a93e77925e00b4d74460c68196f0326f39
+f1ba9059cdee88c8a5377b623fe791c7eaaaf6ee2454ec3f681bf42600a97acf
+bba69df07157f26b64a642849a3da66f0e458fa885255699024771c0e448e620
+c3a2607b32cedb6e0b72a5a5dddb10981c3cefe08ed3921c4d0ff87c18cea04c
+16bde8148e530979c140973c68ae56da99048448c6041f1e15d162163c46f7f0
+5f9bd3ec2c7b2012d6f3c29eb76abfb56641774cf6b6bf17ba63b7dc94efd7f0
+c5412d1448ae3a093cce169908469e6ce06971d89a547f25d5479fbce1b89278
+2440339e904500a9446ed22e000a9213858c097896a83f008b180d942e397804
+a669da5d731b2b29c3af90d009e4d5fc9885e9a446a8e4c7d1e4f2e64629d829
+444da50342173a10deee11630aa33d865c532f39cad6326674ec49a348a02da9
+966cf4f77692ec51253f5dbc8650e28992673c97a8bb57ae2b2a1331644d3de2
+0236f8cf1c3f071fa6bb2bbda641410c39585a57895988e5d653daa8558d797a
+87d8ad98fdd07477a82c09076c6d5b8c2b6da0c3f4b916149bfae6ab3e47b977
+9c00f9e01cefe0e56a3dc2820a78bac7ce577e4e3e9b30724ca1ae980bf5320a
+41f38e2fea835dd1b7f8f252ec7052793b34314e0faa27ab4edc58ce25feaef4
+011cda00461160b772ac3dbb072cb3bb02b27f33ac2b9873a8131954a4abcffe
+1ff1b3acbb237877ef0faebe4ba87e886a217d62a86fff64119cb68a54ea16e4
+6042eb0ac3194be279f6662a3bc1f6d46aad795274df8c5810832a8a7b442a4b
+acdef42384bd3c25765321fa46f000e330d01a4d63cd71f11bf2cccecbc0916d
+3a78816f519b6ab5e58cae3543236eea83678c61c3bf8da0db91f4f9553565a1
+ef7cada3a2eaf42bdad0fb7702dbf84ccaee6e01b97b2acec60a47cf19f42871
+c4255eae175c0265ce822ba301bbf495104a995014cd9456e73d2ec34d417402
+38c08c6c296cca1ca3b6591e3bb34ed9aa0061b1309444c29f544bdc8b22083e
+f8a1975c3b20d6d5ca9c351c26334eea4dd8b8a94f33e6195c1a80d6e37bd2e8
+4122ec755d4c52614af9dd42d9741e365b019654bfadfc69a90e2ef9b84957fc
+6d546f6d8d8f75f7dcf53ac4f597cb302ff298fd67a72ec5fd2d2e480e239060
+d6c242f3101f449fb1c394a0e5aab4e2c045506d3839f811d173c0564fcd1657
+f668ba835451a95dee465ae2ea790afe0e9538a6da482e3c9dfd134366ebe8ef
+d446d7daa9c05da01815985a2a5f2c3ec1f0a7a929717d234e3e2eb26a5552dc
+3577e7aa7bb1ca1ca6c3b36503d08f079892a2e797a54def5681eee1f9a3577c
+40e7ca2fc5dea406e54890bab23732e6384a647926d8e072de0bd636f1c75e51
+952c8aa250389079d6b04490e372b4218613ab658ac1c77ea9a1ab429277b778
+6ea8dde78ffb71bc0bc4dfd9ff7e1a6b2ef19a12276f3ce7d4a5b281401af046
+f8b6c6548bda69192ca355cf43bf1175b033f01e1bf61ef403053a79288671ba
+85dfda331ad1fdcd203e875047c740c42bb12304c4a18a407bc99a2c13a87c60
+645b158b0513a90049845a03cf574e1496acd0f2fb0f914e011b103533e61aa0
+1ac25bcfadd456f33b64000440c1e935fc1681e9fb6685786b72c909055e4f40
+b70afc227ac5f2dc1e3e58363547223cd835d369351f36cf698120403c53e4e5
+a16b756600bf0a37d1965a2ea457df75e07f3021023072b12aa7f3d9e8dd010b
+2dfa92b24d7375b7ba1290060b5b5c9f1537dd0969cf9a7701fda9f9a96359c6
+ccc15f736f5d2479824e8488f6aba11b3d25187a9337effa41ae611da4e35763
+f60ab26fc896697bddca64b70300f09a104d041ea41c4dff6f33861b8ce4f45d
+98fe08b1626cd9e60a4453597e2fc3287d9790296da5087c25ae7b85be898583
+ba29d25a59c776bc93fef8f3059e77827449056c2e55a4772385354bfdd19284
+7a02b8d0d5ab6b34ad9968d68c1641a4ccb2c03e5253f1aac444c1afcca8c94b
+934806c61442f810a7c53f06db5b23d0b017a72afb5be70287eedd1e0289ad87
+43965dae9770a18faa7944cad4c638eb8d918558aec3a0a695765172d5c834c2
+13e680fc79d9566fc3b47601c706c2609892dc9d1650465703ee6c2f7eeadf92
+809cd4831e47a0188c633102019b6425cb76403f967af48976a2737504060649
+954c118f616bae1de9cb94ed75f6a3dff1ce4954dd298ce8b5007d1e182a7e6c
+e0c6873f582d21cbc2f0638d4a7b2679ae58bb54c7f8a1bc1c3e9eafbbe909c6
+40ac2bba74aeed432242720e473163091cf6fbfec4d44095434600789d7d795b
+e5ae5500584e8e637daf9a7ac458532f3e44320be7ce7aa1f106b6420e67a96f
+459c23b7b02404c1b7620742766142545551de23fa8ca655c1579fb2c0a10e93
+1c220e584e9fc86eca3aab882f3881c530ff92dae83340193d5aa93bc857671d
+ca3fb1f03534ef6ab4cab64e29c02a5c37e19d1274618211f2e7493a4a7045b5
+3d39adcc8de68bc7f2265914218d2d69abd7c49bc9237535b4d364cd67bbbe35
+f13f71b88e83e3bd6f09caf327c629cbc2b8ee728daa1626795c2995c7919a2a
+e966108bc76de252e4df625e95338332b6aba850f0fe97fdf247e85db1fa4a8f
+3d307543c80426061ff86b4ef8d26997baaed5679cf430bc88ce20e31cf510e6
+987be01e58a170bfc8a6259f113cdd3541fac41467a862acb3de36fb60f72de3
+e0cb8a02e17ea8cd2eea27aba315fc34795ab06dcabf5ffd1b83e2308b2a21c7
+72cb356f98a7d2d0f0cfcf69d672aaf7fbe6198344aadd2ec28f593c7332b0f6
+1809ef49d7c19bcb6c065ec2d890d4c8a172e061f47ef2cc392e974d1874278e
+250453cfbbe55c77a0715475a7cb383a287cb83f5a14fd027c332f79a09cfcfb
+2c7546c8aa601d925ef6b72a2f1ac1df26505bcc8229a4aa0c7167b9d7c56508
+901efdc77754cdc396482d17a6e9fcd200a0cf35f67948d11beec6d38794a826
+7c48da337fd191705e26eed2981df5ac1e8f4bb23952b9eb6f92cef4139e1884
+bff9f37257cced07790b6ec333d66a6800d8c8befb1d6362786b3ed00ccd233d
+80e8c4fbec37dc7dcde3e5ff2b8a81596dd10b714d9697419ecc542a661be66e
+a5082e50ab273dbae50d60ba68273be1adc6f750ba2115af58ce22e8e10443e7
+2048d5b248294cf81e9e1995dffa731795817c556133b714d4970b379b310698
+d4f690f2d799153b8890e74dfb72a5a50c476be90cd65157b82fe6b42c5f1be4
+3d5cb0c00ccc62f270ba879311467dcfe9fb62aec22c5d4872f6b02b8a8442d9
+d309722c4d30047232cd0c97049695a2cd80478ef3845f6d1005f36730c41429
+0576753e869c83d458f0dc3f42f68264e020270889c63218df8e1a8ee73488f6
+49ff10926847ac73be9e89879157cf78af203a66ac491eb93ba365b8891dbb8a
+bf8a4403b36e0334beea6749cc202ab18dcd07028f2f6b29b0eb5865a3748aaa
+c60575212f532b4cd4c9dfdf00aedf6cfe6703ec89b63950a1011785f6dc9c78
+d09280ba67eb5a78f9bfb797add8d7b705ac5ac61a16fbdedfc1ae720f9a4ca3
+4cd9828c287b680c24f11bad00b644beb9f6196dbaec8ba231de2d065bd7f42c
+d731e09725adc7213bb1f5e675bc11aadaa56da37f62ede33a390d43f42a795a
+0a5df034adca9ecd0e3132453aba99fdf4ae8cc2a7600e4aa10db1db113674f5
+10b2427b1dcb424623a259cca49ed14c97f0613edeb93f459fe9b9cb75b4f308
+15f5c65f839208a6f25652c8f63196ebc83c081c4739a692bf14ee8ee54ec4ce
+72a35bf84d5f4e6c0015a4443e7f799b7bc6828e30a97687a0e5b8b722fc69ab
+5baa477d07bbe2ef09a4fb7f05db2e02923bc8582f27693a5ce2b473ca02a815
+9c0789ef683ad4c7d8708dd28c408a21e358fa2c30cc598bc36c5651917da2e0
+593e472b57d70c0d6854fa5422e8b3825a9e39b0a54b0634190c2aebff4913a9
+7534bc70e3e9b3458f272d24087e74a538764b43168e74108da00351ea558e3c
+34bc8bdac1e1bc9061a688723bd54f0bd4ed7c165f32dec016b056b7cc566a4b
+1c873e36ffdb7e29fb088782ce60524e2d75cacd23fc274e307dd1d2b07558a2
+7f64c4742eb3ba451c8ef3b6591d193d4a55a42fddc17f12ec3aaf51de8056d2
+80f09997bb31a07d6112a855dbf7062218dbe89a6844a006f23899f98624ff6c
+6c364bf079c4b3c2f92cd79534f89925cfc55016218192a98cdf6cf4fe1b5fc6
+9540ea49643891db45a7a71cbb85f41035f507dbc6b161f7f1eada70e7905aea
+0b1983f7909d5575e3e7e8095bf0f4978217b23f329f057f1152ffc14b9bad8e
+cd6fff8598542a6adabc992ab3563f424d108d95abffe9977f09630ef4c6ba01
+90828d34098bb40bd230685153e7282671478138e578f608873d64b00eaf1339
+6dbfa02de3fa983735d90fe2d0cf7d28bd9f35a475a6f55017124a58988d00c8
+713745c7c999ea746bc969d31aa62eb0d722cab1e57cb0ee97cb969d1c3c1b41
+cced2d7ad87df16215f908e02a358b725eb5b234fd620f7f54d70f764eafe8a5
+de65e3e171c885eb012b3db1707c76724b62bb0c1a51be0b67179f96a46ea538
+17bd480e774c31fa131141e017ecd4c1864f62636127a4c7e9bdd05679462314
+042f0858441e3d261ded65833d91d80a6ce303de41060312e60527a3dd45404f
+55c93a9535c3a393f1748609b002be9511d87af20c7bb2c4cfffe466df21b100
+e20172c8215855aec00e3680c7efc58128a95820085cbe598b9603c70638e128
+bf0c2ab7113eb95b62fd40fef845b1eb5414737cd94ac65970a4d6dc2726d2ed
+2ba7567155154ad30301ee997a3bd52738dd17030860d3a479426c652cdc22b6
+2c70f185d812543f836c7d0253ebf8788cd6148ffc163699f72a85bd2600cd2a
+72104cfae46c836939c5a8a6584a58022d0ffbbc1d1f035d959fe58013611cd5
+be08db068630e5a4c5fe915e20e5652bcdcc44f0ce93221ae09c55fd565c2523
+79189a85ccb93f2d6162402f2209df604f140799d7e0b34639149eb5e54c33c4
+05389d46c775ef19519f1ad4a6f63a62435c090d8e3bdcd6423f5b7dab32d263
+165c93dbfb1af542af08aa5ea25595b3d8ef053e19a16e5a1b13f437fcd2586e
+4534aafbbfe1e76906e832c6d1d47a3c5c868e65afa7af2e21e54ce233170b0d
+e44bf290c512ff3acc84f0e492c73a0d827d701bc9766abc4c08d12fee986020
+d06762d4a0163a976031bb13e7e86fdd3101b8d5155046ad9d82fc26496661d8
+8cc516350229461d6c11114c35a0d13a1c6ffcb96fdd56105d67bc5666512de9
+6aa15713998ad53064b7023f64dcbec8ecfee625ef76f5a024c92330585cd17f
+1c3f82bbcb509d4d31b535cd5853d803ab7cf1862e8a87cc3d6557aeb7567ac7
+9ee04bfc8a445169afdd1b1375274fbb78ce431ffb9c6fd61a99a0bf1b41c19e
+1ccd5dff38a45647d51fb014099c74ebb11c5a07050078507d45a1ff1fa2e178
+567f745448157bad850f1fdcf9a37afc8968120892ebc0f85f5dfce9116a7108
+b5cc34c5d85a91a65d18b9093fd27e9778ce26bd93644e4c20ffad131503b509
+850f4fb260edb6d8cd0bdcca4beb8a25ec1fa44eb5b31e9213d11d0a42d1aac0
+650e1ad3880d59d2256a2720081fa2a7482e3bf1e4f4afa59ec16b810c0c1cf3
+2cfc726205b285ad7cb21d7a59fbe526a844a5d75ab5d61a801701c2984b48f4
+ff020607f2ce9b468df2f74e04812262943d8932d2e3c0478afd42a8a04eb7eb
+44f9dd70b1da28153f4f366df0fd25e89f942aef374fb9bd31d9575803c03e57
+50e5e75975f108b6c7bb770d824e5eb88d67dac4526d6e4df1c6fca0a5fce033
+84b06624e906912a26412b36cb177fa2dd18279591a0db289d18b1754461b97d
+076631b5d598cb2741652bcbcb9804e1d01a8502b03739981b42e4754e21287f
+ae2adecc9ab60a3ba753bf94d1b586c0f689309cddf12131eb442180c1c25b75
+05bae86b985d6975d1048d3da9ccac02bac3e6d8a2748082e252167809ec7b81
+7cd72fd364f27f9aa276cd5e70edf56cd5a0b9cd00b815411c766b67dfa424a5
+1d9a9c7e27cf57e513965379e180f1bb94b174bf827dcbdc89ffef77f5e2ffe3
+9f1dafdb747f003723dfdfc91af3c6c23619237e4f661de027809a510242ea85
+d9b93733b9276e37c01da481bf86fd040b2a21df21b3aebd8cb974925ac339cc
+110b604531af29325b8d844305196e5e1366edbd5e6196ddfcfafb27e28b1adb
+6c2a3fd16d904a1ffb4a998a4614cacc85157d8463f7cda5363f5f03b9141bbf
+4788f8e92de9680f3fdea66415c9ce8295c8a1276fbb9b57e97f899caa8ccd60
+275e20ef7c0ea888cd60bae018ab8d59d4b9b0e9a19d71cdaa96392893a7284f
+6d94291130edefb156dd9a7d172c89b881aa5dd8d1a4d9177640bcc8d308a801
+547a4fe7b75f04f99d529c854dc059641c7652132291dcd0deca5ae1044880d0
+4a82600f293dbce78d2dd503a4b20ea39f25633c216021b78cdeedf51fd73c9f
+ff3b4e7b637e90de3838f8aa7e1703cbb6b5c74e1f0ef474c9ecfedd79d0b8a6
+1559407b87435fad2ca5ec36b170391fea7cde5824286f66bb0f2b14431d2481
+0fe6b2a6905c2ef0908bdd4b05a16c01c00b2eae7bf294251633e806ba08d21c
+3e1823e8c0e9b6ff171c19f2d4b27f070389606d90ddbe0303133f565644c8ec
+d9dac4daeeeb490b2dd2ffecc57d69daf3fd4ac8666adc318c245e14f5e67d44
+d8941a967696da623d22c077d7484bb1c9029bcf536aa060626df3d5593bd4b7
+af56296934d03d03c682426f00542ad5d3f9f8631e97eba46416979b6b2fd5f4
+4f19b0cc18d28d551fec3b3fc5440f3dd5a0d79fff8df917dd515f68eea2ed9b
+73fe68e395c7fa2cfcbbffbb4a7cef13b875b96f5e805dd45a7c8d3e2eaae5a1
+34e00e26988281b159cd8f9d31a2f15246efd8d927e881a173487df644cc7656
+b847682c04542f35de33a7d638b5a2187a74f036ac693e4ad95142e8fd852899
+b8ffd2405f5d79af6fa919f2613c2f6f4fb729f5d6c3fc8ae921eb23a0827667
+11432b8e7ea4c6c823b57acb073fc00ae93223a0f893bb8e3ddcde28de69cec6
+5c0ab65d48799ab8d1c269ce7ce346d40256992947b609cf9d3da1d758ceb593
+1d6f05acddafc290e2b59a93bd94507be8a4d4d675dccca80891ee13846be24b
+8cc4f7052c5de700c953319b29aa4b87ac3c85377ad31a14584ed6970928e850
+5819d527dfdb85e330a5cd47a60932ac93b359bc0921ca40703f8abea6ad6d63
+83f8d2660f8c8571c5a13d889437d12d6a41552d9c4afcdf2fbc2bde8055d681
+3b7be320d48247a34cbe1b84791d2c8e9c417777bab9869cafea2c522c59b12f
+23cd59779cb58f047e0b81fe4d42d1f4433dae0ad691b9c26e8f82b5ab867ea6
+fcb2d7f2c0718610130ecb7ce93d1ec2eddd2cedf1458412dfed5f7f067c473e
+aa0235862bac6f8c8408c0f61d978312fc1a671c292620bc863537d286a60a22
+03a2d5dbbb772b365b925b9a616ec398359425e3b935c7e833e572ca8ecddf98
+121a7bbbef3f8e262d4c08cab12d97f15776e7e202f493e2e12b87f2e421e5b7
+6e3db0ebae2f1e62239072e98391135f5ce4141cea6d7e7a19ec1236b81d1ea8
+0fde0b0f2fb4aad1fdde1a004ac235c896a8a5cb15a4806401727e7dbd148ec5
+8bbdadd754e31592c37a7207a82989e5405a7ac58067f07932acadb77de65c56
+92caff85e5c183181cfde88b045e596cfbca6b4e0d37e6b881bd3f081ccc4a04
+a6d576b41e277b1eccff69feeb5253bbd9a8e11b554e496a16f714ad3bd963f6
+b8425f07ca5de122de2717d09917f19ec3238fe557c44782caca0e4e52f38b5f
+7c207b180b498a8fa04e66f6abee7d7e79fae9e54cf566803a885db7488506e3
+8727b5bd772198624051c64da3945da5e904788ec969dbb890f264e54e2c661a
+9574347d34634550d5b1a1355f35140adbc07be4d5ca5c791cc8cc01c4b26875
+26d8937cfea55684de8165b792f7740044e50ba4c19677a30bd453419a7b8d46
+437f44d206dd8a871269c903d78bfaa35c4d1a054d0935afc44f048da40cdfdb
+5c15cfe9c8f19cf87cb93a6594cd394c7f64fe21cb599985a69836ddbaf80410
+4848eb7252b523563b1c24e57d4966d1eb261e4c9c5ecdb8547e936506175440
+381020593bd8cbce659a7a345a4c05a9790df98ef2bddc9fe45f6968c49373d2
+d55cbdb4de22c4f7d53f8263843f8690baa842f6f1bc7cc1545f3e0a169633a4
+01e5656f08a48b17a69d7babea61d476ec62fe1395d1c01988926fb0e69326da
+c69b5e5347167c0541f226d4d2d8ed2b4828ba74f3939a4b77dea31d881c7b70
+18e1c1b646c10a935807e6d3a9e6674aee575023f941a098d2df22af754b5296
+360777bd1a5b1a8fbb29ec9ad201dbe0170850ecde219ad290af01017f26b777
+56c14a5d3ebe7ab8560ddf0140924131cb0a693433437d4a17f33bb22e92503a
+337de079b27ceea348d9218278870e1404f7733200e31de980e38ffc55268921
+200bb4a6bf116ffc94e5d68015c7b0957254589bbcfd6b0d7318e1983d18bb22
+982e598484b2c7c7ca5b08482cc0f23f74d1ca4baa5bc61d1fd9094b860edffc
+44c5bba1d314b46d6c45a5a1ad97706893733a9eaab22bec79f4437a5917beed
+f24271040da4386867c830621ffd6744ddfb078caf5cf59e7e7fa456eac5abba
+dfea921d226d36cc6d03c73716badab60c4021630643824a81ae62a731d7d647
+69749ec4f34068330b11d2f9664f56ab8b8a91864a3a2d2680717a6f7ebf8785
+a18fababe5abf9b1e759573dad33c4a968505e887c7b0f9bdf65db75bc04c8e3
+f397740b94b07dd154a5523b19b87879f8f0466d8fc9f3acfee32fba47661417
+a6a97aa66eb83ab06a8347a76890bc83e6bbb6de46f007e7ddf4c316e904244c
+214382939b4507a69a29fa3a60ad96e420d6873f0f28c8283fe85c59f9a5ec11
+0001740aad1de7f7bb72ab2b699492b5b6855c1bb78562caf497ef78ce8ac3fe
+b5f05639785318ce1b7e9ece0f28d560dbda6d134ba8222d94fc98e1910eabb5
+b8ac30c4d0f2574f2e83f309293c3530ffa08d140c4b568e1868c7b5b3edc79d
+716a5597bc45616e2f11b4caa3e248fb96d9e84afc205280fcd76842b913fdb0
+d22a1c741274af30fe94c29a2c690e4e32292220e58e31e815000aadd624fee2
+4d72625d57ebd75a24ead23e75889bf3fb35738516b3b77a6fddb6590ce59804
+6bc39e4e9ae32ecdccb0bf9490ce9c8cd23e997a63bbef088fdedfd9ea40f21c
+4d89f264329c674689a8536d83e395b5c17e6714cb48fc72a4879e525a954048
+595ef80d4b03d92f12664b841e00220e3a2c7abd65457f95d1d6b53ac96e545c
+25b8ac77d8549bf343fbd27700f1d5b344e965aed917c82211f723410c8260e5
+ec603111b03673922f118f1b41f1b436dc347909355467db3bf0dee4a7844c27
+5b15c9ffcdcf334ccde89fb61c58e42848582bd44e2833619b0ad907fdfaa8b6
+97a09f114e9e204db7e2319a41a5866eccc8665ef6d94a9abaf54a71760bcae3
+f253f7079fde5e138dfa210368707b963f0c4ad772d30e1b56b969d911192482
+e1c3484f52bcc8751b4c4a20430316d90adb8114ac02e8fa845dc66978cdf313
+4f1c58be8a9c2990a1dbf0c1151996d1eed8dbc6f35d4a0a5f44c22515575d32
+6772a70d00dd938360ef9940beec9ac9a9aa2143cc02c3c030fc6288ae783bf9
+e6c6a04cc0549e2e0108807b31acc0def31458a8b32096d5ec0ea1e9871db2b6
+35dba89e4b0cb6b965c48be61d891a0faeedc6ce4ff743bf826f524d4f313414
+9368b2da65c9c14546084d955e937ac3ebeb5823d90670e2480b29402d0db8c4
+31e12164912a5214b439d8546711a39648e652d68cddb4c4e28aec95cacdf5d5
+73a7141e0963422cd82cb1d4568ff99cd8cfc938614f13a5948add5d8914dc57
+ac6b916f4fc5fa6b0a42f35e7c2120b623c61a6b95e35ee2c6040654623d2e78
+f2009bc55a897e0373ec35427787370450bf03ee788482396b09dbe8e2ca8d19
+dbbd3d71115978044e3568b63fe15d56cc5b08767f5a72861b85262e8b194db3
+5e2bd58275c19de4a46f45b3ac0c297a9e1d8238cacdad6b8728fc1c3c7c980e
+49e05424f7cb02991dafd70e277388591e3e34923415c383d62cb366dec6f2f5
+da863baefbece4000fc620227cd325071aef5e62dc3e4bfdbd4915c03e686d17
+371c75ae25ac8cbc6683a378f9f617d19725057d84531c89cb0bb686bf9e66e4
+844fc8d6ac46b4c309cc74a1a948bffdd1d5d663672ccd358c9ed2e21e72767c
+16a968605b3c403d99b8bc2bd8d9fc41a1f30a692a1302943db69e5b9c69076c
+51233bd6963cfa363b280b8499331c172abef2000c09ef43f04db75fd5502a77
+b0c1027c3fb29456f60b56d7e9983e8574b1e75032b8605a27a713be6e0106f5
+fc61ff596c600275a27a13140056fca17426d235e71cb6c4eaede2cac9529d0e
+e0bbe303ef02bd6e4cf0446fc21d90079f91eab4484a9251bd956483113c5eab
+36e7b8c94cd0dcd6c5c82a082d335665b37effc612bcb5793e7db4a831b51ba3
+2b44f1f9a10027a92cd92634c93167e88b0564498af080eb0e1f92bac9672986
+ceda766c4d674cf74a1124f9aba60cf8074b493bb28cc379485fc6d11f050dd0
+80a6fb444933008df47b50a0214bc210090f9a0cadff94f254e58af23ed76340
+34eba92a70693c7288a79477d7e9a6afbe342867ccbf6b44ee7c9adebde42793
+e4a59ef5953d031f33fe4f12652f26a74b958fe4ced8e6025289027340bb27aa
+946ae6445d6c3f79147c67393cf739061c2417cf81c75f85be80c99c8f37331a
+e6526df9d46a11c3c1fd15886f07ca08112fe60ee83cea46016d15f68e64bb48
+167cfce032d138f6c124ac0281f297439995583cb9825547be0017918dde64c0
+1d78776bee428f7534f80a3bac7005aaeb2ab9abc0b534be7563cb582d298390
+f47c2e5a6f0376e7312d6d0093bf017f64f6697b18b9ef831b2a29ac630f80dd
+1d6cee9eba526f2eab63cd1a53d842f52e58df2815f07f166e7960a410726b4e
+b4437978696e134d4cbc80a465744b872bbf340155d9789194a06e891d1a1f88
+7a74da8de0065d6bfe492d1326910624cc8f50405bd9510c27dab17e281b462f
+7decb40932108ec5ad173e146a3258d88c43c9f30be963eefc7bfe130beaafd4
+98e339cafccb138f954576ad1a38c0f149aa852cb3f0354fde3c7949de59dcbd
+a42a73f39348a8fa776c27b5e23dcb6d90adf52289f3c84d49c185fd5394f712
+0d2c35f112a99197484795467b3215ecc0d51a807fc77d76264c305cb61377b4
+97a0f1dbaf69c35529584a46de46f516fd4d1a8172360bb1e1d0d0c44e158511
+87b471d543e4884c6ae7cb6f5618cc584d1e978f744b3bee0269bbae4dd4718c
+ba02513e332a6170d0ea4015dc74aec1ff63a35b13b2bb91233026504a8a328f
+ac1e11c82fe34d6d24bd5753caf5f0d8b83c945add3e7b36cd0aed086bcd0e47
+3996dd6f821236d36324e3d387ca7bf1707e7de378286a5065e050dcbfad91d3
+5c82b436b4fdf3736b422c00c6728583539527b554ef64b1c2e6a9c1d17e35b1
+3c58226254ac2a47dd1223193b4e1649095013734c40aeaffbd7160679f8e801
+f18376260ee6e393deec9e0693b4e6b728e35de36b1f40a6683f7e34e6e086cc
+9805432b4ccda429b0136dcb4115c56cb01fc85f333204e534fa17558f7fe48c
+ad9a89128ca6c821b01864bbb67062d5286283ebc1c559e5668e11f469bc7e33
+ef69003732d1bd8f0b3f153bc22e8b99a3ca9669ebc50526461fe3baca5cfa6b
+5f7962fbc8cf0372cc716062f29dc7728499c9861424ee92b094227303046fbf
+54aa62766e8c9eec066901a8a54f395beadf01af8620298cff08f20a455416e2
+2247b637c6597a272f7b0a28d616a82e234e49b0ada3999073e77b58f11fd0aa
+ef941d49f73e962e9ae57574d57fc46a6b3ba2547dfc9e9667c08ebc05c98fbf
+02e2e43bffc8a244136206f481f0d584fba9db1ba41ef8a2e4c10227633aadb0
+846c79c5d7049648f50528b5ad34e449540418ae28b53c3e34ae0d8867ac3902
+9746aca8629d847851a6a13b21e2cd95acac4abca1ccbf5f955e6749656314d4
+5bfa890e77a9fcee0d7d67b9a34cb2a140954678555ee64f95aecb843d32b4a2
+281ea103839fd3860ac5de0de726d02341bbec556d8626fe39e3df189c3269a6
+b73d27881d4bc81c125a1cc26b0c6f28811d4f74bc8a757785053162f64ec819
+4b9bb8300b7184699ac7edaeb491f1f7bf236ce695a5ff56bf5c1f4390fb3c68
+68fbe2d542005635927e401e338b6bf6bfc12702ff8fc951a22fdab1d720faab
+e66f16d85889d6a15586b2f53edbc5f907179169c2a8a22e747a12d1bba358e0
+b0fc014a1c42990e01d76268d311e05e4ce5948c42bfba4ce63f0b86e0cfde4b
+4be316bb2cebd0e73e26f446ca8478bebc132a064ed117f1a02affff29dafafa
+c4ecd8e8e32bf8128a62aae5e7f2d84e6b63435cf6d2bfdb918a5d28ef147547
+f9e569b09b50dbee423c1a7ee40355a1cbece709a13273cba0da2e3825c8a970
+8037c94de0e67d5ada84c0bfbf5b96c0741fa043868c402079df2ffe8b8eb03e
+2492ea5a6338a5deb290315e26eaf43086655014ba9ffe48f4d19e172f9bedd1
+218e1164730a81bbc1391615488c5b66d2f4c66bf1bfb54e226df3893a4b5a48
+2f540f19560d9f7d3b32757582782aeaf607567e8f4f0a0a30f976dca0ba5206
+02488b0b7f41861a28247eaa84af240a91ee8a4292454f7c3c05ead6f814bcb7
+84b96a394e834cf5ef4dbef50ca3d287409b060f767758d95114a26b1613266b
+6debb528e85d77deb7f3bcb8a572ea917bf39aca584c4f841ca622e6e06ff8f9
+e8970dd1c9510c1618d01e1af0baec26c3882b6eb80a7e8f945ad9abd60911e2
+6bcb6aaa5f0ce78068400f9eea67fcaccc763770ccdcf7477f32dcc49abb5566
+4e75f91acaea2a9141436861ecbc358628590b2adc5ac76f1971818a49383737
+9fd6fe8d74e76a367abb6f7b624568fc7a4e2132513fdb1e66e06b9f81067e91
+3d6d1e5d95e052d569e75164830c1cad1bc168213da1cd06de144f4c714b0e42
+34ad2f8037115ec2c1a8fa611ee4094accd1ac585bf7235b1f07b6e2b14111a7
+409fe76c1abfaca8a35436eec605e4b43760883b31c6ef176342f834647de59b
+b54c16ffe522bb5d71f432a253b966587dbc5bdf85472472d9111b8a23c49285
+391771f0e037bcb24a5803b323a27f6f0aa96b46f1c23dd91cf899bb59b2beaf
+6ba551cacfed10663e3e7c6e7dd856ee8a3aef30b0dbbc22aecf3b3a8ee10fe9
+873892898ceab62300309a4cbe779873fb3bc7784a159e02ecb484f2016af7c6
+bff8798c7f00bdf847a6fe0e317465343b4c1585d03f83f2aa56904148de6d90
+65999bb74ec61877d74bb895b7dbb977d88e55fde157d4e0176416b03044e895
+655fcd0d98ea455342ce0e27a1c71c9178fbecd2eb6c35666763a6558d297fa9
+9983a24384848911542f0fb3956c7c6761b7e2b8588354eeaac4aa145203dbc6
+2d3b319538af1da0602ec260085ce3256b9a0ce6be0208920dc49eb2954ad6cc
+912846d093aa398ae77075faf578d0c86650318fd8bfb82341281507691449d2
+b498947c9ac0c46f7676e0ec94a1a7aeab00dafbe8b1a464bdc9b41137e0219e
+9eeb5a8c72795c7bc34600c7325efac4f2cbf55aa9b4a996c42dbbfc0329a694
+7f9dd261330856fbc0819043b357343eaa1d3fcf362fbd0af9a99ad57c5cc4fd
+a10dd10097941bec739ba978da465599d16b6be018bf7fa1ee3ffff04d35db7c
+a7f6acf148a663f999280b36e51bd77763d6a6ef6ff28950a8d4b04590d8b51a
+dcb555cc47d92b21ad032d31cd54ff911516f356127e38f2fa9108aff32d2e5b
+1f1452901c38cccfc8f23122f4680bc316785e012f61d2ddb468cfa55ac91998
+93a8ee273eec61e760ac5097167994cb9f52e62182fa0845b2fa6779c914047b
+41d1a35ff4c499474ac5c277aedc0ee957400fb6643a0437f7f6eace0f512ed9
+3a33686f28bbc8ec68aca9a2bdeb407e975a8524ec3c068bc0a63ac6779cd84e
+1d46c983fac8c89073669d02af734fbfddec35e195c1444c4ca678835a54b0f3
+f8a0aba3f3b5db7cefb364f491cd80110b6333f061613b5bcb834a3ceb20d95e
+8119457df0838105bc9998b41067b9ce9612825cdf10a56c7b3af464fc4bd133
+a9d9b14bbad06ac3598419ce3537fcb383ce9f650bcec756592bee0205e0ab71
+77b172fc8c15425359e43af5aa75959561e3b0e6ed08ded9da737f3181218d1a
+1be1044211e9d9fc86f23f48ea8dabeae2de9ee4ebca14a734dfffe643c55875
+e934dc377f170ef8fd4e83b1ea53026cf1343dfff18fdf667bfdcf21f82a5a3d
+d40d34e7c3090e76c1dfc01aa0194721d9a0a3783dda58edcdd65a497718a66f
+658a3c0dc104bc44b36a4372794702baadb0f7cb1594598414d5eaf0369405e8
+9726efdc14ce04bfea94bb1935c4d07587400eaa1d565714a33891f3e64c1d25
+fa527eea2ef14dad353c11733d6b0189dbf7c910601528d5bae1f98c01c6a347
+47b9c484e09e03e10e13b5be1d22ab7b8906c5e41a0b2ebacbf640e187468ce8
+af86b096de62d206bbc54492431b8728c55249aa887c01a82219e62e17b62eac
+82a7e09b484eca291295f575593607af6373ad85c692e7130a4b652ae5a706f1
+2457875d6098e477354eb52010424e6ecb1b69447261fd1b1f93e72de04e3f31
+7dd7313d7bcd5afcfff4e0f02f7e2dafe2f476810450fa2cf4daae1f85150f6b
+df4e5e7ca6533523dbe78054c7bdf73b04c4b562fc75fd6f6e2f9285410fb08c
+7bade37df9910ded1d21f0543b24ccf0c1c984a4d7811b8dab97ea56a14e9028
+999c9f71c8ee8fbcf78da616194678da5311ad63acafee762fc268fc2bf79881
+60f1a3a61ec3467956f15085c1808c8187ac5c5cc91b9165bbd978dd45d2c100
+de3fd8533581beb1864ad04e7dd74a016dd77520d2739a1d3e9524c8498bdf6a
+d7e9e0571c5108c7c7870dd240ca2a53e30353d7bf39bf075d3839681c1f8eb4
+b289720980a1d3ee6c5bc7349bfc37345b1ad0d770bb12819d72834cbe2670a2
+7129cb9f51973694153d80309e5a4f6f3a0476025074c15b091989d0976b5e15
+858a83818105b144f5097087f3219bcb26b446b033510a955c891a501983b25a
+d72cbfe682ad3b64c90853163504c4d2872a88ff8db26c3a62ac564d49eaae00
+c7f92cba1712464281b4100725792f62d16981ec1b1ca7a7c82ae9687c326ee6
+36ff55fd3211461deaaff7dff6bc94b248ea50f86a39d5edc51572afecb891d4
+c893dfd74fa47ebbacfb0d4490b5a8c012db8a6aa5f4531250869150c1c25fd9
+2769ee82670d8ea963329d3119005d280dfae888d29494533edb026fe0f11018
+3bf246ec4bd9f5c0de7dd158895ff40b7c2fe9f8cc537d23771cddd1bfdc171b
+72d58e2fe9de17cee61af288318932a6137f976d3804e2fbfcfdbf66bc31f435
+ee1e0dacc8599ef22ec48010d4c79707118da59588bd89557142af786ca66ed0
+a5a2bd6424d137c2352a0fe3ffca8db62656d143093f205c18e8dc761448fec5
+510fd026493991ee9ce12f4b2cecdc157f3bb6bf46509abbdabeaf66703a6a2e
+616d5d15f1145ef8654f810b2e30158cc883f04d31798672421afe80979630da
+fa9c444aa6e4b242c28df13c80d8799b8916c9dad6eeeca41ca24cbe3741de73
+193674f95269bfb2f832513d434c34704365a3aa76d3f6fae51b8d75f4460df2
+95b64bf0668fe311f78177bb0f560dab089ab5a6fd11ee9bd6faac3e6c5ee08f
+79c94b34fe82b1a2561000fde54d3b8fc1fbc7d82392690b2216982e8d9b09cd
+1151a8aae36231d9606f2d737dbf282f4f685698e48698815664d0a4eac63dea
+f0d58ea4edc1ef27ee2f4995e0231c246aaa5e212bad9a4f133f1752140cd8bd
+7e203b235b3f54a74a8c2d8a4106ce38d558d3e9061c34cbbcb5d133820cd5ec
+fc7ad5c83bb1b22781bf049a9381bd3743c6ff79bf98a25fbd361dfb8a7facc1
+708d075739668598c9d0b597a9417849de1e8169df8ba2f2dc6fd57d7d07ed4c
+bf6f64734a56795c6480216208026a24e0272e871a44752e850892e9fe72b22c
+15d83271bc1613cccdf6616838668fa293c564f8dbfabfea0305b7497286fde3
+411e00adaf00fe89f1625c57ac1121927000bf28cf712e55b2a717a4f5d2db33
+c2921df3f6449bfea10d566ffc096450e78807f7611e281693e30463d32b2135
+4d94b8a6d32f158f1053513eea31213d73dedbd3db635e0497bf3ff4876049d2
+922e0bf8a8da35e25d19b6b0ce73eb7da1402b73627e90ba89877bfc16f3166c
+5a458aca38c58e7c652926c6547d018e109c4fc5c9961aefb4607e48b0c89cc5
+b373ad46dacdea10a1d503a7496ada54d41c342c36dfeee1dbbe8e7fc3664460
+6860622ae1c90d5045e3a6eba40df23d4eb867693a5c3d25ea2e1c31d8d9e630
+b22140e1078d577df6cc81b92bebafd700562ed45772247a7edd7fb4b99db8df
+6aa8ab82b059d995474744b7a7bc637a449cd4175d19116cd1e122beda56868b
+f904ab38fe601ee18ac0d56afaca712d10c09763cd21df0b222405160cba40f7
+a1828fae4db2edd32a8e2c5c173434348271dce53562d1c7f96264023f4e54da
+63035f84596d3a2a028b4692ef98b3362258c59996c07b04e83b66d7e5ce103d
+2352d8404e32c8408a5b05cf150f151484d3cd493f2429ca170b555d7d2be9da
+96212b3b60936fa14a71b72359b03bc93d72f9018aab793ea640582b24109e63
+5bae72061eb66bdc9f28a9c6ba27caf470e05e41dee81b43ef660d5baea7463b
+d36c47bf900b78f8053cdc842fa4c642c5e313fefa82c1aed58038df06201027
+79450a5b69923a4057d090544c8b343d45be71ecb3564b5ae59e18685679c804
+3ca2680a60ffb8dea818085870c75b54014fb031764e95b666d6ed70b3711b9a
+012479e72d988d5cd8f8a95c3d923b9ea9717eb15c47885c7cc5ce411d4adbf0
+8cd49e76553efefb04888f85f17e6000c0d1c3a875b54ff0aa1a85bf1ca4dedf
+d0adf53da875eefa66319d47b8b7af2e641dafe9466a8a1ee3f81458359780f8
+4037e4c647a86cc2630a6cb49675ffc1dc97667a589df4e77e3cc5c41215c16d
+43691a68f62e779eb70af689ba7770ce5c2f6e060b35c7289e3fe5369ea79179
+8bc466d7c415a0c68fdccdf5af35776cfc5074a92a8f131b5c2423cdd13315ad
+a0e4969330ca8b505c0d86efcd3cadb7f5c77c2666dd9e89b9b03023e1db13b6
+34d442bcf0e3df94c239e06e9519c70632788c64bc9f4c0d92be238be009cd8f
+c8765e62d5702bd31435c9eaf5f2a04d3e2cff1406337338821096da07740689
+68e4918bd0e5e26d0404e4a8b0169ab68b2980d2eb8a5c1ce0a8c751f5db447a
+b0cb1f731aa4c912ed0e07c3fee570ccc6653b649e6ce447bf2fa355ac39f692
+b54d744d5a6c1c03249e03b298301941d89bf065a2b30a0c3aa993ad18eed0b9
+cd70955aa7aff8003711178da2eadac58cefc9c1e0e4fe7ec10ced8be8e1b40e
+56effc97fcd8c1e68230a111da8d36806ff8c9bac32c879fc461c4cce809c49a
+5c489b352f86456032a19b6bed07c13089bf948d32831f2ed25c587a477f21b8
+ff3bc437460438312fad6141cac9311bf29dd9f221b7a0cf442b48ef64c810db
+82a097530a129f2e087cc45cb96d8e7a9b3e761d3f3ff61f8f84ae47d6a2407c
+c653914a9c1dd0139f299c84f5f964e6266edfeeacabc4f5fb89b9189b119ce5
+7c8ba8709b3f3b73e9e152de57d9be8c3a339ee8ca40c1bedff6008c158d777a
+611b1f2a7e8ab0b52d211d7a8998f6cf2e59d5b6ed0f0ba65985353d2734f025
+11d235720b0860e35cb63b65657f2c002fa54d6ba8054cd94cb450559bf18ff8
+41f886d991d1298cec09ab4bff98440cbc4977521f74c2c1024eb76c6c827b20
+64af83474d982ef71b5b4d10ff5e99f69943d9ac4cad444fe5aef239e51d0bd9
+2bf89b9aa0462413961b459b1d7fe25672cfba44da5d787590db7cf6f41bf8ff
+f568147f252935fe642f45be670a1f5ae82a53d6e850e6a0d7a37efe8b6c3384
+7df10e6dad855b05d8cc56db3d5b457cb44ba2061ed18bcd714b6ae4cfeab6c1
+f44a983d3b09fdbdbcf49ab4d4328aa83bf6d35136cbfbd7d9efb9387bb43859
+82761e4dd9a2e8af77f8f975b08f158dcec98b861a7da0da7b72c54fc7f32ded
+961d8a402150fa9936415195a5d45da466d90bcf96269b77a549c615291b4aa1
+9387d40c6fc1befd017e98de8f8353e8eedd53e13f858f5b428d8d481d43b23a
+34cc07b024e84e56ba46df541baf7ae5444de3aa77a387d27d5149ba0d83a910
+44f1f3692324fb3a7cc2adb22d5063e5bda1979b7cf64e264da24a21c636699f
+57b75385698366c7a44d025c4d96735ea99cc446bb20371b234bbe531aa36e4e
+2d2aadb8e6282fbee95b8b4b71f90f2d7d6d38c9030428b0ff301c8e6620375c
+13828b08801a6099924f114fd3fb26afdba9100caa7796f312e5b98b9057a932
+e5789fd8f710032d29b526c9df47af7023d4acaf512e4b7f4f2d13827e235821
+3c4a8e0224939013a38fc7d73fd599b1a7f2fc9e314118864621981243462328
+61e1c62f8ce1d4ce8473d8dff76600b670e7649fbf8422d09dd79b80526a6284
+e74b3d04f1ffcb2a31bca539a25b64754f893b9ad03d965b5fdab567cd967320
+8a1511b9442f896f0d5d5094649b6296279549ad3699f348796aed2316194404
+e0ddb0d0f89682de1648f4675c2d8db63b68cc653d390fe2333ea0829eedd9c1
+b6f6a945aeb5ae4d7973756eb8297231c9ce5213446345bc133ce5b1470c6b79
+063c8877442e16dbda8de7c5365a9c8a91a8c9c99767d15acbcc2b82e54c8784
+7c93f7a51f91c2dbaf550130ad4dad6896d9c1d905aa7746d7af81b7f22e4e9b
+18db615cf7e7bc813159f5b125fea93787fa42db2b4602432c487bfb49f29d3a
+71ce1ffd9c30206b294f8281f58fbd27218317587316c308ff6f719cc347dca6
+49536a86205c1273edfda12610b212f375b91a841657a3e9b9a359d398edaa43
+f21041a76f2b2249e0f07925a996b24d56bf902cd2e6a2ea2b21c1840c0c1a2f
+85c98789b1438292bc27dca7dfe417258144d267c15b53532e49023d620b4471
+da5f47f99ba092cda984a7b9bd308b65fb0dda4c92cb84fca3e5086df0100ff0
+7a031a99e1b432fdecbdb2136e29faac53425c65cadb7ec198da5cb63f6e652e
+c84866add1b4f754c639ed962928edbfeef1325fb2bd80f8624ebc1465fcd609
+0931ffbedbef0aab40593c54a51d44ad7491e07d443ee0ca86167233de12657a
+34a84d7a96e9b73698356f5b7010eedfd0190287810dfe3bf4b75dd58e98ce98
+d10b180f754b686e8ab4c621e4684b34680d062bb6e93c5676f9637d72520771
+82e1f2d39e889b03785b28928291fb0997a2a859c7e732b27cd15ad1b70fd13f
+9200246496651f4d5580c0372a946a6a272c7eae3b071afca478e5f23e95f950
+f58ac0bdf7dc5e4ef0616bfd8e2b51fd5d2ff5ac5a816c4f79efa4a4435ff54a
+bc65250ec3ba0671ec26fd1179edcbf4198b5dc25434170efae9f03b5e6da69c
+f9d7c08f7aecfb5da7e493708e51339a7ede11d3de96e97300f97b550f84fc78
+175fa0d6ec79c2b29cabba67356f91f43bbc1bb659dac336d54a39eacb36038e
+29f8c412a4b8ba0272c2e0bda184577d41e3e257194925643c04ced8484e7f3a
+899b12a5d50b058f62578cffb5fdd06eaeb05def00d246c2a7233629efe00e24
+1d5a9e8bdb053c506cf1ed8d8305d4ad7bc7575d84fa8cd1c135a6e69ac8da9d
+09b0115a6110c6df36f7bd530e6f01beb2730acffa423c64537f419bfc76e56d
+099a956ee260ec7848be76d4ee19d16eed24b8f0a3d8ce3df142125dd863d00d
+8ec3a0b843378fb2c3ea865cae64914acc78f8951503547690233b4541f3f07f
+a38bfb69c3747cec408346d846d9a94b5e259b51833e258f78b8d56562190a80
+1e649a8ba76fcb180b6c93925e26d258c7c407262739e0252e1c41f4404d4e85
+458d6c0c821b6b5f099628edd92ed5e49ec2e72c3429ab2f3e5745d42d2e2d5a
+8f110fad66cdc39fa4a5cbf520130e2bdc9cae5b182e4d06d4e14caa48f035ea
+f9661ce17cc29d4c91c57d757ee5586400ee768b901c4fa51ce00bcfcfcfc5ce
+1b2c8a7dc2eaa4e4acce7692f00ee06e6f9ecc8be980cbedeeefa1de4571dbf0
+2ce7bd33eda192d31d31befeb2af77c43795fcc79b8d8f6cb0c693d3aa6cf49e
+0f3925aaa9f1173ef04f03d98e712334039fdbdc37308ffce34b411448bdca7d
+cbc32d6da5087c881d55e98f66f4679955b7a93c02f6b2960052319bb62c4063
+865b82366471478fd4f07b44d6443e308f8b2296068e4a32301cffa189a2a287
+74ee405234755f042addeee91b058edc49d37f438bb8693b5c8ec0dc84719b27
+c578b0586ee86290d38c1122da90e89102be47e53ac93f3be3fa55132dda885f
+4bccd87bdc334c32bdeb08933d4a5e6d85fd678f3858cf91f2338207c490c475
+7ce9d56960d5043b8c6b373bee6c0e173e6b3af58578fe6b36679793f07aaf5b
+388c7727d8e04a6f0076eaf9095a3ac2ca87eed2bd7a5c35fb9ef360158b0f64
+48ff114f281f2e8844d0ac1eaf0d3436c5310e6ae25b11e3740cd03ec49fdaab
+a40732a5bbc101b84d70449cb1c8880b0723e07bb2ef63bcc66b95aaaa03ad7e
+c163bb5e98ba3452d5373fc320fb774b13861b72624b46f92178c2d6212fb3e9
+abb7827037747261fbc93521cf1ba38a47a44efdbd6223a0f3f929309cd4e160
+83588b4ac6a9b874aea87a69a8081c5b6fc2f2f70a6b5a086493b644e5ab1d01
+31041fbd60c970f675b13a311e48b86425a83f68087dfe05a6aa22d6238e3e76
+d31d029915bd11d27dfaab5a3ae95e04ca156d2a600166616ebed20132690873
+8fd58eb219a91830624f6c1556436d82ceba3293b1c742f29fc5896fce7d9e54
+e1fa81bbe5b0ab391b73172c7ce9b1bee7b0784cf4495bc6fcb755092a5f2327
+c164e6d783be38ce46375f27afff4ed0878f8eb46634b8f0d84df58a54226d5e
+e4d25edb042bf51bde2ab6c83852c628bc95fb10ffb4cba46678b30f7b108ff9
+3adffe98f6adc255bf4b0d323dbf68fd559caaa55166dfc4f1987508ec46d6c6
+f1db251152af4b9366b559382a3741e8fb0383eea8deeb6b4ee9ac5c9cdad65f
+5996ed7dcc672c41852e9891d25ff257dd49989ed6eda6b3e5f762c930fe6d39
+75b412254c0279ddcd9fc664b9cfebbf06c884986340fdc76c8e4eb560300943
+3d2d96a10f09e80ae7b2eb1f74d82bcc9e16fd11617663768f5f51dc464355ce
+f33d0c912abc567c2d327b0bf95aac49f503927236814663541abb7c689365ce
+75587190537231e16c0d049332d3ce07562cea63b3dc191d68e86824b43887da
+4dbeec79ea7b0089f9bf2e61248ad3d27bb881b704b827e303c21cab98d23447
+dc20818fe8a249bdc03d772aa548f6c75925c74287ca9100e91ac1693d029f7d
+73027e8ae734e354f2828f18a8e87b13d803fa68e6d7c4a79333b21d177c61cc
+6097ca7ccbf700a75ca3b340472daacac6e39be45dd201f4517b22fc0acfd06e
+8920c858542ded1c95590a1b5814103c700c13ac904e7ca36aaea572110080e4
+0057ad118b690e48cb8545bd2eb99b06003c1387354fe3947eb5fbe0244e9350
+e7fa6e8da13b62b862b22fe5a28fb5f933d0d97d0e54d2ea8811762ad02488f6
+163c146a53f8ee78478ffab1b9e123872ee7f50e7f8b124765f3bad9c43b588e
+27c75b8e7ac979e20edfdd2cf68b506baade781facd7c00fe6c967d84c2a8758
+3a44c786038aaac152a10f82d5fcad2fb609f62c24fad60bbe5e8e0d3c3396f6
+3b00be25467570f5595f6dc9e53bd5a437fb7388acef7cfcbc22689149c386e0
+97f429be6c7ba8d70d8548e796192921b764deed94f5d4de9365d3535901aa91
+6081b7b548c3b7555f8936e343eeb062a2596b3065ed42f1489fe03ed0441236
+eda3796353e768a256f7eb8f6f9c05d9f8449bbe0a1d7e99519b7e119ca452ef
+46600b666c010d1f9a2eb21a66cff233852e8fcd21cd475423d32a5ed8f1df40
+e18b1290ffce0fb278066132598133f094b52988a9e3ceff74297f4bd6f8b8dd
+cc069cc7ef14ba3033ac19413afb7676de9a07a2ba70df9c96bd135564e7ffdd
+6ef470d939c5164eebd1d3824401c6e75b1b5695fbff0ecf5056142cc8afe527
+e260e93ce22b5e0098fb54ca8cb1ccb24ba0c4c8ed9ebc172dbb1b5755d34b29
+d175b739f71d3befbd43edd58f0b3b560466348628aeb205a6db30a05da09ed0
+a2a99eb5126257cd30c90758af231106871da538b340e7d6ce24fb0c43397f2c
+9dbc10f18c1839cc15c0e331deec3fceb468a9c6f60c233df45b0c53840dc659
+6a776c43976449b2cbe76e0939f060263cd00b837d21c32e1dd78d1b974fce3b
+e8df3ade9b4be5ee8c02bf10fc08f0148177e4b50bf6f1c98589092d8d9c52f8
+2ed71c6009abfefb3a2976e40e700a02b2d5ea69f59dc6598d46ff6b7e3050b2
+983a3e12acc618b4a2d369151b49cb89ca2bd0b150d6faa0ac681c932a7fef1b
+e77349f948e67596893acf953df11844fa2b074fcc8ef0b37e5d43a9282f3f82
+92436c0508cadf1b59b4e03f778dec524fd6465467ebe7b064ab2ec7f1e24a7d
+20ae5843b6919e1f4bd0215d91ab1ef346fd97fc9e4871ba30f83b06cdfd5180
+79d2926690101525f2361a54983ca427b4a7937b550aa9d3fa3409f6d4087cc3
+3507a7dcc6b5769a8023cc0b790e149494e0b4a62474fcf199f0522d0c0edfd2
+68a3b5b86d600177616323dec75753f4462239d88eb6c44827119de33665e57a
+a7a1f133819dab77d51b264df8b9028651050598ee266a645cf34719989a059b
+5493aa548ac308eb673f9c5edd38fac3997c4f7249f28883d763a6dbe8faaa9f
+ba42981773763490fd6ea356aabc5178f8e97387390910b3310bf74d879a02df
+15ce08929c489e7ab1b7fd5dba4b31f4d10a4d5d860b354549b948179339d08f
+09459bf54e119824e5a856cd287ee1066a69f78e0846fef677dcbe223357861c
+80e5b7f08bff3a08b1d3c18e0fbf66b43adbd1a91e0d609d92636ddc654afd83
+d490b7909b780c130d5ce89818169700a3ea8de81df1ace8a8443b4c365531e6
+ff35353f0923afa95c1c3fd19cdf0b9327ca8708ef9c891c5d5c96ea695d9047
+0ef9b222757d150824fe741bc0f9afa6ff11a882da853c1ef45a7f6936fbb2f6
+6efd8fcb33650ded480ee0d3857581fee0623dcd0207e6e708b19b0b93822994
+0151e373c3d60366d6a1b61f42ad9dafb751e557a7d4488ab09acda685610bdb
+4122e2ae576196f50c38a26a757b2b6e6ebb332a62b98c801fb67bc10a97df07
+f5b6ee43ba7a1c4583439a69b90f160a46f3cd05ee9d58774ccb8f11f8a1bf9f
+736579cf2d3b1ecde9546d86aac7e5a9602bc9cf2cb68dd3264a6b12f5d185bd
+10d2cff08d2ee240c3b2d4ae0d5f93e2dac55b8d3adf1166473119c3ea90cc51
+bb7f28012b48a63ee7acab3e774f043a008666af6d0efe9460c40ad82ff85ab3
+cbc09e4cd5867a52e25b1a72e678ae7cc06f6d8f913ac783749f30dee5feb866
+5ee18012b1c596918a5869a2eca7539063f16bbc2631f4ea99f041fc64b0be69
+82d3761638d1dea2ef828388c0629e3d953ba41816bb171d65e4b99cdb003281
+cd6c9b350577d2a922144402e90d8b2ede231304f055f7db115536c6fccda5ca
+a5f182bfbcb454c4d45dc9754d04a0e881fd03fed5383d8b30e8397fcef19169
+df94e71ba1689f4cf42e60fed255e8f49ab1c90a7148ac377807e088bf4a9110
+afb3a02181f44a7caa8fdc61d2fd78fc18883ef6bf006126737d5889dcba8e99
+7223053cf17a110798b1a1c5f319317ae63f9a6ad23eb6d47d61b34c596e37b9
+c1e107ba5eee08c98af96261458d758b9077c3874a4b7ad2cf2bf1d3a8f7abd2
+9533934e5f6adab0b87b68d953fb98e219f956ceb8f172f03286cb84384e9288
+6166b2b49da6e5696f042bb6006d942e34589984e970c2641b97fa88bc536d40
+5155f236de881c851a52ab869969a377433cafade08178b1d1133891e9df4b21
+f8348e0bba155ed17ed3c26fbbc282aa8c3f27192e1cba91e2c461dfb5a52977
+71874292019b743cbdbf11dd1a6bd6b7c1695f774a34f37748d49aa32b5ecf5a
+8a1a30e329e2ea9eef8f90bfffa68641bd11d749f5ba343cfdb0181a92ac9702
+97725b3968c8cd20a0af360e9b740bcec8644e4e1bab4cd7a3dd1876f09b6af7
+a90fa670a465949784201096eea610fcf2d5c15f57d39642bf0d3b246da6fcf7
+d01da8ce59b4d349ee76e8ae72fe26a170b2b320eb5d0a7d816488e248cc016c
+d9f4af759126fed20f54f6176b44203b3f1c248d9cc0e3c6ce05aeeb2d127183
+e1f89aad8ab25e5660ae88852b0465f2bf00f234aeff78a82abf21b3bd807172
+94760e71a0472d14f9d3445c24b38d72df442b8f78640c55412f21d6b42f7645
+277caed0dd19ffd5941e1a325aad194512d13e144259ae01fea735580c28c4c1
+48ce5ee289d8bc05bbe1323019076da77187d90a35fcc1d0b52e921ebef447c3
+fd0e445d5fc62c809df85bc141fe677098b65d78c29d9f279cab0e7b452d4490
+e93e6c3ce30c84ce73e2c419c91a304d0e265df004b549f73f69995347e3a654
+7e6a0ad2862de39e182bc4e880bf717643165a90b094ab19207369bd92fc3cab
+e1b71c496698044f2f1d2d30248eb0de57281ada534155d6991921fc3442c8e9
+0c77c1b1811bb3ab2bf9045afa0ef9f74f8577e48580eb9840354602b222e9a6
+cc9dc2c70f7c7e7ff18687a420c3f2a46680007c14d75fb701e5b8981f4e4175
+fc81d4286920db1080ad6c8d066ed15881d710952e5bc28c444448aad1df885a
+634ee6bc8ddb52470854ad55958e179f05e61f8151a6fc6311bba2419dd9a122
+93d6e5cf69411520f281b8ea2908ad8f4d774e777a633a8e9aafab363f426b65
+cd8889d109f0c0ef516da05063d1d3256250eed899d6ecbb587c7b2ea943127b
+f76453e581002ac170d6764cf3ef98ca600f07c75e2d124e3faff95fb28e23b2
+3969ed2b48b2be9b9c388660cbef5dd96ca0249dd307bd2044a5acc598aa0bc5
+73a97be07ccf5b6bec78d99b0e97238e550e4c56da12ec2657b6dcc9d9687795
+8411cf3d159f8796f6e9b8ee98daef301c4cd44bdf9da4c6aa6e50ff517dbfc9
+7be31ee0955a1440fe1cf27b6a708022104a9131d6ad031f87143394c7d21655
+2a540ddd9eb2cc438c06c9e38caa404ae87726a1edd9aaefa7e6b269cc543224
+35319eb5556497653b293197f1a5fee96c0c8fd7cefab51a34022eb008dad5ef
+e010fbea98e316dce7b94a9a766c121907be64f3759dee51413669aa882b454d
+51fb13a4b428d61b3506c20d551a6e96c1b538d6f10098abdc6fb8dd739cfe69
+512dce4336bd31eb5a626457ff6c1495200056c716a10c4c8e6cbf24e5a89a91
+a78d31db4b1dd71d65bb978edae1b0821bd3ba879a646c4e41ace1fb44d08e61
+1a9d945397ac69d01e8eb8e75fd60ca662ae1649346031ac32c3cfb352d84419
+52190ff0f58283bbd1bf4cda15bbd219f22e738d2c0a114e3d50f66f9fa5214a
+34dffca2cf6c88883b0e416196e49e45a513994ce8f1abe93591c2b6d67a151c
+820f1c7f4ec6a0531d2d13056f35c2c625044abce5111e3f625c09fa566c52c3
+615e8f83b28506f21ec2a9f0d93fbcccd3a2362ca0612bb8ca2123300786581f
+88f1632c5aa76dcaac69c8f0140b45bb46871caa9ff1c221dcebca18e506ea12
+c8ca217029dce549a4d34a58c3a5333e90286ce87919665657acec87d268c255
+36837e5b6cb6d46367faa2951dfdab709b2d2932f44561f73545c82e85599f5a
+8cd72bad593341e0de99b41b34f3e5f8c1ae749cc7e551f465da293ba7af4973
+35bf4d08a68e7abb0bff04ce36b4571b4212e6b98539da495ed516f0438baf8a
+ce13dcb57eeb473bec625cb3c5bc70ae22067e6f9a8b0f4b4fd231110aaf84c4
+a48559e8303e7016c0d02125d73c690abb75c178855eb2cfe2ededb11e76b06e
+6942a101a031fc5b110576389fbade6619ab0e05e20b8d750af2241619ff8224
+fd1396f44683f3735c0b83f93da04d435aabba9073a7985040bbc0ce5094ab39
+7579d5bf618b518ca8b1c4f9a8856c711aec8ca0609d7d338fc6edeb27fbb400
+db045314795a74317812652d0c1ffb1fcf453d5d769df6b38831c3914aa0d5b9
+ad41e0b7b9da3074043926028a84d1b09f52b619703814911710cd915e3ba310
+d17cd6d1e5bd4b7a9e7e3fa2f9d0dd50f88b50c895f3c7cea1077242cae145b6
+b6311f96996d437e369ee8e46d6680affb65c151541ce5e2eab2c851b693cf8f
+2b304d62d428fee0c99841d4518fe2f67ffd0a26934bc70b2d970e0dcc13892f
+d3b725ef5a946c273da4714ad1b2f4f54f1c257511770cb2bd0c010c3c8b03f6
+3190f52f50bb4db5aa56eacbc174e40452fec845b974920633f2185bb09beb6f
+f7f1be65ec2fef9d10313e6830db245c9c976784acd842f22c9a12d51fa47759
+d9c0db2858e0ac424e923a48e89eb55b4d8ec738caf394d3b4761a1370fa496b
+6d318f951299eccfd3de0b94ac27a952911e165ef590fd2729cc2cd86e0362ff
+f5b51dac1c08519ea8fd5cabcc45d1672f3f04d1e463acf0bf4125434e72cc76
+e900f78a48609069187cc80acb13d0f177b56280d468737cdfc77501bd9b4036
+fb146ec5319f7a3d828d5523ea007c9cd545ca043bc48f174f72bdbbc984b40f
+f6d51401d414fea2bb1ed31761cd919dd49199c0e0d0f1ed1f00e8c3b59659ed
+55a1bcbc97ad9195e1104af67d4ca355c0c37ee8fb6ed7be5f5ff6a68cc2ddec
+49cbbb71871262c4a1a7cfaa39aa6c86002050d91f7cc507715fd6b2d3777d2a
+3b9074928121dca38c6934019d4fb8401c4c2484dd0c0145950067025edaac4c
+67a0d18a77005bf9c9f953e9e55344f449278cd75a65a3b79071f4c3f0431c82
+f9fc739fc4f3e54611bfb7fca635a02492dadaccd7c85d8a4465aeb5285620be
+fc6180d39c44795748f3fa23b9de86a548feb40bd8658b4adaf79ba705edbe4d
+1e5c36ec57d22a9c8d64bbc765b504658c167ca8829e60526239ca6af7624781
+2c3d3b0d06f4a51cc6d8c8edb841bc0a5ed2f1ac4b9fe64e09fa32417817304e
+bc2ab036447956eaf189ec62f0e415f7020da9264ce26c5dbadb6dab9166fdcf
+56e1e323947513da6e14e111996db6f92e93c429cc62d4744f9ea96cc7c55cc8
+f1859730327991842fcc7c2f2fc74895ec3974ae60a1b3345edd7929761b6338
+5450c2927d83993844e156ecf5266e8bc5613bae81fda0a628954e111db74915
+285e808bb8a4660c098b60f251ce38512910d17628131476de7f1ac985ae1fbc
+a382688cad091dde470d6407a0967cdcd26328fa9a28952ba14fc49af8a908ef
+82cd09ef940c6f66076bf67250e40b19a1e9e4f164246f6d1324c702a59c143f
+53a829d967d83e905f49fa3a64a2b72689d35213f3ac78f80ae21fa415f2440c
+4bcaceb6ba89836679103b22bc93a425606d83c43fef202e4d6f20530da1ce81
+2963064da571136d79edefe79ee876a4e01fd64a5466315ecb61832fddcff2c0
+73e6aaed9940bd8d28eb78f75a04dca7235f8794074ccb5b2b8e5099124b91c2
+047aeeb70e8fc5dc8bec6e18376fb7d5546edc7bf20671931dac014c6fdcdb35
+43d15211873ee65f793aeb3291f3e88a603367f5beaa7ee59e862f54add1a819
+1c4b38065adcf525170842879e91529f35bc5f7f78e58f17f870dde83938c149
+b9d15b3e8996fbc2429993e05683a4c7e64f7bbd5a7efc2c9ddee24ba3f329cb
+0a7b32180e73cc1eb891104bc8b0a294816011bee7039c6fcec289e625c725c1
+546ac9b505162aa68156bfbf40720498408809e5312b4fcb1505e42effe3d3a5
+04d792517c2b85388280567a1554181855fa60e250dd91af20b44bc17931fc52
+a88f9a9eb5b8c1823ae73f4a7bffcdbef86288b38efb87dcd76ba02bacbc8349
+3613b0938cd5e98c4f3966339748854ee61bc5592ec209de1759a50663bb3360
+831bbcbe02f72cd061ad784137e2480fe026a9c1130a5a557062d72808530bd4
+0b470f97a025ec3b2789b6274ef44b92f5d7a7f138683cc6dfe03e0cd78f9df6
+5fc8eb68eec6f8528ae8a469cf0521062ab95a4c7997f38969541cdfecc7f89e
+3263cd46c25cf6b7cc64fd20e741017b89d0298e2c55e9b582e6afcdfa1edc1d
+fc65f458a91007254db8a33c38db16db0fb35352d54d9c88d602caca571af64c
+b0ce3b21689d64fa4561dd65be69b6e8fb81d3b2e32f7aba81353cb0f583f90e
+57df42cfe601d5c05df071b49d4bc7a45df5ca2eb8a1b547d8767a1c0263b93b
+e41caa3561b425093e8c8a44034d9553d96fd11e72be93b81b975533b30530f6
+fb7c44a81fc33028d1474d6d04ea1e8863403a4f27da27dbdb37f2af7eaba079
+ac68f395e8237597cd822fed413d10e4bc700e8e3409729b51ec700b17732149
+742fc60c9f5b26b11aaf47d3e80d4c671e5bc0511f8bd6e7dbd28790e622a32d
+15e9cfb4739f4c6d67c8dbee302232a3fc9cda721102dcdda24447202601daf9
+f6c374b9b5d687d06c0755df942e51d5522b896292211705cae907523b9e872a
+04f49baf43d89c3a4a37cddba1d3f20301666f7bc970b72f5cba25029bcbda99
+9afcb5df7a9fca9e11239902bb4b2e4609fc6fed679f9324c16216e4fdce2f3a
+50057b1e73344eb5a8b739788b30d3c4e3ed6336c95c84a2237525c33e59dcd8
+4a08ca60d031829e9bdf9a3ab6dfc6f8bec79e40cac1e69ff6d5d2ccc9c65a38
+bddcc8526320d847d7c6543f29f6f50959efaff66812939136bb69233f36d376
+c01c368e7fc20fe2afd1192229305000ab8c2068d8d2158878144b545c2ae572
+20700483bce6259efaf2cef4706059847bd721da53a0f842011e6ccf8c2c3ff8
+97a477bf6dc659a256cc73f4f3b02f541bffd23439b12520e45e1f731e06af12
+0c93c3b21993d1cfee5ca71eb495e705a830eaab201d4ed332d65661cd38e493
+039f250a3ddaf8c3a88231fb9a92c72e02d50159faeb9aea556c89017a8703f4
+f2bfc73d0a40182597a260cfb29c7cc946780548fa1fbf28d35a93a2393a0793
+1c789d864ef906249c1d36ee78a62512e29a9a3f970f7a1be3e7e566206d07b5
+15135f143706e0b713591524dd318f1b10210cda652d5a6cd43e4e4a1bb0e855
+26157e515df835a962e7fbebc7f51a74d5224d20b5e0472ffb65913d8da5900b
+31d90ca409446da57450fafdda1b5975d898b4ad2cd43b9d0660aa81417d2847
+d4d0cf0c9d73ec645e61f16b80e1506a2d2ab8f87a1ae7617d7fe8e5caba611e
+ba028b2036b33721f78dd420557254b02c071c99fe068be1430a1fc3a29ac15e
+37097bfd6ba1d7f095e98874ba909a6a58a419f82545c65aa70f5f2418fea14b
+1dc7efcb3144c4cf060e6bcf0bb68c3830e09d069e83f830e6e4cabda75e9cb8
+cdbe388b0a7c23f7e62a4990c705ef96fa6d95ecfd38b2575b0be70d0872509d
+d66bf4e2951121cae49c0b21a13685782a2a83a1ec70bc987bb9a0fa8f41b273
+e071a94a9fa55c8cfce83d1c7d9b0549c6227b8f8c0ed03760f5ca091c29a663
+749627d4038021a7c90bedadb4fa40b8548126986369e04f62c1b9c31624ff89
+debef79c9ca9b8ee82c6db65d8a67fd9c6fb4463e956a1d7120a5bf735902327
+6cd611fae157909073a41db7f5586666efea0c0ee7216a8a5699102ecc251f18
+f26c6630cf5273bfc9c1a27e6abc5156231d8879c0d1d767f6c86e7bb09829fb
+6b1434eef852c1c72751010e25f689adadc0e0ec47fb9e4515ee95bb75a77271
+0b13290ca72e10d1ee9ee35e9aecc6f32c83f2b2eb30aaf52d6948bbadef1b58
+7092fede63953a88925df565bbe37456e085bc3c22c2b340d07106ad7b699614
+f7e74dcf82bbba467ee87152378a4ffb6192fedbb103b9abe1d6af8e0b84143f
+f2da81b8bd16f4b9c110602129530d051a23df6d993e5f0251429a9dd8bac65e
+e13e0ad97335e389f3bfff17b562ed1526ed4ba8f1e52450de53dd275acfd050
+dfb13f167c1f18ca022c2b6a74cf496999b02ffc072a6aa998e386cb1418dbbd
+c30ab9ee4cd0ee2f0b4eb816f7cfff1916d0cc8cf278cc8a1ff0501f48a46b6e
+8dea7d7242efa272bc744d96dd89c2814c27417ab2d6ed7b765c02d8b9a1403e
+3ec34f94e6c198994da83a17ebfaff5e310140b44f9496caffedb1640d85d8b5
+d3e2b2b5ce786f6d910da398db88738498eda315c4c84acd1e3534f6612b1c41
+a27fef1368c001a24ec8b5ee18a9e0d84c6d79671cc362b0c89217158b930cbc
+c852b3ba003143a4a945aafa7d9c060c911722bf05018898840e8f72f459ed72
+13d17972624f593de7d4574a1a9c7e2590cf1d17c5e284021472e4c16fe656ff
+e0cca210ea8ef7a64190c489ecc098c09ce5d0fc6700ab2b4ff0d6dab59f37aa
+8962226106bcb7a416c48170e107f5bea3fc9fe680ad4a7858be6f1184bf8521
+4eb16a7549cabdd7fc6dc23c62f3164728dfbadeb65487fd0b979d3ddadfae17
+06cc49f9e781de915ff8d88ae280986549fd6e8e228940aff26e1263879af4d7
+228c6b84de15ce2b2a5ea3d3771bf8e95c04afa2e922582f3b7b4aae3bf322ff
+30a6464cf8c3dbb95a21a4f724c4e59d581a01bd6be1ec8948f82365125cc16f
+3caaecb92d36829d781ecb3b6cc69c70b28912a75010ef84402c6245a9013d0f
+7f24bce18f8d73b5c2ac9e9d91337bdde61801fde21bc848876817d2cdbd500b
+f745bdff95fe3e4e3751b157a590e88ca7cc089ef91d55757792019742bb9cf9
+000b47a1f0d14a1fbb71b8875f7069f246ce622727e771363475a132909f2723
+9d4782bf2bcb24db445a975d7b84c0c0668b0dd8dd990583788bfb5f6a4c853c
+32bfe6ca6718801b712e3eadccbb086a9245fe255413893f64a6a4083586f476
+c66112c90d7f9572e2ad81853e8157af2bdeb4d0945c02c046a1f0309ca2c93c
+fae5dde45a34e2d6e27f28af0a606c854eea86bc0ffdf96c77df1b89b39dcb75
+a49c09b7047689abe355ea5c2db4f5ec9c67451a4a3b7b92ba4a202b96b1f314
+ba8935ee7497f1d4375fe761841bbed442591f8154a5e9a62cc7091d594688f8
+70f11412ae503e594e26b3a55d6ec4f24b4c3697343f22ef816bd8fe8d13acc2
+c7544368358b2dc55ec9312ec58c536793ad6488e7a410427ea80bee493881e5
+273efb06078b999d66884513052db33f5304aa8a5e35f2881c3d7a3400fb6302
+9997a24ae45d0a092bc78d0d005da85969615da06213333c212cc42f4ac43902
+750caf338f4cb0c590c4d5fbc21d676c5b5c39e1cf9139ac9b66272abf97f0c5
+863f4a4be9c213b2b1bd67aa45b917d919ff99748ac72fa997a52d5b5958b38c
+40b3b89322dacb31b2ff49b98c2c6a5bcc5d2e26ecd69659bdb88ffa1e226a74
+d4810d6f0a7602d79992067908c058016b9d0d71c6dbf7bd1d13a2cf2280b7d1
+c5e53ae86e6480fe7fb57efc26de2df4cfb7afbf0a0f6fa6060f0b2a3d1c64c3
+cb9367e1f54d49cb808c1adcfbef9b278095cbbd2d3720f5ded47187106ae1bf
+becc86c6676c98a95eda58cad196e2da2bfbb8f11681ba95207df27dda96d8d4
+9943d4554ab623396c271b4a087a3693cae43f944a00bedc65e2c724cc3eef22
+0289695379b2bbfc594837e20963dfd0cf5dca06d8b2452be1fb37fdec2229bc
+e6928948e26596437978defafaf1020386c1523f047ac699802cd674359b35fe
+9bb4dcd3e2b60b54c70a28a5bb780baeb2a12c5e333fa7315b38eda4d03af438
+ed35fecb6f50f5cdd3b9fc770fb3bd6e3564b49737dfb730fce448a1ab008366
+2ed9113051be9589966ec890c750df83b89f07e95a8d34b9970503336941c35a
+fe2f09432285cedc586d70ce35ef5b6b678c2f20b5011e92d285421272c052e5
+4bac78ffdbfa89d1407572c8595e274244973fc29f3c1e3a85fb63ac3ee099c1
+5cae84c945970fbe4f8513055d5803139b8207687d2aaf9eae9f87f459422bce
+6fa9ac1b6b7cf09b8529b93b31abdd91a44278ee39faf29974f34ee4d787ecd0
+e689b5076c9113e4242c5e62c01a847825e402376465ba14d12477a241779bbe
+659eea57ef267d35ca3c88d34326028bd24747ac2cb6fd04e556a99f0bc44d8c
+35137b165465764bc92c9ccb13f77462fdae83fb68a2b36d70719346f56e79ce
+5271fb10bddd68982938c5a4be1601b16b32af11586fac8299a76d37ed286643
+18c06a87721cbfa8c5507b6b40458afd111e0945077f145cbbd2137ceeeac951
+5295ee98c1747f3209f8ce643017911a30a0d184889104fbfb01f9f1062af6e4
+b05b489302bfb2c8ce55839f7d6186b081e9ace3799cb323894e9d9bb59aaa91
+a7d78afca8aa91952fa5cdd1c76f602c95c31d5f83470c62e4fd90fc59f72b92
+2fb22b1f4414753875674e00ec274551d88a5a15341a2f98ab8bd6555edac3fe
+c5415609051a1c0174e69929fb164223b6cc9171758da0a676a5a95986229dd7
+bbc4f2e3685bad35de0cfe6e42dbc304bd763d8f1fb6557de226e00ce34b6f26
+2f7e621342a34409b3390c4b81c50d1de3ca485e1c56a61d6d13127be952cbb4
+617a079b3137af9e35bf8a65634b212199e232775d98f937d6a8bdf4c8aa233d
+5e598dacab2e87363acf2748ca7670fedb4c7f5ce9dd007a0b786e4619a82cfd
+350ba74410e49e93a15f1046b34c2014022ea2a44c31ca910e7105138c60a289
+a1327aef09a7e4dcd4b63ac29038cf2c57c5440a355446fbae9c90686493f055
+81a1fe20b5a1d9c657ed6c3a0ce0d363c86c5d9bf5a608e148d2e512b9234aad
+295ee956dff7ba58bbb2e2b62bf4d86099c6daceed8859c8f666f758e5533dc9
+63a98e679be18626d5cd0e04f2d92922fd0b1657c57bdd39e85adf80019953f7
+bb5f5c4b7d7b3df072a764e6f1e41ad3ad10d23386ab594cfd17d330c9341e39
+e67ff6cfecd3d7e2d4ba48ffac38572b61bef1f6ee7d631967a357b6299159b9
+a20dfdd3aa5b2b7edb1e686fc8081106912a5524b234b740a627741751ccc556
+ddd0424f5491a7a9101cbb197b99e3e9833790f8a7e65b4086e3f6014f0aa6d1
+424e405a5f75a55bc7d55374fcb4c1f50153f6c3bbae87b0f3af8261bc42f7e2
+48ae94a660db17d3db51527264501e8a5ffd8ce7582c71a0c220573a86181d12
+7346b4c7218a87f9578fba642b365d57cda7aa59a770f402f0c8ee2334360193
+1d519df5ac2e9cba36be0cad1a3817ece63b75eec256d0678799ad0a56f3b56c
+db021b6e8431f840e1a2aa9c898c5ee73cec569a8110878612c459e0356d51e5
+8d132726b2c4ca26209156b91950bd344628d7e6aeb7628ae4154ec8cd1cd6e8
+f77d9ae90930427d682fdf44239c52f43d93a5649ab1a94c8691622057946600
+23767f3b2ac73390eda83db23e6d1f0a3b58f09f7d82221d6ef5835a15644a5f
+6359a98de0acf1b8c06ec0de6d3c91804bb86280e11a982f4f238651e1d42335
+eac6301249802389fd8b45c30056fc29add3c4ec7cb007097799549267a45495
+9b83099d6409ef78d725cff9e13a0d5b13b59cf4f6d6a3b2402a1f80c1e35c19
+c448bc004cdc88cb76867b97b0156b0988a152eecf47b30d473f731df755c8cd
+8693cc86e9f0a1a0dd9dcfbbbc724f521dc300ac50bcd78e181113dcfeb74710
+a70d6e6c9920256d613c6b28b46ab08f82b5af67a4f216ccb31d3cda24d91782
+0e7f45b0a135c50e03fc75e622819aa54c02339474366c07129a5c42f4d8f2a0
+e23a38c34d47cddb09a417f2fb90923cbc508655de01235df27c7eb511aa56c2
+4fcd572512586843ac6a80816816e6dc852a36223bef2299c6a478155776e16b
+90031f71d6b7ae2b7227c5d8bbe57d1af693ccdb8ff883730cade61fde996bc0
+6566259af76a91b9b123665686d660ad9de6b292c5bc47eea9d2ea19a2adb49a
+7a1cb138d99e4473cf1a2c52579ddd41c04d302a9214c5ad7ac412be4794fef4
+15a5e11eeb5a84ec3a2e68811820e084a0256dd625e6f6a61241f8904bfefae0
+899891ffcc05cb11519da935e3b9e692ff22927da8c03efccd2aa9311e014ca7
+548db11e8e8c8c6aa5aa6507c70ffaf846a571e76fe421661dbd503141431262
+fd436c79108d8c7ca8c5eec44fd9eebf54181beed85aeff4e1b6ef6a9769e2f4
+ae97060298f4c7fede54865740cda6cbaf52ceb0c963e935fc894318b997add1
+977b0229fd0eb7d13ddabe457b7aa8a2d05b2b99421e01b4d775cdbcc820b1a8
+3ff39d8a7c4d6e4d96125ae2ad688f997e5f221b6ecc4134ce16f9613881f4d9
+049e171462845117cff0bb70669a62570e7361c5efb8aa5b6b858d8aed27c149
+70155e3406acdc035b1b9d1e49664a75c8d52b2e8777fe77aaa8ced567bbc904
+a44305a7b550
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+cleartomark
diff --git a/e2e-tests/cypress/fonts/Type1/UTI_____.afm b/e2e-tests/cypress/fonts/Type1/UTI_____.afm
new file mode 100644
index 00000000..058c34a2
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTI_____.afm
@@ -0,0 +1,1008 @@
+StartFontMetrics 2.0
+Comment Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.
+Comment Creation Date: Wed Oct 2 18:58:24 1991
+Comment UniqueID 36549
+Comment VMusage 34122 41014
+FontName Utopia-Italic
+FullName Utopia Italic
+FamilyName Utopia
+Weight Regular
+ItalicAngle -13
+IsFixedPitch false
+FontBBox -166 -250 1205 890
+UnderlinePosition -100
+UnderlineThickness 50
+Version 001.001
+Notice Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.
+EncodingScheme AdobeStandardEncoding
+CapHeight 692
+XHeight 502
+Ascender 742
+Descender -242
+StartCharMetrics 228
+C 32 ; WX 225 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 240 ; N exclam ; B 69 -12 325 707 ;
+C 34 ; WX 402 ; N quotedbl ; B 206 469 489 742 ;
+C 35 ; WX 530 ; N numbersign ; B 89 0 620 668 ;
+C 36 ; WX 530 ; N dollar ; B 66 -109 586 743 ;
+C 37 ; WX 826 ; N percent ; B 133 -25 830 702 ;
+C 38 ; WX 725 ; N ampersand ; B 95 -12 738 680 ;
+C 39 ; WX 216 ; N quoteright ; B 147 482 300 742 ;
+C 40 ; WX 350 ; N parenleft ; B 141 -128 493 692 ;
+C 41 ; WX 350 ; N parenright ; B -11 -128 341 692 ;
+C 42 ; WX 412 ; N asterisk ; B 141 356 493 707 ;
+C 43 ; WX 570 ; N plus ; B 93 0 577 490 ;
+C 44 ; WX 265 ; N comma ; B 46 -134 208 142 ;
+C 45 ; WX 392 ; N hyphen ; B 117 216 376 286 ;
+C 46 ; WX 265 ; N period ; B 82 -12 204 113 ;
+C 47 ; WX 270 ; N slash ; B 35 -15 376 707 ;
+C 48 ; WX 530 ; N zero ; B 95 -12 576 680 ;
+C 49 ; WX 530 ; N one ; B 109 0 464 680 ;
+C 50 ; WX 530 ; N two ; B 33 0 573 680 ;
+C 51 ; WX 530 ; N three ; B 54 -12 559 680 ;
+C 52 ; WX 530 ; N four ; B 67 0 544 668 ;
+C 53 ; WX 530 ; N five ; B 59 -12 585 668 ;
+C 54 ; WX 530 ; N six ; B 91 -12 586 680 ;
+C 55 ; WX 530 ; N seven ; B 165 -12 635 668 ;
+C 56 ; WX 530 ; N eight ; B 81 -12 570 680 ;
+C 57 ; WX 530 ; N nine ; B 86 -12 571 680 ;
+C 58 ; WX 265 ; N colon ; B 82 -12 283 490 ;
+C 59 ; WX 265 ; N semicolon ; B 46 -134 283 490 ;
+C 60 ; WX 570 ; N less ; B 86 1 564 497 ;
+C 61 ; WX 570 ; N equal ; B 93 111 577 389 ;
+C 62 ; WX 570 ; N greater ; B 86 1 564 497 ;
+C 63 ; WX 425 ; N question ; B 150 -12 491 707 ;
+C 64 ; WX 794 ; N at ; B 123 -15 832 707 ;
+C 65 ; WX 624 ; N A ; B -23 0 658 692 ;
+C 66 ; WX 632 ; N B ; B 38 0 671 692 ;
+C 67 ; WX 661 ; N C ; B 114 -15 758 707 ;
+C 68 ; WX 763 ; N D ; B 40 0 802 692 ;
+C 69 ; WX 596 ; N E ; B 38 0 692 692 ;
+C 70 ; WX 571 ; N F ; B 38 0 695 692 ;
+C 71 ; WX 709 ; N G ; B 114 -15 772 707 ;
+C 72 ; WX 775 ; N H ; B 40 0 892 692 ;
+C 73 ; WX 345 ; N I ; B 40 0 463 692 ;
+C 74 ; WX 352 ; N J ; B -43 -119 471 692 ;
+C 75 ; WX 650 ; N K ; B 40 -5 821 692 ;
+C 76 ; WX 565 ; N L ; B 40 0 603 692 ;
+C 77 ; WX 920 ; N M ; B 31 0 1037 692 ;
+C 78 ; WX 763 ; N N ; B 31 0 890 692 ;
+C 79 ; WX 753 ; N O ; B 114 -15 789 707 ;
+C 80 ; WX 614 ; N P ; B 40 0 681 692 ;
+C 81 ; WX 753 ; N Q ; B 114 -203 789 707 ;
+C 82 ; WX 640 ; N R ; B 40 0 677 692 ;
+C 83 ; WX 533 ; N S ; B 69 -15 577 707 ;
+C 84 ; WX 606 ; N T ; B 137 0 743 692 ;
+C 85 ; WX 794 ; N U ; B 166 -15 915 692 ;
+C 86 ; WX 637 ; N V ; B 131 0 821 692 ;
+C 87 ; WX 946 ; N W ; B 121 0 1110 692 ;
+C 88 ; WX 632 ; N X ; B -1 0 770 692 ;
+C 89 ; WX 591 ; N Y ; B 131 0 779 692 ;
+C 90 ; WX 622 ; N Z ; B 15 0 738 692 ;
+C 91 ; WX 330 ; N bracketleft ; B 104 -128 449 692 ;
+C 92 ; WX 390 ; N backslash ; B 124 -15 406 707 ;
+C 93 ; WX 330 ; N bracketright ; B 14 -128 359 692 ;
+C 94 ; WX 570 ; N asciicircum ; B 118 228 582 668 ;
+C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ;
+C 96 ; WX 216 ; N quoteleft ; B 165 488 318 748 ;
+C 97 ; WX 561 ; N a ; B 66 -12 598 502 ;
+C 98 ; WX 559 ; N b ; B 82 -12 592 742 ;
+C 99 ; WX 441 ; N c ; B 81 -12 500 502 ;
+C 100 ; WX 587 ; N d ; B 72 -12 647 742 ;
+C 101 ; WX 453 ; N e ; B 80 -12 506 502 ;
+C 102 ; WX 315 ; N f ; B -72 -242 539 742 ; L i fi ; L l fl ;
+C 103 ; WX 499 ; N g ; B 30 -242 608 512 ;
+C 104 ; WX 607 ; N h ; B 92 -12 623 742 ;
+C 105 ; WX 317 ; N i ; B 114 -12 363 715 ;
+C 106 ; WX 309 ; N j ; B -60 -242 365 715 ;
+C 107 ; WX 545 ; N k ; B 92 -12 602 742 ;
+C 108 ; WX 306 ; N l ; B 111 -12 366 742 ;
+C 109 ; WX 912 ; N m ; B 98 -12 929 502 ;
+C 110 ; WX 618 ; N n ; B 98 -12 635 502 ;
+C 111 ; WX 537 ; N o ; B 84 -12 557 502 ;
+C 112 ; WX 590 ; N p ; B 57 -242 621 502 ;
+C 113 ; WX 559 ; N q ; B 73 -242 602 525 ;
+C 114 ; WX 402 ; N r ; B 104 -12 483 502 ;
+C 115 ; WX 389 ; N s ; B 54 -12 432 502 ;
+C 116 ; WX 341 ; N t ; B 119 -12 439 616 ;
+C 117 ; WX 618 ; N u ; B 124 -12 644 502 ;
+C 118 ; WX 510 ; N v ; B 119 -12 563 502 ;
+C 119 ; WX 785 ; N w ; B 122 -12 843 502 ;
+C 120 ; WX 516 ; N x ; B 31 -12 566 502 ;
+C 121 ; WX 468 ; N y ; B -5 -242 540 502 ;
+C 122 ; WX 468 ; N z ; B 39 -12 518 490 ;
+C 123 ; WX 340 ; N braceleft ; B 135 -128 458 692 ;
+C 124 ; WX 270 ; N bar ; B 165 -250 233 750 ;
+C 125 ; WX 340 ; N braceright ; B 15 -128 337 692 ;
+C 126 ; WX 570 ; N asciitilde ; B 133 176 557 318 ;
+C 161 ; WX 240 ; N exclamdown ; B 17 -217 273 502 ;
+C 162 ; WX 530 ; N cent ; B 129 -21 598 669 ;
+C 163 ; WX 530 ; N sterling ; B 44 0 584 680 ;
+C 164 ; WX 100 ; N fraction ; B -166 -24 404 698 ;
+C 165 ; WX 530 ; N yen ; B 107 0 680 668 ;
+C 166 ; WX 530 ; N florin ; B 39 -135 623 691 ;
+C 167 ; WX 530 ; N section ; B 90 -115 568 707 ;
+C 168 ; WX 530 ; N currency ; B 91 90 571 578 ;
+C 169 ; WX 216 ; N quotesingle ; B 196 469 309 742 ;
+C 170 ; WX 402 ; N quotedblleft ; B 169 488 508 748 ;
+C 171 ; WX 462 ; N guillemotleft ; B 114 41 505 435 ;
+C 172 ; WX 277 ; N guilsinglleft ; B 106 41 302 435 ;
+C 173 ; WX 277 ; N guilsinglright ; B 79 41 275 435 ;
+C 174 ; WX 607 ; N fi ; B -72 -242 624 742 ;
+C 175 ; WX 603 ; N fl ; B -72 -242 663 742 ;
+C 177 ; WX 500 ; N endash ; B 47 221 559 279 ;
+C 178 ; WX 500 ; N dagger ; B 136 -125 554 717 ;
+C 179 ; WX 490 ; N daggerdbl ; B 74 -119 544 717 ;
+C 180 ; WX 265 ; N periodcentered ; B 124 187 246 312 ;
+C 182 ; WX 560 ; N paragraph ; B 144 -101 672 692 ;
+C 183 ; WX 500 ; N bullet ; B 145 192 464 512 ;
+C 184 ; WX 216 ; N quotesinglbase ; B 28 -109 181 151 ;
+C 185 ; WX 402 ; N quotedblbase ; B 28 -109 367 151 ;
+C 186 ; WX 402 ; N quotedblright ; B 142 484 481 744 ;
+C 187 ; WX 462 ; N guillemotright ; B 64 41 455 435 ;
+C 188 ; WX 1000 ; N ellipsis ; B 120 -12 908 113 ;
+C 189 ; WX 1200 ; N perthousand ; B 133 -25 1205 702 ;
+C 191 ; WX 425 ; N questiondown ; B 38 -217 379 502 ;
+C 193 ; WX 400 ; N grave ; B 181 542 403 723 ;
+C 194 ; WX 400 ; N acute ; B 249 542 471 723 ;
+C 195 ; WX 400 ; N circumflex ; B 222 546 519 720 ;
+C 196 ; WX 400 ; N tilde ; B 172 563 527 682 ;
+C 197 ; WX 400 ; N macron ; B 228 597 524 656 ;
+C 198 ; WX 400 ; N breve ; B 262 568 536 698 ;
+C 199 ; WX 402 ; N dotaccent ; B 287 570 394 680 ;
+C 200 ; WX 400 ; N dieresis ; B 207 572 522 682 ;
+C 202 ; WX 400 ; N ring ; B 221 550 437 752 ;
+C 203 ; WX 400 ; N cedilla ; B 97 -230 276 0 ;
+C 205 ; WX 400 ; N hungarumlaut ; B 211 546 490 750 ;
+C 206 ; WX 350 ; N ogonek ; B 103 -219 283 0 ;
+C 207 ; WX 400 ; N caron ; B 248 557 545 731 ;
+C 208 ; WX 1000 ; N emdash ; B 47 221 1059 279 ;
+C 225 ; WX 880 ; N AE ; B -53 0 976 692 ;
+C 227 ; WX 425 ; N ordfeminine ; B 112 265 495 590 ;
+C 232 ; WX 571 ; N Lslash ; B 46 0 609 692 ;
+C 233 ; WX 753 ; N Oslash ; B 114 -45 789 736 ;
+C 234 ; WX 1020 ; N OE ; B 114 0 1116 692 ;
+C 235 ; WX 389 ; N ordmasculine ; B 121 265 455 590 ;
+C 241 ; WX 779 ; N ae ; B 69 -12 832 514 ;
+C 245 ; WX 317 ; N dotlessi ; B 114 -12 334 502 ;
+C 248 ; WX 318 ; N lslash ; B 80 -12 411 742 ;
+C 249 ; WX 537 ; N oslash ; B 84 -39 557 529 ;
+C 250 ; WX 806 ; N oe ; B 84 -12 859 502 ;
+C 251 ; WX 577 ; N germandbls ; B -72 -242 665 742 ;
+C -1 ; WX 370 ; N onesuperior ; B 125 272 361 680 ;
+C -1 ; WX 570 ; N minus ; B 93 221 577 279 ;
+C -1 ; WX 400 ; N degree ; B 187 404 463 680 ;
+C -1 ; WX 537 ; N oacute ; B 84 -12 557 723 ;
+C -1 ; WX 753 ; N Odieresis ; B 114 -15 789 848 ;
+C -1 ; WX 537 ; N odieresis ; B 84 -12 567 682 ;
+C -1 ; WX 596 ; N Eacute ; B 38 0 692 890 ;
+C -1 ; WX 618 ; N ucircumflex ; B 124 -12 644 720 ;
+C -1 ; WX 890 ; N onequarter ; B 132 -24 840 698 ;
+C -1 ; WX 570 ; N logicalnot ; B 93 102 577 389 ;
+C -1 ; WX 596 ; N Ecircumflex ; B 38 0 692 876 ;
+C -1 ; WX 890 ; N onehalf ; B 106 -24 847 698 ;
+C -1 ; WX 753 ; N Otilde ; B 114 -15 789 842 ;
+C -1 ; WX 618 ; N uacute ; B 124 -12 644 723 ;
+C -1 ; WX 453 ; N eacute ; B 80 -12 518 723 ;
+C -1 ; WX 317 ; N iacute ; B 114 -12 415 723 ;
+C -1 ; WX 596 ; N Egrave ; B 38 0 692 890 ;
+C -1 ; WX 317 ; N icircumflex ; B 114 -12 418 720 ;
+C -1 ; WX 618 ; N mu ; B 46 -232 644 502 ;
+C -1 ; WX 270 ; N brokenbar ; B 165 -175 233 675 ;
+C -1 ; WX 584 ; N thorn ; B 51 -242 615 700 ;
+C -1 ; WX 624 ; N Aring ; B -23 0 658 861 ;
+C -1 ; WX 468 ; N yacute ; B -5 -242 540 723 ;
+C -1 ; WX 591 ; N Ydieresis ; B 131 0 779 848 ;
+C -1 ; WX 1100 ; N trademark ; B 126 277 1129 692 ;
+C -1 ; WX 836 ; N registered ; B 126 -15 854 707 ;
+C -1 ; WX 537 ; N ocircumflex ; B 84 -12 557 720 ;
+C -1 ; WX 624 ; N Agrave ; B -23 0 658 890 ;
+C -1 ; WX 533 ; N Scaron ; B 69 -15 596 888 ;
+C -1 ; WX 794 ; N Ugrave ; B 166 -15 915 890 ;
+C -1 ; WX 596 ; N Edieresis ; B 38 0 692 848 ;
+C -1 ; WX 794 ; N Uacute ; B 166 -15 915 890 ;
+C -1 ; WX 537 ; N otilde ; B 84 -12 560 682 ;
+C -1 ; WX 618 ; N ntilde ; B 98 -12 635 682 ;
+C -1 ; WX 468 ; N ydieresis ; B -5 -242 556 682 ;
+C -1 ; WX 624 ; N Aacute ; B -23 0 658 890 ;
+C -1 ; WX 537 ; N eth ; B 82 -12 556 742 ;
+C -1 ; WX 561 ; N acircumflex ; B 66 -12 598 720 ;
+C -1 ; WX 561 ; N aring ; B 66 -12 598 752 ;
+C -1 ; WX 753 ; N Ograve ; B 114 -15 789 890 ;
+C -1 ; WX 441 ; N ccedilla ; B 81 -230 500 502 ;
+C -1 ; WX 570 ; N multiply ; B 123 22 567 478 ;
+C -1 ; WX 570 ; N divide ; B 93 25 577 475 ;
+C -1 ; WX 370 ; N twosuperior ; B 70 272 434 680 ;
+C -1 ; WX 763 ; N Ntilde ; B 31 0 890 842 ;
+C -1 ; WX 618 ; N ugrave ; B 124 -12 644 723 ;
+C -1 ; WX 794 ; N Ucircumflex ; B 166 -15 915 876 ;
+C -1 ; WX 624 ; N Atilde ; B -23 0 658 842 ;
+C -1 ; WX 468 ; N zcaron ; B 39 -12 519 731 ;
+C -1 ; WX 317 ; N idieresis ; B 114 -12 433 682 ;
+C -1 ; WX 624 ; N Acircumflex ; B -23 0 658 876 ;
+C -1 ; WX 345 ; N Icircumflex ; B 40 0 488 876 ;
+C -1 ; WX 591 ; N Yacute ; B 131 0 779 890 ;
+C -1 ; WX 753 ; N Oacute ; B 114 -15 789 890 ;
+C -1 ; WX 624 ; N Adieresis ; B -23 0 658 848 ;
+C -1 ; WX 622 ; N Zcaron ; B 15 0 738 888 ;
+C -1 ; WX 561 ; N agrave ; B 66 -12 598 723 ;
+C -1 ; WX 370 ; N threesuperior ; B 94 265 424 680 ;
+C -1 ; WX 537 ; N ograve ; B 84 -12 557 723 ;
+C -1 ; WX 890 ; N threequarters ; B 140 -24 851 698 ;
+C -1 ; WX 770 ; N Eth ; B 47 0 809 692 ;
+C -1 ; WX 570 ; N plusminus ; B 93 0 577 556 ;
+C -1 ; WX 618 ; N udieresis ; B 124 -12 644 682 ;
+C -1 ; WX 453 ; N edieresis ; B 80 -12 525 682 ;
+C -1 ; WX 561 ; N aacute ; B 66 -12 598 723 ;
+C -1 ; WX 317 ; N igrave ; B 114 -12 352 723 ;
+C -1 ; WX 345 ; N Idieresis ; B 40 0 496 848 ;
+C -1 ; WX 561 ; N adieresis ; B 66 -12 598 682 ;
+C -1 ; WX 345 ; N Iacute ; B 40 0 486 890 ;
+C -1 ; WX 836 ; N copyright ; B 126 -15 854 707 ;
+C -1 ; WX 345 ; N Igrave ; B 40 0 463 890 ;
+C -1 ; WX 661 ; N Ccedilla ; B 114 -230 758 707 ;
+C -1 ; WX 389 ; N scaron ; B 54 -12 492 731 ;
+C -1 ; WX 453 ; N egrave ; B 80 -12 506 723 ;
+C -1 ; WX 753 ; N Ocircumflex ; B 114 -15 789 876 ;
+C -1 ; WX 604 ; N Thorn ; B 40 0 651 692 ;
+C -1 ; WX 561 ; N atilde ; B 66 -12 598 682 ;
+C -1 ; WX 794 ; N Udieresis ; B 166 -15 915 848 ;
+C -1 ; WX 453 ; N ecircumflex ; B 80 -12 510 720 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 690
+
+KPX A y -20
+KPX A x 10
+KPX A w -30
+KPX A v -30
+KPX A u -10
+KPX A t -6
+KPX A s 15
+KPX A r -12
+KPX A quoteright -110
+KPX A quotedblright -110
+KPX A q 10
+KPX A p -12
+KPX A o -10
+KPX A n -18
+KPX A m -18
+KPX A l -18
+KPX A j 6
+KPX A h -6
+KPX A d 10
+KPX A c -6
+KPX A b -6
+KPX A a 12
+KPX A Y -76
+KPX A X -8
+KPX A W -80
+KPX A V -90
+KPX A U -60
+KPX A T -72
+KPX A Q -30
+KPX A O -30
+KPX A G -30
+KPX A C -30
+
+KPX B y -6
+KPX B u -20
+KPX B r -15
+KPX B quoteright -40
+KPX B quotedblright -30
+KPX B o 6
+KPX B l -20
+KPX B k -15
+KPX B i -12
+KPX B h -15
+KPX B e 6
+KPX B a 12
+KPX B W -20
+KPX B V -50
+KPX B U -50
+KPX B T -20
+
+KPX C z -6
+KPX C y -18
+KPX C u -18
+KPX C quotedblright 20
+KPX C i -5
+KPX C e -6
+KPX C a -6
+
+KPX D y 18
+KPX D u -10
+KPX D quoteright -40
+KPX D quotedblright -50
+KPX D period -30
+KPX D o 6
+KPX D i 6
+KPX D h -25
+KPX D e 6
+KPX D comma -20
+KPX D a 6
+KPX D Y -70
+KPX D W -50
+KPX D V -60
+
+KPX E z -6
+KPX E y -18
+KPX E x 5
+KPX E w -20
+KPX E v -18
+KPX E u -24
+KPX E t -18
+KPX E s 5
+KPX E r -6
+KPX E quoteright 10
+KPX E quotedblright 10
+KPX E q 10
+KPX E period 10
+KPX E p -12
+KPX E o -6
+KPX E n -12
+KPX E m -12
+KPX E l -12
+KPX E k -10
+KPX E j -6
+KPX E i -12
+KPX E g -12
+KPX E e 5
+KPX E d 10
+KPX E comma 10
+KPX E b -6
+
+KPX F y -12
+KPX F u -30
+KPX F r -18
+KPX F quoteright 15
+KPX F quotedblright 35
+KPX F period -180
+KPX F o -30
+KPX F l -6
+KPX F i -12
+KPX F e -30
+KPX F comma -170
+KPX F a -30
+KPX F A -45
+
+KPX G y -16
+KPX G u -22
+KPX G r -22
+KPX G quoteright -20
+KPX G quotedblright -20
+KPX G o 10
+KPX G n -22
+KPX G l -24
+KPX G i -12
+KPX G h -18
+KPX G e 10
+KPX G a 5
+
+KPX H y -18
+KPX H u -30
+KPX H quoteright 10
+KPX H quotedblright 10
+KPX H o -12
+KPX H i -12
+KPX H e -12
+KPX H a -12
+
+KPX I z -20
+KPX I y -6
+KPX I x -6
+KPX I w -30
+KPX I v -30
+KPX I u -30
+KPX I t -18
+KPX I s -18
+KPX I r -12
+KPX I quoteright 10
+KPX I quotedblright 10
+KPX I p -18
+KPX I o -12
+KPX I n -18
+KPX I m -18
+KPX I l -6
+KPX I k -6
+KPX I g -12
+KPX I f -6
+KPX I d -6
+KPX I c -12
+KPX I b -6
+KPX I a -6
+
+KPX J y -12
+KPX J u -36
+KPX J quoteright 6
+KPX J quotedblright 15
+KPX J o -36
+KPX J i -30
+KPX J e -36
+KPX J braceright 10
+KPX J a -36
+
+KPX K y -40
+KPX K w -30
+KPX K v -20
+KPX K u -24
+KPX K r -12
+KPX K quoteright 25
+KPX K quotedblright 40
+KPX K o -24
+KPX K n -18
+KPX K i -6
+KPX K h 6
+KPX K e -12
+KPX K a -6
+KPX K Q -24
+KPX K O -24
+KPX K G -24
+KPX K C -24
+
+KPX L y -55
+KPX L w -30
+KPX L u -18
+KPX L quoteright -110
+KPX L quotedblright -110
+KPX L l -16
+KPX L j -18
+KPX L i -18
+KPX L a 10
+KPX L Y -80
+KPX L W -90
+KPX L V -110
+KPX L U -42
+KPX L T -80
+KPX L Q -48
+KPX L O -48
+KPX L G -48
+KPX L C -48
+KPX L A 30
+
+KPX M y -18
+KPX M u -24
+KPX M quoteright 6
+KPX M quotedblright 15
+KPX M o -25
+KPX M n -12
+KPX M j -18
+KPX M i -12
+KPX M e -20
+KPX M d -10
+KPX M c -20
+KPX M a -6
+
+KPX N y -18
+KPX N u -24
+KPX N quoteright 10
+KPX N quotedblright 10
+KPX N o -25
+KPX N i -12
+KPX N e -20
+KPX N a -22
+
+KPX O z -6
+KPX O y 12
+KPX O w -10
+KPX O v -10
+KPX O u -6
+KPX O t -6
+KPX O s -6
+KPX O r -6
+KPX O quoteright -40
+KPX O quotedblright -40
+KPX O q 5
+KPX O period -20
+KPX O p -6
+KPX O n -6
+KPX O m -6
+KPX O l -20
+KPX O k -10
+KPX O j -6
+KPX O h -10
+KPX O g -6
+KPX O e 5
+KPX O d 6
+KPX O comma -10
+KPX O c 5
+KPX O b -6
+KPX O a 5
+KPX O Y -75
+KPX O X -30
+KPX O W -40
+KPX O V -60
+KPX O T -48
+KPX O A -18
+
+KPX P y 6
+KPX P u -18
+KPX P t -6
+KPX P s -24
+KPX P r -6
+KPX P period -220
+KPX P o -24
+KPX P n -12
+KPX P l -25
+KPX P h -15
+KPX P e -24
+KPX P comma -220
+KPX P a -24
+KPX P I -30
+KPX P H -30
+KPX P E -30
+KPX P A -75
+
+KPX Q u -6
+KPX Q quoteright -40
+KPX Q quotedblright -50
+KPX Q a -6
+KPX Q Y -70
+KPX Q X -12
+KPX Q W -35
+KPX Q V -60
+KPX Q U -35
+KPX Q T -36
+KPX Q A -18
+
+KPX R y -14
+KPX R u -12
+KPX R quoteright -30
+KPX R quotedblright -20
+KPX R o -12
+KPX R hyphen -20
+KPX R e -12
+KPX R Y -50
+KPX R W -30
+KPX R V -40
+KPX R U -40
+KPX R T -30
+KPX R Q -10
+KPX R O -10
+KPX R G -10
+KPX R C -10
+KPX R A -6
+
+KPX S y -30
+KPX S w -30
+KPX S v -30
+KPX S u -18
+KPX S t -30
+KPX S r -20
+KPX S quoteright -38
+KPX S quotedblright -30
+KPX S p -18
+KPX S n -24
+KPX S m -24
+KPX S l -30
+KPX S k -24
+KPX S j -25
+KPX S i -30
+KPX S h -30
+KPX S e -6
+
+KPX T z -70
+KPX T y -60
+KPX T w -64
+KPX T u -74
+KPX T semicolon -36
+KPX T s -72
+KPX T r -64
+KPX T quoteright 45
+KPX T quotedblright 50
+KPX T period -100
+KPX T parenright 54
+KPX T o -90
+KPX T m -64
+KPX T i -34
+KPX T hyphen -100
+KPX T endash -60
+KPX T emdash -60
+KPX T e -90
+KPX T comma -110
+KPX T colon -10
+KPX T bracketright 45
+KPX T braceright 54
+KPX T a -90
+KPX T Y 12
+KPX T X 18
+KPX T W 6
+KPX T T 18
+KPX T Q -12
+KPX T O -12
+KPX T G -12
+KPX T C -12
+KPX T A -56
+
+KPX U z -30
+KPX U x -40
+KPX U t -24
+KPX U s -30
+KPX U r -30
+KPX U quoteright 10
+KPX U quotedblright 10
+KPX U p -40
+KPX U n -45
+KPX U m -45
+KPX U l -12
+KPX U k -12
+KPX U i -24
+KPX U h -6
+KPX U g -30
+KPX U d -40
+KPX U c -35
+KPX U b -6
+KPX U a -40
+KPX U A -45
+
+KPX V y -46
+KPX V u -42
+KPX V semicolon -35
+KPX V r -50
+KPX V quoteright 75
+KPX V quotedblright 70
+KPX V period -130
+KPX V parenright 64
+KPX V o -62
+KPX V i -10
+KPX V hyphen -60
+KPX V endash -20
+KPX V emdash -20
+KPX V e -52
+KPX V comma -120
+KPX V colon -18
+KPX V bracketright 64
+KPX V braceright 64
+KPX V a -60
+KPX V T 6
+KPX V A -70
+
+KPX W y -42
+KPX W u -56
+KPX W t -20
+KPX W semicolon -28
+KPX W r -40
+KPX W quoteright 55
+KPX W quotedblright 60
+KPX W period -108
+KPX W parenright 64
+KPX W o -60
+KPX W m -35
+KPX W i -10
+KPX W hyphen -40
+KPX W endash -2
+KPX W emdash -10
+KPX W e -54
+KPX W d -50
+KPX W comma -108
+KPX W colon -28
+KPX W bracketright 55
+KPX W braceright 64
+KPX W a -60
+KPX W T 12
+KPX W Q -10
+KPX W O -10
+KPX W G -10
+KPX W C -10
+KPX W A -58
+
+KPX X y -35
+KPX X u -30
+KPX X r -6
+KPX X quoteright 35
+KPX X quotedblright 15
+KPX X i -6
+KPX X e -10
+KPX X a 5
+KPX X Y -6
+KPX X W -6
+KPX X Q -30
+KPX X O -30
+KPX X G -30
+KPX X C -30
+KPX X A -18
+
+KPX Y v -50
+KPX Y u -58
+KPX Y t -32
+KPX Y semicolon -36
+KPX Y quoteright 65
+KPX Y quotedblright 70
+KPX Y q -100
+KPX Y period -90
+KPX Y parenright 60
+KPX Y o -72
+KPX Y l 10
+KPX Y hyphen -95
+KPX Y endash -20
+KPX Y emdash -20
+KPX Y e -72
+KPX Y d -80
+KPX Y comma -80
+KPX Y colon -36
+KPX Y bracketright 64
+KPX Y braceright 75
+KPX Y a -82
+KPX Y Y 12
+KPX Y X 12
+KPX Y W 12
+KPX Y V 6
+KPX Y T 25
+KPX Y Q -5
+KPX Y O -5
+KPX Y G -5
+KPX Y C -5
+KPX Y A -36
+
+KPX Z y -36
+KPX Z w -36
+KPX Z u -12
+KPX Z quoteright 10
+KPX Z quotedblright 10
+KPX Z o -6
+KPX Z i -12
+KPX Z e -6
+KPX Z a -6
+KPX Z Q -30
+KPX Z O -30
+KPX Z G -30
+KPX Z C -30
+KPX Z A 12
+
+KPX a quoteright -40
+KPX a quotedblright -40
+
+KPX b y -6
+KPX b w -15
+KPX b v -15
+KPX b quoteright -50
+KPX b quotedblright -50
+KPX b period -40
+KPX b comma -30
+
+KPX braceleft Y 64
+KPX braceleft W 64
+KPX braceleft V 64
+KPX braceleft T 54
+KPX braceleft J 80
+
+KPX bracketleft Y 64
+KPX bracketleft W 64
+KPX bracketleft V 64
+KPX bracketleft T 54
+KPX bracketleft J 80
+
+KPX c quoteright -20
+KPX c quotedblright -20
+
+KPX colon space -30
+
+KPX comma space -40
+KPX comma quoteright -80
+KPX comma quotedblright -80
+
+KPX d quoteright -12
+KPX d quotedblright -12
+
+KPX e x -10
+KPX e w -10
+KPX e quoteright -30
+KPX e quotedblright -30
+
+KPX f quoteright 110
+KPX f quotedblright 110
+KPX f period -20
+KPX f parenright 100
+KPX f comma -20
+KPX f bracketright 90
+KPX f braceright 90
+
+KPX g y 30
+KPX g p 12
+KPX g f 42
+
+KPX h quoteright -80
+KPX h quotedblright -80
+
+KPX j quoteright -20
+KPX j quotedblright -20
+KPX j period -35
+KPX j comma -20
+
+KPX k quoteright -30
+KPX k quotedblright -50
+
+KPX m quoteright -80
+KPX m quotedblright -80
+
+KPX n quoteright -80
+KPX n quotedblright -80
+
+KPX o z -10
+KPX o y -20
+KPX o x -20
+KPX o w -30
+KPX o v -35
+KPX o quoteright -60
+KPX o quotedblright -50
+KPX o period -30
+KPX o comma -20
+
+KPX p z -10
+KPX p w -15
+KPX p quoteright -50
+KPX p quotedblright -70
+KPX p period -30
+KPX p comma -20
+
+KPX parenleft Y 75
+KPX parenleft W 75
+KPX parenleft V 75
+KPX parenleft T 64
+KPX parenleft J 80
+
+KPX period space -40
+KPX period quoteright -80
+KPX period quotedblright -80
+
+KPX q quoteright -20
+KPX q quotedblright -30
+KPX q period -20
+KPX q comma -10
+
+KPX quotedblleft z -30
+KPX quotedblleft x -40
+KPX quotedblleft w -12
+KPX quotedblleft v -12
+KPX quotedblleft u -12
+KPX quotedblleft t -12
+KPX quotedblleft s -30
+KPX quotedblleft r -12
+KPX quotedblleft q -40
+KPX quotedblleft p -12
+KPX quotedblleft o -30
+KPX quotedblleft n -12
+KPX quotedblleft m -12
+KPX quotedblleft l 10
+KPX quotedblleft k 10
+KPX quotedblleft h 10
+KPX quotedblleft g -30
+KPX quotedblleft e -40
+KPX quotedblleft d -40
+KPX quotedblleft c -40
+KPX quotedblleft b 24
+KPX quotedblleft a -60
+KPX quotedblleft Y 12
+KPX quotedblleft X 28
+KPX quotedblleft W 28
+KPX quotedblleft V 28
+KPX quotedblleft T 36
+KPX quotedblleft A -90
+
+KPX quotedblright space -40
+KPX quotedblright period -100
+KPX quotedblright comma -100
+
+KPX quoteleft z -30
+KPX quoteleft y -10
+KPX quoteleft x -40
+KPX quoteleft w -12
+KPX quoteleft v -12
+KPX quoteleft u -12
+KPX quoteleft t -12
+KPX quoteleft s -30
+KPX quoteleft r -12
+KPX quoteleft quoteleft -18
+KPX quoteleft q -30
+KPX quoteleft p -12
+KPX quoteleft o -30
+KPX quoteleft n -12
+KPX quoteleft m -12
+KPX quoteleft l 10
+KPX quoteleft k 10
+KPX quoteleft h 10
+KPX quoteleft g -30
+KPX quoteleft e -30
+KPX quoteleft d -30
+KPX quoteleft c -30
+KPX quoteleft b 24
+KPX quoteleft a -45
+KPX quoteleft Y 12
+KPX quoteleft X 28
+KPX quoteleft W 28
+KPX quoteleft V 28
+KPX quoteleft T 36
+KPX quoteleft A -90
+
+KPX quoteright v -35
+KPX quoteright t -35
+KPX quoteright space -40
+KPX quoteright s -55
+KPX quoteright r -25
+KPX quoteright quoteright -18
+KPX quoteright period -100
+KPX quoteright m -25
+KPX quoteright l -12
+KPX quoteright d -70
+KPX quoteright comma -100
+
+KPX r y 18
+KPX r w 6
+KPX r v 6
+KPX r t 8
+KPX r quotedblright -15
+KPX r q -24
+KPX r period -120
+KPX r o -6
+KPX r l -20
+KPX r k -20
+KPX r hyphen -30
+KPX r h -20
+KPX r f 8
+KPX r emdash -20
+KPX r e -26
+KPX r d -26
+KPX r comma -110
+KPX r c -12
+KPX r a -20
+
+KPX s quoteright -40
+KPX s quotedblright -45
+
+KPX semicolon space -30
+
+KPX space quotesinglbase -30
+KPX space quoteleft -40
+KPX space quotedblleft -40
+KPX space quotedblbase -30
+KPX space Y -70
+KPX space W -70
+KPX space V -70
+
+KPX t quoteright 10
+KPX t quotedblright -10
+
+KPX u quoteright -55
+KPX u quotedblright -50
+
+KPX v quoteright -20
+KPX v quotedblright -30
+KPX v q -6
+KPX v period -70
+KPX v o -6
+KPX v e -6
+KPX v d -6
+KPX v comma -70
+KPX v c -6
+KPX v a -6
+
+KPX w quoteright -20
+KPX w quotedblright -30
+KPX w period -62
+KPX w comma -62
+
+KPX x y 12
+KPX x w -6
+KPX x quoteright -40
+KPX x quotedblright -50
+KPX x q -6
+KPX x o -6
+KPX x e -6
+KPX x d -6
+KPX x c -6
+
+KPX y quoteright -10
+KPX y quotedblright -20
+KPX y period -70
+KPX y emdash 40
+KPX y comma -60
+
+KPX z quoteright -40
+KPX z quotedblright -50
+KPX z o -6
+KPX z e -6
+KPX z d -6
+KPX z c -6
+EndKernPairs
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/UTI_____.pfa b/e2e-tests/cypress/fonts/Type1/UTI_____.pfa
new file mode 100644
index 00000000..de07ce5d
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTI_____.pfa
@@ -0,0 +1,1165 @@
+%!PS-AdobeFont-1.0: Utopia-Italic 001.001
+%%CreationDate: Wed Oct 2 18:58:18 1991
+%%VMusage: 34122 41014
+%% Utopia is a registered trademark of Adobe Systems Incorporated.
+11 dict begin
+/FontInfo 10 dict dup begin
+/version (001.001) readonly def
+/Notice (Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.) readonly def
+/FullName (Utopia Italic) readonly def
+/FamilyName (Utopia) readonly def
+/Weight (Regular) readonly def
+/ItalicAngle -13 def
+/isFixedPitch false def
+/UnderlinePosition -100 def
+/UnderlineThickness 50 def
+end readonly def
+/FontName /Utopia-Italic def
+/Encoding StandardEncoding def
+/PaintType 0 def
+/FontType 1 def
+/FontMatrix [0.001 0 0 0.001 0 0] readonly def
+/UniqueID 36549 def
+/FontBBox{-166 -250 1205 890}readonly def
+currentdict end
+currentfile eexec
+a9994c496574b9cb23f8cbcd64a4a16861c70f0f2da82f0c7b06ceacd6521bb0
+cc26f1cf47836cbab75757d7a81793f43e56cc8f22f926da04d715ab6ff2e257
+5a135dabbaaa58f31f548cbe8a76c69e2402589b9e5e46e757f06bf2eddbbe6e
+e48a624cbe1c4840a338e90f7efbe9f2194aee1c869bc4cd76e2f1937d78e207
+d8149c05b50ef0bb361f5905977c40be7d4dad07b54e087896acda5aa70ab803
+9dfc55a73134c7f1c9be9028d3ec6ccb0fbb8fda52bba4d7551a8124e68481d0
+775ed7f8ec68d8073bfdd3b67f72ec68634ffc57727e16b9aba841546ae54d99
+b60e227682315510edda09bd6ead4d1652f449d737592c44bb178689a3840169
+53d899636686efc6f838b19f966be5f833f5e7d41af38a899df96fdce1ebc116
+9b0ec87c930d9fdcbab7e74880e9693a24de9c67dbb0dd75b3dbd113b079c490
+60018433ccc06ac1df33fc090c4642fd5225fa0a188c131974ca8820319704dd
+14b1719b958779d1475d92e712322eb2e6a79d652c4e3f5833aca3091675fef3
+fde103446e565428267b009d87bb7d6bc40f46b498b19bf1223aba33079e41ba
+a9561254a9df97a48d71015b6a24cf952539f664f172d565f39a55e063154d80
+a960cd56c34011fe26e8849c6677427b2c7e728cd16272363d76661b3ab8bb2c
+85c20b747519a4431421d4c83783feb896c45688ff7f824381cff5654a1bfc68
+6f4894e1d265455fc00064d08c37dc3a47b59a5b2d14dd893a2b871492b07c4a
+591695c8df012ef46a750d6cb7fd86696e7ead280648bd737e9ea490140f3b1b
+1dffab2ff8e085a8b1c78e4b9ae9cee277a29233cec5ab2588d1cdaf4bbb6512
+e1b1476c68fd3db18ecc2536968ebebf4333c2c3709ca354f21fffc38c3858c0
+561378a701158ed0201a3b236ca9cb7f7528d1e5e07a9bfab04f8d0fa3d12405
+5ba2fa9dff76f41b980bfb36e8db1300f7172c902dcb9f5f0bba3cb9c212bf31
+76cb802ddebe556f4610021fa3700d476a2be2a66ae6c658fd0a95bc1b9629ad
+03fe4e775ffdfa9f87a274fd904c0db5beae258f9af213f257db2be0265b964a
+681fc4d2855f5356fc87576d847db274fed9ca5bb4961ffcf53f28b673f50e31
+c3c634604e70fd0895af711c9fa302572eede069f65502f68884a90d9f1ed7f1
+cdd95fc73ca6ae925cce4e01e49934539739f082f0ff1398deafda84c3d9b1e0
+cafdefe5bf321670bfa286078bc066bb1895b1a3f0253abc079b753c8f020fae
+ceb70acca52c4fd011825ba3ab32ce9c7ec263b4828d979b57588c48de210085
+e56eeb002dcecc967c95f219728511ccc6c51817125e1afdd3e4a9b35c8e5551
+06a026900808425393fa10ea4b6f4513f0af047d326d80462d36c2c7d033125c
+ab05bd0d3058b69f11a1d16f23345b3c97e444ffcfb4b3496743d9f6abca53e1
+b702a359cedda4d78e1a771989290f86ceee70502c574d6582a64045acbc33ed
+436871e9beaaefb12174806003037e8616b5f12ff470cc6e08b0af6789b9614e
+6ce98441b72e79b0c613e4a01835cb4d61c9cdecde9a66184af5876f0c6bea8d
+8280472a4c6a2f38fa95b7c475ce75f8acd6850e8e82dcbbbb96e7b389612cfb
+082c280fc4babf5da29e26f1f163d5ce353c9eb61122a10634ca3224261089f5
+e999b1d591d92216afe16a409f7735043885cd0218b4bb7289fb4547fdca93a1
+30280807d3c5fe5abf6d6ce88af04beb42ee031be89efad865f05daadc0ec4f7
+871c1a8d78e51a453a8c4b0ea780268639009c2ed6741fef8c598248ea9a2c5d
+f3ab0401a05b0fc35f66bcd6c812087ac03be6cee22ff97df7152681279de3bc
+be0a5c3574240a561f667c87a9fd065b788c9bf4e38dce8abc94f29acfef1933
+6568527336b48570be215a8ed6c54642ff28aa5edebe0228873c55a65430ff76
+8502e836e97ae8f2f6ac4dc5eee76beba71cb16389399a34535296a0175974a8
+c44918f7c668fcb07a72c0771fa8c49ab7d238611d8c2eb17a2a6f7bdce45b45
+2616dd51ec465225f83e886237b273488f4ec220d1837272df8a504273fdd95d
+ad88d197a3a6e92519eef80d7c18b31df15b9530d96f9982c1f7f870a95272c1
+325b394ec41c9e7fd2f13ce4adda1cc0cca0ad9b068e4d1c92e17d1ebbd4e365
+f7f4a9c547a81470f3ea6386dae456937b302a2e6f9c4553a39ea280895ad232
+57b00bf0b1c9c38746dd5438067ea5ba365b1a4509a29bf938f3c028cd2d0dbe
+441c1e881135b7abd353f1b14f225001812697df59ae92b11a92eb68b41a6e62
+637f097d4b079e266a81e6bc30a69d5741ebc518a6bab96ee0f79b9cdebb1d35
+04f88ac6a46db5e7be43a3c290399f00e2f78da44d348c66d7d12ce239b30dc0
+8f916c875e3e54cef7f5f1b966f606d8fa3e072d4a417b3cbd43bd3b644b2928
+626f29f4d369502b63ff43c7be25193bf11216db04ddc2f39faf825a559576be
+427bdb147daa9d9dc8a9839daba32fbfbc8d9fe41bae9d7b15d8106ae01e2cf0
+d59c6f8c2cf498fe7518fa7b84f38c6f4be7cfcfd0e75908650f52a107ab7d7d
+2bbb6361d5378232b80edb45dfc91750eadad74c9b42a19339d0227d83caaba1
+72a43d3e5c64129284a50ecbc669f29a4d4caf634369d069118fd7a3fe12bfdf
+e202f3376727ae5f1a9340d824dd8b76a517da44eacd882063c8243e121769d2
+809ff04b1e23d34d9ce5f496c7f1926cf1b0930466df9076dcd5b82c97fe1b45
+d1579437aa0820561298a96e22c94befa09929f74c067706594eb37375c136e1
+a9491f80c7a0a0fd447f078898778428309ad92d89cae7f48727d8912deba5f0
+d7b49674111a6033b67c19bb92129b7127d4ce80bce642b1880628df772a87ee
+5b48f395b55aee658c50ab2c81ab7de66eaa52382fadcc60957581a4e09ee989
+331ca245586217a7e6b0f746455c59552906d8a525f5ba5a07cc1df0c45b6ed0
+95230142ee05ec75e12531f65374f78173615c913532a330659b9beaa653eaed
+c0895b65d1a44304e09765cc9e070f61820249b6236194d952103f17799b68e0
+8b447986aea15ecfb780f7717986537516a6ae928648b16a51895f33adc17310
+f0813e7c715368bce559bab6c2292b42198f7f28a2ef66661f70aaad4e5ff132
+07de7de9f1691d5d1235d1e77a71403c2ce61c8f8960c262ea5fdf4e93fe87e3
+b4d7a400080f90a71a8805257c24318563575bedff7ecf2ee53a6ca8a24f8ac6
+e26c70b028f87a1806e63784542067b3d9deb6c96122c644e4052624a9f5e2fd
+c04d4c1d02d39b438097040027c0b7131dd5febaa0f09b767cce35c4e4447387
+567dc3887cbe792114a9a0bbcd01c3aeeb167b5ea4e08f2e04eeb67783669fc5
+16e91b1fd880efd3b1e9c1553a2900b5f1ef5f7b6ac7d3574456eb2fb6494511
+cdf324639998efce019716334e92407673b4ce1d19dd9ff89d301a7954e95b4b
+11e33d64682ee3a8e215ecbb482532e9f0dfa9859b014711a1789d97b8aa0809
+a1338f2de4e58a9c3d727b6efb754806c2cb3698d01ffda6ff4baf277aa570d6
+524ddf0898a02c508bc6684ae45ced430993828c8193cdf0091b2dfd7d811424
+364490b8e607aa52cf695a4179703ca2ea9d0b595770729228f912fd9228360b
+359e298a8845c00bb1fb2aca1abdfc1c570b044bcded5e35986c484f0c84cf37
+6fcccd6374804a3d380b48a6b50506b0f52c51dfa6d49c83c9a2a8bc366137cf
+3840355310b16356606f84b5da7783d2f6d6f46284f63f1ca4bd7672e114eb49
+9a2d80bf132de70d992d326f70a272c64f6264b68aa7722e7a0e400eee21e634
+8fc2524eed6e4df4ae7da2667dca0cc4d79eb78dc7db496a7f54564931434aba
+a06e085a1d1ecd916fca69e8cfbda338b6f873c90f3440ff8c442646dbf337dc
+9631f88f60d4efe91d35df767fa8a0e3c1db3d0053cb419e78453c595d6cb05f
+a4205d1b3334183c34b5bc80a898ed9ab1cc015c65189fd0efebe5aea9c71196
+ae040a0598a301c4aeeef4864e4789b1124dfe3f8e1aaf66a3a510705f6a21a0
+cf0de7e2f9a2b89179cdd4b009b33935b87765f3bab41ab667e39b74c3774edb
+443c772d06d80f7248bb6366d64b1a408044b55f61f641793436d23b35fd55db
+3a8f7d0e99953a6e74338c1d20083b579e534e887994c2080731ef4f9c6cba7d
+5ed6975b583022715232c005033ffb24995ccdffcb1f8655de53044fc3a46950
+694b82f991cdacaef5879fbbbc1ba7de04cab076fdc76b7e75aeb177c61143f2
+4ea610269642f4b1c765063418ebdb6e0fd220f6005ce8223f956d6755b7d145
+e734a947ffebea2904bad11b0fe43beb9baaa38108e475ff79e8ac33bc9d7138
+3feb6e6e7083fed7a31f178ba5b930f47f4c197fe5217f9ae5b64c3ee13ce1ea
+5db10538f5e54aff7b81aaeee741713bd60bffc2c8d3f7361ceacfdcca39882c
+6b0e2217bfe1990908c1c79c59939a2344174f73d069d0bf1279aa30f722691d
+faedace1bdbe056026c8c7970def57b68fb8fc30e7a86d461a2210998c103c6b
+835238192c02ceb0144f2e035fc89f160fb9f14f3a6772b5dcf908244629ef82
+30fd14e830fec7280100b5eda94d83208463e7358028f83fe2605e7dedb85355
+50384276f3b72f5895a6bd4170abd2bf25d6a6d8c165b2ac5620a3417927744c
+2a9798576343f472e6edd840b3082a22cc0efef8f53787167273e0cf926ebee7
+868803e55a266e761dc6ab8f7722efc301fa93ee781e081f4f58e6b6ebad140f
+ab33411eca49b23928a425e4348cf705a1e0f4fdd320025f3c315cf0da1170fe
+1a13ae53d45b712a3370f2e68051c1381311cd34da1096e7cc8d2c8e018bff87
+45621d8452fd40edaadb9e5d4f89d92b51f8ed170dbea911ecd14ac32be75dce
+dac5e26c212d389c94cd67126b98cdfab76600fdd7f36fb51813987daa38bab0
+974d2ff8e114b529a87d187e5d6e782e8f4f5e01342a5d8c95131a799149e496
+9549a3cd70a85c5e69894ea8b544c3df17e4f25d704b96e1ab28aa76babe5b74
+e39b799c88ba47826dd52f55d7840a32481524a5067c247389bd392e80318ae6
+cca790fbfd0d7c6ed9ed7523b80390e2a67d5e11d03cede53ca91580a4f90e83
+17fe325ce9f63b99ba9874c84cf44fd1b87d593bea28b5b674a643753758f9fd
+27aa8213e088f885272f0cdb0792cf315ba0b03ede938756d26a025ce9f682fe
+2c6cfd369314eecb40063f24f365edbeee61d41370bbec9b08665e43857ec274
+6516dc151c4fb114da529efafe44ba73a2675d1d19b1dd9e696c380379df7682
+42de54a93033c757441b7fdfa404678109b0034c4b4777ff05f4badda38c5b88
+d6c1ce1d281521ed0077f1bafd0426e63b522c3ef1eeeec9df67f3bae4bc48e5
+15ba02efc3be471c0ddb42372553ebcb4863d6625245dbb6c0eebb15efdf7e37
+27335a4e3ad99e4f70e57e218e749f30bf9c9a20f172c40f0532dd17d93eaff0
+911ffbee62309b59cc549faf8048f56afbd6936cf686514b16bca3113d17671e
+15731aa12b2ea9c46f887180c3885878a4b59f965f15e1af4cfa20f1d9fd84bf
+7d21d8488356d638867f2ce794c2d1b836bd471bd561daf63e1b427ea3e3c371
+1acd9f55e8fd1d53c279f5c93a2da8a8aa80857f1312d2e9e418ae4440a7c4ad
+6216861f66844f4ff36f5ccf37e96f76706f8dcf7d2a8b6e76a8bdd30d66abe8
+6846fa3b8d5e8c9b516457aec4036b402bffd6913d24cce8714449de2ba50848
+6d3f3fcfc5f81e9f85c99e20cd7551f1f0b63a04b5d8d9734fbbcaf55b44897d
+621b90bb0f3039ef877c04ea5d7c747cdbc9c25b98df15c1ae3946453ba2b8ea
+733dddb19f5a1f3ddcbd8d0c6bfa2e4b914249156d26e0e0bf16a088a9eaa107
+60fcbf45dd8e9d8bd7a837c3e4f840e1d747ec1bd31a82172152819011c53c1c
+7f2ddeecfa67ae102bcd5b12532bf43d6e1b515b27918c89c32d4df49c3afb6e
+3b719f37cf1cc9bd04c45129bde8b221f88e10cef81a75e890543f0a65afb387
+7df9dffbbc8a0bf424628a26a48448e17f180c4dca4c25daab5f4b5fecba8ec6
+24d80359767b2bb4dcc58163295031ff07ffe7b73bd22928f63c62c2e556c419
+c3aa968ba13b7bb559197339835490f2c15d7617578787ceb25e01cf3c7e031b
+607a2397a682db4b4ae408247d00136abd2e631c85653f45c833948b48aac4b9
+ce421dfc528d49cfe98ad6f5bf332ea672288b85520b13efdd67bd326a10e118
+6b9e7026afe8f70aa21888e25b6e28ceea8dbca35e168f23aeb233a18d291a34
+05cf105b4bb6966acf3a2ea3395cf406dc6271a358c3cf51ee3f63949e738c69
+3a2622ee7dd92ed867a3c279aa260499e75ce46503f33c62a85404c6715ee35b
+861357f1e5ad91e1779f474165b3c61b3f5cadb6e38af59fec5eb9fb81773bf9
+83e923c4a9d7aac4c64a4490d575030e5d1d2c788de808e626968374fddb4977
+954430d7513b84f74d7c16755027fe46e5b9ec95cc3913dcc28f8963e90cb00e
+ed7d1af4c8c05c52f4f799aa42dff81ce677d750aeed905ddce7ed886f1e7ca7
+ad587245d7c74f4b9272860b36a76701b184705e7e0225ab64ccdf3aa20f102c
+82ba6d86254808c33fd7205f340622360844e19a937645702c923faa2dbd1218
+ee5a8eafe6e66913d3a895ba65dd825c7dc1b579b0b8f6d4bd47de45e5f922a8
+25b20041f7b8a1a55281f0fc44b149f0e346e0f3e1e8fdf5634eed22537df9eb
+40d8c8bd63d844ba24798ca2060786679aaaf1753f2350f12c3a984628362db7
+f4df3e4ff03f14be6ca123bf8d4bf43817df72c92dcbd6234c0fee3b8bae81b8
+5d7495a6acdff5033bbc102016c267ba56013a739c9ac62dfe9fdafe427d22a7
+1fa02c29744c713dce48b9e52be12797084cc12dc805dda41fd60578a10a9f2f
+0a51b5efa3feb126888d0430649e858dcb9c59a17872602334edce0a92336f99
+250406dedea2e1b1d3f8031b2eca7ae2e96973b852c90b59878faf06286e8dba
+f7b1ff75a5454235035d1d68158585cb5f08aa68b4c5971ff0499f0a3f491d41
+8e2ed07b0cdfba8001740ae437674704ab6378f650d84b6b562bf346d5b3a338
+e21fbe69d330a12d69703a6625fc77e4eeed50c3eed7db53553e1183258cb374
+25c209961144262d9145735df60c1e3cee6c21ea9667d328aa6424dc1d0d4c31
+bdfb91c001b3b9c30e6343bc3e6af5148ef219e48e9c49e2d9f8818b86e13ec1
+ef1ba0e04c9a93b509972038925babe2964cc034d5947c9f8232fa9a40999fbc
+b26adb0ad1a69e25c896c97307ff214ff08b76427f982d0eb9eedd8cf2126219
+9758b2b07735c086e9a6dd43b4094d320379b3ef0c20bb9fcb691a6fb40b9c25
+9612905890f8fbbe837b1edac168764ae7721a97d9e0ef67424d2fd6166951f8
+00097747f5db43cd16c8f0a5e2041366bbfec05cf950876f7e4462c4231f83f4
+3f14e61553b99ebfa7ad033f51ecf10ad964d71c18b90e360e208ee79f86b722
+d6f062498e4294ec0991b318d18270374de5212418f54a5a1700a19b23a31dc7
+b1c4bf77bb4c4fb26d219d87c7c0ac10a88f65bf5cbb122cc250035499ebd3be
+7b839f311cb67349e68cacdefcaf9970b1e22a120ba90bdec02374a7fb96b232
+08619b8a9cb59e86fffb65f1f0cb64579de0cc5b4a1d428c0c4f4b46ce3ea0c0
+3a4c815b2d210e3ee18f1c5cef74ec07c3415f2dee72c7511cb479e78d3a0d96
+aa7f15360ae7455210f4528a18d74863516b1c688ce1e539306e260fcc203e9b
+5af0c06c3c04fade53ed1009672d75ddbf3358396868c2225e9460590590a37a
+d9b361c5872562a91042a635d8531a62088d1341f9519903f98e339a196c4e2c
+8e6ed843cd24fdbeb101f09bc4516a7cfc4554320a9318f10448fb9d10c45939
+050a55114e979f4cd9473af6254311e3f50d619d9f304df427ad53ffddfad0c2
+f849b4ea37eaf66005afa5269d435d1b3362553043e98eb631d8b4e820e9571f
+88ac251d767e3e92666f3c0aad0f5a5901dd5f0a72a44ca5f957b40bc29541ef
+5c28f10e36b8102e5038c96ca8a7cac9698d6f8fcbb82bc413d364e3bedde064
+a77e2347719c840ee53489d836646e8fd4ac6019d00a045157bf223f32f0e68d
+379965445dc50bb7b2c223f2aa12fffb8858197093319a10d3203038f4e16c04
+c62294d51a206da6ce0ba74e5b204656bc172c22cc2404f882e91ad157bf4dcb
+dd619aeb837e0bf53c4328e57e5be92943a9dc6f2c5664255d7872c976386c42
+5235e3ee2671789c3c32b7d5e7fee0262a43cde4d3bb98d2aab133429ae002f3
+b71c2b3688d9297cef802a2330b12b1e03989fc8b9d1dbc91a7bdaba94703525
+4efd991952a2be03e1147e9b5ca770638a5778a7a141a705ae4ce474000033f9
+e1ba21d948c4b016162cfff6bca97a7f8f81cc2c22f8011b0cb74e4c7c92ac80
+c7c53ee9c0520d2c7a5dc97a78fe15cee47994362c65210336de52f9841b8d09
+4be1aaa04e39d7f66d0c4d01eaf270e26eb38e09c8b303816c34ffb8cfda9f41
+c03d95d95688a154c087930ede2dfa63ed8f228e3917dcc8722d1f8636305073
+31d43ea9b53230b0d0797013e7604b528215f1a128cdbb26b0cc3fa43e3fe25c
+b57c9bf8f8e290586e27924cd20025c54d6d8792b4fa52450684c8d0e131aa49
+2ab0a4fec2d0f91f4b81d74153b5e2e9bcca216c1b49bcff347c57a727202068
+0e040eb5d5e2479209d7e085760a7604544f0e1b541bbc73431ebd722806cc24
+505dbef65e4ca3457d0587c4d4fccaf7f14c31b5cf62ae5f0c08661c9f5cb386
+ec057c82dedeedd54c8f380320831f2d6d9094a5d0075a0c5c203be292231353
+3aab44061f0c75483c8c90ddfe2de61eebaa5c23f3c1e14bead51e741963e5c9
+393e8c906adce443244f5fd6ad7ab164ee9a217e609fcf90e5b8157d27623d62
+a803207d9839752a19da399e82082ded26d15088bc3623923fe5d5bcdc2bec1c
+639e190dcf9181e90ddd7a07c773e6a0511f1e268acf8d1a0d985abab4c95363
+c88093bb9f111cd66e7b7d8cd8eca3407fdd45d01d566944abbfb99bbcd5602b
+dc8e250247dd7a37f4389e94c1342b5e6ccd99ac4d62595b91b15462dbad20b6
+5bf5fd83a8d8864cc9e75f018caba8d7feb8dbe58766dc0a06405ba89e39a6dc
+a302161f88b641a7427aa926d438cff5b5e463309a6cd2abfea827a512d03128
+ec1f323c8a92929a4a6f63c798fe0b73fe59ebcbb1e4614c2d3ea0ce81cf5cc8
+8a6d82334df23cc978d9a97930cee6f567067f7c3877cd46b672d116805b1543
+4a019e47fea549f9942732bd1537c24fdc2f1b0780f7c9b3ef7e846906abd277
+82e21a4a8456cd0a880174efbec65e6eb267a94a9e0bb6bdb6c1c4998d7523ff
+eca5ed22cd608abad4f8570d80ff8cb65bca81b09536ae22a0925ec620cfb875
+70ac3a8e19abf258af93113ffc08aa07f0ef6d751440d874b3631f8a4bbd7721
+9a796911b9dbc0b2e1fc5768d40cb8dbcd38ee9f970c35bbef52deec4582d18e
+17a184203a891ef39fc8d56c29ed3b9010bf9092c8afc63f072c6ae93eeb9342
+6647475cb38c7a634dae53b14995468e4e39464ad9829b48c97e78c580ac32ae
+b2f5631d37bda7f69936d4cfe342279eddc7ce5262969c87e74ec6c4728c82bb
+eff94703031bb14d7743d97690d7a2ff863904ebcd94d1a2fd7270343f38355b
+ad318250da6b388c644a9f62bf59482d7a7161f5d3b65ad5b41005bcced2c16b
+c60f81fb3b013544a32c81c2f455f34cdc12e195fb29631b613d0db8fe3b4957
+a49b3e0382b3ca954a0c135e19cb83b7e29c7904b5852641bb08cee1bdba5a09
+f1f03521a866d99a6b5e670351b73cfb453263adf92a8677de8067ff52ee5a63
+e9ba822e8a11dcbbfa745caf433ef673fbd0e6c7d28f9d3260e2b2d75b888181
+3662efea0548a4f4c2ea1031376182e12b36987f2afbdf205af3229b7e87b20b
+6b334fe8615d13b3c640d25b5c822a2270111d964e4cff01f36d2df200f41872
+29389c1482c576422c158fbf2405e3dc0d87d76237f6666e6e89c46b83289a9a
+718686fe767577688e8524474e522ba5fe3f6222ae659a12855b295d408b7da0
+042c02087ffab03b422ba9b4bac8c39d08605624026e355fe62328d4df8c5d9a
+63e3118bdd69a142412d43f20d23d990ccb50ef669a76a9fc3924a64fbd9bb39
+fda7015bd5b56d3402151be6c4240dd647138bce226f7ebb9b723b3b3e6b3b27
+4da0d1245dfe453bd9ea5fcd36eed1c09e800b20eb206e3f1782d70c67a82d88
+10451c2cdf052ef603a41d50b90dfa15a5e967c6ae7b505b1136fabc9898c004
+5c1043754b8bd712703233855db0895697dbfcec02ef25092d073f37f8f86fd5
+e647c80b0af65453bdd645f92209412ffdf14cb48b7daa02781bc981d89e075b
+39c7e8251d2d384835691f495b3d5a43557f1dff58b935246c2c0b715b40d4b7
+ed7386bcdf5be2beecdbb223dbf57c9a06864f399d4748c11b89760579f1e420
+a4e8c67241a8348ef3bd04634b9e69d5dc5015f89b10f7580a008e5dd424ffed
+60f14a39a759a0c05e4bc3b1e344feaf613be4e1007f5535a7850832760b84d7
+3391a60001d472449b3e4920f46186d0fb8c8a1b96165e2f463e39e289274985
+e4062504ce4ce8b24764d050c371e6545c3aaad645242e39876fa2d8c8fa931f
+73d56e5206d47649cbeae14ef3187d43f6a3bf48bad527f6c01c8b1287edefa1
+1016690d1f50d310d463f9636d6f8514a60337fce30e6d72ddd2f381bf2bbecd
+d416f7ad7338fd928296aeb8c58c33e08737974840b690f1e8d6c46928fddf4d
+65c07eb4cde5f6b5dedab807a67ee995fdef7e7fcb285ea981e0877da1b8bc42
+fc201fe269c44a48be11a5ee8a365b50760c477e51005db9fde781c080f8ab14
+44181e4a0bc80741530370fc987e7eed25cd1b2de3bd5c53e0cffd67f24f9e2a
+82fc866abac0d9e690a0075cb623a625ec6154da7bc0c36431f20fe0a8df2fe0
+50cdb6c3d767b5f640833e7ff56016665ab4d42f28853f5bc2726de5a7372ed4
+8666c51df290f796fb129d441bff05f661a123dad14e5b8bd3839f0de22e44ad
+77cc296603290f731b23a695b45f5b930a8d6d3c6783466226fa0a20ff0ac7c9
+f63eea109a17da0ddf5e1356e23c37528776ebda66b0a5ab37b7aaac291eae01
+aadf7c555a4fb77722c0a527b5a711188e28944494ac58f58c6dab6df90971f9
+9ed636c4eba0dbcad659aafc776c3efff28a7d28da972a8bc5d8eeef3b7bdcbb
+8de66ae1d2fa6ec3eb33c37e632118bbc8dda3bfa80da3b7a2770908fc4575ff
+de9b1db2fa31637a13f8b97e73a55f7a975fc836eead6dad2453aa31d00447d5
+6350145197d430f8e6af1246a7d605e7df7a5ff4fb58e8241469481bf48b8436
+31acf36fd14a7de24d3a90d681246d5bd65d31dcb7795f2c9f6dd3f04ad330ab
+37636fb9da5f27693b7613cb6268d36cbe13f628cbbd5257f2644b9be91b69f7
+2403f914beb707992fcc653b96fe7b23eacc1f46c325bf02427894a28ea28e74
+482a2585f57f4cca2a49fc473b73b2cc47ebe014e78f9dd868dd4c63923f660a
+0d465c1053e7a6dd9138ddead25cdfe5f3a211d6f06e2c19453b405173a814a7
+5017f34fb8504208864db35ea2406c19d989e65e5417105ddd09fe62ddce224a
+daffb81a19999b0459da464d3d10492b62c12d085f4381fb91b597f218b7fc1e
+c0418a9292c6d2fb2cca2786b1b9a5cbd5edef092b5a3afa05d09e6e59a70f02
+1346ef208122d8c8f6bd2da33609aee5917f0c92fbc0941dff2cb3df9feef235
+4efa7155231d3403ce8211ca2786915280811d9dc9275456e491331899265c69
+8b3698810effb9e6e6fc2f5b1daa88e0740f8195cc8de64711be512f43128d28
+58f130156cc2bf774075a2b0bd3a0fec0729494003d58fdde93a2fddb529803c
+76834d20a2f0654a56d1cb58ec344aeac5bb976c694fbb328a3cf9c355a5ccfa
+3de62645b792d17c86e64a14465c5d29686a03037abb8eddebf2c55c94e4d984
+713982f983d942b390674695a46485ee6eac26cf864348762b018ce478fcf5e1
+c434b8a26187f6707c6963a933c1e7e5addb9dca4d5e9f5ea43a40c43d068b87
+f3d8bd0324f2d10f52d92da826ad20917b039076bafd236881654a5abbae112c
+7fcb2bd027c96d69fa1224a8108d1b7caa4d4b03f813d461157a7b5956be1296
+d929e3e3e5249cded2fe79950e619b47b99cfcf0ce26e6c5135ef0d5a05662d9
+24d57e0be99056aea4094b3894b644585ecc7a6f016df96ce3fc1ae75635ec2a
+2761bf9402fa697a76e326afb8d7ae5f7d375d260ce60143a38913a4e085381f
+ae19ced0d207c80cfd0da23b999fa947408f31c00ed5046f48187ffa4a9dc8ee
+1e410719a9e1fa29f15ab776e8013158bef0615e6960273646af89da0fa935c1
+8a7a1b67df2b0fd72b42cf02c5e916b0761e89550303ff45560bcb350b7e9ac8
+0422c68d1e34211212999f5a99800c7b103afdd7c8c5e32ce86d906031418297
+415ecd921c44e3d65759bfbecdc688167896374b5cd91a64cf08e3c224b9f22b
+06d26bd1bb52679fb75e2489027a7cf868ce7fec588def5493f6570d2340a2d7
+61a6272402e9d29d02546929d02ec741a4f54c94a25ac1dc87b8f2087e4d9275
+32d6afc9597fa33166e593112001404b15e1691f002afaa8a4fe0fd803adaab4
+ba64ffbefea2acdb3059dd040b2305997fe7a6fd78db6e47e385af2cfa834af9
+3cff1bd3773d6b6d2f303ef4950825d6c3768532ba44a111af37f8b632cca18e
+7325171953b66ec7ad79eff3b69219cb31201f5bdd8da70a8bac1df11ca37519
+5658f07151fdecba4c1e14c375de60affcee4998a0a377587f54271bedd8b226
+ab3c9b3638b4e1f4b633120aef8e4a2989e2925cc84c7281674e7089b71bcd8f
+da8d1a26cc2181ba716d80be532578d6798584c364f9220f1725670c78007454
+0ece6487869cecd8e66f8adfefcf23cfc5fb8f1ce23a5e1e7159493a78742f05
+660bb55526f65f17eb255e84cf0a5c0b6b98f1d54d7fd00c1f8236c60c596f6c
+28c7adc584b38671839b5847e37a7017f4f7857e31c3108cac29640fe9cc2ddc
+9165273404769a556bd04c3bf5f9d76627603543947cf885de71f44c65760076
+a889b5b2d3511219936476ffd38e2d5e86fdeafaa17dd36436ea3dd6914e812f
+b1d5d0333a7ebbcf6e908d334ec8dd335357546e241e6f3064251323b2fd746e
+54d67d5e2d5b425cbdf89105b0c19ab4cea931a557f2d7d488009fa8685a1c78
+57f58bf6f62a8c71eae1b8944d78df9897dde68b628df2706d7aadbeb2d4a4b7
+561810d4f7e1988262f7460684575b38f13c4680c3dc254c7ad5bf5bf200950d
+857556ee412c33b42820f740ad368a8392ad0605f16692e2c06b78ed542729e6
+d399a052823686f83e8c3f0e0befd2e7405c52e1d083c13e9283b50b4a834249
+6214238f65e52491566c183d5ff36711e6a99eb49f5518efdae0743467dcc32e
+4d4c92ff8de5f8c649152d48106228117f5c160fc8bd2c69b1dd365337722742
+2c59cd0589bf70287b487799df5e698698f4ff13bf4618c63c2b0e000e57aad3
+dae45bdae9bbb459fd55584839dd185f48a75687a8192f3a63d2280648e9322b
+02b9625144e67c6080f0eebb7c741968736caaafc151c4a9a1c1395f31b61ca9
+004eb39a789a9ffc247a249991106df6e1c52e40e4533f0fb6087a59080bf55b
+f197ae35804870900129d9a941004273ca0065c42ecec8cb517609d01405ef3d
+b500f09d410e230f680e12e5cd607cc0d1edc63e1845fd1bf3d9ba07e1b7a5c2
+eb0accd178e1c0178ef55eceba43a30599a0477fa4f8ca397b69ae64f6b6310d
+9fdb875a00545daa9f0dfc0ff9fa355a9ad4b08d08ca7bece29a5d3a325c1463
+8f38dfe12ccd5c942a5aca6414a7b04c698c9adae4d8d43732e41dc0f04da349
+8bdf6b9f3a58296957d88f217d5c46f45c7c3a433ff9c355a5ccfa3de6553bae
+ddc411e40b9f5e95cd6e9f62dae79353c5e1b874f6677bc593d2a2a07b1c3b20
+9596abb07ee8f71212674a53b3a604f5af5449dbcef830433acdcf845e44d879
+fc619fe992d5f6ea9b0cddfc24fe223e7a357cb0d5771cbd12bf50851f400b49
+9e55a3efcdae3b6d6cfb72ca9e96f9355234ea284bf70abc81d29bfc928dac80
+5eaca5ca15e3b8303bd3710a1eba32e3c13ef7641c7637afb5cef5dc59902e6b
+aff4c7ce9fae89977d96691d306dfb9a54bd8b9cb8484ae4b50f7e2ae5ade3a2
+9076c21e036a78c3d78918c529b3eafbcffa7c842dc917f162fe68179bb45b4a
+75ccb684f7c4b5a6d5db3cc876537e4b643e447dc64bb76d9e74a3d378517880
+9896262b00893cd7474bb605994df0ee028f4a703b4ac00ec95dc3ccb3342835
+418a5a158b4e6072a657a7e3d2728c0f64242be6fbcdf27d5093139fd7bc95d9
+89320f2ac958c06cec8fdcf56596bbb45b18abf2c6b5b891236063441ba7331d
+613c0f1de49f50a839bceeee1f2ea065fa879f50918c6a3280bd7bbf1e366e27
+4d5e9e0a6bda5a8768f72b811252f40704a1190b5887bc5c586b1704ff1f7d4b
+a8ea68ab335a506771a681f80dde3b8aa04ee1bd6cf881217b829942d0282c3b
+e262314736557d8a43e8686f3280d4c9aeb9fd78262834ea5569a233d459abf4
+5e50d17d666d3e9caefe4f8d74710aaf2497a405d73fca9e107a922d0a9eea59
+6de7f71ef541a0a5b141f786dfdd1861f81651f540f82505b3fe83aa32067db1
+13812fa761a7f3b28342bf1ef531746b033232fbd06a12a0b80a9e23571fc1d2
+184a850fcceb331ff7076d3a03e8ed50991837925efc91e1ca33491b36cb1aff
+2c1c58338fccc4a9f0e298e781afb718990cb9eb141c6ebbebc79e437b52a17b
+0b4a8cfd0da2f9401ce9370a1ef04986793a67fea0fc96d8eb325cf73ae51c14
+c5030d9a8340b8b33986ccc325f93903c0928af3fb3766c8428fa690cda94c97
+939fd8df41abc0652e0ca8e82e048b133aba600278e3a227c4eaf8b557856ff9
+e6d2fb6671ba75b779e90f2bee7e349ec394ac76e16c73509b5e99d44fa10636
+ac63f8d546d5eba3c6bca7b738d4338134d2b79dd8436932d6ee22e2183a5c07
+a72f4742edfc2ed11145fcf8a9bb54313894709b28d214fffcc4b917e0c389b2
+590ef307e3ebbf0e42570de893328dcfe81e7edebf72b88ff1a557e2f038ef1f
+8c25c68dd7c037226f7dbab36cf73ee00b92ba6efdbf9a9e0f4a7a91853394b2
+a02077209350a2b61d4771f338e739c5382c07e18386e398b2946ade4c3f0a44
+ff6f333d550ea7db004654aad32542571febf3c31271cc5d167976c6407c7570
+115bfb3f838701b58b320496c39c59553b33fdd7fcf135bf4d849b130fa58739
+6a983a93344bfd37afdb01b18232af9f65d5e3f5131bdc40063d7916a2b67ade
+09e2fed7968e96c6ffdb6b41b0a7bb6b8e93c2ac4f1b466f16e219321c9ad83a
+8027101b3d007ed0e43594efafac9dc0e4764477f6328ef0936444bbb4fd20e4
+2ae35cc3090ea9ea8ffc5a85236a4597f661568964f6a839650470f79eac69ab
+e1794cc369c4183f66715746b9abff4c151ab6f06be72ae63f41f5f806b3fa05
+eee4f22990464b7e4950f5654b36b7e3158a7b4ecd4fc7d5600c831ea7521df8
+e61c5f5a3942eeb82e06477461ddbbbc9551ed095d78309f0dc9465f88e70fdd
+7cf599e493318efbb07aef45a81f2d36111838a736b422891c562c933005dc2f
+99a44557c978e4d9da41d3e2c3820e4a40fe28a8c268cb593960058f0fec18bd
+c23102a74807835b1244daf8bb9a7553961977ea5a7d0e8a59f58b868c9daf82
+5c4cca4d84f4f37484b6bf8c1da54c5e089d3cb47377a99eba19e3e81a13d2f0
+59c22952aad0fa4f3a4d82478a06a1913b8f2dd141e776fdd43d76fc9d28fb93
+d198f84310f271baaae7e44752a4f8fd1ab089d3555f9d7083999bdedafca730
+50598a2b85541f071716d335c793b55b007392a1f757469aae1436de65d00f07
+66e196c47010731b77aa457876cb8994b97e20991af4e7b47bdf752f59c7e6c1
+27f3043e2b06dead3d6fd024aeb2413d1dc2ac897aec8c2a97c848d09b102982
+06f2dc08f0f0ea755c68a0aa8062521d34f16beaf87d359db9cc79faabc3d256
+3cae31022f3d4ae7dbda58194866a7a29dd34f17a4d9b7f7987e01a6db74bf0c
+5e0c53bd07af310a2756f22c1cb9bf2a0e1a57aba6753d037887c4b65a661f70
+b2d10fad50c5ffd365d3778295d880f498ff4950257e5ce855161796989611b3
+e793c99f4e12e947b51643fa80b129c6664b283c100b5694d2d8bf60a3213157
+a17b72080842b4ee055d0860a801d930d63e5354f599854b5a142f11f6b78717
+8897d66f3f6a3ff20029ed1870d538076c1d4fa4ece214b0075ada5cbd5f4087
+8975b21a79d8a67be6d73f8777ba26c18ae3b8613387b25f43beda7a2c9f6692
+667015b80847d0759ccf2ef45aeb00a02d63f630dc6823d5f61e91b410fcbba3
+677470b440063d43bf734b16ab45a42b698037ca3cc5b460b563467d9b6f9453
+d31b47822f6fc7fa1604e77d00ab1f172bf8f4cd9bde3635703a4166293494b4
+37f032570a165604cf5b8b153da54077095196e55252dfccd90a8ebc09bb7f45
+ada3560fd014a844b2d9bae654a533733ca8ae359bd7537c60b52997e10ef2e6
+f9a2210dc484431af83a956af66f06646d918d2832ecd425ff9cd8e9268557c9
+efc84868728aff4d8174e6f85135c11a6968b13f7d8346334dba97a1a3f0990c
+d1d2bac3aa8a5c294e4d0d67e893a609189383a24cd3bf86c22720d320d59a6b
+5e543b799bb5d45fa9826b832a809026d104293f363200780278d973c9c63492
+8576754d19d28c0e4058684cd1a71a919e5814346bd40fc15eeca1591e0a709f
+f083b86841f04cdf8aadaedd95f26fb807bd09886a011443d410466fc6bbc56f
+95468646ababaf39887f25e485776892c89a6cb7bf5d20b6c4e8a0bd65c5de90
+7a988a3e2e39aa721dd03ca69608d59cc1e8a1d2ea706b0426622e4dc6c94013
+3f979e5d141f06fd2051a54d84fdd4c7170a44f81ac786d258f98893bb4559c6
+ac59332f599dbf644367e28fcf01da3e8f33b14f8f83052d9c0af2061465b059
+5a923f4567274cefb60341ba793ec77e5d8c7f96bb88b63591d8cbeda48642dd
+c631a115cba7694f64506c787271a8c9cee064d9af1ab39cd121f7a1ff1c812c
+903aed1a0d13791c051ae21ba92b5db47e72e7d6001fb31535a385202edd815a
+40bcb9937ef719f7cd20e30fda23fd356476d22ed6c206f15cb2a78fdb45389b
+6cfa3cc388cddb82886b8eedbd9447a48844921a51d4cb19921c3d42e58c0173
+5189ba388aa32ac9b75b5ab43e428b20957bf253a117efbdc6d9dc0931099af7
+eb10790dbb14844be86d80c5e1ae17d0fa0f73703f180effa98dc52c5a8a9740
+869fa97d0bce9fb59b36a960315cc1d7264f5fe730a397c6ee121021a3c82cf4
+bcd40e844b78f99ac28c27d9c4f4c90cfe0062736be1bd39d45592020daf0b9d
+91efd616232a6aed299c9a01388e0460b46ec288d4e1cc5b08769ac88002094c
+9b680abb4d3e0823b0832d0d60d08dd7e1a27531803b26f74966f6f4fa2f4117
+2048782c3e3ea7744dc52e7113d3e290d309881514fb40773b1ec9bd7932f7b7
+42c3a3dd44ad5207b89125d7a11aed1a249863a6e537c887b00161ffae6817d6
+269d93d4752375c48ad0a598d2486f1a173bc55562bc889eee0afac3a4b12fd5
+cc2a57080eb6e3880e1e2930fbbf7e6629c428a1025966b47ddba3d48874e8b4
+ac01d33158f31760d82a88ee60be24a62fb5ad9f24254e2198d968255d79e4df
+206d37d52891300c669855a59e666dbb4676c4ec6fa988d6378ce4d00892c711
+3fec62c89dac139cc2b77a954f6fe953064c52a9345df309d8baabbeb1cb7910
+5aecc2f99b451c1b028a81d62927e2c932805d38cc8ab7aff70fc84f944d3fe5
+12694fe2dc46e548177bb78e4a51ad574c30f29145be59a24faadf91d34e9750
+1b1bba9cd1563e7e339e9d8795a3cf887053f1ee7062fe98bbc2257e8a6a964c
+d8542df1129217515f38beee762ca45f9ac85c4497afc6b7200c540287d85c40
+7dd6cac0ae86a4d324d1b3fd2f249d3613772ab3362840a78ee8633b3f89b46e
+b6749c008a46eca09c3ac738011772215d2e61e59e2b35b0d9bd012378c2e8f9
+7a58b7febc683901bf086f32282a9e539f2816acd16d5b5a0d7c26372e1b7ecd
+d64464cbc518da0ab436329afd5f6e12d0ada7cb9bd84cd2b6eeadc4b966e2d1
+33dbfa0ebef84b45019f47c668325dd037f85dbca5fbcc54ef9aa710d998f3cc
+df1e64c0809bcf0386de718201cb3c474fb5ce9cb8054ad3394bb6626f7eef64
+f60626142f6939666fcefbc920932d389770acb70180dae25a1e06fd6be4f45a
+07e382d0c055447a99dd7e0952f24e89401a1c15a277d02dfb6d31f41ebcb754
+2f4f608640ef289ce4147dc1cab77ce02b1823a87bd46f0c00cefe0191d68573
+6af019569e89ab388d3d770af3f61546fe350adac3337713bbc091318cf92de1
+7b61ccda6e28c314cd2d6a27e758e48dd6038fb55c6841dcede725a1a30d5fda
+87a37ce7461be94135e94dc72c976fc326a32194046979c351a53fb544d1ea5c
+dc87382f3e020e019e03ab3ae26eaa4e64256cd32aa1eac38539d04ae425e1cd
+e4548d0c352b680b4922b6b407856f1ebf6f3ea6ae68eb941d914d50642dbf74
+fa2a8df6b682e0d53f3b21ffa7225b105e359f49488183dc94cdaf1d7aa849d8
+88849fbd3bf680dd02198a557e768668eb2fbbfa23cf3af7bcd76940e1329b04
+bea1e1da8d71850e11eed811616410e2f7db0557aad2d531e1d3af851329a38e
+96b192d5ab246fb72788f99e07d22f9bf6c35bd15036db0fe66364654ca2c34d
+4aa14542eab8d979311725ae045f9af9790b26f13d0e447b3f58a8b817eaf26e
+f9ac8938054c130583237c616a5f3be196c9252a0621cd35183ffa748b0c4e23
+869a58dc345282066b213b441823389934fea2e40aba453edbb936ef4695b6ca
+5734f49bf4c21f0c66287df7cd723cd1d0e6d1ef02514a89a0360d6630f41781
+e06d5d12f793d64c257de894f58a074c06e4e18a5991b46af372e7c1065a9132
+f88a83fdb08a4a54b50d4218785e15b6a86d90b7ed621302558b510c0c205100
+44c2ff47efed6ff373b46f1b1188f96da0b38b0a9430e571d4dad21eae98dcd2
+8d36affac5ee640128374d182d451b3aed3f2e2080a1531a2723ef40b496ae2d
+f0ba56582870fe7ddd397548d59042d981da2c5b15c2d3cf8ac934d91679067f
+e601409c220b8a6647414d0b73afe5e96fd5106f9e5c9883132953083730f75e
+2947bb0a0ad58d73cf0b0ff80481d2a152262e1b024df4ee74d22e9933f1f222
+f389b8367ec1d5200c70f7f48079fc559a6610e483fd55df95a8621c4e42774a
+0e978ad53ca8466cdfc66e1eae6ea5df41dce8369b40c6aebe585f4e9b3b23eb
+19139f371c9565ee85a2d8eec6e0c48ac857ee2660ce13eb5cb81616649953e5
+53e23fc2fcfbf98a83cf202cf6ec7f3816e9d70a4f86391e51327e047315f653
+906b49384b9c069fbdb27c84521aaaee74e53055897cdbd167a7b5d33b8feb24
+aff3e22b5d9da4966788ced11943e209ddd32f3085aa28f2b631e56693a390a1
+74b5cc902bd67a2c3959dcd84a08f8ff2dcfbfff5f75e2ef01c9fb619117fd97
+f8682a84f0e718362659df2d082fa8d9428010a76268380f525c4a3292cdaa9c
+475554ed161cd2c43355af9350c3fbc654993a3f8901391308af9bbb1fce0813
+7592a1a4b271cd98d094d77ba038314ea68a4e64d68b0165bb593b873b24fe3b
+8a59f22ea3714023825140e0658fb050e8c4e32d85e92cc0d2eeeeaff00c353c
+c00483ad71627438a50b4dade6b0aa1caac73ce846581b858910486820ad2190
+33a9a108a502fa6ea60b8c85d1ec5c816a325dc802f213ba6bfcbfc65306d07c
+3202f0d42fdbb7b62fc6eeace31837cc024965695c529e274ea638ea2a24847a
+db9eabb31283bb13944309c17e3615256c7b95d8f99f86feddc2c7a566e4dc87
+bb060d88c661f589080fae039f4fa4b5b611caa6c65455dcd4bff9ff89b63f60
+5dbbdb21c07404e02688b7f92a8373ee1a6a891bf6c315b793a9fe81079c1ef2
+804238c72039924ea63039020363282f24f969f6a1aa10ef2d9e7cbcaf53c9cc
+1f0925637190af45c7aa731e7dac0f091dd22e5ddab9a8dea87a0269d5343ef9
+f8297d133a037679c6834beb8741396754f2dc5a89a1621c13722289eded1880
+c9859ce2a9739d003bb874f747abe3ab28d79930680ca53ceba6db84aa3eb869
+0830eff19972d32f37eb6eab0723478f881127a84536374025a782a7cb24f90e
+892bf7b8b9a44a237779c4cdbe5325d3e67cd11ff1a58b9d69ce962a659486ba
+6649708daa2f7a0cb144f6b11161501464b7c7b20659f6e5ab21231aceb813fb
+b30f9d6c6afd24d638ece5dac5259af8eeaa46abfc1d6f29383188b4ef4d8e08
+fc13a8cb07f61f55727619cde4f86944b879e23dda590ecc57f312c9d6d6fbc6
+8c875291b617f8040f4494323cb93e84f2100092b5a0527dbf02fe55175a1b8f
+7b8b130de1ee8d39589b7489f2d087e6f865cb536b595c552534475f1bae5981
+807cfd89532d91a6316154dedc1b8fb3f8fa0270651107c5ab24ad8310303d98
+39b801b2f0158aca98833b79846369993bb4a3a715afb6eba5a2237652b17d60
+2dbc21281bbecab7df65812e9ba59b78f6e3f6248d5cbebfedde90149d823635
+0ad0a8cda735b701534eb682dd2261883e19355d6e31d980265351f1895cd212
+a8a006a6953dff2ff7ba40d5263b6547b6cd5a5317bd77c08364987424033bf5
+bd9dd20c0ba69bb3d25df2ef5fc06092981c2f57ac3d38b93976eced658544a7
+942cb27ef9cc649835863b2e14a4390fc3e5bc2c0be8fe2a67f1ecb3d4734636
+6b29b60d7261a743583915fba3c8f832c4c41bc1dff13178f2d61832fdd13853
+db2135b4d92b1628b80f3baca26481a48a9e0d0847e4c53427e5066816b474f1
+052fdbf71f550738c13f1c3ab27d1fa12a11194c145d90f61e4ef2a1fae0d25e
+6974f191d3e1106c406ce4fba266c952faa505954f6ddf00068422eb0cd4baa5
+34b06c1805468d7fc0b88a491b48a393c55726d4941ea7d869b239a69565a892
+82e020721bc7ed828c88681584e0c49452fa8f2bb88914491074cf0ffcb5fdb3
+628b292a0f2fbb488e96767aeeb4b5b738ddd65d50e143aeb6e37fd6110e0dda
+5e2120fbd267c00b1b90626ede7eb1fd37636e7f3b01ffc6f7216b396bc1a5a6
+04db15b065c9a030d08127a538f69779a5bd73a6d6f55d2da851afb9da60bbba
+d782ffc339762792fbf66b2d139b11e76a28e8b483aa5d858ad779e905456574
+8c2f59a94010ed153e90a961b45a225cd9a131315d2691f6fcce8b321078deea
+6582997e824705703029a8aecbfce61d427d476b559145f5ee40364c49ae13ee
+bd940e7104a70a5501f5c0a632ca1902b75598d3ae606319d0ff4f0bdae32c70
+b1e4d5065157499c15fae5349fd68e77acedeb296cda2c912562546b9d1a966b
+e7959a20045af835d9e7348b5b33614005f05fa26f45c3de3397e5ef9cf02036
+37b89a5085099eedcc025785eb5fc2130988a19570a1661772be2497d4c8ebdb
+fde6619b3af0fb42f4d4c3516db21d44941a62f62f087ae3731735beab50f7c5
+27856884c3c4c01b83a40af32613638be95390c4c66f71672651df6bb4c4a0bc
+a9533884c477f6ae268cf7c21fb12678c54e4b3fd8ef032d3e3c2d3651bd905c
+562e643f42d81fbbdf819a28c81c9874100a65e70466226d90662ade31127e95
+c7f5052cf24df0a04d14d1d8a5d5d9b89d212272860db459e07731b3a6537c6a
+83c352d2d1f09389fd4da36da0cc25a57f402976cf3a473ff27b6554faf254c2
+ec8e52fe59d346baf8b1fa0afb13289b6ab06c8d25afaf4f218ba88565cd3acd
+f1fbe77f5209e6d5dc361c18d9152ea6a5267116769c987b89dece4fabec70de
+3adbac8e5e7cffa82f4b7e2ac41696dfe89443c64d8fc50c5699117d094ce852
+c2d7dc91ca9f1e764bd117ea7cdcf98ecb87648e82c64d93f58daee619f531c3
+615ce9df8abd7a63b2957eaa3cdb10e48c379a96334f8510a1f04a7c433f3756
+81c565d30f7c7d9e05f4d6f249d203b286c45eaf851190ac74822c5a6caf4f04
+94520c329c09cc8e0f5ac803219b76c8be672d5eeb878eee0c1116ca9942b133
+0734efd931c8e5d54bf9f22d194475e14eb923a453c9b7f4ebba83c80b4d36e0
+617ba6de745ac02953a63a9d73bc2e07c52a6fb3182e001c021916ca4993647f
+8be57505636b75e2013b781c2ee65591694009f1aecc0195349d48d780db2da2
+dc8c60410235e5c3c7b4542873c609f921a62953760759d81b718500fa45b09c
+504f9cff973ce3e32a35b426c6f3fb2cbc366d980e133fee86f961dff6a21602
+d8e14529e91e10105afe9f88c9f65052db9b32cc2d88c7bbd1248d4448706d3a
+defe4dd13ed573d1e8d31bfaa9fdec9fb1dd2ecba1ec8cffb0601523d86b8187
+e1a1526c20166314bb65cfc266d01bbd562c79f00c390ff40c42e424976ba541
+ee3860910e852c72b4c8de0cbc50679ea9434f4f21940480f148365bcb9f8889
+9c6e39c309a84f63a0b01896d4165395618198fc9ce773a874074a2667120e7f
+3bd73ba915a657c161c04ed2ad2a25d81c286f5ef0e0fdf91f6c0a1da5da4d13
+56a4df2f14c4cc408165ad0fe1c60373909ce04828b2c333569b78f35c6594cf
+11becc7df1fe0d9980cbfc9b083e6ff52fdc26cd74e64b22196586f4b21768d5
+2fa9107d541384b53737b872b0d7b0f931eb1525e118a45764bb1945fa8b91cd
+944fbf3b63be737baad604e8e93d08521e00f8cc93052e4692f1de842505c39f
+054d49d7bc49a836c724d887c0905d218fec71b710fb0a5fd2dd736354c6b43a
+4bf4cca989e6deb360b45e8a353b712c2745da57934d5629923523686d471c60
+6e09b539c13f1c803dd11cdc41a41e8427a513e4808dd4f787114870b3bf8ece
+f68fcdc6820e82b4d537534cb2a7c81f4e6878bd0cf435cf5845fb8402aeb47a
+8be7f8658575e7fbcce387587b6563eac02d806aae5a83c185627d9e8857edea
+2fa37ab90a1aa2cacdacdcca8a23a2be13b1016b2716ff29c2a660d49d4d2400
+dbd73edf65aa2981dd46641d15f2b5bfd40768309c570d5b5bad657915ae022f
+7fd0805df33437b8ed5d961a0c65b33a6ac36a9ddbfe78169d4a6d2cfaa5672d
+9354964e4c756a20b65f1550064e435dc5b0b8a53732a97ae91c337bbfba8e1e
+87d2ce72b4577c8474a0ef36cd33a3dde20e650eba05a242065b8d1fe3c702df
+ce2f7ed86080bf5e0efd27e162ebc4471770197c13ab838945cc2df10daee14d
+901fe063ba909462ade61c130d7911c73be3d448f36455ed30c21bbc2527d0f2
+5bacfb066c0c45300752906845190fb3e76db5fa537fde62a1e7dea1af35898c
+3edf2d9e6bc359439e38d363fbc37f89001e3dec0a7b6a4a7b112cd687e7c5e8
+4b0fa42d52b534b2d16ee3ac26b4dc3fec6272d1867a390ff9fe074d449b6cfc
+93129a45a6624d91800c7e638c32c057c99673e7b57c5fa81e60e1b7a0604424
+458e1bdf2f7f77d1fd1275512e3f6d5411cfc20cb8571ef52c16adc6073a9542
+da94754c931e74704ac639fb69b227ec66d4446231bb566792321da24cc3510d
+89b59bff3d647ef55be28555e93318d9eb888e32abfb2b0276ad9483ae8bd4b6
+5f2a3876c094f8e22056b9a26dbda1b09b0ce8cdfa085c8803151de9187d5c63
+406d87c00f3ce085b9bec794b99b2365504e7274aff81c5032db910e31c8f2f7
+c647a598171a4e8b19d05b5a77f1073ca0d5f58ebef2be11c6f390af9c835f90
+9b726589c9e75687dd9ba33beb8a42b5bb63076a5cd2cbfbbd43498934b67f6d
+35556aaf2341c4d3121ace6338238cf67a76ad99ce7be30040484133b44d08f6
+e0d6acf93cbb3d7b3b860e620efafbb0c644fbf90228a339e9a5e99632238e99
+b1644b65c08859787d58988cb9b6b792aa43981c2eba7136fe17c9fa4a671864
+d39bc4ce208357e4e178a4c0ece034f01955199d38dad2b9e9f3c47ce7c652a1
+29476742bb9219514553a1bf9aba5af84a3530746f5d24795f7d79a1927f1266
+4e9e31e2d86be65a64463db3077fbb4d8ba24f577ad99d58220c09d15d7478ae
+7360e3f048ce8bc4e7c171f86d2055c61cb4e32c8e28e52d3e8a87109f5b9c43
+b3948f9e62356c4f55797f5c349ee40bdd6e6d98406e361af69bca61870057a6
+63c08cd95a45c1d49e5ded4ca719b675f8fecbbf6cae5bf666352afee85c48da
+e728a4672cfd6a669015d51c96e74ef3d42233fa3e5271134aac9354d289e7e6
+4d5d7be4433a62bbe54900fea32c6bb7863dde80c914a4d50e11db676af24cbc
+6161712ed2e4ae8f0d7407c83889a3e52486a3d7b1e4ff2676a80039d4217eae
+febc054ff700f06cbc565353cb08267b50276528e5da421aca6d458c8d381645
+3d524f1b7d143791c3939bb177dedd8000a364a4fcf6d27912cd57f8b1aae2e1
+2f012bd0049675d4b47fab974f28d57b151bb0cf1290aff5fa25e4219aa3e1d0
+e6007552aa41b3446b5624135d748405749cc1db00a6575685b8f78856607a3d
+db56c0232cec6d31608342c785fcf455898832088ae4e608d6fb63c57ef4b2c1
+055ba60b0945c580292adeb56bded15de6311695fc74c35f311ebf99702b54d8
+77a677ac6df01ce3fdc54c527c543d3bfd19265fc579c4fb688ba9f1b7233e29
+ae0355e5a94f2a25fffb4a8fbd74f6798c863a2f318b4ce0c826f738614f5726
+2a2dccf3d5b00fc8140b153f360dfcadc36873b124a035781afb7ed41fd3d546
+79598d5030a8661603465eb2ac7a615c3537ff96556dde086f16040935e04a1d
+32fca9256ce570eaa1ba42b53032bea880447949fe73fe1633a552346c930fea
+020e94d8f22f115b195d0d8ebab7a07cda81497b824bd9961b660836709e0c9a
+a7e61662b0684e925e4dd74fc97b675b79674aa3f6d16e96a1c86fdd2c6cc712
+1b00906b4d70c2b27390ad840962c2aea555822be94c6a46dd5a09d0ef16d346
+32e6e2d9563d10762b2606ed532d652b7949ab11efe383cd0afd2a92804998e0
+7559a8140c01ae4b4fb4eae2c147464cc71403bbfb42cf1bfb2e009327d29ed8
+65feaf33fe93c081568949c382806f465e178142d956b250a827bfa2c39f462f
+5fa069853ba2e2d18ef99bb53878c78ae61d3ca268e5123ce3706d0aa11c336e
+f5d8cc7036c9ff0e73c16bf7fba5d692847cf0016535be27b2119efef09c8890
+ea845adc88dae8bc41a11a861d31e86ffc64ea135aa53c1823a4e1d4aa8ba507
+9e5df4efce318b6d3f486b385377ebe65bc916f13609cea424f9c42e8921f757
+50a6c10c6c13ac5f8a5b629fdb16ce10200ea8f825280a115dd641cdf1763f47
+acad9790c90657ef835ee1d02f3d2e241b7d6749c9e4371216931887778de4b4
+60d0ce386aa03cad70c7e8a133ac4ed3ccf31d91dd0986caa5962c6312679d01
+359dff0b5a357fee803d61922367be5e97abb50f4048aaf6c76b6c65410a57eb
+77bbdaaa596833dabaedf537786063320d766fc6e6826be6fed511babaaee583
+7b0643306d7cdd12f02e183d46350bb7c25b94b7d4166666c556d2f3adaed6af
+fb36837cd861192b7a36e91b295cd832a67094197972348f44d0ce3870a0891e
+0ec2fdd4413f67060f412a3cd31a509028d6361353a5e50058fe4ea46a6732c9
+98900dec7fd4b20e6d6fec5a30fe1f4d743d0deca667b5fe871a7c68143acc21
+ed2512990fd94b7ae6fe738dd326258f6cd6060f243df995a3af97687a78eed4
+271f3dc4f79ed18dea9a84694cb969ef7221be2485c8ce9101e9b1dc59af6222
+02a8ff7225ea661d01d5370b535c33ea4202b6f1a3ec6525a56e1ea71611491f
+47bd21ca86ce6d8c924f28c8d5e5f66b9222827382dad72df22ea440726154c4
+19aa0517d06423294bcbc7c6f64940c999d9fe1c6f514c73cdf05acc4105bd2a
+764b69f7d17204e9291a1d13be111261c53b5aa1fc255a44acfbd6a3e27d20ba
+0c2d9fc7cb0fd2bf3a9cbfbb53eb0b03c6ab2cd636bac90076e486ce9ee10b8f
+e69b03cf6aff7b5a3fec75294bdf32f198d65680f6779a9c6ef6b8edd936ce66
+e9cca6bbdcf4cd0808f33175d40c3f2ab76a02d0051ab512e32adbc595014767
+1ff6c57a77b8303e50e7bf024c8af22a2c7d5adcb9ba42ee6565ffff73212f22
+5f69eec749413749bd663976da9e39c7f2120fc475427a64d578e332cc5d2ec2
+84307c30b92e8ec05500c8635a069294902bf4f2969dad5068bac49cac00a307
+0e5203b294f7a42c2db51b4bf3e80bbecb780b0adac425db4e167f893ef4d257
+8aca20afe2773ef6fdc6e07c20ed0b0b4620ff0d5dc28edb59f39ed3e976c510
+e339c4a0fc9f56524df0aaebc9d985fb6781da97e651df018635096cac944d58
+ab8634a3b02cf1376b1c5fad86ab57c5ce85435ace10216a9eb3bd3deb8ee5b9
+acd93838e9c5b26f6f02f5541ecf8e94ff33839b93e16b740beba88c19410250
+301614938b4a69ba7b9cd47d79d9f58649225ec073b8c86503ef606afcbcc79c
+287c9315cad03844d0a465ace3e83ed75182fa6de0166ed492659d8e872e5f32
+c3043b055e44b91ee3bee18777b28169dadc7375fd25ea67288c8a7919fb6b90
+e5a805a38a40f1ebd4bebbf6e26b5d5ef18e381970be0753c35de24b6253f9d2
+4a77ed4532f5eb2f464e946babea6cfb2450543655fdd5ba1d46894538dcaf49
+d824b3dcf5484ca5098cdcd138997398a60b711568f26ad6eb964de44038fc32
+07c85ae853ccf6406906bd00b9af16f3eb2246d3ae95f6944ff39b64453b5a99
+5592ab75507ebdd4a7f5ed567eef5aa2697e973e95b512549da9190dafa3f3bd
+abb118f5a81cda2103aac289be4ad5dcfa566bc51d8ba2d5b19b7bb31a484cd1
+8a84c5dce3c7e50429d637c22b29d8f750b60e65e2fe3a7316d458557c1e87c9
+c6f8105537aa3b5bc789cd7c370b741477d21af453ea2781d5ce29d20f2176c9
+9418ea8db195cdc57bc982e91ef665411741fda4f9fbfe4bb3966ac00b76e517
+c7c0988339bb8848e616a134f95078a0a5c1a295bee68499d1428ada2d995633
+39d92748fe4016a88f9d534b35c766c9f970903c0b466bf4867cf914c570ee9d
+f4ebc45e058759a851b8aa472d14c305f6410fc4d05acc1b11b431fa438490e1
+816475c1a6316c7635226b49399563b3dda5efa5b38d1a32efde767ba4a0fb3f
+3d93da113a8e3e32a33b5fabf64917251122ec6dc8165f26af07005b2da6aacb
+b7c8e299cd7cac6e3251bb812a600ffb1a028d36ce351b3216a7d23a195a9afe
+e3461a40159201f1a89b8ad3da7fccf08906310fd84eab1ae0a76589d69e13be
+ee3ba496b5c3f34757363c4f5623f975ec58c115901ddd9cf4b8e135f1d996e9
+1a438123b50d453656a44088b62fa37f575da53a2a42bc659540bc759ca7f24b
+d7290430dbd53edd3c602f2decc5d5c2e305e8c9ce56626fe5591ae3f6fc9b50
+62fb69b77e75d7e0808f984b7a062383d2514b0d682ee525b39fbad9b84663fc
+3b74f5ff1adee4775b8060b227c1df3c3b2fba8bd36cb216a5717ae6d6629a06
+c099300377b741877ef1db03047a531dffbf8dc6aca4ce0097b9e623717eef1f
+b40465ba5b56d6d618aa3e837cb2791736223243621b3c0f36e8cf3c5fa3b815
+36815d1a277411b9026a9eb2b1d1591aacbfc92b49babeb49269eecdc24efcbc
+7b0871150cb009a229b3d1665d05a796f84dea63d9c2e23af3a999f11f53aede
+953dedb336ff92e7839bc7023f8f4b868b5171e03857ac0ad870c8dc42c6ebdb
+cc7d51d1484f93106fbc79a677e97db44190ca4478f1c6e3ed2dd57eb8098798
+f3f163280a6ed0a87e57581fd663917674328cd2d1c6f720ba8c01f8ab79d4fa
+7c08f1417a9a9eedfdbc6d7c84bb796f3be629e476565a34885dfb7c4164e821
+529be9d79022b6e1268cf6517a0fc2864e4b718f4018744984a2376a822dff4c
+0344668df1caf06b38c229dabc5a8f51ba120b53ff9291b99340e46786c809f1
+912c2d910700252081efab3d5e1d96d8933b6709ec5198b27fe6c511209d84e3
+cb1aeaf13f364a8dc62bc37803dd71c663f7cceaee35e5b39f87bc7affc1dc97
+16f93f931ccb3164490787b1432be66246f8e8d2d01232ad3ecb7962b9ff256b
+886ca9f86af2956dd082491d015635e02d867a199c481d51f8751c6c9a3efc2c
+7de4970992cb00d45e1d71522ae651b177c2a5afa2ea22a1c083eec3fa104296
+b5b9b74464f293e394fe8ec2d4bb82b7a27b7b4878eb9ca6d8fda476b76c872f
+5f030a803c7b1f1c0483a96c6fbd43a97302e17b5e0e28eacca87be1015d91f7
+0ebb16c26c51a8d7efcf50cdaa4d7c3449b1134c30d181932ccd93ec16b44993
+329b44a6c3638b23521eefdd79e7200228425465b0c3df07868818cdf53d9945
+a740ccd2413b8d53b52f3f6b435f960173b0b4cff2e52cea7bf54e5002d254ed
+c7ed8f69fbe4fa8de61973454ca0a0c9ab2c2a9882f346d5ae456b074f367547
+54e4995e568d48065f9421f039c6b36d2b4baee80bec636374611123995da79d
+54a8e318f034d9245cc5f2fcb4096743366a1b6ea840461f45a74129ef2be0d1
+ab028681ab144f68f02aa84cd5bf824f06d7ebe88d55ce185e9defa29b55592f
+3896f917281ff282a252bc9e725ffafe2db8374c7c34820944991671ebc16393
+cdf712bb663640cfe1e700bed129c5b3b37cdc5b725396e471534e75acc23657
+12cb452bb410347fa3de8f13d2ae52de9e4a0291cff3f45bd1387e0b0717b0ab
+c7998d15558b3c80adc7f8d1dc3d3565a506050f3bb187f49ee8019da0b49917
+124e925bc05e633da732acaf407477b47dd9329dcaae0c3af728c577b5402521
+887d20bfefa2028e1ee7ebe6f5092cbf0493e4256dfb59d62b9f26ebbf5e3b96
+5be553da8a9e3ffa7ce4de7e428d8e02795ff5d7fe6dc2ccfde9c708233201cc
+b433e0fabcbc498b649af575066ec7a198ebd6d6bde12a1a5fba108bb34d316c
+03ff8b0368fef25689c95d3f1a1dbfdc891cedd92dbe22a037a2f3054897eeb4
+ddf01d1bda48d1eba76f5441354c342963da0b9b3f86b0738a8c58f4e798fdcd
+04882c1a3f18075e31b33c78529f4ee2fa66f09ff4c3975df960c6ba32ae0532
+3719cbbb5702844f3a84478c7e234341a02d44cc60e4390f335629b692832ffc
+ea98f9ccfcad11ea232a22998660ffd04320d94dc276af68dda0050406307287
+5e1af46fcc2859f6e91190b15e68e7aa56cf02593dc4e76631c3dcb7fc9ddd64
+6c7ab907941ec4348ff95fe5dba5d6233696b3ed777eaab40c4eaea7766f4856
+3679565c66ca57b9bf714be5504191337b4c9ac40f87b0255e22d24e4adce456
+3502c29ac30988ded18faafc96bd0ff8a268b0bf5830a6ff11cddda5929659fc
+ed4cee41b07eee3229d1241f8b0a3b9b1757e1a6cd2b53b29d245c0409321e53
+7f2c66df8356115857782d74dccbd69190852001b7bd6612511ca1c458c0459b
+1b1afc2dea548cd9984cb34af196b08d5147ab49aba60c0014077442e5b39170
+2c6c265b25a215fc6bb84931dc00156975908b76cf1fb54996cd798ea06d1840
+937c7e1caf0f5c595bc038d3689e7d4af6c15a3943bd3d5fa645bdc0fe0c7fa4
+a100b71d2b60cfe9ffc65d2503372bd5d7713cbbb0dd9481c19e5b24f987ab34
+861beac180f61c7e8ee68846db98c9c88aad992d8875d7b44b5de8e8c780722f
+4849ebaf90244b14d141264fceb58ea3ecffd9ca893f44a1d5104c6c9c4e63b1
+ced9e015b9f415690d526fff0be7ed7a93535c5f823ac753dc074d9cacd4a3fd
+c03879f8216a892924cc696b1d256f0d7f8f24850db602ddb9591126aa067343
+d61163475489a8ce2a4643c5e3db6a9608c1c506cfa2439991d807b8a3d89a5e
+711969217daca3bbee9289e474a0de12e6ccff0984f10aa965e6a38725f99f0d
+881053165dfe7f8a1bab8b24b9bfb7a382298cb7e9b625b78e53b731de5e5938
+24f1d53d970622a380d913c0db2b4f156033aa2c3a4423c5ba3be0261808c000
+a42bda95a6768ada32b929234e4c3a9d0c212f0b197615695f84c9a12cf77a02
+d446971dc5ffd7cbe68775d948fd9c9aca5d93438b7b2f77e83cca3d3c3cf885
+badbc9166d3eeff0473edbef598a9ddc5e5d50637fe9cec73e695d38e413fae5
+7f11aa2c1453c9c56d3a92fc33ad60e0f79e3da79b49c8cc77c40e4c82f2e9e9
+3f7eed0a0b488dcec44f8846f8a58a970dd88c12114ea58e1cdd105c656c4c34
+3d01f7bb5a35e18d730c3967c5b0e711009f14574d947ef626b4ed333baa9537
+ad0ad7d4bb5daed5af9903990bef203a529e310e5fee72aa629cfbaede5bde1b
+7a53c7cbf91a177fd3a40fff8a1eb215aa5ac9ecba228b853d55d54e5205d0d3
+2d68813164ff61cb3ed67387de44ee9d93ab2a082b50aa39050f11cb7c894914
+3850b334dc7db907ad6b5e7f3f89bacad3771604f0f1eb2843a1a5df85dc6d23
+bdfbaf5665edf79e652006a822f6b9dc0692af54a51860be74678b1c57f16240
+8c18a8efb515987fc1ef7522224dcd42450dd4a195c43117885d66850aa1f701
+54af834ee208ff22b3d49667cd64b18aab0c528ddb3363c1fc375f1f6c133f1a
+5e5ca1e83aa6cde513b11d0d80ce36bf7562a35643266996e4502fdc85ba05d3
+c1a7e39f62276ff85deaa199c3e357757f547d77467886ef66af72968e363221
+25b228b05df472afdf38d64dd4b846df73cbbf2ce1030d8ae964b330e8d7a3ce
+52eb0aa4f164d44d8683a6270e952f984dfc78a720775be462cbd41bc8d56ccb
+383daf0007b58e6fde73ad44e701c23750fb4a2afc174905b2554d8d95aff499
+713856cbb4cbd369455ba97b123ecde74a928afc03e2e11235b0c7373293bcbe
+6193a86693f5db6c6aa905dd539c19a5e953222e1c68007068f5d6b7e232fec9
+4ba203a44ec7318ef6a0db9d255585b7589f0d3e08e420f8ef7b612637a8ef0a
+2692f52d9b094d32e8c197d8c3aa8c671529a4012453bf23e7ae1def8600f056
+e715dbebf72bb7eeaee282c926f4ae871c495282c6dc98f191d03380337c8881
+07e6113ff0fbf360a225f354f0ed4ddf9c97d30fe5d4dd03dd929ee799714ce7
+6182e399f53969cf4c8cc2df7332a9de56dbd19a8704661d842997f06f3589ba
+f63999f94b4c00cf23d1b540818fe68ffe739b1773d8ac1746e51d8606e06400
+4eaed25d729b5168e8f4bef2c59eb953fcb9956d3f3e733591a3978700510553
+8f9c1721aa7cf7552188c2ee521116b1cdf4852ef70b0d2066a2b2e5dc194fe4
+c14503d57f7d7802014079f915f3af34d0fa4b3fd4a0be01952933c8e939eb95
+dba63306dfa0f2ecefa6f8a868e9b513f5ae04d8fa9715e04daef3225ff5dc69
+b6e455f4c8e8670f7b4c27f62f0c9c3083945d538b46edd1c20cca764e370178
+fd0e0753664691abf1be81c197fc26aef772e4cd6e3b3c08b092e84b80946788
+1a00df4bc03890f5464d453a683d59eef81734570e635e4c2ddfa34b41a6736c
+b740641e676556182d98b7e50c56f8bd57020f515d73867fe1a24e098a656833
+38039c76da859391f701e404a0218adb77abb649687e685821f0f32691a82b07
+15ab96d0bdb3baab325089911ea11f6112b69c1e877cd8f2b8dcfe55108f0fd2
+12efbd4f7954149e64e1aa1bd0d0b709f13d032d1ffd5c68bd4fb14fbd7721d7
+a9f9355767875dd2dbf55d85ff6f0378d60e81b3e37ff6bcb6031668ffaa0ecc
+cf414fe67e9261eb0f791a092f7c97dc98f9fdfd212a363bf7473fc107ee5fac
+0790704f6fb87246143e0a53472593157180617772180173d0b0ea6949dcc49a
+3f8ffc3f033bd306ef4b6a9001d34d05e7a8a200016afaaf6a69b5a10e6fc316
+5d9341b62bbc709ab8424b84d25965834f27750afae63468b791cba8dfcc32b4
+158cec29e8112e70bd237c1a180d078070c6feaabb7d35f5167447e54ce4c6f6
+0f55d9f97ce8f1a157af6b49567a16bbe23233e68bfadba699b69b22d70b94b3
+bb551113de57d25e2cc22a3096eae40bea7e353034231ec33554fba3920e3f80
+3611e4dc2d2bb7580869d0bf7cd3a0b24ba6a2cee9210164bf0e14e76642cea9
+bed0b279b01007826296ff79b3fdc11c24beac7857667139a80f0243b16b153d
+a927925575ae6315fba5e54d5f9958004e31a4acdb315b78d08188accf9a0099
+e8e706553f926d14afbd8c39cd805eff90b3993c96405990aa9d189fd66acbd3
+123e94f66d3d1a5f4061b0ae1a032ed9eb5010307a368c1c6104767bab95bc46
+86ddea0248d2eb0a886115e22304e81deafce6e1dfca09a8a8c93e7a44b6baee
+d222d3cdcc5a8b1661b9a874c1bd871a4cce7f9666d0ae94735fa3535a44920d
+6740d5e6dd0372733bebbc603d89cac8450df40be169d9bb90e3480f3659f28f
+5b2de56f7c596b3d3c2b80dd82c1f01196c301855d6217b99bfffa93b9d40e43
+1a22f7a9ab31462d3f14b5dfb0795ca0675cceddb8e7eb010b1521406689b491
+9d264bc1f084564e6a564a76437c6bd4317fcfedb47653f191d6c6b649270aaf
+d083771031afca63850e7a552dc8cde90b1c6556d49c09bcd3284956ff555e57
+77365a2ab6d7e265a1a4f29de8203a2a15756ae595a48e11b958c6ebc478ecec
+29e0ba3a7cdba448c7abc3bb6e21d21bedc9e49e730a5872082e9b2ce588956d
+b35eacbc2f3f41485b0b0403029b7a5405ed1a40d6d0a7a802c0bc4a0b1fa6c3
+32450dc5b1cc97378a2edba812918155e84e78267ec5e73f2c0bbdc96604294b
+3f840dd3d7159999fe24b2c6f297adff2074ae10a9087e2222af7bfa938a2058
+4dce0e6185a493b9c177b8238af099bd737ee30fc40381833e6a08176f3cb0a0
+504270ae6e7701a47a721d70ca5099a2c55e9558320285e4584e64d639560d37
+50993e0757a1419eeeee61f8186f8493034da3997e6000354bf0e07d948afd88
+ed9fb679b76b39b0b4b444bca762ffe47103bf812b63a2b7c26e56ae4b8c73c5
+58d0f7d6971cde60c257de6688d38bcda59d35ccda430df05fb25f2a3c75d377
+d88b1f22eb46731b7b36f8d9ecfdacd3f11f9fbbc1b4587141c4b3b9b0d0a9d9
+bac41cf6c67a995491d52d695aa14aedbbe48d49126e55b2ae43d0545d031e05
+1d52602401c68d41fd21e4faef1cc86a15dcd17c1975b2d841556017a8f39f26
+b6c0c9cf80e7b5d9488f9d4b08564c8ff25c52c16c3d11ee55e18dd3462f9436
+ce1e4bb7eb090929b4816aea6150fea2659389445800576aac75dd801524bc6b
+3f8852f9a999c05c7b29820afd544071e9432ef5cdea3610acb574cca130ddff
+aca7b6ac5a6d86758edcc686be2a61b6f5f2a3405faf7a9b06edc40df07b74b7
+409e2a172430b1d504b041a5c7b0a4107bf60581e1fd5e43a5732ad62a2301d0
+07c3a3489e8ed7c963cb5ea2bb5f4befeddce251c8fa914c424764311d16c7d4
+1e855f4b2fc2d4237ab0457b3a1752971bccd0e321dea65684d0e44a73023a19
+4a9823950a620ea838eb5d98f9959992f48d5ad23be313438385a6ffb29362ea
+a69f16b642066ffabd919bbccf49db54550299476a7143d3d89a6c1a18e94db0
+656a51dc9d77dbb58568b3365ccdd85d0e98097342559bace953b33cde10d826
+8a3839a28d7c218bb4b0a31d105c5d5464c2fa0ae31f69994057717892369191
+3b67d16584ae555dfe1aa32d4b40d144cdb9584cc5777d1e30429f09e0fc25ad
+c87af39d21328dca6c31be361d2382b2c20435e255c6f3014f052e8f4d305a06
+7b3866bfed5eb9b9434909f117bb602c1b743496c9ea13dfe4558b443c347a96
+6665d7e5c06125a8aa4e906f7cc0bc04118a26e9129fc23a63fb0eaa010deb73
+76fcce5d4a3d4ae626517d2d77550c50f7a45b4ade74018b746f5aed001cef52
+fec5e73534a2de1256ada58aa26e5bc0d9d848c8d0a7f42dbd7ecab47d6be1df
+71b7632ba5fdfc28fad09fab97fcdf3ba729973d39a5cffdcec0d1173be7e7d0
+85a59a4504617981421dc9600b2b65fc122d34ee2fab5640893e34ed50f53037
+102330d718f7c2bb7b9f50d18643e04af5c860215f7211271fb2075e0152d85e
+c440a933a87d20a0677ea2717baec45e8578a077805615c7d492db713259949f
+0392cd91480059059dff830b24e2e6e5c715efb4aced3dc637f88cf3ce57ca45
+2e96edc986edce66a831ee92e6e284c72b1307ce4ac479e57014d5114a9054f9
+cf9fba1ae66b0f83e173f31378fa74e8b087490bca05f77da7842d7167d8964d
+c59089eb222f1a33963622a9a17c3542d9940baf4d1b8d68cd4d51f16acf31f2
+91570def9261b67da9222acbeba21a6f3fa905e081be29befe3152b326902afb
+bc6a57c110bdfda8a5c4ce8e272eabdca671be16f4eb7953d1df46e250acc0b0
+674deed452de3b04f258e081110c5ba20a934fa9180abc09806e257abb22041b
+836ab3b73f5ea8dd1dd8cd56216d1bfee9a8ba78d83e2cf0935586c0cd7340cc
+5980e5ca4989174cd46fdadb86398bbcca75c9580310422f692812bbd8a91602
+4f28a49d35ca1c80d5c6c7b879f14d037ce98c0be18eefbca8acccec2fb01923
+69154709b772b7a13d2a9737652a1bea28da5be3815867a952fbdbdb8c45c347
+afb7d7de74620d97b52a6bb27d2b25fc3d0515129eb92be62f7efcadaeba61ef
+88d82df3daf2d1b0a117493075b501ad84ff1650bd7196a1e481c393de324dcc
+c50ed36647ad32f2c8c1820aee9190126c1e622d1f686c71855f8a50062abab5
+54fbc041de1cb0f6163df274e0b148cc24ff66aa16590447b5018474e5bc20eb
+77a870366b3ba577a6f484ef97c606cf71f15470905be27bac6fa4375629f80c
+b8aef8957b856abe4f885c47acd94a7a32b2d81cb5ccea65a52328820404f5bb
+512ae315f257f6196effbbb484ae7c877ca278a1f086e771092915d375667811
+f874c618cc6a117310287382fecd5fad1bf3656f9d41c6b798cc1f6e6f7f789c
+8eab2b1634cc255f999a72ea0e3e201a4335558e1dfc26a8b02a8a1fa55cfbca
+d1bc49f81b8f7837a29d7af533d9fdca050cbc2809ad4cb54699131364977a03
+775148adc0957ecf2d7317ee6b582592baea2f25d926a1738df65b37a519ab95
+a99bab4dc07e56df6e4ebfa0a8e4d8f90693f8de9d1cba77b3f11dbf7b1eb907
+a381fa256e2e943d68e773459c7ae4e7ff969b43d5693b5b612fbbcccc4c5411
+49a3dd6f01ba7bb3d11a331cfe3734ba2b9e1f8a353be0f7a6a0567e1f0acccc
+0e5f27eb18fd60d809ddea01e8d6d1e8c5f59b9554910206ec9c56e7c483478a
+1e4a4cbe7bfc97c7c96f119726e45d23b337e547667281022269e8b66dbf241b
+d74d3cb856da025944794c90cd5196bc8f3a57869c9f9647a0faf133263e96d1
+78ee01be0f6b67db214b2e9c3f10aefa22e7e0dc1ff6d463a2cf77d66218fc1d
+9a0f754c3681591d66f8c455a85e3e7d396fa1cecc04b218a892da24ea8dedf6
+2084965bb34be5e897a6a6c7818f0cc42be52edd5cd7c45cbcfaf89c94569343
+de0c4bfff1f6120684fed8652a071b6587f37aa970b5be3e8574918b97cba372
+81ec8e16a93fd111d796dc5833b76f3f3254a1c126fe28e103ce61a0eb89ef8c
+9202f06e04949602431a5cf57a940c059876b3724e461b7cba19a398427e0d07
+8a88c2eb0f9d12b608d777cc1558522e5f0e96362ee59cf1ee03b1ba9b864bc1
+d186152f24e7e28b92e3a799b23c2416afc0820ecb57a0fc36f44b73aaf6ea3e
+3b952de790af21f21a273a5c0f86ea671ed2ab467469a0ebba738b6257ad34ff
+499b8e21a77a5685dc1de5f21f8ae20d85c6c921f1ca434b67e876e9d3bc0136
+dce9e8d97c769e07e7535ede8bcc169bd38b5f3881f94fc7526ffa91b7870255
+f62e423e2ea2946faa96353152b56ae03010a42f15cea95dd27c89a4614605d3
+636f44785350a920f63f0cbc5566aa9fdfdaf29c109dc83169b3a64ffb0a7c58
+084a88d385ade5c3273c02f809e3dac76dbad71e76462d909348c609dc0bc7f5
+0cbe05994df34bd38feb8b5876cf5a50989a44792c36f0a989084acd73cb700b
+64103a290df126dadf1449a45c713a71353969d41bdda892871644f895fef631
+15b63299dc61de72c7201d8392dd3f10b819b792ef91f1bcddf9f04bf3b94c25
+ce81c6edced5ac26c9cc0e304683656c9ef456b1b666bedb3fac1c597bff1d9e
+932d05de433889db501bfa1abf7158b428b24a0d49b467b3a38f916601c9d36e
+22374c7c96dbb04a5de8ee1013ec7d8515a9eba05c41def2b8af7bae6691abad
+51b58b489b3b20f3276878bd520e6e70a9005ee6cd9651199e71269a9dc9e1b4
+cd24d01d37448b25ea34d6e0ec921c4c1904b7c4deb93a5c6a0c736a501ce7b3
+5e386a065c1f5df136c8bb89103febc7b9b3de8f74e0504fbd7ff702c5c7250b
+881e83499ecb6d80b3f9d9d17aed8414a6e723603c8762a3409f6766be726bc2
+79f70435a56a0e096209231f5b0c134bdb7aa6cb3db9895720cd02b9dc0476ca
+90c7a9ed37ff0b380348f66d85037e9abc95499cf6a3006a40c3874fbbc4efbc
+93b261cde619c9f86a4203dcb642e59c59709dfcad0d15235264fc7923227094
+ca105019b1bd20e4bfe5f3ea8edbc33a7bcb6ebc523533228ff682e8a1150aca
+fbee38b10517877329be50b45bd87df864b02c6c8cb5125b01e79c64048ddd3b
+7bc817f3998adc78819febb2d81c7cbfd179908cd36b7e5169218b449cfd1a1d
+d0979eee4aa2c2f664b9532775113f6d528c0e7c1ddf7f92738b1ec1c903710f
+9543712c0133b208a9bbafaca0a5ec4a087a1e5de118fda50b4b79a658e73a04
+ea556ee2b9891278e414984da3dc6d739ce951cf058e2bd79d671877c4d5680c
+2a757744cfe0cf31bf587043af1acf031fe82e85f1199c1e606232edd4501949
+dc70516a3c30b356f35c2b4e14fdc62abc3cef6b7703937a1c5561694b2e2022
+457603ec1210cfe44401a8ef8e43ab2c2244428c4f8138828f2a7646a8482b49
+00a7fca8dbfc30cf7dacbcb003d06c36644d61e901114cc5348ebf805af2f70f
+84398891f59cc49846e23463d89ba78dcadb3efe0f1f00f4b45c40bc0d2f3cd7
+63c231487557e68ebbb01cc9f96f246e9c2634cecff4a381dd27814e07cf1eef
+75a68378e92e56ff486dafb1bba124ccb1f2e9dfdf43c5fabc0336d8f237f9c8
+c0a656c7691c674c0031ec4531b6e7adffa69d634c9af413fe014cb6464b4ba8
+649cf7bf0b6ef90126d312c361fbf4618957798879d967496ae34ecdd53985be
+1bcea07ff99f19a7bf3dad5bc31e1941161e60b98a7f78316b95db862315bd25
+c46474a7e5cbe24c54f54fa23b299901cc2488480d258db25713dc97ab0e4b5a
+d66bc1131efb7296f2edbf6d27734cd45603cf5c1e0915dee25996df850dfa31
+13e23c722aef49ad2be71db794c86926c13ad66f17d8b8de2a4b66d9191afc44
+2b4e95d2cd04cf23374f172a183ba3d6adf8b2f919f14b4ab794f53531aa0da7
+fa0d5ea6f4c30e24db53640d51248cf1001e7330286c1668a7250d3a0d34f84a
+7acecf297caec2198b56eb1b9263ba531658d06a3d9902c3c96034cf79f4038a
+2859c9e47088ed6025d59a69abe6c86ffb467b4d8b44028fcd099729e37bf8f9
+5584763055731e7d7ac8b76765955c5c73c00bdbd426950f69ac97f55f9f9f2f
+bc217e166c442e627ee893965fbc7200396403b1f52693f6079886eac12d3553
+a02944981ead829e3bef62ce5d9274e039e305c48b23c604a86e6d816b649faa
+ecdff4777472f32094796aae48960b0f3d74e40fd1e4e72bb37e54322c126341
+039c3b3fc15a137fbee162877a2b1a27dbc2cb91b9b8cecad345cb00d519c051
+f46df1f96224e95646e02d4af7739638c5a261e36e46be731e3425d8f62d25ce
+5a7f1e72abf75237bc7109d0f6503b684c728a888ce8a67d622a298fc09d08a0
+39b7508db67d1fad998be17d49438696e9c402bf6ca7e5ad7ca7a1450e04d17c
+2b0fa2feb0b9d5af5dfa5367faab7b3e2f104dffb5da896d00b890764b83543f
+70d04bf1fb56fdf0a5873edc4124e42b1546dcd43f4896a2b313617c5675e84c
+1fb558d1b944084e431c29a8b1b1ea67f22bca2ea0e503c422f13d3d18c16dc2
+2f8215c71346762875d4864e20c107f50d9b42963aa2185db553e60af6c6a6a9
+886ed38b48052b0cce1e5b3f2f5d96975849eef01399553443d2722f1fbe8266
+aab7b8c1d69ee6926c1f916c62dce7a293589e10141173e30b5790f18763f1d8
+e1168cac05882de798bf361b1ec201c9f5fc970e5bba72515ecf3ddfcb14a6d6
+2f38cec6004fca8ec75d0b2ae1fc8bc94d4bac124c97b770759987d4322ae181
+55c7a1d7c0d3005385eeae58b8a07c556dd439b676b496682931327031d68f43
+daa631dff340b8531a8f1cddc1f7061dafd88109b78958d52acbf458dd3ee2a1
+fce78a3e5a8ad9a5314464a53699701ce17dfba2e176e9ac0f34e1692224926b
+61782f6ffad4c6278a32b1bdb73cf6b8b2f5646dbea6de31b2078cad5a87dcd9
+04ea4e60601f7547c1544b863ba383ee95ceb4a42ce89e815740e0abf8d3087c
+637a29e0645de6f2edd027cdd56e8b558dbd26e9582186bd5b4792500926f8b2
+5d0dd38c7af1ce0e2d99942b0099757140f9dfbd514c38e3d15e502dbbc34c0f
+61e3262e48f80bc3b6f93ac5aaab909cac3b776094c2a101b0ef5c891e28ce69
+9ecbbb70fa9a71bab19a9a4ae49f9316b7dbdfd63b43db9aa064df4092cee544
+7507275807a430358ff0df98f2fbaf716785742ba18888994a8e2b54aa0f6fdc
+07feee88c4c98951495943bd42b395b92b545cce05be3db4a0ffc43ca064ac4a
+9baad9d402c97fb79b8f70184e011f78d8e96538f4ef66d45950a80d186f3b96
+56ce530f019a1269ecd2249f6514a6b77e486198ed8e4c28760a6482b1d3bd8e
+8104a8c964154d8d23fb33f0028960d92c5bd3dfa6bb390f6dc708a9f4fec7a3
+7327a97723acf7b62070cd7dffb6aaa77505a05fb292c54baaf428f9b81e5ded
+8c92353e46081e4fc0edf39c53ad0e64a29e34aed73d18e25d0fb73134ca03fb
+382c1f2d703e241986e9bcb36f4a9a45bf183e5898364d95369febc1c8867d7a
+77b76f92c3668ace60e180b685698d403b86e27cb9725405955fecf496aaf327
+e4f5e75ff5123402b23b0e8c5f7cf7ccf1364fbc8d9bcb94f0f3f018a2902f61
+ce2e4992a5dd21e2432a6f412e77b13cc968355f9298c0fda7bef338086560a6
+7c774e05aa3e6d1f0a3b61bc1ea89cbe39423ab0c3bccf10e091a76563ff8b09
+c509f9f69409b7bbe8ccfb574ea7a80cb1ff7502388b4969f91f319e0c609bb5
+c3ab477330dc67d7d900f2cae9890caf14312ba25c8722f73ab0d9b0c884aafb
+b548a565b92af0ff6940d36d40442671cd9513f93d19107cca2462d5432696d0
+97f56f580721b0cc583aee69922f3d0209989ed6e6bf55e70e293c0c3e89f0a7
+caa0d50b519ffe06b5e86068deb84ae448d5041d20e03955b5bbf0d78ae45bbe
+2e548c64687229f1033fe264d1eddd5437e2519aaf8d1f7f2b8acff36cd650a6
+3cac39cb7c215c7b34e29b58fbf5d8493cc1bbc9e4dc92d8df9cbc28cb3884a9
+b5031e13c12ea77e63e74504fb6609b446ea85900cd8351853e71bbaad97ef8c
+14c8691ea4a1b39c94b7ffd97605796e7125ea5a6ef7cb631ed1d7805155f6dc
+d64e6773ab236b4c9eeabdae300229d4710af35c88f3a61fb3934dfcf4756533
+bf1a27c68f82bba998f2abd86aab3b741fa09837a3f32cfb5f0a35732c7df553
+1277c984e1c32777107cfbe7962971b6bc352ef17179d94a3accaa0730f3d360
+ccc9e41a9d019759443990c8ad253b2726872bb53a3e6217b3ecc57242068b70
+1116465f90b58a16e703ad75c3da86b2ca6f6e2f88ce00a60175843321e6e6d1
+8e1fb76455d729634c196a8048ba1d7e399b7e5529e312610cd6e38e11de0500
+47bf6d8e7fc899e90a40f639ab3ed83e4f8614245ae7929da6a8872a15b3b6d8
+c0769d8468fa9911fda9fe875c7f4532d2cd641d8024b1b43de1d9fc0cf0dd55
+0337769be1a0da6afa42b12c8b3d21ea450df447196ed23071ecc65385cf7111
+d025b3d4d396fc2ff5f814fc7b249b88c012765d66402a3cf926e3769522c650
+62336560b22fd0865350c75541ddece29da646b137d467aea2b14e0c8a9d004d
+c914e1e36247f010d5acdfc06a5a86477392e6b5df8bfa0201bd160c16edd42a
+1611d8569db8d294d64cd76f9132970e961a57cbc9478fa37507f44197d73490
+3a6aa1706d0e0b4115e3c6bddbd4cf4be087fe8cdde3a8d764b299fcaefbf13a
+2f480e9385aa9db1d4358cecb9958ef760f6dd14459f2a93bd2d49e354b7a672
+8e6bd45f17e2c6d86512f24234f1e8df4e5ebfab6c094504041adaaea0139028
+5b4223e9cefe6e67c7a055c9b56402202fc3964989ae3d435ab73b071c022aa4
+6c2226809e1c38f8436f42d3ac3a262c94cd71383b50dca30ca2d275ec463405
+e7de48bbfcf53a152083382b8426ce804c958b9aae95a13f2575658991c91988
+e3af9fc6d24986700798775ed0edfe717510f248d3fd68a5e229c9862f7e5850
+1527886041988bcb5fb8e76b80a5292e982f25d51a97a594c4bc30a0cf3739e5
+6381ee320c7a2908da6732b29184ba5312b24e0935fdea476a5aeaa75df407fa
+4ebf181d5a15a5ad7d0561ebf0a4e55c220a9beccae6fd830c0fda764583cc5f
+f59dd84c9c2486afd7039eb22a657ebcb48c5ca9053f9c6671cc6cb448897f01
+a33036fc2438c9afec583261506e1ce938894644af8d14b5af8b020f0061502c
+685d2919d5babfcbb1658002a2b5b6f5fe8636f4addbf97dc7c28eb93dde4f93
+831cb9aec8d2d3f6173f16d4c31ae3871e4bfad4b652c0f5e11af2270c983cf7
+47e3d8ef4619c2d76db67ffac11988cf3e7f6a2976305532a2a2174656a926fc
+1e9dbc981c19937c1fbfd11871ab3b96599182227ba0f840f29bf8533d33e4c0
+052e60cc0a266a97857f7c6bdb412ea779b4094a5ca237960957dd0fa2a37c66
+97db26e03b892bbf9ea9b6e39d131f8ceeaced68e12b7f2ce9ac64dfc61009a6
+a4959924651f7158dc9c25f3e603629c979aac5c308fb57a460f4229ec663d88
+51e6947cc40678dfc56e8472f021d3264c06a3b76cf1f77f4b4d0b1886039d3f
+e166d739bafe7cbfd72331c343fdd7395cb8cda3a85c735bb35a286090b6055b
+62808bd999289763d9e433d22573fa3f069f40aa45df44cd9e17e843e9841828
+d18b10f2325aafb6c872e26486f9c059476a50a4a3654ea8ae9e7f6fb7496a4a
+887054819293cfe78eb82ce32bdaf4c291faee9b01a04ca557cb09e614d0568b
+4fd1eb0cadaa3014aeb0325eb68606f07885a5234e54e76ef6d9769b80e640c1
+8b21a8f0d80f7029228c704ac5d747edc455493f33e844a3b450f36141417a52
+0677fad5d7812b65640113f8daf1c45aac012f587719d26e84e8f98cdbc9e3cd
+a9e148ec08d03ebbc9e875b21ae8a3456a032d39fb1e726effa0b5039871cd72
+6faf210bc4927de53fffa0d987c2232bf27dcecda650bb7d7ebadaa7036079c8
+0f067aa09ade73b0f22f92ca57ab4a1873ed901c7eb934fef1524824604092ff
+6f255608a2ad18636b7f69d842764c672d3392b35b7e12ddc355aaec9023f8c2
+730ddc4e7b49e754e7550c70a21b5bdd7acd0bde562b34faa2ecbbded4956cc6
+45747aaf479ddf5269180428781e96f3589ab47bf5d772b0a6a01eda4dbf27f1
+49cc8d9fc57e95245f9da122f1e3b6a31ca21d7bce9e846b3484bc32310902ff
+acbc3b5e1191ce9a65878695678a3ea186d03fa6cd13b1743f327ab65a848d30
+1dd0abd9aac5b38e9f727365120dd5024f3da20c5cd4f6105490e3e45df85ac1
+300a041e5a5ea1d8d882bbb1afd4cd7156f19a48f43ac58cbab60674b4daaf45
+7528e5c201d32ca7e06ff8719b79b94d6c571a99221e5ef00c8bcd7d82ce818a
+e2c4f8b426438788ba84c02e7c9395b00f9780b7770facde25094236e106c405
+14913fcd39c2d2b0d084931cb5b4ebdda3d91d8a25ecdef23154779b30a277c9
+509902c229d32557328bfb32b20561dc38e88fa16a9ebcd74377ee96dae69167
+87481289a5d322e9c34dce09c833d7e479200afdd9998669b16a30264e72ca70
+f34e79f2c544ab96c323dadda4c9900737121d68aef6978feac182fe1214b1e5
+e2f3321680eb5f0e6fa1f2bee600b07db3d9546954fb6137443956de879d58ad
+cb7d8a870f12a58c5475ab59790565de421f59b39e643ab54f38a94336cc0f6e
+c5c54c6fe6f61d6c6121aad3b9b1d66350061d1679e795c252dc429647c83d29
+c684a07d0b41c72a62be91564cccf41edc5b079b2f06e6a9236addad84d0fc4a
+b5d3403b42c9dbe80d119de3855e1b17498a9c2e827f4a8b85736371e39bc01f
+7d7d14421219d136d182783f5d6f8aadad83109f5bbe65ce504735367ca9b26c
+40e10288fbdea5d46532331eeaabcdd5ea937f1537796f43c7a3ff713ea2c86a
+2f325acf06ef921222eaaa928a4dbc95a9a720bdcab273737e7753294e42a8ab
+3a27daddd130ef705f2702702f39c84de8e5de998f507bc54a69de88621f1968
+bc135bd14c3ce226befa65d8e117c7dad682ce6dd839c443e5368b1dfcbb8cb1
+7357d126b021a47bc4b3eb4967183f4a5ad66bb4ced909993a13f365604432ab
+42bbda92d93d132d4194a64717c65d6bdfad697e3e2c1b2962382d43cfdb20c5
+ed6a61484b3322140ae300953acaff45077505701f7c161f108b902734803f93
+b6e89a2cb7640388248e2c263135f0dcf9d6945f8b648c89ffa5d39911bf196d
+24dd049e561cf823988605fb0b3b2b960be8448dafe7cb335dfd8870f2fed3a5
+ad8784ec4c5835f2aaac6a5f7fdf8f15e1bb5f28296f6af7f8fece5ba290030b
+885e30d245ed99db82c0d669ac135390af59032d6b44b34cf43df8b7e3434bec
+eb8794acf138dbe43d028c6bfb9b27f2690324f201352eaa4f3ae5601cfd4802
+81180cb05c6bb3a90948f261ae1e31edd7b34dd8f1dd2451eff15bc7b881e47e
+98183454b03554d5aaf541f087bf56ad3087681521bf3c7b84c8047d3c23b02f
+b80a55f6a26ea426feb545f7075a84d903e1ee6386e8bf13deef625de5ebef1e
+5d693c8fde15c264113394b035931bfd00308cb7fe9b84d4b18df639a65f454a
+9136598ad3f5fe4a71e2d7ee28ac54ab4735957cd1aeaf5b91247426a706415b
+1f974db500682c188fecb8ec291a81647d09c3326e05a671e6f406140d94a86c
+edccd20538009a2867c3fd9d8b8a703de2f9e2dda4a495f6c8adf2d8da8e06ef
+9f21cc11807dfa35a9732f9fe8bdb8f2406552ab3d67621d23bf5f425b4dbdca
+79a2796dd74cab6305c5a8459e54f5cd6d8ed1768676386d19e483f257b9fcee
+1c8d9a0ad09997343314ad22fd713a49b8a7da1e2302ab57360ac15abf917f4a
+930dece02b588a0564271e207746e4e7c330889a3f272e85120c7d1348379a95
+80f40a595b1a0f939aa46b7eb17d07981136851acc677e78e8715639357668fe
+fb162d0bf9e7b58eb0a0a8e3040de3cc5d6ba9236cfe5a921dc2342167ec9d9b
+1a6ed1b7da0da8474ffed17740221cfeb7de9aad199e58bbddce88ba826a14aa
+9b11abcc6f897453ddca5565a096a79125a192cfebe71ed37b5810f13fa07e1b
+286bca46d8f8c1e76c7ea100c2dcc5501653c7cf156582152bb6aa34ae041580
+6292f09c7c3ccf7581a1d1828a99866f5148de42ab060932d1366c2bd8104053
+f5d3744c9d825171812dbce8ef600b937ec0430f4a6129d69fefbefbd94aeaa5
+12673d7ab07f05528c529d4552ed20f00e8bfe54721cd959ccbca7f5c1850168
+5d5ab669a3aabbf6d431a429696274df399f3cf3760a4ac9453d407b211b2bd1
+b8416c1c23b3e68343ec28cc5e520fc2dacff7d41274aeb8f77beb6174560e35
+fcbf7fc20042393bf52b5278402ac5cd9f2e1ecd5a9dba963b9acf08d78cc5ec
+33bf5c620b6f5520cb781a48f247158899cb347b977cd62c54cad66c7e6cfade
+433e5be9ff8af602bdccec15daaa4316c8289921d19f809c20e68fca2b71db82
+df2bf6f0ab9d90b9830d399d838547fecad841b07fc429abdf02d544b41d5e31
+7655829d65c3c4eceb08dedbc1b2db023ad5e524ac9cfba00bbfc2bb2df14d60
+954d55560fa6e2ff31786acf9277e5e2018ea2f839873e3a4cff611d83a1cf8b
+7ea655c6bd34c7904818fd857cfcf4a9b39e5fd5cc551655dd75602a432a077e
+653d3096ebdfe93e0c7c7fdb0e52ac0145a5857d5bc46daccc9d9bc048cd6e56
+1ac2f5cac16d537abcf20dfe3c6aba54f400573da27a781dcbcee2bae55db664
+18f8f003aba535662a2c891070ae99839b4e0fcdf5ff9b7201ff83bbc9a5d39e
+bb61de440b63d08fe86bbb51e251a65501f8ace59ec1907c1268c639012974f0
+d8c09babdaad610d2c065174687fa1d783dad6567f1bc9056b4e7f90796ae034
+529735bb2c26f89166b67385c0b3ea8befaf0d3c5d2021c5e95f332659f57c20
+7b74c058fe712f9f6f7bba2afa24d922819590e2fdaa7b44e2b7a401fad4d28f
+05ab33026864f395ec60b658d42784533883b1cf6d5c32baf24262d835be78f1
+eb7b30ab043abc420085fc13622e1eea3438d9736c4ea328912fe1f587d4252d
+bac29ae8f47e156b7183c1b79034bbda31e08619fc106ff4ab743bf8b9d8bfcf
+a8ab6b810c1e1f862201010a3e3da3ebb9457071f5bfc811869fdfa0ea873a0f
+fc47d86b663bb3309bce467a81d6f757f62a195f7c6962a729794666635c8703
+db1e2d71ae67d5dcbae3cbf5b948eaae1e7e7ee478a0e4584ed70a019449b463
+9f812dea0ca7e191c9698c48defd784c76b4692337749365039b13055ad9114b
+1a456477c879893ec3bdd4d34184ae5f3e7bbd8b8ac1abeb315540673a792b21
+6473e225bf2b925d77276d0741cc50fb9ffdd34459642d337c044cb4d58c123d
+652dffe4dc9fb7df87bef364cd7d62d8fe7046cdcf4b387798754c409fea2867
+c3c3e9603e79d53c02465ed8b57b752f76477dbbd1c8945522901478aacf4871
+c3dc4aa90b10c08351d608a02d168a5978fba8eebb48ec93653a8e178d44daba
+e42ec334aea08544a01dd57943e050e87fdf8200d2cc475c02dff5ab8f90e58b
+d3efce06d49ea472df3f149b629059dd9e839536fe014503ebd1e6acc1c35637
+bee2f01138614af7521c7a9b0ad46187f168e387a095d795c067660f71b43521
+7132c561f442b12f84e183d1f5dc887b3e38f904dfa73777e7bd7d3ab6e91fe8
+fed58db8f42e4b65856d633c75e5c9f27fa20a7c352f30d2ceda868f61fc0548
+f2ef8ebcaa18fcbb4439ad597a2f2d89ecba33181e770d5e8b89d95ac0c9b137
+a8cabedafbb3a2071a0484c54dc24ba3b00314555dd3268690308c839cb0f0e0
+410116d5bdea76dc4e37d89e5262df1a2d5214bdc3287a67e7657cf5b5f66892
+0e5b549eb0929093bc809afe32a60894d7d5dbcc546b0664cdea07442c75ad9d
+8bcd96bc17af39925125ccad1a305829e9054454ae7db205fdb3c459836ae540
+ce773785b55b103bbb400a6472c14b125d85426138c06d67c5944cd0cb27b224
+e1dba5ac9631e37b2a3ef25adfb0271f729f6c4b9402771e0dac6068e12a0df7
+7dfe8ff7eabc22211bdc35062527e1ef26da58a905331a5062c66abafb46feb5
+05aa06ae1a4d7e88143666dc8a7add3f049fdede30f7785f968dbb0ea2de8bf0
+a08da2f0c9473885963b710f96fed7751c4ce01b4f954e486325488951d4bf83
+01662f9bb15f48623f8b61c7bf7dd47fc35e73669f03286ac8b1d714dfb24415
+7083b0340c5535e329f6a114070d01df96675ca34d6a0280ae26e81a64ce055a
+8261cf5db776cfae5cf1fdc3f8bee14efdd08b832ac93ed7094b7e57a02b86c1
+574ed345d13e9f9ee91bc99fff620fba401de9c6265a1ab696a581e15a3e5d56
+dc74fc5c276b7d1136c94e1816aa95c0f3207ee25769b17948d9a4aa60f7968b
+e033c5e124780bc4f532ec96f64e542a0851a75664b073dd16034e76adc06931
+5f5f2492fb97859d59cf2f6fa3a6053caf4013c4f47469b9bb1636e7017a0c3f
+5d90c8d94894613b51895a11f37e9f31fd98a37bbf4cc1e01e5420556160dcc1
+25bd769c66ebe645bfdd4eaa9c53d414ab9757c860861eac507a612a9309e5fe
+e75c85868fc360a41846a8fc79b60a175606bce995b584b5d645f9223b7b3a3c
+e4e249600921f783ec8f13abf5aaa3b5008389c46da5721dc304f56d7ed99f40
+264d7c378d96a6d1a960cc2e412a85fc3a2c47c6f70edf2cedede7cf39d8512b
+9679bb345fbc28716239bd95246b59d21a9d8378897e9c7607af76ce59e66ca9
+d07928d73ca7744dc5cddfbdac11468497cfc89dfa1a24b120753229598af48d
+0d2e6332a73189656cd1f584496e6f3b1552b04cdd6e51a8388dcaee3dcf8c88
+072651ee9cd44e3cf938172570ebb994bb821bc04e1a7723e16ea720bd8cceb8
+af7ddbb6ac14a58e144ebc37819ce9f2e78c0b8e5f68ed48a5529e47b2d9a7d5
+b381bbbf863a318d719ac4e678cc1092f721b5ca28430dd1b597004390ca2a34
+a54e73d1d8a556c14c4f04eddb0adfa43ce6ec57c8fd98ef7abc2cefbe991786
+a58ff65ee2bcc0ac940a0f07e367c032f0a306375948b54546c7379053ce25a5
+707d9c54aaeabe39fadadd74e1710f617d751f7751d396c5d054d28f93314ea1
+0e6803b627f3e1a1b2a2c4f14a8e9f9da3a032f7bfd872f86470b2f50e7247f8
+97ac03b1a4fc9ae5fe6dd088171746706b39432cacff853edf1b2fe87208a44b
+c1304b88b88f70b2c7596cf5ea33d9d11317da7dd2b8497aac6100ca08551dd2
+a57ce4efd1522a62f292caf6e273878073c82f48a6c9f10c42be92c439cf49aa
+8d4302a85a9f6f313ffeeef59e54aac64b7dbaa8265613f46e62d9a93b3e5740
+9eed68eea907fc84a6ad170b8e1ca16b62e46b1067f28eb6b35756c71443e0e7
+f0ea70df14dba384368a68baef33b7b2105085d179980d6622db47c1dd4f027c
+0fb25e8b1ed04be113f6961107c235eec39afa4db672fccd5c11737b22ca2a50
+af750d838462311c1111d31211fc41b76a96197ed5f62505bd627efc44c98cbb
+12d82f9969735d8f4f36e097ef1cc66629b901cd7d530d74521decc86ccc0707
+07e8a5208fd26e0fca9dc03e4b5b5bb6ee8d899d439457f139f7d2eaa4dc6676
+a411a40412cf8808cd1e9ece8df377dd30af8dcb28e8635c0596c9f68728a877
+671ff5872c81635145e98e055dbf7eff602a4cd98986e49a26c3754b8a264b99
+b53dde3ac4448ba76a550df75661094484ae263c8bc0613152890d17b1a09774
+a16fec15c779257a9038a6b81ce29162050e1fcdfb9aa0c88dad199a74470938
+21b4585d40e628823c42a45bad0c67427ae3dea6367b69d76820550cf3b9820b
+043f6c646cf42d20f1241d0ce8196b2c652e9e10278545cd289bcf37c813fe83
+1a437c6e4991750aa4c7970e200b86ec8e26657deaf058e633011946e8569c27
+fa7da592b78df95c94607eb1779393c5c2b4de50c425384431505a93cef43c13
+e042c12a57eeaa2c48d002369aceedda1b7866c865c8f043bc4e50d824d2e312
+04bd8c1a4d01a688bb2133a4ef37b858fe1de27fdf8e5382e4a3f323e016e151
+8b1da5df2706b1d76b72db59fd5753e23b41306070650d6990cc2e7ba3014d2f
+fde0d8e36ddc3de6917bf3ef03c84f8eafdbfa7dce1c3007bb57c3860f65e991
+13cbca57852b97ed21a7a55ea9f0803d13accab45fac1e4009be81d3da217dae
+45b421d6a76150d7997de83c0aec3d7c09d5e0cacc5678f2834e21c22aeb8ab7
+aa20ed2112bf7d3583cb287740f2e6f828f9d07385eafed61527d19055cdb129
+e1e5f1326edb56d107250346184e4580fe83bf44e2343cac0f366e04dc9aba72
+0261224f33ea17f779ca8532400edad7ac6a5868a0251b030360b1ec93aad4dd
+04caa52edd01246e2e275f007849b317394f9ef09064dfa18fd3c0b32ab4073c
+855cd08ffd407d937292f54b0857c281b0d7dc4ded8070d8616e92faac8fdecc
+710a60c61f88319733d888795c8b45ed895fd29863943b6632336aa49c2fe365
+752b6463cefbc1fcec8e8ace944ba4d35a4dbd87a519eb5ce02348e106c3eb9f
+a57577ba58e7d357e55259f182f72c426eb9e12daa1cee7210f75cd7519fa1bf
+80b5b2f3bb7eef8348e39c270336f4c96a4e458f71699be5eb94f26d0c7ae5a1
+b815789c4c038cdc8285c6394b442ae05dd2322f5aa943badddad4449606020b
+f0f63647645d0c1cefe0a0e4964c2f2c9df5754c5d671182d8e15774752e06e4
+f9bc5cb91fc5d31fe4fe2f9bfcb53b920c22ce8d0c244851c9433f2d8f9d00be
+47a03a8d4246ed7a9a9bf27235f7537d071a6208b6eaca1b2b6c7c43e4354a23
+b97ba4ecaa7d93fbb73a4bda88c406a9a1fbd9861252f03b2385c3517c321411
+2f968c018db6e3e3de8246c05da9f4dfa2b5d0f2cd2fc3ccc8005486b9c1d1be
+e1e8c14ffa77b18c024a98dc5a38b6b0e057d80111e7d2296414e87276c13da0
+e550c489381ec3da91eb4dcca6affcec210ecee0c8911fd338b9dc590d17896c
+9f96dd780503bb15e0638db81ce3f26b1857c3705fed78959c83ba079e49fd0e
+08b3a60d8641ff061398f779498633441313aaad5d54e4b3fdef4e0ce6013b20
+f17c6b7b1bcf14289c52cd55b0c456a69960f32fc06077819f32bade75c69b23
+bc97b0dc78532580f9fcb1a7d13b2dca90785a7e958b579d397a5df187d4b0b5
+f76282b4b00e94c92cc3653a047ea5a003a72f2fa4053e3e9098f82944131394
+8c471657767911fafb254abb22cb50693c8c644096a922069e2e2498d230bd19
+4d2cfb8086eff1761fa2b13a0ea61d6ee0923c7c4d31897eebeb1d73982e21d9
+66f303ae3ec4dc108f8ff4da3bb381be3f4050d101d37042b11b314535a1c370
+1b3dd8305868241efaeb394ea2d3efbfbbf3cadaad79416bcd47f8c19cec479d
+19bc73567bfe6d55713ad85a42337200752777a1af0bdfe46bd53534c4fee290
+c23aa58863bc3d0f1afc9622c886244c3da73f429f8a130510505b554f1688b7
+709b0282c94e36468a0af39d04fe42825369197784a82d13011cb3d193bc40d5
+5ca5393d2b32c57abbed3a0bdcf6f5dc0bf26c97aa46daa4269d6e2e104a78a5
+c96a39b4b121958a0e643244f646480b348563e417335e5703c368081c178054
+293bd69b1d6de52d140ac2dba03cf45c83dcadcddc046526b004373d8aa9d2fc
+bd3b354ea1136b0161a7de8ba06051f3cc22b028a2f76b6780f74c5509c49268
+0ac5e71a1578e344dfa147d4136d57a579bbae76a482d742b44ca381a96ee349
+613690f1a6f8bb1e8af1df6b2555681485122512162894c2e85b2b9a3bf5628c
+2a7d925e263edcf66eda45022c012d0ae52faced11324543ce842b5c6349a832
+d91999d162fdeea3451b87671f83bfe321726a6244d6f948088c0392d140d0d5
+5931ea72d9f6ef32bfbddc2fc6ed0c50decba208beefbe2da0f1b6d918f55ecd
+be29f3017d358dcc4a3daad96fbd6edce1a0144dcd3bac07d163935f8580040b
+426a9434da7a234354e273f62500f88237cba7771d4ca0f50a506f07b7eced7a
+6704154a7eb342bf879fc79f7bc6c488a155fadb56df553ac0b5546cdf3a167c
+528b6ea5f65f0e02c863a738ffba4c4a0fb67a788609bcea6e504547c6e66cd4
+45ac2c76dcde706679ef2d509f7f45624afc984e0a3f2bb2e1d1364b038d214c
+11260ea09afb681a4fdea1df8aa130bfe08ecb071e7fdc72c90dc1b114d81935
+92e0b7a609fe06b2c805730195748a02b1879ee6e350c7837c109063628d1f76
+e39e0b4fd212b2da075c82e052a9f66be9446c306a956f412a39fa903e430eba
+d831d86c7f66765f92137a8cf309cefde84580726dd922a4defc48604b992684
+a793c7a25a21438dc7eed60e0a0c82007888457ff63cbab3184d5009ebc30428
+b5c1359c3fd71ae27bccf2be4eeabd9ad709ac34bc6afeb60c3afecc39358bdc
+5b56f65b2768111635621d7797882dd1db755d26bb2e0f03b1a8d80f902c3960
+396dfc9aeca6ff7d42e7f6b4f83d1f217a68f3affb54e7e9aa1d0524e1bab538
+3843852ddf61b2ad706af3d609a8d1f841f9e9d12b1b0f92f3b47b0dd0bc5f04
+866528c0f63db3feab76645cde31f0283fb8c711fc34504697fa0cfb8cd7b150
+172cfc93d64ab6a1da1bc96d501d49ac87cc6a04db1635c167a1fdb58eb9e3cb
+94c90d2f9046890700513232d5bf1678651012e2f817114f46a1019e72220d7e
+d216d12712bb8ae73140aeeb48a3339dc1d5a6efac1e8f50c92b58e32725424e
+ba86033bb1bba686695cedd75be66d54275c8ddecbdcbbdda44f595e6b686af6
+f6117bcef51df5e3c98e90171952a23f445c2bca9b8626400905fdce9e0464b1
+c1d241ae619844513e9cc3a58a6f978089d209bd775438d7b87108a342c76b62
+8c3a6a28b9d0c42e696f3d5908cb2c70d8d3ead811ef4dd19023faf86ee053c3
+014ff20983774efe8e26646abda4954ead06c80c7670
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+cleartomark
diff --git a/e2e-tests/cypress/fonts/Type1/UTRG____.afm b/e2e-tests/cypress/fonts/Type1/UTRG____.afm
new file mode 100644
index 00000000..d5fb72f7
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTRG____.afm
@@ -0,0 +1,1029 @@
+StartFontMetrics 2.0
+Comment Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.
+Comment Creation Date: Wed Oct 2 19:10:44 1991
+Comment UniqueID 36552
+Comment VMusage 32987 39879
+FontName Utopia-Regular
+FullName Utopia Regular
+FamilyName Utopia
+Weight Regular
+ItalicAngle 0
+IsFixedPitch false
+FontBBox -158 -250 1158 890
+UnderlinePosition -100
+UnderlineThickness 50
+Version 001.001
+Notice Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.
+EncodingScheme AdobeStandardEncoding
+CapHeight 692
+XHeight 490
+Ascender 742
+Descender -230
+StartCharMetrics 228
+C 32 ; WX 225 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 242 ; N exclam ; B 58 -12 184 707 ;
+C 34 ; WX 458 ; N quotedbl ; B 101 464 358 742 ;
+C 35 ; WX 530 ; N numbersign ; B 11 0 519 668 ;
+C 36 ; WX 530 ; N dollar ; B 44 -102 487 743 ;
+C 37 ; WX 838 ; N percent ; B 50 -25 788 700 ;
+C 38 ; WX 706 ; N ampersand ; B 46 -12 692 680 ;
+C 39 ; WX 278 ; N quoteright ; B 72 472 207 742 ;
+C 40 ; WX 350 ; N parenleft ; B 105 -128 325 692 ;
+C 41 ; WX 350 ; N parenright ; B 25 -128 245 692 ;
+C 42 ; WX 412 ; N asterisk ; B 50 356 363 707 ;
+C 43 ; WX 570 ; N plus ; B 43 0 527 490 ;
+C 44 ; WX 265 ; N comma ; B 51 -141 193 141 ;
+C 45 ; WX 392 ; N hyphen ; B 74 216 319 286 ;
+C 46 ; WX 265 ; N period ; B 70 -12 196 116 ;
+C 47 ; WX 460 ; N slash ; B 92 -15 369 707 ;
+C 48 ; WX 530 ; N zero ; B 41 -12 489 680 ;
+C 49 ; WX 530 ; N one ; B 109 0 437 680 ;
+C 50 ; WX 530 ; N two ; B 27 0 485 680 ;
+C 51 ; WX 530 ; N three ; B 27 -12 473 680 ;
+C 52 ; WX 530 ; N four ; B 19 0 493 668 ;
+C 53 ; WX 530 ; N five ; B 40 -12 480 668 ;
+C 54 ; WX 530 ; N six ; B 44 -12 499 680 ;
+C 55 ; WX 530 ; N seven ; B 41 -12 497 668 ;
+C 56 ; WX 530 ; N eight ; B 42 -12 488 680 ;
+C 57 ; WX 530 ; N nine ; B 36 -12 477 680 ;
+C 58 ; WX 265 ; N colon ; B 70 -12 196 490 ;
+C 59 ; WX 265 ; N semicolon ; B 51 -141 196 490 ;
+C 60 ; WX 570 ; N less ; B 46 1 524 499 ;
+C 61 ; WX 570 ; N equal ; B 43 111 527 389 ;
+C 62 ; WX 570 ; N greater ; B 46 1 524 499 ;
+C 63 ; WX 389 ; N question ; B 29 -12 359 707 ;
+C 64 ; WX 793 ; N at ; B 46 -15 755 707 ;
+C 65 ; WX 635 ; N A ; B -29 0 650 692 ;
+C 66 ; WX 646 ; N B ; B 35 0 595 692 ;
+C 67 ; WX 684 ; N C ; B 48 -15 649 707 ;
+C 68 ; WX 779 ; N D ; B 35 0 731 692 ;
+C 69 ; WX 606 ; N E ; B 35 0 577 692 ;
+C 70 ; WX 580 ; N F ; B 35 0 543 692 ;
+C 71 ; WX 734 ; N G ; B 48 -15 725 707 ;
+C 72 ; WX 798 ; N H ; B 35 0 763 692 ;
+C 73 ; WX 349 ; N I ; B 35 0 314 692 ;
+C 74 ; WX 350 ; N J ; B 0 -114 323 692 ;
+C 75 ; WX 658 ; N K ; B 35 -5 671 692 ;
+C 76 ; WX 568 ; N L ; B 35 0 566 692 ;
+C 77 ; WX 944 ; N M ; B 33 0 909 692 ;
+C 78 ; WX 780 ; N N ; B 34 0 753 692 ;
+C 79 ; WX 762 ; N O ; B 48 -15 714 707 ;
+C 80 ; WX 600 ; N P ; B 35 0 574 692 ;
+C 81 ; WX 762 ; N Q ; B 48 -193 714 707 ;
+C 82 ; WX 644 ; N R ; B 35 0 638 692 ;
+C 83 ; WX 541 ; N S ; B 50 -15 504 707 ;
+C 84 ; WX 621 ; N T ; B 22 0 599 692 ;
+C 85 ; WX 791 ; N U ; B 29 -15 762 692 ;
+C 86 ; WX 634 ; N V ; B -18 0 678 692 ;
+C 87 ; WX 940 ; N W ; B -13 0 977 692 ;
+C 88 ; WX 624 ; N X ; B -19 0 657 692 ;
+C 89 ; WX 588 ; N Y ; B -12 0 632 692 ;
+C 90 ; WX 610 ; N Z ; B 9 0 594 692 ;
+C 91 ; WX 330 ; N bracketleft ; B 133 -128 292 692 ;
+C 92 ; WX 460 ; N backslash ; B 91 -15 369 707 ;
+C 93 ; WX 330 ; N bracketright ; B 38 -128 197 692 ;
+C 94 ; WX 570 ; N asciicircum ; B 56 228 514 668 ;
+C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ;
+C 96 ; WX 278 ; N quoteleft ; B 72 478 207 748 ;
+C 97 ; WX 523 ; N a ; B 49 -12 525 502 ;
+C 98 ; WX 598 ; N b ; B 20 -12 549 742 ;
+C 99 ; WX 496 ; N c ; B 49 -12 473 502 ;
+C 100 ; WX 598 ; N d ; B 49 -12 583 742 ;
+C 101 ; WX 514 ; N e ; B 49 -12 481 502 ;
+C 102 ; WX 319 ; N f ; B 30 0 389 742 ; L i fi ; L l fl ;
+C 103 ; WX 520 ; N g ; B 42 -242 525 512 ;
+C 104 ; WX 607 ; N h ; B 21 0 592 742 ;
+C 105 ; WX 291 ; N i ; B 32 0 276 715 ;
+C 106 ; WX 280 ; N j ; B -33 -242 214 715 ;
+C 107 ; WX 524 ; N k ; B 20 -5 538 742 ;
+C 108 ; WX 279 ; N l ; B 20 0 264 742 ;
+C 109 ; WX 923 ; N m ; B 32 0 908 502 ;
+C 110 ; WX 619 ; N n ; B 32 0 604 502 ;
+C 111 ; WX 577 ; N o ; B 49 -12 528 502 ;
+C 112 ; WX 608 ; N p ; B 25 -230 559 502 ;
+C 113 ; WX 591 ; N q ; B 49 -230 583 502 ;
+C 114 ; WX 389 ; N r ; B 32 0 386 502 ;
+C 115 ; WX 436 ; N s ; B 47 -12 400 502 ;
+C 116 ; WX 344 ; N t ; B 31 -12 342 616 ;
+C 117 ; WX 606 ; N u ; B 26 -12 591 502 ;
+C 118 ; WX 504 ; N v ; B 1 0 529 490 ;
+C 119 ; WX 768 ; N w ; B -2 0 792 490 ;
+C 120 ; WX 486 ; N x ; B 1 0 509 490 ;
+C 121 ; WX 506 ; N y ; B -5 -242 528 490 ;
+C 122 ; WX 480 ; N z ; B 19 0 462 490 ;
+C 123 ; WX 340 ; N braceleft ; B 79 -128 298 692 ;
+C 124 ; WX 228 ; N bar ; B 80 -250 148 750 ;
+C 125 ; WX 340 ; N braceright ; B 42 -128 261 692 ;
+C 126 ; WX 570 ; N asciitilde ; B 73 175 497 317 ;
+C 161 ; WX 242 ; N exclamdown ; B 58 -217 184 502 ;
+C 162 ; WX 530 ; N cent ; B 37 -10 487 675 ;
+C 163 ; WX 530 ; N sterling ; B 27 0 510 680 ;
+C 164 ; WX 150 ; N fraction ; B -158 -27 308 695 ;
+C 165 ; WX 530 ; N yen ; B -2 0 525 668 ;
+C 166 ; WX 530 ; N florin ; B -2 -135 522 691 ;
+C 167 ; WX 554 ; N section ; B 46 -115 507 707 ;
+C 168 ; WX 530 ; N currency ; B 25 90 505 578 ;
+C 169 ; WX 278 ; N quotesingle ; B 93 464 185 742 ;
+C 170 ; WX 458 ; N quotedblleft ; B 72 478 387 748 ;
+C 171 ; WX 442 ; N guillemotleft ; B 41 41 401 435 ;
+C 172 ; WX 257 ; N guilsinglleft ; B 41 41 216 435 ;
+C 173 ; WX 257 ; N guilsinglright ; B 41 41 216 435 ;
+C 174 ; WX 610 ; N fi ; B 30 0 595 742 ;
+C 175 ; WX 610 ; N fl ; B 30 0 595 742 ;
+C 177 ; WX 500 ; N endash ; B 0 221 500 279 ;
+C 178 ; WX 504 ; N dagger ; B 45 -125 459 717 ;
+C 179 ; WX 488 ; N daggerdbl ; B 45 -119 443 717 ;
+C 180 ; WX 265 ; N periodcentered ; B 70 188 196 316 ;
+C 182 ; WX 555 ; N paragraph ; B 64 -101 529 692 ;
+C 183 ; WX 409 ; N bullet ; B 45 192 364 512 ;
+C 184 ; WX 278 ; N quotesinglbase ; B 72 -125 207 145 ;
+C 185 ; WX 458 ; N quotedblbase ; B 72 -125 387 145 ;
+C 186 ; WX 458 ; N quotedblright ; B 72 472 387 742 ;
+C 187 ; WX 442 ; N guillemotright ; B 41 41 401 435 ;
+C 188 ; WX 1000 ; N ellipsis ; B 104 -12 896 116 ;
+C 189 ; WX 1208 ; N perthousand ; B 50 -25 1158 700 ;
+C 191 ; WX 389 ; N questiondown ; B 30 -217 360 502 ;
+C 193 ; WX 400 ; N grave ; B 49 542 271 723 ;
+C 194 ; WX 400 ; N acute ; B 129 542 351 723 ;
+C 195 ; WX 400 ; N circumflex ; B 47 541 353 720 ;
+C 196 ; WX 400 ; N tilde ; B 22 563 377 682 ;
+C 197 ; WX 400 ; N macron ; B 56 597 344 656 ;
+C 198 ; WX 400 ; N breve ; B 63 568 337 704 ;
+C 199 ; WX 400 ; N dotaccent ; B 140 570 260 683 ;
+C 200 ; WX 400 ; N dieresis ; B 36 570 364 683 ;
+C 202 ; WX 400 ; N ring ; B 92 550 308 752 ;
+C 203 ; WX 400 ; N cedilla ; B 163 -230 329 0 ;
+C 205 ; WX 400 ; N hungarumlaut ; B 101 546 380 750 ;
+C 206 ; WX 400 ; N ogonek ; B 103 -230 295 0 ;
+C 207 ; WX 400 ; N caron ; B 47 541 353 720 ;
+C 208 ; WX 1000 ; N emdash ; B 0 221 1000 279 ;
+C 225 ; WX 876 ; N AE ; B -63 0 847 692 ;
+C 227 ; WX 390 ; N ordfeminine ; B 40 265 364 590 ;
+C 232 ; WX 574 ; N Lslash ; B 36 0 572 692 ;
+C 233 ; WX 762 ; N Oslash ; B 48 -53 714 739 ;
+C 234 ; WX 1025 ; N OE ; B 48 0 996 692 ;
+C 235 ; WX 398 ; N ordmasculine ; B 35 265 363 590 ;
+C 241 ; WX 797 ; N ae ; B 49 -12 764 502 ;
+C 245 ; WX 291 ; N dotlessi ; B 32 0 276 502 ;
+C 248 ; WX 294 ; N lslash ; B 14 0 293 742 ;
+C 249 ; WX 577 ; N oslash ; B 49 -41 528 532 ;
+C 250 ; WX 882 ; N oe ; B 49 -12 849 502 ;
+C 251 ; WX 601 ; N germandbls ; B 22 -12 573 742 ;
+C -1 ; WX 380 ; N onesuperior ; B 81 272 307 680 ;
+C -1 ; WX 570 ; N minus ; B 43 221 527 279 ;
+C -1 ; WX 350 ; N degree ; B 37 404 313 680 ;
+C -1 ; WX 577 ; N oacute ; B 49 -12 528 723 ;
+C -1 ; WX 762 ; N Odieresis ; B 48 -15 714 841 ;
+C -1 ; WX 577 ; N odieresis ; B 49 -12 528 683 ;
+C -1 ; WX 606 ; N Eacute ; B 35 0 577 890 ;
+C -1 ; WX 606 ; N ucircumflex ; B 26 -12 591 720 ;
+C -1 ; WX 860 ; N onequarter ; B 65 -27 795 695 ;
+C -1 ; WX 570 ; N logicalnot ; B 43 102 527 389 ;
+C -1 ; WX 606 ; N Ecircumflex ; B 35 0 577 876 ;
+C -1 ; WX 860 ; N onehalf ; B 58 -27 807 695 ;
+C -1 ; WX 762 ; N Otilde ; B 48 -15 714 842 ;
+C -1 ; WX 606 ; N uacute ; B 26 -12 591 723 ;
+C -1 ; WX 514 ; N eacute ; B 49 -12 481 723 ;
+C -1 ; WX 291 ; N iacute ; B 32 0 277 723 ;
+C -1 ; WX 606 ; N Egrave ; B 35 0 577 890 ;
+C -1 ; WX 291 ; N icircumflex ; B -3 0 304 720 ;
+C -1 ; WX 606 ; N mu ; B 26 -246 591 502 ;
+C -1 ; WX 228 ; N brokenbar ; B 80 -175 148 675 ;
+C -1 ; WX 606 ; N thorn ; B 23 -230 557 722 ;
+C -1 ; WX 627 ; N Aring ; B -32 0 647 861 ;
+C -1 ; WX 506 ; N yacute ; B -5 -242 528 723 ;
+C -1 ; WX 588 ; N Ydieresis ; B -12 0 632 841 ;
+C -1 ; WX 1100 ; N trademark ; B 45 277 1048 692 ;
+C -1 ; WX 818 ; N registered ; B 45 -15 773 707 ;
+C -1 ; WX 577 ; N ocircumflex ; B 49 -12 528 720 ;
+C -1 ; WX 635 ; N Agrave ; B -29 0 650 890 ;
+C -1 ; WX 541 ; N Scaron ; B 50 -15 504 882 ;
+C -1 ; WX 791 ; N Ugrave ; B 29 -15 762 890 ;
+C -1 ; WX 606 ; N Edieresis ; B 35 0 577 841 ;
+C -1 ; WX 791 ; N Uacute ; B 29 -15 762 890 ;
+C -1 ; WX 577 ; N otilde ; B 49 -12 528 682 ;
+C -1 ; WX 619 ; N ntilde ; B 32 0 604 682 ;
+C -1 ; WX 506 ; N ydieresis ; B -5 -242 528 683 ;
+C -1 ; WX 635 ; N Aacute ; B -29 0 650 890 ;
+C -1 ; WX 577 ; N eth ; B 49 -12 528 742 ;
+C -1 ; WX 523 ; N acircumflex ; B 49 -12 525 720 ;
+C -1 ; WX 523 ; N aring ; B 49 -12 525 752 ;
+C -1 ; WX 762 ; N Ograve ; B 48 -15 714 890 ;
+C -1 ; WX 496 ; N ccedilla ; B 49 -230 473 502 ;
+C -1 ; WX 570 ; N multiply ; B 63 22 507 478 ;
+C -1 ; WX 570 ; N divide ; B 43 26 527 474 ;
+C -1 ; WX 380 ; N twosuperior ; B 32 272 348 680 ;
+C -1 ; WX 780 ; N Ntilde ; B 34 0 753 842 ;
+C -1 ; WX 606 ; N ugrave ; B 26 -12 591 723 ;
+C -1 ; WX 791 ; N Ucircumflex ; B 29 -15 762 876 ;
+C -1 ; WX 635 ; N Atilde ; B -29 0 650 842 ;
+C -1 ; WX 480 ; N zcaron ; B 19 0 462 720 ;
+C -1 ; WX 291 ; N idieresis ; B -19 0 310 683 ;
+C -1 ; WX 635 ; N Acircumflex ; B -29 0 650 876 ;
+C -1 ; WX 349 ; N Icircumflex ; B 22 0 328 876 ;
+C -1 ; WX 588 ; N Yacute ; B -12 0 632 890 ;
+C -1 ; WX 762 ; N Oacute ; B 48 -15 714 890 ;
+C -1 ; WX 635 ; N Adieresis ; B -29 0 650 841 ;
+C -1 ; WX 610 ; N Zcaron ; B 9 0 594 882 ;
+C -1 ; WX 523 ; N agrave ; B 49 -12 525 723 ;
+C -1 ; WX 380 ; N threesuperior ; B 36 265 339 680 ;
+C -1 ; WX 577 ; N ograve ; B 49 -12 528 723 ;
+C -1 ; WX 860 ; N threequarters ; B 50 -27 808 695 ;
+C -1 ; WX 785 ; N Eth ; B 20 0 737 692 ;
+C -1 ; WX 570 ; N plusminus ; B 43 0 527 556 ;
+C -1 ; WX 606 ; N udieresis ; B 26 -12 591 683 ;
+C -1 ; WX 514 ; N edieresis ; B 49 -12 481 683 ;
+C -1 ; WX 523 ; N aacute ; B 49 -12 525 723 ;
+C -1 ; WX 291 ; N igrave ; B 5 0 276 723 ;
+C -1 ; WX 349 ; N Idieresis ; B 13 0 337 841 ;
+C -1 ; WX 523 ; N adieresis ; B 49 -12 525 683 ;
+C -1 ; WX 349 ; N Iacute ; B 35 0 331 890 ;
+C -1 ; WX 818 ; N copyright ; B 45 -15 773 707 ;
+C -1 ; WX 349 ; N Igrave ; B 18 0 314 890 ;
+C -1 ; WX 680 ; N Ccedilla ; B 48 -230 649 707 ;
+C -1 ; WX 436 ; N scaron ; B 47 -12 400 720 ;
+C -1 ; WX 514 ; N egrave ; B 49 -12 481 723 ;
+C -1 ; WX 762 ; N Ocircumflex ; B 48 -15 714 876 ;
+C -1 ; WX 593 ; N Thorn ; B 35 0 556 692 ;
+C -1 ; WX 523 ; N atilde ; B 49 -12 525 682 ;
+C -1 ; WX 791 ; N Udieresis ; B 29 -15 762 841 ;
+C -1 ; WX 514 ; N ecircumflex ; B 49 -12 481 720 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 712
+
+KPX A z 6
+KPX A y -50
+KPX A w -45
+KPX A v -60
+KPX A u -25
+KPX A t -12
+KPX A quoteright -120
+KPX A quotedblright -120
+KPX A q -6
+KPX A p -18
+KPX A o -12
+KPX A e -6
+KPX A d -12
+KPX A c -12
+KPX A b -12
+KPX A Y -70
+KPX A X -6
+KPX A W -58
+KPX A V -72
+KPX A U -50
+KPX A T -70
+KPX A Q -24
+KPX A O -24
+KPX A G -24
+KPX A C -24
+
+KPX B y -18
+KPX B u -12
+KPX B r -12
+KPX B period -30
+KPX B o -6
+KPX B l -12
+KPX B i -12
+KPX B h -12
+KPX B e -6
+KPX B comma -20
+KPX B a -12
+KPX B W -25
+KPX B V -20
+KPX B U -20
+KPX B T -20
+
+KPX C z -18
+KPX C y -24
+KPX C u -18
+KPX C r -6
+KPX C o -12
+KPX C e -12
+KPX C a -12
+KPX C Q -6
+KPX C O -6
+KPX C G -6
+KPX C C -6
+
+KPX D y 6
+KPX D u -12
+KPX D r -12
+KPX D quoteright -20
+KPX D quotedblright -20
+KPX D period -60
+KPX D i -6
+KPX D h -12
+KPX D e -6
+KPX D comma -50
+KPX D a -6
+KPX D Y -45
+KPX D W -35
+KPX D V -35
+
+KPX E z -6
+KPX E y -30
+KPX E x -6
+KPX E w -24
+KPX E v -24
+KPX E u -12
+KPX E t -18
+KPX E r -4
+KPX E q -6
+KPX E p -18
+KPX E o -6
+KPX E n -4
+KPX E m -4
+KPX E l 5
+KPX E k 5
+KPX E j -6
+KPX E i -6
+KPX E g -6
+KPX E f -12
+KPX E e -6
+KPX E d -6
+KPX E c -6
+KPX E b -12
+KPX E Y -6
+KPX E W -6
+KPX E V -6
+
+KPX F y -18
+KPX F u -12
+KPX F r -20
+KPX F period -180
+KPX F o -36
+KPX F l -12
+KPX F i -10
+KPX F endash 20
+KPX F e -36
+KPX F comma -180
+KPX F a -48
+KPX F A -60
+
+KPX G y -18
+KPX G u -12
+KPX G r -5
+KPX G o 5
+KPX G n -5
+KPX G l -6
+KPX G i -12
+KPX G h -12
+KPX G e 5
+KPX G a -12
+
+KPX H y -24
+KPX H u -26
+KPX H o -30
+KPX H i -18
+KPX H e -30
+KPX H a -24
+
+KPX I z -6
+KPX I y -6
+KPX I x -6
+KPX I w -18
+KPX I v -24
+KPX I u -26
+KPX I t -24
+KPX I s -18
+KPX I r -12
+KPX I p -26
+KPX I o -30
+KPX I n -18
+KPX I m -18
+KPX I l -6
+KPX I k -6
+KPX I h -6
+KPX I g -10
+KPX I f -6
+KPX I e -30
+KPX I d -30
+KPX I c -30
+KPX I b -6
+KPX I a -24
+
+KPX J y -12
+KPX J u -36
+KPX J o -30
+KPX J i -20
+KPX J e -30
+KPX J bracketright 20
+KPX J braceright 20
+KPX J a -36
+
+KPX K y -60
+KPX K w -70
+KPX K v -70
+KPX K u -42
+KPX K o -30
+KPX K i 6
+KPX K e -24
+KPX K a -12
+KPX K Q -42
+KPX K O -42
+KPX K G -42
+KPX K C -42
+
+KPX L y -52
+KPX L w -58
+KPX L u -12
+KPX L quoteright -130
+KPX L quotedblright -50
+KPX L l 6
+KPX L j -6
+KPX L Y -70
+KPX L W -90
+KPX L V -100
+KPX L U -24
+KPX L T -100
+KPX L Q -18
+KPX L O -10
+KPX L G -18
+KPX L C -18
+KPX L A 12
+
+KPX M y -24
+KPX M u -36
+KPX M o -30
+KPX M n -6
+KPX M j -12
+KPX M i -12
+KPX M e -30
+KPX M d -30
+KPX M c -30
+KPX M a -12
+
+KPX N y -24
+KPX N u -30
+KPX N o -30
+KPX N i -24
+KPX N e -30
+KPX N a -30
+
+KPX O z -6
+KPX O u -6
+KPX O t -6
+KPX O s -6
+KPX O q -6
+KPX O period -60
+KPX O p -6
+KPX O o -6
+KPX O n -5
+KPX O m -5
+KPX O l -6
+KPX O k -6
+KPX O i -5
+KPX O h -12
+KPX O g -6
+KPX O e -6
+KPX O d -6
+KPX O comma -50
+KPX O c -6
+KPX O a -12
+KPX O Y -55
+KPX O X -24
+KPX O W -30
+KPX O V -18
+KPX O T -30
+KPX O A -18
+
+KPX P u -12
+KPX P t -6
+KPX P s -24
+KPX P r -12
+KPX P period -200
+KPX P o -30
+KPX P n -12
+KPX P l -6
+KPX P hyphen -40
+KPX P h -6
+KPX P e -30
+KPX P comma -200
+KPX P a -36
+KPX P I -6
+KPX P H -12
+KPX P E -6
+KPX P A -55
+
+KPX Q u -6
+KPX Q a -18
+KPX Q Y -30
+KPX Q X -24
+KPX Q W -24
+KPX Q V -18
+KPX Q U -30
+KPX Q T -24
+KPX Q A -18
+
+KPX R y -20
+KPX R u -12
+KPX R quoteright -20
+KPX R quotedblright -20
+KPX R o -20
+KPX R hyphen -30
+KPX R e -20
+KPX R d -20
+KPX R a -12
+KPX R Y -45
+KPX R W -24
+KPX R V -32
+KPX R U -30
+KPX R T -32
+KPX R Q -24
+KPX R O -24
+KPX R G -24
+KPX R C -24
+
+KPX S y -25
+KPX S w -30
+KPX S v -30
+KPX S u -24
+KPX S t -24
+KPX S r -20
+KPX S quoteright -10
+KPX S quotedblright -10
+KPX S q -5
+KPX S p -24
+KPX S o -12
+KPX S n -20
+KPX S m -20
+KPX S l -18
+KPX S k -24
+KPX S j -12
+KPX S i -20
+KPX S h -12
+KPX S e -12
+KPX S a -18
+
+KPX T z -64
+KPX T y -84
+KPX T w -100
+KPX T u -82
+KPX T semicolon -56
+KPX T s -82
+KPX T r -82
+KPX T quoteright 24
+KPX T period -110
+KPX T parenright 54
+KPX T o -100
+KPX T m -82
+KPX T i -34
+KPX T hyphen -100
+KPX T endash -50
+KPX T emdash -50
+KPX T e -100
+KPX T comma -110
+KPX T colon -50
+KPX T bracketright 54
+KPX T braceright 54
+KPX T a -100
+KPX T Y 12
+KPX T X 18
+KPX T W 6
+KPX T V 6
+KPX T T 12
+KPX T S -12
+KPX T Q -18
+KPX T O -18
+KPX T G -18
+KPX T C -18
+KPX T A -65
+
+KPX U z -30
+KPX U y -20
+KPX U x -30
+KPX U v -20
+KPX U t -36
+KPX U s -40
+KPX U r -40
+KPX U p -42
+KPX U n -40
+KPX U m -40
+KPX U l -12
+KPX U k -12
+KPX U i -28
+KPX U h -6
+KPX U g -50
+KPX U f -12
+KPX U d -45
+KPX U c -45
+KPX U b -12
+KPX U a -40
+KPX U A -40
+
+KPX V y -36
+KPX V u -40
+KPX V semicolon -45
+KPX V r -70
+KPX V quoteright 36
+KPX V quotedblright 20
+KPX V period -140
+KPX V parenright 85
+KPX V o -70
+KPX V i 6
+KPX V hyphen -60
+KPX V endash -20
+KPX V emdash -20
+KPX V e -70
+KPX V comma -140
+KPX V colon -45
+KPX V bracketright 64
+KPX V braceright 64
+KPX V a -60
+KPX V T 6
+KPX V Q -12
+KPX V O -12
+KPX V G -12
+KPX V C -12
+KPX V A -60
+
+KPX W y -50
+KPX W u -46
+KPX W semicolon -40
+KPX W r -45
+KPX W quoteright 36
+KPX W quotedblright 20
+KPX W period -110
+KPX W parenright 85
+KPX W o -65
+KPX W m -45
+KPX W i -10
+KPX W hyphen -40
+KPX W e -65
+KPX W d -65
+KPX W comma -100
+KPX W colon -40
+KPX W bracketright 64
+KPX W braceright 64
+KPX W a -60
+KPX W T 18
+KPX W Q -6
+KPX W O -6
+KPX W G -6
+KPX W C -6
+KPX W A -48
+
+KPX X y -18
+KPX X u -24
+KPX X quoteright 15
+KPX X e -6
+KPX X a -6
+KPX X Q -24
+KPX X O -30
+KPX X G -30
+KPX X C -30
+KPX X A 6
+
+KPX Y v -50
+KPX Y u -54
+KPX Y t -46
+KPX Y semicolon -37
+KPX Y quoteright 36
+KPX Y quotedblright 20
+KPX Y q -100
+KPX Y period -90
+KPX Y parenright 60
+KPX Y o -90
+KPX Y l 10
+KPX Y hyphen -50
+KPX Y emdash -20
+KPX Y e -90
+KPX Y d -90
+KPX Y comma -90
+KPX Y colon -50
+KPX Y bracketright 64
+KPX Y braceright 64
+KPX Y a -68
+KPX Y Y 12
+KPX Y X 12
+KPX Y W 12
+KPX Y V 12
+KPX Y T 12
+KPX Y Q -18
+KPX Y O -18
+KPX Y G -18
+KPX Y C -18
+KPX Y A -32
+
+KPX Z y -36
+KPX Z w -36
+KPX Z u -6
+KPX Z o -12
+KPX Z i -12
+KPX Z e -6
+KPX Z a -6
+KPX Z Q -20
+KPX Z O -20
+KPX Z G -30
+KPX Z C -20
+KPX Z A 20
+
+KPX a quoteright -70
+KPX a quotedblright -80
+
+KPX b y -25
+KPX b w -30
+KPX b v -35
+KPX b quoteright -70
+KPX b quotedblright -70
+KPX b period -40
+KPX b comma -40
+
+KPX braceleft Y 64
+KPX braceleft W 64
+KPX braceleft V 64
+KPX braceleft T 54
+KPX braceleft J 80
+
+KPX bracketleft Y 64
+KPX bracketleft W 64
+KPX bracketleft V 64
+KPX bracketleft T 54
+KPX bracketleft J 80
+
+KPX c quoteright -28
+KPX c quotedblright -28
+KPX c period -10
+
+KPX comma quoteright -50
+KPX comma quotedblright -50
+
+KPX d quoteright -24
+KPX d quotedblright -24
+
+KPX e z -4
+KPX e quoteright -60
+KPX e quotedblright -60
+KPX e period -20
+KPX e comma -20
+
+KPX f quotesingle 30
+KPX f quoteright 65
+KPX f quotedblright 56
+KPX f quotedbl 30
+KPX f parenright 100
+KPX f bracketright 100
+KPX f braceright 100
+
+KPX g quoteright -18
+KPX g quotedblright -10
+
+KPX h quoteright -80
+KPX h quotedblright -80
+
+KPX j quoteright -20
+KPX j quotedblright -20
+KPX j period -30
+KPX j comma -30
+
+KPX k quoteright -40
+KPX k quotedblright -40
+
+KPX l quoteright -10
+KPX l quotedblright -10
+
+KPX m quoteright -80
+KPX m quotedblright -80
+
+KPX n quoteright -80
+KPX n quotedblright -80
+
+KPX o z -12
+KPX o y -30
+KPX o x -18
+KPX o w -30
+KPX o v -30
+KPX o quoteright -70
+KPX o quotedblright -70
+KPX o period -40
+KPX o comma -40
+
+KPX p z -20
+KPX p y -25
+KPX p w -30
+KPX p quoteright -70
+KPX p quotedblright -70
+KPX p period -40
+KPX p comma -40
+
+KPX parenleft Y 64
+KPX parenleft W 64
+KPX parenleft V 64
+KPX parenleft T 64
+KPX parenleft J 80
+
+KPX period quoteright -50
+KPX period quotedblright -50
+
+KPX q quoteright -50
+KPX q quotedblright -50
+KPX q period -20
+KPX q comma -10
+
+KPX quotedblleft z -60
+KPX quotedblleft y -30
+KPX quotedblleft x -40
+KPX quotedblleft w -20
+KPX quotedblleft v -20
+KPX quotedblleft u -40
+KPX quotedblleft t -40
+KPX quotedblleft s -50
+KPX quotedblleft r -50
+KPX quotedblleft q -80
+KPX quotedblleft p -50
+KPX quotedblleft o -80
+KPX quotedblleft n -50
+KPX quotedblleft m -50
+KPX quotedblleft g -70
+KPX quotedblleft f -50
+KPX quotedblleft e -80
+KPX quotedblleft d -80
+KPX quotedblleft c -80
+KPX quotedblleft a -70
+KPX quotedblleft Z -20
+KPX quotedblleft Y 12
+KPX quotedblleft W 18
+KPX quotedblleft V 18
+KPX quotedblleft U -20
+KPX quotedblleft T 10
+KPX quotedblleft S -20
+KPX quotedblleft R -20
+KPX quotedblleft Q -20
+KPX quotedblleft P -20
+KPX quotedblleft O -30
+KPX quotedblleft N -20
+KPX quotedblleft M -20
+KPX quotedblleft L -20
+KPX quotedblleft K -20
+KPX quotedblleft J -40
+KPX quotedblleft I -20
+KPX quotedblleft H -20
+KPX quotedblleft G -30
+KPX quotedblleft F -20
+KPX quotedblleft E -20
+KPX quotedblleft D -20
+KPX quotedblleft C -30
+KPX quotedblleft B -20
+KPX quotedblleft A -130
+
+KPX quotedblright period -130
+KPX quotedblright comma -130
+
+KPX quoteleft z -40
+KPX quoteleft y -35
+KPX quoteleft x -30
+KPX quoteleft w -20
+KPX quoteleft v -20
+KPX quoteleft u -50
+KPX quoteleft t -40
+KPX quoteleft s -45
+KPX quoteleft r -50
+KPX quoteleft quoteleft -72
+KPX quoteleft q -70
+KPX quoteleft p -50
+KPX quoteleft o -70
+KPX quoteleft n -50
+KPX quoteleft m -50
+KPX quoteleft g -65
+KPX quoteleft f -40
+KPX quoteleft e -70
+KPX quoteleft d -70
+KPX quoteleft c -70
+KPX quoteleft a -60
+KPX quoteleft Z -20
+KPX quoteleft Y 18
+KPX quoteleft X 12
+KPX quoteleft W 18
+KPX quoteleft V 18
+KPX quoteleft U -20
+KPX quoteleft T 10
+KPX quoteleft R -20
+KPX quoteleft Q -20
+KPX quoteleft P -20
+KPX quoteleft O -30
+KPX quoteleft N -20
+KPX quoteleft M -20
+KPX quoteleft L -20
+KPX quoteleft K -20
+KPX quoteleft J -40
+KPX quoteleft I -20
+KPX quoteleft H -20
+KPX quoteleft G -40
+KPX quoteleft F -20
+KPX quoteleft E -20
+KPX quoteleft D -20
+KPX quoteleft C -30
+KPX quoteleft B -20
+KPX quoteleft A -130
+
+KPX quoteright v -40
+KPX quoteright t -75
+KPX quoteright s -110
+KPX quoteright r -70
+KPX quoteright quoteright -72
+KPX quoteright period -130
+KPX quoteright m -70
+KPX quoteright l -6
+KPX quoteright d -120
+KPX quoteright comma -130
+
+KPX r z 10
+KPX r y 18
+KPX r x 12
+KPX r w 18
+KPX r v 18
+KPX r u 8
+KPX r t 8
+KPX r semicolon 10
+KPX r quoteright -20
+KPX r quotedblright -20
+KPX r q -6
+KPX r period -60
+KPX r o -6
+KPX r n 8
+KPX r m 8
+KPX r k -6
+KPX r i 8
+KPX r hyphen -20
+KPX r h 6
+KPX r g -6
+KPX r f 8
+KPX r e -20
+KPX r d -20
+KPX r comma -60
+KPX r colon 10
+KPX r c -20
+KPX r a -10
+
+KPX s quoteright -40
+KPX s quotedblright -40
+KPX s period -20
+KPX s comma -10
+
+KPX space quotesinglbase -60
+KPX space quoteleft -40
+KPX space quotedblleft -40
+KPX space quotedblbase -60
+KPX space Y -60
+KPX space W -60
+KPX space V -60
+KPX space T -36
+
+KPX t quoteright -18
+KPX t quotedblright -18
+
+KPX u quoteright -30
+KPX u quotedblright -30
+
+KPX v semicolon 10
+KPX v quoteright 20
+KPX v quotedblright 20
+KPX v q -10
+KPX v period -90
+KPX v o -5
+KPX v e -5
+KPX v d -10
+KPX v comma -90
+KPX v colon 10
+KPX v c -6
+KPX v a -6
+
+KPX w semicolon 10
+KPX w quoteright 20
+KPX w quotedblright 20
+KPX w q -6
+KPX w period -80
+KPX w e -6
+KPX w d -6
+KPX w comma -75
+KPX w colon 10
+KPX w c -6
+
+KPX x quoteright -10
+KPX x quotedblright -20
+KPX x q -6
+KPX x o -6
+KPX x d -12
+KPX x c -12
+
+KPX y semicolon 10
+KPX y q -6
+KPX y period -95
+KPX y o -6
+KPX y hyphen -30
+KPX y e -6
+KPX y d -6
+KPX y comma -85
+KPX y colon 10
+KPX y c -6
+
+KPX z quoteright -20
+KPX z quotedblright -30
+KPX z o -6
+KPX z e -6
+KPX z d -6
+KPX z c -6
+EndKernPairs
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/UTRG____.pfa b/e2e-tests/cypress/fonts/Type1/UTRG____.pfa
new file mode 100644
index 00000000..d9fa0e78
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/UTRG____.pfa
@@ -0,0 +1,1126 @@
+%!PS-AdobeFont-1.0: Utopia-Regular 001.001
+%%CreationDate: Wed Oct 2 19:10:38 1991
+%%VMusage: 32987 39879
+%% Utopia is a registered trademark of Adobe Systems Incorporated.
+11 dict begin
+/FontInfo 10 dict dup begin
+/version (001.001) readonly def
+/Notice (Copyright (c) 1989, 1991 Adobe Systems Incorporated. All Rights Reserved.Utopia is a registered trademark of Adobe Systems Incorporated.) readonly def
+/FullName (Utopia Regular) readonly def
+/FamilyName (Utopia) readonly def
+/Weight (Regular) readonly def
+/ItalicAngle 0 def
+/isFixedPitch false def
+/UnderlinePosition -100 def
+/UnderlineThickness 50 def
+end readonly def
+/FontName /Utopia-Regular def
+/Encoding StandardEncoding def
+/PaintType 0 def
+/FontType 1 def
+/FontMatrix [0.001 0 0 0.001 0 0] readonly def
+/UniqueID 36552 def
+/FontBBox{-158 -250 1158 890}readonly def
+currentdict end
+currentfile eexec
+fa444f2716d92b815f58ca9049c815358e22e32e73a3e6a653c538ee56873363
+67713b8cab082730570f5b5efcf34c2cdfb6f8dd2b7905a37c1924a2424c16e8
+711db76501f564506b0f45fef10d83c3e3c6df2dc0af7802e7f42c81b4243697
+ca09088b868d983e79e4b2c3e17321993cc8837921fd4ff7b92a1294c5ba33e6
+8fd40e63df624b51865721e034f71bf57bee0e0c2a9c169c7a626496540d45db
+585d2bc93f83e6829eacb859a194d7a57904cfae75e6188c7121003ae40153f0
+0fe6fb52f339e72d4aef38469328465a3bf0ca1ef58dfb447612eef4ada248e3
+ca525b0884971b85099d73538152affcad51d03eb4060a5c580f453ee78e5c4b
+991f77c6e9600c135395d56335a7c3a75c0341979d6b404ee496214a5d20ae06
+e17d80d37bd3540b8fe913bae1674afe37d0c41e3d9836a7b5147be32f9ba01a
+df1a73b89d30bb595a4b279032e37c8230b6d4933065947c3e9150d2c7c4d439
+d60197eb69991ef059edf886ec0bdd28158a71a2df96dcb67fc0bf81c34cf27e
+12d6ec950a75993af99fbb040af595134198151e7361672d8538cfadb4716785
+dbd48da442ccc911ae9157b14e433559dba1a1c5619e99b57274d6998eeca3a4
+7e804d8fe3a664f39f33237831385949a230a21e52a2dda733f2f89b02ef4ce2
+5d603c9bd2f8e4c0443070940f0380dd1ac60ff462a9a71965a79d787e0711df
+0a69bd86a4846965ac6c77f2e1699c4620c1bd9f5b2263e13a2213631c55f7ab
+cf60d2e63a7830d5011d9b9934bcee2103f4a5d56942ce0f590a15dab480e5e5
+723afc8e1d8d96f3e0d8629b88ee477ea6cb8121b0adddf20405f8a32756004a
+8a59b2cf922a6dc0747bc15e99079aa12ac0d9fabf8b47c5a61ca5268e01317e
+1f0a38c468ffea597a392ae3399daf7c03126987750258b5e077d703cdbe05b9
+a5fc4d80e8551c2c06a2c579272877fd462fac6569a7a9f964797df79628ba7f
+f77447fc2aefb25089ce946e0aa5c5ff21370f0cf84d8e2affa8b30891081b16
+6fb86fdfe65e7e02ee551c6b3e2e324cf7e18664883dc538018ec8cadf7093f4
+a13708013d6ca2afdf644c0155342917c2725598fcafddaf8807200682fcf04a
+4efd5919c21ed2ec2f7951c2db953ed292a54783f5c541bca68d305b05df5c5c
+f17b9af31035dd8a77ed8b54d4de7d684f54c58a28f0dc55e7c692cfe444216b
+18751c24ec1c8a00a5cbd458a0ad54e7c13f26b4c4257fd992fbd1338845fc69
+6b97a2ee6a4d4f03fd387e745eeb8e97eb67e40bacbdf1d6c08e9fa2e317d30a
+c4234559096affd5b2e65602ad9c4868e24d7f1d35e8b6679e436a010be5ebd1
+52666a26ba7f7b68a693add1d21138ad64425e9202a043cec98ebc182c152b93
+9f88cb8c89069b72f3c264e2bea7cea9aedf1a8736beb55cc77b02e3989e7013
+b4fdec2767194fa85b1aba185aa01fc8902209068fadfd1d282a59f8b1d7e30e
+3829383779fb9a28404759c595b986deb7c59d315023e542b461327e8645f440
+3d1b571403e30d988d5d17b09590d644cdaa885cf36881602f220a7891130e31
+2ed47ee703368c5beb240427e1b30446dae522143ffeac91f3bd6a3ea5c57a63
+f81919870daf71d9b522529438d5e51173973f612b3afa5e4c5c042c8a8a4ed8
+27d00b1f5a102ba8a11ee7d942e1954cf35b875abb184df026b1f600b27aca0e
+4dfea33866de9d8b6b8829c128876a2b882b1aa1518285d87de3c39bd74863aa
+229a0108f3070908664f4f77a62f0e67923d844c9ef91a6affb8a72606f5085d
+88c35fb9e8eeeb663742369e6b0ab87c82e3eec3dec4adb2098cb25a35d543a9
+70025ebe038e7057a8d0e8f7b2eb86418dc39b494884bee8c92c87f1af4cfc44
+c14a177f200389c0a23c24ef1c59e9a143756c1ac0ae50a12a82228faecf86c3
+4f7ba61f010a0d285063676097c7c3fad3b19479c8c50d712b48ab99e4858096
+a1799936df4bfc4169e5544adddf343c7fcb63e83e2b2b6eb68c8a33edf201c6
+5d89e5b0c3d21d1aad02bb07fd463f763c09dcbbf7c216b1554a9402a8eeb5cc
+9de4edf0753d8882ad183a9d14e2de51fd81daee2ddf53745a076dbbb790cdda
+4b25bcea0f98d832886f26bc15fdae9ebfe42f0a95b7d02c0f65ebb1fa0d730d
+a1f6d63c3da03f9ee0961f5e93cf943915e0dae3147e4c100fc97859f1db0e39
+a85a06f140305e9cad70247718385e9151a091aa9a09552c199e3ab164f28270
+fc60a08b30ce11e5d115577e54b57c73b2008fe0f9679cb7d60c3c80b8e63cc1
+046a093b0838c832125c9084c9d3691fdf6236f1e4b2cbbb4ce8d898d4c27a97
+5f4eb9ee7e3c2d1a694465a632356ac7c06abb42f89eb2791ecd146d29fae767
+2f132e065ae5f5432a5705414049f68b871b55c409ac3f02cddb56deb21c8d4b
+202b93e8a5f22d09b287fde695e9beab9fb790fadf1250dedfc8188b0a3975be
+db8973fd1ee42841b2bf6ae539bd46d0339dee6804a92a0c32ca204ec9897f76
+95d04cd16c3e09f37306109da8b55b5c69660682e8328724655f88a08f11f9b0
+6d3f1629240662f0a0ca024f16265503390d9adf8b521d971d419a4cbc15d322
+1eaa0b79e4a45d2f0bfc9bcbf135e44b937e4bed0a4ea896da90e40bd793ff7a
+d0bf85fed6224690f801cd8c0100c4a79be124833774b86ead4d58bfd2598e52
+6bb5d08264df1c4ccd29a918bb27d7c75d0fea288e066a74236de1828a0c9fe9
+6b1de344a4f57ff0997707f9e83c25cc5639ffcf2cd01152d9f676a80c637a69
+5cba153263f8c1d93bfc03642796263c3b460b1fb22fd4f63e742064a337d1cb
+c12ad7b5c36cfb0a62341c849e97ed8a6b5e3e93911dfbe3795f9debeb3cf7bd
+6311ccabcd1d80c76d608e0366e23725b4c27210c788f25726bcb13149c781b1
+3e77341630a99a22cd95717254844bfd4221df25ab740978011a301861c4f0cd
+cbf77e1b69b90ecd4aba88d2c0298bf525c4f6f0be9d34d74371517e485eda6b
+d38374ebc2ec4abd9b7969b426b40c356982b735289f80583b377ebca2c14068
+77d7a6516135878a18deefa530a88685cf0bb9ba03c45d283510a68b17853cbf
+c1c16428a598e078564a8d50bb1171771ff483ee395ed074d7f524eb8af23061
+5b30a01122d440365c7787ff477ce068ad1c8e4f9a409199138cf49a435326f3
+645d5e093179b96831b7dfc926ad11a2b9bf2ed3a5f4ad135278dceabe5f8aaf
+298bcf406c9c11cfb55400fcae95910ea7c09e607a1c2447ee82ce7e3faedbdc
+2b1827ec539eae6ae25927a35be1aa6b7951073746e5f684f758fcabea3cac25
+f608e033b3c92cb05b7c9738d3a6d2cd5e0c2d29b968ed7c7bb43d7fe734a959
+7636ca80ee97f6d88550c39d36db458997a98e8bef087c1000add77f8c6d24dd
+2e0f6f4772ef61c5bd5148c3d67dbe77df27a300e5d8c9e4b325a6d8cbbe8675
+15ad15966b130bf4499ea1923bef7a9047eb1b0d35acbf0b8303fa08b26c706b
+7368332adefb42b66dd9ccf61fdfba00b5aee231c2232e8d752703b0af6ae850
+ab8274563f8fead4bbe9fe860dfd95a6a7e0cfa3d8f6ebd0e066cdfd396d8c57
+f8f17bce0c5678649034a62418dd2463b8f963b43ca02d011a9a1ac4aef9c9fe
+151ef8c726e376158dacf1131f0ee855156d95730d3626003e51983624b417ad
+c258164ed5b2bd3d4e562f16da8a0b69871787cd9142d297ea3fadad3c2c6dfe
+3491c8313c2608cfb1d6a6b21d9569049031b0cfc1d9e96a7186164e04d713a3
+2f1350ab08811688619335e7f331cbf390039502cfa7f68ebcbd75937862add8
+f0c7f498d6695e9d48c4d3e07753062a992675f1eea3cbae77c7bf35433bf424
+45aeaae7876749942982520abcbe95a7ceeaff412a02f6922a1e6c05e9a94d82
+11db8144984dcec1d1fd6418934a57338e070b19ce6d45c0f861e429b3828c9a
+c98821dc4ec4d5b7f64cfef16380c478bc5a0ea0c4b82d94e3e01067b3027c73
+e93899ee5f37db55e0be88844dc7c5a538e43510a65716320dac3e01e164fe26
+3406de45c19c250cd2a9c3bb3bb7cd5fafe16d9eb7e00d012b65668104a68eff
+bd0c11b55497378896ac50abb47a6daa32f7c21cd34b19f3f797d883fc8745c7
+db21a910abd7e9733239c6751f71b126bd8e26f7cb16bcbe5a066cda9fce63e0
+a9d40a9527bdca47211648ec231d0aad9f4659cad640181b76d6b02309365348
+4e7e328bb1f7280ed4f8fb571d05a7a55842ad1dd294cc46cda410160f2f7c09
+4a34dbb0aca809a6271c8fc97f4103bd83b701f2f474e0bd1e7f37fb7d898e16
+6612f9b03e25bffde2a8d6ae1dac8b7c28b67c371ceaf8020ffc9e1e35973dee
+63cfb07a6e3eee03799b4841d195eba565c03e10998fb80fb279fa123c2e8dae
+94644927b473c414c1fd0fa0b359a279310e23fe44104f0988de3345d9aab3e1
+e64dd94ec8a6c20556d785e7e4f4bec46c0de73185fdf9a84c6f46ffe7c52c1d
+c7d62ad792987dccc225abddbb6fed504864476f1e8d0e2ae009de5f4c50bc6d
+8d3902c6c169cc0c2008dafeade6308daa473bc4f8af398ad4a8300f6b989271
+dd59f6d52ee210bab4028b4160979401ed9f626526e43b2362ff92b208a29b04
+8110c2801db3a59f1cbb89139c80bf025d45d61b2bbb6fed4456089976a94d82
+a094aa8201027413899a402050ddc0421afe5d357a8d8e0ab398e01da601a0e5
+1c6bdb86ca2d064b2d8af3ab7a8d39a564d2c2cfbb74f62aebe4c9cdb668909f
+2d83bf84894edf23a3db2d3b4ed77d091f21ff313c67a0abab68e57a541942f8
+58ebe5334687a8dfa0bc1eccfcb1b793d5aff3d7479a72ef70b3490f7ef3dbcc
+1beef6d171dc7cf8acb25d04181009407b2d61dfb7de55cc433272a46e86a74f
+a8a5bc69419e54c39220b7b3b716c5c4cac5313e4d8e1389a2217de9ebc0a4eb
+f72dd28b7b2e7481457123167f2a138a2a9ab099706c81b83e3a02a9f9e2e321
+c9658c611fba436a649d72ab9be1707e3711d20e15d778bfeeba66b09286be38
+2f903ec637a8ea274ef8a8c488c5cfeaaf616fa0e090ca2c7a767048b6086dc0
+ea0e3bbc8ef9d01a74cb264b3ca824f0d6ad786127c61d0d8ad13c31e5a25555
+ef5e64e1d19d150d24d1459e8da3ad799fa554dc7877a417f51dd1a3a02e7341
+51b6cd7a2c6915127017f748942865f30fe048b7e749bcba0e015761c0a9e1cd
+e1cbae4bb1807245d7547f062a05c1f13a753eb6fc3473caf620347338bae987
+1d2bf08764c309b32b0c915b4735481e25259f53c53c144b8ea3e8c2a4dc2725
+85e197f684936c0690e497653a46a7b86c7e57b8f4c890319279034cedc18bfc
+ebe1828d23cae6d1fa854149d1cde376470869f9383e83ccab2af11c2d35eb8f
+1bc9273cd399c7e8478ecf742c2db209479460e310f8013f1a64a385209d2e30
+000d71f5fcf62f251989a3d852b9c36e57273a60d565aa387236f61162e46821
+9f755ec5501247ae6bd14644106c04f36662afe7c3cf741664c6a4789f3f8ce5
+baf6e347f6564be8d6fd92ba9bf04ef31c9fd60a2650b1086f9a244a113d85e9
+405dcf23e29d7b46281baf01728b7355b8a1e50febf4443c2d1a2560cd47d652
+cfec6f5cbe060f8c1b20fd4720ed00186609c40771ec8db4e7e4434eff8aec89
+007816aa98863f8f8fd3ca454bc228bcf4d2522d948ab346b5137f1cb52b5a52
+3877acaca92215b9fba427112786c2fac9e5b57879d28249d2d71040aba21fc0
+f73498c856cbc6c5a5f25bbcf19ec97768b8ee9aad3436536a9ff8f22ff72206
+8555d0356ab16b1fce02b79220323bfee1ebbf156308bed2d91febd3f3a13927
+be4d619c75ed738dc1913971fab9a76bf697c6ce9db692d0495abb1034fe31c7
+23eea2feaee80aa07117c16008a7b5e00a13fe2dba4c4e3174762c28da98a191
+0d15eeef183cb994c829de35324614301a6e98311d9f5f282d28da1b485997ec
+41eab629352822a5583d307d6d417b869f6dfeb158523a581557f6acf91c4f28
+ba0af9e428099b152946769932a190410319fa3cf1274c6745ab16df56afc3eb
+4e67f521dd52f3e3b38e41d0055a8bb6f957817850d693f76c2237f78ef9e660
+9ba5ec79bb0cbb08f461991a7baf70e3f8c8d619c0ec7d3d7aeb964f2e8b7113
+8e9868c98d8fa3a5769147fb72bb82ae844c1331ab285dda9df924cf36f2200c
+b0c4b28f19aca7c4591413c8e89ce410d469a7844af4ad00b6089d1efa64de2d
+49069a4094f2af0552b68fbe03655686cd845e46a319cebb984fa717b7fc2565
+4cc9843d079512d8ad6d58d55332a962c7b20514d1b53818b47d028b59ec1176
+43d3571e690531d6980c0bbf2150f5afadf3198dfc48995afc745a8fd0b40b7b
+d48c00010ebce60dcf13ad056bb53ad3e7a3d0984dc28b8b4aa109ed4d62bbf9
+9f7be92fccdaab5c52cda9fdc972e55a933b2c4c1d2c54ed5d88ec8132d0241a
+91fda05af2a736547dbc1da85588b04ac56602cd9335d9a31bcbc50a5e13626f
+f86b9cf6bf32d2a5b6c4a3754f9447e2eefdb2207e1c277794bfa5defc88cc63
+1ff2093a0100c2db33baeb231846754a10eafd8640674dc03d737446a345eda9
+fa03143bcd01de7aa1486de001ff31f1f7e67681017a32dc46f8a9aec6cc0573
+4a0253dab3476519de29344ad7b0eee93d0e4438bc852f4ff4d018a690781924
+1730a369bd2312732816efa8000f0b30e8d19b8551857eba645f76f38257d96e
+4088b20e11f5485dafce80ab03820127a291acd4ddfadd039c03962078aee0fa
+6429e748bd3ea0290ac67f1c14eb8f63cd5b7f4a3e61b91f3428a73509151784
+3ec501946e468e8b68374ff8b0560cdd38f2eeb263106ddedaf7d54ff1ed7c65
+c9910de2993171eb6f5d9a4f0fc3368d6f6b5e57e280fd9c5c1969cc814d46ae
+81c2ab001f03e1aaff8017b6d7e84cdf0e3a747daa3d2e1d674cc49c8049e8f5
+8914f54313f1084396beb5defd89dfd0d73ff6ba76ece9939b988bc5353f221b
+e4bb40b79c19612319d196938935747e96030a5df12caadc7afe24878a2d6e82
+5230548f931259546b44b4460eded3188fd76d06a2d0392e2371036401e9ba72
+a14e50d6aac474d148a40872f222d3d3ea322c37aed0d04cbe2551e513e40943
+04b30dfb33710c66f3df8e24296604bb84e3e498200d942bf52c452f79b855f7
+bfc47420a44f80ea16be3f8e0716695d20ba1d28739ae88c04ed2848b70d7369
+46734ecac041a14ce87397706f1ddc57144b7a61231a1d0e458d3d6d014ce66a
+b6e37b46774a7a9c74d6a5b636303d3002151be679abfbb7399a0dcae5bcb5d8
+7c84bbc66fa03df92d4f5ae78e85313acd62522caf4fafd187c553302e692133
+f7971b24b92b9e0321f93e7b747bd6ae5f51cdbb247d36255740752549bcf8a0
+09e774a01bead6ad931c11453a179e0ae2d23c09acb7b2bdcb16fbecf07515da
+0ac8a4b2728b3d5c8945aaf10fcf770008a0aafd33c402b7304f6b203d0d6a7a
+be7a27608b10f9d67c39f5fa2c09ba5900f0a6e6c6bcea1a0ca14411ada0a36c
+d19c25ddd2d29ab25d4cb7dc3afaa0f5aad7e22761863cfb427fc4fb8c991251
+31cd3376c227747a6d3afedd341a4e6474ba273e687640eec85bae3bc83d762c
+3380159c2af1dc118fa1bd2382d339c5d213bcb89e89fc8241707e9bff40ec4f
+85a889421643ce7a87953bab67fda678da5111da8a95f29290ef70d672f0ef5d
+ce90ee85275392766ca752cb7a2bb9eb416ffce5166a955bcd54fa2082d942c2
+89a0fbac57b9cde759be0b98d0e6f5b51c7dbd6331730a15635edf1a3287c679
+61c0a248578b53559a3d040f8847813ad1d76480c43f4b674bc0659bebf9f1f7
+e8cd4acdc0e0fc27104601abbf25304f8d5aa221e02e73c37868baea6106e069
+88a7b580b091e6c4ef56e0b52bf9055039d2c3de83c3a6d4f8186646a6e1ec4b
+5ee53764f122509d63e339c51b15fdfdade4cba2880c66bd607977684ad01aa6
+d405553066de3d2bde7eb27b332d5bd0371881eb0a9d2c2b7728f827444a13d5
+758591eacaee4121c2a00c05b58357d8053d2223490b5124fa646965a0ddfcde
+0f99b41916f8f80d81970753daa6079aa0014e800c54052e01d0861a7d3add7c
+2b542695ad2a3d6c56190a170d23bb34829b049e2c02c69e79d597fb5d2f9ce2
+713d681f072435c8e3740fb61127f5d3bf4cf1adae5e99e66a0c472076f476c6
+171660a613a35eae2d353cf78a06dbe7dc227dff30a501a547241c6cfe8123ae
+053293e9ed0c1bca42ceddc775a9200420a37ef81552b721c077c005afcee2fd
+fb2e9088451e0260fef0be7dcaf9d0e4eeb3557241bbe429583e4fb813fe1992
+1ffec808630e4b3e64cdf9d2e664802f3f0846f0216b8422245a0363a2a6ccc7
+3f7d8b39438246611428c6c32584976b10375003469fe0efd968e21319e382f7
+50c386b57d7b7e8e4b5dd8dbdb8d816ce712bb7ec4d3a2ccb6fc5784c066f79d
+f9fab8f575b42bb18f22a7f2919364c61e81cd47d0b121bc0352247ad8d83ec9
+36891e106f85e8fe25efaf00582095dc7f16dc48b3cfbc34234aedfdd357643f
+79cea8fb1fa81b91b49f8c063fecaf8b8bf0fddd8f2063eaf6464de3e963acb4
+33a3afa3944fdcdf61597fc7d6caa8e7538a784024ed4cfd497ac35c6796fb14
+65aefd0498b994fca5c74c003c6ea1d6c8f935df6ef5cebc5ae7024986be09a1
+3a368d9da9061aa8ed1015a60778e63640addc44dcf2e32bb1d017f10a319f44
+fc8aaad5cb149ebfafc0dfcd665b07f6387f4e3fa9273815a7c0e87390b89964
+18fe077d8330190a04f348648e1eb790cc3cdb5005afa7dbeda6e102d848e725
+ce4d4a9a9e7dc16a9035892a73bfbbe130b8d059e0282693ba7f062ee031cd38
+edbf41556bdc7c34c6f2547797f2b4b202fcea7d6c1f6d6a890dc850549e2d9f
+d4fed66e6a66f93ac27cfbfe938832d17c171729fb0eedb5befd2d07fee6db06
+ee7d2e5302904d0ca4b21b40e3ed346e6976024f6ddf05ab1b7a339c42d2989b
+8afb7447b2b772f3cae03abfd55b1d14f412109e7e99057c66ed1fc1c842e336
+8f8ad95e67626219b6d538fa55e6146f7c4d2819b6ab69628d3f918d5c21ba65
+6101700f2f9b5d5399a1209e0fe5cf62cd4dd8b065abbb2024b7766478b78d89
+3ea80e633d445a240e27c7671018dbb01e4b38beb85ff02de34a4c06a092467b
+2409c7a6f7d94461d55073a87ced3a6048ee3b688042bcdaf6d4ddbde6edced1
+7c0b7a2317e60410453cba8646e00976bbf1542aa0614f93a6a323de99774176
+bb2babb068c8c29bd4e4cb7f8b3646ceaddcdb611215a5ee9e61bad78d8bd5ff
+f66177e2069d7b4f87c6fb13a61f2772e8632f158b4f1cadb8e3daf55e5df78e
+9d50a27bdece942f579490f570d3eb34cb69203627abe7bbb6c5b2323fd8fa7c
+1a9271f36f727ae3bd9bf660514a052c86625b1245e190fc91df37b7e6960fb5
+1396405a4f2de27f7146ad45f9e5e14755985c189cda794fa21d336354bedd98
+14ccc92cdecbef77ee962b78d053752a22f5ef66879af4d77cc798b2d6579d54
+9077dafbf4dcbe07c70ff56070ffcd869bbf2d96271e461c5432aacb7935ac07
+fd239929e5d27e34c91a785fdcc479b84a99dd3db6711f1026a613d24a6f5de9
+00e4a410ea22a81b1106f6e11edcf83a3a9f3adba5b22b129e3d8f30807303ed
+fe50943d68951bc743e7391d7aa1672fe0dda7fc8a6c2e87aec891a6d8ecc050
+03fbb9bc38114416c4e330c5b7b43ace475f0c8edc9abdf5fc137c9e07f1f8ea
+bb13365b3f7e3dbce2afc9388e9279136ec6e0edb289beeaed74370bcb453884
+3ce1b3afed75a47bfb50b7bf4d267b014f2480cff2d9e21685a64b5a6fad8963
+1d5ccae56079f4871aa6b9b6a7ec8982f266b6eadc060087f2f7af187dadf05f
+c31a47b30f2a250fb501f4faf7e18c3aebff55fa08c74ef27d96aae869a26b2c
+45ba8c84b958db1a7679ad0e6bd09c498543e6d20691b072d84c6760c70e8e90
+ec179e6d00f684e6c2d070f894b98d495cf2d2978b96ea13cebf5c8b9ccfbf05
+72a00358a8faa812b00c06f2ad40237839f0e2ecff57d359a2c2055a1361768e
+f816ffec8254c512e3d57ebb23da610fdfd5e1e16b97e1fe97e10a279a203e6b
+b52f0be0ae9742d1a7faf2a0d5d6e1560d6a3e8e1596fc57a3288fac6a541646
+5ad8c50485e42cb5214d4dcbb82a16c2a15d406698ddefbb5fa3dab291c4020d
+52b76c9f0c91b5516143d99d82611eeded385015cb53e07a30bab7f27f22ac73
+95210586bd245bc0d2366952c1d85ecea90e68e189fc313823433321d6ff1785
+0d8eb6f1e4f2f6153d84b4b0bf198ecca9c87a80dfc33977a9b06e994bd86800
+4ab2f317727243b9c9704fbd88a0e6fb5ab8c94f38c7e20daa078edaa9878c8f
+3509b2c135e9801c79500327adc9537318d0f6b700fe99166d4e7c8f6a41bd45
+6bda6719e0ea158b8aa25e73731e02d215294d91f44985ba21d4045b1f8dcd99
+69a0b36e07b3c7534e9401960683ac1696459b2a45e0a4133d630e5bc81024f0
+b1ed05e19a6991bcab03482ad446a1d542249cc9e1f13e091508eeab9f744a67
+85e3d64fa0c7c79275292a4284f62754d46c0835e432c3a1776396feab389c20
+775f2f94aaec26813eaffc4d1e6b4df16f7b25ad9cd3e8645dff53a354718283
+e6da4a788dca6de2521295f51bd4b41d7b1f06c02f9cf60ad1136425c7996c55
+a00798c6f180ee8bdf52c71c01b48b3798eff89600b2281d68fb2e8567d61d22
+9b31ddd64b49fd662131d12bb500ba609e2f298c237b90c17c4a29e1c4e75ad5
+c1631c7058f7aa486b9c19c760b2bbae67167472490d537d14c9c70437d8a964
+fa40effa6af2862db37b5ba62da47685971093e11103228b85a6a0acc3168486
+cb4d94f0b1b01bb35ff617b5b36a120f82f783752079b66cfbfe2088290ba9c0
+cf8255443a6230274299b82c7e0078a9435c2a48ab298954043521391cdb627a
+efc038208a97a62fefbc2284290e83c78fce56f1f6aa31428092800897d6adc8
+ba2c0bdb767e9b684dd51cb44dafbdd3f084b3ded0fa1af187bedd1e7271ba2d
+9b040ce2d98fd7a56f82191756bb8f582cb83ff0188350d8d5451671a2bb3478
+97ee71a4621d366d810945c790ec3bf4126eee53265a2ddf2bfdb8cb8d78352d
+8032e0b87fb87323d4defe5c5c974db8cf06203d0cfd1470aeb474dc0fa2d32d
+e1ac57cf43f5dc2d2751c84e10a5055ce00c594a0dea8c1f65268de077f06ff5
+c8e083d70fac84a5605eb80c81bdeb92a65fcda58979a93566b29c935b3ad83c
+a1303afe4eef046c2ed3646c0ced2e1066d4eb7f5db11f171b2440a0ca362253
+20098d9388aac930db7be1cffe5d6eb05dca2a6355df026393b99ef5e3cd70c7
+c82ee1fe5cd113766bf011bba63e78b2ddf4d86519aa68bd991d782713ccd2c1
+3f2eea70dc273576aafa2a0d2ed22e35f246e92d88c907425ad6843abbfd75e5
+78e1cbd1e59969cddfd6d26f89e617005718492b51442c55fe617da67d8ebe5f
+bef9bf22357a125d611e04d9e5e28d5178c83e2c3b073c205d7e3a792d4d0232
+c70e596680b5947e6504a7aa48c67795a15160d49d40174fea72dcce9f97edc4
+75d030d02347961b7d725b78c7024f98aa0d4fe953807b39632f0d4531c6c1f3
+7062320fb655447c355e55dac7faed0643d33f14cced8ff7398ec85fd93bef6d
+e6032c7689658a96cb9ddd856fb6c0a4be0fb15bb46c068673374bae1fa73bbc
+20dcaa9398b526128991d68eadf0b90bdfcfd097b2f858363d819fc6ca7fbb47
+6aa237cf38daa9653b5877aa322e3039f86bf54ca078c55a35301fdcad322d14
+8e65067a0e48cd62cecb0d7f50faf272b51c6d0a22c402f0df1373ddef2d3c2f
+25f3c636f9f5f8710bdb1f5998f2040b7bc4bf6dee428357a695e5f177feedc2
+be6ca3d7456f4fb7b97803df6de76aa4bd622cf080800628d35841a838371511
+04623706136658fa3cfacfc116a30542f8825d80532454675cd5c9ef6640dc79
+aeb9c37a0691d14bd260f58995b7ef2cbb236da7a93c042450d940df347494ca
+6d5df95bd04d3077b47926e34610583984253fa1cbaf4b0d544e71d5f7405e49
+41445c23b0a1c3c6882254602d0ae6582f64dddd079f2d112b0dbd31ca685bf3
+dfea6d3f310bea2b2f4cf4154cd37fb19016f703a1f9a045c7e21cc994c0f23c
+5e4a49d0ff47deee398d478302cd0c90380885ab4a59b6f3d50bfaf00d9e7f3f
+0b97562161091b4eb201c7bcd4d1cd239b00df09d03836a63e8a9419cc9dae0b
+493aca2a7fdafe0ff81b6a9896c7e57faf1355fce5f3d54afe62c018a5211fae
+935f1f9795d1606cf77673b699829c9a136c6534438ff817759f8684d5345417
+70f6554539184ad886156fd610f331532d0ed22685d56ed02fa51e265ff34a1e
+fc8cb40b749148ef80016f71cb0647e1232b7450237f686828c36dffba04aa6b
+46a526e693da4fc9e2c84d7245b76ee7dbeb360b2f92b112de8e5feb3e433321
+eb67e59f3a41c7c91e5fc51e811b2ea7a70a9562bcd8fd854d761ef290ca50c8
+16f3c58d8e48d13edc3ba242c35ca241fa15e6243aab8f4b5441cc0cffc1c9e2
+7729da8ba3fe1837504d73bb13ba53ee7029aa5a1b137244be2d16ff3ae6cd71
+8151c53d223def6463c124acf3c37bd5a42e4ccfe3db5d3a22f249d0a5cf59ed
+ab67a3a6cee4f7838314e9e167d3ac722539c6a60e5961cca947c8b9d818802e
+54162eed974283ead7dd09fc33bcefe33a78ae540a98fc6710011f0cd96300bf
+e5b9acc91af99c6b38d005481771655d1b89bf8fc5fa8d682ac36a54753a106d
+6cee95baa71b131786a76f6ca7cd143270f679c91635822a4a2285d6aa44699f
+38603c3af88b82d5658531bf36ff97bb959c44345f51ad2c990347d7f803c2d8
+3345ba5554ec0af247e7ff00f483ea94a3a6407a2d0ccc9fe5a530139336190a
+166575049e84f3964141e64f9f9fbe2feff15930340b49adc782b7ec9c2c24c7
+816b7cf5e7aa3c194b3f50af218aa2b1a69e9772a74cc661a96100f9607fec9f
+0e6a34b6d3b9b6df0d0cdef014c6e3ac43491883a33304b464c3219b883a1f2c
+9a1f882449ce7c0814be53e0a26aedb003f25f8ed3f9506efadba4b8133db373
+d66c0fb02ea5d1db6484418398a374bc43f624dc1dbcb70f9b6b97691d297b4f
+50b0b7aec4de5732ac5db32e488e8131d763ea2234a8b6ba67acc6ba688c3f09
+b1da0f7074b2ec71ef2778dc1dfdfa2b27d74756a43fdf2fb8be3fdc9189166f
+5747a30bfdd73b8507e6d41bff9607a770cd6d30a8b476cb972c896b90f15057
+f3b5095adf44ef4347da078dce7b6a41f01c29abb94e2c70e3b3a24f4169f80a
+7eff7a9b53909f97160964eedcc0862697c872a164be415d28661702c33f4edf
+bc8c172fdcd3e70d3db0341ef1ff453232617b89935a9249bef30cfb7115addc
+c06bac8b37d4363710731a69e3975c983b96bc5b8033cf0c17c4a6dee8e45f61
+53c1683d296a937f733c332fa08bc9f4c5455e308733567fb7b51300c3dd4665
+15a2d90c58d2155270e64f2a555f45084bc75cc2fcc2dd5ed0582943a7a5359a
+4fab1a5310c083343ccab56c224f114e46c8e6f8233e9fb01a352048144592ee
+fbffbe37b26c257f91220d792dd0f08da33f10baa54d92044f24646f9b998283
+1bb052d3873debe609ab925b8cab12a80067e52adaa1bc0f469b0406c975a17a
+c99594a7a8c7ff29754319338e16508989ecf307e9ea3680ee18cee760a02b76
+90e09970a402bf91895df85dde19f9f125c75558ec5002cabc1d4ff0ca1dd979
+ac53b2d742f45d787eebe0c983e12740ac3b74eaeb8ddb0a058030e2b5aaabe7
+3acc75052330fdce054d0df654b4dc7cce4b07b9e48abaa918a2c45b0675a360
+8f9aa1f307b1d93c13075a57903935978e764ef45628f219ea21740a2c720dc1
+4037ee1edec2082462f882bba13fe63a335eb8c18d95c420da75b152b5791200
+2dea7d6f2eae40f84a622f33376da36234c4baec46bcddc2a7874ba2f1f57b19
+b47a7f3a4f834b5780eefc08516d223b42a4e201194f03cc94aad45cc49cb238
+2f6af71dfd8abee459fe9f9e19f1d52659d6dd9d378c4f06c39552012e17e541
+f40704a1190b5887bd0cb07fdf4a3a3adf04639c237242cba12758e371dfb2f5
+5871567815806e74fe8cafda5031354aeecf88feaa172bf8f4cd9bde36362120
+2938fff3f84180ab0d5e2ca492631ff71835add3434bc1aca1589cf50ba27fee
+f0d86e6de410c00c5a40188a91c01d944d3b941ab249ce3de0dc988482ac835e
+ba5e0a045a6dee34f7c92ea4a1dba8c403d614dfdf3bbecafc61ee5762ae4f3e
+11511eaf682fbd245d75530476c2151c4ff6aa7959c5dd0c2f54c483ea556c95
+8774853ea5f841577eb57e89aeab3ed00e79902cf36fba0a18e460c849f4e5b3
+db02888493dc249f1c1f44bc7d7011ae65184280f5f4cb19793b3bb002a439a6
+5518fae1d5848e10ccc01e75a77fb1db966111754111044e6bd282ebf00b3731
+7227ee6f76a51e1921b2ba047a5f1f09f21f13e1cf44b3582af9c2d26df159b5
+04738c1d27acd2c898ef8148f9058dcdd4ce885927ef51c36b43fafdb08dc186
+180df681f94cb9b84c298bf7804280162f58f62fedfd7bc0e9b8214992f4063f
+7685d655f248ba694252e5ca13623f12062c4872b5989e13d9c162773f976144
+cc8fcc96013595b498562ae4261e7a1efede4b79efbe7ed5e4398d1d9e66b253
+a0f99f969c15fbdac089a5ccabbbad21402852801736f0dcc64b0d4d7cfcad33
+ee75b328427178082a96435c3c9f40e9cad04d223ab295eda1ffb193abb5718b
+f5efa9db4ae428bcfb422ef2ecb16225c67def9ef76ad21cf8626f46854ccedf
+885709053e56e6e758824ee80b0c8e4d67cab4e5d6d9e92fdd9b99912c709ff4
+57d0ae3c258229551c93fbd68e47b00bd643872e5ae53f292866e566f2f9787c
+1edd81b005745b10fafc9911644c5fee21e092fde448155c9f69ee2f55c023b1
+2eff9b86a86f8786b26069ff3f2a5f7112070db4f4bf93f40ce24d1a5c4bc5a2
+b577d39f069f506f197be8c590c73bb5f44ffd6994e2eb11786004c71d349521
+2efcba99561ba4cfd479ffe07ae4c45f5ec38c164db89b192d0dd7c07228bf2c
+0ca8374f377c4b8ae58a265cee76e71e952accb01a9cf1676faa4f49dc456c9d
+c020521a2e88d2e5c3904157d3e68158ca53371c73be43bfbe2cb78e3e1a9a15
+f29359be03fe111b87b252b060c453fe8bb0ce288a37907e7648352fbe66792e
+e4060dca4506ad6a7a8806951df8d3d29d0347fc5f4f408472edf2c4abdb7418
+28dd1afa26e6e8f84e09308bb68de881a61d106757c09840e49993551dae5915
+a28a25ee7949c3a00b7f33d3b2ac252f1b8d0896d3116396e2a7fe57c028308d
+d7864d81cabe4824bf15cb5778b941a6b5e9e46539a8206f8bdab18e92bc0aff
+2f7640dcdbc2b04f7cf5781345ea8513d00c7a22dd5f1e43b60ba05acfe45572
+76ff2374e36f5132b884ed8dacaf72d5926bdaf62a16715486bfcbe4758ae053
+687031d0fdba9199fb4221ffc9b12844c4d2e94e393f6f4ff2d6842abea45372
+12128d3234738783ae23850090718bef9adb55d26bf606dc4736e8be6ab5c079
+d5a6dcf0a6f1b1df6f33ea9c1cb5e916aac2b80395522382a1801e15b44e97bf
+c32017aa2eb223c188e63a7f5e093b7354bb25071eee2c3a545763dc93e44e35
+96b9dbd7c4b4818fe8237ee258a2032153fbae62f5dc4afc16bcad79d2550a1e
+2221482e072b9ff488bee9730aec44740b55a35d9454a4afd2542ca9e992d781
+6fb6ab598ec696cd7d58db737ea1e72cc4e4ee39e1d50cf8af3d94636239bb87
+2d4532e783ea90cb1c8278a9541e90aa0b6cb5a6522617b406b495cd2f9af263
+6d9e9453174251786b4c449aa9ae25f9f48d6eb00234003a421570f08b3081cd
+62e4de0418033395b45f559ade69a75075f363a13add803d20d13f54f0f6fa6e
+4061f653d4021f3eab7808ef212a5565de8ff35e46e8bb02660326fe6bf92cda
+02f4990e9c9653d2ec5687662b965468505f26d772499e649c3a0d69e118247a
+72077084bbf6828daca78ac4524c599df56a05ffcf11d073465fff2c52b8200f
+2ab7de3212d573455e4be882a31e3f3b92b79a9fd621c5dce0c70f09df800df4
+8a594435652f454905ac19fc7aa3d43a5a5f360778e85e0b6b82d126136f4858
+3796833f454bee0f16c40dd79fe440b263d9a354ce8e7dda6947ac89828f0a0b
+4ea40d62a651421c15fbc2d271cc47116df9ffb388093efe6a3bd4a6d21150b4
+286cb50aee34371b13083fc5cb8ce57ade9ddef9bc4a77f5ba71e9c71d159199
+b29203c64c53041e38b413b901e90bbc9c67378734c049e497cc67bff03fcac3
+684c5f18857fb8a0d941660e9b04b55209241226cab4684ed71dc9a212d2a7d8
+75d0090ebc27e3ea81cfe4daa0c3277f91544d134cec4b5da927a8b9c0727ffa
+9db44a2d6d9e34a8673903879b041462708af18ec1c658f83d4f65d3df623fc5
+db60e9ca57db545ba05c10a9a68e94f85b4c2a42a686de90a50f482cf150ca6b
+f1758078bf655b04bb9cb71336c96ed9b1a217d9a0619a747cbf82c4421f5520
+3ccb5cac0067f83d17b6e57264856491bc1220aa9b628ce4252a9932aaf5fd6a
+cba6cbc11a5cf2d34bd21c77f3963a49ca746d4f1661cbdf2803ecab28f50aae
+66d2c2dfb81f0aea5fc3ff710a5e35edadc08792b69c4af174fed9cdfb2e0c43
+b5cc87adf3fc71121b1cdfed660ae99d8d036c4896362463da11a7d7ca08c7c3
+49ca8733021acad25f152e797a946d1b4010deb8c6dbb9432979556e0cb80a89
+493d58cc933e173cbd48118605bf7de3e8a909519b1f7d58c4b9811e7d3b50c7
+297270d1c1edbcbe0f4c3e93143ca44dc128eb5b17d1cdb1cd8b8233387b6270
+d2a8215407cc1df046713aed6d1495150bbea29bebfd0c08fc1124a4624b7cd1
+fec7f156f95321825fdee4c0ea668e2fe1b0d92d6aced305ff511f13ca92afd2
+e99535de55a09a3b3e0355645df6b4547a50c2c29a2536cba3909bca6b761b91
+caf5d4ab8e9eb5cadf71eee71f0433b9b9d9ce0394b5c87d38193498f3961673
+7a748fdea2248f936dadf0d8952c33a4ce3eda258cb902f87e4f623c77151eb9
+18e6e4fdc9537fbf03b743693187dae6c9a749b5c7c07c325b9b27b025c38b48
+c5ece2d82a1b982b9d058dc49b33aa425e7eb79827b5a62a4bd7c5bd4488099f
+f7c8d9aa169ad961e17a946b4e65599e268a2a96d53a9c0147da713517f0bffd
+cd992a1ab06f6057e17eede9c7f9077fa60bf4f11e885810564c0f40b29ffd2f
+b6e9a8051c661dfa341ef0ca3c630e4df019eb057bce4c1407f9185a8ab4f882
+81d175eedd45086905342282d26ae4df524b713ef2088451f1073a59acc3a1f2
+4004418469561682e72d20f59cb9ad4ee5232aeefc996ccb77fb3beb9a635900
+06fe6d2bb606293d40410919e31db52063c87c17584854d108f711af8112eefb
+890429a772e77bd03e50db3a2a1ef371268ea9cac1bf7bb15a7746790d67cc0d
+a592fec74f0000f2d1073985fd1053537a455548d43a12bd556063a6fafb0127
+e8c1190f11b992a0fd26a3ca823cce5fcfd5742afcdc02a790a25fa57dc9359d
+ff8fb3550da075e3eb3582ef454c92493e1578788f966db39d0038d2cd7811ff
+da49bfeba9cfe0654b68fc512616439226b08264533f295ae94108168eaf284f
+290d61a62e8c8247b66a64289303ac847dee0f651c1b4575115d79a9abb9e71f
+8cf7dc4b3db7c835f2abb42c37456fe3202ed6ce061841723bb2cb759bfc78c9
+07c51737b52469f1f2dafa7ece85cc798050dc1bb63436c94ed1af3a0e6628f9
+dbb10bc24d2a6d94000b2d142f654a924911de0779ee7ca12d82e04bca01b3f0
+bf6d1679d8a4210bd58933be71e5bdd9e22ae192637c90e7b8905586d8312945
+4d68274af56c5d9636dd4976c0c8574e7804bd8fac44fef418eaa7f5a0410be5
+a4c592bf762ef8e66132f71378c37ce7c1fb485d5be0a27628125cd768ca1427
+790fb8c160a7f94b80188dbb55184ea6d657178f30c055596a370c9f40dd64c4
+f7246bba74679a347e03d7037a4994f348c8c4874df31358f02157132f98ddd2
+322231a9271018a49672897625fa0041550c99ca2cac35cee4281ed9db5cbb0a
+229fecf98aa5facfb951a1b116ed5c58cf4015a9bf03a0b1505132a46ca6b3cc
+02002c40f040e0b97303ed8c42d9f62a95610de3506c7a628546f9e4d6e9973f
+2a77cff929c7a41603a99363dd7f87f3c076990546e7814ba3150d70ce635943
+88152a0bb6d23bb4b0ad0c7edda0e5fd83842a6c2db044330119c3557ece70ef
+8297d1d86334958ed8037d6fb57ef20bcdfaefa66d2f02cf9a074dac8097469a
+b35a644035a7a0f91a813aedb3d9ec6fe2ded7c9d5a414cf394dc646df0510ba
+f27f501fb2627834376f9776d4f00869bc8b0189ce88008503bb16bbc588eaa1
+edbedee5e41b81774054df5b31c4323c4f2d2289f018a3d07dee4397b69470b8
+b1d64a694f1333130b5310763054870b88d2d017fbd62f3a2d042ddbc4cf68f8
+e1aa1f790bf2c7f598ddf275f11899e46d18d9ce4645671b14617cbee762a485
+95bf927abd6aa985c1bf117d82727432b8cc573197cd87a096daf130de5c101b
+ded264c14969b04f8f55299ff35cb77b033a1752ff04c96a4dd43e9e66fba118
+9dad547bf90c89bdbd77200042172c6243d1c465751dc96f5101eee86c76e4d5
+435d1f1fa667e7b575760681d8ef3b97f01f58e2406d3aa084552ebcf985ebed
+06baf00f2098f01976db0d4a36f39221b08c00b680ee425a9fa22b5baeb571d9
+31c9009a03a88501ca1cc8da7a3afaaceb87a92835161c3f4c84970ee219dab7
+ee9f5542d108b9326d5fc58cadc39d10527d481d303cc11cc424398756932aaa
+b06d5600e4d24a0d7dc36da7e8b32871fe36e59e1049cb9ae8c361c3c3ebb365
+5ee4a2facda18377394b68f046655d679ff8122da647ca64df6a7b99a3bae5b9
+da56fbfdce787f9f8263eae5e16f348c22aecb06398d714a69dce1a932f2b50d
+4964641ded3143548e33c879b8c03206f6feb025268f61f9cae4642e6009c804
+3adf0e4a1b5ce27a812559bbbecb7402ac2a580c6d8471dcdb02e6960fb51396
+4058ead51e6b121158c9e3d0dda75439e6eb47293c642c60a70bd9307ef337f5
+ef4601dc0eddaf6d675d17e37350ac6bf3cffb442a602054b52a5ec600f883d4
+cce1ff0d70304522df3d2cacb5bcb2cb37d453b76f7d4d915663e19c6e1335ef
+6fad8e7b76fab514abe26d837565908135cf8518819916bc9f0db07fe3b6439d
+c42de0dec1f5110b8fa5ef40289834953438a7bf83a6261dc80cfef8405eac76
+32815aef696c5b86d2cdec5d3588bb8fc5e9dc61d97d2a5869115c8260f31f06
+986110dfe2c477f80bbf6b3d9f59a6dd573eff782887c9b84bed766b63fdaf13
+dc4ac55b7a127be7c2555bfdfb63ddae8346cd6ccb6c77ac057bdb1c51372603
+8cbb594119970a50f42acae2b9d69784c5f621f53292fac082bf8a78c197af6a
+8b56466e9b14f825e45a9d8f554db27cafcae033831219f6bfb6e3568c7532c0
+ddb22ed77cfd1f46bb98480d1cea4800e3d2b8f623051da01bde6c8bfd1aac33
+240f6f89d49c824e4b5d8bfed9b536fd1773a3ea254d2e56725861a1289de1f7
+8960dd096d6ba285716881f26795345504bf0685127d91c893b57122f858e1f1
+93ce8270796fb9b0fd7246445d484005689948c40376a8e612fb8827965ba50b
+565748c8afb55247f32feb36a4245f136d6cabd1f491b753eca2104199bb3f1f
+461d1be91c8ebbe6379b2ff0eef8a6fdb5fa98029d7cf458e17e15631c2b63d1
+72264f66e45a8c108460e3e353997d9ee098f83537a22b5cef20fde0ee260b85
+c98befd03e86abce66eb6551625f8dfe379a07f53c32888e9e4c04817d6d5abc
+f02ca074a8d11754f4c6fafd3c0751886de2fac502bd1d3d9ec6b56b4d5d5c01
+90aef863969d280220e45f9bc52a18784cafa7e9bd0aba47832cd22e6c0cedb6
+28cf6a1eea4e519495c60be2f0657a43f311cf331e83e9e8109b6ca5235097e6
+d116373c76e9dce208bbd9573c7b815e33e3e39f8eda3d1d07be9ade9f74181d
+c5b9214372c93275865d0d4f3902c5dafd4fb246808dc253d15626549fd97102
+1be3384bdbeb3e70f5b846a85088dc8c4a9a411ef04cb3ee0399e2f92e889ec2
+800faedbebe32208a0f81f4fb33b97bbc9daa92a3b3a53d1f7a1a957cfcda2ba
+76ec22331c2ded3de9ccb0f51e7ca924ff8d4f0f541bba1a62da701fd2a05c02
+204ea9debffce3adeb99995087932d5963d2a6c39f715225d7662873624158b0
+a3fb470af5036028a3f04600f9e86ad203fe14dcb5360aa9cfe4e225b5abee97
+e21237ae6d76aecc97470950ee7a2d6b2c5d6ec7a88578ccd349aab544d151c9
+67d693f7de7005e4d92c5dc7b14b6979b1636092fd75b52ab6e83d7de079b2e3
+f5187467a0a6b3fb4e001ac42c3ca4ba743536d9e9a8f0a37b23a5509fd88ef8
+bbeda576aa8440eb7c76ed2c1b2309120478e5a35e071d75c16ef0086eb07ce0
+4e7242a68c801dc3c68663444db00efd46649bd00616b5a346955ba95b881e7d
+82733c8f7b563ef8a124d7312fcd0ce0dd57a26e6e104a080ae51b9b80b48960
+f1ac9f15f7ad23fdae73b7e93edb14823fadf09afe275d1503a0bbbbd57efc66
+e7b4f6468369718601a36e3b163be29fbefda393416a86f976d631b80ec34755
+9a0b2e328ffdb6a1ed1ede49ea6749d7d55b04342e40804f030b04daca04b43f
+f04d2e71c3686d9a3509086cc247524725cd7ac89fba7bd37212754fa0afa635
+317f846959f2a4ddd0dcfd1b1e5cb66bad528e47f850941d7ce69a967118f490
+95a8f8978254def9e7c5e91d736a70cc3ac70d1aad33d1195256020a9ccada88
+76c0ea39dabeebf3182b0fd89fe5be65661cdc62afd9a3ced45628adc41a1a3b
+d681dbb4c941020ce8b8478cacb0e4fceb5b6c16ed9ba14f1c3704c89114c5b0
+80d1a7115d69e6f932096eb9059a6252cd63531e9a72a36bab438a17686c41b1
+5a7616d857eebeb60c66d3b2a991fc2eb3f0b93df445b370cbfbc1754051d1bb
+b2997b93b66ecf29d6e7d3c552e0538fa41e0896d05c10684e280d683e48379e
+164b356e916afb48f42fb90acb10223b54e74a2a821787322e628f6eb02be9d0
+55f46574342907e69038ed2258dd96cfe873c6fd5ecc7d460252762e7fc4af9a
+608b65b1abd49db62324c1efb0742c2ac7453012840729e4798fb62bf15a2db5
+13fa7ddf79c204efe0d4b7cb7978aa3eb36a2c53c8d2c913ebf3debb9df9c52d
+c5ec37eed5d8924adee4ab8313467f3f0afb3bf9b424749f73715cabd5b86ec8
+7264cdcbb8c111a4bab2fbc568b65a081a8e7e18fa0d04462720baac968cb6a9
+445cdf029ab4adb8786f35a8d2e02459401b5be2ba6522a7a90b46d8333c35ba
+38437539b4b994e1c1ac8f6ec84a301b764b284ceff5ffa842f298d9c2f3c197
+54809e99550cec6ec73cc66b94d6e2980c54f17506e7c67902631704f51252cc
+1596405bf72ec7243687fc7a0de9b8c2f4b311dc8ec7fee6bc31645e4322efb2
+a59483c1734df0ab32ee58b6a156ca6b9507797706894aa412b9903203fb506f
+345be538c0e253dedcbef172cb9365f99e55b740b45a091848f8ce635f54f499
+43afe4697285361f718b6de2b79f98e0e3adae0509bf571370c425d1aa795cb4
+a1b10ced0d43b59bea690990313afe7ba7eba130f7e5639bf12c6b7dbbc1a17b
+4e0fdd4b02e7fe481ea5ca3cf45337d47488bfc09a0312b0258596771ec1e26b
+c7803227bd4a5bdfa0495c227a454a579dcab23fd4ae8034458fb61b7d89de7d
+4524d7bbdfb4e00a6b4275691be8f77a490e768309d6174379f52ddce411b55d
+80843074dc394e4e88b7311918985a58e5012c6eb8007f22903f3ee0cfa1aa61
+3128fd7b6dda6e93a6cafbc6a7f3f277868dc34939542072fa746c228745319e
+c0aeb14792d9d266b2d58c804d7f4094d5cae77bfe150debdc47f2f23b0b37c3
+b19e19cf0ebaaf50d64df8f624048c3cae65e0c5770792d6a6cb97202174b003
+a4deb8a3758a83283dc162cc88526a3d037d8ab1f823eff5115da7d2373d1d11
+56b883d419931f7956024a22312130659a8d2a946dc9cb721599fe7d5d28dd66
+12977032a94bf26215af645d8659ce53528b367e30368fe0db4b194487a4890c
+26230fd2bce623a12bc09b437df3a3faeee49571df114d6492b0c1e3ef4adb7f
+8ec807c39c3ac56aa82b2a8e7b4bce3777ce3104d3a62e8bba72608649f12b04
+e5908b5f9d167d865c9cbdc94e3e8c99e06693fd43283c58cde48e5295b327dc
+176b0ca861033c1f183a41a7002e9ebecced00584c5c1fccfeb630c20666d91f
+0eda19c9a59ef508260f6bea436b351af6518cb4e009865fea06d4fd3b2ba970
+c3632c4556861e54a6441f7398d420a99624fe52cbfb68b8c1ce2a27038809c0
+424bcb7e54c5781be2be39e52bcc1df2b74ce7828056cf8e3f5a56df1e54e7db
+d281570e270dc818aae4581789c7494baf7b1ac9cf3dcb9cbae77da67c39c6ba
+db606df4098d341e81cd5a24d1abe2dea05ea064cda4017526b956cdb79540bf
+7f1324f9e256d555ba4945767f2c72db5597abcabdc81e3015660232c67e0949
+0ba4b7a04074154e8083a8c883c8b884a1681cc2525f6cf45330d8818cf92906
+049da3a2455d2041a0cf9aeaa2ca7fd0f4e5d15580e5bae137d39baab9c400be
+0ff27d17d87970db423398e931f58afbc415758b1b1a8312facde8c4451dc91a
+09cdfa481770f0e445d35b98460b6a7b3e93f2b3d5f0b49b967a3819c13ff6b0
+4b3e665d95d8760fc380ba7f7d400154684e081ce7e2000c6e3a02322d3f3835
+4e49daf3c8bbb58422d64e73e0a491d3edfdb599614ef55b603b00d8e1aa200a
+225f39bc76f58506ee900c2db1d8fd16872998017927c4a561b83b6167a34ca9
+4407b21a2607d96846a9bb7b543382980e39fc1a9dd9f47c562de35e18a341b0
+63878dc89f993080a1d995ef07991d52ea00c6372863b56b744344541feddbf2
+75b432e36368e85161e1ad0ceeb772939ace8a7db790c66d5b821a90d1cccf7d
+ec5b2970a04efcc3060b0c163594d7b4f0215cc30b01e71b97f382e11f27b871
+922ce013ba8fc3bb678bd64630936f29f853e1379760bde9562f8708f8905a59
+6df302ff8f289896a7a38809246e2ece8e99e96b6b471916c883bb7c948e8fac
+6588e2b30bfed55f074fdb0989b85fdfd023f15f1ff89c2f29f4f5381c5ef9ff
+8e702aa40c1c6dc7aa48430c564ed5712ebb99af607df5860af30e1b4ec1a492
+6b9ddf2aac1352f4b15613cf6e86708c73bf73728f3bba7eb6b0d30c427454b3
+c8e882eeeaa998e9f02d10a787f6017a5c0a55027d2ea1da1485ab60a397f449
+4f2fab1106c23e3ed97dd268a6f5a9abb2f0cb470e1ee6411b27708b1f2cc7f6
+9efde016cf7c122903ac89dc47aa11d0c45414b8264bebce5850b39884195860
+39a7cb52c877e5ea6a0bbcbfbf3a104c0a66af928f6aaaf062b8da0fdb290f51
+9b26b6b2cf2fa49ff36e7435b86f720c06036f2a322c3fd0fb37ea7b850ea80c
+6cd6b4224e8eddd5adcf97f69d9c80102ae4f6f6e5663d0e47ccf992e11233db
+5f37e9efeb51f8cb754b919a80c0f0999311d1784221347d746be1590d077f8f
+c8a98ec3ba007a04866bf3e1e24bbdbe7839bb7adedb55c3584ce2e2b85a631a
+5bee6015589a877604e67121ede937afca89af6e186d10d3fa057c9703b17f26
+30025f6332087e97b1ce60c7e26e48d546ae484dbc604a1b33528b5dc41bc2b1
+1b9d717dcef2d469f7b5f0b3047876c578f3dcb15425678136229a7387966c00
+593c6e96e603f222d4bf10880978e1d0f2e246176e6daed0ecf75e30430ed2a1
+8907963577026bf6689b4a4c420c5a622602f2390ef9fbd5f6c716c8550acf50
+3180e80c95e8d4db4a8fa3eaccd85672b2fb2c0f7b0f8deb4955bd625f131904
+289166003bb3451c1d82b19c461b21f9523681468ea247f28d6e8eae788cf06f
+3148ea2d53810077c14b98b5aa1bb2294b202885808854bc6107f4667ebaec53
+6dca8dac30a933b183f5465bdb24ac02a6fe61d7c440e068d5c1b2511599971d
+09ca25be37e1baa6580850a7656ef4875ac5dca306ae04090496932140464a48
+2b60cf202b4e308a97d4f75123e655e4d210b6f2f285ff921a9906028e331aba
+6542a742fd93bbb77167d04267e3b7fa862437c37c2a8cbb1e4085ce24887ad9
+b00155d04414e5e8d35903c4b2f842b43a333442d17a0545681a20188e6f58ea
+bee8d954f42963533dead29cfeb41dc6cb8e70c122948be88afec65d315d3a1e
+8440ec98d8191c891affca48a8fcb5feba24cdb0716466840bdcec527520aa6e
+2fc538993766cd018effd726855a2759a0b90c12995b475d6e4703a8fb39a59f
+1499ca0079d2a25ab06e0c8772889354840c25e10cc6706e9108b62ae8e545dd
+1be37905f8adb05fa0265b736c55ffd4ccfd8b0e4ae6b558c1debdf02a836d0a
+bee910df3e245d668cd878f528b6d9d16f4a0fa5540623182028ca199a0a8386
+ae47cda55b8c71081a78efdda2d709d973d69ef1b8f27db52ebb298bdf2fb718
+afff5377c78edbc3a58fa3d56a3b811bf5873a8f787897c62ce1a281141df5af
+be323c10e74c8ce430dd192dccde81adc639fef9e5fbfecee5be9d0da1547211
+6d6961617b07b6d8a8e184969da1cecc18745012ced3834fa40dce5a0e93a673
+8f51f43afb5ad5ae0939f4d3cae2b1e386fbefd8c840e2685b156c4d2f2cb563
+511bfd502ea44f80eab5a58ea38a682e863e13b07a442864c1d8215db4e824c5
+bd5abb55ed959042b65bb7d1272c0e4d429024cb101d8d340e3c9b918661a55a
+d29cbd42b25000959044f9d5f9256410d5dbdc9619a54229a1ca1a96f939cd04
+7fb87288db04a3a105e61b967467123d186ace108abef8ea741a4f3aa45738cd
+37b9af8a997c9f6a4cf1b335e3248000dcad05d7565aa99dc144076bb70721b9
+eafa34f716ca82830e8981c4368559f859ebe5a89ef467193a08a3ce62822321
+4637619f8ec9595107d156be7153b6433bedf24d330968dd9153bb90a09f0a91
+43c851d4a5de88c60b0a5d8e3fd2ce90617d14c2ad91a3b9e54b5447bc0fc905
+d27e961b1888ae18d5455fd5878cd43849dcbc2b01a1cfd2543ef89aa4ac0a07
+e4b3bc437520361daada75f76668e37daf5c2f21133653ea1855851a3fac8a3c
+a8bcfc449febdcb4566b0ade704fb68e8715eb37461750897b802d099af47ba5
+3efd1f74f9d13ee62aa163174cf38d8e75f793f763f9ab03fafd8d52bef23f5f
+86f4da2b1cfbea8233805b9d9e47e7c4232631a56360dea075491e410ce10b33
+8e9bb61387fda4dee805d038c67ef2c9cf36ef4f2d7705ec90002afa3ee52059
+d4412a1ace2939ec8753653aab214344cc65b2c2e2a0fca95fe6144953c4b2f0
+8dd8b6d0bb097ee5d333e4f9c0c7a54687c3f2745a5ca79dc3238ab554ced0f6
+29707805440078bbbaedc13a2ebf81fe9d5686b9ea7aff8d6d98cefb0f453fbc
+441e9d82023541d3dcdb8d255e16a7b48832bfbabf7802161c7b164bd7fa7035
+8cb6f75c1dd4838277a5baabb00276394b7d3af5dd1ec4db34c33e73c6be810e
+d1e1c0f07b397c466ca035edadc839ccc6c3b7b07ec1f669ed0798d1296b191e
+31551b05356c2fed9fe7680e0b5e1ebc1dd117730554c57fa82854729f3e4c6b
+2428a8de3d3555ffe2220259ba377772616f367966c983a3ac18b04471506e6d
+07fce9847e39bd3a845bb894f7b0e04daea1f10f8a23d030a0054659db2b8949
+a79ff3d3df5e76e7bc5a0163e1d108e30d47c01a0d17780736a36b664372f6fd
+71f65fda05da8fa900c3f9e6f78c2e3a83e97cbf56aba6e4d6be0faa5ec5d7e0
+bb97e659c6c1a778354907ed74bd12d79d7a0b4fa78b2a157c3fafb6814b4759
+3db04c2a8b16f3635efb2d03525be6d6372560bc4c96e489e971559e49960bfa
+a5be48071bc009df1482b6c9b452dd558cf537ed3263a10b278ec050492728d0
+4f335c50b2ff81c70a78f8d8d3f2e3f9efe9efcc2c5f4e52e00b97ead7d11295
+912b35bd0dab31c9c3e661db42eb98b86e090e55db806b16035848de4b60bc07
+f6b7d9ba8841a73471692dcaf020cda34343fb71a18910aa400927d4359c535d
+773d3e4b7b95fc2b10292a38660f0aeee380148ab062142399c767c4add99367
+f28c35d5b8aea728aded4ae367b41d43b6adb2032410d70dd875828fdcc5c976
+e221f05c764c5042d2a7ab671a234a4baaef58054064a3265d6e5c92dc532663
+87c81c142f1b94ed0f5a16eb83fa9811c475aa0dc93f9ccf237970904d983cae
+a7914a8e481400e58f431701bf0cb884c1e5c1f52d592ad3caa7d8a37d1c9568
+acc90fde3c96fca19bede3d8d92114d4702e1d38e9526fb918c2b8b8a80fe52e
+5545fdc287108a435029613684952b6d9f5b14ecd5b02d607a311b90b84d68df
+ef5dc37f2c55ac5aba975bbe16f1f18b2f17b4a9fb96754fa1228a848fbc6fb1
+c874307680c9c58f46e69bd27ca7e55e7cdd7cff4271341dcb34ee5b300e8dd4
+bdec4fee74bc74cdbbd0ecacfc1a4ac7f4714b4bc308ce6d88438920fca53ece
+8367968559eaf9df11a0d3bb80e650e4d836ca854dc1556b5ef36b9e64bf60fe
+650125a292b63ef6982b7fc80de9903cfc18ad9a1a14dc03d32e9ee15adcacc5
+a9b11139757d948bf23e547769f4d9e897ba15ef4a495735af47de7bdaa63765
+40d035a6ae198f03ef371a49fa624478fee97359bdbab730e4750a79e4ecaa3d
+182c6bcf9af7f132392b5e0940c10d8cbb27a58f84001a951c356706ad2463b7
+24baaf3a13bb4aabd80f4c1d32d48a0a5a5be34b03fb140c638de3cfb14c4c1b
+ca45d82c270692d193f0aa4286cfa414beaa7be6908e4d1208b5c2ed29407091
+c6fd5e78400d148a9ae24dc6589dca89ada0ddf0126203229b33de78f409c247
+90ec215d8889e450b6041406aeb5732dea90891059e6f03edba61e003ef6eac6
+925e213b1b4cb51fec8578d36527de080e9b6a2f2a8cc08dd888b705e580f4a0
+5dfac8b6c05b297368fe786caf8730fb703b28ff3fb63c5183f4f8f97719e4ee
+d65d13043f1b28b01a2b555ce07acaaf98e47d034e6a3e271122935f6224a167
+5b4cd9fa85ee51deb3b132b36fccd473d1ec278c87a6e4c2976ea93c5348cd6b
+9eb4dddf8f3c9fd6d5b7a746756762f641adc4e1bce84423d589cd4d66d521b4
+a99ea4c0766e9de460ba8f32d580383884a9c5e4907d7469752ee7d68d04372e
+db9579b6b20fdc55c9bc29b227248252f595272fd12639d0f2179f27754adc25
+334f39f6205ba259f5544c39dada5d7ff050d80afeac0c552fd467b10023e530
+f791ad4b12d04665a9f9e04f7fa19f74c73890f6bffd42a7da2947ae70dd13d5
+8ab82e2c44a7bf38c6bce43d7026441418f12fa7a2a7cb81b7e4d4d0652d13b2
+2d6b130dbf0b6433db47ebd76cf0ee4f2aca9a4d8c9b3339c03087f871793105
+d25f5f4591a8c6af03c42ad8bc129437b5698a6c67fb36ef68ca0e9291a3eedd
+1e5fbe6e97a8a56482c0c2f99919648a24e4067c6180a35f37e066af4134abd4
+e366adf29467371a14c80002b5afc5003db922fa92af13306de6ad27edc0ff47
+ad4da4af9771c76ef2c77b54fe2a67719478718989abf9beefbd962b449ad634
+9ae7fce4e6f425e752460dbad42163fe5e9f8eeb93375fb3f46990445ef2e59b
+de0045c70a03300752482f1ddc1755806babc181224471af546b31584d50ab56
+9ff247586ef4e52363df927b8810a5975152d92915572e13c92a65e5444a8ca0
+46fcd260cc88761a3ec8834e169af050408655605c793f9298edb5f657fd9e0e
+0142cbcc53e74a5b00c3d0c77222b4b5df610ece643275968122e36135d86ada
+7fbea4d22e1b51017274365069d75851b50e8401c1c34a228c23c10cbe330dc6
+4ed9b9aca8f3069f636e286bba6cad0159321d5c27718f410b39f99f4724aaec
+1afc7f6ff415a8fc73d5e26be4cd6e93b5464b764b7cac70827d48a3fa61c7a0
+f1943551599bb6216bb70b19a1a0b763e2f6b1c4babd95094bb87c222fdaf5cf
+9eb0467a5c93a6c42f4ad9be37d0137a057aeee887f9ced56ee8ccd85bb8c5c7
+7de9bcbcb7200c063735150ae6f2fa20cf3cd920d5198483ecb81c0eda832843
+27e0c1828a244ff1106323d4ddef2c71430b4c4de536abbb28f87e5ff93c3a23
+7697a73cd0653bca405e167aa727b132e98092629971184405e342bd802d5b72
+f1a61d42152936472892786973ef23a6f4254b3c3a42531fe34af9569c466373
+8bb88d96f27a8478a732fe405840e685aa416052a8a86beb62fad458e6b90c17
+a45b1ded2eb9a31e2c2f7ae2832c34326dba870eb3fe7439fc69d6f26d5572c6
+ce9b77a07d977392f52594c4610d5c1974378d2626a79d7f084ece540d32ed18
+5ef252ad0b4be2fc499cc708cccb88a80ae59a31e3038fef230dd0c101781d95
+b754338c0286917d565feda78b54e9a46f929fe351c74b649d05e014f4b9f941
+14d84223a0f3b11f6985fac073e2e6a3124b664883a92a0efb9b1d017f823525
+fac9bc150808a58caad6b7c0f4892e0285d86e79d587686d0a52284572ccde01
+7a9eb12d4559e1793e09f92a84e99a0ca40e00024a9d80ecfafe6a4a0afd607b
+ef3c195148ca0699a363057b98be0ff2981bb9413fae9891d8ce9fa723c278f8
+828be8479cc8f1a792d5cc074bb7ec4c3e65c1e2ef34c14241fd0b6c097ad945
+479e7a932aa4654f34705cabc11d8fae6f4aebed345c325f907bfb04b41b35f2
+4d261885880bfb5b1f98b138ef31e3a7b96e439232e4d0d9bb84139f41f48561
+d828648a60ed3ae4f092b86bcd6789015b085506fe1f266cc02ff5c5f3ad7280
+4456b4ef8ad3c1eb2f4fb956f726dcfe2d49a73d7e9f7abca13cde848550cde8
+7e083b85084000f39d390d707a017c63b44a9563547648c48f3d3e5c577a9e15
+a7e663c19b7d82aaa700229bb541d7dc95d83c3a953670c6aa4ba8a95e2119f8
+ed8b056d8dc936c4fea8e8b298819a56cf089c0be99820bf2eb51455e3482f57
+62acb88c1baa53ac2dbae3030043e8992ebe1a7863e47d9e106a60fbb83355f3
+b98fb2744652df95ce801cc169936cb12c211aff9d77ae43dc7fbef2112e2d44
+a3ecd9a8c2cd6fc94b4ecc6d1f5b2fdacdfd153e11d8c936d822306ce7cd6d7a
+2172d8f72ecff7f8a4d1eab8247c1261f8df09ad23e5893f900e1e414d13c25a
+1f235af5fff7743baac23af56c8d8baa12e095ace339a4541e87eed758ae490d
+04da6c27bdfae6c59e263c3143caf2febad0f61a618a372052f3f550e80f6a25
+3109a9d2cd667ba408e019fb67f8360d5590850e1ac72160f57ff00cb8e766c1
+dfd23f82fb24b407dbe5cd232641880da9054ce3934475db0ecbfca3cf56eb3b
+5d5f9d5b69a847eb7bae3f80f5d96e49cc11251cd4e3ec235b211a75551d3df6
+8c9bf13c5cb2191f42a92d9ef1419a0b31996a53a1b13235b6dedf6719ae00af
+af731d1bdeb99eafdb8ac4007e4a689aaffe5afa456ef93cb02b4a6842d60ce1
+cd0b0d9ec8c90fc47779fc2494dbc683b6db5360887f367ad6f5cc2986d21cff
+622f47f4dd116d5e0300b0d1d5ac15ff4162435c7a10ed614359d63d230132c7
+bab8dbc07d28aa915a91c5bee6f3073f6de313d561c0ef9c8746ce87cee5d42b
+74a9bb276cb8797f1715136b8a181a7ce12c85f5b9e288ca852617add6b72ab6
+07eace83bf400342f5273f2670212358c22192bd0ad7a5905074e42081df0fe4
+de698f14471fb3162a847f5dfb83a84f93f8ee6b6519ea588e0d4baff3953bf8
+81b78049dcbd4b84ce45bdc45499e3a5776b8e4e9063137d4e2556aa2e62b2a9
+eb5ce11735ed65ed5cd49135bba4b6d5f184eed7933f698157525da9793c8b10
+41d1bdf92d244fe701d30ace06cc28dc4afbc99ac05f2b8ece0bd19771c7337d
+d7470206c9e4f9ce5241ba4f038c02a8db27f75218875cf8334ed4bdb739a13c
+9d1469d58033f821c98b3684b51731451d3b05dcb1a7a691d3f0e2fe44a82688
+cc91e3711114d2997e159c81408665b2050c89e10a045ebbec019bb9e7567d95
+9176ee0cd69104974a26c6c7da1f690b6503a75cfaeed44e9e37be1a9454b7bb
+921f7922c213b175abd329b680f5def8130c911c25f4ba34fb2bf49c65ce74bb
+223904c11d7860ee74ce7aa113fc40a64fa702db89517b0fd444d0166cff31f7
+da476a7415e5c308ed98b09cb41adad1807694c1a39a20d3ffe0f33211e36d54
+395061b0df33dda283e1d7f5a9399bc144ccb6863611e7aac1817fd2b95182dc
+92755c6bac1b4074dcf43e556641616541284d5eb13fb49a4b375c31df13db9f
+ed989c06f374582862caad3b745fec8145649dcc6b23adf6b4852b6021394dd6
+a19ce0cbfe5b041afdad277df48db2955de8cda9a54379ed246370442196dd80
+0875fb8a65e3300e3e4b97ee6cb4134aaa59ee3517e285982a4e65ce966836f3
+0465e0a7d4bd636870dab95869fbe7b7b9cf28db23839a964612322be21c4f0c
+3cab665c4dd6f40150542c3ac64d147c09e5cc969642154a35edcae9c119bdac
+2b82e582e2ff515e1c88f91f474e03381368417b4084f6788a8e19519c9f771d
+92133700a7ffbf3e82ae50fb19f59a7a385d169d1393c150b88e5156c41e28fd
+055eeeac7fee07c6a3be5a8587be73b77826274bd3d35574e2ad12bd1551be0d
+d4ff959077cdff691b265cb353ade31f77ecf60057c0eba5c51a7bdd69739633
+b45c312f9b77b0d87ec77fdac0049645c979663dc0314c8f101681e0e47aeeff
+fc8f7d39a57009ae49735d6c386262bd09cad0f52d862b60832ead3b3cbd3efc
+bb992f901443f78f45f9112a8b79d83888b412803e5621e70dc7b2eb475a7a3e
+0d07c36ab6dd2eb352738536c879ca83b31906ebc89c7304dd1cf95559cf7bae
+eecc29a5d0f839b64c0a24b6b9135dd0fef4bbfa01b7d6a86d407abeda8775a2
+6e20bdc6d2189cd7055b39d30315b9ba68f717840c10f27b88e22075a7288c8c
+3db83c27b55d6032a2b71f326469af86bc7342f43c4c95c8b13bf295ad130cb7
+421ce98e16cb34afd5342289819a8927447c0df7c98e9a965ee83269056a7c91
+961efa627927d007bf3836ff5eab5b6241ec8debc29dba3055d38101d1032307
+86644c8ae82f2c516028f780853be93fb7a220d71e60dafc26e582065444bda3
+5afe0470dfc2f2faa9d2af52eb11f048b16ebc88f23f33944085e1d9b1e85870
+18029b166b6a6eaf36f2507415fc1a58ad4b403c89994910896bd32acb360703
+17534352e8b001ea7a79e81c07e740c3dfae6f247579f957fac2c71cc792f7f0
+1c5d51b81f5029866f454fefc4c15fbc369d0b5f45633fe46a0fb141ad869770
+91af47762fb7c2291a413e51f9d697864c8a7018818a03203bda7d356382fe40
+b271118745973af3455e9021cffffc9c429273404e0813699841332ae86aa519
+608e948ba76806963b3656c44263a1c5df864e8634cecc6f8dc053ffedfcf030
+fb3c759daf5376152d39ad6e724f2a09a06e77bc1eb78fac830149954261f8cb
+765d2aaef1847073e6a5ed1d965ee9d3ef47650fc131a229bd68a22aa3bab374
+b0ca5b3ae476fd880957cc7c08f8729185aed5de06dc4ac1727ca7942ca50e7d
+8bd0c697d43c70cc1992205c9b914312b4a91344d47be4a9ba955da671977af9
+a92f0102e56705575bbb13106995f75de702f0eef35474ac55608c72bd513cb6
+e6ad69f1dd84869c03cf13d22550471c04cc72a5664d3154d56886fca7cd18fb
+e7e172dd975923c6e6a34dd59653260a28d90bd16e5ef7ed6a76a55345b08015
+b5e1e209e9db3471b9d0008174922d1e120f00620519d526e85a80fbdd658357
+56428f1549147e1adcf069508723625f929ceacdcfbcb0eac8337b3de3276894
+1298a952cc3fb5f55589aca8bb44111dd1023305f8a1d2b70d1da6fd510efddf
+a55a18dfb4cc9a1b4378f9dba73e29d16daed328d3b915821159b0bfb6872240
+acd1a3fbda983af9d64188be5412b15a03b52982bb60a7748f06897c63d97775
+3e60bda00e6cb10c4fd1c14a49a9ba5383f025393f79efffe774ad9cb66bdb11
+e3feae202ac9ab6dc25eb347b967b9a9af308368cd0702dbdd5f8ca2744eaad8
+0f0a9093c127b1b01bc718e7979c40d2c3c130dc3e9eaeed82e44f12872ff753
+ab30138a8b8ffa23123abf35e67a20a44fce65e6f7d20abaf919f8e1dae23d04
+5e3c5ff827a3ae677e0ec586f828a0f2a60e61eb82019ac0e1798af174de34f4
+d65c27344cd9f575de52695e1564d76b3bcd81d02fcb1ad4b12499fdd6ebfba7
+c1c55c98c7418936fcab4606dc7203dbca8471df5828ba2a8d6ca99d4aeaeb7c
+bef1d59a6daa5d0e7ec0a82fd392790b8e17d25f51cdf802784931d9f10b824d
+ab7026fe65d2832d166cfb9b108a75b02ac5b74c7726cb5f178784f68d478ca7
+e29097b39014e81f10828dd37e22889551489ba2f6a2f53c72e831c75d2c36fc
+e19cbb452652673c10a79b0eb2cd2bc4360a464a8bed2e369cf49660c9ece723
+7d8cbec3c3d7fe2b223b469a0feaccea1451d1cb931ecc6c307132d89a281fa5
+85626cd2ab00279137c326061a1f3165b68d04ce49bf0d308a21168d54f4a3f0
+7d8b8984b41248a4b5ba90c1b88403e69c920179c41c83a5e2dca301dc67bc54
+7171f622980e28f452568ea30e9dfb145d06afff9e28f43b04535ac63250cd37
+22ffda3ddc5049cc71ff4592766a786190c7fe9c7e87331b3bf1c8c25e289e00
+e9aa9e8ac24a978998366cb808f1dd3bf204af1adf8b6c3fcc93482510c720af
+03e18d7f6820935881c4732bc2bd68b1663260581ac200be9b0dacb1e6a910ee
+d5186437c5bdb966b939edaacabc104c3db19553117de2371839e6034f0f9f9b
+878626fb6de546c64224475d416418a1fffd40341edf45f4d91df8571000b6c4
+e7e890ee4ca95e65f18bd0ac230b98d2487a59af084feb16e4aa48cdcd150549
+f93a03156ff8b76913decf03fb5ca3213b14686b664daab69d9d6f056cc6d372
+31122a599b78080a0ef40e75b9d61b17612c0982690bab7a8dff6265189ea5cb
+4d8607b8a3447e2de6463f97e35c40efd806d021d47f01568ad586ea33fcdd4b
+37680417b08ed5fd7bb1cf72b3751fd861e0f5170d6cb02a803c908420482226
+e009240ba80b0275b1402f6536da71325bd6c88e2d0642d60f964ed85acfc28d
+d0fbcc7ebc3d462cb97fc41b8c4bf8352a09d71ccf318db3f2ef5e16d6711c37
+93bd1ab7f7c73db89c93b7843c2c2f2deb720a2e840fda79d3220ac62c280628
+0062d213b27010e5332a1bfff9ae17063c4aac27a190a5559933cffe1fb01db3
+b048fbb50add93b115ff5370ca33678389a703bb381841ef19d8cd1b930c2c3e
+0f1211744ea18b15e6b29d8fb3fee9f5416718e1da724b7b3a1e82a28ee059fe
+463640a5fa80025208ecdd98c0bab3ba2cbbcd80b730686ca27b14240fb1e9a5
+c751f88f42687a2901c489c8e7e428616ae0553d503d093729b64edf3937434a
+390e6c6e1094848d10511feedeaf239a1afeecb5981ff1f6d042914f02f17847
+4ceb12269438aa42a0f9bde00db8783c01383be5c3a45941f8afd0177055870f
+484eca20fe4288a96ba1d7a341b3c48cd24ba4dcb14e703db1389a5659e86ee6
+e4884fa3363238f8532d950d5b1566dc7f68eb0a31a0aa314deefdec9a87a7a6
+e7fbdf52e14cf413fd6029a77aeb5f0806ba51f9e63063185c926ad04df113e9
+265be0f9fc4e71d2b18680bbd314dae429c9eb2d84789bb85064468c26c04215
+321132cd4eac9046092f1979e2833d2d5d1a724d10629a0d4e9c7e03a9d3b2b6
+3c07d9f4cfed06a476ccd1941a071679df08f38154c2936fa47177986edac8a7
+925814c83814c705994bb0c9546a1fbce22111adaad613960c0a95d98d9452b8
+7441c3666b539950a27ce6d892c2d65b412fc58547ab21a6d02f1209cd304235
+ec32f4349fda2d1a6d9299c9ad560781dcd011e1d2d7cf91ecb6f5193bf9a33e
+9f73a66b5184f9266afc4183131e7854f949d4114c48cc079cb892e8cae078c9
+c88223e65723bbdd78e23843515712bff13f4544d43a144fd180639c00e0f6e3
+9f871f8c056d8e774fd3d05175e8b411038483f333e18f169048ff21000fe84b
+2541309205f6635a05de58dd71efacb7182f3972ea4aa2c5a6483ac9e38ddb1d
+008e5a2f4940fcf820987e2bb7b73fa38f8441806ac0de93736109af54048a1d
+e95cab99b1f3c5e104af17c67f0e0a99a0dd9c7cac7d917a8a0032abae8c577b
+f6a647f505fca2e854998057387da13f8189a3f9bde597d66a1d87388d840f09
+8a9e712c6e3485eb1b8005e3e3224d959b925a237e167e10965f6aed03941a4f
+90354b6bc6dcab1a2079d0fec8afb174b57d339252bc36a554682cf59773fca0
+36230a8bfe5538f7770e126ebd9dd87d800663c14f5f8a4d4bb935fe7d05d2e2
+11b720bfb754d323a8a252919a048db606eac7c590ad68115d052a78d8f0f7b4
+89e048372b293ae6a44169ea795cf46d25637c7252bc7a0062896f546624e151
+891ca2e70dac19e06a3fd3c67de2ff19c3d3838413f82a38369c5f2166ccd869
+5d4dbd9856755ab9f7c94810488bbc713b1aba7f66c07155138ce859a79f023e
+8954decac00adaa02e38cdfb927460cf42cd34caaaa62acb7ad369f917bdabef
+9e2103e0117f6f99b1e9dd73edae4b8c353f6961c8f078327d7256b5ca6a5996
+bcaf0cf21718f574f6f2b501dc8a3b115d4af7af8062e17a9f43ef05c1103ed1
+b406b350ad72f31ed50de231536a0ca1db4373b3641b39471774426a87d5731b
+7aebab1f6a9cc832f7a8e542abe1b8815d20addf94c143f4824ad09d702acca5
+f80bc581e0c848248613f5d86a006ed509a5a55591852200aa9a3534e90ae0fe
+95098ccb45bc63a4d86e2e1883db0bd1e293360158a5403467b96c1b9e11f082
+ac3d3a5ca47a04b7605ba54c8f9b34abe2fbeade06c3f3d32d667b6a0074eeb8
+84a40571a4e5f2c31483accf78230043c5a17f8da13b1bc9fa92d1db17724bac
+73e48427410b582352164389f95357f53902454e167ab86c5a5c404b648c4c37
+aaced6c49af4479405fb9d32f6cd78e2114b820cc03877c29cf4707642c942c7
+3874aa52f3f2955ca7832a12545f745cfb8768b1d24354f9bf721d55bd9d0722
+acd1fbbea4b1f26128902892bfafc8c9b1c5860f33321edc5afe7009f6ee10e8
+d5cd98fa521b3f45869b41f8ca4032e8f814ed3eb0dac7828893cc1ef2649390
+e70cef649f6c469c830a2820ac2b19bce71b76e2c7fb653093b9fcf65477ea03
+0e09e3de1306a2131a1792a616755b670e91a002166433e03a221310b633f96f
+7c4671b45b9e5933f67019ce6992ce8575efb90d4691680a02a4acb8978ce207
+817505cd7f01e027a965f79e6d698d6de13f56ce427a7ff627ed78237f99a702
+efb328304bfb288641d8685cbf43e96e85e2d6aedfe97e8bec0bd5f9622b98aa
+316450a090f406879c74985decdc96962a50b5de3a5848f92492037011d0f4e7
+dd0fdc25d578a0f86f80a8ae25b9975fbac71fbef5c04810ea0a19b89facc0cc
+aa73cd34b809854296cbdf532f9593332114e100ee289cd181bd3b3aa05c4fe7
+b3f323638087a2b97157b23505f9a653cee29017a39666e3680df121aaf641e5
+f971007acf53e8065a0fe536bb72e2fddb8bc51701f5d069faabd6c101e786c8
+ef8d299eee8fc6f7f97c1d9c09bc82f8204c9231083394ce840312344d592d98
+f652724515c99539d2c463fabd42c8360e0f0f5aa9739e3d2e484d3d39f2ce73
+f522a79dbc46266aa36518f0278432bb380904ebb6f30ea6edc835e33b81be18
+21b6e0d803f31e8be97a261b3b1426281b00b8b6baa6ddb76ced67d802415006
+1dbb011d04048481d815ea0db0dbccb20044fec53da88ec8cadc39bd57a2dcd0
+fd36197e8075729a1bd4517751dbae89bd44e2ca7cbed060e3e6e6b124b88ae3
+02f90a2388bce72b3ce9ef9e1bc8392246271d4cd5fa6067ce5eb426b2692836
+6f00ffbd94493239104a7d71adb54f3f1d74447676511fdcb82674cc89ed6b67
+7e6a24a5dd5d29f76eff3bcfbed6b5e81ff42a04b42b9c30763e501b1bd830e4
+4c9890fbe3b039eed303f064e63b7bf7291c58f2b45aa511b6e6fd2b169d6ce5
+3c9d4331ba7029bb94b7b5265da7c0cbab1afeeb6368ad410552a8b23c4838f5
+eb07bdafc3d60458649ac2c8fc787bbafb1b17128e82359c89a20075bfdb63e6
+d9be78d619689ca0f8c2e9c38a5e8b5eb42b4d07a9a3542cf835a423cb65bedb
+ba210e7b2073919ef7d76471d17b165d6aedef70651f46d22bb1b6d3a8b3898d
+e9370aaa67830689c4773e20e0264978655ee01d30e42f8a6ea3ce955c657970
+23129accb969cf84005341b24f92e23b4b3962834ed91ed40c6866f6aae07219
+f49f0d260d329c287d4bf91c504dbb44d13da65a96b522673b7912dde6fe5e76
+2ee0d89ed7fd96886e761c4f262c88db3e212c603e8981298e4520428c058480
+2f5fe0fb223f697f946330d97f6275c100e5d15f36867a7e9de9338d654cf267
+5bb52e5ee6c925faa522780519c15b655ae54ff19203a3465582e7b40e904716
+45152728ad912b05d25eff9cdf03b762507d6980908e22ba5399b2fc2cdddb10
+47d8d3b6d502ae49527a62ca8968a2c8b6c814ee0cf1a7618a8ea8d7e7c5f61a
+50515dba6ed6187394d8051121b685be8d029d97449c85c95b4d68da2a31278c
+dc7f39c44039a3a363d243910bd26c882ba1ef5be080d0f8b8eef954d9b7c186
+ea5e664aa4141f9ffea443d59124e25899d0d18dced06702499ab54e72894285
+fa1ce9a831ab914be5711a225ced342a9de51a7178a4e5c4491a3aff1a606aea
+4b91188a20a63747374aeb9014cb11cd6f52bbf906af5aa6b5ac2cfe27e75efb
+c554648f01f12eccccd3d94ad3b3271ec690e9a874284fd22587a58a4d78680f
+20bdaa27a59c0fe1eda28b86834234d291682efce38bcc93bf85ebb7b493066a
+40ecb74eb37dd1d018d3af16977ca7b539e5d9e68312d255b930cf1a859625f9
+bf81aec791a6fa63f578a4ecf972e1f61ef9258eff34f91ad002ea7a5c3b29ee
+2ab6d77ac6c14c71ad07895206b0a68ed4c21e197851ad6944d4f61f3d089c06
+d0589e1dc7494f3e4ebab1f56666b2290679a7656cf84ffa3f1a58c1fed9a62f
+3c1b0de1496a711f5b8e074712c3b44f49bffe341ed6e5926368388d0ff7882f
+ff24a63273155908d94e78ed8252603dd8eebe7d19e588b029e5136840a26664
+9918867370b5f8d3942127044ec81cc46d1e88fe88e47b8af625b6826f2c9ee3
+5f8a6afd319d66812d80a171a13316c2a4c73900686d4616126aa83d635c4f9d
+27fae14feef992bddddc1bd7c890e67cc544c31ac9d05541d7af116724081cb3
+8517dca6cc490a02295fbeed7e147b1e45033c70afbfd711a2d41248cba6b56c
+94e1b2c1a12409ca6e529f82e0f23bd91da21fbba01f22968a8cac2a231362e3
+61523dd5ebee42c524cfdf2eaf818650db8da4e382cc519214a7b4fb3f90e82b
+315db75c77b7ca98f2f21258a68417326cdb55cf6c2b31f197872a8d9d65e17c
+dd64e130ae2d6d2a16a717de89257bca08f5bca777b3021ce17d5b1a07aef098
+88fc1e420c9e08ae3ebd4ecd38d4417191658035a623a3dcc20257c0e1c4d1d7
+d7e0e54defa4d27fb4f815ddb2604103030e95f27871c7b0e9a4c4610948fa16
+012c4b30cc2e6c03a7ccad7f0643b9a1cc1eec9c7a2681ed802ddbe7d8eb18f0
+2770b022d4f2aba40045ffd23a888e04b800735f917f0abea17b934e4a7e4d06
+aaad3add44cca6826070ec12bc89e716dc416bcefe0725c086e7804ab8b7fd91
+6b1e1e3b289afbcb202d7074de5c61f2819f13735639aaeb50e7dc9aef89209a
+eca7316bb46f7f03ee99f56817d385102faaee63ca8eda61bc7b905ee48a9625
+69dc8122ebd8bfabcb8de515b7dae09542db46e0e6036611762442e8d3943d90
+a89024e8cf46bab577380287f30284717c433f304119762b276fe84fb0d0f075
+3d6591879886da5fc0e179a7fef454699e159e8a2d35dd1f678c8a5825febc07
+6b2d56fc48370317c9fd3cc71a4cf3772bc6a9f202a736e863ddb4cebe059958
+1ee4c17ea43b964cbc2add4d2e2ac013b43d11d1c1471565b90d1bb0da9b914c
+44810b030e137b37ef8942dbf6b041d9f7f90305afa411ad92527177f894c2ce
+835b8942eed694b6e7503851d5dc49bd40e05056841fdd21d6c29012161cff37
+8e213657bb2db9f3655cc1d18b8c6a4d31b2d37036b1be5f51d1277cee26c758
+685b9b2bb9b9f9e88f9146ba4ab794f535358fb49fc0d40ffa41ad402644120d
+0ea676f3d5588683683b79520de05a35c0f394f0f5ec6d13ac6b1df71e030dc6
+9d51c242324df025c96c5429f19c7190c77666731a46f8143c37252db87120e7
+8bf1c9f22b7a80dc4b999baa805d9f1eb5ac4e28a70d0fbd6c7dfdd7c1fbb560
+26073cc5cae5a1cdf5f8471585165ee0191d353ccdcd05f2117cdae9cce71d72
+9d0f61304f1d29eb2c5b6ec18290a9b27baceae80a405afb5664ba513ab26596
+bbaf87266ef51dbd35112eac5576b04b650031528738edfb937dec3f9b0caedf
+2447aeeffec2129814289b09842d3dd90b86300f1a035849b87fc2dc2e4292eb
+568ecf43a518512f9dd5287988e688c5f040a442150d447196bc5216df070c0a
+551928a0629cf3c7e430baab504b2315156cc367f55f090dc1f6ad0214353780
+f1cc7f4a9b262debe551a5de29cf1cdf08117de2df62f164fa91d166eaa72758
+9b862a3dfdbff31b404c11e411f0c642ca06962c18304403db2eb93cfadd392a
+c7a030971f2f897017317054572a4b5fe8fda9dfbad4c079b0c9c3e3336aae94
+b11baa3d0257a88a1919be409db0e1ea7f2f55fbafd3b4a8b5f55581aab8fb31
+a233d2081374327616cfc745b3c7039dd295a0b5068560fb68eb67c567c589f6
+08a45a312ca25accb1775badfd7b2eb0f4bc948b62362b3bcf40766b987622b7
+da7d9b68a7909104056fd5e0709a5928a0c314d407745c9b5f4c9a65483a7bd2
+3f550b7030671bba32d959442573fe4e6cbeb995b2fbc273b6894f70701fd2fc
+3f6ed6fe95cd521e83862350df167894510c93b44032187f171773b42f483e61
+2048daacd1f4be69c9ef5e6c1f63c72b759651047cd9ce71199dcbab1c36c1e7
+da6dd872b1a4c03d7145e4dca72064e2ffca5e43fd66a7c532feda1000904032
+843fcb1e24d99146dd27fc507cfba1a63b0843af0ebc0a336aafee4eb27d8796
+c0c1298a23874928f35c7cdd27145835742eab898c2d04e7ebd5c0fabf4806c4
+d44ba374cb1ff5052be5066d3a52e1f2ba4841fe35ae6c097cdd1c8b04883764
+efb0f51f646cccd0321ecab07647066dcff2bae0916983248b080730e45b8aaa
+df9fc1805e6c5fb1b536c598ad35c0f69f561dd949ed142ba7fde6778620e8d6
+ac443fdfdde254bf1475ef95bbd54d1ec0d9b531d1cf690cfea6ea9dd4d732e3
+e5bcdec23e92eeefd411298360effa920d3ceefe61858b9a356023766a181bf2
+aedd269b7d4c610fcc03cd8fda705186b3c842bc6d7299abf6f704b419bd2b2a
+a3f75c8b0ebdeab45eff8f1b9bbb9b187b03a0735b28813860d2916f7279fc8b
+e6df8e6f5155c3dba782aecff52b1be8710747c51e9a76d2574fcca699b9fdc5
+48043c2fb363a596f6b3b371657bdc2be900c9707a78fbfcbd1c0bf881468e2d
+f7adefd8bf30dee1b2347aecde496c012f0cbfe3816068ff31f3b3adf56b612a
+632703d469de017d29769ed5d55aa1f37651c11f0739ebb4c3af0a3c62c4523f
+05081ee09ae3076d361ca40f8b13d4a631b9f24a5366f4e678481abe3993ce9b
+ab8813c00b4b95be15086c69a55e65ae908fc071f0b3c59daa9719321edff148
+62fa6efdd42bd192d0138f6bbf5890f78a9c38d147ae00157f4307dc3095432f
+5bbe889da9a5bee8669c6651e758ed5ad38856433fb9702d70c12aa29cea4424
+8e50e17ce514b8ef3131a972b8db0b0be78c93831806e98916572e7959b8317c
+54149e5372718963eb74983316ba7a0418a921433d759e73cdacb1bb6ac69df6
+7961040ac80e5459f3120ca3c8eaea02497b64ef871030f1501ad79dc405019a
+00e355ae3b7c48ed22873196a98637ba2b25bf8b946ce509da8bd25456373c29
+dbf08751baf2bbb23ed57b32d3abe306c29eba95e3a404c3a15b172a8313932a
+c5ae1588a0227de80156713ea8239110c31e0452a2e2cf376cfcf90a41f68bdf
+c08018786b166a5e9326b1beff6a4ed7fa44f523911d1819cb6aa1ea58fc52cb
+51a74b886466964dba973726352554470d5c35353b60b68830bfcc8f14cbdf00
+f8974ab1914694025375901d71324ba933b2ba61204c4ba5f6429077f429f17e
+8f1f95c2308b2900bca909945f96f33d73a7a2186e28d4132db91aa6d8751e47
+fc1665361bbeaebdcf6bae29423c6fadb892bab7cc872ee4cf3490d2956907ff
+baeb199c2ccde45a2649aa2e4d8dc65142df9b332691d9b154dc072ea33d1710
+e64eea0969e54a54fcc812d6c8ce00c790d71ddd67c6b8d4ca7baa6575cfcee5
+8af7c205f455c24ede135b653875780f54c98e3159ed284139f4fb00f64ec8c1
+1191c0b484b9fd15fa6f0fb5b6999de6358741a0a48abeee116a44cbc015c0b4
+5584b76b7e73d6d3148bcd17cdfecbefd83aa402162367bc40e7835fb64a4b04
+dc4827158e4b8f193bc470c9bc0c46047b4cc4d797ea9ed7f35868331517fc43
+15b9a080d5171104dce9c6b8d9105b0031f4b1f9ce7d8e2b7d08c5d633290fa8
+8356b073770339203622f09407cd263412ecec6c7f58f8d8f5be935ccdebc64d
+5f0a0287fdcabae9ffd00c24e7cfaeddf9fae5f60d4864e8268885f71a8a941e
+facc6eca987d76f1ca3ff9795045b4b9057a7ecee50a04d09b859207ee869750
+282bc24be3dd0d84e33963c4c1a484de057541399cfe79a5bded1bb90833d8ef
+4bb6f36b7b9f236ff00bcf441ea1e1ea04bec9f3402e7993be2d024a5d06c653
+6c2c090d51187ad3eafd64bb0389da839cde99222868ce4ad6e03d6fad5411b0
+9826c410db9179d22db159ff72b0c9246c690b527bbaae566d145284f3e7bf24
+20c2a7bc2ee6607ced1e89c558f3a4bf132f23ddf53b240284de7c4839ef3b53
+6532a60c9df1cbd3470d44c7c0d26136cf4955951fdc3f0b85ff4d4057f47a6b
+8648c3640059f4543316ba9007c0e784f79c79156b04f60b030cb98b4fd47577
+dccc5c223e0f1211744ea18b7c0e676bfc5ca9592a7d9e7c13b93bbe2bc62251
+71d0a4c44950754ddc2ccd486d2b702f4188987595bf8988130b0c059b279418
+ee3d0e21a1a8e2b1852ba7e01e9cf19746131e126bf5eedb08e6c635d700b0b8
+caaf2de2c01f82ed911278abf80a8a47aebe6330e0d1bc308f00a9fb8944052b
+4cf11ec1f73f3dbac7a9124c6fd2009944732d672c3d4cd2d387b4235fec275b
+907d25342890dc44dd670020814e4155463f6bc4804f68efab0501388a3ef816
+e2ed8b83840782e22be4a0b562a6b0361b8129872c5ca1c0055940378bae85f8
+2a39e182e758f970c22aaead53ee5f934dfe39abfd55bdd3275b65b0b71eca9e
+467dd786fba4f8c7084db6069703dd97b05f80013cd99dea6698ad3ab5f09f11
+5a00e47fbfcd13caaa7e82191d1881b3992b69c6cfa15307d6ae8370252145b0
+97709d56b69ecb253757ae7b1a11cd9a8c2210af5c91fe93bc2147681b19c75e
+a360636446935d85470b0c28d927b07afcf63809a53c0a8baee76632982e9fa3
+1cd9b8f117a7b41689c83a23043550a3f165bfe81f0e0e224a6568e60e28c466
+28b633d66fc7370f65fd2ac5694e7c9a616bb88298cc7a7bdf9a39f1ffc1aefb
+16de11a575a58fb8f65c47aaab60b465d3e8eef5240a9773874bb658d7356723
+f449270f0eaa74cda1a6d5f732bffebf4415e77d3a566d48e47d7629fa6b8a21
+6e8032e7ceedbd8ead4e922685d6be6448a14fc839ae4a74f6a5a59c01375a4f
+d57555f569d036d0337351c7305c49217103adfe4b6ddc8376f134eeb11ca705
+92d1e2e80c423ae05a0822375e0d3f059cbe839b07a290a6b421c7d513f1961b
+27c632269a8c35eed49010c561b580412b9363aa0e9c76e210b6a86a55016da9
+e04e8f7a5c525ad3ef4edd7550b6e2f6669508051973e36e435579c178d2d39f
+3e27bde7de754bbc68da87698aada323575cca997a1cc75f706634834d1dac10
+7103b40c71a1e59358ede2d1ee87082235e808481f456968bd649a268e566680
+3a8471be291dea8358c52ad22508da5762fb1e1f6128b34e32bb68039237a400
+c1afe9e07c69d8b486281a9ec09067d09b0c8bc16c0960771066aca2f4c18907
+4e2ab98be4da38b2ef92f3a922a1fa3afa66ccb7dc5c59cd06f7b6dfe00eb86b
+8abc28551decbfcab8ae483839cbedd2cfd6e7c8f6348444ee0e67e943e88444
+f9f871e9291a5eae7d63522caf0396c4cb477d2df5377c39c46b14125e100ad2
+4f15e1aa3dc33b356f6a6d9e1404efa7f05b6208948b01acbfb8a1079181872b
+f06cdcd561a855b1884008e605d67e653ef3357b9c6d5af90228c6b4676644fc
+cabbab905c7e9164a901ba7fd1c4742d454784887907b7e1a9b2d1c844ca2f03
+3991fd9fdd19fb4af35738bb380bb940b03e9fddaac1f64afc6e3c99ee1d7dc5
+5ce48afb651c9595e41c894a8d75af382c709720a2e94f03fa45ec2cfbf7d3e7
+6fc0dba65ae93bad352e1b14aa0f3c39cb8766c106a1fc656401e4ff5c4584ab
+974e93b2bc7f16209d80bb566ed5b67acbc84635bfea38f1e87c39027a0d1ccb
+1a93049e936116e1847fb3123585096edccbfd0aa63e623b8503a0d4760aae40
+0a88a7e87400875eac6f14fb7a7a8682560de6cbb43fcdf303758e182c4d12d3
+87ed4533d7c9f2b8e13489130d6ae5fe066f4ad856b167725e4ca2e7b842dd7f
+599de83285aa617d5ead39c7ac6ab77e42f5e1153250803103f7769cf35430f4
+fc5b91642a1b798ef39d359f7196d3f7b0f864e016da2e43ea1a2b8e8334e8ba
+c0af735eeec83a2e344ac6a62461cc1dbfacfafe1cc1db2ab1655ba0a7d64eb2
+4f165b64608fb31dddf15f708f052adcb6ce974d807e6ad8f7b94979ac09fd65
+163a3699cb80a8a32d49efcafad68856da3be30b616873ee511e361963983106
+f700c080555e719697b0de489286e397f5f81af94632216e357f1020dacd270e
+c7d9bb4235aa738a8dca7f23680c377a643b0b59e6a7fb83507c8cfbd1df3758
+3a565760689d25526f064e961d0c3d2071054eed80850bb10ee9e73f27654d20
+32bc2a750714ece1a4ede7b7c4be79f4e0c3d7a6b433f44774d6cc6a93f6dd1a
+29b855b805788ff4436794d2aa348da5ab69ce698e7d5b1956d1a7888607d7bb
+a6b59226b144dc8589e4787c8ab0342c8a085e12fe9ddad5fe1c185099e207ac
+f4308371fa4afbdf90eb5d08a0076d70615a4d17264baa2e712063032c188877
+fcec4971f2c7f4a9012fbf3fe15b76adf4f35e5c0f337862584b12d04bfbf85e
+8b3e8eeb64bc0fea31faa7a697b34be4a8c3fcae69ebd8c25fba58f3d101c9cc
+7d6cb4a6835a5262a28f9304dae690bfaf6eaeef09d44a0418b78e2a4d381047
+35a4523e8341a74509eef699744865f10168cbe739d4eed29e6542cb0dbad470
+d9dd0ddfb8164959ec59a632863920e18ab5c001845ece330868dff7b7ed51a8
+c50a11861df524d4dd80c7b370886921b9f6b28015a83412cdc4fab786791bd6
+37081dff54753214e35477c847ea13286c28fdefe690fef446f4cad67ac3ba20
+958743af664767e8d8ef8a389ec1c138128aaff1b7557b52eb59dee75db67cbb
+60d9b08b3a443b10bc84417aac4aa89973c8d8932af5b91b82b7569cac09d69c
+0cae12b7411269a1c550c94dce68d60ef418f21ab29ab552eca48a4ee94a4b27
+5d174ae06ce469bd5110d23678269b244cd596650ab74f024460c84b564cfc31
+f10b7d0a15166acbc8e9a70668aecd4fda96ec72af4d438f3e9c42e068bb2504
+66e725c097f17e533d6ddb0223a8c018e6445a468e5afed7d7d93ca5e8807f6b
+cabc59e9d7eb20de335ee9328a6726bab86e2bbcb847e18e2be079bda3c1a380
+2d338cc33a35b0bb17b37188cbcd54c760cfb17d5da0c66197da9123942a8789
+6bf0e622dc9e93457e1b73b7f5b96c894eb76af290e1f9dc439e36e972b3f865
+187781130941b3596aa9dc699a6a586c1acfba7e1d7b340312856ba9caf63662
+02f6f303f92f15726dd083b1555263d2e3bd50baaf7c0bcca4ca774f5e35cd0c
+85caca2366e06fb942b482bbfb1926fe47b1d95a83a5143ba801efbc7d62c003
+2f915672ee8ae1b235096c8f62635bc5c42a03d6be518e9c4a68ee0d8e48d1e2
+2de2828cfc76efb4a12ba5c3a3a08175545256dfdf53edf0c91bdff981f5b9b3
+fb8b9ffabb007646755e76a278cb1752b8081ebe91dc77177aeca8ca90ce2077
+f90d35e6f7aa90b1505e472ae5f09bb05f2a37b5e2cea2e1a3e5fe0e367a07c0
+f31f855d7f1c7b4272ea37398bec83f7b79fbf86129799e2ca9e07b2a01213aa
+2d73499beb7ff978dafb22518227532efae2a60cae34347ad352a00c0adfea43
+e3f95208c6f5c21e1fb3c0b07a1d143d5a8b55cac717fda2b72ff0c3fdbabf56
+34cf4c2051dbc59ec4c00b6e1cfe454cdbd9fda033cda90a2b66fad3e8271e17
+fafa42139467358062f2a1c244c48d52a9fbf5c1f360bfa45d0b03ec7ec4805f
+3e7f67e258936a690090abd309b2b93424efeb8d5d7deabee5eaf0db5d56c33b
+3a728c08624ea5d0c85f77beac600c5fb8c184ac63b3fcc5344f22dd878abe96
+4320ad7ad8803c28b4753b071213851247f35a2f90a59072312059373cad6276
+2a17bb449602f8424ac583843810a1c6db922a87bbddf800bea24f6ebb071d2f
+3a82053bc5ddf3eb11c3c32f551a81deb3000e57282552fc0b8ee17ad909bb2d
+b851793f65123af449d07279de0999aa5208cc293593eb9ef607c7e20e873e64
+6097ff18507152a8d111e24e419c148a5a1fbd339428f184bb3c5ce3a36af6a6
+9b211cee9150c40506c1e990d19ba8b4f0b5c01e7c19a05904cb63d127118908
+5ab937a91b2dc3b59ac6bd1971c084e2c156c10661b2c7a3d170bde0fa3e682b
+abbbf187ff39304d6e5ae80eabce1c54a0bfe09f22d97133e94afa9981e837cc
+0a80b34b29899474b65ebc6df73e5abc2f2300226c4e96e7c32b3f80a26fadc1
+e503fed433b7cfaf681e6cb33057a915ec53112e76cd9602ff64a75543eeb82d
+e6f45aecf9620a5a3bae1f4459b9ae6162e472e93eb2e0daceec8f0608721072
+e456972d5fa6bc4a45afe5a8a5694e538e659107ff4cd6a8ee3d38544e397ffc
+26a98e8771a5e07c99402b1722b09cc959f9d25126a82c7820de21a9855eda2a
+1b7e4d0145ec7017caa0349bd13177070509276241a0aa7dfae14f1e2d1805ee
+58714e9f2a647fcede216c7b3297910df45f91d8e0c9ec279f16a744930f3b77
+0b80d299b44b478377ef237341ecc5d9a59b333cfbbc229be944d368559988c4
+940209b2a4fe6d55713acce94181339a1d70589e96a54a952163b072e116f5ae
+4e672556501ac126d0dc91531894b69d5c8c6bc531db69a961cd517d4bc07375
+5683fa7cd9a77fc56c6ae62cdf6bb0857e8906f835035e78d3f7ad14375b6f21
+fd0436e6c914c3b8d6e543df1ee4b992c024e9d9fd316dfba3ba9809a089cc99
+8f34466ac94d689350a5075998d7240efcda132f3b8f2ccc0fc78f5d77a313e4
+09d5ae9f2839b9ba67cb79e1a0e7ff8f77e766aae4e1931a51890b3f0e3b39bd
+5412ab04baeeae007ef88984afdd56b7f81f403fc6eae6993367ac9071b396b7
+f690f613193c638b9c6b10350d0170696db08055fc583cb04c3c66cb71d354a6
+6c02a9c33e312dd36d23c37836795088375e42241d176d2168dcea614a14850f
+d283c962ddd274d58fcbba7668fc6987cb0d204cc4db70ad44c53ab5da5e15fb
+55c5c53b85d0468b1798bffc105d55973d081db5cf801d98525597cfab8be326
+f6723c6c5060c6001ff9da8d01855933f68d88ea8c403e6dc8aa446e5eabd57a
+4d8c080ab4c164e3a97c1c6e87b3e279b97e99127fbbf1126099928ea8c0af7f
+3e592175164f46dd15ce60c6087d1893dd388b7a8eb65d0c38c56fa3fbbf88cf
+314e3ca2b710ff61f610ba945f7405c373c2b4147713206be618b238ba3e367b
+9d158dbbbe4dfc0aaede3f9457893910f3a83bf6baa3688fb1176c93132e24a9
+bdbecae4b53fd2cd971091dc99359d11af0aa3e94264538203459faf0cc36410
+def19e919a2714e2ce1895ceeb1f26ce613e5f1bee352ef3a63f94fc343fe7c3
+4f831ab892db8e438144821d5a701cb71d36526ac44a9354f3440f74e9e1fda7
+723f0120f75059513bd52cb2781a08b52142e53d0a4a23d7d003cae26b838552
+a1376832da4a1b8cc20cfa9f72fe3a9380a282bbb73a9ac07972677045589c0f
+e08cd002c80e0064a732a3dbb6dc83393d470f826fecdba4f1d2e4be76a5a433
+9a5e25b1f2b734d4f1469b84d977e3ccbe5951c768c9274d0423fe5d740ac8a4
+e3affe4fa68dd8cab058ebb21962d72dcfeaaa47e541a2ff3e6ecdacf0484ec3
+7151b6b1fa6109a0113e3bef3145b53d05c62e0391e266393ba1a4b39a806a12
+f2fe6562dff2fc97eff531ded8dc6e0ed359b2bf16d00031e9e0753bb32c156e
+ea81b7db813752506558d39f967450fa05390368950c456f106211dfc0621e52
+66c14e1ebc6a9f3de3165500218fd683fa0dd2b4fcb937f14f7d3fb85faf556e
+3f53ad74a4bcd96872b46716e0bd737dbf5d3115ce382ffb8243c499503283c4
+274a0e51c9d41b302fd57c648a4e1769dcab4668ee624f3b9c84f296ba9ca925
+056358d1e7f5a56ce13333e53b5676d5e9ccfa4cf68f18f28aee890b1bfad737
+03b64dcd7c5f34387f4522f421045adb7cb8b552d5145b83e24667d4f80cf636
+45f80b863f0293b1d494ed65532c3963b69fe1a858850799155ed2450d836d30
+d28dbfc50c7a58fd713346961d7f0e5513394b29c06c52b786b48a2f13b95b9d
+56d6aca8250f3b716f055275af003304d36713f14228b5ec40de7c3ba9ae32cd
+eb91f8110bafcf7c92ea383bd9ca4d4fd0f6ccb0902b07841a4cbee2cbe21c4a
+8124a11ef70c32d89e4b86d0e84294412f305ec186afd600ef7d72634e931f60
+a75a22b03abbb87b4a163044b96d43cb1a8eac9484df8955edd18bb298612f6d
+a9ab0fe3153be8def9616e883e5ca081c79f607d7439ce5a28ceb6b17f51f0f0
+9affc2a8a766dcbc19e1dafdb34bab555934bc8b3bf2255e4aab837de34079d7
+3b310d8d82d2abe249a28e1fc6f65e529c301933849883e86fd7b7b44271da63
+15913982ad40ffb9c796f3b519f84d15ba2fb42fdd632e042b7d9f94b28f5b81
+5139956c9d4e3521d59777d3c34a07e4ab2d92ce1e64c02c612023bd84153d02
+ac1b86b34937b82522277d5c11a7a3daa95e63e4d122e626d9167a5a17b4d041
+8d5aaf88b8a59de837362e48c9fd7123c3c30d906cc081ff9a0a1ce3b168b9e1
+ce50fb45fcaa3061d7dd0fce1c7051ea3c032fa52fa63442629461bbf742df11
+e6a548e7f73cd0441032ffb059fb81b468380c5eafb679d5ae2f384fca2236da
+5d5d878a6e29e282d045a419488daa5d3500bab9ddbb6123112949548b37cd85
+46e466baf1c18d05475029cf8b06206338df4d287541137ee8d509ae9e3716df
+32627c0e28eb8edcc62bcc37536bf8a36b8a545d539f2754fc39eb8da84fef99
+02a3733603379181d07b780b1f0dab6b69ab3268adeba5cac438510e80b96eb0
+79ccdf7e76b2f88a37beb1b83e4b3529ab0b6b1a35d5b7add114578ce3783582
+fc23f6c5df43330e9397c71e984817b908dddb6a02e6da93c87f623b3dcacbb7
+5c7b032d24a5e516bfe910b1569ef48975b8b7448f2469a41b4ab69f419f9f39
+2d3ca7b9bc768151274aaf55b06c3e399cb19b6e0f15ec8868c6e668e524aed3
+ec176ffb6a8accdc068a22eff5d79498a00a99562e490c7cff7e59e7d3b3b1f5
+8e48e1ef5e08391391c7e9ccebcce6376480909156717c07de203306e8c7c7db
+0b8737da43a6a8e9218133af777bcc505e5f5375a3fe8411cb5a317619e9d259
+07cbec0088f2610250754acf52e22dfd59a160aace94a7392f2f3a578980b1bd
+6d1dedb930b6948583f7d978fbde76276822ffa4014cfae2ac22913621b0f778
+d50edfe8517092759af5bdd038316d1b08f2b31550d1fe1fab8b62e79419010d
+6a59355a2e73cb08443700ceab7ee437401683f20cc5e9a21f056866f2b67dfb
+4e1e8df8ddcf2c5823e7fef6f91ebd1fa571ce77ba080c0f604a263379ec4e2f
+f5694975fa6adb5a646ad327db787c204f9b5ff9f9228a3337c111d32cbd6f8b
+1da885425af61709e40d4352a1be6d8e2775372a3f7717e9aa9b951f79d2bfb8
+399d1f915e26836d454d286655657b72378551cf133bfcaaaa2a45e98f840bca
+522311100c7c92f46661aa1a75fb44cb28a1a7f0cbeebc2008c5fe882108e2ee
+1e0c683320c084d1e5ff7c0657de7942b0daad28494b24649b271d82c10b7373
+f02be60741104e31bc70a2fc2ceb7db132f272caaf093993de5cc3d139d559b8
+ddfd287fb1705ca461eb03a4e039428ad0ea3baf3b29f824fc8c8edfffed4803
+e5d0af7bef8aac625a184911bb28809661eb2631a078fb9a244f043adb379476
+d0dcdabb21be4f1cee1b21d775a53dc6a1bfc4a8
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+cleartomark
diff --git a/e2e-tests/cypress/fonts/Type1/c0419bt_.afm b/e2e-tests/cypress/fonts/Type1/c0419bt_.afm
new file mode 100644
index 00000000..daca3412
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0419bt_.afm
@@ -0,0 +1,264 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Fixed Pitch 810
+Comment bitsFontID 0419
+Comment bitsManufacturingDate Mon Nov 5 16:16:55 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530419
+FontName Courier10PitchBT-Roman
+FullName Courier 10 Pitch
+FamilyName Courier 10 Pitch
+Weight Normal
+ItalicAngle 0.00
+IsFixedPitch true
+FontBBox -44 -299 664 858
+UnderlinePosition -97
+UnderlineThickness 82
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 579
+XHeight 452
+Ascender 639
+Descender -195
+StartCharMetrics 228
+C 32 ; WX 602 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 602 ; N exclam ; B 210 -11 392 613 ;
+C 34 ; WX 602 ; N quotedbl ; B 154 337 448 579 ;
+C 35 ; WX 602 ; N numbersign ; B 105 -58 497 674 ;
+C 36 ; WX 602 ; N dollar ; B 98 -122 498 666 ;
+C 37 ; WX 602 ; N percent ; B 101 -2 501 618 ;
+C 38 ; WX 602 ; N ampersand ; B 88 -11 501 547 ;
+C 39 ; WX 602 ; N quoteright ; B 215 317 396 579 ;
+C 40 ; WX 602 ; N parenleft ; B 201 -196 401 597 ;
+C 41 ; WX 602 ; N parenright ; B 201 -196 400 597 ;
+C 42 ; WX 602 ; N asterisk ; B 103 201 499 579 ;
+C 43 ; WX 602 ; N plus ; B 48 49 553 566 ;
+C 44 ; WX 602 ; N comma ; B 178 -157 399 163 ;
+C 45 ; WX 602 ; N hyphen ; B 76 178 526 273 ;
+C 46 ; WX 602 ; N period ; B 204 -7 397 167 ;
+C 47 ; WX 602 ; N slash ; B 114 -55 503 699 ;
+C 48 ; WX 602 ; N zero ; B 101 -17 501 632 ;
+C 49 ; WX 602 ; N one ; B 126 0 499 630 ;
+C 50 ; WX 602 ; N two ; B 80 0 487 633 ;
+C 51 ; WX 602 ; N three ; B 90 -16 497 633 ;
+C 52 ; WX 602 ; N four ; B 88 0 493 631 ;
+C 53 ; WX 602 ; N five ; B 84 -16 503 616 ;
+C 54 ; WX 602 ; N six ; B 100 -17 499 633 ;
+C 55 ; WX 602 ; N seven ; B 101 -10 491 616 ;
+C 56 ; WX 602 ; N eight ; B 102 -17 500 633 ;
+C 57 ; WX 602 ; N nine ; B 98 -17 497 633 ;
+C 58 ; WX 602 ; N colon ; B 204 -7 397 438 ;
+C 59 ; WX 602 ; N semicolon ; B 178 -157 395 438 ;
+C 60 ; WX 602 ; N less ; B 34 68 543 547 ;
+C 61 ; WX 602 ; N equal ; B 51 189 551 426 ;
+C 62 ; WX 602 ; N greater ; B 59 68 569 547 ;
+C 63 ; WX 602 ; N question ; B 122 -8 488 613 ;
+C 64 ; WX 602 ; N at ; B 109 -53 476 668 ;
+C 65 ; WX 602 ; N A ; B 0 0 607 579 ;
+C 66 ; WX 602 ; N B ; B 42 0 558 579 ;
+C 67 ; WX 602 ; N C ; B 39 -14 541 595 ;
+C 68 ; WX 602 ; N D ; B 49 -2 558 579 ;
+C 69 ; WX 602 ; N E ; B 33 0 530 579 ;
+C 70 ; WX 602 ; N F ; B 45 0 547 579 ;
+C 71 ; WX 602 ; N G ; B 34 -16 574 596 ;
+C 72 ; WX 602 ; N H ; B 49 0 553 579 ;
+C 73 ; WX 602 ; N I ; B 94 0 508 579 ;
+C 74 ; WX 602 ; N J ; B 54 -14 574 579 ;
+C 75 ; WX 602 ; N K ; B 35 0 573 579 ;
+C 76 ; WX 602 ; N L ; B 28 0 555 579 ;
+C 77 ; WX 602 ; N M ; B 7 0 595 579 ;
+C 78 ; WX 602 ; N N ; B 16 -8 569 579 ;
+C 79 ; WX 602 ; N O ; B 36 -16 566 595 ;
+C 80 ; WX 602 ; N P ; B 43 0 547 579 ;
+C 81 ; WX 602 ; N Q ; B 36 -128 566 595 ;
+C 82 ; WX 602 ; N R ; B 29 0 588 579 ;
+C 83 ; WX 602 ; N S ; B 70 -23 527 596 ;
+C 84 ; WX 602 ; N T ; B 45 0 557 579 ;
+C 85 ; WX 602 ; N U ; B 31 -16 570 579 ;
+C 86 ; WX 602 ; N V ; B 8 -7 594 579 ;
+C 87 ; WX 602 ; N W ; B 2 0 597 579 ;
+C 88 ; WX 602 ; N X ; B 42 0 560 579 ;
+C 89 ; WX 602 ; N Y ; B 38 0 562 579 ;
+C 90 ; WX 602 ; N Z ; B 89 0 492 579 ;
+C 91 ; WX 602 ; N bracketleft ; B 202 -181 400 579 ;
+C 92 ; WX 602 ; N backslash ; B 114 -55 503 698 ;
+C 93 ; WX 602 ; N bracketright ; B 202 -181 400 579 ;
+C 94 ; WX 602 ; N asciicircum ; B 126 448 476 676 ;
+C 95 ; WX 602 ; N underscore ; B -22 -299 626 -217 ;
+C 96 ; WX 602 ; N quoteleft ; B 210 334 391 596 ;
+C 97 ; WX 602 ; N a ; B 66 -12 568 464 ;
+C 98 ; WX 602 ; N b ; B 33 -10 566 639 ;
+C 99 ; WX 602 ; N c ; B 48 -12 523 463 ;
+C 100 ; WX 602 ; N d ; B 44 -12 577 639 ;
+C 101 ; WX 602 ; N e ; B 51 -12 546 465 ;
+C 102 ; WX 602 ; N f ; B 93 0 530 640 ;
+C 103 ; WX 602 ; N g ; B 66 -196 567 452 ;
+C 104 ; WX 602 ; N h ; B 49 0 561 639 ;
+C 105 ; WX 602 ; N i ; B 82 0 540 672 ;
+C 106 ; WX 602 ; N j ; B 115 -193 452 672 ;
+C 107 ; WX 602 ; N k ; B 51 0 575 639 ;
+C 108 ; WX 602 ; N l ; B 91 0 531 639 ;
+C 109 ; WX 602 ; N m ; B 0 0 614 464 ;
+C 110 ; WX 602 ; N n ; B 51 0 563 464 ;
+C 111 ; WX 602 ; N o ; B 45 -12 557 463 ;
+C 112 ; WX 602 ; N p ; B 33 -195 566 452 ;
+C 113 ; WX 602 ; N q ; B 39 -195 572 452 ;
+C 114 ; WX 602 ; N r ; B 53 0 556 464 ;
+C 115 ; WX 602 ; N s ; B 87 -12 512 464 ;
+C 116 ; WX 602 ; N t ; B 43 -10 550 591 ;
+C 117 ; WX 602 ; N u ; B 35 -10 558 452 ;
+C 118 ; WX 602 ; N v ; B 21 -18 580 452 ;
+C 119 ; WX 602 ; N w ; B -10 -18 603 452 ;
+C 120 ; WX 602 ; N x ; B 40 0 568 452 ;
+C 121 ; WX 602 ; N y ; B 28 -195 570 452 ;
+C 122 ; WX 602 ; N z ; B 98 0 508 452 ;
+C 123 ; WX 602 ; N braceleft ; B 143 -183 468 581 ;
+C 124 ; WX 602 ; N bar ; B 259 -261 342 789 ;
+C 125 ; WX 602 ; N braceright ; B 142 -183 467 581 ;
+C 126 ; WX 602 ; N asciitilde ; B 64 249 538 366 ;
+C 161 ; WX 602 ; N exclamdown ; B 210 -29 392 595 ;
+C 162 ; WX 602 ; N cent ; B 93 -33 493 646 ;
+C 163 ; WX 602 ; N sterling ; B 127 -4 501 625 ;
+C 164 ; WX 602 ; N fraction ; B 114 -55 503 699 ;
+C 165 ; WX 602 ; N yen ; B 38 0 562 579 ;
+C 166 ; WX 602 ; N florin ; B 17 -139 541 632 ;
+C 167 ; WX 602 ; N section ; B 75 -105 526 579 ;
+C 168 ; WX 602 ; N currency ; B 60 188 548 675 ;
+C 169 ; WX 602 ; N quotesingle ; B 243 330 358 579 ;
+C 170 ; WX 602 ; N quotedblleft ; B 107 341 475 596 ;
+C 171 ; WX 602 ; N guillemotleft ; B 149 44 453 407 ;
+C 172 ; WX 602 ; N guilsinglleft ; B 232 44 370 407 ;
+C 173 ; WX 602 ; N guilsinglright ; B 231 44 370 407 ;
+C 174 ; WX 602 ; N fi ; B -1 0 602 672 ;
+C 175 ; WX 602 ; N fl ; B -1 0 602 642 ;
+C 177 ; WX 602 ; N endash ; B -22 184 626 266 ;
+C 178 ; WX 602 ; N dagger ; B 121 -15 481 595 ;
+C 179 ; WX 602 ; N daggerdbl ; B 121 -15 481 595 ;
+C 180 ; WX 602 ; N periodcentered ; B 207 214 395 402 ;
+C 182 ; WX 602 ; N paragraph ; B 57 -77 551 616 ;
+C 183 ; WX 602 ; N bullet ; B 222 256 379 413 ;
+C 184 ; WX 602 ; N quotesinglbase ; B 199 -147 380 116 ;
+C 185 ; WX 602 ; N quotedblbase ; B 114 -139 481 116 ;
+C 186 ; WX 602 ; N quotedblright ; B 122 325 489 579 ;
+C 187 ; WX 602 ; N guillemotright ; B 148 44 453 407 ;
+C 188 ; WX 602 ; N ellipsis ; B 31 -15 570 122 ;
+C 189 ; WX 602 ; N perthousand ; B -44 -2 664 618 ;
+C 191 ; WX 602 ; N questiondown ; B 118 -25 485 595 ;
+C 193 ; WX 602 ; N grave ; B 124 504 407 670 ;
+C 194 ; WX 602 ; N acute ; B 195 504 478 670 ;
+C 195 ; WX 602 ; N circumflex ; B 132 509 470 657 ;
+C 196 ; WX 602 ; N tilde ; B 123 537 478 636 ;
+C 197 ; WX 602 ; N macron ; B 130 551 472 611 ;
+C 198 ; WX 602 ; N breve ; B 113 528 488 661 ;
+C 199 ; WX 602 ; N dotaccent ; B 239 527 363 652 ;
+C 200 ; WX 602 ; N dieresis ; B 117 522 484 641 ;
+C 202 ; WX 602 ; N ring ; B 177 499 425 747 ;
+C 203 ; WX 602 ; N cedilla ; B 178 -233 431 -9 ;
+C 205 ; WX 602 ; N hungarumlaut ; B 168 504 535 661 ;
+C 206 ; WX 602 ; N ogonek ; B 248 -224 435 15 ;
+C 207 ; WX 602 ; N caron ; B 132 511 469 659 ;
+C 208 ; WX 602 ; N emdash ; B -22 184 626 266 ;
+C 225 ; WX 602 ; N AE ; B 0 0 578 579 ;
+C 227 ; WX 602 ; N ordfeminine ; B 117 110 518 596 ;
+C 232 ; WX 602 ; N Lslash ; B 28 0 555 579 ;
+C 233 ; WX 602 ; N Oslash ; B 36 -55 566 631 ;
+C 234 ; WX 602 ; N OE ; B 34 0 578 579 ;
+C 235 ; WX 602 ; N ordmasculine ; B 95 110 508 596 ;
+C 241 ; WX 602 ; N ae ; B 18 -13 579 465 ;
+C 245 ; WX 602 ; N dotlessi ; B 82 0 540 452 ;
+C 248 ; WX 602 ; N lslash ; B 91 0 531 639 ;
+C 249 ; WX 602 ; N oslash ; B 47 -22 557 475 ;
+C 250 ; WX 602 ; N oe ; B 35 -13 579 466 ;
+C 251 ; WX 602 ; N germandbls ; B 21 -11 550 640 ;
+C -1 ; WX 602 ; N Aacute ; B 0 0 607 802 ;
+C -1 ; WX 602 ; N Acircumflex ; B 0 0 607 789 ;
+C -1 ; WX 602 ; N Adieresis ; B 0 0 607 773 ;
+C -1 ; WX 602 ; N Agrave ; B 0 0 607 802 ;
+C -1 ; WX 602 ; N Aring ; B 0 0 607 858 ;
+C -1 ; WX 602 ; N Atilde ; B 0 0 607 768 ;
+C -1 ; WX 602 ; N Ccedilla ; B 39 -233 541 595 ;
+C -1 ; WX 602 ; N Eacute ; B 33 0 530 802 ;
+C -1 ; WX 602 ; N Ecircumflex ; B 33 0 530 789 ;
+C -1 ; WX 602 ; N Edieresis ; B 33 0 530 773 ;
+C -1 ; WX 602 ; N Egrave ; B 33 0 530 802 ;
+C -1 ; WX 602 ; N Iacute ; B 94 0 508 802 ;
+C -1 ; WX 602 ; N Icircumflex ; B 94 0 508 789 ;
+C -1 ; WX 602 ; N Idieresis ; B 94 0 508 773 ;
+C -1 ; WX 602 ; N Igrave ; B 94 0 508 802 ;
+C -1 ; WX 602 ; N Ntilde ; B 16 -8 569 768 ;
+C -1 ; WX 602 ; N Oacute ; B 36 -16 566 802 ;
+C -1 ; WX 602 ; N Ocircumflex ; B 36 -16 566 789 ;
+C -1 ; WX 602 ; N Odieresis ; B 36 -16 566 773 ;
+C -1 ; WX 602 ; N Ograve ; B 36 -16 566 802 ;
+C -1 ; WX 602 ; N Otilde ; B 36 -16 566 768 ;
+C -1 ; WX 602 ; N Scaron ; B 70 -23 527 791 ;
+C -1 ; WX 602 ; N Uacute ; B 31 -16 570 802 ;
+C -1 ; WX 602 ; N Ucircumflex ; B 31 -16 570 789 ;
+C -1 ; WX 602 ; N Udieresis ; B 31 -16 570 773 ;
+C -1 ; WX 602 ; N Ugrave ; B 31 -16 570 802 ;
+C -1 ; WX 602 ; N Ydieresis ; B 38 0 562 773 ;
+C -1 ; WX 602 ; N Zcaron ; B 89 0 492 791 ;
+C -1 ; WX 602 ; N aacute ; B 66 -12 568 670 ;
+C -1 ; WX 602 ; N acircumflex ; B 66 -12 568 657 ;
+C -1 ; WX 602 ; N adieresis ; B 66 -12 568 641 ;
+C -1 ; WX 602 ; N agrave ; B 66 -12 568 670 ;
+C -1 ; WX 602 ; N aring ; B 66 -12 568 743 ;
+C -1 ; WX 602 ; N atilde ; B 66 -12 568 636 ;
+C -1 ; WX 602 ; N ccedilla ; B 48 -233 523 463 ;
+C -1 ; WX 602 ; N eacute ; B 51 -12 546 670 ;
+C -1 ; WX 602 ; N ecircumflex ; B 51 -12 546 657 ;
+C -1 ; WX 602 ; N edieresis ; B 51 -12 546 641 ;
+C -1 ; WX 602 ; N egrave ; B 51 -12 546 670 ;
+C -1 ; WX 602 ; N iacute ; B 82 0 540 670 ;
+C -1 ; WX 602 ; N icircumflex ; B 82 0 540 657 ;
+C -1 ; WX 602 ; N idieresis ; B 82 0 540 641 ;
+C -1 ; WX 602 ; N igrave ; B 82 0 540 670 ;
+C -1 ; WX 602 ; N ntilde ; B 51 0 563 636 ;
+C -1 ; WX 602 ; N oacute ; B 45 -12 557 670 ;
+C -1 ; WX 602 ; N ocircumflex ; B 45 -12 557 657 ;
+C -1 ; WX 602 ; N odieresis ; B 45 -12 557 641 ;
+C -1 ; WX 602 ; N ograve ; B 45 -12 557 670 ;
+C -1 ; WX 602 ; N otilde ; B 45 -12 557 636 ;
+C -1 ; WX 602 ; N scaron ; B 87 -12 512 659 ;
+C -1 ; WX 602 ; N uacute ; B 35 -10 558 670 ;
+C -1 ; WX 602 ; N ucircumflex ; B 35 -10 558 657 ;
+C -1 ; WX 602 ; N udieresis ; B 35 -10 558 641 ;
+C -1 ; WX 602 ; N ugrave ; B 35 -10 558 670 ;
+C -1 ; WX 602 ; N ydieresis ; B 28 -195 570 641 ;
+C -1 ; WX 602 ; N zcaron ; B 98 0 508 659 ;
+C -1 ; WX 602 ; N trademark ; B 56 337 547 616 ;
+C -1 ; WX 602 ; N copyright ; B 11 45 591 625 ;
+C -1 ; WX 602 ; N logicalnot ; B 48 170 553 445 ;
+C -1 ; WX 602 ; N registered ; B 11 45 591 625 ;
+C -1 ; WX 602 ; N minus ; B 76 262 526 354 ;
+C -1 ; WX 602 ; N Eth ; B 20 -2 558 579 ;
+C -1 ; WX 602 ; N Thorn ; B 55 0 518 579 ;
+C -1 ; WX 602 ; N Yacute ; B 38 0 562 802 ;
+C -1 ; WX 602 ; N brokenbar ; B 271 -172 331 699 ;
+C -1 ; WX 602 ; N degree ; B 143 349 459 665 ;
+C -1 ; WX 602 ; N divide ; B 51 99 551 516 ;
+C -1 ; WX 602 ; N eth ; B 45 -12 557 640 ;
+C -1 ; WX 602 ; N mu ; B 32 -201 534 452 ;
+C -1 ; WX 602 ; N multiply ; B 105 125 493 511 ;
+C -1 ; WX 602 ; N onehalf ; B 59 -89 539 680 ;
+C -1 ; WX 602 ; N onequarter ; B 59 -94 539 680 ;
+C -1 ; WX 602 ; N onesuperior ; B 164 240 462 633 ;
+C -1 ; WX 602 ; N plusminus ; B 105 44 497 619 ;
+C -1 ; WX 602 ; N thorn ; B 33 -195 566 639 ;
+C -1 ; WX 602 ; N threequarters ; B 59 -94 539 680 ;
+C -1 ; WX 602 ; N threesuperior ; B 140 222 461 633 ;
+C -1 ; WX 602 ; N twosuperior ; B 145 240 451 632 ;
+C -1 ; WX 602 ; N yacute ; B 28 -195 570 670 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 0
+EndKernPairs
+StartTrackKern 0
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0419bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0419bt_.pfb
new file mode 100644
index 00000000..4a49dd59
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0419bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0582bt_.afm b/e2e-tests/cypress/fonts/Type1/c0582bt_.afm
new file mode 100644
index 00000000..f9e7d56b
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0582bt_.afm
@@ -0,0 +1,264 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Fixed Pitch 810
+Comment bitsFontID 0582
+Comment bitsManufacturingDate Mon Nov 5 23:53:48 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530582
+FontName Courier10PitchBT-Italic
+FullName Courier 10 Pitch Italic
+FamilyName Courier 10 Pitch
+Weight Normal
+ItalicAngle 12.0000
+IsFixedPitch true
+FontBBox -92 -299 664 858
+UnderlinePosition -97
+UnderlineThickness 82
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 579
+XHeight 452
+Ascender 639
+Descender -195
+StartCharMetrics 228
+C 32 ; WX 602 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 602 ; N exclam ; B 183 -16 418 600 ;
+C 34 ; WX 602 ; N quotedbl ; B 154 337 448 579 ;
+C 35 ; WX 602 ; N numbersign ; B 105 -58 497 674 ;
+C 36 ; WX 602 ; N dollar ; B 57 -123 525 665 ;
+C 37 ; WX 602 ; N percent ; B 101 -2 501 618 ;
+C 38 ; WX 602 ; N ampersand ; B 66 -10 492 552 ;
+C 39 ; WX 602 ; N quoteright ; B 227 321 445 579 ;
+C 40 ; WX 602 ; N parenleft ; B 207 -194 498 597 ;
+C 41 ; WX 602 ; N parenright ; B 107 -194 398 597 ;
+C 42 ; WX 602 ; N asterisk ; B 103 201 499 579 ;
+C 43 ; WX 602 ; N plus ; B 48 49 553 566 ;
+C 44 ; WX 602 ; N comma ; B 141 -160 400 147 ;
+C 45 ; WX 602 ; N hyphen ; B 93 178 541 273 ;
+C 46 ; WX 602 ; N period ; B 204 -16 397 159 ;
+C 47 ; WX 602 ; N slash ; B 114 -55 503 699 ;
+C 48 ; WX 602 ; N zero ; B 61 -16 538 634 ;
+C 49 ; WX 602 ; N one ; B 76 0 440 630 ;
+C 50 ; WX 602 ; N two ; B 25 0 504 631 ;
+C 51 ; WX 602 ; N three ; B 37 -16 498 633 ;
+C 52 ; WX 602 ; N four ; B 68 0 479 634 ;
+C 53 ; WX 602 ; N five ; B 31 -16 515 616 ;
+C 54 ; WX 602 ; N six ; B 90 -15 554 632 ;
+C 55 ; WX 602 ; N seven ; B 136 -8 533 616 ;
+C 56 ; WX 602 ; N eight ; B 80 -17 526 634 ;
+C 57 ; WX 602 ; N nine ; B 40 -14 504 632 ;
+C 58 ; WX 602 ; N colon ; B 173 -18 428 438 ;
+C 59 ; WX 602 ; N semicolon ; B 96 -160 434 438 ;
+C 60 ; WX 602 ; N less ; B 34 68 543 547 ;
+C 61 ; WX 602 ; N equal ; B 51 189 551 426 ;
+C 62 ; WX 602 ; N greater ; B 59 68 569 547 ;
+C 63 ; WX 602 ; N question ; B 134 -16 494 597 ;
+C 64 ; WX 602 ; N at ; B 131 -51 534 668 ;
+C 65 ; WX 602 ; N A ; B -52 0 555 579 ;
+C 66 ; WX 602 ; N B ; B -11 0 544 579 ;
+C 67 ; WX 602 ; N C ; B 37 -16 582 596 ;
+C 68 ; WX 602 ; N D ; B -11 0 577 579 ;
+C 69 ; WX 602 ; N E ; B -11 0 586 579 ;
+C 70 ; WX 602 ; N F ; B 12 0 610 579 ;
+C 71 ; WX 602 ; N G ; B 37 -16 582 596 ;
+C 72 ; WX 602 ; N H ; B -11 0 612 579 ;
+C 73 ; WX 602 ; N I ; B 45 0 556 579 ;
+C 74 ; WX 602 ; N J ; B 26 -16 637 579 ;
+C 75 ; WX 602 ; N K ; B -3 0 617 579 ;
+C 76 ; WX 602 ; N L ; B -8 0 551 579 ;
+C 77 ; WX 602 ; N M ; B -36 0 630 579 ;
+C 78 ; WX 602 ; N N ; B -1 -8 633 579 ;
+C 79 ; WX 602 ; N O ; B 21 -15 580 596 ;
+C 80 ; WX 602 ; N P ; B 5 0 578 579 ;
+C 81 ; WX 602 ; N Q ; B 21 -128 580 596 ;
+C 82 ; WX 602 ; N R ; B -9 0 545 579 ;
+C 83 ; WX 602 ; N S ; B 46 -21 566 598 ;
+C 84 ; WX 602 ; N T ; B 71 0 604 579 ;
+C 85 ; WX 602 ; N U ; B 82 -16 628 579 ;
+C 86 ; WX 607 ; N V ; B 66 -9 647 579 ;
+C 87 ; WX 602 ; N W ; B 60 -7 651 579 ;
+C 88 ; WX 602 ; N X ; B 2 0 609 579 ;
+C 89 ; WX 602 ; N Y ; B 86 0 621 579 ;
+C 90 ; WX 602 ; N Z ; B 57 0 558 579 ;
+C 91 ; WX 602 ; N bracketleft ; B 165 -183 512 579 ;
+C 92 ; WX 602 ; N backslash ; B 114 -55 503 698 ;
+C 93 ; WX 602 ; N bracketright ; B 82 -183 429 579 ;
+C 94 ; WX 602 ; N asciicircum ; B 126 448 476 676 ;
+C 95 ; WX 602 ; N underscore ; B -22 -299 626 -217 ;
+C 96 ; WX 602 ; N quoteleft ; B 218 339 436 597 ;
+C 97 ; WX 602 ; N a ; B 54 -10 524 463 ;
+C 98 ; WX 602 ; N b ; B -10 -13 562 639 ;
+C 99 ; WX 602 ; N c ; B 63 -12 573 463 ;
+C 100 ; WX 602 ; N d ; B 39 -11 591 639 ;
+C 101 ; WX 602 ; N e ; B 63 -12 552 463 ;
+C 102 ; WX 602 ; N f ; B 72 0 619 640 ;
+C 103 ; WX 602 ; N g ; B 62 -196 599 464 ;
+C 104 ; WX 602 ; N h ; B 17 0 522 639 ;
+C 105 ; WX 602 ; N i ; B 56 0 504 671 ;
+C 106 ; WX 602 ; N j ; B 3 -196 463 671 ;
+C 107 ; WX 602 ; N k ; B 28 0 575 639 ;
+C 108 ; WX 602 ; N l ; B 57 0 489 639 ;
+C 109 ; WX 602 ; N m ; B -34 0 574 464 ;
+C 110 ; WX 602 ; N n ; B 17 0 522 463 ;
+C 111 ; WX 602 ; N o ; B 48 -13 555 464 ;
+C 112 ; WX 602 ; N p ; B -55 -195 565 463 ;
+C 113 ; WX 602 ; N q ; B 34 -195 611 465 ;
+C 114 ; WX 602 ; N r ; B 41 0 599 463 ;
+C 115 ; WX 602 ; N s ; B 60 -12 536 464 ;
+C 116 ; WX 602 ; N t ; B 77 -10 505 591 ;
+C 117 ; WX 602 ; N u ; B 83 -12 534 452 ;
+C 118 ; WX 602 ; N v ; B 54 -12 601 452 ;
+C 119 ; WX 602 ; N w ; B 22 -14 636 452 ;
+C 120 ; WX 602 ; N x ; B 9 0 601 452 ;
+C 121 ; WX 602 ; N y ; B -40 -195 604 452 ;
+C 122 ; WX 602 ; N z ; B 63 0 544 452 ;
+C 123 ; WX 602 ; N braceleft ; B 143 -183 468 581 ;
+C 124 ; WX 602 ; N bar ; B 259 -261 342 789 ;
+C 125 ; WX 602 ; N braceright ; B 142 -183 467 581 ;
+C 126 ; WX 602 ; N asciitilde ; B 64 249 538 366 ;
+C 161 ; WX 602 ; N exclamdown ; B 192 -16 428 600 ;
+C 162 ; WX 602 ; N cent ; B 84 -17 512 630 ;
+C 163 ; WX 602 ; N sterling ; B 42 -17 519 636 ;
+C 164 ; WX 602 ; N fraction ; B 114 -55 503 699 ;
+C 165 ; WX 602 ; N yen ; B 43 0 609 616 ;
+C 166 ; WX 602 ; N florin ; B 17 -139 541 632 ;
+C 167 ; WX 602 ; N section ; B 50 -105 540 580 ;
+C 168 ; WX 602 ; N currency ; B 60 188 548 675 ;
+C 169 ; WX 602 ; N quotesingle ; B 243 330 358 579 ;
+C 170 ; WX 602 ; N quotedblleft ; B 114 339 553 597 ;
+C 171 ; WX 602 ; N guillemotleft ; B 141 47 460 409 ;
+C 172 ; WX 602 ; N guilsinglleft ; B 227 47 385 409 ;
+C 173 ; WX 602 ; N guilsinglright ; B 225 49 383 410 ;
+C 174 ; WX 602 ; N fi ; B -22 0 596 671 ;
+C 175 ; WX 602 ; N fl ; B -22 0 588 639 ;
+C 177 ; WX 602 ; N endash ; B -24 184 627 266 ;
+C 178 ; WX 602 ; N dagger ; B 151 -16 505 595 ;
+C 179 ; WX 602 ; N daggerdbl ; B 121 -16 516 596 ;
+C 180 ; WX 602 ; N periodcentered ; B 204 229 397 403 ;
+C 182 ; WX 602 ; N paragraph ; B 98 -77 615 616 ;
+C 183 ; WX 602 ; N bullet ; B 222 256 379 413 ;
+C 184 ; WX 602 ; N quotesinglbase ; B 164 -141 382 118 ;
+C 185 ; WX 602 ; N quotedblbase ; B 54 -140 494 119 ;
+C 186 ; WX 602 ; N quotedblright ; B 128 321 567 579 ;
+C 187 ; WX 602 ; N guillemotright ; B 130 49 448 410 ;
+C 188 ; WX 602 ; N ellipsis ; B 27 -12 574 133 ;
+C 189 ; WX 602 ; N perthousand ; B -44 -2 664 618 ;
+C 191 ; WX 602 ; N questiondown ; B 112 -17 473 596 ;
+C 193 ; WX 602 ; N grave ; B 193 515 466 660 ;
+C 194 ; WX 602 ; N acute ; B 241 515 514 660 ;
+C 195 ; WX 602 ; N circumflex ; B 186 507 486 658 ;
+C 196 ; WX 602 ; N tilde ; B 182 537 536 636 ;
+C 197 ; WX 602 ; N macron ; B 188 551 530 611 ;
+C 198 ; WX 602 ; N breve ; B 226 528 568 660 ;
+C 199 ; WX 602 ; N dotaccent ; B 297 527 422 652 ;
+C 200 ; WX 602 ; N dieresis ; B 175 522 543 641 ;
+C 202 ; WX 602 ; N ring ; B 242 499 490 747 ;
+C 203 ; WX 602 ; N cedilla ; B 146 -224 401 0 ;
+C 205 ; WX 602 ; N hungarumlaut ; B 211 515 580 659 ;
+C 206 ; WX 602 ; N ogonek ; B 176 -224 364 15 ;
+C 207 ; WX 602 ; N caron ; B 226 510 527 661 ;
+C 208 ; WX 602 ; N emdash ; B -25 184 627 266 ;
+C 225 ; WX 602 ; N AE ; B -92 0 609 579 ;
+C 227 ; WX 602 ; N ordfeminine ; B 108 219 484 597 ;
+C 232 ; WX 602 ; N Lslash ; B -8 0 551 579 ;
+C 233 ; WX 602 ; N Oslash ; B -25 -28 613 611 ;
+C 234 ; WX 602 ; N OE ; B 3 0 623 579 ;
+C 235 ; WX 602 ; N ordmasculine ; B 99 215 504 596 ;
+C 241 ; WX 602 ; N ae ; B -12 -12 598 464 ;
+C 245 ; WX 602 ; N dotlessi ; B 56 0 504 452 ;
+C 248 ; WX 602 ; N lslash ; B 57 0 520 639 ;
+C 249 ; WX 602 ; N oslash ; B 10 -24 579 476 ;
+C 250 ; WX 602 ; N oe ; B 7 -12 596 464 ;
+C 251 ; WX 602 ; N germandbls ; B -20 -13 544 639 ;
+C -1 ; WX 602 ; N Aacute ; B -52 0 555 792 ;
+C -1 ; WX 602 ; N Acircumflex ; B -52 0 555 790 ;
+C -1 ; WX 602 ; N Adieresis ; B -52 0 555 773 ;
+C -1 ; WX 602 ; N Agrave ; B -52 0 555 792 ;
+C -1 ; WX 602 ; N Aring ; B -52 0 555 858 ;
+C -1 ; WX 602 ; N Atilde ; B -52 0 555 768 ;
+C -1 ; WX 602 ; N Ccedilla ; B 37 -224 582 596 ;
+C -1 ; WX 602 ; N Eacute ; B -11 0 586 792 ;
+C -1 ; WX 602 ; N Ecircumflex ; B -11 0 586 790 ;
+C -1 ; WX 602 ; N Edieresis ; B -11 0 586 773 ;
+C -1 ; WX 602 ; N Egrave ; B -11 0 586 792 ;
+C -1 ; WX 602 ; N Iacute ; B 45 0 556 792 ;
+C -1 ; WX 602 ; N Icircumflex ; B 45 0 556 790 ;
+C -1 ; WX 602 ; N Idieresis ; B 45 0 565 773 ;
+C -1 ; WX 602 ; N Igrave ; B 45 0 556 792 ;
+C -1 ; WX 602 ; N Ntilde ; B -1 -8 633 768 ;
+C -1 ; WX 602 ; N Oacute ; B 21 -15 580 792 ;
+C -1 ; WX 602 ; N Ocircumflex ; B 21 -15 580 790 ;
+C -1 ; WX 602 ; N Odieresis ; B 21 -15 580 773 ;
+C -1 ; WX 602 ; N Ograve ; B 21 -15 580 792 ;
+C -1 ; WX 602 ; N Otilde ; B 21 -15 580 768 ;
+C -1 ; WX 602 ; N Scaron ; B 46 -21 566 793 ;
+C -1 ; WX 602 ; N Uacute ; B 82 -16 628 792 ;
+C -1 ; WX 602 ; N Ucircumflex ; B 82 -16 628 790 ;
+C -1 ; WX 602 ; N Udieresis ; B 82 -16 628 773 ;
+C -1 ; WX 602 ; N Ugrave ; B 82 -16 628 792 ;
+C -1 ; WX 602 ; N Ydieresis ; B 86 0 621 773 ;
+C -1 ; WX 602 ; N Zcaron ; B 57 0 558 793 ;
+C -1 ; WX 602 ; N aacute ; B 54 -10 524 660 ;
+C -1 ; WX 602 ; N acircumflex ; B 54 -10 524 658 ;
+C -1 ; WX 602 ; N adieresis ; B 54 -10 553 641 ;
+C -1 ; WX 602 ; N agrave ; B 54 -10 524 660 ;
+C -1 ; WX 602 ; N aring ; B 54 -10 524 743 ;
+C -1 ; WX 602 ; N atilde ; B 54 -10 546 636 ;
+C -1 ; WX 602 ; N ccedilla ; B 63 -224 573 463 ;
+C -1 ; WX 602 ; N eacute ; B 63 -12 552 660 ;
+C -1 ; WX 602 ; N ecircumflex ; B 63 -12 552 658 ;
+C -1 ; WX 602 ; N edieresis ; B 63 -12 552 641 ;
+C -1 ; WX 602 ; N egrave ; B 63 -12 552 660 ;
+C -1 ; WX 602 ; N iacute ; B 56 0 514 660 ;
+C -1 ; WX 602 ; N icircumflex ; B 56 0 504 658 ;
+C -1 ; WX 602 ; N idieresis ; B 56 0 543 641 ;
+C -1 ; WX 602 ; N igrave ; B 56 0 504 660 ;
+C -1 ; WX 602 ; N ntilde ; B 17 0 545 636 ;
+C -1 ; WX 602 ; N oacute ; B 48 -13 555 660 ;
+C -1 ; WX 602 ; N ocircumflex ; B 48 -13 555 658 ;
+C -1 ; WX 602 ; N odieresis ; B 48 -13 555 641 ;
+C -1 ; WX 602 ; N ograve ; B 48 -13 555 660 ;
+C -1 ; WX 602 ; N otilde ; B 48 -13 555 636 ;
+C -1 ; WX 602 ; N scaron ; B 60 -12 536 661 ;
+C -1 ; WX 602 ; N uacute ; B 83 -12 534 660 ;
+C -1 ; WX 602 ; N ucircumflex ; B 83 -12 534 658 ;
+C -1 ; WX 602 ; N udieresis ; B 83 -12 534 641 ;
+C -1 ; WX 602 ; N ugrave ; B 83 -12 534 660 ;
+C -1 ; WX 602 ; N ydieresis ; B -40 -195 604 641 ;
+C -1 ; WX 602 ; N zcaron ; B 63 0 544 661 ;
+C -1 ; WX 602 ; N trademark ; B 56 337 547 616 ;
+C -1 ; WX 602 ; N copyright ; B 11 45 591 625 ;
+C -1 ; WX 602 ; N logicalnot ; B 48 170 553 445 ;
+C -1 ; WX 602 ; N registered ; B 11 45 591 625 ;
+C -1 ; WX 602 ; N minus ; B 76 262 526 354 ;
+C -1 ; WX 602 ; N Eth ; B -11 0 577 579 ;
+C -1 ; WX 602 ; N Thorn ; B 5 0 518 579 ;
+C -1 ; WX 602 ; N Yacute ; B 86 0 621 792 ;
+C -1 ; WX 602 ; N brokenbar ; B 271 -172 331 699 ;
+C -1 ; WX 602 ; N degree ; B 143 349 459 665 ;
+C -1 ; WX 602 ; N divide ; B 51 99 551 516 ;
+C -1 ; WX 602 ; N eth ; B 47 -13 554 636 ;
+C -1 ; WX 602 ; N mu ; B 32 -201 534 452 ;
+C -1 ; WX 602 ; N multiply ; B 105 125 493 511 ;
+C -1 ; WX 602 ; N onehalf ; B 59 -89 539 680 ;
+C -1 ; WX 602 ; N onequarter ; B 59 -94 539 680 ;
+C -1 ; WX 602 ; N onesuperior ; B 164 240 462 633 ;
+C -1 ; WX 602 ; N plusminus ; B 105 44 497 619 ;
+C -1 ; WX 602 ; N thorn ; B -55 -195 565 639 ;
+C -1 ; WX 602 ; N threequarters ; B 59 -94 539 680 ;
+C -1 ; WX 602 ; N threesuperior ; B 140 222 461 633 ;
+C -1 ; WX 602 ; N twosuperior ; B 145 240 451 632 ;
+C -1 ; WX 602 ; N yacute ; B -40 -195 604 660 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 0
+EndKernPairs
+StartTrackKern 0
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0582bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0582bt_.pfb
new file mode 100644
index 00000000..9baa1da2
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0582bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0583bt_.afm b/e2e-tests/cypress/fonts/Type1/c0583bt_.afm
new file mode 100644
index 00000000..032ef458
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0583bt_.afm
@@ -0,0 +1,264 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Fixed Pitch 810
+Comment bitsFontID 0583
+Comment bitsManufacturingDate Mon Nov 5 23:57:48 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530583
+FontName Courier10PitchBT-Bold
+FullName Courier 10 Pitch Bold
+FamilyName Courier 10 Pitch
+Weight Bold
+ItalicAngle 0.00
+IsFixedPitch true
+FontBBox -44 -310 665 875
+UnderlinePosition -97
+UnderlineThickness 93
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 579
+XHeight 452
+Ascender 639
+Descender -195
+StartCharMetrics 228
+C 32 ; WX 602 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 602 ; N exclam ; B 205 -11 398 613 ;
+C 34 ; WX 602 ; N quotedbl ; B 154 337 448 579 ;
+C 35 ; WX 602 ; N numbersign ; B 99 -58 502 684 ;
+C 36 ; WX 602 ; N dollar ; B 84 -122 510 672 ;
+C 37 ; WX 602 ; N percent ; B 100 -16 501 633 ;
+C 38 ; WX 602 ; N ampersand ; B 57 -16 525 562 ;
+C 39 ; WX 602 ; N quoteright ; B 206 311 403 579 ;
+C 40 ; WX 602 ; N parenleft ; B 196 -197 452 597 ;
+C 41 ; WX 602 ; N parenright ; B 151 -197 407 597 ;
+C 42 ; WX 602 ; N asterisk ; B 102 197 500 579 ;
+C 43 ; WX 602 ; N plus ; B 35 42 566 573 ;
+C 44 ; WX 602 ; N comma ; B 170 -159 410 169 ;
+C 45 ; WX 602 ; N hyphen ; B 76 170 526 282 ;
+C 46 ; WX 602 ; N period ; B 199 -7 404 177 ;
+C 47 ; WX 602 ; N slash ; B 103 -58 513 703 ;
+C 48 ; WX 602 ; N zero ; B 57 -17 545 632 ;
+C 49 ; WX 602 ; N one ; B 103 0 516 633 ;
+C 50 ; WX 602 ; N two ; B 62 0 506 633 ;
+C 51 ; WX 602 ; N three ; B 73 -17 502 633 ;
+C 52 ; WX 602 ; N four ; B 73 0 527 631 ;
+C 53 ; WX 602 ; N five ; B 69 -17 515 616 ;
+C 54 ; WX 602 ; N six ; B 78 -16 521 633 ;
+C 55 ; WX 602 ; N seven ; B 61 -9 476 616 ;
+C 56 ; WX 602 ; N eight ; B 74 -17 528 633 ;
+C 57 ; WX 602 ; N nine ; B 76 -16 519 632 ;
+C 58 ; WX 602 ; N colon ; B 198 -7 403 438 ;
+C 59 ; WX 602 ; N semicolon ; B 170 -159 410 438 ;
+C 60 ; WX 602 ; N less ; B 43 62 558 553 ;
+C 61 ; WX 602 ; N equal ; B 36 160 566 456 ;
+C 62 ; WX 602 ; N greater ; B 43 62 558 553 ;
+C 63 ; WX 602 ; N question ; B 115 -8 495 613 ;
+C 64 ; WX 602 ; N at ; B 66 -50 524 668 ;
+C 65 ; WX 602 ; N A ; B -6 0 609 579 ;
+C 66 ; WX 602 ; N B ; B 34 0 569 579 ;
+C 67 ; WX 602 ; N C ; B 30 -16 564 596 ;
+C 68 ; WX 602 ; N D ; B 33 0 569 579 ;
+C 69 ; WX 602 ; N E ; B 35 0 547 579 ;
+C 70 ; WX 602 ; N F ; B 39 0 551 579 ;
+C 71 ; WX 602 ; N G ; B 25 -16 602 596 ;
+C 72 ; WX 602 ; N H ; B 33 0 569 579 ;
+C 73 ; WX 602 ; N I ; B 94 0 508 579 ;
+C 74 ; WX 602 ; N J ; B 53 -16 580 579 ;
+C 75 ; WX 602 ; N K ; B 33 0 582 579 ;
+C 76 ; WX 602 ; N L ; B 21 0 575 579 ;
+C 77 ; WX 602 ; N M ; B -12 0 613 579 ;
+C 78 ; WX 602 ; N N ; B 25 -7 587 579 ;
+C 79 ; WX 602 ; N O ; B 25 -16 577 595 ;
+C 80 ; WX 602 ; N P ; B 55 0 560 579 ;
+C 81 ; WX 602 ; N Q ; B 25 -157 577 595 ;
+C 82 ; WX 602 ; N R ; B 40 0 603 579 ;
+C 83 ; WX 602 ; N S ; B 56 -16 545 596 ;
+C 84 ; WX 602 ; N T ; B 30 0 572 580 ;
+C 85 ; WX 602 ; N U ; B 12 -16 590 579 ;
+C 86 ; WX 602 ; N V ; B 0 0 603 579 ;
+C 87 ; WX 602 ; N W ; B -12 0 613 579 ;
+C 88 ; WX 602 ; N X ; B 35 0 573 579 ;
+C 89 ; WX 602 ; N Y ; B 38 0 556 579 ;
+C 90 ; WX 602 ; N Z ; B 69 0 512 579 ;
+C 91 ; WX 602 ; N bracketleft ; B 205 -181 428 579 ;
+C 92 ; WX 602 ; N backslash ; B 103 -58 512 703 ;
+C 93 ; WX 602 ; N bracketright ; B 147 -181 370 579 ;
+C 94 ; WX 602 ; N asciicircum ; B 104 422 498 671 ;
+C 95 ; WX 602 ; N underscore ; B -22 -310 626 -207 ;
+C 96 ; WX 602 ; N quoteleft ; B 202 327 398 596 ;
+C 97 ; WX 602 ; N a ; B 51 -12 562 464 ;
+C 98 ; WX 602 ; N b ; B 30 -12 567 639 ;
+C 99 ; WX 602 ; N c ; B 43 -12 546 464 ;
+C 100 ; WX 602 ; N d ; B 34 -12 571 639 ;
+C 101 ; WX 602 ; N e ; B 43 -12 555 463 ;
+C 102 ; WX 602 ; N f ; B 97 0 549 641 ;
+C 103 ; WX 602 ; N g ; B 34 -196 571 463 ;
+C 104 ; WX 602 ; N h ; B 39 0 568 639 ;
+C 105 ; WX 602 ; N i ; B 82 0 536 672 ;
+C 106 ; WX 602 ; N j ; B 63 -196 426 672 ;
+C 107 ; WX 602 ; N k ; B 46 0 569 639 ;
+C 108 ; WX 602 ; N l ; B 73 0 528 639 ;
+C 109 ; WX 602 ; N m ; B -7 0 616 464 ;
+C 110 ; WX 602 ; N n ; B 40 0 568 463 ;
+C 111 ; WX 602 ; N o ; B 34 -12 568 463 ;
+C 112 ; WX 602 ; N p ; B 1 -195 567 463 ;
+C 113 ; WX 602 ; N q ; B 34 -195 600 463 ;
+C 114 ; WX 602 ; N r ; B 59 0 573 464 ;
+C 115 ; WX 602 ; N s ; B 78 -14 527 467 ;
+C 116 ; WX 602 ; N t ; B 39 -10 548 600 ;
+C 117 ; WX 602 ; N u ; B 33 -12 560 452 ;
+C 118 ; WX 602 ; N v ; B 28 -12 572 452 ;
+C 119 ; WX 602 ; N w ; B -27 -12 628 452 ;
+C 120 ; WX 602 ; N x ; B 53 0 553 452 ;
+C 121 ; WX 602 ; N y ; B 28 -195 572 452 ;
+C 122 ; WX 602 ; N z ; B 82 0 501 452 ;
+C 123 ; WX 602 ; N braceleft ; B 143 -185 473 582 ;
+C 124 ; WX 602 ; N bar ; B 243 -261 359 789 ;
+C 125 ; WX 602 ; N braceright ; B 142 -185 473 582 ;
+C 126 ; WX 602 ; N asciitilde ; B 50 236 551 380 ;
+C 161 ; WX 602 ; N exclamdown ; B 205 -11 398 613 ;
+C 162 ; WX 602 ; N cent ; B 70 -33 517 646 ;
+C 163 ; WX 602 ; N sterling ; B 80 -15 512 633 ;
+C 164 ; WX 602 ; N fraction ; B 103 -58 513 703 ;
+C 165 ; WX 602 ; N yen ; B 42 0 559 616 ;
+C 166 ; WX 602 ; N florin ; B 39 -139 562 632 ;
+C 167 ; WX 602 ; N section ; B 63 -105 540 579 ;
+C 168 ; WX 602 ; N currency ; B 58 189 544 676 ;
+C 169 ; WX 602 ; N quotesingle ; B 245 337 357 579 ;
+C 170 ; WX 602 ; N quotedblleft ; B 98 327 486 596 ;
+C 171 ; WX 602 ; N guillemotleft ; B 142 36 459 411 ;
+C 172 ; WX 602 ; N guilsinglleft ; B 223 36 380 411 ;
+C 173 ; WX 602 ; N guilsinglright ; B 222 36 378 411 ;
+C 174 ; WX 602 ; N fi ; B 21 0 578 672 ;
+C 175 ; WX 602 ; N fl ; B 21 0 578 639 ;
+C 177 ; WX 602 ; N endash ; B -22 179 626 272 ;
+C 178 ; WX 602 ; N dagger ; B 121 -15 481 596 ;
+C 179 ; WX 602 ; N daggerdbl ; B 121 -15 484 596 ;
+C 180 ; WX 602 ; N periodcentered ; B 201 208 400 407 ;
+C 182 ; WX 602 ; N paragraph ; B 45 -77 566 616 ;
+C 183 ; WX 602 ; N bullet ; B 222 256 379 413 ;
+C 184 ; WX 602 ; N quotesinglbase ; B 196 -159 393 110 ;
+C 185 ; WX 602 ; N quotedblbase ; B 103 -159 491 110 ;
+C 186 ; WX 602 ; N quotedblright ; B 110 311 499 579 ;
+C 187 ; WX 602 ; N guillemotright ; B 141 36 457 411 ;
+C 188 ; WX 602 ; N ellipsis ; B 27 -12 574 133 ;
+C 189 ; WX 602 ; N perthousand ; B -44 -16 665 633 ;
+C 191 ; WX 602 ; N questiondown ; B 113 -8 494 613 ;
+C 193 ; WX 602 ; N grave ; B 121 493 407 670 ;
+C 194 ; WX 602 ; N acute ; B 195 493 481 670 ;
+C 195 ; WX 602 ; N circumflex ; B 130 508 472 662 ;
+C 196 ; WX 602 ; N tilde ; B 122 533 479 640 ;
+C 197 ; WX 602 ; N macron ; B 130 547 472 615 ;
+C 198 ; WX 602 ; N breve ; B 114 528 488 661 ;
+C 199 ; WX 602 ; N dotaccent ; B 234 523 367 656 ;
+C 200 ; WX 602 ; N dieresis ; B 117 519 485 644 ;
+C 202 ; WX 602 ; N ring ; B 168 499 433 764 ;
+C 203 ; WX 602 ; N cedilla ; B 179 -233 437 0 ;
+C 205 ; WX 602 ; N hungarumlaut ; B 165 502 535 666 ;
+C 206 ; WX 602 ; N ogonek ; B 240 -229 440 15 ;
+C 207 ; WX 602 ; N caron ; B 130 510 472 663 ;
+C 208 ; WX 602 ; N emdash ; B -22 179 626 272 ;
+C 225 ; WX 602 ; N AE ; B -36 0 611 579 ;
+C 227 ; WX 602 ; N ordfeminine ; B 102 143 511 633 ;
+C 232 ; WX 602 ; N Lslash ; B 21 0 575 579 ;
+C 233 ; WX 602 ; N Oslash ; B 25 -67 577 636 ;
+C 234 ; WX 602 ; N OE ; B 6 0 606 579 ;
+C 235 ; WX 602 ; N ordmasculine ; B 87 143 514 632 ;
+C 241 ; WX 602 ; N ae ; B 3 -12 601 464 ;
+C 245 ; WX 602 ; N dotlessi ; B 82 0 536 452 ;
+C 248 ; WX 602 ; N lslash ; B 73 0 528 639 ;
+C 249 ; WX 602 ; N oslash ; B 34 -75 568 516 ;
+C 250 ; WX 602 ; N oe ; B 0 -12 601 463 ;
+C 251 ; WX 602 ; N germandbls ; B 12 -12 554 640 ;
+C -1 ; WX 602 ; N Aacute ; B -6 0 609 802 ;
+C -1 ; WX 602 ; N Acircumflex ; B -6 0 609 794 ;
+C -1 ; WX 602 ; N Adieresis ; B -6 0 609 776 ;
+C -1 ; WX 602 ; N Agrave ; B -6 0 609 802 ;
+C -1 ; WX 602 ; N Aring ; B -6 0 609 875 ;
+C -1 ; WX 602 ; N Atilde ; B -6 0 609 772 ;
+C -1 ; WX 602 ; N Ccedilla ; B 30 -233 564 596 ;
+C -1 ; WX 602 ; N Eacute ; B 35 0 547 802 ;
+C -1 ; WX 602 ; N Ecircumflex ; B 35 0 547 794 ;
+C -1 ; WX 602 ; N Edieresis ; B 35 0 547 776 ;
+C -1 ; WX 602 ; N Egrave ; B 35 0 547 802 ;
+C -1 ; WX 602 ; N Iacute ; B 94 0 508 802 ;
+C -1 ; WX 602 ; N Icircumflex ; B 94 0 508 794 ;
+C -1 ; WX 602 ; N Idieresis ; B 94 0 508 776 ;
+C -1 ; WX 602 ; N Igrave ; B 94 0 508 802 ;
+C -1 ; WX 602 ; N Ntilde ; B 25 -7 587 772 ;
+C -1 ; WX 602 ; N Oacute ; B 25 -16 577 802 ;
+C -1 ; WX 602 ; N Ocircumflex ; B 25 -16 577 794 ;
+C -1 ; WX 602 ; N Odieresis ; B 25 -16 577 776 ;
+C -1 ; WX 602 ; N Ograve ; B 25 -16 577 802 ;
+C -1 ; WX 602 ; N Otilde ; B 25 -16 577 772 ;
+C -1 ; WX 602 ; N Scaron ; B 56 -16 545 795 ;
+C -1 ; WX 602 ; N Uacute ; B 12 -16 590 802 ;
+C -1 ; WX 602 ; N Ucircumflex ; B 12 -16 590 794 ;
+C -1 ; WX 602 ; N Udieresis ; B 12 -16 590 776 ;
+C -1 ; WX 602 ; N Ugrave ; B 12 -16 590 802 ;
+C -1 ; WX 602 ; N Ydieresis ; B 38 0 556 776 ;
+C -1 ; WX 602 ; N Zcaron ; B 69 0 512 795 ;
+C -1 ; WX 602 ; N aacute ; B 51 -12 562 670 ;
+C -1 ; WX 602 ; N acircumflex ; B 51 -12 562 662 ;
+C -1 ; WX 602 ; N adieresis ; B 51 -12 562 644 ;
+C -1 ; WX 602 ; N agrave ; B 51 -12 562 670 ;
+C -1 ; WX 602 ; N aring ; B 51 -12 562 764 ;
+C -1 ; WX 602 ; N atilde ; B 51 -12 562 640 ;
+C -1 ; WX 602 ; N ccedilla ; B 43 -233 546 464 ;
+C -1 ; WX 602 ; N eacute ; B 43 -12 555 670 ;
+C -1 ; WX 602 ; N ecircumflex ; B 43 -12 555 662 ;
+C -1 ; WX 602 ; N edieresis ; B 43 -12 555 644 ;
+C -1 ; WX 602 ; N egrave ; B 43 -12 555 670 ;
+C -1 ; WX 602 ; N iacute ; B 82 0 536 670 ;
+C -1 ; WX 602 ; N icircumflex ; B 82 0 536 662 ;
+C -1 ; WX 602 ; N idieresis ; B 82 0 536 644 ;
+C -1 ; WX 602 ; N igrave ; B 82 0 536 670 ;
+C -1 ; WX 602 ; N ntilde ; B 40 0 568 640 ;
+C -1 ; WX 602 ; N oacute ; B 34 -12 568 670 ;
+C -1 ; WX 602 ; N ocircumflex ; B 34 -12 568 662 ;
+C -1 ; WX 602 ; N odieresis ; B 34 -12 568 644 ;
+C -1 ; WX 602 ; N ograve ; B 34 -12 568 670 ;
+C -1 ; WX 602 ; N otilde ; B 34 -12 568 640 ;
+C -1 ; WX 602 ; N scaron ; B 78 -14 527 663 ;
+C -1 ; WX 602 ; N uacute ; B 33 -12 560 670 ;
+C -1 ; WX 602 ; N ucircumflex ; B 33 -12 560 662 ;
+C -1 ; WX 602 ; N udieresis ; B 33 -12 560 644 ;
+C -1 ; WX 602 ; N ugrave ; B 33 -12 560 670 ;
+C -1 ; WX 602 ; N ydieresis ; B 28 -195 572 644 ;
+C -1 ; WX 602 ; N zcaron ; B 82 0 501 663 ;
+C -1 ; WX 602 ; N trademark ; B 32 329 566 590 ;
+C -1 ; WX 602 ; N copyright ; B 1 32 601 632 ;
+C -1 ; WX 602 ; N logicalnot ; B 34 156 567 459 ;
+C -1 ; WX 602 ; N registered ; B 1 32 601 632 ;
+C -1 ; WX 602 ; N minus ; B 36 257 566 359 ;
+C -1 ; WX 602 ; N Eth ; B 33 0 569 579 ;
+C -1 ; WX 602 ; N Thorn ; B 68 0 526 579 ;
+C -1 ; WX 602 ; N Yacute ; B 38 0 556 802 ;
+C -1 ; WX 602 ; N brokenbar ; B 253 -172 349 699 ;
+C -1 ; WX 602 ; N degree ; B 143 349 459 665 ;
+C -1 ; WX 602 ; N divide ; B 36 86 565 531 ;
+C -1 ; WX 602 ; N eth ; B 34 -12 563 639 ;
+C -1 ; WX 602 ; N mu ; B 25 -195 542 453 ;
+C -1 ; WX 602 ; N multiply ; B 93 110 509 526 ;
+C -1 ; WX 602 ; N onehalf ; B 22 -86 572 680 ;
+C -1 ; WX 602 ; N onequarter ; B 22 -93 570 680 ;
+C -1 ; WX 602 ; N onesuperior ; B 175 241 428 624 ;
+C -1 ; WX 602 ; N plusminus ; B 60 29 541 619 ;
+C -1 ; WX 602 ; N thorn ; B 1 -195 567 639 ;
+C -1 ; WX 602 ; N threequarters ; B 38 -93 570 689 ;
+C -1 ; WX 602 ; N threesuperior ; B 127 222 452 633 ;
+C -1 ; WX 602 ; N twosuperior ; B 145 240 465 632 ;
+C -1 ; WX 602 ; N yacute ; B 28 -195 572 670 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 0
+EndKernPairs
+StartTrackKern 0
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0583bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0583bt_.pfb
new file mode 100644
index 00000000..08f6871c
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0583bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0611bt_.afm b/e2e-tests/cypress/fonts/Type1/c0611bt_.afm
new file mode 100644
index 00000000..4578e779
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0611bt_.afm
@@ -0,0 +1,264 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Fixed Pitch 810
+Comment bitsFontID 0611
+Comment bitsManufacturingDate Tue Nov 6 01:15:55 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530611
+FontName Courier10PitchBT-BoldItalic
+FullName Courier 10 Pitch Bold Italic
+FamilyName Courier 10 Pitch
+Weight Bold
+ItalicAngle 12.0000
+IsFixedPitch true
+FontBBox -79 -310 665 875
+UnderlinePosition -97
+UnderlineThickness 93
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 579
+XHeight 452
+Ascender 639
+Descender -195
+StartCharMetrics 228
+C 32 ; WX 602 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 602 ; N exclam ; B 170 -14 408 596 ;
+C 34 ; WX 602 ; N quotedbl ; B 169 323 481 579 ;
+C 35 ; WX 602 ; N numbersign ; B 99 -58 502 684 ;
+C 36 ; WX 602 ; N dollar ; B 62 -123 559 672 ;
+C 37 ; WX 602 ; N percent ; B 100 -16 501 633 ;
+C 38 ; WX 602 ; N ampersand ; B 38 -14 538 562 ;
+C 39 ; WX 602 ; N quoteright ; B 185 304 418 579 ;
+C 40 ; WX 602 ; N parenleft ; B 182 -194 524 596 ;
+C 41 ; WX 602 ; N parenright ; B 75 -195 417 595 ;
+C 42 ; WX 602 ; N asterisk ; B 102 234 500 616 ;
+C 43 ; WX 602 ; N plus ; B 35 42 566 573 ;
+C 44 ; WX 602 ; N comma ; B 115 -164 396 167 ;
+C 45 ; WX 602 ; N hyphen ; B 62 170 526 282 ;
+C 46 ; WX 602 ; N period ; B 182 -12 377 163 ;
+C 47 ; WX 602 ; N slash ; B 9 -124 580 704 ;
+C 48 ; WX 602 ; N zero ; B 43 -15 559 634 ;
+C 49 ; WX 602 ; N one ; B 48 0 455 633 ;
+C 50 ; WX 602 ; N two ; B 14 0 527 631 ;
+C 51 ; WX 602 ; N three ; B 19 -17 507 634 ;
+C 52 ; WX 602 ; N four ; B 54 0 532 635 ;
+C 53 ; WX 602 ; N five ; B 37 -17 560 616 ;
+C 54 ; WX 602 ; N six ; B 71 -17 585 633 ;
+C 55 ; WX 602 ; N seven ; B 114 -8 545 616 ;
+C 56 ; WX 602 ; N eight ; B 52 -16 549 633 ;
+C 57 ; WX 602 ; N nine ; B 26 -16 540 634 ;
+C 58 ; WX 602 ; N colon ; B 155 -12 405 438 ;
+C 59 ; WX 602 ; N semicolon ; B 84 -164 427 438 ;
+C 60 ; WX 602 ; N less ; B 41 62 556 553 ;
+C 61 ; WX 602 ; N equal ; B 36 160 566 456 ;
+C 62 ; WX 602 ; N greater ; B 32 62 547 553 ;
+C 63 ; WX 602 ; N question ; B 123 -14 510 596 ;
+C 64 ; WX 602 ; N at ; B 76 -53 550 667 ;
+C 65 ; WX 602 ; N A ; B -44 0 562 579 ;
+C 66 ; WX 602 ; N B ; B -11 0 551 579 ;
+C 67 ; WX 602 ; N C ; B 31 -16 600 595 ;
+C 68 ; WX 602 ; N D ; B -10 0 572 579 ;
+C 69 ; WX 602 ; N E ; B -8 0 593 579 ;
+C 70 ; WX 602 ; N F ; B 6 0 607 579 ;
+C 71 ; WX 602 ; N G ; B 24 -16 586 595 ;
+C 72 ; WX 602 ; N H ; B -8 0 606 579 ;
+C 73 ; WX 602 ; N I ; B 56 0 544 579 ;
+C 74 ; WX 602 ; N J ; B 19 -16 624 579 ;
+C 75 ; WX 602 ; N K ; B 1 0 621 579 ;
+C 76 ; WX 602 ; N L ; B -4 0 543 579 ;
+C 77 ; WX 602 ; N M ; B -54 0 638 579 ;
+C 78 ; WX 602 ; N N ; B -25 -9 625 579 ;
+C 79 ; WX 602 ; N O ; B 16 -15 585 596 ;
+C 80 ; WX 602 ; N P ; B 3 0 579 579 ;
+C 81 ; WX 602 ; N Q ; B 16 -158 585 596 ;
+C 82 ; WX 602 ; N R ; B -6 0 561 579 ;
+C 83 ; WX 602 ; N S ; B 26 -20 572 604 ;
+C 84 ; WX 602 ; N T ; B 51 0 612 579 ;
+C 85 ; WX 602 ; N U ; B 56 -17 628 579 ;
+C 86 ; WX 602 ; N V ; B 44 0 640 579 ;
+C 87 ; WX 602 ; N W ; B 39 0 645 579 ;
+C 88 ; WX 602 ; N X ; B 0 0 581 579 ;
+C 89 ; WX 602 ; N Y ; B 63 0 587 579 ;
+C 90 ; WX 602 ; N Z ; B 44 0 537 579 ;
+C 91 ; WX 602 ; N bracketleft ; B 144 -181 519 579 ;
+C 92 ; WX 602 ; N backslash ; B 80 -126 522 703 ;
+C 93 ; WX 602 ; N bracketright ; B 66 -181 441 579 ;
+C 94 ; WX 602 ; N asciicircum ; B 104 422 498 671 ;
+C 95 ; WX 602 ; N underscore ; B -22 -310 626 -207 ;
+C 96 ; WX 602 ; N quoteleft ; B 212 323 445 597 ;
+C 97 ; WX 602 ; N a ; B 35 -11 514 465 ;
+C 98 ; WX 602 ; N b ; B -21 -9 541 639 ;
+C 99 ; WX 602 ; N c ; B 27 -12 557 465 ;
+C 100 ; WX 602 ; N d ; B 34 -12 582 639 ;
+C 101 ; WX 602 ; N e ; B 34 -12 537 464 ;
+C 102 ; WX 602 ; N f ; B 66 0 616 639 ;
+C 103 ; WX 602 ; N g ; B 34 -195 600 464 ;
+C 104 ; WX 602 ; N h ; B -1 0 520 639 ;
+C 105 ; WX 602 ; N i ; B 47 0 483 672 ;
+C 106 ; WX 602 ; N j ; B -19 -195 471 672 ;
+C 107 ; WX 602 ; N k ; B 6 0 558 639 ;
+C 108 ; WX 602 ; N l ; B 37 0 473 639 ;
+C 109 ; WX 602 ; N m ; B -54 0 573 464 ;
+C 110 ; WX 602 ; N n ; B -1 0 520 463 ;
+C 111 ; WX 602 ; N o ; B 39 -13 545 464 ;
+C 112 ; WX 602 ; N p ; B -79 -195 549 463 ;
+C 113 ; WX 602 ; N q ; B 34 -195 605 464 ;
+C 114 ; WX 602 ; N r ; B 35 0 571 464 ;
+C 115 ; WX 602 ; N s ; B 45 -18 527 473 ;
+C 116 ; WX 602 ; N t ; B 66 -8 508 601 ;
+C 117 ; WX 602 ; N u ; B 57 -12 523 452 ;
+C 118 ; WX 602 ; N v ; B 55 -12 575 452 ;
+C 119 ; WX 602 ; N w ; B 5 -13 635 452 ;
+C 120 ; WX 602 ; N x ; B 26 0 551 452 ;
+C 121 ; WX 602 ; N y ; B -7 -195 575 452 ;
+C 122 ; WX 602 ; N z ; B 48 0 522 452 ;
+C 123 ; WX 602 ; N braceleft ; B 143 -185 473 582 ;
+C 124 ; WX 602 ; N bar ; B 243 -261 359 789 ;
+C 125 ; WX 602 ; N braceright ; B 124 -185 454 582 ;
+C 126 ; WX 602 ; N asciitilde ; B 50 236 551 380 ;
+C 161 ; WX 602 ; N exclamdown ; B 184 -14 422 595 ;
+C 162 ; WX 602 ; N cent ; B 82 -33 543 646 ;
+C 163 ; WX 602 ; N sterling ; B 78 -15 559 632 ;
+C 164 ; WX 602 ; N fraction ; B 9 -124 580 704 ;
+C 165 ; WX 602 ; N yen ; B 42 0 592 616 ;
+C 166 ; WX 602 ; N florin ; B -34 -140 614 631 ;
+C 167 ; WX 602 ; N section ; B 35 -103 566 579 ;
+C 168 ; WX 602 ; N currency ; B 58 189 544 676 ;
+C 169 ; WX 602 ; N quotesingle ; B 242 323 360 579 ;
+C 170 ; WX 602 ; N quotedblleft ; B 106 323 550 597 ;
+C 171 ; WX 602 ; N guillemotleft ; B 126 35 469 408 ;
+C 172 ; WX 602 ; N guilsinglleft ; B 220 35 391 408 ;
+C 173 ; WX 602 ; N guilsinglright ; B 220 38 391 411 ;
+C 174 ; WX 602 ; N fi ; B -21 0 595 672 ;
+C 175 ; WX 602 ; N fl ; B -21 0 599 639 ;
+C 177 ; WX 602 ; N endash ; B -31 179 633 272 ;
+C 178 ; WX 602 ; N dagger ; B 143 -16 504 595 ;
+C 179 ; WX 602 ; N daggerdbl ; B 106 -16 515 595 ;
+C 180 ; WX 602 ; N periodcentered ; B 201 210 400 409 ;
+C 182 ; WX 602 ; N paragraph ; B 45 -77 566 616 ;
+C 183 ; WX 602 ; N bullet ; B 222 256 379 413 ;
+C 184 ; WX 602 ; N quotesinglbase ; B 160 -109 393 166 ;
+C 185 ; WX 602 ; N quotedblbase ; B 41 -109 486 166 ;
+C 186 ; WX 602 ; N quotedblright ; B 114 304 559 579 ;
+C 187 ; WX 602 ; N guillemotright ; B 125 38 468 411 ;
+C 188 ; WX 602 ; N ellipsis ; B -11 -12 536 133 ;
+C 189 ; WX 602 ; N perthousand ; B -44 -16 665 633 ;
+C 191 ; WX 602 ; N questiondown ; B 97 -14 484 596 ;
+C 193 ; WX 602 ; N grave ; B 126 499 386 673 ;
+C 194 ; WX 602 ; N acute ; B 276 499 536 673 ;
+C 195 ; WX 602 ; N circumflex ; B 164 509 499 662 ;
+C 196 ; WX 602 ; N tilde ; B 160 532 518 639 ;
+C 197 ; WX 602 ; N macron ; B 166 546 507 614 ;
+C 198 ; WX 602 ; N breve ; B 159 528 527 660 ;
+C 199 ; WX 602 ; N dotaccent ; B 265 523 398 656 ;
+C 200 ; WX 602 ; N dieresis ; B 148 519 516 644 ;
+C 202 ; WX 602 ; N ring ; B 231 499 495 764 ;
+C 203 ; WX 602 ; N cedilla ; B 95 -232 354 0 ;
+C 205 ; WX 602 ; N hungarumlaut ; B 198 503 576 665 ;
+C 206 ; WX 602 ; N ogonek ; B 188 -226 388 14 ;
+C 207 ; WX 602 ; N caron ; B 164 509 499 662 ;
+C 208 ; WX 602 ; N emdash ; B -31 179 633 272 ;
+C 225 ; WX 602 ; N AE ; B -79 0 652 579 ;
+C 227 ; WX 602 ; N ordfeminine ; B 93 260 476 641 ;
+C 232 ; WX 602 ; N Lslash ; B -4 0 543 579 ;
+C 233 ; WX 602 ; N Oslash ; B -33 -35 619 615 ;
+C 234 ; WX 602 ; N OE ; B 4 0 652 579 ;
+C 235 ; WX 602 ; N ordmasculine ; B 90 258 495 640 ;
+C 241 ; WX 602 ; N ae ; B -27 -13 587 464 ;
+C 245 ; WX 602 ; N dotlessi ; B 47 0 483 452 ;
+C 248 ; WX 602 ; N lslash ; B 37 0 518 639 ;
+C 249 ; WX 602 ; N oslash ; B -29 -60 592 490 ;
+C 250 ; WX 602 ; N oe ; B -7 -13 587 464 ;
+C 251 ; WX 602 ; N germandbls ; B -16 -14 527 640 ;
+C -1 ; WX 602 ; N Aacute ; B -44 0 562 805 ;
+C -1 ; WX 602 ; N Acircumflex ; B -44 0 562 794 ;
+C -1 ; WX 602 ; N Adieresis ; B -44 0 562 776 ;
+C -1 ; WX 602 ; N Agrave ; B -44 0 562 805 ;
+C -1 ; WX 602 ; N Aring ; B -44 0 562 875 ;
+C -1 ; WX 602 ; N Atilde ; B -44 0 562 771 ;
+C -1 ; WX 602 ; N Ccedilla ; B 31 -232 600 595 ;
+C -1 ; WX 602 ; N Eacute ; B -8 0 593 805 ;
+C -1 ; WX 602 ; N Ecircumflex ; B -8 0 593 794 ;
+C -1 ; WX 602 ; N Edieresis ; B -8 0 593 776 ;
+C -1 ; WX 602 ; N Egrave ; B -8 0 593 805 ;
+C -1 ; WX 602 ; N Iacute ; B 56 0 561 805 ;
+C -1 ; WX 602 ; N Icircumflex ; B 56 0 544 794 ;
+C -1 ; WX 602 ; N Idieresis ; B 56 0 544 776 ;
+C -1 ; WX 602 ; N Igrave ; B 56 0 544 805 ;
+C -1 ; WX 602 ; N Ntilde ; B -25 -9 625 771 ;
+C -1 ; WX 602 ; N Oacute ; B 16 -15 585 805 ;
+C -1 ; WX 602 ; N Ocircumflex ; B 16 -15 585 794 ;
+C -1 ; WX 602 ; N Odieresis ; B 16 -15 585 776 ;
+C -1 ; WX 602 ; N Ograve ; B 16 -15 585 805 ;
+C -1 ; WX 602 ; N Otilde ; B 16 -15 585 771 ;
+C -1 ; WX 602 ; N Scaron ; B 26 -20 572 794 ;
+C -1 ; WX 602 ; N Uacute ; B 56 -17 628 805 ;
+C -1 ; WX 602 ; N Ucircumflex ; B 56 -17 628 794 ;
+C -1 ; WX 602 ; N Udieresis ; B 56 -17 628 776 ;
+C -1 ; WX 602 ; N Ugrave ; B 56 -17 628 805 ;
+C -1 ; WX 602 ; N Ydieresis ; B 63 0 587 776 ;
+C -1 ; WX 602 ; N Zcaron ; B 44 0 537 794 ;
+C -1 ; WX 602 ; N aacute ; B 35 -11 547 673 ;
+C -1 ; WX 602 ; N acircumflex ; B 35 -11 514 662 ;
+C -1 ; WX 602 ; N adieresis ; B 35 -11 527 644 ;
+C -1 ; WX 602 ; N agrave ; B 35 -11 514 673 ;
+C -1 ; WX 602 ; N aring ; B 35 -11 514 759 ;
+C -1 ; WX 602 ; N atilde ; B 35 -11 529 639 ;
+C -1 ; WX 602 ; N ccedilla ; B 27 -232 557 465 ;
+C -1 ; WX 602 ; N eacute ; B 34 -12 537 673 ;
+C -1 ; WX 602 ; N ecircumflex ; B 34 -12 537 662 ;
+C -1 ; WX 602 ; N edieresis ; B 34 -12 537 644 ;
+C -1 ; WX 602 ; N egrave ; B 34 -12 537 673 ;
+C -1 ; WX 602 ; N iacute ; B 47 0 536 673 ;
+C -1 ; WX 602 ; N icircumflex ; B 47 0 499 662 ;
+C -1 ; WX 602 ; N idieresis ; B 47 0 516 644 ;
+C -1 ; WX 602 ; N igrave ; B 47 0 483 673 ;
+C -1 ; WX 602 ; N ntilde ; B -1 0 520 639 ;
+C -1 ; WX 602 ; N oacute ; B 39 -13 545 673 ;
+C -1 ; WX 602 ; N ocircumflex ; B 39 -13 545 662 ;
+C -1 ; WX 602 ; N odieresis ; B 39 -13 545 644 ;
+C -1 ; WX 602 ; N ograve ; B 39 -13 545 673 ;
+C -1 ; WX 602 ; N otilde ; B 39 -13 545 639 ;
+C -1 ; WX 602 ; N scaron ; B 45 -18 527 662 ;
+C -1 ; WX 602 ; N uacute ; B 57 -12 525 673 ;
+C -1 ; WX 602 ; N ucircumflex ; B 57 -12 523 662 ;
+C -1 ; WX 602 ; N udieresis ; B 57 -12 523 644 ;
+C -1 ; WX 602 ; N ugrave ; B 57 -12 523 673 ;
+C -1 ; WX 602 ; N ydieresis ; B -7 -195 575 644 ;
+C -1 ; WX 602 ; N zcaron ; B 48 0 522 662 ;
+C -1 ; WX 602 ; N trademark ; B 32 329 566 590 ;
+C -1 ; WX 602 ; N copyright ; B 1 32 601 632 ;
+C -1 ; WX 602 ; N logicalnot ; B 34 156 567 459 ;
+C -1 ; WX 602 ; N registered ; B 1 32 601 632 ;
+C -1 ; WX 602 ; N minus ; B 36 257 566 359 ;
+C -1 ; WX 602 ; N Eth ; B -6 0 577 579 ;
+C -1 ; WX 602 ; N Thorn ; B 5 0 543 579 ;
+C -1 ; WX 602 ; N Yacute ; B 63 0 587 805 ;
+C -1 ; WX 602 ; N brokenbar ; B 253 -172 349 699 ;
+C -1 ; WX 602 ; N degree ; B 171 349 486 665 ;
+C -1 ; WX 602 ; N divide ; B 36 86 565 531 ;
+C -1 ; WX 602 ; N eth ; B 38 -13 545 638 ;
+C -1 ; WX 602 ; N mu ; B 25 -195 542 453 ;
+C -1 ; WX 602 ; N multiply ; B 93 110 509 526 ;
+C -1 ; WX 602 ; N onehalf ; B 22 -86 572 680 ;
+C -1 ; WX 602 ; N onequarter ; B 22 -93 570 680 ;
+C -1 ; WX 602 ; N onesuperior ; B 175 241 428 624 ;
+C -1 ; WX 602 ; N plusminus ; B 60 29 541 619 ;
+C -1 ; WX 602 ; N thorn ; B -79 -195 549 639 ;
+C -1 ; WX 602 ; N threequarters ; B 38 -93 570 689 ;
+C -1 ; WX 602 ; N threesuperior ; B 127 222 452 633 ;
+C -1 ; WX 602 ; N twosuperior ; B 145 240 465 632 ;
+C -1 ; WX 602 ; N yacute ; B -7 -195 575 673 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 0
+EndKernPairs
+StartTrackKern 0
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0611bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0611bt_.pfb
new file mode 100644
index 00000000..ec6ed060
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0611bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0632bt_.afm b/e2e-tests/cypress/fonts/Type1/c0632bt_.afm
new file mode 100644
index 00000000..31721010
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0632bt_.afm
@@ -0,0 +1,628 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Transitional 801
+Comment bitsFontID 0632
+Comment bitsManufacturingDate Tue Nov 6 02:14:13 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530632
+FontName CharterBT-Bold
+FullName Bitstream Charter Bold
+FamilyName Bitstream Charter
+Weight Bold
+ItalicAngle 0.00
+IsFixedPitch false
+FontBBox -166 -237 1263 963
+UnderlinePosition -109
+UnderlineThickness 91
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 672
+XHeight 488
+Ascender 740
+Descender -219
+StartCharMetrics 228
+C 32 ; WX 291 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 340 ; N exclam ; B 94 -8 247 685 ;
+C 34 ; WX 339 ; N quotedbl ; B 40 418 299 715 ;
+C 35 ; WX 736 ; N numbersign ; B 56 -24 675 710 ;
+C 36 ; WX 581 ; N dollar ; B 61 -102 533 742 ;
+C 37 ; WX 888 ; N percent ; B 36 -12 863 683 ;
+C 38 ; WX 741 ; N ampersand ; B 52 -12 725 684 ;
+C 39 ; WX 255 ; N quoteright ; B 47 395 220 698 ;
+C 40 ; WX 428 ; N parenleft ; B 90 -142 387 718 ;
+C 41 ; WX 428 ; N parenright ; B 37 -142 330 718 ;
+C 42 ; WX 500 ; N asterisk ; B 53 338 447 718 ;
+C 43 ; WX 833 ; N plus ; B 124 0 710 597 ;
+C 44 ; WX 289 ; N comma ; B 30 -176 221 129 ;
+C 45 ; WX 326 ; N hyphen ; B 36 191 291 291 ;
+C 46 ; WX 289 ; N period ; B 65 -8 224 151 ;
+C 47 ; WX 491 ; N slash ; B -28 -93 472 672 ;
+C 48 ; WX 581 ; N zero ; B 39 -12 549 683 ;
+C 49 ; WX 581 ; N one ; B 108 0 495 681 ;
+C 50 ; WX 581 ; N two ; B 48 0 533 684 ;
+C 51 ; WX 581 ; N three ; B 42 -11 523 682 ;
+C 52 ; WX 581 ; N four ; B 25 -32 566 677 ;
+C 53 ; WX 581 ; N five ; B 54 -10 525 672 ;
+C 54 ; WX 581 ; N six ; B 46 -13 554 714 ;
+C 55 ; WX 581 ; N seven ; B 75 -34 556 672 ;
+C 56 ; WX 581 ; N eight ; B 41 -16 540 685 ;
+C 57 ; WX 581 ; N nine ; B 42 -54 546 683 ;
+C 58 ; WX 340 ; N colon ; B 94 -8 252 489 ;
+C 59 ; WX 340 ; N semicolon ; B 67 -176 255 489 ;
+C 60 ; WX 833 ; N less ; B 128 22 704 574 ;
+C 61 ; WX 833 ; N equal ; B 124 156 710 440 ;
+C 62 ; WX 833 ; N greater ; B 129 22 704 574 ;
+C 63 ; WX 487 ; N question ; B 35 -8 437 684 ;
+C 64 ; WX 917 ; N at ; B 74 -154 854 693 ;
+C 65 ; WX 651 ; N A ; B -12 0 670 678 ;
+C 66 ; WX 628 ; N B ; B 28 0 590 672 ;
+C 67 ; WX 638 ; N C ; B 40 -13 602 683 ;
+C 68 ; WX 716 ; N D ; B 28 0 682 672 ;
+C 69 ; WX 596 ; N E ; B 28 0 566 672 ;
+C 70 ; WX 552 ; N F ; B 25 0 529 672 ;
+C 71 ; WX 710 ; N G ; B 40 -12 691 683 ;
+C 72 ; WX 760 ; N H ; B 30 0 734 672 ;
+C 73 ; WX 354 ; N I ; B 29 0 329 672 ;
+C 74 ; WX 465 ; N J ; B 11 -13 465 672 ;
+C 75 ; WX 650 ; N K ; B 29 0 672 672 ;
+C 76 ; WX 543 ; N L ; B 27 0 533 672 ;
+C 77 ; WX 883 ; N M ; B 24 0 863 672 ;
+C 78 ; WX 727 ; N N ; B 24 0 711 672 ;
+C 79 ; WX 752 ; N O ; B 40 -17 718 687 ;
+C 80 ; WX 587 ; N P ; B 24 0 569 672 ;
+C 81 ; WX 752 ; N Q ; B 39 -179 720 687 ;
+C 82 ; WX 671 ; N R ; B 30 -7 692 672 ;
+C 83 ; WX 568 ; N S ; B 58 -12 517 683 ;
+C 84 ; WX 603 ; N T ; B 15 0 594 672 ;
+C 85 ; WX 705 ; N U ; B 20 -13 695 672 ;
+C 86 ; WX 635 ; N V ; B -21 -3 661 672 ;
+C 87 ; WX 946 ; N W ; B 1 0 945 672 ;
+C 88 ; WX 637 ; N X ; B -1 0 644 672 ;
+C 89 ; WX 610 ; N Y ; B -11 0 627 672 ;
+C 90 ; WX 592 ; N Z ; B 44 0 550 672 ;
+C 91 ; WX 443 ; N bracketleft ; B 135 -133 406 709 ;
+C 92 ; WX 491 ; N backslash ; B -8 -93 486 672 ;
+C 93 ; WX 443 ; N bracketright ; B 42 -133 312 709 ;
+C 94 ; WX 1000 ; N asciicircum ; B 201 437 798 714 ;
+C 95 ; WX 500 ; N underscore ; B 0 -237 500 -152 ;
+C 96 ; WX 255 ; N quoteleft ; B 49 395 222 699 ;
+C 97 ; WX 544 ; N a ; B 40 -10 535 500 ;
+C 98 ; WX 577 ; N b ; B 9 -2 547 740 ;
+C 99 ; WX 476 ; N c ; B 34 -8 464 498 ;
+C 100 ; WX 596 ; N d ; B 36 -10 577 740 ;
+C 101 ; WX 524 ; N e ; B 37 -9 493 501 ;
+C 102 ; WX 341 ; N f ; B 30 0 412 744 ;
+C 103 ; WX 551 ; N g ; B 33 -218 555 498 ;
+C 104 ; WX 597 ; N h ; B 16 0 586 740 ;
+C 105 ; WX 305 ; N i ; B 29 0 293 724 ;
+C 106 ; WX 297 ; N j ; B -80 -215 242 724 ;
+C 107 ; WX 553 ; N k ; B 17 0 572 740 ;
+C 108 ; WX 304 ; N l ; B 22 0 292 740 ;
+C 109 ; WX 892 ; N m ; B 30 0 883 500 ;
+C 110 ; WX 605 ; N n ; B 27 0 594 499 ;
+C 111 ; WX 577 ; N o ; B 36 -9 547 499 ;
+C 112 ; WX 591 ; N p ; B 21 -219 560 500 ;
+C 113 ; WX 575 ; N q ; B 37 -218 572 499 ;
+C 114 ; WX 421 ; N r ; B 24 0 421 498 ;
+C 115 ; WX 447 ; N s ; B 40 -11 411 500 ;
+C 116 ; WX 358 ; N t ; B 18 -5 357 599 ;
+C 117 ; WX 600 ; N u ; B 22 -10 583 499 ;
+C 118 ; WX 513 ; N v ; B -7 0 535 488 ;
+C 119 ; WX 799 ; N w ; B -1 0 811 488 ;
+C 120 ; WX 531 ; N x ; B 11 0 532 488 ;
+C 121 ; WX 515 ; N y ; B -5 -219 537 486 ;
+C 122 ; WX 495 ; N z ; B 45 0 466 486 ;
+C 123 ; WX 493 ; N braceleft ; B 46 -134 421 705 ;
+C 124 ; WX 500 ; N bar ; B 207 -237 294 764 ;
+C 125 ; WX 493 ; N braceright ; B 62 -134 438 705 ;
+C 126 ; WX 833 ; N asciitilde ; B 86 212 747 384 ;
+C 161 ; WX 340 ; N exclamdown ; B 93 -8 246 685 ;
+C 162 ; WX 581 ; N cent ; B 58 -103 504 612 ;
+C 163 ; WX 581 ; N sterling ; B 42 0 540 680 ;
+C 164 ; WX 167 ; N fraction ; B -166 -1 333 672 ;
+C 165 ; WX 595 ; N yen ; B -7 0 604 672 ;
+C 166 ; WX 581 ; N florin ; B 12 -149 535 683 ;
+C 167 ; WX 500 ; N section ; B 45 -142 455 720 ;
+C 168 ; WX 606 ; N currency ; B 36 166 571 699 ;
+C 169 ; WX 175 ; N quotesingle ; B 40 418 135 715 ;
+C 170 ; WX 475 ; N quotedblleft ; B 49 395 443 699 ;
+C 171 ; WX 449 ; N guillemotleft ; B 34 53 404 427 ;
+C 172 ; WX 255 ; N guilsinglleft ; B 34 53 207 427 ;
+C 173 ; WX 255 ; N guilsinglright ; B 37 53 211 427 ;
+C 174 ; WX 622 ; N fi ; B 30 0 605 745 ;
+C 175 ; WX 627 ; N fl ; B 30 0 616 746 ;
+C 177 ; WX 500 ; N endash ; B 0 195 500 286 ;
+C 178 ; WX 500 ; N dagger ; B 17 -130 484 718 ;
+C 179 ; WX 500 ; N daggerdbl ; B 17 -132 484 718 ;
+C 180 ; WX 289 ; N periodcentered ; B 65 256 224 415 ;
+C 182 ; WX 491 ; N paragraph ; B 18 -79 458 672 ;
+C 183 ; WX 590 ; N bullet ; B 150 227 439 516 ;
+C 184 ; WX 255 ; N quotesinglbase ; B 32 -174 205 130 ;
+C 185 ; WX 475 ; N quotedblbase ; B 35 -174 429 130 ;
+C 186 ; WX 475 ; N quotedblright ; B 47 395 441 698 ;
+C 187 ; WX 449 ; N guillemotright ; B 42 53 412 427 ;
+C 188 ; WX 1000 ; N ellipsis ; B 87 -8 914 151 ;
+C 189 ; WX 1287 ; N perthousand ; B 36 -12 1263 683 ;
+C 191 ; WX 487 ; N questiondown ; B 37 -8 440 684 ;
+C 193 ; WX 500 ; N grave ; B 85 551 307 742 ;
+C 194 ; WX 500 ; N acute ; B 205 551 428 742 ;
+C 195 ; WX 500 ; N circumflex ; B 96 551 404 742 ;
+C 196 ; WX 500 ; N tilde ; B 87 570 411 723 ;
+C 197 ; WX 500 ; N macron ; B 85 603 417 676 ;
+C 198 ; WX 500 ; N breve ; B 98 567 403 719 ;
+C 199 ; WX 500 ; N dotaccent ; B 185 578 316 713 ;
+C 200 ; WX 500 ; N dieresis ; B 83 578 417 710 ;
+C 202 ; WX 500 ; N ring ; B 131 546 369 784 ;
+C 203 ; WX 500 ; N cedilla ; B 171 -230 374 0 ;
+C 205 ; WX 500 ; N hungarumlaut ; B 107 551 490 742 ;
+C 206 ; WX 500 ; N ogonek ; B 176 -225 336 0 ;
+C 207 ; WX 500 ; N caron ; B 96 551 404 742 ;
+C 208 ; WX 1000 ; N emdash ; B 0 195 1000 286 ;
+C 225 ; WX 890 ; N AE ; B -59 0 863 672 ;
+C 227 ; WX 408 ; N ordfeminine ; B 30 323 402 681 ;
+C 232 ; WX 543 ; N Lslash ; B 9 0 533 672 ;
+C 233 ; WX 752 ; N Oslash ; B 40 -83 718 754 ;
+C 234 ; WX 1010 ; N OE ; B 39 -11 980 682 ;
+C 235 ; WX 433 ; N ordmasculine ; B 27 324 411 680 ;
+C 241 ; WX 768 ; N ae ; B 40 -10 735 500 ;
+C 245 ; WX 305 ; N dotlessi ; B 29 0 293 497 ;
+C 248 ; WX 304 ; N lslash ; B -3 0 328 740 ;
+C 249 ; WX 577 ; N oslash ; B 37 -83 548 571 ;
+C 250 ; WX 861 ; N oe ; B 37 -9 827 500 ;
+C 251 ; WX 642 ; N germandbls ; B 18 -9 622 743 ;
+C -1 ; WX 651 ; N Aacute ; B -12 0 670 930 ;
+C -1 ; WX 651 ; N Acircumflex ; B -12 0 670 930 ;
+C -1 ; WX 651 ; N Adieresis ; B -12 0 670 898 ;
+C -1 ; WX 651 ; N Agrave ; B -12 0 670 930 ;
+C -1 ; WX 651 ; N Aring ; B -12 0 670 963 ;
+C -1 ; WX 651 ; N Atilde ; B -12 0 670 911 ;
+C -1 ; WX 638 ; N Ccedilla ; B 40 -230 602 683 ;
+C -1 ; WX 596 ; N Eacute ; B 28 0 566 930 ;
+C -1 ; WX 596 ; N Ecircumflex ; B 28 0 566 930 ;
+C -1 ; WX 596 ; N Edieresis ; B 28 0 566 898 ;
+C -1 ; WX 596 ; N Egrave ; B 28 0 566 930 ;
+C -1 ; WX 354 ; N Iacute ; B 29 0 355 930 ;
+C -1 ; WX 354 ; N Icircumflex ; B 23 0 331 930 ;
+C -1 ; WX 354 ; N Idieresis ; B 10 0 344 898 ;
+C -1 ; WX 354 ; N Igrave ; B 12 0 329 930 ;
+C -1 ; WX 727 ; N Ntilde ; B 24 0 711 911 ;
+C -1 ; WX 752 ; N Oacute ; B 40 -17 718 930 ;
+C -1 ; WX 752 ; N Ocircumflex ; B 40 -17 718 930 ;
+C -1 ; WX 752 ; N Odieresis ; B 40 -17 718 898 ;
+C -1 ; WX 752 ; N Ograve ; B 40 -17 718 930 ;
+C -1 ; WX 752 ; N Otilde ; B 40 -17 718 911 ;
+C -1 ; WX 568 ; N Scaron ; B 58 -12 517 930 ;
+C -1 ; WX 705 ; N Uacute ; B 20 -13 695 930 ;
+C -1 ; WX 705 ; N Ucircumflex ; B 20 -13 695 930 ;
+C -1 ; WX 705 ; N Udieresis ; B 20 -13 695 898 ;
+C -1 ; WX 705 ; N Ugrave ; B 20 -13 695 930 ;
+C -1 ; WX 610 ; N Ydieresis ; B -11 0 627 898 ;
+C -1 ; WX 592 ; N Zcaron ; B 44 0 550 930 ;
+C -1 ; WX 544 ; N aacute ; B 40 -10 535 742 ;
+C -1 ; WX 544 ; N acircumflex ; B 40 -10 535 742 ;
+C -1 ; WX 544 ; N adieresis ; B 40 -10 535 710 ;
+C -1 ; WX 544 ; N agrave ; B 40 -10 535 742 ;
+C -1 ; WX 544 ; N aring ; B 40 -10 535 784 ;
+C -1 ; WX 544 ; N atilde ; B 40 -10 535 723 ;
+C -1 ; WX 476 ; N ccedilla ; B 34 -230 464 498 ;
+C -1 ; WX 524 ; N eacute ; B 37 -9 493 742 ;
+C -1 ; WX 524 ; N ecircumflex ; B 37 -9 493 742 ;
+C -1 ; WX 524 ; N edieresis ; B 37 -9 493 710 ;
+C -1 ; WX 524 ; N egrave ; B 37 -9 493 742 ;
+C -1 ; WX 305 ; N iacute ; B 29 0 331 742 ;
+C -1 ; WX 305 ; N icircumflex ; B -2 0 307 742 ;
+C -1 ; WX 305 ; N idieresis ; B -15 0 320 710 ;
+C -1 ; WX 305 ; N igrave ; B -13 0 293 742 ;
+C -1 ; WX 605 ; N ntilde ; B 27 0 594 723 ;
+C -1 ; WX 577 ; N oacute ; B 36 -9 547 742 ;
+C -1 ; WX 577 ; N ocircumflex ; B 36 -9 547 742 ;
+C -1 ; WX 577 ; N odieresis ; B 36 -9 547 710 ;
+C -1 ; WX 577 ; N ograve ; B 36 -9 547 742 ;
+C -1 ; WX 577 ; N otilde ; B 36 -9 547 723 ;
+C -1 ; WX 447 ; N scaron ; B 40 -11 411 742 ;
+C -1 ; WX 600 ; N uacute ; B 22 -10 583 742 ;
+C -1 ; WX 600 ; N ucircumflex ; B 22 -10 583 742 ;
+C -1 ; WX 600 ; N udieresis ; B 22 -10 583 710 ;
+C -1 ; WX 600 ; N ugrave ; B 22 -10 583 742 ;
+C -1 ; WX 515 ; N ydieresis ; B -5 -219 537 710 ;
+C -1 ; WX 495 ; N zcaron ; B 45 0 466 742 ;
+C -1 ; WX 800 ; N trademark ; B 111 398 710 662 ;
+C -1 ; WX 876 ; N copyright ; B 61 -50 825 730 ;
+C -1 ; WX 833 ; N logicalnot ; B 124 175 710 421 ;
+C -1 ; WX 876 ; N registered ; B 61 -50 825 730 ;
+C -1 ; WX 833 ; N minus ; B 124 256 710 340 ;
+C -1 ; WX 716 ; N Eth ; B 15 0 682 672 ;
+C -1 ; WX 587 ; N Thorn ; B 32 0 572 672 ;
+C -1 ; WX 610 ; N Yacute ; B -11 0 627 930 ;
+C -1 ; WX 500 ; N brokenbar ; B 207 -172 294 699 ;
+C -1 ; WX 329 ; N degree ; B 20 424 309 713 ;
+C -1 ; WX 833 ; N divide ; B 124 45 710 551 ;
+C -1 ; WX 569 ; N eth ; B 34 -10 540 744 ;
+C -1 ; WX 578 ; N mu ; B -53 -206 546 433 ;
+C -1 ; WX 833 ; N multiply ; B 139 16 704 581 ;
+C -1 ; WX 899 ; N onehalf ; B 68 -1 869 677 ;
+C -1 ; WX 899 ; N onequarter ; B 68 -18 890 677 ;
+C -1 ; WX 383 ; N onesuperior ; B 71 268 327 677 ;
+C -1 ; WX 833 ; N plusminus ; B 124 7 710 590 ;
+C -1 ; WX 591 ; N thorn ; B 20 -219 562 740 ;
+C -1 ; WX 899 ; N threequarters ; B 26 -18 890 678 ;
+C -1 ; WX 383 ; N threesuperior ; B 27 261 346 678 ;
+C -1 ; WX 383 ; N twosuperior ; B 31 268 352 679 ;
+C -1 ; WX 515 ; N yacute ; B -5 -219 537 742 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 361
+KPX hyphen T -37
+KPX hyphen V -56
+KPX hyphen W -56
+KPX hyphen X -37
+KPX hyphen Y -74
+KPX A quoteright -130
+KPX A T -111
+KPX A U -23
+KPX A V -56
+KPX A W -42
+KPX A Y -42
+KPX A f -19
+KPX A t -19
+KPX A v -32
+KPX A w -46
+KPX A y -23
+KPX A fi -19
+KPX A fl -19
+KPX A quotedblright -130
+KPX B hyphen 37
+KPX B C 19
+KPX B G 19
+KPX B O 19
+KPX B S 19
+KPX B V -37
+KPX B W -19
+KPX B Y -19
+KPX B Oslash 19
+KPX B OE 19
+KPX C quoteright 37
+KPX C hyphen 23
+KPX C A -19
+KPX C S 19
+KPX C quotedblright 37
+KPX C Aring -19
+KPX D hyphen 37
+KPX D A -19
+KPX D V -19
+KPX D Y -19
+KPX D Aring -19
+KPX F comma -190
+KPX F hyphen -74
+KPX F period -190
+KPX F colon -37
+KPX F semicolon -37
+KPX F A -97
+KPX F a -79
+KPX F e -65
+KPX F i -19
+KPX F o -46
+KPX F r -37
+KPX F u -37
+KPX F y -37
+KPX F quotesinglbase -56
+KPX F quotedblbase -56
+KPX F ae -79
+KPX F oslash -46
+KPX F oe -46
+KPX F Aring -97
+KPX G hyphen 19
+KPX G T -19
+KPX G W -19
+KPX G Y -23
+KPX J A -37
+KPX J Aring -37
+KPX K hyphen -37
+KPX K A -23
+KPX K C -28
+KPX K O -28
+KPX K U -37
+KPX K W -37
+KPX K Y -28
+KPX K a 19
+KPX K e -37
+KPX K o -37
+KPX K u -19
+KPX K y -102
+KPX K quotesinglbase 37
+KPX K quotedblbase 37
+KPX K Oslash -28
+KPX K OE -28
+KPX K ae 19
+KPX K oslash -37
+KPX K oe -37
+KPX K Aring -23
+KPX L quoteright -167
+KPX L T -83
+KPX L U -19
+KPX L V -120
+KPX L W -88
+KPX L Y -102
+KPX L quoteleft -74
+KPX L a 19
+KPX L e 19
+KPX L o 19
+KPX L y -56
+KPX L quotedblleft -74
+KPX L quotesinglbase 19
+KPX L quotedblbase 19
+KPX L quotedblright -167
+KPX L ae 19
+KPX L oslash 19
+KPX L oe 19
+KPX O comma -60
+KPX O hyphen 37
+KPX O period -60
+KPX O V -19
+KPX O X -19
+KPX P comma -259
+KPX P hyphen -93
+KPX P period -259
+KPX P colon -37
+KPX P semicolon -37
+KPX P A -93
+KPX P U -19
+KPX P a -37
+KPX P e -37
+KPX P o -32
+KPX P quotesinglbase -93
+KPX P quotedblbase -93
+KPX P ae -37
+KPX P oslash -32
+KPX P oe -32
+KPX P Aring -93
+KPX Q quoteright 19
+KPX Q hyphen 37
+KPX Q quotedblright 19
+KPX R quoteright -37
+KPX R colon -19
+KPX R semicolon -19
+KPX R T -37
+KPX R V -56
+KPX R W -42
+KPX R Y -51
+KPX R quoteleft -37
+KPX R e -37
+KPX R o -37
+KPX R u -37
+KPX R y -46
+KPX R quotedblleft -37
+KPX R quotesinglbase 37
+KPX R quotedblbase 37
+KPX R quotedblright -37
+KPX R oslash -37
+KPX R oe -37
+KPX T quoteright 19
+KPX T comma -148
+KPX T hyphen -130
+KPX T period -148
+KPX T colon -37
+KPX T semicolon -37
+KPX T A -111
+KPX T T 19
+KPX T quoteleft 37
+KPX T a -97
+KPX T c -97
+KPX T e -97
+KPX T i -19
+KPX T o -97
+KPX T r -74
+KPX T s -74
+KPX T u -111
+KPX T w -74
+KPX T y -93
+KPX T quotedblleft 37
+KPX T guillemotleft -37
+KPX T guilsinglleft -37
+KPX T quotedblright 19
+KPX T ae -97
+KPX T oslash -97
+KPX T oe -97
+KPX T Aring -111
+KPX U A -32
+KPX U J -28
+KPX U Aring -32
+KPX V quoteright 37
+KPX V comma -222
+KPX V hyphen -93
+KPX V period -222
+KPX V colon -102
+KPX V semicolon -102
+KPX V A -79
+KPX V O -19
+KPX V a -111
+KPX V e -106
+KPX V i -28
+KPX V o -93
+KPX V u -65
+KPX V y -65
+KPX V quotesinglbase -74
+KPX V quotedblbase -74
+KPX V quotedblright 37
+KPX V Oslash -19
+KPX V OE -19
+KPX V ae -111
+KPX V oslash -93
+KPX V oe -93
+KPX V Aring -79
+KPX W quoteright 19
+KPX W comma -176
+KPX W hyphen -74
+KPX W period -176
+KPX W colon -88
+KPX W semicolon -88
+KPX W A -60
+KPX W a -88
+KPX W e -83
+KPX W i -37
+KPX W o -88
+KPX W r -65
+KPX W u -60
+KPX W y -42
+KPX W quotesinglbase -37
+KPX W quotedblbase -37
+KPX W quotedblright 19
+KPX W ae -88
+KPX W oslash -88
+KPX W oe -88
+KPX W Aring -60
+KPX X hyphen -37
+KPX X A -19
+KPX X C -19
+KPX X O -19
+KPX X e -37
+KPX X quotesinglbase 19
+KPX X quotedblbase 19
+KPX X Oslash -19
+KPX X OE -19
+KPX X Aring -19
+KPX Y quoteright 28
+KPX Y comma -130
+KPX Y hyphen -130
+KPX Y period -130
+KPX Y colon -125
+KPX Y semicolon -125
+KPX Y A -60
+KPX Y C -19
+KPX Y a -116
+KPX Y e -125
+KPX Y i -37
+KPX Y o -116
+KPX Y u -88
+KPX Y guillemotleft -56
+KPX Y guilsinglleft -56
+KPX Y quotesinglbase -37
+KPX Y quotedblbase -37
+KPX Y quotedblright 28
+KPX Y ae -116
+KPX Y oslash -116
+KPX Y oe -116
+KPX Y Aring -60
+KPX quoteleft A -130
+KPX quoteleft J -148
+KPX quoteleft V 56
+KPX quoteleft W 37
+KPX quoteleft X 37
+KPX quoteleft Y 37
+KPX quoteleft v 28
+KPX quoteleft w 19
+KPX quoteleft y 19
+KPX quoteleft AE -111
+KPX quoteleft Aring -130
+KPX f quoteright 74
+KPX f comma -37
+KPX f hyphen -19
+KPX f period -37
+KPX f quoteleft 37
+KPX f quotedblleft 37
+KPX f quotedblright 74
+KPX r comma -111
+KPX r period -111
+KPX r quotesinglbase -37
+KPX r quotedblbase -37
+KPX v quoteright 28
+KPX v comma -120
+KPX v period -120
+KPX v quotedblright 28
+KPX w quoteright 19
+KPX w comma -120
+KPX w period -120
+KPX w quoteleft 19
+KPX w quotedblleft 19
+KPX w quotesinglbase -37
+KPX w quotedblbase -37
+KPX w quotedblright 19
+KPX x e -19
+KPX x o -19
+KPX x oslash -19
+KPX x oe -19
+KPX y comma -134
+KPX y period -134
+KPX y quoteleft 19
+KPX y quotedblleft 19
+KPX quotedblleft A -130
+KPX quotedblleft J -148
+KPX quotedblleft V 56
+KPX quotedblleft W 37
+KPX quotedblleft X 37
+KPX quotedblleft Y 37
+KPX quotedblleft v 28
+KPX quotedblleft w 19
+KPX quotedblleft y 19
+KPX quotedblleft AE -111
+KPX quotedblleft Aring -130
+KPX guilsinglright Y -56
+KPX quotesinglbase A 19
+KPX quotesinglbase V -37
+KPX quotesinglbase X 19
+KPX quotesinglbase Y -19
+KPX quotesinglbase AE 74
+KPX quotesinglbase Aring 19
+KPX quotedblbase A 19
+KPX quotedblbase V -37
+KPX quotedblbase X 19
+KPX quotedblbase Y -19
+KPX quotedblbase AE 74
+KPX quotedblbase Aring 19
+KPX guillemotright Y -56
+KPX AE hyphen 19
+KPX Lslash quoteright -167
+KPX Lslash T -83
+KPX Lslash U -19
+KPX Lslash V -120
+KPX Lslash W -88
+KPX Lslash Y -102
+KPX Lslash quoteleft -74
+KPX Lslash a 19
+KPX Lslash e 19
+KPX Lslash o 19
+KPX Lslash y -56
+KPX Lslash quotedblleft -74
+KPX Lslash quotesinglbase 19
+KPX Lslash quotedblbase 19
+KPX Lslash quotedblright -167
+KPX Lslash ae 19
+KPX Lslash oslash 19
+KPX Lslash oe 19
+KPX Oslash comma -60
+KPX Oslash hyphen 37
+KPX Oslash period -60
+KPX Oslash V -19
+KPX Oslash X -19
+KPX Aring quoteright -130
+KPX Aring T -111
+KPX Aring U -23
+KPX Aring V -56
+KPX Aring W -42
+KPX Aring Y -42
+KPX Aring f -19
+KPX Aring t -19
+KPX Aring v -32
+KPX Aring w -46
+KPX Aring y -23
+KPX Aring fi -19
+KPX Aring fl -19
+KPX Aring quotedblright -130
+KPX Eth hyphen 37
+KPX Eth A -19
+KPX Eth V -19
+KPX Eth Y -19
+KPX Eth Aring -19
+EndKernPairs
+StartTrackKern 3
+TrackKern -1 6 0.10 144 -2.09
+TrackKern -2 6 0.05 144 -4.02
+TrackKern -3 6 0.00 144 -5.96
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0632bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0632bt_.pfb
new file mode 100644
index 00000000..07011728
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0632bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0633bt_.afm b/e2e-tests/cypress/fonts/Type1/c0633bt_.afm
new file mode 100644
index 00000000..d16eb097
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0633bt_.afm
@@ -0,0 +1,645 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Transitional 801
+Comment bitsFontID 0633
+Comment bitsManufacturingDate Tue Nov 6 02:16:48 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530633
+FontName CharterBT-BoldItalic
+FullName Bitstream Charter Bold Italic
+FamilyName Bitstream Charter
+Weight Bold
+ItalicAngle 11.0000
+IsFixedPitch false
+FontBBox -190 -237 1243 972
+UnderlinePosition -109
+UnderlineThickness 91
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 672
+XHeight 495
+Ascender 736
+Descender -218
+StartCharMetrics 228
+C 32 ; WX 293 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 340 ; N exclam ; B 43 -8 295 685 ;
+C 34 ; WX 339 ; N quotedbl ; B 40 418 299 715 ;
+C 35 ; WX 751 ; N numbersign ; B 56 -24 689 710 ;
+C 36 ; WX 586 ; N dollar ; B 24 -105 544 745 ;
+C 37 ; WX 898 ; N percent ; B 54 -12 846 683 ;
+C 38 ; WX 730 ; N ampersand ; B 19 -13 698 685 ;
+C 39 ; WX 261 ; N quoteright ; B 63 395 269 699 ;
+C 40 ; WX 420 ; N parenleft ; B 66 -142 459 718 ;
+C 41 ; WX 420 ; N parenright ; B -62 -142 331 718 ;
+C 42 ; WX 500 ; N asterisk ; B 96 338 490 718 ;
+C 43 ; WX 833 ; N plus ; B 124 0 710 597 ;
+C 44 ; WX 292 ; N comma ; B -57 -176 168 130 ;
+C 45 ; WX 320 ; N hyphen ; B 9 191 282 291 ;
+C 46 ; WX 294 ; N period ; B 22 -6 175 149 ;
+C 47 ; WX 481 ; N slash ; B -119 -93 525 672 ;
+C 48 ; WX 586 ; N zero ; B 29 -13 556 684 ;
+C 49 ; WX 586 ; N one ; B 79 0 438 681 ;
+C 50 ; WX 586 ; N two ; B -4 0 531 683 ;
+C 51 ; WX 586 ; N three ; B 11 -11 540 683 ;
+C 52 ; WX 586 ; N four ; B -2 -33 551 677 ;
+C 53 ; WX 586 ; N five ; B 19 -10 552 672 ;
+C 54 ; WX 586 ; N six ; B 31 -12 532 719 ;
+C 55 ; WX 586 ; N seven ; B 74 -34 612 672 ;
+C 56 ; WX 586 ; N eight ; B 24 -18 549 686 ;
+C 57 ; WX 586 ; N nine ; B 47 -58 558 684 ;
+C 58 ; WX 346 ; N colon ; B 49 -6 263 487 ;
+C 59 ; WX 346 ; N semicolon ; B -25 -176 263 487 ;
+C 60 ; WX 833 ; N less ; B 128 22 704 574 ;
+C 61 ; WX 833 ; N equal ; B 124 156 710 440 ;
+C 62 ; WX 833 ; N greater ; B 129 22 704 574 ;
+C 63 ; WX 492 ; N question ; B 74 -8 471 684 ;
+C 64 ; WX 936 ; N at ; B 76 -154 871 693 ;
+C 65 ; WX 634 ; N A ; B -68 0 613 678 ;
+C 66 ; WX 628 ; N B ; B -24 0 580 672 ;
+C 67 ; WX 625 ; N C ; B 36 -13 637 684 ;
+C 68 ; WX 702 ; N D ; B -26 0 661 672 ;
+C 69 ; WX 581 ; N E ; B -22 0 580 672 ;
+C 70 ; WX 539 ; N F ; B -23 0 570 671 ;
+C 71 ; WX 693 ; N G ; B 38 -12 671 685 ;
+C 72 ; WX 747 ; N H ; B -25 0 768 672 ;
+C 73 ; WX 353 ; N I ; B -21 0 370 672 ;
+C 74 ; WX 474 ; N J ; B -46 -14 497 672 ;
+C 75 ; WX 653 ; N K ; B -26 -7 695 672 ;
+C 76 ; WX 529 ; N L ; B -26 0 489 672 ;
+C 77 ; WX 894 ; N M ; B -25 0 913 672 ;
+C 78 ; WX 712 ; N N ; B -27 0 744 672 ;
+C 79 ; WX 729 ; N O ; B 37 -14 690 684 ;
+C 80 ; WX 581 ; N P ; B -24 0 583 672 ;
+C 81 ; WX 729 ; N Q ; B 36 -165 705 684 ;
+C 82 ; WX 645 ; N R ; B -22 -7 632 671 ;
+C 83 ; WX 553 ; N S ; B 23 -8 509 684 ;
+C 84 ; WX 584 ; N T ; B 37 0 628 672 ;
+C 85 ; WX 701 ; N U ; B 71 -13 735 672 ;
+C 86 ; WX 617 ; N V ; B 26 -3 677 672 ;
+C 87 ; WX 921 ; N W ; B 43 0 963 672 ;
+C 88 ; WX 608 ; N X ; B -66 0 658 672 ;
+C 89 ; WX 586 ; N Y ; B 26 0 656 672 ;
+C 90 ; WX 572 ; N Z ; B -18 0 581 672 ;
+C 91 ; WX 449 ; N bracketleft ; B 51 -133 472 709 ;
+C 92 ; WX 481 ; N backslash ; B 21 -93 505 672 ;
+C 93 ; WX 449 ; N bracketright ; B -43 -133 379 709 ;
+C 94 ; WX 1000 ; N asciicircum ; B 201 437 798 714 ;
+C 95 ; WX 500 ; N underscore ; B 0 -237 500 -152 ;
+C 96 ; WX 261 ; N quoteleft ; B 76 395 282 699 ;
+C 97 ; WX 572 ; N a ; B 18 -9 548 494 ;
+C 98 ; WX 556 ; N b ; B 26 -9 508 736 ;
+C 99 ; WX 437 ; N c ; B 15 -11 410 493 ;
+C 100 ; WX 579 ; N d ; B 21 -9 558 736 ;
+C 101 ; WX 464 ; N e ; B 18 -10 431 491 ;
+C 102 ; WX 325 ; N f ; B -155 -214 447 733 ;
+C 103 ; WX 517 ; N g ; B -31 -218 528 492 ;
+C 104 ; WX 595 ; N h ; B 20 -7 561 736 ;
+C 105 ; WX 318 ; N i ; B 28 -7 294 725 ;
+C 106 ; WX 297 ; N j ; B -146 -215 285 724 ;
+C 107 ; WX 559 ; N k ; B 22 -8 544 736 ;
+C 108 ; WX 307 ; N l ; B 34 -9 280 736 ;
+C 109 ; WX 883 ; N m ; B 32 -7 852 494 ;
+C 110 ; WX 600 ; N n ; B 26 -7 569 494 ;
+C 111 ; WX 550 ; N o ; B 18 -11 501 493 ;
+C 112 ; WX 565 ; N p ; B -64 -218 519 494 ;
+C 113 ; WX 562 ; N q ; B 23 -218 516 496 ;
+C 114 ; WX 449 ; N r ; B 26 0 451 494 ;
+C 115 ; WX 403 ; N s ; B -12 -10 363 494 ;
+C 116 ; WX 366 ; N t ; B 39 -8 370 601 ;
+C 117 ; WX 599 ; N u ; B 28 -10 572 489 ;
+C 118 ; WX 492 ; N v ; B -8 -1 472 495 ;
+C 119 ; WX 768 ; N w ; B -2 0 741 495 ;
+C 120 ; WX 510 ; N x ; B -38 -7 512 495 ;
+C 121 ; WX 494 ; N y ; B -79 -216 514 494 ;
+C 122 ; WX 465 ; N z ; B -20 -14 461 503 ;
+C 123 ; WX 487 ; N braceleft ; B 47 -134 430 705 ;
+C 124 ; WX 500 ; N bar ; B 207 -237 294 764 ;
+C 125 ; WX 487 ; N braceright ; B 58 -134 441 705 ;
+C 126 ; WX 833 ; N asciitilde ; B 86 212 747 384 ;
+C 161 ; WX 340 ; N exclamdown ; B 42 -8 294 685 ;
+C 162 ; WX 586 ; N cent ; B 45 -104 535 610 ;
+C 163 ; WX 586 ; N sterling ; B -12 0 562 677 ;
+C 164 ; WX 167 ; N fraction ; B -190 0 353 672 ;
+C 165 ; WX 601 ; N yen ; B 9 0 652 668 ;
+C 166 ; WX 586 ; N florin ; B -63 -149 586 683 ;
+C 167 ; WX 500 ; N section ; B -11 -142 490 720 ;
+C 168 ; WX 606 ; N currency ; B 36 166 571 699 ;
+C 169 ; WX 175 ; N quotesingle ; B 40 418 135 715 ;
+C 170 ; WX 481 ; N quotedblleft ; B 77 395 504 699 ;
+C 171 ; WX 450 ; N guillemotleft ; B 10 54 429 425 ;
+C 172 ; WX 266 ; N guilsinglleft ; B 10 54 228 425 ;
+C 173 ; WX 266 ; N guilsinglright ; B -13 54 205 425 ;
+C 174 ; WX 621 ; N fi ; B -154 -214 600 735 ;
+C 175 ; WX 629 ; N fl ; B -154 -214 603 736 ;
+C 177 ; WX 500 ; N endash ; B -26 195 498 286 ;
+C 178 ; WX 500 ; N dagger ; B 44 -130 511 718 ;
+C 179 ; WX 500 ; N daggerdbl ; B -25 -132 511 718 ;
+C 180 ; WX 292 ; N periodcentered ; B 70 258 223 413 ;
+C 182 ; WX 492 ; N paragraph ; B 18 -79 467 672 ;
+C 183 ; WX 590 ; N bullet ; B 150 227 439 516 ;
+C 184 ; WX 261 ; N quotesinglbase ; B -54 -174 151 130 ;
+C 185 ; WX 481 ; N quotedblbase ; B -51 -174 375 130 ;
+C 186 ; WX 481 ; N quotedblright ; B 64 395 490 699 ;
+C 187 ; WX 450 ; N guillemotright ; B -13 54 406 425 ;
+C 188 ; WX 1000 ; N ellipsis ; B 41 -6 862 149 ;
+C 189 ; WX 1291 ; N perthousand ; B 54 -12 1243 683 ;
+C 191 ; WX 492 ; N questiondown ; B 6 -8 400 684 ;
+C 193 ; WX 500 ; N grave ; B 164 551 349 742 ;
+C 194 ; WX 500 ; N acute ; B 247 551 506 742 ;
+C 195 ; WX 500 ; N circumflex ; B 137 551 446 742 ;
+C 196 ; WX 500 ; N tilde ; B 132 570 486 723 ;
+C 197 ; WX 500 ; N macron ; B 137 607 482 675 ;
+C 198 ; WX 500 ; N breve ; B 167 567 477 719 ;
+C 199 ; WX 500 ; N dotaccent ; B 237 576 370 713 ;
+C 200 ; WX 500 ; N dieresis ; B 137 578 472 711 ;
+C 202 ; WX 500 ; N ring ; B 199 551 428 780 ;
+C 203 ; WX 500 ; N cedilla ; B 58 -230 279 0 ;
+C 205 ; WX 500 ; N hungarumlaut ; B 150 551 578 742 ;
+C 206 ; WX 500 ; N ogonek ; B 83 -225 243 0 ;
+C 207 ; WX 500 ; N caron ; B 174 551 483 742 ;
+C 208 ; WX 1000 ; N emdash ; B -22 195 1000 286 ;
+C 225 ; WX 894 ; N AE ; B -103 0 896 672 ;
+C 227 ; WX 429 ; N ordfeminine ; B 13 318 411 671 ;
+C 232 ; WX 529 ; N Lslash ; B -26 0 489 672 ;
+C 233 ; WX 729 ; N Oslash ; B 37 -82 691 751 ;
+C 234 ; WX 1003 ; N OE ; B 37 -13 999 684 ;
+C 235 ; WX 413 ; N ordmasculine ; B 13 317 376 671 ;
+C 241 ; WX 719 ; N ae ; B 4 -9 684 494 ;
+C 245 ; WX 318 ; N dotlessi ; B 28 -7 294 494 ;
+C 248 ; WX 307 ; N lslash ; B -17 -9 331 736 ;
+C 249 ; WX 550 ; N oslash ; B 19 -87 501 568 ;
+C 250 ; WX 795 ; N oe ; B 20 -10 759 494 ;
+C 251 ; WX 622 ; N germandbls ; B -161 -214 572 738 ;
+C -1 ; WX 634 ; N Aacute ; B -68 0 613 928 ;
+C -1 ; WX 634 ; N Acircumflex ; B -68 0 613 928 ;
+C -1 ; WX 634 ; N Adieresis ; B -68 0 613 897 ;
+C -1 ; WX 634 ; N Agrave ; B -68 0 613 928 ;
+C -1 ; WX 634 ; N Aring ; B -68 0 613 972 ;
+C -1 ; WX 634 ; N Atilde ; B -68 0 613 909 ;
+C -1 ; WX 625 ; N Ccedilla ; B 36 -230 637 684 ;
+C -1 ; WX 581 ; N Eacute ; B -22 0 580 928 ;
+C -1 ; WX 581 ; N Ecircumflex ; B -22 0 580 928 ;
+C -1 ; WX 581 ; N Edieresis ; B -22 0 580 897 ;
+C -1 ; WX 581 ; N Egrave ; B -22 0 580 928 ;
+C -1 ; WX 353 ; N Iacute ; B -21 0 448 928 ;
+C -1 ; WX 353 ; N Icircumflex ; B -21 0 388 928 ;
+C -1 ; WX 353 ; N Idieresis ; B -21 0 414 897 ;
+C -1 ; WX 353 ; N Igrave ; B -21 0 370 928 ;
+C -1 ; WX 712 ; N Ntilde ; B -27 0 744 909 ;
+C -1 ; WX 729 ; N Oacute ; B 37 -14 690 928 ;
+C -1 ; WX 729 ; N Ocircumflex ; B 37 -14 690 928 ;
+C -1 ; WX 729 ; N Odieresis ; B 37 -14 690 897 ;
+C -1 ; WX 729 ; N Ograve ; B 37 -14 690 928 ;
+C -1 ; WX 729 ; N Otilde ; B 37 -14 690 909 ;
+C -1 ; WX 553 ; N Scaron ; B 23 -8 525 928 ;
+C -1 ; WX 701 ; N Uacute ; B 71 -13 735 928 ;
+C -1 ; WX 701 ; N Ucircumflex ; B 71 -13 735 928 ;
+C -1 ; WX 701 ; N Udieresis ; B 71 -13 735 897 ;
+C -1 ; WX 701 ; N Ugrave ; B 71 -13 735 928 ;
+C -1 ; WX 586 ; N Ydieresis ; B 26 0 656 897 ;
+C -1 ; WX 572 ; N Zcaron ; B -18 0 581 928 ;
+C -1 ; WX 572 ; N aacute ; B 18 -9 548 742 ;
+C -1 ; WX 572 ; N acircumflex ; B 18 -9 548 742 ;
+C -1 ; WX 572 ; N adieresis ; B 18 -9 548 711 ;
+C -1 ; WX 572 ; N agrave ; B 18 -9 548 742 ;
+C -1 ; WX 572 ; N aring ; B 18 -9 548 774 ;
+C -1 ; WX 572 ; N atilde ; B 18 -9 548 723 ;
+C -1 ; WX 437 ; N ccedilla ; B 15 -230 410 493 ;
+C -1 ; WX 464 ; N eacute ; B 18 -10 488 742 ;
+C -1 ; WX 464 ; N ecircumflex ; B 18 -10 431 742 ;
+C -1 ; WX 464 ; N edieresis ; B 18 -10 454 711 ;
+C -1 ; WX 464 ; N egrave ; B 18 -10 431 742 ;
+C -1 ; WX 318 ; N iacute ; B 28 -7 398 742 ;
+C -1 ; WX 318 ; N icircumflex ; B 28 -7 338 742 ;
+C -1 ; WX 318 ; N idieresis ; B 28 -7 364 711 ;
+C -1 ; WX 318 ; N igrave ; B 28 -7 294 742 ;
+C -1 ; WX 600 ; N ntilde ; B 26 -7 569 723 ;
+C -1 ; WX 550 ; N oacute ; B 18 -11 531 742 ;
+C -1 ; WX 550 ; N ocircumflex ; B 18 -11 501 742 ;
+C -1 ; WX 550 ; N odieresis ; B 18 -11 501 711 ;
+C -1 ; WX 550 ; N ograve ; B 18 -11 501 742 ;
+C -1 ; WX 550 ; N otilde ; B 18 -11 511 723 ;
+C -1 ; WX 403 ; N scaron ; B -12 -10 435 742 ;
+C -1 ; WX 599 ; N uacute ; B 28 -10 572 742 ;
+C -1 ; WX 599 ; N ucircumflex ; B 28 -10 572 742 ;
+C -1 ; WX 599 ; N udieresis ; B 28 -10 572 711 ;
+C -1 ; WX 599 ; N ugrave ; B 28 -10 572 742 ;
+C -1 ; WX 494 ; N ydieresis ; B -79 -216 514 711 ;
+C -1 ; WX 465 ; N zcaron ; B -20 -14 466 742 ;
+C -1 ; WX 817 ; N trademark ; B 113 398 724 663 ;
+C -1 ; WX 894 ; N copyright ; B 62 -50 842 730 ;
+C -1 ; WX 833 ; N logicalnot ; B 124 175 710 421 ;
+C -1 ; WX 894 ; N registered ; B 62 -50 842 730 ;
+C -1 ; WX 833 ; N minus ; B 124 256 710 340 ;
+C -1 ; WX 702 ; N Eth ; B -25 0 661 672 ;
+C -1 ; WX 576 ; N Thorn ; B -26 0 553 666 ;
+C -1 ; WX 586 ; N Yacute ; B 26 0 656 928 ;
+C -1 ; WX 500 ; N brokenbar ; B 207 -172 294 699 ;
+C -1 ; WX 329 ; N degree ; B 20 424 309 713 ;
+C -1 ; WX 833 ; N divide ; B 124 45 710 551 ;
+C -1 ; WX 561 ; N eth ; B 25 -12 524 740 ;
+C -1 ; WX 578 ; N mu ; B -53 -206 546 433 ;
+C -1 ; WX 833 ; N multiply ; B 139 16 704 581 ;
+C -1 ; WX 905 ; N onehalf ; B 49 0 871 677 ;
+C -1 ; WX 905 ; N onequarter ; B 49 -19 884 677 ;
+C -1 ; WX 387 ; N onesuperior ; B 52 268 289 677 ;
+C -1 ; WX 833 ; N plusminus ; B 124 7 710 590 ;
+C -1 ; WX 565 ; N thorn ; B -64 -218 519 736 ;
+C -1 ; WX 905 ; N threequarters ; B 6 -19 884 678 ;
+C -1 ; WX 387 ; N threesuperior ; B 7 261 357 679 ;
+C -1 ; WX 387 ; N twosuperior ; B -3 268 351 679 ;
+C -1 ; WX 494 ; N yacute ; B -79 -216 514 742 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 378
+KPX hyphen T -37
+KPX hyphen V -56
+KPX hyphen W -56
+KPX hyphen X -37
+KPX hyphen Y -74
+KPX A quoteright -130
+KPX A colon 19
+KPX A semicolon 19
+KPX A S 19
+KPX A T -37
+KPX A U -23
+KPX A V -56
+KPX A W -42
+KPX A Y -42
+KPX A y -19
+KPX A quotedblright -130
+KPX B hyphen 56
+KPX B S 19
+KPX B V -19
+KPX B W -19
+KPX B Y -19
+KPX C quoteright 37
+KPX C hyphen 23
+KPX C S 19
+KPX C quotedblright 37
+KPX D hyphen 37
+KPX D A -19
+KPX D V -19
+KPX D W -19
+KPX D Y -28
+KPX D Aring -19
+KPX F comma -167
+KPX F hyphen -56
+KPX F period -167
+KPX F colon -37
+KPX F semicolon -37
+KPX F A -32
+KPX F a -60
+KPX F e -65
+KPX F i -19
+KPX F o -46
+KPX F r -19
+KPX F u -19
+KPX F quotesinglbase -37
+KPX F quotedblbase -37
+KPX F ae -60
+KPX F oslash -46
+KPX F oe -46
+KPX F Aring -32
+KPX G hyphen 19
+KPX J A -23
+KPX J Aring -23
+KPX K hyphen -37
+KPX K A -23
+KPX K C -28
+KPX K O -28
+KPX K U -37
+KPX K W -42
+KPX K Y -32
+KPX K a -19
+KPX K e -19
+KPX K o -37
+KPX K u -19
+KPX K y -83
+KPX K quotesinglbase 56
+KPX K quotedblbase 56
+KPX K Oslash -28
+KPX K OE -28
+KPX K ae -19
+KPX K oslash -37
+KPX K oe -37
+KPX K Aring -23
+KPX L quoteright -185
+KPX L hyphen 56
+KPX L A 19
+KPX L O -19
+KPX L T -74
+KPX L U -37
+KPX L V -102
+KPX L W -88
+KPX L Y -88
+KPX L quoteleft -56
+KPX L u -19
+KPX L y -74
+KPX L quotedblleft -56
+KPX L quotesinglbase 19
+KPX L quotedblbase 19
+KPX L quotedblright -185
+KPX L Oslash -19
+KPX L OE -19
+KPX L Aring 19
+KPX O comma -37
+KPX O hyphen 19
+KPX O period -37
+KPX O V -19
+KPX O X -19
+KPX O Y -19
+KPX P comma -250
+KPX P hyphen -56
+KPX P period -250
+KPX P colon -19
+KPX P semicolon -19
+KPX P A -74
+KPX P U -19
+KPX P W -19
+KPX P Y -19
+KPX P a -37
+KPX P e -37
+KPX P i 19
+KPX P n 19
+KPX P o -32
+KPX P r 19
+KPX P u 19
+KPX P y 19
+KPX P quotesinglbase -93
+KPX P quotedblbase -93
+KPX P ae -37
+KPX P oslash -32
+KPX P oe -32
+KPX P Aring -74
+KPX Q quoteright 19
+KPX Q hyphen 19
+KPX Q quotesinglbase 56
+KPX Q quotedblbase 56
+KPX Q quotedblright 19
+KPX R quoteright -37
+KPX R comma 19
+KPX R hyphen -19
+KPX R period 19
+KPX R C -19
+KPX R T -19
+KPX R V -19
+KPX R W -23
+KPX R Y -37
+KPX R quoteleft -19
+KPX R e -19
+KPX R o -19
+KPX R quotedblleft -19
+KPX R quotesinglbase 56
+KPX R quotedblbase 56
+KPX R quotedblright -37
+KPX R oslash -19
+KPX R oe -19
+KPX S A 19
+KPX S G 19
+KPX S O 19
+KPX S Q 19
+KPX S S -19
+KPX S Oslash 19
+KPX S OE 19
+KPX S Aring 19
+KPX T comma -148
+KPX T hyphen -130
+KPX T period -148
+KPX T colon -37
+KPX T semicolon -37
+KPX T A -56
+KPX T T 19
+KPX T a -116
+KPX T c -97
+KPX T e -97
+KPX T i -19
+KPX T o -97
+KPX T r -56
+KPX T s -93
+KPX T u -93
+KPX T w -93
+KPX T y -74
+KPX T guillemotleft -37
+KPX T guilsinglleft -37
+KPX T quotesinglbase -19
+KPX T quotedblbase -19
+KPX T ae -116
+KPX T oslash -97
+KPX T oe -97
+KPX T Aring -56
+KPX U A -28
+KPX U J -19
+KPX U Z -19
+KPX U Aring -28
+KPX V comma -185
+KPX V hyphen -56
+KPX V period -185
+KPX V colon -93
+KPX V semicolon -93
+KPX V A -97
+KPX V O -19
+KPX V a -93
+KPX V e -93
+KPX V i -28
+KPX V o -60
+KPX V u -32
+KPX V y -28
+KPX V quotesinglbase -37
+KPX V quotedblbase -37
+KPX V Oslash -19
+KPX V OE -19
+KPX V ae -93
+KPX V oslash -60
+KPX V oe -60
+KPX V Aring -97
+KPX W comma -134
+KPX W hyphen -37
+KPX W period -134
+KPX W colon -28
+KPX W semicolon -28
+KPX W A -28
+KPX W a -69
+KPX W e -93
+KPX W i -19
+KPX W o -69
+KPX W r -28
+KPX W u -28
+KPX W y -23
+KPX W quotesinglbase -37
+KPX W quotedblbase -37
+KPX W ae -69
+KPX W oslash -69
+KPX W oe -69
+KPX W Aring -28
+KPX X hyphen -19
+KPX X A -19
+KPX X e -37
+KPX X guilsinglright 19
+KPX X quotesinglbase 37
+KPX X quotedblbase 37
+KPX X guillemotright 19
+KPX X Aring -19
+KPX Y quoteright 19
+KPX Y comma -130
+KPX Y hyphen -111
+KPX Y period -130
+KPX Y colon -106
+KPX Y semicolon -106
+KPX Y A -46
+KPX Y a -116
+KPX Y e -116
+KPX Y i -19
+KPX Y o -97
+KPX Y u -56
+KPX Y guillemotleft -37
+KPX Y guilsinglleft -37
+KPX Y quotesinglbase -19
+KPX Y quotedblbase -19
+KPX Y quotedblright 19
+KPX Y ae -116
+KPX Y oslash -97
+KPX Y oe -97
+KPX Y Aring -46
+KPX Z hyphen 37
+KPX quoteleft A -130
+KPX quoteleft J -130
+KPX quoteleft T 19
+KPX quoteleft V 56
+KPX quoteleft W 37
+KPX quoteleft X 37
+KPX quoteleft Y 56
+KPX quoteleft AE -148
+KPX quoteleft Aring -130
+KPX f quoteright 93
+KPX f comma -83
+KPX f hyphen -19
+KPX f period -83
+KPX f quoteleft 37
+KPX f quotedblleft 37
+KPX f quotedblright 93
+KPX r comma -130
+KPX r hyphen -19
+KPX r period -130
+KPX r g -19
+KPX r h -19
+KPX v comma -46
+KPX v hyphen 37
+KPX v period -46
+KPX w comma -56
+KPX w hyphen 19
+KPX w period -56
+KPX y comma -60
+KPX y hyphen 19
+KPX y period -60
+KPX quotedblleft A -130
+KPX quotedblleft J -130
+KPX quotedblleft T 19
+KPX quotedblleft V 56
+KPX quotedblleft W 37
+KPX quotedblleft X 37
+KPX quotedblleft Y 56
+KPX quotedblleft AE -148
+KPX quotedblleft Aring -130
+KPX quotesinglbase A 37
+KPX quotesinglbase C -37
+KPX quotesinglbase D 19
+KPX quotesinglbase F 19
+KPX quotesinglbase G -19
+KPX quotesinglbase H 19
+KPX quotesinglbase J 19
+KPX quotesinglbase T -37
+KPX quotesinglbase V -56
+KPX quotesinglbase W -37
+KPX quotesinglbase X 37
+KPX quotesinglbase Y -37
+KPX quotesinglbase f 37
+KPX quotesinglbase v -37
+KPX quotesinglbase w -37
+KPX quotesinglbase fi 37
+KPX quotesinglbase fl 37
+KPX quotesinglbase AE 74
+KPX quotesinglbase germandbls 37
+KPX quotesinglbase Aring 37
+KPX quotesinglbase Eth 19
+KPX quotedblbase A 37
+KPX quotedblbase C -37
+KPX quotedblbase D 19
+KPX quotedblbase F 19
+KPX quotedblbase G -19
+KPX quotedblbase H 19
+KPX quotedblbase J 19
+KPX quotedblbase T -37
+KPX quotedblbase V -56
+KPX quotedblbase W -37
+KPX quotedblbase X 37
+KPX quotedblbase Y -37
+KPX quotedblbase f 37
+KPX quotedblbase v -37
+KPX quotedblbase w -37
+KPX quotedblbase fi 37
+KPX quotedblbase fl 37
+KPX quotedblbase AE 74
+KPX quotedblbase germandbls 37
+KPX quotedblbase Aring 37
+KPX quotedblbase Eth 19
+KPX AE hyphen 19
+KPX Lslash quoteright -185
+KPX Lslash hyphen 56
+KPX Lslash A 19
+KPX Lslash O -19
+KPX Lslash T -74
+KPX Lslash U -37
+KPX Lslash V -102
+KPX Lslash W -88
+KPX Lslash Y -88
+KPX Lslash quoteleft -56
+KPX Lslash u -19
+KPX Lslash y -74
+KPX Lslash quotedblleft -56
+KPX Lslash quotesinglbase 19
+KPX Lslash quotedblbase 19
+KPX Lslash quotedblright -185
+KPX Lslash Oslash -19
+KPX Lslash OE -19
+KPX Lslash Aring 19
+KPX Oslash comma -37
+KPX Oslash hyphen 19
+KPX Oslash period -37
+KPX Oslash V -19
+KPX Oslash X -19
+KPX Oslash Y -19
+KPX Aring quoteright -130
+KPX Aring colon 19
+KPX Aring semicolon 19
+KPX Aring S 19
+KPX Aring T -37
+KPX Aring U -23
+KPX Aring V -56
+KPX Aring W -42
+KPX Aring Y -42
+KPX Aring y -19
+KPX Aring quotedblright -130
+KPX Eth hyphen 37
+KPX Eth A -19
+KPX Eth V -19
+KPX Eth W -19
+KPX Eth Y -28
+KPX Eth Aring -19
+KPX Thorn quoteright -37
+KPX Thorn comma -148
+KPX Thorn period -148
+KPX Thorn quotedblright -37
+EndKernPairs
+StartTrackKern 3
+TrackKern -1 6 0.10 144 -2.09
+TrackKern -2 6 0.05 144 -4.02
+TrackKern -3 6 0.00 144 -5.96
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0633bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0633bt_.pfb
new file mode 100644
index 00000000..d68f639b
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0633bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0648bt_.afm b/e2e-tests/cypress/fonts/Type1/c0648bt_.afm
new file mode 100644
index 00000000..6d58e7c6
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0648bt_.afm
@@ -0,0 +1,538 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Transitional 801
+Comment bitsFontID 0648
+Comment bitsManufacturingDate Tue Nov 6 02:52:05 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530648
+FontName CharterBT-Roman
+FullName Bitstream Charter
+FamilyName Bitstream Charter
+Weight Normal
+ItalicAngle 0.00
+IsFixedPitch false
+FontBBox -162 -237 1194 963
+UnderlinePosition -109
+UnderlineThickness 61
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 672
+XHeight 482
+Ascender 737
+Descender -218
+StartCharMetrics 228
+C 32 ; WX 278 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 338 ; N exclam ; B 112 -9 226 683 ;
+C 34 ; WX 331 ; N quotedbl ; B 43 421 288 715 ;
+C 35 ; WX 745 ; N numbersign ; B 63 -24 681 710 ;
+C 36 ; WX 556 ; N dollar ; B 57 -102 498 744 ;
+C 37 ; WX 852 ; N percent ; B 30 -12 822 683 ;
+C 38 ; WX 704 ; N ampersand ; B 53 -12 683 683 ;
+C 39 ; WX 201 ; N quoteright ; B 30 442 170 714 ;
+C 40 ; WX 417 ; N parenleft ; B 105 -142 386 718 ;
+C 41 ; WX 417 ; N parenright ; B 31 -142 311 718 ;
+C 42 ; WX 500 ; N asterisk ; B 53 337 447 718 ;
+C 43 ; WX 833 ; N plus ; B 124 0 710 597 ;
+C 44 ; WX 278 ; N comma ; B 39 -169 208 107 ;
+C 45 ; WX 319 ; N hyphen ; B 47 207 272 275 ;
+C 46 ; WX 278 ; N period ; B 75 -10 203 118 ;
+C 47 ; WX 481 ; N slash ; B -29 -93 461 672 ;
+C 48 ; WX 556 ; N zero ; B 40 -12 516 683 ;
+C 49 ; WX 556 ; N one ; B 94 0 460 683 ;
+C 50 ; WX 556 ; N two ; B 42 0 506 684 ;
+C 51 ; WX 556 ; N three ; B 40 -11 492 684 ;
+C 52 ; WX 556 ; N four ; B 26 -38 539 678 ;
+C 53 ; WX 556 ; N five ; B 49 -10 489 672 ;
+C 54 ; WX 556 ; N six ; B 50 -13 526 714 ;
+C 55 ; WX 556 ; N seven ; B 70 -38 532 672 ;
+C 56 ; WX 556 ; N eight ; B 43 -16 507 685 ;
+C 57 ; WX 556 ; N nine ; B 43 -53 512 681 ;
+C 58 ; WX 319 ; N colon ; B 96 -10 224 482 ;
+C 59 ; WX 319 ; N semicolon ; B 64 -169 234 482 ;
+C 60 ; WX 833 ; N less ; B 128 37 704 560 ;
+C 61 ; WX 833 ; N equal ; B 124 175 710 421 ;
+C 62 ; WX 833 ; N greater ; B 129 37 704 560 ;
+C 63 ; WX 486 ; N question ; B 54 -9 410 683 ;
+C 64 ; WX 942 ; N at ; B 76 -154 871 693 ;
+C 65 ; WX 639 ; N A ; B -8 0 651 680 ;
+C 66 ; WX 604 ; N B ; B 32 0 559 672 ;
+C 67 ; WX 632 ; N C ; B 42 -13 588 683 ;
+C 68 ; WX 693 ; N D ; B 32 0 649 672 ;
+C 69 ; WX 576 ; N E ; B 32 0 549 672 ;
+C 70 ; WX 537 ; N F ; B 24 0 506 672 ;
+C 71 ; WX 694 ; N G ; B 42 -13 667 684 ;
+C 72 ; WX 738 ; N H ; B 32 0 706 672 ;
+C 73 ; WX 324 ; N I ; B 35 0 289 672 ;
+C 74 ; WX 444 ; N J ; B 12 -13 440 672 ;
+C 75 ; WX 611 ; N K ; B 32 0 628 672 ;
+C 76 ; WX 520 ; N L ; B 26 0 507 672 ;
+C 77 ; WX 866 ; N M ; B 30 0 835 672 ;
+C 78 ; WX 713 ; N N ; B 26 0 688 672 ;
+C 79 ; WX 731 ; N O ; B 42 -17 689 688 ;
+C 80 ; WX 558 ; N P ; B 24 0 532 672 ;
+C 81 ; WX 731 ; N Q ; B 39 -177 694 689 ;
+C 82 ; WX 646 ; N R ; B 32 -9 657 672 ;
+C 83 ; WX 556 ; N S ; B 60 -12 499 684 ;
+C 84 ; WX 597 ; N T ; B 15 0 582 672 ;
+C 85 ; WX 694 ; N U ; B 24 -12 680 672 ;
+C 86 ; WX 618 ; N V ; B -23 -5 638 672 ;
+C 87 ; WX 928 ; N W ; B 0 0 928 672 ;
+C 88 ; WX 600 ; N X ; B -9 0 610 672 ;
+C 89 ; WX 586 ; N Y ; B -14 0 607 672 ;
+C 90 ; WX 586 ; N Z ; B 45 0 540 672 ;
+C 91 ; WX 421 ; N bracketleft ; B 138 -133 376 709 ;
+C 92 ; WX 481 ; N backslash ; B 12 -93 502 672 ;
+C 93 ; WX 421 ; N bracketright ; B 45 -133 283 709 ;
+C 94 ; WX 1000 ; N asciicircum ; B 201 437 798 714 ;
+C 95 ; WX 500 ; N underscore ; B 0 -237 500 -178 ;
+C 96 ; WX 201 ; N quoteleft ; B 34 441 174 713 ;
+C 97 ; WX 507 ; N a ; B 41 -7 489 492 ;
+C 98 ; WX 539 ; N b ; B 8 0 504 737 ;
+C 99 ; WX 446 ; N c ; B 37 -7 426 491 ;
+C 100 ; WX 565 ; N d ; B 36 -10 531 737 ;
+C 101 ; WX 491 ; N e ; B 37 -10 449 491 ;
+C 102 ; WX 321 ; N f ; B 28 0 381 744 ;
+C 103 ; WX 523 ; N g ; B 39 -219 513 492 ;
+C 104 ; WX 564 ; N h ; B 18 0 547 737 ;
+C 105 ; WX 280 ; N i ; B 34 0 261 709 ;
+C 106 ; WX 266 ; N j ; B -79 -218 204 709 ;
+C 107 ; WX 517 ; N k ; B 18 0 528 737 ;
+C 108 ; WX 282 ; N l ; B 26 0 262 737 ;
+C 109 ; WX 843 ; N m ; B 30 0 826 491 ;
+C 110 ; WX 568 ; N n ; B 30 0 551 491 ;
+C 111 ; WX 539 ; N o ; B 37 -10 503 491 ;
+C 112 ; WX 551 ; N p ; B 23 -218 517 491 ;
+C 113 ; WX 531 ; N q ; B 36 -218 527 492 ;
+C 114 ; WX 382 ; N r ; B 29 0 377 492 ;
+C 115 ; WX 400 ; N s ; B 41 -10 359 492 ;
+C 116 ; WX 334 ; N t ; B 24 -4 323 575 ;
+C 117 ; WX 569 ; N u ; B 26 -10 542 491 ;
+C 118 ; WX 494 ; N v ; B -6 0 508 482 ;
+C 119 ; WX 771 ; N w ; B -3 0 772 482 ;
+C 120 ; WX 503 ; N x ; B 12 0 501 482 ;
+C 121 ; WX 495 ; N y ; B -2 -218 512 482 ;
+C 122 ; WX 468 ; N z ; B 45 0 431 482 ;
+C 123 ; WX 486 ; N braceleft ; B 64 -135 418 703 ;
+C 124 ; WX 500 ; N bar ; B 219 -237 282 764 ;
+C 125 ; WX 486 ; N braceright ; B 64 -135 418 703 ;
+C 126 ; WX 833 ; N asciitilde ; B 86 225 747 371 ;
+C 161 ; WX 338 ; N exclamdown ; B 112 -9 226 683 ;
+C 162 ; WX 556 ; N cent ; B 64 -98 464 602 ;
+C 163 ; WX 556 ; N sterling ; B 37 0 509 683 ;
+C 164 ; WX 167 ; N fraction ; B -162 0 328 672 ;
+C 165 ; WX 556 ; N yen ; B -6 0 560 672 ;
+C 166 ; WX 556 ; N florin ; B 6 -169 507 683 ;
+C 167 ; WX 500 ; N section ; B 62 -141 437 718 ;
+C 168 ; WX 606 ; N currency ; B 41 171 566 694 ;
+C 169 ; WX 170 ; N quotesingle ; B 43 421 127 715 ;
+C 170 ; WX 403 ; N quotedblleft ; B 34 441 376 713 ;
+C 171 ; WX 442 ; N guillemotleft ; B 39 67 401 413 ;
+C 172 ; WX 245 ; N guilsinglleft ; B 39 67 204 413 ;
+C 173 ; WX 245 ; N guilsinglright ; B 45 67 210 413 ;
+C 174 ; WX 574 ; N fi ; B 28 0 544 744 ;
+C 175 ; WX 579 ; N fl ; B 28 0 561 744 ;
+C 177 ; WX 500 ; N endash ; B 0 210 500 271 ;
+C 178 ; WX 500 ; N dagger ; B 17 -130 484 718 ;
+C 179 ; WX 500 ; N daggerdbl ; B 17 -132 484 718 ;
+C 180 ; WX 278 ; N periodcentered ; B 75 271 203 400 ;
+C 182 ; WX 484 ; N paragraph ; B 25 -79 459 672 ;
+C 183 ; WX 590 ; N bullet ; B 150 227 439 516 ;
+C 184 ; WX 201 ; N quotesinglbase ; B 31 -165 171 107 ;
+C 185 ; WX 403 ; N quotedblbase ; B 31 -165 372 107 ;
+C 186 ; WX 403 ; N quotedblright ; B 30 442 371 714 ;
+C 187 ; WX 442 ; N guillemotright ; B 45 67 407 413 ;
+C 188 ; WX 1000 ; N ellipsis ; B 102 -10 898 118 ;
+C 189 ; WX 1225 ; N perthousand ; B 30 -12 1194 683 ;
+C 191 ; WX 486 ; N questiondown ; B 60 -9 415 683 ;
+C 193 ; WX 500 ; N grave ; B 104 546 300 737 ;
+C 194 ; WX 500 ; N acute ; B 212 546 409 737 ;
+C 195 ; WX 500 ; N circumflex ; B 107 546 393 737 ;
+C 196 ; WX 500 ; N tilde ; B 97 572 403 709 ;
+C 197 ; WX 500 ; N macron ; B 101 607 403 668 ;
+C 198 ; WX 500 ; N breve ; B 108 567 392 709 ;
+C 199 ; WX 500 ; N dotaccent ; B 196 589 304 697 ;
+C 200 ; WX 500 ; N dieresis ; B 106 589 394 691 ;
+C 202 ; WX 500 ; N ring ; B 132 546 368 782 ;
+C 203 ; WX 500 ; N cedilla ; B 179 -224 368 0 ;
+C 205 ; WX 500 ; N hungarumlaut ; B 133 546 473 737 ;
+C 206 ; WX 500 ; N ogonek ; B 182 -217 330 0 ;
+C 207 ; WX 500 ; N caron ; B 107 545 393 736 ;
+C 208 ; WX 1000 ; N emdash ; B 0 210 1000 271 ;
+C 225 ; WX 866 ; N AE ; B -57 0 838 672 ;
+C 227 ; WX 380 ; N ordfeminine ; B 30 329 367 679 ;
+C 232 ; WX 520 ; N Lslash ; B 10 0 507 672 ;
+C 233 ; WX 731 ; N Oslash ; B 42 -78 689 748 ;
+C 234 ; WX 993 ; N OE ; B 42 -8 965 680 ;
+C 235 ; WX 404 ; N ordmasculine ; B 27 327 378 678 ;
+C 241 ; WX 725 ; N ae ; B 43 -10 683 492 ;
+C 245 ; WX 280 ; N dotlessi ; B 34 0 261 487 ;
+C 248 ; WX 282 ; N lslash ; B 4 0 300 737 ;
+C 249 ; WX 539 ; N oslash ; B 37 -81 503 560 ;
+C 250 ; WX 817 ; N oe ; B 36 -10 776 491 ;
+C 251 ; WX 609 ; N germandbls ; B 18 -7 581 741 ;
+C -1 ; WX 639 ; N Aacute ; B -8 0 651 934 ;
+C -1 ; WX 639 ; N Acircumflex ; B -8 0 651 934 ;
+C -1 ; WX 639 ; N Adieresis ; B -8 0 651 888 ;
+C -1 ; WX 639 ; N Agrave ; B -8 0 651 934 ;
+C -1 ; WX 639 ; N Aring ; B -8 0 651 963 ;
+C -1 ; WX 639 ; N Atilde ; B -8 0 651 906 ;
+C -1 ; WX 632 ; N Ccedilla ; B 42 -224 588 683 ;
+C -1 ; WX 576 ; N Eacute ; B 32 0 549 934 ;
+C -1 ; WX 576 ; N Ecircumflex ; B 32 0 549 934 ;
+C -1 ; WX 576 ; N Edieresis ; B 32 0 549 888 ;
+C -1 ; WX 576 ; N Egrave ; B 32 0 549 934 ;
+C -1 ; WX 324 ; N Iacute ; B 35 0 321 934 ;
+C -1 ; WX 324 ; N Icircumflex ; B 19 0 305 934 ;
+C -1 ; WX 324 ; N Idieresis ; B 18 0 306 888 ;
+C -1 ; WX 324 ; N Igrave ; B 16 0 289 934 ;
+C -1 ; WX 713 ; N Ntilde ; B 26 0 688 906 ;
+C -1 ; WX 731 ; N Oacute ; B 42 -17 689 934 ;
+C -1 ; WX 731 ; N Ocircumflex ; B 42 -17 689 934 ;
+C -1 ; WX 731 ; N Odieresis ; B 42 -17 689 888 ;
+C -1 ; WX 731 ; N Ograve ; B 42 -17 689 934 ;
+C -1 ; WX 731 ; N Otilde ; B 42 -17 689 906 ;
+C -1 ; WX 556 ; N Scaron ; B 60 -12 499 933 ;
+C -1 ; WX 694 ; N Uacute ; B 24 -12 680 934 ;
+C -1 ; WX 694 ; N Ucircumflex ; B 24 -12 680 934 ;
+C -1 ; WX 694 ; N Udieresis ; B 24 -12 680 888 ;
+C -1 ; WX 694 ; N Ugrave ; B 24 -12 680 934 ;
+C -1 ; WX 586 ; N Ydieresis ; B -14 0 607 888 ;
+C -1 ; WX 586 ; N Zcaron ; B 45 0 540 933 ;
+C -1 ; WX 507 ; N aacute ; B 41 -7 489 737 ;
+C -1 ; WX 507 ; N acircumflex ; B 41 -7 489 737 ;
+C -1 ; WX 507 ; N adieresis ; B 41 -7 489 691 ;
+C -1 ; WX 507 ; N agrave ; B 41 -7 489 737 ;
+C -1 ; WX 507 ; N aring ; B 41 -7 489 782 ;
+C -1 ; WX 507 ; N atilde ; B 41 -7 489 709 ;
+C -1 ; WX 446 ; N ccedilla ; B 37 -224 426 491 ;
+C -1 ; WX 491 ; N eacute ; B 37 -10 449 737 ;
+C -1 ; WX 491 ; N ecircumflex ; B 37 -10 449 737 ;
+C -1 ; WX 491 ; N edieresis ; B 37 -10 449 691 ;
+C -1 ; WX 491 ; N egrave ; B 37 -10 449 737 ;
+C -1 ; WX 280 ; N iacute ; B 34 0 299 737 ;
+C -1 ; WX 280 ; N icircumflex ; B -3 0 283 737 ;
+C -1 ; WX 280 ; N idieresis ; B -4 0 284 691 ;
+C -1 ; WX 280 ; N igrave ; B -6 0 261 737 ;
+C -1 ; WX 568 ; N ntilde ; B 30 0 551 709 ;
+C -1 ; WX 539 ; N oacute ; B 37 -10 503 737 ;
+C -1 ; WX 539 ; N ocircumflex ; B 37 -10 503 737 ;
+C -1 ; WX 539 ; N odieresis ; B 37 -10 503 691 ;
+C -1 ; WX 539 ; N ograve ; B 37 -10 503 737 ;
+C -1 ; WX 539 ; N otilde ; B 37 -10 503 709 ;
+C -1 ; WX 400 ; N scaron ; B 41 -10 359 736 ;
+C -1 ; WX 569 ; N uacute ; B 26 -10 542 737 ;
+C -1 ; WX 569 ; N ucircumflex ; B 26 -10 542 737 ;
+C -1 ; WX 569 ; N udieresis ; B 26 -10 542 691 ;
+C -1 ; WX 569 ; N ugrave ; B 26 -10 542 737 ;
+C -1 ; WX 495 ; N ydieresis ; B -2 -218 512 691 ;
+C -1 ; WX 468 ; N zcaron ; B 45 0 431 736 ;
+C -1 ; WX 822 ; N trademark ; B 118 398 716 663 ;
+C -1 ; WX 900 ; N copyright ; B 66 -46 838 726 ;
+C -1 ; WX 833 ; N logicalnot ; B 124 174 710 419 ;
+C -1 ; WX 900 ; N registered ; B 66 -46 838 726 ;
+C -1 ; WX 833 ; N minus ; B 124 269 710 328 ;
+C -1 ; WX 693 ; N Eth ; B 14 0 649 672 ;
+C -1 ; WX 558 ; N Thorn ; B 35 0 534 672 ;
+C -1 ; WX 586 ; N Yacute ; B -14 0 607 934 ;
+C -1 ; WX 500 ; N brokenbar ; B 219 -172 282 699 ;
+C -1 ; WX 329 ; N degree ; B 26 434 303 710 ;
+C -1 ; WX 833 ; N divide ; B 124 66 710 531 ;
+C -1 ; WX 528 ; N eth ; B 33 -11 493 734 ;
+C -1 ; WX 547 ; N mu ; B -39 -204 532 433 ;
+C -1 ; WX 833 ; N multiply ; B 146 26 691 571 ;
+C -1 ; WX 867 ; N onehalf ; B 59 0 836 678 ;
+C -1 ; WX 867 ; N onequarter ; B 59 -22 857 678 ;
+C -1 ; WX 367 ; N onesuperior ; B 62 268 304 679 ;
+C -1 ; WX 833 ; N plusminus ; B 124 20 710 577 ;
+C -1 ; WX 551 ; N thorn ; B 20 -218 521 737 ;
+C -1 ; WX 868 ; N threequarters ; B 25 -22 857 679 ;
+C -1 ; WX 367 ; N threesuperior ; B 26 261 325 679 ;
+C -1 ; WX 367 ; N twosuperior ; B 27 268 334 679 ;
+C -1 ; WX 495 ; N yacute ; B -2 -218 512 737 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 271
+KPX hyphen T -37
+KPX hyphen V -56
+KPX hyphen W -56
+KPX hyphen X -37
+KPX hyphen Y -74
+KPX A quoteright -130
+KPX A T -111
+KPX A U -23
+KPX A V -56
+KPX A W -42
+KPX A Y -42
+KPX A f -19
+KPX A t -19
+KPX A v -32
+KPX A w -46
+KPX A y -23
+KPX A fi -19
+KPX A fl -19
+KPX A quotedblright -130
+KPX B hyphen 37
+KPX B C 19
+KPX B G 19
+KPX B O 19
+KPX B S 19
+KPX B V -37
+KPX B W -19
+KPX B Y -19
+KPX B Oslash 19
+KPX B OE 19
+KPX C quoteright 37
+KPX C hyphen 23
+KPX C A -19
+KPX C S 19
+KPX C quotedblright 37
+KPX C Aring -19
+KPX D hyphen 37
+KPX D A -19
+KPX D V -19
+KPX D Y -19
+KPX D Aring -19
+KPX F comma -190
+KPX F hyphen -93
+KPX F period -190
+KPX F colon -37
+KPX F semicolon -37
+KPX F A -97
+KPX F a -79
+KPX F e -65
+KPX F o -65
+KPX F ae -79
+KPX F oslash -65
+KPX F oe -65
+KPX F Aring -97
+KPX G hyphen 19
+KPX G T -19
+KPX G W -19
+KPX G Y -23
+KPX J A -37
+KPX J Aring -37
+KPX K hyphen -37
+KPX K A -23
+KPX K C -28
+KPX K O -28
+KPX K U -37
+KPX K W -37
+KPX K Y -28
+KPX K e -19
+KPX K o -19
+KPX K u -19
+KPX K y -28
+KPX K Oslash -28
+KPX K OE -28
+KPX K oslash -19
+KPX K oe -19
+KPX K Aring -23
+KPX L quoteright -241
+KPX L T -83
+KPX L U -19
+KPX L V -120
+KPX L W -88
+KPX L Y -102
+KPX L y -19
+KPX L quotedblright -241
+KPX O comma -60
+KPX O hyphen 37
+KPX O period -60
+KPX O V -19
+KPX O X -19
+KPX P comma -259
+KPX P hyphen -93
+KPX P period -259
+KPX P colon -37
+KPX P semicolon -37
+KPX P A -93
+KPX P U -19
+KPX P a -56
+KPX P e -56
+KPX P o -51
+KPX P s -32
+KPX P ae -56
+KPX P oslash -51
+KPX P oe -51
+KPX P Aring -93
+KPX Q quoteright 19
+KPX Q hyphen 37
+KPX Q quotedblright 19
+KPX R quoteright -37
+KPX R colon -19
+KPX R semicolon -19
+KPX R T -37
+KPX R V -56
+KPX R W -42
+KPX R Y -51
+KPX R quoteleft -37
+KPX R e -37
+KPX R o -37
+KPX R u -37
+KPX R y -46
+KPX R quotedblleft -37
+KPX R quotesinglbase 37
+KPX R quotedblbase 37
+KPX R quotedblright -37
+KPX R oslash -37
+KPX R oe -37
+KPX T quoteright 19
+KPX T comma -148
+KPX T hyphen -130
+KPX T period -148
+KPX T colon -37
+KPX T semicolon -37
+KPX T A -111
+KPX T T 19
+KPX T quoteleft 37
+KPX T a -97
+KPX T c -97
+KPX T e -97
+KPX T i -19
+KPX T o -97
+KPX T r -74
+KPX T s -74
+KPX T u -111
+KPX T w -74
+KPX T y -93
+KPX T quotedblleft 37
+KPX T guillemotleft -37
+KPX T guilsinglleft -37
+KPX T quotedblright 19
+KPX T ae -97
+KPX T oslash -97
+KPX T oe -97
+KPX T Aring -111
+KPX U A -32
+KPX U J -28
+KPX U Aring -32
+KPX V quoteright 37
+KPX V comma -222
+KPX V hyphen -93
+KPX V period -222
+KPX V colon -102
+KPX V semicolon -102
+KPX V A -79
+KPX V O -19
+KPX V a -111
+KPX V e -106
+KPX V i -28
+KPX V o -93
+KPX V u -65
+KPX V y -65
+KPX V quotedblright 37
+KPX V Oslash -19
+KPX V OE -19
+KPX V ae -111
+KPX V oslash -93
+KPX V oe -93
+KPX V Aring -79
+KPX W quoteright 19
+KPX W comma -176
+KPX W hyphen -74
+KPX W period -176
+KPX W colon -88
+KPX W semicolon -88
+KPX W A -60
+KPX W a -69
+KPX W e -83
+KPX W i -19
+KPX W o -69
+KPX W r -46
+KPX W u -42
+KPX W y -23
+KPX W quotedblright 19
+KPX W ae -69
+KPX W oslash -69
+KPX W oe -69
+KPX W Aring -60
+KPX X hyphen -37
+KPX X A -19
+KPX X C -19
+KPX X O -19
+KPX X Oslash -19
+KPX X OE -19
+KPX X Aring -19
+KPX Y comma -130
+KPX Y hyphen -130
+KPX Y period -130
+KPX Y colon -125
+KPX Y semicolon -125
+KPX Y A -60
+KPX Y C -19
+KPX Y a -97
+KPX Y e -106
+KPX Y i -37
+KPX Y o -97
+KPX Y u -69
+KPX Y ae -97
+KPX Y oslash -97
+KPX Y oe -97
+KPX Y Aring -60
+KPX quoteleft A -130
+KPX quoteleft J -167
+KPX quoteleft AE -111
+KPX quoteleft Aring -130
+KPX f quoteright 74
+KPX f comma -37
+KPX f hyphen -19
+KPX f period -37
+KPX f quotedblright 74
+KPX r comma -111
+KPX r period -111
+KPX v comma -120
+KPX v period -120
+KPX w comma -120
+KPX w period -120
+KPX y comma -134
+KPX y period -134
+KPX quotedblleft A -130
+KPX quotedblleft J -167
+KPX quotedblleft AE -111
+KPX quotedblleft Aring -130
+KPX AE hyphen 19
+KPX Lslash quoteright -241
+KPX Lslash T -83
+KPX Lslash U -19
+KPX Lslash V -120
+KPX Lslash W -88
+KPX Lslash Y -102
+KPX Lslash y -19
+KPX Lslash quotedblright -241
+KPX Oslash comma -60
+KPX Oslash hyphen 37
+KPX Oslash period -60
+KPX Oslash V -19
+KPX Oslash X -19
+KPX Aring quoteright -130
+KPX Aring T -111
+KPX Aring U -23
+KPX Aring V -56
+KPX Aring W -42
+KPX Aring Y -42
+KPX Aring f -19
+KPX Aring t -19
+KPX Aring v -32
+KPX Aring w -46
+KPX Aring y -23
+KPX Aring fi -19
+KPX Aring fl -19
+KPX Aring quotedblright -130
+KPX Eth hyphen 37
+KPX Eth A -19
+KPX Eth V -19
+KPX Eth Y -19
+KPX Eth Aring -19
+EndKernPairs
+StartTrackKern 3
+TrackKern -1 6 0.10 144 -2.09
+TrackKern -2 6 0.05 144 -4.02
+TrackKern -3 6 0.00 144 -5.96
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0648bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0648bt_.pfb
new file mode 100644
index 00000000..72a1606b
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0648bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/c0649bt_.afm b/e2e-tests/cypress/fonts/Type1/c0649bt_.afm
new file mode 100644
index 00000000..0f721845
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/c0649bt_.afm
@@ -0,0 +1,547 @@
+StartFontMetrics 2.0
+Comment Bitstream AFM Data
+Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
+Comment All rights reserved
+Comment Confidential and proprietary to Bitstream Inc.
+Comment Bitstream is a registered trademark of Bitstream Inc.
+Comment bitsClassification Transitional 801
+Comment bitsFontID 0649
+Comment bitsManufacturingDate Tue Nov 6 02:55:16 1990
+Comment bitsLayoutName clayout.adobe.text228.new
+Comment UniqueID 15530649
+FontName CharterBT-Italic
+FullName Bitstream Charter Italic
+FamilyName Bitstream Charter
+Weight Normal
+ItalicAngle 11.0000
+IsFixedPitch false
+FontBBox -226 -237 1175 980
+UnderlinePosition -109
+UnderlineThickness 61
+Version 1.0 [UFO]
+Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
+EncodingScheme AdobeStandardEncoding
+CapHeight 672
+XHeight 486
+Ascender 737
+Descender -218
+StartCharMetrics 228
+C 32 ; WX 278 ; N space ; B 0 0 0 0 ;
+C 33 ; WX 338 ; N exclam ; B 63 -9 281 683 ;
+C 34 ; WX 331 ; N quotedbl ; B 43 421 288 715 ;
+C 35 ; WX 745 ; N numbersign ; B 63 -24 681 710 ;
+C 36 ; WX 556 ; N dollar ; B 21 -102 514 744 ;
+C 37 ; WX 852 ; N percent ; B 49 -12 802 683 ;
+C 38 ; WX 704 ; N ampersand ; B 19 -12 665 684 ;
+C 39 ; WX 201 ; N quoteright ; B 51 442 227 714 ;
+C 40 ; WX 419 ; N parenleft ; B 79 -142 460 718 ;
+C 41 ; WX 419 ; N parenright ; B -67 -142 313 718 ;
+C 42 ; WX 500 ; N asterisk ; B 98 337 492 718 ;
+C 43 ; WX 833 ; N plus ; B 124 0 710 597 ;
+C 44 ; WX 278 ; N comma ; B -58 -169 149 107 ;
+C 45 ; WX 319 ; N hyphen ; B 22 207 260 275 ;
+C 46 ; WX 278 ; N period ; B 25 -6 145 114 ;
+C 47 ; WX 481 ; N slash ; B -111 -93 525 672 ;
+C 48 ; WX 556 ; N zero ; B 27 -12 528 683 ;
+C 49 ; WX 556 ; N one ; B 82 0 405 683 ;
+C 50 ; WX 556 ; N two ; B -22 0 518 684 ;
+C 51 ; WX 556 ; N three ; B 0 -12 512 684 ;
+C 52 ; WX 556 ; N four ; B -1 -38 524 678 ;
+C 53 ; WX 556 ; N five ; B 3 -13 519 672 ;
+C 54 ; WX 556 ; N six ; B 31 -13 501 716 ;
+C 55 ; WX 556 ; N seven ; B 44 -38 596 672 ;
+C 56 ; WX 556 ; N eight ; B 18 -18 518 685 ;
+C 57 ; WX 556 ; N nine ; B 28 -56 523 684 ;
+C 58 ; WX 319 ; N colon ; B 46 -6 235 478 ;
+C 59 ; WX 319 ; N semicolon ; B -33 -169 235 478 ;
+C 60 ; WX 833 ; N less ; B 128 37 704 560 ;
+C 61 ; WX 833 ; N equal ; B 124 175 710 421 ;
+C 62 ; WX 833 ; N greater ; B 129 37 704 560 ;
+C 63 ; WX 486 ; N question ; B 94 -9 446 683 ;
+C 64 ; WX 942 ; N at ; B 76 -154 871 693 ;
+C 65 ; WX 606 ; N A ; B -79 0 585 677 ;
+C 66 ; WX 588 ; N B ; B -29 0 543 672 ;
+C 67 ; WX 604 ; N C ; B 41 -12 622 683 ;
+C 68 ; WX 671 ; N D ; B -28 0 623 672 ;
+C 69 ; WX 546 ; N E ; B -25 0 554 672 ;
+C 70 ; WX 509 ; N F ; B -27 0 540 671 ;
+C 71 ; WX 664 ; N G ; B 39 -12 650 684 ;
+C 72 ; WX 712 ; N H ; B -29 0 741 672 ;
+C 73 ; WX 312 ; N I ; B -23 0 333 672 ;
+C 74 ; WX 447 ; N J ; B -43 -12 472 672 ;
+C 75 ; WX 625 ; N K ; B -30 -5 660 672 ;
+C 76 ; WX 498 ; N L ; B -29 0 453 672 ;
+C 77 ; WX 839 ; N M ; B -28 0 868 672 ;
+C 78 ; WX 683 ; N N ; B -31 0 720 672 ;
+C 79 ; WX 708 ; N O ; B 40 -13 669 683 ;
+C 80 ; WX 542 ; N P ; B -29 0 543 672 ;
+C 81 ; WX 708 ; N Q ; B 40 -160 700 682 ;
+C 82 ; WX 602 ; N R ; B -30 -6 591 671 ;
+C 83 ; WX 537 ; N S ; B 9 -13 511 683 ;
+C 84 ; WX 565 ; N T ; B 43 0 610 672 ;
+C 85 ; WX 664 ; N U ; B 64 -12 705 672 ;
+C 86 ; WX 590 ; N V ; B 30 -6 649 672 ;
+C 87 ; WX 898 ; N W ; B 51 0 952 672 ;
+C 88 ; WX 569 ; N X ; B -83 0 633 672 ;
+C 89 ; WX 562 ; N Y ; B 31 0 642 672 ;
+C 90 ; WX 556 ; N Z ; B -26 0 572 672 ;
+C 91 ; WX 421 ; N bracketleft ; B 49 -133 448 709 ;
+C 92 ; WX 481 ; N backslash ; B 34 -93 489 672 ;
+C 93 ; WX 421 ; N bracketright ; B -45 -133 354 709 ;
+C 94 ; WX 1000 ; N asciicircum ; B 201 437 798 714 ;
+C 95 ; WX 500 ; N underscore ; B 0 -237 500 -178 ;
+C 96 ; WX 201 ; N quoteleft ; B 70 441 247 713 ;
+C 97 ; WX 525 ; N a ; B 17 -9 488 483 ;
+C 98 ; WX 507 ; N b ; B 24 -10 453 737 ;
+C 99 ; WX 394 ; N c ; B 14 -10 370 486 ;
+C 100 ; WX 523 ; N d ; B 20 -9 501 737 ;
+C 101 ; WX 424 ; N e ; B 20 -10 378 483 ;
+C 102 ; WX 292 ; N f ; B -151 -216 404 733 ;
+C 103 ; WX 481 ; N g ; B -31 -218 480 483 ;
+C 104 ; WX 551 ; N h ; B 23 -6 505 737 ;
+C 105 ; WX 287 ; N i ; B 32 -7 255 705 ;
+C 106 ; WX 269 ; N j ; B -128 -216 249 701 ;
+C 107 ; WX 514 ; N k ; B 25 -6 494 737 ;
+C 108 ; WX 275 ; N l ; B 35 -10 241 737 ;
+C 109 ; WX 815 ; N m ; B 31 -6 773 483 ;
+C 110 ; WX 556 ; N n ; B 32 -7 515 483 ;
+C 111 ; WX 502 ; N o ; B 21 -9 450 483 ;
+C 112 ; WX 516 ; N p ; B -70 -218 461 483 ;
+C 113 ; WX 512 ; N q ; B 24 -218 463 488 ;
+C 114 ; WX 398 ; N r ; B 27 0 400 482 ;
+C 115 ; WX 370 ; N s ; B -17 -9 324 483 ;
+C 116 ; WX 333 ; N t ; B 43 -7 337 580 ;
+C 117 ; WX 553 ; N u ; B 30 -9 513 483 ;
+C 118 ; WX 454 ; N v ; B -9 -2 435 484 ;
+C 119 ; WX 713 ; N w ; B -1 0 689 485 ;
+C 120 ; WX 477 ; N x ; B -47 -9 495 486 ;
+C 121 ; WX 475 ; N y ; B -113 -218 485 485 ;
+C 122 ; WX 440 ; N z ; B -15 -12 434 490 ;
+C 123 ; WX 486 ; N braceleft ; B 64 -135 418 703 ;
+C 124 ; WX 500 ; N bar ; B 219 -237 282 764 ;
+C 125 ; WX 486 ; N braceright ; B 64 -135 418 703 ;
+C 126 ; WX 833 ; N asciitilde ; B 86 225 747 371 ;
+C 161 ; WX 338 ; N exclamdown ; B 60 -9 278 683 ;
+C 162 ; WX 556 ; N cent ; B 41 -98 492 602 ;
+C 163 ; WX 556 ; N sterling ; B -22 0 547 683 ;
+C 164 ; WX 167 ; N fraction ; B -226 0 392 672 ;
+C 165 ; WX 556 ; N yen ; B 2 0 616 665 ;
+C 166 ; WX 556 ; N florin ; B -81 -169 563 683 ;
+C 167 ; WX 500 ; N section ; B 3 -141 475 718 ;
+C 168 ; WX 606 ; N currency ; B 41 171 566 694 ;
+C 169 ; WX 170 ; N quotesingle ; B 43 421 127 715 ;
+C 170 ; WX 403 ; N quotedblleft ; B 70 441 448 713 ;
+C 171 ; WX 442 ; N guillemotleft ; B 13 67 416 413 ;
+C 172 ; WX 245 ; N guilsinglleft ; B 13 67 218 413 ;
+C 173 ; WX 245 ; N guilsinglright ; B -7 67 199 413 ;
+C 174 ; WX 574 ; N fi ; B -151 -216 547 733 ;
+C 175 ; WX 579 ; N fl ; B -151 -216 544 737 ;
+C 177 ; WX 500 ; N endash ; B -25 210 488 271 ;
+C 178 ; WX 500 ; N dagger ; B 45 -130 512 718 ;
+C 179 ; WX 500 ; N daggerdbl ; B -27 -132 512 718 ;
+C 180 ; WX 278 ; N periodcentered ; B 79 276 199 396 ;
+C 182 ; WX 484 ; N paragraph ; B 25 -79 459 672 ;
+C 183 ; WX 590 ; N bullet ; B 150 227 439 516 ;
+C 184 ; WX 201 ; N quotesinglbase ; B -65 -165 111 107 ;
+C 185 ; WX 403 ; N quotedblbase ; B -65 -165 313 107 ;
+C 186 ; WX 403 ; N quotedblright ; B 51 442 429 714 ;
+C 187 ; WX 442 ; N guillemotright ; B -7 67 397 413 ;
+C 188 ; WX 1000 ; N ellipsis ; B 52 -6 840 114 ;
+C 189 ; WX 1225 ; N perthousand ; B 49 -12 1175 683 ;
+C 191 ; WX 486 ; N questiondown ; B 27 -9 379 683 ;
+C 193 ; WX 500 ; N grave ; B 181 546 341 737 ;
+C 194 ; WX 500 ; N acute ; B 252 546 485 737 ;
+C 195 ; WX 500 ; N circumflex ; B 148 546 433 737 ;
+C 196 ; WX 500 ; N tilde ; B 143 572 474 709 ;
+C 197 ; WX 500 ; N macron ; B 154 614 465 667 ;
+C 198 ; WX 500 ; N breve ; B 177 567 464 709 ;
+C 199 ; WX 500 ; N dotaccent ; B 248 584 361 697 ;
+C 200 ; WX 500 ; N dieresis ; B 163 589 452 692 ;
+C 202 ; WX 500 ; N ring ; B 209 557 426 774 ;
+C 203 ; WX 500 ; N cedilla ; B 61 -224 267 0 ;
+C 205 ; WX 500 ; N hungarumlaut ; B 175 546 569 737 ;
+C 206 ; WX 500 ; N ogonek ; B 90 -217 235 0 ;
+C 207 ; WX 500 ; N caron ; B 184 545 470 736 ;
+C 208 ; WX 1000 ; N emdash ; B -19 210 991 271 ;
+C 225 ; WX 873 ; N AE ; B -115 0 879 672 ;
+C 227 ; WX 394 ; N ordfeminine ; B 12 325 366 671 ;
+C 232 ; WX 498 ; N Lslash ; B -29 0 453 672 ;
+C 233 ; WX 708 ; N Oslash ; B 41 -74 669 744 ;
+C 234 ; WX 1007 ; N OE ; B 40 -13 1004 682 ;
+C 235 ; WX 377 ; N ordmasculine ; B 15 325 338 671 ;
+C 241 ; WX 671 ; N ae ; B 1 -10 628 483 ;
+C 245 ; WX 287 ; N dotlessi ; B 32 -7 255 483 ;
+C 248 ; WX 275 ; N lslash ; B -14 -10 293 737 ;
+C 249 ; WX 502 ; N oslash ; B 22 -80 450 548 ;
+C 250 ; WX 750 ; N oe ; B 21 -9 704 483 ;
+C 251 ; WX 574 ; N germandbls ; B -151 -216 522 739 ;
+C -1 ; WX 606 ; N Aacute ; B -79 0 585 930 ;
+C -1 ; WX 606 ; N Acircumflex ; B -79 0 585 930 ;
+C -1 ; WX 606 ; N Adieresis ; B -79 0 585 885 ;
+C -1 ; WX 606 ; N Agrave ; B -79 0 585 930 ;
+C -1 ; WX 606 ; N Aring ; B -79 0 585 980 ;
+C -1 ; WX 606 ; N Atilde ; B -79 0 585 902 ;
+C -1 ; WX 604 ; N Ccedilla ; B 41 -224 622 683 ;
+C -1 ; WX 546 ; N Eacute ; B -25 0 554 930 ;
+C -1 ; WX 546 ; N Ecircumflex ; B -25 0 554 930 ;
+C -1 ; WX 546 ; N Edieresis ; B -25 0 554 885 ;
+C -1 ; WX 546 ; N Egrave ; B -25 0 554 930 ;
+C -1 ; WX 312 ; N Iacute ; B -23 0 418 930 ;
+C -1 ; WX 312 ; N Icircumflex ; B -23 0 366 930 ;
+C -1 ; WX 312 ; N Idieresis ; B -23 0 385 885 ;
+C -1 ; WX 312 ; N Igrave ; B -23 0 333 930 ;
+C -1 ; WX 683 ; N Ntilde ; B -31 0 720 902 ;
+C -1 ; WX 708 ; N Oacute ; B 40 -13 669 930 ;
+C -1 ; WX 708 ; N Ocircumflex ; B 40 -13 669 930 ;
+C -1 ; WX 708 ; N Odieresis ; B 40 -13 669 885 ;
+C -1 ; WX 708 ; N Ograve ; B 40 -13 669 930 ;
+C -1 ; WX 708 ; N Otilde ; B 40 -13 669 902 ;
+C -1 ; WX 537 ; N Scaron ; B 9 -13 516 929 ;
+C -1 ; WX 664 ; N Uacute ; B 64 -12 705 930 ;
+C -1 ; WX 664 ; N Ucircumflex ; B 64 -12 705 930 ;
+C -1 ; WX 664 ; N Udieresis ; B 64 -12 705 885 ;
+C -1 ; WX 664 ; N Ugrave ; B 64 -12 705 930 ;
+C -1 ; WX 562 ; N Ydieresis ; B 31 0 642 885 ;
+C -1 ; WX 556 ; N Zcaron ; B -26 0 572 929 ;
+C -1 ; WX 525 ; N aacute ; B 17 -9 498 737 ;
+C -1 ; WX 525 ; N acircumflex ; B 17 -9 488 737 ;
+C -1 ; WX 525 ; N adieresis ; B 17 -9 488 692 ;
+C -1 ; WX 525 ; N agrave ; B 17 -9 488 737 ;
+C -1 ; WX 525 ; N aring ; B 17 -9 488 762 ;
+C -1 ; WX 525 ; N atilde ; B 17 -9 488 709 ;
+C -1 ; WX 394 ; N ccedilla ; B 8 -224 370 486 ;
+C -1 ; WX 424 ; N eacute ; B 20 -10 460 737 ;
+C -1 ; WX 424 ; N ecircumflex ; B 20 -10 408 737 ;
+C -1 ; WX 424 ; N edieresis ; B 20 -10 427 692 ;
+C -1 ; WX 424 ; N egrave ; B 20 -10 378 737 ;
+C -1 ; WX 287 ; N iacute ; B 32 -7 379 737 ;
+C -1 ; WX 287 ; N icircumflex ; B 32 -7 327 737 ;
+C -1 ; WX 287 ; N idieresis ; B 32 -7 346 692 ;
+C -1 ; WX 287 ; N igrave ; B 32 -7 255 737 ;
+C -1 ; WX 556 ; N ntilde ; B 32 -7 515 709 ;
+C -1 ; WX 502 ; N oacute ; B 21 -9 486 737 ;
+C -1 ; WX 502 ; N ocircumflex ; B 21 -9 450 737 ;
+C -1 ; WX 502 ; N odieresis ; B 21 -9 453 692 ;
+C -1 ; WX 502 ; N ograve ; B 21 -9 450 737 ;
+C -1 ; WX 502 ; N otilde ; B 21 -9 475 709 ;
+C -1 ; WX 370 ; N scaron ; B -17 -9 405 736 ;
+C -1 ; WX 553 ; N uacute ; B 30 -9 513 737 ;
+C -1 ; WX 553 ; N ucircumflex ; B 30 -9 513 737 ;
+C -1 ; WX 553 ; N udieresis ; B 30 -9 513 692 ;
+C -1 ; WX 553 ; N ugrave ; B 30 -9 513 737 ;
+C -1 ; WX 475 ; N ydieresis ; B -113 -218 485 692 ;
+C -1 ; WX 440 ; N zcaron ; B -15 -12 440 736 ;
+C -1 ; WX 822 ; N trademark ; B 118 398 716 663 ;
+C -1 ; WX 900 ; N copyright ; B 66 -46 838 726 ;
+C -1 ; WX 833 ; N logicalnot ; B 124 174 710 419 ;
+C -1 ; WX 900 ; N registered ; B 66 -46 838 726 ;
+C -1 ; WX 833 ; N minus ; B 124 269 710 328 ;
+C -1 ; WX 671 ; N Eth ; B -28 0 624 672 ;
+C -1 ; WX 532 ; N Thorn ; B -30 0 518 672 ;
+C -1 ; WX 562 ; N Yacute ; B 31 0 642 930 ;
+C -1 ; WX 500 ; N brokenbar ; B 219 -172 282 699 ;
+C -1 ; WX 329 ; N degree ; B 26 434 303 710 ;
+C -1 ; WX 833 ; N divide ; B 124 66 710 531 ;
+C -1 ; WX 500 ; N eth ; B 21 -9 464 726 ;
+C -1 ; WX 547 ; N mu ; B -39 -204 532 433 ;
+C -1 ; WX 833 ; N multiply ; B 146 26 691 571 ;
+C -1 ; WX 867 ; N onehalf ; B 51 0 844 678 ;
+C -1 ; WX 867 ; N onequarter ; B 51 -22 848 678 ;
+C -1 ; WX 367 ; N onesuperior ; B 54 268 267 679 ;
+C -1 ; WX 833 ; N plusminus ; B 124 20 710 577 ;
+C -1 ; WX 516 ; N thorn ; B -70 -218 461 737 ;
+C -1 ; WX 868 ; N threequarters ; B 0 -22 848 679 ;
+C -1 ; WX 367 ; N threesuperior ; B 0 261 338 679 ;
+C -1 ; WX 367 ; N twosuperior ; B -15 268 342 679 ;
+C -1 ; WX 475 ; N yacute ; B -113 -218 485 737 ;
+EndCharMetrics
+StartKernData
+StartKernPairs 280
+KPX hyphen T -37
+KPX hyphen V -56
+KPX hyphen W -56
+KPX hyphen X -37
+KPX hyphen Y -74
+KPX A quoteright -130
+KPX A colon 19
+KPX A semicolon 19
+KPX A S 19
+KPX A T -37
+KPX A U -23
+KPX A V -56
+KPX A W -42
+KPX A Y -42
+KPX A y -19
+KPX A quotedblright -130
+KPX B hyphen 56
+KPX B S 19
+KPX B V -19
+KPX B W -19
+KPX B Y -19
+KPX C quoteright 37
+KPX C hyphen 23
+KPX C S 19
+KPX C quotedblright 37
+KPX D hyphen 37
+KPX D A -19
+KPX D V -19
+KPX D W -19
+KPX D Y -28
+KPX D Aring -19
+KPX F comma -167
+KPX F hyphen -56
+KPX F period -167
+KPX F colon -37
+KPX F semicolon -37
+KPX F A -32
+KPX F a -42
+KPX F e -46
+KPX F o -46
+KPX F ae -42
+KPX F oslash -46
+KPX F oe -46
+KPX F Aring -32
+KPX G hyphen 19
+KPX J A -23
+KPX J Aring -23
+KPX K hyphen -37
+KPX K A -23
+KPX K C -28
+KPX K O -28
+KPX K U -37
+KPX K W -42
+KPX K Y -32
+KPX K a -19
+KPX K e -56
+KPX K o -56
+KPX K u -56
+KPX K y -83
+KPX K Oslash -28
+KPX K OE -28
+KPX K ae -19
+KPX K oslash -56
+KPX K oe -56
+KPX K Aring -23
+KPX L quoteright -185
+KPX L hyphen 56
+KPX L A 19
+KPX L T -74
+KPX L U -19
+KPX L V -102
+KPX L W -88
+KPX L Y -88
+KPX L u -19
+KPX L y -37
+KPX L quotesinglbase 19
+KPX L quotedblbase 19
+KPX L quotedblright -185
+KPX L Aring 19
+KPX O comma -37
+KPX O hyphen 19
+KPX O period -37
+KPX O V -19
+KPX O X -19
+KPX O Y -19
+KPX P comma -250
+KPX P hyphen -74
+KPX P period -250
+KPX P colon -19
+KPX P semicolon -19
+KPX P A -56
+KPX P U -19
+KPX P W -19
+KPX P Y -19
+KPX P a -37
+KPX P e -56
+KPX P o -51
+KPX P s -32
+KPX P ae -37
+KPX P oslash -51
+KPX P oe -51
+KPX P Aring -56
+KPX Q quoteright 19
+KPX Q hyphen 19
+KPX Q quotedblright 19
+KPX R quoteright -37
+KPX R comma 19
+KPX R hyphen -19
+KPX R period 19
+KPX R T -19
+KPX R V -19
+KPX R W -23
+KPX R Y -37
+KPX R quoteleft -19
+KPX R a -19
+KPX R e -19
+KPX R o -19
+KPX R y -19
+KPX R quotedblleft -19
+KPX R quotedblright -37
+KPX R ae -19
+KPX R oslash -19
+KPX R oe -19
+KPX S A 37
+KPX S G 19
+KPX S O 19
+KPX S Q 19
+KPX S S 19
+KPX S Oslash 19
+KPX S OE 19
+KPX S Aring 37
+KPX T comma -148
+KPX T hyphen -130
+KPX T period -148
+KPX T colon -37
+KPX T semicolon -37
+KPX T A -56
+KPX T T 19
+KPX T a -116
+KPX T c -97
+KPX T e -97
+KPX T i -19
+KPX T o -116
+KPX T r -74
+KPX T s -93
+KPX T u -93
+KPX T w -93
+KPX T y -74
+KPX T ae -116
+KPX T oslash -116
+KPX T oe -116
+KPX T Aring -56
+KPX U A -28
+KPX U J -19
+KPX U Z -19
+KPX U Aring -28
+KPX V comma -185
+KPX V hyphen -56
+KPX V period -185
+KPX V colon -93
+KPX V semicolon -93
+KPX V A -79
+KPX V O -19
+KPX V a -93
+KPX V e -93
+KPX V i -28
+KPX V o -60
+KPX V u -32
+KPX V y -46
+KPX V Oslash -19
+KPX V OE -19
+KPX V ae -93
+KPX V oslash -60
+KPX V oe -60
+KPX V Aring -79
+KPX W comma -134
+KPX W hyphen -37
+KPX W period -134
+KPX W colon -28
+KPX W semicolon -28
+KPX W A -28
+KPX W a -51
+KPX W e -74
+KPX W i -19
+KPX W o -51
+KPX W r -28
+KPX W u -28
+KPX W y -23
+KPX W ae -51
+KPX W oslash -51
+KPX W oe -51
+KPX W Aring -28
+KPX X hyphen -19
+KPX X A -19
+KPX X Aring -19
+KPX Y comma -130
+KPX Y hyphen -111
+KPX Y period -130
+KPX Y colon -106
+KPX Y semicolon -106
+KPX Y A -46
+KPX Y a -116
+KPX Y e -116
+KPX Y i -19
+KPX Y o -97
+KPX Y u -56
+KPX Y ae -116
+KPX Y oslash -97
+KPX Y oe -97
+KPX Y Aring -46
+KPX Z hyphen 37
+KPX quoteleft A -130
+KPX quoteleft J -130
+KPX quoteleft V 19
+KPX quoteleft AE -111
+KPX quoteleft Aring -130
+KPX f quoteright 93
+KPX f comma -83
+KPX f hyphen -19
+KPX f period -83
+KPX f quotedblright 93
+KPX r comma -130
+KPX r hyphen -19
+KPX r period -130
+KPX v comma -46
+KPX v hyphen 37
+KPX v period -46
+KPX w comma -56
+KPX w hyphen 19
+KPX w period -56
+KPX y comma -60
+KPX y hyphen 19
+KPX y period -60
+KPX quotedblleft A -130
+KPX quotedblleft J -130
+KPX quotedblleft V 19
+KPX quotedblleft AE -111
+KPX quotedblleft Aring -130
+KPX AE hyphen 19
+KPX Lslash quoteright -185
+KPX Lslash hyphen 56
+KPX Lslash A 19
+KPX Lslash T -74
+KPX Lslash U -19
+KPX Lslash V -102
+KPX Lslash W -88
+KPX Lslash Y -88
+KPX Lslash u -19
+KPX Lslash y -37
+KPX Lslash quotesinglbase 19
+KPX Lslash quotedblbase 19
+KPX Lslash quotedblright -185
+KPX Lslash Aring 19
+KPX Oslash comma -37
+KPX Oslash hyphen 19
+KPX Oslash period -37
+KPX Oslash V -19
+KPX Oslash X -19
+KPX Oslash Y -19
+KPX Aring quoteright -130
+KPX Aring colon 19
+KPX Aring semicolon 19
+KPX Aring S 19
+KPX Aring T -37
+KPX Aring U -23
+KPX Aring V -56
+KPX Aring W -42
+KPX Aring Y -42
+KPX Aring y -19
+KPX Aring quotedblright -130
+KPX Eth hyphen 37
+KPX Eth A -19
+KPX Eth V -19
+KPX Eth W -19
+KPX Eth Y -28
+KPX Eth Aring -19
+KPX Thorn quoteright -37
+KPX Thorn comma -148
+KPX Thorn period -148
+KPX Thorn quotedblright -37
+EndKernPairs
+StartTrackKern 3
+TrackKern -1 6 0.10 144 -2.09
+TrackKern -2 6 0.05 144 -4.02
+TrackKern -3 6 0.00 144 -5.96
+EndTrackKern
+EndKernData
+EndFontMetrics
diff --git a/e2e-tests/cypress/fonts/Type1/c0649bt_.pfb b/e2e-tests/cypress/fonts/Type1/c0649bt_.pfb
new file mode 100644
index 00000000..b5d4ded6
Binary files /dev/null and b/e2e-tests/cypress/fonts/Type1/c0649bt_.pfb differ
diff --git a/e2e-tests/cypress/fonts/Type1/cursor.pfa b/e2e-tests/cypress/fonts/Type1/cursor.pfa
new file mode 100644
index 00000000..24e674df
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/cursor.pfa
@@ -0,0 +1,954 @@
+%!PS-AdobeFont-1.0: Cursor 001.001
+% $XFree86$
+11 dict begin
+/FontInfo 10 dict dup begin
+/version (001.001) readonly def
+/Notice (Copyright (c) 2000 XFree86, Inc.) readonly def
+/FullName (Cursor) readonly def
+/FamilyName (Cursor) readonly def
+/Weight (Medium) readonly def
+/ItalicAngle 0 def
+/isFixedPitch false def
+end readonly def
+/FontName /Cursor def
+/Encoding 256 array
+0 1 255 {1 index exch /.notdef put} for
+dup 0 /X_cursor put
+dup 1 /X_cursor_mask put
+dup 2 /arrow put
+dup 3 /arrow_mask put
+dup 4 /based_arrow_down put
+dup 5 /based_arrow_down_mask put
+dup 6 /based_arrow_up put
+dup 7 /based_arrow_up_mask put
+dup 8 /boat put
+dup 9 /boat_mask put
+dup 10 /bogosity put
+dup 11 /bogosity_mask put
+dup 12 /bottom_left_corner put
+dup 13 /bottom_left_corner_mask put
+dup 14 /bottom_right_corner put
+dup 15 /bottom_right_corner_mask put
+dup 16 /bottom_side put
+dup 17 /bottom_side_mask put
+dup 18 /bottom_tee put
+dup 19 /bottom_tee_mask put
+dup 20 /box_spiral put
+dup 21 /box_spiral_mask put
+dup 22 /center_ptr put
+dup 23 /center_ptr_mask put
+dup 24 /circle put
+dup 25 /circle_mask put
+dup 26 /clock put
+dup 27 /clock_mask put
+dup 28 /coffee_mug put
+dup 29 /coffee_mug_mask put
+dup 30 /cross put
+dup 31 /cross_mask put
+dup 32 /cross_reverse put
+dup 33 /cross_reverse_mask put
+dup 34 /crosshair put
+dup 35 /crosshair_mask put
+dup 36 /diamond_cross put
+dup 37 /diamond_cross_mask put
+dup 38 /dot put
+dup 39 /dot_mask put
+dup 40 /dotbox put
+dup 41 /dotbox_mask put
+dup 42 /double_arrow put
+dup 43 /double_arrow_mask put
+dup 44 /draft_large put
+dup 45 /draft_large_mask put
+dup 46 /draft_small put
+dup 47 /draft_small_mask put
+dup 48 /draped_box put
+dup 49 /draped_box_mask put
+dup 50 /exchange put
+dup 51 /exchange_mask put
+dup 52 /fleur put
+dup 53 /fleur_mask put
+dup 54 /gobbler put
+dup 55 /gobbler_mask put
+dup 56 /gumby put
+dup 57 /gumby_mask put
+dup 58 /hand1 put
+dup 59 /hand1_mask put
+dup 60 /hand2 put
+dup 61 /hand2_mask put
+dup 62 /heart put
+dup 63 /heart_mask put
+dup 64 /icon put
+dup 65 /icon_mask put
+dup 66 /iron_cross put
+dup 67 /iron_cross_mask put
+dup 68 /left_ptr put
+dup 69 /left_ptr_mask put
+dup 70 /left_side put
+dup 71 /left_side_mask put
+dup 72 /left_tee put
+dup 73 /left_tee_mask put
+dup 74 /leftbutton put
+dup 75 /leftbutton_mask put
+dup 76 /ll_angle put
+dup 77 /ll_angle_mask put
+dup 78 /lr_angle put
+dup 79 /lr_angle_mask put
+dup 80 /man put
+dup 81 /man_mask put
+dup 82 /middlebutton put
+dup 83 /middlebutton_mask put
+dup 84 /mouse put
+dup 85 /mouse_mask put
+dup 86 /pencil put
+dup 87 /pencil_mask put
+dup 88 /pirate put
+dup 89 /pirate_mask put
+dup 90 /plus put
+dup 91 /plus_mask put
+dup 92 /question_arrow put
+dup 93 /question_arrow_mask put
+dup 94 /right_ptr put
+dup 95 /right_ptr_mask put
+dup 96 /right_side put
+dup 97 /right_side_mask put
+dup 98 /right_tee put
+dup 99 /right_tee_mask put
+dup 100 /rightbutton put
+dup 101 /rightbutton_mask put
+dup 102 /rtl_logo put
+dup 103 /rtl_logo_mask put
+dup 104 /sailboat put
+dup 105 /sailboat_mask put
+dup 106 /sb_down_arrow put
+dup 107 /sb_down_arrow_mask put
+dup 108 /sb_h_double_arrow put
+dup 109 /sb_h_double_arrow_mask put
+dup 110 /sb_left_arrow put
+dup 111 /sb_left_arrow_mask put
+dup 112 /sb_right_arrow put
+dup 113 /sb_right_arrow_mask put
+dup 114 /sb_up_arrow put
+dup 115 /sb_up_arrow_mask put
+dup 116 /sb_v_double_arrow put
+dup 117 /sb_v_double_arrow_mask put
+dup 118 /shuttle put
+dup 119 /shuttle_mask put
+dup 120 /sizing put
+dup 121 /sizing_mask put
+dup 122 /spider put
+dup 123 /spider_mask put
+dup 124 /spraycan put
+dup 125 /spraycan_mask put
+dup 126 /star put
+dup 127 /star_mask put
+dup 128 /target put
+dup 129 /target_mask put
+dup 130 /tcross put
+dup 131 /tcross_mask put
+dup 132 /top_left_arrow put
+dup 133 /top_left_arrow_mask put
+dup 134 /top_left_corner put
+dup 135 /top_left_corner_mask put
+dup 136 /top_right_corner put
+dup 137 /top_right_corner_mask put
+dup 138 /top_side put
+dup 139 /top_side_mask put
+dup 140 /top_tee put
+dup 141 /top_tee_mask put
+dup 142 /trek put
+dup 143 /trek_mask put
+dup 144 /ul_angle put
+dup 145 /ul_angle_mask put
+dup 146 /umbrella put
+dup 147 /umbrella_mask put
+dup 148 /ur_angle put
+dup 149 /ur_angle_mask put
+dup 150 /watch put
+dup 151 /watch_mask put
+dup 152 /xterm put
+dup 153 /xterm_mask put
+readonly def
+/PaintType 0 def
+/FontType 1 def
+/FontMatrix [0.001 0 0 0.001 0 0] readonly def
+/FontBBox {-484 -517 517 484} readonly def
+currentdict end
+currentfile eexec
+d9d66f633b846a989b9974b0179fc6cc445bc7c8a959a39a32e9dce7faef17ee
+3bec9f509ea0bb8adf2cfac3626f0443046fbcbb211c518d48ebccdd3cf3d833
+4877f4ef1dab34759b6d38f757cf015a5ccf12d00a3239084905ecae5ec9d399
+ff3b917ce3cd4e85a1c49a750549d42f00f90f5110c822aa441ea813981685af
+17a0e41240e5c924b9c12d51f92ed0f3ef4fbc72c8b3eaef402c3befd6a52631
+47ff2e91ace4fcc6f381161f004dcd1db73271726d2ad25185a9b1ad0097955e
+b319d5f6d40a9f7163a0e106a361ebf256f5d931e06b8d9fe4e84e785bd29b7d
+fb70d68d41d66196ab5099bc38e613d11db08b2e9194806b651615dbdad9c797
+4bf4ad35bd0ce5d087baaee81b670d70d72475c0236751e9f2ba029abcb75682
+ffd70e77bbc2fa605cd824d77659de79f11275b5d6185386f0f39988ae683aff
+876ab78964fff6118dcb35b502ed5c7a38c6e7f898a352c307c6ddd0558702cb
+f9c763be246b45355d103ca1bf99d92e0f7743000d0019d4e66bac2292422b81
+7dd20274357d55680301b21eeded828c8836a5c47b12daaf00bdecf88289692a
+da641e1893c2eb202c614afd6b70524c9e2d6385de26e6f48a5959f587f80a69
+5401aeaae3c78d5928dc524e12cf874a89961e67d9d71db9aafbbfd960e6c395
+1f0ba3a4af64ec2379e956b2715829f009faa9fedfc79e06559cff13a4c8b860
+4a07ec9c6565dd8530acee97e2d2f0b92eab28e5299648f00bfedf9c87bd0223
+02495fde3f194d735815acc2e09bf4abc693de6e975f0c2b6b0051c90d727988
+c6a90a3dd10bc83e264984752520880d26f1138dddd805001ec71b935a8461cc
+0d6aa7cb0cb6c7c7cbbc99af3a513ac06848f7e6a222363f8696cf4ba2409054
+9b616ffe2d893a8f5066e6e7cdb21656530c2a720578af66d456de1723b3dc3d
+1406bbf7dfeda3aa3f649077ea7ec19f8bb18b28b876ce1f8e61dfc50d7b4db3
+968efa987e8479766356441906d6298fb426a428a1109ee9058d4a71235ff03e
+205a60f64b77d3e0920790200fe13d6676c77f78e4ced35f23912234da3b568d
+e729baa323bb7ebb41d72994261eac9f2c02709de342ae5c070d9e7b394a3ca1
+aad2e95d98c05cd4696759cdf12d16c3e000f3b2a7b5ea03d3fbb204c6d59b48
+e4dd5094a54aacd3f8f62d63b93e3524bdfa95fcbf75fd8be02544bbe661c485
+9f4a12191be62ca8b60cfcfba14097f4f7daaea4af8675fdec8f6ac22dd822e6
+bd4cbb2380c0ce2f6e7e4db197ba213942cefeca436f33249a881c60c77e58e0
+93100c28caaa5f861d8557cd2779911d42563f5fb8f7c890c8e24bebf300c3f7
+15d0854bb460462de7364c007c0be17a57c23959bda2282f9fa8bfc1571d7e2e
+5cd033129735be336dfe0efe4bbe19669e7c1a3f3d21e11c8ca081ec442fcaf9
+e4d5a1eac515db6b686f9da7757475c7b47cd2dafc56aa63208f5e0bdc127a3c
+51b9bd6796842d3089641604bf32ecd04874ecbcaa8c4be805cbb6bbd3988040
+8505740a2023924894097f466e1736c0bd795cddb95d80937d662592c70763b8
+97a8dad80e3467ee8192d4d8667cae0ec40a5bc9192d59eece9a405dd140f988
+ecb324c75704e502d088b5184cac0ee0857289ec1bfe9a2bee359911b9bb30ea
+8daade18b1ad9ea1c28b2d9aacd6e93833d9d8acb46349225b4d6b0e32cd7848
+fe8ec185522eee670d0cbeaaeaab94fe7904232249027292be36d2aa06ce4b53
+adb71f29d4746fe19cec148ada960820ec7eca5ebce18f19de5a7974b0d15b14
+2c0f1d0af773b4bc5ab79c7f16747b468ff0a9ce39970cf439720bab853ae9da
+c6cd1e282d0860f012ca867969474e3d396370a876dd9f603a6b64afcccecab7
+dc0186a089a815288a68564f62db84d3a295f31efe6c1b40d1602e40c8fc8ab7
+713c11ec7f175b31389f70bd3255ba32c10b2db0215aa5fddb1c02f74bf8c33b
+d4059f1f3640a4dbcbe0fd59bbc830881326e28b7c8d5b9704f95827e5e21d8a
+f720e3ce0c1bd124963ea970e1c4eaa6d340eae793aff5f8257f471937147c59
+fdddaeed8b547b3b98adf6b5bc81a86dbf9436d010d14b3b5ecc4ed9aecf3ffa
+9a4ae3b08b9cfd79370f4c6ade8406e7808ab8fe30694ae7bd9008c3507786da
+543465aa40f8ddcbcd1d561a5871be7f39c95ecb0e3fd7754469b1103c71d18d
+446ab34f5ea212f0b5c4d3fa8c50658466d770adc17f53706ff5449729b9c23d
+72cb4ec02402c38924dccbc49c976312dfc8f086050dd5a38132eb139b43422c
+756a95879b56babe8f77771f221f4f1f25d1a308064ec10eecfbb306eb55ee78
+2692db518eb3d477c4b52a182fe897e45c0c3d302aaa0fdc69ab3c26e55728e5
+5e12e7a51d28af01f104e5dbc61bd210daaa526c4dd9a93abf8867178cfb178a
+70dabac26de06f5abb43392964a7acb193fbf31070de491ab477a4c5492f4fc6
+4c3bc40d760d41ab31d8a3ec657814086a133801701ba8fc4dc5ba92b03b463a
+0f8bbd73b9bfcd6eba567e0c84b5ad5b32dce09e5edf39bd276d75c3cec89124
+a4a8bdf208fdc212469bc9ac96ab17e9df27ddd8b085dce1ba68ab50ea445bcd
+0f002681a8dd517660794d96bba265fbfeef267d26fa2a1bda94235f763209c1
+1f817081f930f20d2d0cefb9261e22eea39cd0a390626fe71d312fc0fcf11db4
+6b0b70010dfadb9c7725f419c1c89f34fcbcf3b268c7a4f838d94cefd82685ec
+b619d3f6df7571031a82e2d2a8bb98b8471d8c9febeb853ce0d38d7747c0bd63
+fe04e36dd00d2ef30b0c842d2a59457b3b41e2b8cda904a5d281290219814b59
+c4a75469375e9e0b3a358434a910bf1774657bbdc64cd408c680e2a09ca26507
+06f95e8c16735cdcca195f20c9a03a2a0ed60184c03f4e3478859c6639ca8ac7
+241b33432661741333406add1d9439690f07564d09f4b67152f32943cf93e247
+3f2a17a476bfd897ff69d4fefed1f4a994fdf535e3e45b1a3d46d560d5ac55dc
+2e4c20408c8ceb82d7bc21016bfcdddb8d5daa9e902c327261549ba62090dc4b
+27ad160070880cbdff5d022dc9c7d6f3f51a49af6dfb713308dc590c7dae5236
+74d7946c1db33177e4caa604ecf655b792be55e14272e0daa0cceb2afad8f3b1
+6558426e102f72ee42a5d9dff66d65d672451316fe551fc2923e4b24d9788091
+66457f55f9c83350a62bc5bf590f7135929b365c0dd003a95e98760472ce988e
+0e5affd7c654bb99eb4255bcf030fa008bde51daa55ed736086953c599bdc3be
+16efb2368acc883bd670d753fc7bbc65ec5bd797d0cecc14ac15f6423f0f22d1
+6b630dfe8736c2db51b45774f99280ea1274ae1c207ef3574e55478f68bac3b4
+8e24180f8891a2f3e8c0c9f74c22744830f1617b99a78feba5fbd752134775f2
+5ab542c17ce32465058ffb9e3dea8030dfe1ec82a1818c9bd1e3db30e25d8128
+9ecfaffe85bdcad5126fd461b6d5137d2b15cee2d97486f3266e4c49964d46b0
+db16d96593bbf257efaf0dc10f18e2f4f14a347cdcc220e8ba87eb215a66030f
+8f30d405308dc15ee9f7d7c92c607140a6bd1cd8c8274a507c5f12293327b9a9
+e5c3a13eec859919a805f12aad2f1c665d054a17fd4dbff471890585a35f4e0b
+5e64c4df9b687526be10da6eca391eb351ae661e16b930ad77ad33ad2a2777a4
+ef8881b5be91a83cbffd8b95a8f7c91fd86c71061e7717e4cb2f44f4ba2cf5ba
+a64c4f7290690036427ec227446192efc7c14479525994dd0e86e7bca081a9bb
+00d55ec51e1bdbb504c4d43463902c05ff0c1d67f3125b8ec48f5d1d394c7f13
+54e70eee5ec074d6a7e0dd6f3d2f006fe8a527b56a560685cfdbdbc9f5c905bb
+1dd508ec7829a13a2d49cad2db823dfae5b85f3d7c04c0d8ba1fe9878964534d
+c23e890fe460c0b25700c5f6432f420f3698c6a655296104eef778a5b125c74d
+79ca60a8f987e278421b0d4a311bc4ccd59fc3f85972091516c8a4662275060c
+8851c2532e2a1f72dca5c350a03fe829db297d0accf6458b10836d22cc461b68
+452cd2f3764a3abbf2314105e414fefa39686715463afcb6fb6782eddd94781b
+5130a77d16bbf0689bf64fcb46c14682df23e70f7a571aaef0aecfda9c889562
+080d3bf39422821ae0093fcab7bc29f9c4281eac9ca14454adf3ec397855c008
+fe7a9d64ad134fe9122f7650e37c17bce7bfca7661a0a11b45b42bdffff2decc
+47df5d33671daa487d7bd6a5aadb32e7257dc8d0feb9b7f0a4a7825fdb8a5cba
+a31a988477486d0f09858c772b8c468b794ab2db0defa88c3b4927b615bb34a5
+8ac9d9af50a150e991e6f4330786f8a4f98654558a4fec8144eb2e46a6190b4b
+5043ae396e7fca1eaea3e9a96c0b200826cc065f49c47c2797ddfeecc6e9b95a
+f2deca74b594224dbf75041f00fb37f3aa70b0b0eceb4343a9331e3047e86548
+c3a7b1a42e7af0792e02a5bbd2ab548f7cb8cd112babb89213f66913dc109d8f
+c1fd5307e7c20d7340372935cc459a795ebc9c4ada9136b71fdb3f68aac98729
+16052f949b6657114a04472fe931a761c4fc5f9b8ed340c16035889172f7669c
+c6dfcd50bdf910c615e5f3c64101e2e6546f252cc4e0ecec03c5e19249b37269
+a0bd2e68296f9627e4ff72e37bf3c2be74e3b34d259aaecab472a595af08b2fe
+db51394d6b59c390046ad76dcb8285a32c247c7cd181d2e386c6d5abdcbb3789
+3e8d9859a02c13a72743047d8880606488a521185ca446499f7adfc9a25c8ba5
+1b1c84ba24c0612ea3d957e76bc398457821e25844bbd906defed4d3ae434c39
+9e52e60fedecc1acf4ddecfe92452e9841eca92acf99c4ca9c2cb32afab1dc46
+d0ad255a851a78cbebac54fd314846c35d35cb7ea10596734ab79be35e11dbb4
+9d30fc29b140d5823f429eb8eb3d1fabd90b9c1509faf33f609b3ae40a48f873
+763b3a5d5c9236aa0a2be1661be917921b58ba7fba139796d4c125127885779b
+6c71427cdc5812dbb6725153a83809446d26f9bc64bfc2a2a4bdca0c57a20918
+4415951c5d56cff5a3824ba8f8c0652e91a5d9c04d1ed0d928b33801b98d36bb
+04af8348436aef4abd549a063eaed12f7a4655f5183515b067eccf550f7bf906
+abf1dc63139fdb1e8a1d2472133fe920777df1acb2d12f5b655e568bd42e381d
+861daebf84ed9c6db8d307de2bed017dece82881ed7677b5f26dfa34ac25e19d
+5837f76dcaf237de6391c1d6679dc1a981737ea3e07e729060a0472090d5c76a
+dca9262a3823f24b1ecb646aaedd7de4a319f091a948935480d53376703049bc
+a45851f924ead472f1b100ee7b4b388c6d37e6e9cb850ce7da81896c1ab68677
+c3298a9e5aa71ae6379990327ec585aa1e291354e44ad972d3193424a9805226
+f43b07992c5f1c5e01cb5d049ce2922dd2921ddb5c4dbce968dc7a340ba0449d
+11ed4160c846a508b5227bb5ae574c8c7dc5d43a0db7a0bebcc638814f8933b9
+03404c4e720929833b4a5d5b3e54b806adb35724c2ceb47d77d39e43465e7a12
+51da0d53106f41313f92fbf9cd992bcfa4d13054bb7154965dcf530b2e0cd18c
+92690f54a6c6d7bb38ff4c59d8e35359eb3ee07061356ec6506b2081878986de
+80a2ba253a34519189e706fee9906a723bba749cef1c15abe3653eb31c59e486
+a6191335292bdbba1c442b229b56e7180be446a319e811327ed75f939936cb0f
+56e123a274c843f27b94b35babf938717b4d02c2a673df0a88d35c35833c4d84
+1bc1d5cfb8a00a181cc1bb68f9dba8fb10ce8ff9a520d938ed21fefb140b5634
+8f2c237d2d1701e50d3132ff93ba3c0543694c83dd500000b829b705e3268ec6
+f94887386593f0e629f6e74d3cd192a48c20d0b1a00ec7b18aef659a62777caa
+3692512ea0efe4b8a4f2a138631b10b59f0df0fe27be23f2d3e1b195c2375be9
+6e307b3b8404d2cc6f798fc995c1547354c24e04cbe50d273b50967dfce4ec7c
+ba9bd4cdefee3ff4310358014dc02ef0be941c4a8273cce80ca9b8f75eef61be
+854b698a2ca1100f40987aa622fe8fd1932984e77e8c9781c691734caf0c4e86
+566def79a60cd4d3a3cd79f59332ac99315844424a627e2d6d38b5d6e81838fa
+357f0d137804d5a29abdd374afb2e3c4f5901cbaa94b8d15a5a8988a62fb92a2
+e06cf8788f37a1aee7adc0e67626d0de2881d822f940dd658b5f310a1c4f1bff
+cb7fc9fdb347ad0372127ab143e4d80657e5f0c364f4721d5d68ff624705da2c
+27090a53f2d481492c3b15bd70fc050f909edbc2bbfcea26ae850abf2c2bbcbf
+fc4ab4d34d006a1a41469e2c8a8f5b4ca5f9820d105ca12f1d1240dd32d955ce
+ef9de1e096ba539a3fb11415bdc0851356a40e0571a1033aaa55cc3f618f372d
+8c71ec445768cfc42b2c10c04e45e98a649e770a324defb330b84952d46e5287
+65c417a6a82deec37727a517c12bb296eeb5e6c285dfd16a479369bb85289a0b
+e24254e6ab5de4e3a0343e7f901c6e39d79902e7bf437087fc894556dca62fc7
+114b344d4052b3d7f84b07ef17199a938f69b2586ce6a9bb840c7dc1336e185f
+6264b644cb411a2eecdc43e1dd26ec51a837401cf78dfad608a3b9038f9a3a40
+9fc464ff2214599f8ed573112ac944b593ae83348f92c54ae5d6db5956de20d7
+4d48f8f6c685a70265766a97367ec2d7c6e1ac00c30fd3b0fa4d2891d013d840
+693f61956cd4dd4ad134886868a55d9244f58c27f101b321ddb99b4e2eb23137
+a2e596ee6d131304df4265d6e72f9425127729028cf2c3375a4bba7ca9363e30
+9278afe6c282536537034c787be5e9ef1a0053c64b461ff496050cfc8da1182b
+c1abe0eb88089e2d4a994108b1d24a2205f33f3293954b8e8102be94aee1e8f8
+d725d52e87e57a2de1f79c68ca6b5b36d05ac31c2d2f73cf5b955b602a1f2eb1
+6091edaca5e90cf43a1fc23a19e2fa06eeba82b15704ccb5b1b93587b2762517
+abd0e6375409f518e74d727b145daac480a52602ef35aa037647d7c0d403408c
+095daa192800fcbb56e5af904fb72d069f6a15d4f95897f530bff36828630bc6
+e4f83f82a6f0814a943a49fe2288eda1452db8c48cad67b1b6398e481ca1835d
+b0f9f9257fd3c6aa11a7242786b47fe196cb4860964cba7d224acc20d22fffad
+c9e11d0c6a4487b53654719506c04b2e1ec53b3ca316d5232b821e5256fcbc7e
+7ee284e54eca74acb23fc3efeee59e1dabdd4e9c46c7a4234f7e3e5784b3e65d
+32ebba3709c63dc9ebc2dd6891e39c87bf846c3fe524e434d98e2ad199230432
+c84c95422e8950c4323caff03a9de5daa32a5ad3d07b5ee831bd589c0e331888
+33ac8dd264a563457fe1213d23d0d14536ab8c1e75e5af941e2068d1ba9a74d3
+ada91585fde5c2797229599749c420c694108595966b815c96adf3328894e76a
+a0cd504efacc06434c27cbcf96009947a0f80315879d2108bd1611d6f4a5455b
+178b5a85b5ea739546dcf226c6889dc02dc08b5dbad34b702fb77b82069777fc
+3892034144ce7995712e3295cb7e31095f430622eec4117d98fcfd6075a4f0f3
+01794fb45f3a05e725588fccca124eeaebce479b9a835bac7b113dddab302dee
+6c50e47c8e87718b3867c99025be81d05a905b2a9335068b59de14733d656b89
+97d21bb34893b9548830c45071921d0f13add9d9b75a6f38ebffe39f1d2cc4a2
+2482a80bf04e13b0e3614d1326e97e86b3dd77757764a9cbf6a0eaa530450928
+0c5926cf98b98bda505da3a35684fd0fe656e71aa67ae6e7693d3ae11425e6c3
+7fe01890b287f354f747e4de8d5bd48451ce4f28ed4bbaca7fac03eae6525174
+6e75b117e4c7fddb6979ed288f81053783f49f4ecf3af9cda24a7dd6647368ac
+4f2b8108451496612276dd42d7281d6365810a5ef3cdb304fa2630432c5fed60
+ace8924e807b54a77d63185e7c7ec65c887ff17a3633322c0d4c8367affe2ab1
+0845fa3f85262ed12a0ca9c74cf1c0f87ff1233868b83238831f911e41845abd
+73a346e7ac01c2b07dac7153de91318619f576a5ff1c0f9ba90315c5e89b8f37
+cb2d8d413daf121e0c1b3aa7bf1a62a2ce28e39d7c43da5275e8f0972aa73986
+98dac6dca24adbaecc997ad0488bd9cd356dc86c15756ecb6fe2fac61d4553b3
+0b996e98157e29e174bab3af12176094b414bdd13f4145144f087c349b8a7042
+b1e5b9e705c93ade44defd86ffe43bb513b3254e8310ec64409e79743c2f897e
+40270fd5154140e0e65566556892c49762a351845f767621f5f5e5cbe0b67109
+84e30ad39832cc5f8857c23aa7b19981ffdec5e4497cb52da24fd1ebb53855f7
+8f87823ff65f6650f362d6741f496c21f8db8a609af33f6e60f858e7bd5ab91d
+9ac64226eae4994670acf610130e10e7e3a6b1e6f3a07b87d4e6b712d243a493
+d5280549b827378ace2f866acaf117f9094c2b15c2430624d2f1c9a8ed3c92aa
+7c3ceee15ef6eba0e90686949aee9f375acb97a49bc96942d20abeae79fd8e04
+fdabe92b3c8b493f3d7efb13ca6bb9b8d98c41fc4c4f8e171dfb2ac038aba4e6
+60724b873c96a9e7b7b4edb24f09e22c937c8068589afba95bfab3c0618aec42
+fc7b68f1fcecea13a54f421c23e2df1b04785dd471f82587616fbd140758e21d
+17a1ff6dc30b301925fb1d2956cfe57f8b1baa1ccf4d171fe03c9336bdd0678f
+4640dffdc2ea1834e5eebfe68d4c26c9d009cc04d0cb8ee8f4f0bc477df068c9
+95eb41e087a9a4c390ac5ecc87667aa713ada2cc1991262a20fcda33625eb9c9
+1f419c6723f2875f40de2258fbaeb821e2a398dc0ec62119c4efc1e7da92fbb6
+c0175c56d32e975115a12697fead8852b8fb75b23cef1bc53b8deaf493c7cf1e
+9e9a9ce177bd9a78ec55f3dbfd247f9367974984f31ba071365fd79bfaacb882
+fab5934f730081165771963587130b5a1826b618ee51001ea81ee3372507ed03
+545d33d977d4bbdd73e0d1f235e7a2e020a28759dc53d383a980958ea0e49865
+b3658294acf2a04e0d93bc0806150eb192c8fd02c5dda1fb457bbaf0f71fd063
+29b87327fb05fe02ff88f7850d17b65ef608e7857d2876fb37fb0665e634cb39
+2ef9a4e3751103c087fa26998f68fe730c0ae114e34a3d412e715c7a12df3b56
+862f24f1fa06f6cc83769ba4d310467b07a91ef2dd6bb873deee11f1f326c40d
+89aeeb8f7ab1ba650444b2d3f077dfcf18803f38942f3d30f22d93cbad14fe1c
+1d968b62b018525144a5d2ed4bc636a0c9064940ef84d9ed682004774d8b09da
+998824b6cd1e7efb724fe553a8566b51bf2c6ec6f949501ce0bbc54a9309eaa4
+378e143ce3e035d57beb62873b294d07b920fece0f58d5ad1c5c4c67d3180a3b
+33eae3ce510ab906c841ecd9366ff615f07a905df9810a163ddf8d681920bd96
+d7b2212c74a05cea57e1e474be64eea47b8433755d72e5c1f79b8fccbcf7106c
+b70022721bd1bc779bc3f59301fb393d4429bc19084e228034291413f21fbd79
+83661ca0f6d331c1657fa69e2117464b8fe708520655c58b3f346f9213630fe0
+a31a55ecfc61f5466ccda3138f52cbbd575a56d4abe425266cce9e490d03bdb1
+0f9f959cbd9e34505dc46b9a439167931828d18f8e2b8a0b5d33b4bfb64df5dd
+b3825d34568453b740127811f4ef2995d0e7a09f29d432e0e0097343d5726719
+b04a72b4a5e0aa2804d6f7e155e1f89a168dfac8d593a8af33c075e213c0ab66
+366fe474ddb9ca17e6e7bce12b473ede15fc938239f21583bac0d6951648cc94
+cedaef748a18fd237b8b50f36fa97dad4b0a2b9e4bca1eb243a60b0eea59df8b
+74c76ea6067e77c0fa5b0aa43330690462851f923f6503ca2076f6b3d10483d5
+a1e8871c08434f6f2707f7cdf3a68d7dbeb4f2c7332d01435e37787877a7a29e
+a59c1493f2a959981026d58da06923d04c47502c7b5bb7f151ad5f0ebaf72c93
+db1db631865b261af7835ab3b596e4efa81cbaa8e6ec72f784e0b268f71d268d
+517cfd39804d0759486f8968ec149b1952be695da8553900c7e7c6878b63c664
+078dde438b8c75888e32c0d3285f6bed05132ff8e81e393b8be800662356b8d7
+b0fbff19c35c9ba9326b36586a179ae3e8869af85ca655406e1bd5a36c94ada6
+f280d1352577263444122341f71b118c95ed413a1237a72a78b59d12eda3da5a
+3d9fa119572142e43ae69551a024fbce71d153a1a1c211288f878bdb3f8a9502
+4a6420b3ff69f0ffa246d5ae44370a24576118dcd8c5f28f6988fd922ffa8cc3
+af68ab7dc10d2c3bd5900b05060b984a063bfe7b03932a1a4427de62b8e4bc21
+4ef0c83fe86919a766478406f57d148ddc4ae0500e6e15246e31dcf0a7149ff9
+1800ade772d44268a63d11ee0f3f8ad11b01cb96d04a52d57d827fd931e7aa12
+f31373e2de4027c293a1329311510a63a268af6d73bb839ce767b3bbd4da9680
+4e36dad7495374b17f506f899ff61fe23f71310b8ee2f530bdb6d8c951fdb34e
+8fcfe71b12a555f174cd72428acc96eefa57652a2f5dc1a446d9f363c7994c53
+0159b89ec2694fc59cc9e083f506698db58d841d03f3961897168b96a3965a92
+500e29b74743e35681fcf93debff5785569e3b0e42b2b586dacacb72ceb0dfa9
+6c2aa8902fb26f1c3293177dcc7f70b54d85cf1ea4ca1a980e0e053bedde3eca
+0aca76c214fbed2982cd51892bdc35baa7e3ddd34da26a5b9302ded1de8099ee
+0e87410403d8f782f46987be974b25ad7766608525de89cd18bb00f831c8b3f0
+54db8cb4ab0ee2a9b1e63c8840ece040c3d7d763c7f65bf4ffcb33e7ba98679e
+2f2f77ea277314b42e94e936751b1c649afe60fbc8cbba2655c4bba13e622e92
+4f72a5612bd507388c7cfc8077209c68c4252a81f8efbe8b8f9799a947594894
+8a1d1087bd458d2436f837ddce87ebea385e3c44f433f2b775f6257b1f80dd96
+2bbb0fdcc8c78d32dc1e09521bcd9810fe25117326f2ba159a60c26d5aebae75
+8d8a422e14feebf2cc3d30e6c70e320d3448eb3edc08ab72e44824e5175790fb
+4a6401326b5ffe81fac99839bdc39a9af68eea930ffcb80223bf21e301c531a6
+2a523ac9985cbac3bd9e426dd9cf6ef112817313ec460f8b90a4a2349f819d74
+3444ed4872acee556af837375eff1b44dcc310f4ef67758731f7be7c1cfec351
+ada83a533c06701f63d2d8b7b2084afc5e7949ff250340351eb4b2886d16ebc9
+7cca76190f3a4a58995fb281a0dae019ed92b64190656f7bc5bda4b2532c52c0
+aa2d5ec6168e24b120ad5ccfc0bd31d631345c2e2cf3c7fe1ef9ca9cd4322b9b
+9d587204a24e18ae280b2ece030626bbed9f66f2979131ba38043a92d19429b8
+53e3475c7b933e054616aafdf0d3fb6345ef888e3fcda12985c2b2afeb2e59ad
+ed76cd456c6ac11d453deceb45655fe71a3ad5f5f770a7124a2976639a0a5847
+b79379244738b5a7b176207080dcbbda96f43a68370b434b348ab5631dbb1d04
+1f1082c04c564b44191a51afdfb3950c2b65f9c4ed4afb5f4bde68fb7a09d710
+4bbaeaab1612c1a0b1060fafeb385da056099a2765d69388d9a276601bc13a54
+fd114f2ce447e68ef452a6703d6fad6acc3bb591c3da0581c45de6f2edd02422
+aaed358aa940272865f7f93acc28460c8f0f5222454c05fdd3eb100219113eef
+5b0c02314cd29c2ff20f4e86b7866ddc9b8eca4c7256e9434091319ed99f9446
+f606dd0fe2e5b86e3237b2af55df6bf9624b286a1a9970759fe83b3bde380b8b
+df7132016d4be25517449ad88c5e6da4e292da5632eb662a898bf2c76714b963
+ebf7e7cfd268198ba84cbbfecb7c3fd01612f7c6a9f118aef4cd59307cdde680
+25d9f98dcc9542ecfe6a8bd8b2d09caf77f29e4644bb7b6ae295277eb8f5b733
+617f4ed5a2111a5d9eff858db8ebec3e59e8fe318c2e7a474f07c9f4234c9add
+fabaf11cac0a958be217bdce993dc99865333aff57576ea0e43fc70016c23077
+8ba456546e8ffba24581ab50e72fc463e2f524a07cc6bc11a347584d4c9f40aa
+8fe8a6873b680b5e7871b47e1103a77f0fdf2110b9801e8f24ca8218e4a9724d
+021144c9449ded25b0532a33d410bfef2382e1ef629e04ba1b5fc09d86b85481
+bd8b1f1d719cb01352c49e1cc54c34d115a6c1d0f07a83afa2acea650ce304bd
+919eda435e6f5a044ce65aeba0884f5099d860fbbc90dabbf0ae5cb1dd59555d
+2ada2f748f3a74010519af69b8ffcbdde7e88de7ed1b0f53c255cbfb8e41fcb4
+8c80603c8be3f3965b3af9f94b027dac3754764f75509ca2f554ac1de60bea69
+a53d7a9c4973b51a41aa7c0386117b6353555b31670c803db4e475c330be39da
+4d10f247ecf2992f3e3b35cc56bc19ec40a3914da09c914005419448a633518e
+3d4a0ff8c39f6ea57917fc87f890ef834dd2c6531b3fab0eba79e0f580c19291
+9974c58cc1320d8e66765886ac966a55e594abf6fa69fd1ef3ad7dbe2d964495
+08dac3b161a45e836755af3ce6b0c3e3247fa56d71723e41838bed42de4404b3
+967c900ae419a7006f0832bc24559a56f417921ff6c83120e70fb9bacc6ed431
+8563d3a82877b5c1422cf56cda1305e7154ad1f63873237de5ac6f382217c4c1
+17559be71ea1491edfc3afeb9809fe0b4e9b6e830a48af9798e70c9411acd054
+9d835fb1466aacfb17b7a863c7a1cdc81811c220007ca99454e8c423ac95b8bf
+13ea2b29bd1af8ba4e56662df2eca3ac252b683df1e9ef0cd47326252131649f
+e64086725efa8a6886b8c6dbf56490143d5d91ccd1c1f3db4c78ec614c3f6ee0
+3a5cb384ad5b7f3c7e1fa13ac9cdbed4eec6c5012de7f3f48b41531a79708f53
+d5f16ac0b89c06267e11a5fb32aae3ab16a1782489945c14492824724206dddf
+98cbf03cd9d969a543ea1d516df6664b4a839281b179d1096a434bdf08b3b6a8
+b0231c8857f9d4b4436f9c02211b6af4bd22d073d008bb8789545b7f93635082
+641c6e475c12ce89c0a4361e91dadca8ec20583003b1c9306122accc59fd089d
+28757e3e3324c4bde7ec7a161b49bc994084a42880693ffb8676366879ed7554
+43661ba1326baaf4dc099884f753648a05a37c71be30b4a3425d768b049a00e3
+2a3f3c3b12ca5c99bbf419fd25714cbd00a850c7d84a0b4c3e5346cdb1964a0e
+9cd1ac316f017f6dff9679091a6e93a70fb6d26072a2dd886baf9b4bde98cfc8
+372927bc92f28bb3eaf14252c7661be1c656129af8b3e468456ce20f872bf151
+22ea95d1c79560268976be3ee9bacb2a468e1e84c8c387ac41d3531742ff094b
+c3e14b27444d2d0dd8f4dc738e0af4133cfdf7b0808498646e6d28b738e80117
+870bb74f8d4947a15100bd891188a1609560000bb7bfdcc210508903dbdff736
+07dcc542b58699beef7545cf0456b8ecda464126c86ae3db47c7ef6a6477808e
+a8c1dc20768895055bacbe2562921d078fe91794be3ef5ac268f410e4ea47f0b
+68bbe84e54fe139dc9f18808574ae9316e15d0f3db78ccb4c0a8ac63ede19a31
+09749a0829ae1302fc18d38d27e43cc24dd43b091a58ec394f998c5fd4145781
+d6ef576e99c850fd41d46450fbe5d5cd4063d0a24c8bad4a45753335937176df
+36d2d60a677fbcf63d8342232b3d9c0fe3062ce4f1ba853b40bed54da124d913
+4ccd113e18fe2d0c4bd55b58fd5c5fe6321996316eebcdaead6751004dc26676
+1f47c198fd7c0b686705a63a8d2d7e79fe826edbba1d166822aa804858c48f3f
+2a126fb0e7b2d0b4f2f082b78b98c2702f5ff3e4307e1b55fd99ddd861bbaadc
+90075adb2581c3d4e5ecbfa5bf8fc2a4cf6df7bdc0e3382bf12e612cdf016479
+3e237ab37bb1049ba63bbbc0da17d1ba3f98d88ba38f98d7133f53c2b9e87889
+5304664b4d5b6a43fb2577c5ae332fef6d7b7982e0ffb57b265d0f51cb569936
+8fac060e6ac472317597615711d6c13058b2f192982a9e858c093f854a60ae49
+8687141101d45bcb53dc69d432e0632265d8704cfb3766836caa3b09b2a44a1e
+bee69f88f9b8e62d36dc16fad6fbbad7f15c91f1d2431ec61b659e9072fa34d6
+8306c715e2d111f834f7d9fc93a902976ae995bada6bdcfe09d5d3a67b247c74
+1e45d070c6e02c7a9e4aa540a8fe3a41df7fb4b60dc943bea339219bae869762
+223c7492de1434dd6a75c0366624f936179c494f7166085416d126a6bedd962e
+1321f9a2e1b359ba90023ac18942f3d19f8dc7ea36468d055d68c9e758c0bd3c
+7bd2b10e7567282b6fe689a2c713c3272a664df39acf1eff9e3a64a353195290
+4e100ec249c9de6196020e30791284f028f8d98e4447330cafe620642db22f16
+e4dc191d9c2d4ac9b54c1de35a59b2d3138fd686741e836d55d4b12ecdf1cc85
+5bcce70a55d9dccbd33005e654150899527801be8ca74b238a7be4fa846ed624
+9e4d5a6a0fa190963db0d0b8c5f982aa477bd202ce0fa2fd54c3cd4d788d3eb0
+1317a307682c2a2813b48ffd7603a4b40d79209d49eb456064e66cdf7551161e
+9204ccd7d84c0deb458cad1a4556a1cc1e7df0c815da189fd9499e58c9d22610
+f387782f059929860518d3ad42cb245278dd67b01c9e3ad391e0a57b19fee753
+824cb856bd538a05898ded31e347c26ad123ff3669ac5b39e137d655b1c05899
+a9f73546d556290347af154004be5e9ade507c27292e9c60d8a2bdef92d57a90
+89ddeba1dfb5bd781c8ee4d036bb8b67ede9cdd533269c40a7f862a4ded71e16
+cd04e4f144cd4e2070379f4aee9c47781a15b46878ca5d640e0f434010cc15fd
+220aec4821174563c17e19cd934cef3290b3462ef9cb001b84991f29e8de05ff
+686787bd434149a2dadd81e4bc4079c31b7f19ba28eb7240a06b9cd5b5e11490
+66573636e87e5c0a10d0ba380117618f8bcdf3152826b110a7218c329e49fedd
+c3d494bda6c0fed8ff7da20da6a9e0b54ec79af50c7fd1577bc4c5fe1bcf35d6
+891091bdbc81d468935fdc75756067516080fa9f6cdffc557537b959d150d86d
+9e0e1794de22b011073e79eec4ea6f14d6ffe0a75677d20fc0dd0391603569bc
+8e7ffee11b6b4f75f081fe4e7a9eb847a49d11d8531bdad6a73f1b9146625294
+85db9550e9ce21213904a4f9997a83c1bb9d53f5c2099ba94f56bb7985fb1cfd
+be30d619af77363e943b7e94b12f88fe3f3a1900d722107350b0376291296f94
+8515d1b41e6bbb5addaf252985596f30df0aaa37b0e16936dd786cdc624c68d3
+2d796eb0faa89df09f0b41980b8b86009b682dca85eea7c16638d99ea711c631
+35a79c1d426ca9e8782007c61e424c8ede1d72dbf919267d25c73727882557eb
+f78da42d7e816cc83d80b95f3bb86356314e65083022a08f82148044d071cdaf
+db03c7e44017af773f0e7d03c2124e3bb8ef458736c1051f70fa18536395e521
+cbbe933c514747710cb9edfc30f774f75bba103b091e24e4811b4a8b9ea2b492
+de7c5ce1ed103b70c913261fd04caf13727c6df5d246a419c52f175e580b9f73
+fa96742e4f119405174784450411374263d2fa6d277cc0a9dd60c97e9bd91498
+a7942547c6888fb80f8f9a4a3640aff4c00b2181e9a89f367a680cd1932b7ccf
+6e1604774602cf412130503d43f8bc54bee8514a24da005c733295f022f6755b
+e5387da8e24854cdb5a73c39b1c34c4d1884a4d234adfa8d104c004efacdd2eb
+43f20ee50840d297bff0095bd290ed796f726b506e316224907130eac9d99a69
+3a62927d3ef4bafe96258b2b391b373b255a84d6e9d03797f0b1db056052a5d7
+4d4754284dac23a05ad8f2a0c823a8a9add8669678c9acc3d1d086ef67e116aa
+2542986d3a021f8bfcc00f66e812c83e1235990d4adeb39dec07e62c10089c93
+f1801ab8f6210bd328aaf6d44e0a4df0d5b2ee8ad1c8f2a21f43e4f9b897ada3
+88653a83ff3cceabaee4fe26de33ad5fb3337e48e35d14423718642c4071a56d
+f39725f27ad0f2a71b67a14708e70007bac926be651398bb19746f14d1271873
+b7d77ac822aed13b6689b311ca57b895287e16acaf3a03cb9424dc840ba5fd17
+543bf2b93b4a11cc9cb7f9cdf118feaab5d22244096b4ace849d8e51c9b86d4e
+4c14d5874bb374b75e8e468a10db921c066c5c095c5886f4f21b4cf32801a1ce
+ba7968215ea52e217b2f892722f3749460b65838227b45b4c697c2f7d9ddc59e
+e76a83887354180d4eb39a789f0c4ce770f54802b55c238b5189e8809bc29775
+e02104bddfab6edfb13614960b68a49f8e903e6c53d38faeb9472c8bafed7516
+0fad6c4b186735d5de8f6a655ca46f880fac0981383b4720fbb3cb90c757de45
+8f0036a7281b877143b5ae8d0bcfcfbf15f7fe641e216d09aa391ade483d0b4a
+f304f07036ba5b7f32d9f2d0c8969c4f392fa855dbb57940e80e56b3209853cc
+f140c62c9af0a44b180280b95c9af522e77b0f32de23eb5f7625251bfda99170
+e9dcab1f6ddfc1fc7d9ce75c8b9d9350a73823790b789a41b33bdd12e5f527c3
+6785611b74f8232deae963e873156e473f2282f7598d6b05275e8c5ebeac183d
+5239da1e92b56d987cd9e8d817b2be1730651955754e028b4041189921f28683
+8f80a3097ce2e58784a85a18477617d1565160895c588601d71584c097711d07
+fceb3809a57de8185ef6f4492f0ed8691693cb3c83a889f4116684657f1aca5c
+d954d0c87ab67b71e6f13358a83c1a651805a7d3571c0c9791cb761014129388
+de2dea7ba4d0e10c3e3055155fe5f713e51e35d927ad9bbb85b033b9c6d94737
+22cda751a32c178a3e9a526ec9f50d3ea4dc1c4e979597a1f85e3d4342e25e4d
+2bc78e24765ab3cfabd4db25c870488c2d5962756d737bb1c27ec5ea049e6ae1
+91ea186f69bfa8c8969c6c0453a47b73d9f6550131e73f00140249cd8dfe74c6
+c585467610842babae638f2c67ddddde42cf80d561cb19fa06a3b9ef7dbeec22
+e0083cd8916482c5badbcbf03e88f28aa322480baed5cf147f7077da4368da03
+9ba3ed7a96798b9bf4af20946fca2d5cce88915df1e03bccc0a8e8c8c65bd6dd
+6b2195c57bb04dd20928222a230f22f564f51b3e9fe79564525eaef5d017872f
+3020c9326b49682962c78064c067c2b46fa3b94567647e04e9e059287bf2895d
+9d9d169816cd864157c41b432c86626680157cd9e3bf1554965ccf68d033123c
+3667deb2680ca27d3241d6ad950b50ad22aa3166f566c91128cf9448777a33e5
+15a5bf3acd90e0148c7798a5a3875c7aeef66ac2d30c16e9ce00c71c82484f9d
+39ad43a5a30dc01aa0e0bf5e18d0f91f0fdfcf8c30fb6bbea79a848af26d02c8
+431733f1f7e82fa4b37afae125d374fc172d1c9197ead90f986afc0da1243ff9
+97d02fc4b4b5c340a748429841b3a8b56cf3f72aedd6a4d90575fe7c781667ec
+155153ca416dc9eaf8df3830eb21e077653f595ddad569d8bef8802ba2d2e2b2
+7123b7f55745380a83080ea954ba9f2c9039867777b33d1d3dd812d4f8166baf
+9e1124da1b7abdd6c69596cdeefb999777943235314d3098beb54f8f9ed68ec6
+b6acb5aa9d3347b138636a0718f19a06285f194e7a7a81fc94e4d54110561e5f
+61b8fee49f71566c8109162f879f16b80e3ef0342c1000e0cb32bb3b81dff1a4
+dbff77e91e6a561630848aebd0d1c40c5f3129e409da578238889870a7bce285
+b744707adac54c8091b1b1f50390f2886144294e036008c3e4f20267974310da
+229b68182c2bb1af96139268da254217c47e5e2bc279200d858ff72a885fb791
+43ae0be2efd2ba6233620c039a1d47d8b111cd330609cb1d810279a7882aab28
+0a20204aa812c580712c7179488815e2366103d96cb0d564afb0f5a020493076
+46cdb743dd9df4f304a60871e597bda480624939ff921d7d36957afc64d5a507
+a48f8a087585bd0548b1f2c5a4c72533cef1db0694faae62c035bab307b765ca
+422f221a5536c51eb201ff03b81834aa581f36c62916dfb1dac0a680f1f4ad6b
+2353c8603e960f4a70b2aecf44c0d134374c35c88a77e0a62b542efe834e48cd
+4de9d5a989923c8a6da285b3f6ba2e8ba4df07e63a16fe98bdd392df72759073
+f7af0cc58e8f279a232763a8a9d18dcb205350cefa25012c0183713203ec50d0
+f2d6bf83edbe9a6d933ccda8b1745b183adf413617af83605120075f49db2a24
+df5dab6f8085d71c4eec8944dde3ec0c1e51b4800a90116b25204a3e51def988
+eaa3ddb44d44a052477e2ecdde55d0282a5a9c5f4fe31f1d0bc7f40d1f76866f
+c31db401b2865de254b1699ef0660727bc944470949955feb0a8077be04a333d
+72de1f17e061040ebb66504544fcca2ae6b1b58dec1478967b7b17659a4e8c9c
+fb5cce0a95f52d3b5b9fae59451608518b907aef2e1e94144b72fc846b1990a4
+69674c263d050b38ee61ca447b0b25043c980d68fadf6114bfe9319f27ece2cb
+2437c5e78984c9599761d264bf15379d554540b13a2adebe9d5dde18b5663650
+025f9db1e17d5ebdd776125112f32c157d87978c564afc1ab4bb9d734689edf2
+e165cd4ed1eb93b699350d5253f3f869953eeabb6add680ba6a2d840bb7d90b4
+c4e478460059c38a99be0b7c07173b2fac2c68e242e9d3a2eae427c6fb733698
+e429a14c8b04fa08bf6b9980895b4706da12e7f8cf42fe3eb14aaadc87638a45
+704b14972f1df00b8e7a9354c15462ae600a6a8c4b95f33acd11a7da207e49a1
+192cf225d16b58ded179c8ae20fa7d6ef7fd384e7941a588fb7f8c5c29d98172
+4a848cd2a7109333f2a5963f2f42c6835170a57f92c54f03fb2bd434364d6a55
+d5adc8f90b553cf66848ead1de1197b267061f83442021bcd668fa332f3f2034
+143168d02812313f6abea888a321affb1a540a20df468fa35562ea4e45640c54
+e03f932f5581de862bf2bd2c05d2c995bf6ee2039a22ca68c9a71990cb499081
+60b1ee8c5dc10c9174e88f2aa5791908fdc7f7d37e7bada45f04043ee35d1dc9
+ec3704d315bb4e372ea62da6580a5cb5192ec21865ed4ded15de27bbb1672531
+bc5b4c3b2ac266dea3812192324b38c50e264f2bfd995e3a33119e6b47f23fd5
+51271d0a7603da01412889b76031948f722f9fdc54012b4ff53cc77e9a787946
+b79a81d8a32577317f3c7f715f8d3f0daf03877d06d4161503cc441c723166f7
+40e4727f23a4c75bec756e4c40b0cde10315afed35bf03d523d6ae00bad2dbd0
+358e691bf92867237ab36951657aa0c4d79726dc75d8c1c272da1b3d0b47f95d
+2436d59ca941577a232e540928bbf58673d51715188aecfbee7aaa56fa672850
+b047bb56b9c8ef5104b10f8117402b229e483c863d4831b9a1efaf164e99cb35
+39c0299e73158510e5e1f861a6715b979a9982dfd800b0e38a394f169f0f3d9b
+f4f931de232db24c616bf79f59719e6bad4e451df62e9769ef3b2e74033bb0d1
+437baee3efb900853193f6ce69c7d28a871449e9f84909a96e594db7fd08efc1
+217482e67b2e02065e60f4dd931136556c80f39641860d84b7354e86feef096f
+06e84d35dc3645f05626b80e24f01df1031a4e1ee3df4fbf139d27f95926ad14
+fec86931515bb225c1334c86a2a59dbbfb13d6558d389a7202e0fedce9df3d06
+3c4559b11255d23e5a45709756d857060f186c9dd2cdea53f0c3031a4f0392d1
+4beeea8c705f18ae48fd22c8412b71aa4c62231bb9f34d2220e361cbe109818d
+1b6b0be26b2ff128546d68029dc837e42204d73a35ca2a18e13c46d488fbf3bf
+7f6a6f623085e42743ad29b3b9c042efbd195dadc87928ca26487e711463969f
+e972c925d517c754fbf308c3a4ec5c604359a4c8fa57e8db4ab77341dc6364cc
+c6d0640113d541476389195295d68984a2f25b33441d7d7bdf581229503759d5
+e955ff29ec234f2a7ddef7fa898dc3ef0e82da664087a42b6dc28d665622cd30
+92868e09703aee3764518038ca3969947a1b4e63cea5d6a7ec7aad6e7577b88d
+2c9fbe177a04401747ef15b436b75d660c7cbbeb77bb44e430e9835ab409c2a9
+108f94117951832a5a0af0a5acd4e8233fe636a348f252d22fbfea95a5eae39d
+d145abddd1467561b7fb12660b7cbfd5cf9cd84ee2988ddb6d56429e4b87fc44
+6cf4b70f45e8fc082d7020bd6e8ee33c6b94fefc99336031d219145fd293f4ac
+bce0f52f8ea974d74f2b858a3daddfbfcfd50c3a778ddadd1d1090abf318fb58
+2129ab1f2acd880911be86a256af0d0f74657d53a339a4f527e0e9809e47914f
+d7719db5db1ab21f486d85dc8852e9e2cdce6d35f5ae66b361980bc0516d1d12
+51f9444d3830473e30c9060a76edc532e8d74c4fd22b5bec03021bdb293a3a60
+9d8fc3aa0ff599c7a4593e555e1b82f0186671be699c73a26a281aed93af14d8
+5f9961d44c7f4954d3c878f8bfa0c5632b31d9a0c978442e917fc9ee88666818
+9e13fb731cdecca15c658320272ac9e968ae89bcb8fae5765998c8b4d98aa59a
+26e36ec1543032f391f712dafaeaff23be8821b3a23ca765270cb0bfc601f57d
+da601e2e28bac03b7b171101ac26c195863ec701a6ad7b9db8177aad94be5ede
+eee3b11860892cec50ec4059b6adb16c63482233de6c780a26109f8639a029f4
+5c2d3e6a2d95ba25de8097aba992609cdd0a76683f8fc50acca1533e04f0eec3
+96859b3a92dfdc7d8d34f2add87cd55ff8cbdfc446e7154adea4dda504f6a835
+8e2b3433070f23fb153bf3838e13a23672dc8a1f88ad096693212d994c523955
+75b925b8ee182522d63a882f6d6a3ba45fe37eb499f44ec274666b1256595d2e
+537684f807838d60f07ab06603e23f360d6ab50c94cc48d3ac9b31926cb8342e
+b5a881221c1e34888336699c7cee8199dcf5318cdb661f9188be6dab543d2bd6
+9016f5094d7b04677de039ec3fc22732fa1ace506320899074a31d1e9e580063
+39613072d3eae526b88b5933a0c5c1a438558372ae20cb80e0d4682571f75a1b
+47d69c6868ee47dec033c4ac744d0f958e2915b2c5115a73b5268aa94ede8f3d
+182e89777695d9bc36217ecc7ac1bd1e460a6503d1f1a33919e1bf91349ad296
+45cfef1aa1dffa8a68d4cce0d3f6f5dfad069c8363c68924647562f23fb98a00
+62212b4b82a5ca2d9c98353b7863909d6c1de93a2bea9327757528701415eb45
+43472b40f1a044cd32ba7ca9737ebc5e98d7a0bb808b66d616526ec48e60f788
+d5051dfb52801b9d3ba318da1354dfcc13edb89536e9eb6216bf35a77a1f4b7e
+5eca0840226eac5a9ca586ad811aad53a1938860af4fbf5fdf1af521c07ba519
+e2c2fa0dd7dbfc1153a5a77bda9d78b0ec5f28e832943ec8268a9fe453bbb53c
+6a45a9e2f679126e323cf885ba754bcef56ff40bd992f0372bd85c303930be6f
+14087eb0f5b26e5900f236d2a8895aa299e3b769767af33f88da11e5374eb32c
+6f5cbc9cdd8b067abc22e26504fa960e4464803f95af1ad57b249f0826e3b092
+4a5508b46c8ac2eb5d113da479890a7be56a7a5a4896fb097e2374f2ac7d2e05
+ef4d4a31d8e9fae9a0df448f198cfb26ee8462dc01c448af7389076577d4814b
+65fa2e4cd81d22d81db6ac028c2c0419cf6c6b0c8678e1c8bbeab1b89637fb5d
+f04d600e7ea27f4768bb2200fea08e1872d2643cf36b7dc9a1d45425c2c330d2
+44dd92eb0631c87d8e6a275ce000dc7891a91f71a326d191c50c6426c7a5f677
+17a628260eabd753444083e6ff60c1a4257d97c6026a15efb19af06c73de3845
+878b618e10ecf189126b9c1f440b5114bf0d736226f86753da676fa34ea9958b
+863e2f5e085ffcce7ba461ed3191701eafcace0c98abdd6ec4e5e37cdd1375a0
+cde67542bede694bea4bd5a92bcb296e6ff79e40f730490fb4ffa1c0b88e5d7d
+cf6819b545612c1978c34ddd242ce9f05bd2de8c6bff8e79fb747035c89f0c1c
+8bd4f55f14b6c510060105605dac0e571cfc02a8a580390292473b7b1705175d
+33efe45867e7fbc9029106d0e66341ed9fa6ecdd6c8b4c50ce213a022a4d16f6
+adc0ef53f46870add3d7d1038523d8fff0b2443a58f087f910fe374b3181c982
+732b86c0e23bc8f9b657d5c49f4925868ece6e5257b6aa08f08b2df97380cc7f
+d9c8e09d7b6886fe8adb6184751bb1a776b0f0b365dbca4d2e470a9f33c0e424
+318ce60407abf86c71cf2373e4226cc556491a91f4234f75d8a974a5b4c9748b
+25e143d76d91150a4796b1f668e9d709b8d92c47173ca4323a5b1594c97409c1
+82a52797bf1f74a8cdebbe221e9d1868cfa963b57a28f27ced76ec79dad9eafa
+c9dbc14b8a0542e93eb35bee947ea0c0fc373170406275d6f94483b5adbc3c48
+1d877a83805751d169c08a772f3dbab227d3ff467c9804ca7ccbf700a75ca77b
+14f32e0fd063e6462ef94bec9bdb4e8a392db2d4e392a270e982b05cfd2dbe37
+02eaa1954bb92e0c74873c3e7b780e7ac3341635c5a48ab40a855052f1f769b5
+48b78ce8549bdc2d972b41b2ff8d4c188a6c6a3009b3cfbf20c9320c78b823cd
+dd1b836a827ab16d2479c289f766029f35fe1fa278ee20c0c8bc21b02837a586
+2112128d04df3d2677260b123ff58fb1b7e6075f1aa0f6c7d56b9cb593ca07f1
+7e5ee7ca93720a395140b558549933bda0106ed4fb29284ea2b76eeddf0e1ca6
+89aa2a31f3a655c6886efc94ab37c6f2bd2cd81e351554ac3f569683ff1876da
+ca0e0f2183c0613ff4266f9c4399f11dc9db5257a48f44ae2994dbce930db588
+46ba3d42b1e318a207645ee9b90ab34aa0ef3e113744471e5b827a90fb5f7717
+27f72758ca2e49aa0fbb427df80dbc2dca243e2d6c332d205fd8e8ccf4efb9dc
+be143004845e09908024e4e59e8828f3108da8b9a590decc82e5f821cf3154c5
+319038719c08714b54082e645fe0baa6667f6ef885a19de9033e25f6f2274f77
+8e8fe9b9bf35e5b8186e9cc7ab124d0e49a8df2343922ce64f0f65a00a5ffd47
+9dcf079a301f59192a3b9f6115845ed1909643e3171ab6f105cedf73ef9652ac
+c7f9c68b0b0fd090ea8bd3d3838af25fa3cefa1580faf0c50bffe0e964a12859
+e27e017bd84f8d89dd445f0afbf8da3a73904ca77d2b1ebfed76c96ec2f8a1ca
+d38cd76b2343f3b3ffae2ce64de4a0e3dbd9f0e265807f731f6f01e1d61bedfc
+f214ee2ab782eb6d3cf4746fbc372acea6153853b94f37f3f8771c5c01d46e1e
+1d307cd73f2963313f46b0ece915298005a2e37a20e8b0da1eba7f0c06a7e2c8
+5bb8385b2034401837bf414b33e4cfe4c40f46efaeb4eb66d8c73c82798b4dd8
+ad3e231d2fd7ab77e3bbab8f4e7b15bb6c93f92139beae8b92fe3903aa35db17
+35cc096486740c95add2ee1c3acab366b335d361f39b441d0e22794696cb2a89
+279f434cf1ca1cc2263218f4084261de8635c0f87e38240a07f5a0b8706201d5
+e84cde169961dfa50d0aeb38a6606b76289c8234a918a37c766877fd4b254b55
+44b31946a76521b83b6175e892670905956aebb55876d19cbb009346b8f6f10f
+a6a7a4c86da23716364edff21cfe7427d6bceae58e5fffe9e3ae92f3f88d9ef0
+786ec2ce0f1440cf0ab52ee2249d0eb924f20687e6716af698f8ab76f919d5c7
+4b4a860d28647c916c23421c50754d52749551e7349f72a5afdde899ae41a0a7
+074f2099a53d4403e3bebc0270bab5d191672b4fd9b24a3a32bd62c863ab113d
+0c98bd274aeb746e8c592c72010af8b3806c86d1434113fd3a915e11bdfd4e3e
+b975ee34dce95ef509c14f5cbc0ad0dc6b2eaccbfcea974baf685a369aaf802c
+0df511bd0d2250813063525c128eef1a2ce9e0015937a9ab1d590a71587a9a7c
+b75c6a13ab9072c934953e93ee05c36b99d54a765cb1b1062e5a312de7a5cb2c
+092a5b06a9ac8914f57212df83432441a1eead79584163d3aaf35be581bacb6d
+b70a2e42bc886f8bd7224c1e98dbd4fded0f293650901d03b9015b6a6f761978
+770bcb647758f87eac41a98633abeb13226e46171db24a7f0cb2f94e43cb7775
+e389dc2cc772f99b17f04dc1baecab2cbb77fd8810e1d818be0a22fc3e313d2d
+462d439e45ba764422692d68b01e7e18fac49e91501607974c9c1cc61f2c9a67
+1cabd1ea8ce615786bfee65314b414fc14528e59befb56f4863056b4fc18bc21
+d61bb8c0984bc7c16b491267a7f80255249179c6f3378c9485514ac7c2c84f61
+00e5d81d362e49f278fafc1b6bf0aa9d76c98476a3e3635165d53a904dd6b699
+d6ca2e9cbcd609d11cc9373f4c1179891d2c370681c8010ec75ccf893b040b7a
+a413663f8e262d4c054bca74f0c5fe40e24d57ef4994db6543bbd561f2b33504
+0356430cd25451126ec91f82bfc73ba6d843c07f304472aeecb7cdd660a34a2e
+05c1e3c06be790aedb514288ec1e029bdcbcfc13689377be037674051972e96c
+df6753b2e31da945b56c0a7316ed42adaf78a531d7412b3a26e33c3fb926446a
+4a8abcd35799ef47481ac60948245280236668ae7df01f3cba2384dc88945c36
+7c9d70039f6adecabc5be2bfeaa8dfc69bc1d40b593140c974587905557c4a68
+71ff97652466fd44e8f18d31e9855bcb25640f15a398c257292c61ca77b752bd
+aee91ff16e070da93ba5a9cb1d2e137fac8d78499b015ec2c3f8e38422c058b6
+8ced660fe0af345c9d4513983a7662a0c679236b9deca23879c983788e49b143
+67ce3f436353b3a86148ec21402c05a5e815ae1e375de539628557a8caa5af62
+7bc56019eb0c11f753193b3475c6512f1af8cd4032b4e13ceae3fa51c167a692
+01d054707f40b3cf7d7fbd23a6cd3b680d5c20e0181208d4b102e0f96841b6e7
+ec818a881bca226699ccf9587bac922a6b51b545d8222e9fec0fc90bb7b88496
+01a7c60dc2adcbcd798c8d3b67a6c4b6978388d83ee7f052e58d117c15067d4c
+7e0c8bcc3064287cf2cf511ab902872592280b67295c0cfc7b1e51b3f8b71fda
+b9e285f6f0de7030ce9b57e50df59fb2ad6ed36d423f7f2e148246e66d343396
+82ea928d4a3153a382a9638e9bac663b48e2d1f565ecf22d2a2eed624f3eb980
+444697474a2b983697ea5614c7a867d48363a1ddadf4efaedbdcf6306543d62a
+4e2c4b153ea731c1e44b1f362870621b3f7b992c85fce0d4889b78b25d1a1a1e
+c017613de16f6da6ccea3e6c0958d27aad413ef02097a1e379a965ccc3a75b04
+73dd0b82caf8ec8788d4bdd46665810855f8bac3daffb60ad8707c728fae783b
+e9c8a8d34d949caa5122f37fea44063211379c4b88c5fdc06f451d1c06b1fb81
+3a25044a5f8d348b4eaabdbe4f9c1ca8943d2af204c41dddc2c1a2bbf979128b
+665b9f84cdd35c82fe3fc9f7457513bf4a908c3bc2a0d917d8af2417ad776767
+9b8c7be588b373753077761f99fe50c8df8a8f85eff5e4fd53d36644b49e7cae
+3f65634f6feb5838017ed45a4ca1dba9adc2d4c525d1f952090d31a49b8e2aee
+6b45e0423385f52628d8e43e0cfe89d07640971455c629da5d938f551f8e60fd
+03eb7fc642bcded570f8f592eab13c71ea7c9e833ca2d2d08c453f84b13fbdaf
+7cdcd6383ff2c103fced1d16cc4d8a21d57a9e65d89f913f1a028250cc3226b7
+0fd0d223f4d715e233caa1aee5e9fe4ffaaea243b0e13c198bb8dc8af12fee06
+09be128117b9c7bc338f21434dbc92bba4f7034b062a3232f73132558ede0fb4
+5ef02c7bb6c146e4290f0d897ca4d72eb8d5312a9a5fbd0b1fb878aa3ac69c5c
+3330a735239e9181ae7f29267cfb5249fbe39470d119ca8dc6e0ddc18d5c277d
+b2f4a12cc489660685fd824e4db3a9053cd6fca133c94c75f8ac1485ca36045f
+8ff8c6640b95d2eb5a7b0d4c5931ea75418c151157bd25f93e05cba374c43c38
+f04e4e89cd5cda0b60e1e5d4713396e2b553727a20bb76a33273bfa660ecfcee
+2d3df3bce3c339a423fa84d97a637e3a3d99aa19ed8d6e5a68246ddb6d3d609d
+7515cb0f19df665d9f5cefbdbb745a89d3d69887e5a0bb1b4e59f4c5bc0c1a03
+67568e924f8c50654474b48003c443652a2e7a32d5c1775e9b930cb0c99f1a3f
+e6ed3e505ef02e231e9ba5a08c77571fb397b28df3f6334a59d62e87b9b73bb4
+8b3f911be84cbf41565870434642285ab575d94cf326ac39d601db42c83371ff
+55b1f9d5471f81754a3ec6233b47f444e28c0f537d91d8d13f57be0b8cce06ad
+f946dfcd3b272ddc13c3360225b79919f97bd2218313b06a80e4d76d899e17e8
+90a00622db00345e86a42f406a6f4185fda063e80b2f8b98b80bac88b400eb82
+12676ec8390a716d56e244320febf16f4ecb70e06caba17d7459801da82232ef
+71c80a924edaacb29af50d26be34bde0c1554671f4b0153a40a8693790d3ad6a
+5422eb65661cdca9379e47e813ddf9e1240cb259990bd8b92d96ae9576df599c
+743168ff8f0c64c37e870e3ef25146a9231c46578ef77cc3464adf25ce35131d
+640e067c5843d0d628a36b9f5fba003cf19d15ec0d693a8862685d46a30a18eb
+7d66a20b365b258edc2e9d9a6b414ed0f1ac1894ecdd4bc2d707d88833a1e34e
+a72389fc95d26ec2c10e8474bb00dc4200aae15adce11e1d8c4014d7e13c5c0b
+6caef835ae542019f4ea14c7283fff973eaa8a310f40de4de0ff56807bd4c46d
+c45523fc703fd8c6d08b32ee6e31943dcb52a47b30dad4d3d2a5cb4d647febff
+ca269375f1cd0455dc0a81e1886a1dd4b78a9d30138e03517011550a22997fbc
+4c6a833d32631d4a6cb9bfce3d8a98fc0393aca086a6bedc8ff833bb9c455e2d
+1cfd953d3f4e60f06e8226c561ec5dc4994ed8745d934e231d95765a0809b952
+48e85d325ebf37d8084619d4de9cc4aec1204c9122b918279b1ab7e63d9eb477
+74654bfa2cc04b8ca5984e2ed6cf7358df124b5bd67b523c4785046780708e2d
+982f3a7f3e6e266a5b3b72c1d9d13cd7d055a957d96015b11ac1e05dcd9fd6db
+aa69fec04dfd90eda81bf71aa205d1587eb8d1ce5b97cf43f32bb9bb2a41ba55
+2fe72fbcf8573a998a4be2e870380d4f6cb4d086ce926c86db0e248b4e594d4a
+e233bc13dbc6a55c5e7fc9c9ec994876cdc9795e0d3d0535f702f13c8cf64659
+b0248033530ea3b00295bc96240e3a41fd97a7e175b544242eaf7146a6d413fe
+fd9d4c4bc1362f20a1678b8d6ab03d7a72d9cf8d79a9fedcf826274efea6ae0b
+837eed3e717c09f713dd29b6ed9f5a6bd7ed0676b6dfea8c9c02a32fe56d704e
+bf1271604acfc1ea301d86e270427c417baa917c5d6e8e24af0b352d4f6b419e
+b63ac268e581a5aa86c7eeef14a718e33f4dfe5bf05a94675368e780b8f81ef5
+ade6d7647f4282ec319bf5991a09714594dd1854ba9c20d6b00ffab76e54f388
+a60780ec12af64779f2adc061dd71135447bca3daafcccfa6183fcc78522b533
+1f39f30e8750c0a3337a44766cc3f7f210c1a8e10f4ed1cc54d0729b26f1fefa
+7367aee66b33e8dd0e78cbec4bee5a70ca809ac39f84b72f1d119e4353c2e4d6
+20c7f6383ab7129b62270e6f7e75a1648d1da0536e79d60787aa3149b96a284f
+2ed1e0d2c9c4de7be5db53b35d360c1c6210362d4a35f91c6633daba2c2272ce
+d87c53f85ca1b55002d4e8680cbef6bf500632752827c0a5f19a062bbad900c1
+11442036e66d360743faeb0636e1f31b554b6567e4540d07abd6e1ce39313277
+5029e890b63ab4708ad351725bccd79d75e49cdb612da57efe3b44a2e7870e67
+a5d9686eef4b57c2a17f2d83cecdeb54914610e5a3401a97050fa517f1662297
+e06d29442d0fd89ba10f21584e71eb8bd5b03fbd56f5a538f72c9cc8ba5da11d
+9e4a7f8d9dc2fe56f2976d3e6bfc901de356a3d0690a0aa715ec6d339d72061a
+530bfb5323a8d038b8ad3adae140668e505bd100bdb20b98371228b841d0873e
+832309912ac7e9d4c7ee9ce81285c7f8e9afa8e2077e635bb36167c9e9c6153b
+c73fe799800c39fe343d70e38f30a721cf2796839096add3c82b675f0b659f6c
+99441f9352a332065c9df5b419a020d9b2fff06efd3295250d5335f7a256ef97
+992005ea2fd03f408a7a923ad8afbc8224daa112e8e4faf2ca974af2cafa92d3
+d5e9fc80b0bb00d933f50ab9010d671751b3a12196919e073678c2b047435a8b
+b826809e33bea0f12379ab1a0392d37f27766ccf15b3648e574e7dd1aff8295d
+df3db858bc078153072770fbe45196c3107a12df7f48d7bb6e4b99cd3553e01d
+59bc130f7877b18b65b15ea50db1146c8ffb2da5180b603d920c099d15aa9533
+b594791c144a3822f337abcba4d33d36299bbcd94190f85e0c28f4fd51f9cbb8
+9d301eb3f41179389b4e3323bc407c1ab223ba1608a5f8cea8d08a13dc4fc307
+2f93d59a3c8b32cf0721dd80c369e4f590d6fca9930d4ff110611d755b5fafd9
+9645e29003540c72f70f1ba5b1a884d1938f77ad689e6e70afa7216f31988919
+86df34a1089f9a009926b8983b05663bebbc603d90ed348ec33afc5a549af5db
+86b90fd34f534b24234f229409f960e2e921575cf45520f2d651c62bce7ab637
+6c2fcc99345aa8880a8cb7a67c54ca398cd61ad14162c9561ed8e4e9737aa9e6
+b513003edb220ac92894ab112e100509608504afe0fcf85b7bef8000adb3afa0
+3f85f52b856c23a68f58bc28a601e5430ae318ffafc24975eec7836b147a5a50
+d0369b90f111aeac20b4db1abe66c32bf4231006d79ef4c303e7aefeca4cd5b3
+00b6900a26b2b9d98da2809a9135dc6e16644dc932f106b016fa02730f1d6dc2
+b1c9e10b610f2bd769fc071e06ee636ef566bfc05899c740bf3ba380cd7164da
+7fa355324aab667f33fe518cdb19060cf3fae8a0363e4460a6c2c9b70bf39054
+b2d3eb81093b0610cb03e2e7180523561cb05761b5ca8cbaaf5a35f7179b1d4c
+18d0b7eedde1634f120fc0f6134e55a6de259aee6d440444e0ef0fb7db8a9c63
+95f3200e5d40b854ea844860529d1b694585b8826d988f432be1f620629691ae
+f282e79b2874754f96c4a4a1155c2ea07450c2a256b9d8e33eee33c232c99860
+1e34dbb63812ca08aff99255ae6def9dc3ee2789e9bc1d956de9fb255bd51d5b
+09da88ffc9d8744f61e002da68f41aab3afd0314ef876318c8785f87b04dfbcc
+bcf681dc8a0d615e24ce8f6322705a59a0824b5d1f0d35c7b04062d13f5cce04
+7885cf30dada61ba4cda74df52c4d4afa32206ca799ff9e9ba2bf939deaad2fb
+5b14cff0283dcb41869cfbab91eed9065d69a6ddc91cb241634df75a81fcb4f1
+490bf1195ff98db50e9fc3c82f10d91968259bb1b07645881c274d7751e5c2d9
+40e9e8a23de33344c3e15b8c0a3801cf73eb1dc8e9df066e7ec61667ed349f0f
+45ba6b0832143d4e6e7901d4ab7b461ea6489f419b59ef6fa892663b01a222e2
+7bc2f4cbc81b1982f595dd2cc94d6388312cbb61ff2bfecd27b1220dec43e9ac
+143a83b5a63c057e2ae6393054710b5d616316ed94db2e6698c4c1ddeab6d9ee
+2444408d659c7b68d4cf5c294dfd4299507c41190068c3905bce2425f652c49b
+de244eb94d28d3d5297e9a11adaa7c9a9138f00659f2f4a42fa34db8b8e29246
+15ebbafefcdc538911625e0e0e9ac8eca76662bf295c8fc2f871fd9aab87ef3a
+3a7b293875328e89d018f40c760006fba666b8c1aec0c64231793e22825df5da
+923979c72db39f5541e7f489fb5d9510674e8be697a1aa7a0249dac954fbad79
+06d11031ce4605ea491e9920524f4a60ae55c85371fcc9f046c1b83bf3e5812e
+2a2f06cd724c777d2238fc465bb016e8a78ce7c86808698a852d474b63c811f5
+43591f5d8391fe4a4d2dc3abc5005757a17922b6eb0be4b9c238fd162f9224e7
+475456f639c59a26c8c6107ec20330c8724d6f6842edf3875cd0f87dc434ee65
+421155bbf5226c31de48374d426bf3f3d07b9bd718ec54f8eb9a6f88a9de594f
+ec2c9d4b2b32827fedec8078a6851bbd269993b246b0c902242f0c7ea422d799
+65338a2ed8a27e49a0be708911bff10f485b753574d8ac8313caeab496e0ccd9
+5b67f10cc0c074ca7613d05a67bd45df6b72f6e30eaba836812c91a2a0a4bd42
+2141d7443ab359be588901f46a22d52fcddf6bc04e1fb006a32d37dda270565a
+575354b40af0869c9b1f8a85696326f546e9a25417b38b9b5d972bd0d933226b
+f8b34f70b324255750f5a64c85318907900c3320f070b1f789620e4f5db6f37d
+461f4a2f15e884b948327d53420dadcc4b073b37ee4fe935c8f520d9828ee209
+976fd2c26c71e3bad6583784ffd61a2f419981d51592a6f8f47b4718b6736a2a
+1ed7f729a521155a39394d038c34d8a76e9c6f91ca369da29fed83edfc05a219
+853e5ebaf1f3d5c6ec932e69b7a0dc19d248f81cf541348c85853acbb8334b12
+26ce33140f3965db1a6f79ae0abb24b93b6916496b1698057d4c2d4a6a72305c
+11b731a8a07ff1eecb84fb6e3f3c3706b09a0cef4b1c2012bb9278b76f5189a9
+38d5b9fdd4cb9028f43d16dd39fbbd10a684582951236a05e0692bd1c139742d
+7e75a21cf14fe99d51dcf0a0b71b267a0ad42b8c1d7f46ba46972c8ba7bdf849
+15fa6c5a922dc93fbaa9fc50c7696494e6c372f3467695da5a6d7eb87c0437da
+d7b68ee36874203d741e373e931425d4ce521b2728633a25dbb435f963399f37
+8d0adde8345439cc5295ba3d255a72039ddb1592dce1ea47a92d7b641182d0c6
+85d0616a85361c3b78bede0190834c1de0d4d132f459d8f29586dc73f974dcd0
+5def570984a9c2bc5a720999111c7de6a28dc0c6e9c51f82d8c55a26ff5ed760
+4337606960f847382671bb144d5ae5032c74e0581b19b21eea8ce4529eb792c1
+044418fb0e3da356a5b33cd97a0dce15ee9279b076405e23683f68c63b6b09fe
+c3ce3e1b30a4cb3a1e72455a372b6eccc7e4c97336df45d0bdf94aea01a108fa
+f28d98d7bacd750c7c25874e0fe3fe20d8e1d51043eca67c546278dc13716d9e
+f81a8fe2945a6a49c82b653804d54f626238396d8e4a9bdec4946c365d1164e2
+fed12929b769cb13712ad61c764d31798a87a3e12e1502cab14ca07efb524ca3
+dc31cf17f73feace77494de837790d812dabd36cf4c78123cf76b504886d0cae
+13fc6757bb6878f792a86d52d0e90913d089d7c81faefc56dfdbc3acc2195981
+0728dffd98a2254e39eebfb51742657ac9088cf8adc21208f3a333a1f45beafd
+69ae647754161c6bf0a76800edcfb6a2339f03e4b5ba4308b924ca56344770ea
+1c7322f692c297794b03f3c2a180b645ebf850afb290465ba050ebb9c8298f2e
+50b75571a15a599650a7fb1fde8263e49a68773b5184c30da3fe59a7c75943ae
+c5d26594361b53c26162410d3a1bced7c1cd0053390c1e2f8848e6aeaf75319b
+7ac5654a16cb93832bf981b9d54aebb8daecf97894b560ea75f51bb39a2bdc9f
+f59d01321b51b23a10827e1845e2db316d86d7dce368313f99ce7cab2a8e7c50
+8f91f0faf03626ff87247ba86dc7f3d837020e6aa83921385df0f401e4084237
+95251c27e8944749aa33b0b697456453ad8a5be8324fd22b192adcb22e03c74e
+6a3c9e6f7e452ca491db52e6b18016592cb09955f05365ec73300dc3e179c32a
+d08a443de2753d56f0c968db00d9cd58f1af7e3811202398bd58a337e7ddfd84
+1c8c8efd21aac90582130416d261f3a1b9f36ca431c8d4efceb9eee03d680165
+70ac1f37a1698c0b4da94cfd4c57ff54edc1b25fb713177fbf209928a1272cbc
+25655632d43e6d14c052763452d51cfaa32c8df47c57112ed37ed239f8c00268
+c78d7f3a0a965fd9e3f3c1aef2dcdf74f663a72b15fa09d7309f2d15b232efcd
+baed8a66f17b0dd190081a95a5619684a184603491a32114a5ef2c5076caddef
+0b4eb2197fd59ecacc6416398da20414cdb6a9a3e9ead646c34ae4ace4cd57e4
+430cf5bf55ae076419c0b79095d903753ad7f46dcc1b93f782127d33a309f245
+33484156f9ffcbf9e841d0c71ab0b12594cb2c327112508593573cbd95d75739
+f4ed866beaa48bcfe3599f62b0a848d6f63fbf79937966f3a0ee4c6163ed16f0
+4b8bd51ba483da59fd1d719499ea3aa49fe4917e10e2da13301b0a8b50f3841b
+6e80904dec24e5428b97ec17e66889170d12a27c028cfac9adb2eef49815f433
+b6089c9d6ef39f058f0cc655b27f4241ac74993af39e1fc17d5f9e27c569ade9
+ffdf46027fdd633bec61d8c5fe318810180f4437f356001633e14e098c992da7
+52f392cdb417d2bd30698612dd7b2ac00d0e5b4b3d7a20b99d0858d5e744d4af
+bc9acac37005c3841103a6ffdf3192ff85ed425dd221e4bd71ca3db6a739c7fb
+9602c130a1cbfb5b9bb9342099c2f60497642ed1029e5f9308a5097d6f33c815
+0f9a31a988ab5c162e5aa56357c8186ec5914533aede037fa99820ec322c8fcf
+1ae1a6aa6e048c99d2af1b96ee8b313ca0453923f5fa095aff28e7cf79c4be83
+70058ec3619ae4eb31fae28947355290e967453cf351ecd976ad1c5da7573f0a
+ddadb5b6661c47a0dc3dd22d7c175f9391bb9e359bfec4eb2b687a5a6f2fc8e2
+3ab232c34a95f54cf924fdf8011148145466799cd44a8440a0ac99b7f119e470
+a7fdcf6ff7a57a588f407de565b6a45e7fd5a1925efa770209110c54af8815ea
+ac1af9a0b5a790f6df301f7f9a105e776ba30e2b56d1dc2c0811990c8d2a0d59
+d733ee5bb53fe30323632057131ba4ad1d49cadad65d386ccd7a2c70bccc068e
+e881a538aa3471ea3b66ae24127dcaadbcb01a9abc8df00353c568aab8a971f8
+fe42a15ae22fcba141317584119f38d44e479a0d230273fb235a7895972f3f12
+c9964d1fff7ae88b9d39bd0c948a5ecbb2d858a26c01d57f3ae91ecca2475bad
+2d9c5d8053119644ee9ac5c3448c412b9fa18b8c5cd666b58714124c3c1f7d24
+62b31fe9605544652c1b62a4c16c03876977228b7808919fd49da8c14e12971f
+bf157f70c41e54927f569a8ded69e9ac4af29fc679a278a7c6b0b1a605bc8f3d
+d28c0da975292d88fb0b27a12c56ebb71bd93802ed8634e6615f92622e598726
+f1964331f9f3353638adfc0b3ae281842a8ab707b753f94cdc80a4133dc2fa76
+0e723c34a5962c79901e1c3be3644d926abf5a71cd581c2753042a0264fc5a5e
+f6196bbfbfc152730227798be62643e38c512d889e61b5123e58c173ae6cb8ea
+1f3c5358d9ee649afaa3c0fa0efc9d018be248b664f9b16ff484989e0ed8a199
+85abe77acc50b0da312164a738dd0b0f8156eb2b18b9c18ad7c08ad8b61de543
+84d1f5ea363354272e3d937a2f770a74d30910f195d337780b3b65791a0546ab
+18a8ad95353637c276b4fba66000886be4c478fdf68bdeab674527bbb86d75f5
+f05702581626ac0e693f73f08005b929bc154a5cb26f1c1447a34cf441aec12c
+e061d5c75dabf9b6055131b7e27edfa1197492525b8ecb98a74753f10324b4be
+61c87eed22b31f59c4fc0a434df9d34e3ef204268df01fe50866c937fdd83de8
+be67591219361b44441676d4e24fbffdf0fe6ffc1246c94de5de84d0e2ce54f6
+4001a4d5056dddaf20dab145f59405e14629138ce2aef7f7df4c3ac3c34c8270
+0195c65a7e6770d2e8211b5d6ee9ee826c316acd3063c634114d96b68f9e7be6
+d6f09e1004eb05a1b8d5b8602e7dcc4e9942a1a07cbce0515a5f651df70bd369
+31149d9d07e155b69fb7aa2ee350c52fec11cd82b0bd41dbe5af51b22a0d1aa2
+42bb655f8bc0cf68708f4891d0152ea90a8ace90fabedb75807baefc45b6fdbc
+ce98089dbe4004fb84cf3ca808b355a401bfb7cb0bdf3d34c56920ea7c9fd810
+1022917883c46cea4f9c53a1b735206870f51b55f688da9c8569dea21b033a7d
+26739a6dc374059371d7f0e4f1369f24abac48e2bd6c37033106921804790ef9
+8cd1e8b8e954dda81ce530dfd6b96e68e9e65f68893704d4e1d462e7fcb38be8
+9687baab78161f5e6957db5340f62b08b6a15b34574f8e66a0e3ef88db94a571
+2d01ac0f7b3971c994b8c4867c26294e63e18b06748cdf7067f521acdd28f928
+efd5ba043ff030c24f8dd60f472c254442493e4d5ba92cf85023881b5a222ec6
+5a37ebd19b3cf67310f5fb521ac4f8434ee8e998339f7fed4cb830c5f84198e9
+c5ca9e7347186fd6dc622118c624b6c5ec9554641de66beff929d05364b9ff44
+dd342e36818f8af5a81208e7d92eb91647f1b68e4bfbe708c0ea70e90a1bc7fb
+7fe7d3d81f207dcf44c20fe86ea34cebd111b7c5cf7e0c4ace99eef31ebf1160
+ac50564b361d8d039b8f622e3c4d6e85474d232bceeab726bf66b307ce147c32
+b1351681a3b6be40d13fd8da37cb71bd787b78a922f3e9f70969a716a76c2c1e
+fb6c315bb455ecc8d2475b1e61aea14f788f235f8b5a7cf987b78910b2da28d5
+288a7fdc879b66f78d5aaaed5293a17a1d96d0715e2d0006feb9a9d7e4837fb3
+91f3ff93e02ccee1ddb2b47716abca344666c1573ee338bcf35d7956b9cc82ce
+5083ef6d3155e28f8755aa7f8dc6dde81dce8c37736e78b97c4459be3b471336
+5b93671811cc5a397a4a943b1a82f712a7526d9010fd07514856505621d5d667
+68820a69f5ea83b0d810ab8ed4e5c9acdd54d4c19f958add35d3d21f7089ddae
+f69e4fddb831a94cc51f7be19410c5c6f3b0ae57026c4a2ff33bc8f91afa9844
+abf3f6ce
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+0000000000000000000000000000000000000000000000000000000000000000
+cleartomark
diff --git a/e2e-tests/cypress/fonts/Type1/fonts.dir b/e2e-tests/cypress/fonts/Type1/fonts.dir
new file mode 100644
index 00000000..7d0a5f39
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/fonts.dir
@@ -0,0 +1,26 @@
+25
+UTBI____.pfa -adobe-utopia-bold-i-normal--0-0-0-0-p-0-iso10646-1
+UTBI____.pfa -adobe-utopia-bold-i-normal--0-0-0-0-p-0-iso8859-1
+UTB_____.pfa -adobe-utopia-bold-r-normal--0-0-0-0-p-0-iso10646-1
+UTB_____.pfa -adobe-utopia-bold-r-normal--0-0-0-0-p-0-iso8859-1
+UTI_____.pfa -adobe-utopia-medium-i-normal--0-0-0-0-p-0-iso10646-1
+UTI_____.pfa -adobe-utopia-medium-i-normal--0-0-0-0-p-0-iso8859-1
+UTRG____.pfa -adobe-utopia-medium-r-normal--0-0-0-0-p-0-iso10646-1
+UTRG____.pfa -adobe-utopia-medium-r-normal--0-0-0-0-p-0-iso8859-1
+c0419bt_.pfb -bitstream-courier 10 pitch-medium-r-normal--0-0-0-0-m-0-iso10646-1
+c0419bt_.pfb -bitstream-courier 10 pitch-medium-r-normal--0-0-0-0-m-0-iso8859-1
+c0582bt_.pfb -bitstream-courier 10 pitch-medium-i-normal--0-0-0-0-m-0-iso10646-1
+c0582bt_.pfb -bitstream-courier 10 pitch-medium-i-normal--0-0-0-0-m-0-iso8859-1
+c0583bt_.pfb -bitstream-courier 10 pitch-bold-r-normal--0-0-0-0-m-0-iso10646-1
+c0583bt_.pfb -bitstream-courier 10 pitch-bold-r-normal--0-0-0-0-m-0-iso8859-1
+c0611bt_.pfb -bitstream-courier 10 pitch-bold-i-normal--0-0-0-0-m-0-iso10646-1
+c0611bt_.pfb -bitstream-courier 10 pitch-bold-i-normal--0-0-0-0-m-0-iso8859-1
+c0632bt_.pfb -bitstream-bitstream charter-bold-r-normal--0-0-0-0-p-0-iso10646-1
+c0632bt_.pfb -bitstream-bitstream charter-bold-r-normal--0-0-0-0-p-0-iso8859-1
+c0633bt_.pfb -bitstream-bitstream charter-bold-i-normal--0-0-0-0-p-0-iso10646-1
+c0633bt_.pfb -bitstream-bitstream charter-bold-i-normal--0-0-0-0-p-0-iso8859-1
+c0648bt_.pfb -bitstream-bitstream charter-medium-r-normal--0-0-0-0-p-0-iso10646-1
+c0648bt_.pfb -bitstream-bitstream charter-medium-r-normal--0-0-0-0-p-0-iso8859-1
+c0649bt_.pfb -bitstream-bitstream charter-medium-i-normal--0-0-0-0-p-0-iso10646-1
+c0649bt_.pfb -bitstream-bitstream charter-medium-i-normal--0-0-0-0-p-0-iso8859-1
+cursor.pfa -xfree86-cursor-medium-r-normal--0-0-0-0-p-0-adobe-fontspecific
diff --git a/e2e-tests/cypress/fonts/Type1/fonts.scale b/e2e-tests/cypress/fonts/Type1/fonts.scale
new file mode 100644
index 00000000..7d0a5f39
--- /dev/null
+++ b/e2e-tests/cypress/fonts/Type1/fonts.scale
@@ -0,0 +1,26 @@
+25
+UTBI____.pfa -adobe-utopia-bold-i-normal--0-0-0-0-p-0-iso10646-1
+UTBI____.pfa -adobe-utopia-bold-i-normal--0-0-0-0-p-0-iso8859-1
+UTB_____.pfa -adobe-utopia-bold-r-normal--0-0-0-0-p-0-iso10646-1
+UTB_____.pfa -adobe-utopia-bold-r-normal--0-0-0-0-p-0-iso8859-1
+UTI_____.pfa -adobe-utopia-medium-i-normal--0-0-0-0-p-0-iso10646-1
+UTI_____.pfa -adobe-utopia-medium-i-normal--0-0-0-0-p-0-iso8859-1
+UTRG____.pfa -adobe-utopia-medium-r-normal--0-0-0-0-p-0-iso10646-1
+UTRG____.pfa -adobe-utopia-medium-r-normal--0-0-0-0-p-0-iso8859-1
+c0419bt_.pfb -bitstream-courier 10 pitch-medium-r-normal--0-0-0-0-m-0-iso10646-1
+c0419bt_.pfb -bitstream-courier 10 pitch-medium-r-normal--0-0-0-0-m-0-iso8859-1
+c0582bt_.pfb -bitstream-courier 10 pitch-medium-i-normal--0-0-0-0-m-0-iso10646-1
+c0582bt_.pfb -bitstream-courier 10 pitch-medium-i-normal--0-0-0-0-m-0-iso8859-1
+c0583bt_.pfb -bitstream-courier 10 pitch-bold-r-normal--0-0-0-0-m-0-iso10646-1
+c0583bt_.pfb -bitstream-courier 10 pitch-bold-r-normal--0-0-0-0-m-0-iso8859-1
+c0611bt_.pfb -bitstream-courier 10 pitch-bold-i-normal--0-0-0-0-m-0-iso10646-1
+c0611bt_.pfb -bitstream-courier 10 pitch-bold-i-normal--0-0-0-0-m-0-iso8859-1
+c0632bt_.pfb -bitstream-bitstream charter-bold-r-normal--0-0-0-0-p-0-iso10646-1
+c0632bt_.pfb -bitstream-bitstream charter-bold-r-normal--0-0-0-0-p-0-iso8859-1
+c0633bt_.pfb -bitstream-bitstream charter-bold-i-normal--0-0-0-0-p-0-iso10646-1
+c0633bt_.pfb -bitstream-bitstream charter-bold-i-normal--0-0-0-0-p-0-iso8859-1
+c0648bt_.pfb -bitstream-bitstream charter-medium-r-normal--0-0-0-0-p-0-iso10646-1
+c0648bt_.pfb -bitstream-bitstream charter-medium-r-normal--0-0-0-0-p-0-iso8859-1
+c0649bt_.pfb -bitstream-bitstream charter-medium-i-normal--0-0-0-0-p-0-iso10646-1
+c0649bt_.pfb -bitstream-bitstream charter-medium-i-normal--0-0-0-0-p-0-iso8859-1
+cursor.pfa -xfree86-cursor-medium-r-normal--0-0-0-0-p-0-adobe-fontspecific
diff --git a/e2e-tests/cypress/fonts/google-noto-emoji/NotoColorEmoji.ttf b/e2e-tests/cypress/fonts/google-noto-emoji/NotoColorEmoji.ttf
new file mode 100644
index 00000000..69cf21a1
Binary files /dev/null and b/e2e-tests/cypress/fonts/google-noto-emoji/NotoColorEmoji.ttf differ
diff --git a/e2e-tests/cypress/integration/app_features_spec.js b/e2e-tests/cypress/integration/app_features_spec.js
index cb5261a2..f36439d9 100644
--- a/e2e-tests/cypress/integration/app_features_spec.js
+++ b/e2e-tests/cypress/integration/app_features_spec.js
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import moment from 'moment';
-const dateFormatter = value => moment(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
+const dateFormatter = value => moment.utc(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
describe('App Features Tests', () => {
beforeEach(() => {
diff --git a/e2e-tests/cypress/integration/badges_spec.js b/e2e-tests/cypress/integration/badges_spec.js
index aa9d2f25..88820073 100644
--- a/e2e-tests/cypress/integration/badges_spec.js
+++ b/e2e-tests/cypress/integration/badges_spec.js
@@ -19,26 +19,355 @@ describe('Badges Tests', () => {
cy.request('POST', '/app/projects/proj1', {
projectId: 'proj1',
name: "proj1"
- })
+ }).as('createProject');
+
+ Cypress.Commands.add("gemStartNextMonth", () => {
+ cy.get('[data-cy="startDatePicker"] header .next').first().click()
+ });
+ Cypress.Commands.add("gemStartPrevMonth", () => {
+ cy.get('[data-cy="startDatePicker"] header .prev').first().click()
+ });
+ Cypress.Commands.add("gemEndNextMonth", () => {
+ cy.get('[data-cy="endDatePicker"] header .next').first().click()
+ });
+ Cypress.Commands.add("gemEndPrevMonth", () => {
+ cy.get('[data-cy="endDatePicker"] header .prev').first().click()
+ });
+ Cypress.Commands.add("gemStartSetDay", (dayNum) => {
+ cy.get('[data-cy="startDatePicker"] .day').contains(dayNum).click()
+ });
+ Cypress.Commands.add("gemEndSetDay", (dayNum) => {
+ cy.get('[data-cy="endDatePicker"] .day').contains(dayNum).click()
+ });
+
+ cy.server();
+ cy.route('POST', '/admin/projects/proj1/badgeNameExists').as('nameExistsCheck');
+ cy.route('GET', '/admin/projects/proj1/badges').as('loadBadges');
+
});
it('create badge with special chars', () => {
const expectedId = 'LotsofspecialPcharsBadge';
- const providedName = "!L@o#t$s of %s^p&e*c(i)a_l++_|}{P c'ha'rs";
- cy.server().route('POST', `/admin/projects/proj1/badges/${expectedId}`).as('postNewBadge');
+ const providedName = "!L@o#t$s of %s^p&e*c(i)a_l++_|}{P/ c'ha'rs";
+
+ cy.route('POST', `/admin/projects/proj1/badges/${expectedId}`).as('postNewBadge');
+ cy.route('POST', '/admin/projects/proj1/badgeNameExists').as('nameExistsCheck');
+ cy.route('GET', '/admin/projects/proj1/badges').as('loadBadges');
+
+ cy.get('@createProject').should((response) => {
+ expect(response.status).to.eql(200)
+ });
cy.visit('/projects/proj1/badges');
- cy.clickButton('Badge')
+ cy.wait('@loadBadges');
+ cy.clickButton('Badge');
+
+ cy.get('#badgeName').type(providedName);
- cy.get('#badgeName').type(providedName)
+ cy.wait('@nameExistsCheck');
- cy.getIdField().should('have.value', expectedId)
+ cy.getIdField().should('have.value', expectedId);
- cy.clickSave()
+ cy.clickSave();
cy.wait('@postNewBadge');
cy.contains('ID: Lotsofspecial')
});
+ if('Close badge dialog', () => {
+ cy.route('GET', '/admin/projects/proj1/badges').as('loadBadges');
+
+ cy.get('@createProject').should((response) => {
+ expect(response.status).to.eql(200)
+ });
+
+ cy.visit('/projects/proj1/badges');
+ cy.wait('@loadBadges');
+ cy.clickButton('Badge');
+ cy.get('[data-cy=closeBadgeButton]').click();
+ cy.get('[data-cy=closeBadgeButton]').should('not.be.visible');
+ });
+
+ it('inactive badge displays warning', () => {
+ const expectedId = 'InactiveBadge';
+ const providedName = 'Inactive';
+ cy.server();
+ cy.route('GET', '/app/userInfo').as('getUserInfo');
+ cy.route('GET', '/app/userInfo/hasRole/ROLE_SUPERVISOR').as('hasSupervisor');
+ cy.route('POST', `/admin/projects/proj1/badges/${expectedId}`).as('postNewBadge');
+ cy.route('POST', '/admin/projects/proj1/badgeNameExists').as('nameExistsCheck');
+ cy.route('GET', '/admin/projects/proj1/badges').as('loadBadges');
+
+ cy.get('@createProject').should((response) => {
+ expect(response.status).to.eql(200)
+ });
+
+ cy.visit('/projects/proj1/badges');
+
+ cy.wait('@loadBadges');
+ cy.wait('@getUserInfo');
+ cy.wait('@hasSupervisor');
+ cy.clickButton('Badge');
+
+ cy.get('#badgeName').type(providedName);
+
+ cy.wait('@nameExistsCheck');
+
+ cy.clickSave();
+ cy.wait('@postNewBadge');
+
+ cy.get('div.card-body i.fa-exclamation-circle').should('be.visible');
+ });
+
+ it('name causes id to fail validation', () => {
+ cy.request('POST', '/admin/projects/proj1/badges/badgeExist', {
+ projectId: 'proj1',
+ name: "Badge Exist",
+ badgeId: 'badgeExist'
+ })
+
+ cy.visit('/projects/proj1/badges');
+ cy.clickButton('Badge');
+ cy.contains('New Badge');
+
+ // name causes id to be too long
+ const msg = 'Badge ID cannot exceed 50 characters';
+ const validNameButInvalidId = Array(46).fill('a').join('');
+ cy.get('[data-cy=badgeName]').click();
+ cy.get('[data-cy=badgeName]').invoke('val', validNameButInvalidId).trigger('input');
+ cy.get('[data-cy=idError]').contains(msg).should('be.visible');
+ cy.get('[data-cy=saveBadgeButton]').should('be.disabled');
+ cy.get('[data-cy=badgeName]').type('{backspace}');
+ cy.get('[data-cy=idError]').contains(msg).should('not.be.visible');
+ cy.get('[data-cy=saveBadgeButton]').should('be.enabled');
+ });
+
+ it.only('badge validation', () => {
+ // create existing badge
+ cy.request('POST', '/admin/projects/proj1/badges/badgeExist', {
+ projectId: 'proj1',
+ name: "Badge Exist",
+ badgeId: 'badgeExist'
+ })
+
+ cy.visit('/projects/proj1/badges');
+ cy.clickButton('Badge');
+ cy.contains('New Badge');
+
+ const overallFormValidationMsg = 'Form did NOT pass validation, please fix and try to Save again';
+
+ // name is too short
+ let msg = 'Badge Name cannot be less than 3 characters';
+ cy.get('#badgeName').type('Te');
+ cy.get('[data-cy=badgeNameError]').contains(msg).should('be.visible');
+ cy.get('[data-cy=saveBadgeButton]').should('be.disabled');
+ cy.get('#badgeName').type('Tes');
+ cy.get('[data-cy=badgeNameError]').should('not.be.visible');
+
+ // name too long
+ msg = 'Badge Name cannot exceed 50 characters';
+ cy.contains('Enable').click();
+ cy.getIdField().clear().type("badgeId");
+ const invalidName = Array(51).fill('a').join('');
+ cy.get('#badgeName').clear();
+ cy.get('#badgeName').invoke('val', invalidName).trigger('input');
+ cy.get('[data-cy=badgeNameError]').contains(msg).should('be.visible');
+ cy.get('[data-cy=saveBadgeButton]').should('be.disabled');
+ cy.get('#badgeName').type('{backspace}');
+ cy.get('[data-cy=badgeNameError]').should('not.be.visible');
+
+ // id too short
+ msg = 'Badge ID cannot be less than 3 characters';
+ cy.getIdField().clear().type("aa");
+ cy.get('[data-cy=idError]').contains(msg).should('be.visible');
+ cy.get('[data-cy=saveBadgeButton]').should('be.disabled');
+ cy.getIdField().type("a");
+ cy.get('[data-cy=idError]').should('not.be.visible');
+
+ // id too long
+ msg = 'Badge ID cannot exceed 50 characters';
+ const invalidId = Array(51).fill('a').join('');
+ cy.getIdField().clear()
+ cy.getIdField().click().type(invalidId);
+ cy.get('[data-cy=idError]').contains(msg).should('be.visible');
+ cy.getIdField().type('{backspace}');
+ cy.get('[data-cy=idError]').should('not.be.visible');
+
+ // id must not have special chars
+ msg = 'Badge ID may only contain alpha-numeric characters';
+ cy.getIdField().clear().type('With$Special');
+ cy.get('[data-cy=idError]').contains(msg).should('be.visible');
+ cy.getIdField().clear().type('GoodToGo');
+ cy.get('[data-cy=idError]').should('not.be.visible');
+
+ cy.getIdField().clear().type('SomeId');
+ // !L@o#t$s of %s^p&e*c(i)a_l++_|}{P/ c'ha'rs
+ let specialChars = [' ', '_', '!', '@', '#', '%', '^', '&', '*', '(', ')', '-', '+', '='];
+ specialChars.forEach((element) => {
+ cy.getIdField().type(element);
+ cy.get('[data-cy=idError]').contains(msg).should('be.visible');
+ cy.getIdField().type('{backspace}');
+ cy.contains(msg).should('not.be.visible');
+ })
+
+ // badge name must not be already taken
+ msg = 'The value for Badge Name is already taken';
+ cy.get('#badgeName').clear().type('Badge Exist');
+ cy.get('[data-cy=badgeNameError]').contains(msg).should('be.visible');
+ cy.get('#badgeName').type('1');
+ cy.get('[data-cy=badgeNameError]').should('not.be.visible');
+
+ // badge id must not already exist
+ msg = 'The value for Badge ID is already taken';
+ cy.getIdField().clear().type('badgeExist');
+ cy.get('[data-cy=idError]').contains(msg).should('be.visible');
+ cy.getIdField().type('1');
+ cy.get('[data-cy=idError]').should('not.be.visible');
+
+ // max description
+ msg='Badge Description cannot exceed 2000 characters';
+ const invalidDescription = Array(2000).fill('a').join('');
+ // it takes way too long using .type method
+ cy.get('#markdown-editor textarea').invoke('val', invalidDescription).trigger('change');
+ cy.get('#markdown-editor').type('a');
+ cy.get('[data-cy=badgeDescriptionError]').contains(msg).should('be.visible');
+ cy.get('#markdown-editor').type('{backspace}');
+ cy.get('[data-cy=badgeDescriptionError]').should('not.be.visible')
+
+ // finally let's save
+ cy.clickSave();
+ cy.wait('@loadBadges');
+ cy.contains('Badge Exist1');
+ });
+
+ it('gem start and end time validation', () => {
+ cy.visit('/projects/proj1/badges');
+ cy.clickButton('Badge');
+ cy.contains('New Badge');
+ cy.get('[data-cy="gemEditContainer"]').click()
+ cy.contains('Enable Gem Feature').click();
+ cy.contains('Start Date');
+
+ cy.get('#badgeName').type('Test Badge');
+
+ // dates should not overlap
+ let msg = 'Start Date must come before End Date';
+ cy.gemStartNextMonth();
+ cy.gemStartSetDay(1);
+ cy.gemEndNextMonth();
+ cy.gemEndSetDay(1);
+ cy.get('[data-cy=endDateError]').contains(msg).should('be.visible');
+ cy.gemEndSetDay(2);
+ cy.get('[data-cy=endDateError]').should('not.be.visible');
+
+ // start date should be before end date
+ msg = 'Start Date must come before End Date';
+ cy.gemStartSetDay(3);
+ cy.get('[data-cy=startDateError]').contains(msg).should('be.visible');
+ cy.gemEndSetDay(4);
+ cy.get('[data-cy=startDateError]').should('not.be.visible');
+
+ // dates should not be in the past
+ msg = 'End Date cannot be in the past';
+ cy.gemStartPrevMonth();
+ cy.gemStartPrevMonth();
+ cy.gemStartSetDay(1);
+ cy.gemEndPrevMonth();
+ cy.gemEndPrevMonth();
+ cy.gemEndSetDay(2);
+ cy.get('[data-cy=endDateError]').contains(msg).should('be.visible');
+
+ // should not save if there are validation errors
+ cy.get('[data-cy=saveBadgeButton]').should('be.disabled');
+
+ // fix the errors and save
+ cy.gemStartNextMonth();
+ cy.gemStartNextMonth();
+ cy.gemEndNextMonth();
+ cy.gemEndNextMonth();
+ cy.gemStartSetDay(1);
+ cy.gemEndSetDay(2);
+ cy.get('[data-cy=endDateError]').should('not.be.visible');
+ cy.get('[data-cy=saveBadgeButton]').should('be.enabled');
+
+ cy.clickSave();
+ cy.wait('@loadBadges');
+ cy.contains('Test Badge');
+ });
+
+ it('Badge is disabled when created, can only be enabled once', () => {
+ cy.visit('/projects/proj1/badges');
+ cy.clickButton('Badge');
+ cy.contains('New Badge');
+ cy.get('#badgeName').type('Test Badge');
+ cy.clickSave();
+ cy.wait('@loadBadges');
+
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=goLive]').click();
+ cy.contains('Please Confirm!').should('exist');
+ cy.contains('Yes, Go Live!').click();
+
+ cy.wait('@loadBadges');
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Live').should('exist');
+ cy.get('[data-cy=goLive]').should('not.exist');
+ });
+
+ it('Badge is disabled when created, canceling confirm dialog leaves badge disabled', () => {
+ cy.visit('/projects/proj1/badges');
+ cy.clickButton('Badge');
+ cy.contains('New Badge');
+ cy.get('#badgeName').type('Test Badge');
+ cy.clickSave();
+ cy.wait('@loadBadges');
+
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=goLive]').click();
+ cy.contains('Please Confirm!').should('exist');
+ cy.contains('Cancel').click();
+
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Live').should('not.exist');
+ cy.get('[data-cy=goLive]').should('exist');
+ });
+
+ it('Can add Skill requirements to disabled badge', () => {
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ name: "Subject 1"
+ });
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1/skills/skill1', {
+ projectId: 'proj1',
+ subjectId: "subj1",
+ skillId: "skill1",
+ name: "Skill 1",
+ pointIncrement: '50',
+ numPerformToCompletion: '5'
+ });
+
+ cy.visit('/projects/proj1/badges');
+ cy.clickButton('Badge');
+ cy.contains('New Badge');
+ cy.get('#badgeName').type('Test Badge');
+ cy.clickSave();
+ cy.wait('@loadBadges');
+ cy.get('[data-cy=manageBadge]').click();
+ cy.get('#skills-selector').click();
+ cy.get('#skills-selector input[type=text]').type('{enter}');
+ cy.contains('.router-link-active', 'Badges').click();
+ cy.contains('Test Badge').should('exist');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=goLive]').click();
+ cy.contains('Please Confirm!').should('exist');
+ cy.contains('Yes, Go Live!').click();
+ cy.wait('@loadBadges');
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Live').should('exist');
+ });
-})
+});
diff --git a/e2e-tests/cypress/integration/client-display/client-display-features_spec.js b/e2e-tests/cypress/integration/client-display/client-display-features_spec.js
index 4f60e3b4..cb97df2c 100644
--- a/e2e-tests/cypress/integration/client-display/client-display-features_spec.js
+++ b/e2e-tests/cypress/integration/client-display/client-display-features_spec.js
@@ -14,17 +14,16 @@
* limitations under the License.
*/
import moment from 'moment';
-const dateFormatter = value => moment(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
+const dateFormatter = value => moment.utc(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
describe('Client Display Features Tests', () => {
-
- before(() => {
- cy.disableUILogin();
- });
-
- after(function () {
- cy.enableUILogin();
- });
+ const snapshotOptions = {
+ blackout: ['[data-cy=pointHistoryChart]'],
+ failureThreshold: 0.03, // threshold for entire image
+ failureThresholdType: 'percent', // percent of image or number of pixels
+ customDiffConfig: { threshold: 0.01 }, // threshold for each pixel
+ capture: 'fullPage', // When fullPage, the application under test is captured in its entirety from top to bottom.
+ };
beforeEach(() => {
Cypress.env('disabledUILoginProp', true);
@@ -39,6 +38,23 @@ describe('Client Display Features Tests', () => {
helpUrl: 'http://doHelpOnThisSubject.com',
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
});
+
+ Cypress.Commands.add("createSkill", (num) => {
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill${num}`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: `skill${num}`,
+ name: `This is ${num}`,
+ type: 'Skill',
+ pointIncrement: 50,
+ numPerformToCompletion: 2,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+ version: 0,
+ helpUrl: 'http://doHelpOnThisSkill.com'
+ });
+ });
})
it('display new version banner when software is updated', () => {
@@ -77,6 +93,7 @@ describe('Client Display Features Tests', () => {
});
it('do not display new version banner if lib version in headers is older than lib version in local storage', () => {
+ const mockedLibVersion = dateFormatter(new Date() - 1000 * 60 * 60 * 24 * 5);
cy.server().route({
url: '/api/projects/proj1/subjects/subj1/summary',
status: 200,
@@ -96,7 +113,7 @@ describe('Client Display Features Tests', () => {
'helpUrl': 'http://doHelpOnThisSubject.com'
},
headers: {
- 'skills-client-lib-version': dateFormatter(new Date() - 1000 * 60 * 60 * 24)
+ 'skills-client-lib-version': mockedLibVersion
},
}).as('getSubjectSummary');
@@ -108,7 +125,7 @@ describe('Client Display Features Tests', () => {
'position': 1
},
headers: {
- 'skills-client-lib-version': dateFormatter(new Date() - 1000 * 60 * 60 * 24)
+ 'skills-client-lib-version': mockedLibVersion
},
}).as('getRank');
@@ -117,7 +134,7 @@ describe('Client Display Features Tests', () => {
status: 200,
response: { 'pointsHistory': [] },
headers: {
- 'skills-client-lib-version': dateFormatter(new Date() - 1000 * 60 * 60 * 24)
+ 'skills-client-lib-version': mockedLibVersion
},
}).as('getPointHistory');
@@ -133,4 +150,135 @@ describe('Client Display Features Tests', () => {
cy.contains('New Skills Software Version is Available').should('not.exist')
});
+ it('achieve level 5, then add new skill', () => {
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill1`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill1',
+ name: `This is 1`,
+ type: 'Skill',
+ pointIncrement: 50,
+ numPerformToCompletion: 2,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+ version: 0,
+ helpUrl: 'http://doHelpOnThisSkill.com'
+ });
+
+ cy.request('POST', `/api/projects/proj1/skills/skill1`, {userId: 'user0', timestamp: new Date().getTime()})
+ cy.request('POST', `/api/projects/proj1/skills/skill1`, {userId: 'user0', timestamp: new Date().getTime() - 1000*60*60*24})
+
+ cy.cdVisit('/');
+
+ cy.contains('Overall Points');
+
+ cy.get('[data-cy=subjectTile]').eq(0).contains('Subject 1')
+ cy.get('[data-cy=subjectTile]').eq(0).contains('Level 5')
+
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill2`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill2',
+ name: `This is 2`,
+ type: 'Skill',
+ pointIncrement: 50,
+ numPerformToCompletion: 2,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+ version: 0,
+ helpUrl: 'http://doHelpOnThisSkill.com'
+ });
+
+ cy.cdVisit('/');
+
+ cy.contains('Overall Points');
+
+ cy.get('[data-cy=subjectTile]').eq(0).contains('Subject 1')
+ cy.get('[data-cy=subjectTile]').eq(0).contains('Level 5')
+ });
+
+ it('deps are added to partially achieved skill', () => {
+ cy.createSkill(1);
+ cy.request('POST', `/api/projects/proj1/skills/skill1`, {userId: 'user0', timestamp: new Date().getTime()})
+ cy.createSkill(2);
+ cy.createSkill(3);
+ cy.request('POST', `/admin/projects/proj1/skills/skill1/dependency/skill2`)
+ cy.request('POST', `/admin/projects/proj1/skills/skill2/dependency/skill3`)
+
+ cy.cdVisit('/');
+
+ cy.cdClickSubj(0, 'Subject 1');
+
+ cy.matchImageSnapshot(`Subject-WithLockedSkills-ThatWerePartiallyAchieved`, snapshotOptions);
+
+ cy.cdClickSkill(0);
+ cy.contains('This is 1');
+ const expectedMsg = 'You were able to earn partial points before the dependencies were added';
+ cy.contains(expectedMsg);
+ // should render dependencies section
+ cy.contains('Dependencies');
+
+ cy.matchImageSnapshot(`LockedSkill-ThatWasPartiallyAchieved`, snapshotOptions);
+
+ // make sure the other locked skill doesn't contain the same message
+ cy.cdBack('Subject 1');
+ cy.cdClickSkill(1);
+ cy.contains('This is 2');
+ cy.contains(expectedMsg).should('not.exist');
+
+ // make sure the skill without deps doesn't have the message
+ cy.cdBack('Subject 1');
+ cy.cdClickSkill(2);
+ cy.contains('This is 3');
+ cy.contains(expectedMsg).should('not.exist');
+ });
+
+ it('deps are added to fully achieved skill', () => {
+ cy.createSkill(1);
+ cy.request('POST', `/api/projects/proj1/skills/skill1`, {
+ userId: 'user0',
+ timestamp: new Date().getTime()
+ })
+ cy.request('POST', `/api/projects/proj1/skills/skill1`, {
+ userId: 'user0',
+ timestamp: new Date().getTime() - 1000 * 60 * 24
+ })
+ cy.createSkill(2);
+ cy.request('POST', `/admin/projects/proj1/skills/skill1/dependency/skill2`)
+
+ cy.cdVisit('/');
+ cy.cdClickSubj(0, 'Subject 1');
+
+ cy.matchImageSnapshot(`Subject-WithLockedSkills-ThatWereFullyAchieved`, snapshotOptions);
+
+ cy.cdClickSkill(0);
+ cy.contains('This is 1');
+ const msg = "Congrats! You completed this skill before the dependencies were added";
+ cy.contains(msg);
+
+ cy.matchImageSnapshot(`LockedSkill-ThatWasFullyAchieved`, snapshotOptions);
+
+ // other skill should not have the message
+ cy.cdBack('Subject 1');
+ cy.cdClickSkill(1);
+ cy.contains('This is 2');
+ cy.contains(msg).should('not.exist');
+
+ // now let's achieve the dependent skill
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {
+ userId: 'user0',
+ timestamp: new Date().getTime()
+ })
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {
+ userId: 'user0',
+ timestamp: new Date().getTime() - 1000 * 60 * 24
+ })
+ cy.cdBack('Subject 1');
+ cy.cdClickSkill(0);
+ cy.contains('This is 1');
+ cy.contains(msg).should('not.exist');
+ });
+
})
diff --git a/e2e-tests/cypress/integration/client-display/client-display-markdown_spec.js b/e2e-tests/cypress/integration/client-display/client-display-markdown_spec.js
new file mode 100644
index 00000000..a69cff69
--- /dev/null
+++ b/e2e-tests/cypress/integration/client-display/client-display-markdown_spec.js
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import moment from 'moment';
+const dateFormatter = value => moment.utc(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
+
+describe('Client Display Markdown Tests', () => {
+ const snapshotOptions = {
+ blackout: ['[data-cy=pointHistoryChart]'],
+ failureThreshold: 0.03, // threshold for entire image
+ failureThresholdType: 'percent', // percent of image or number of pixels
+ customDiffConfig: { threshold: 0.01 }, // threshold for each pixel
+ capture: 'fullPage', // When fullPage, the application under test is captured in its entirety from top to bottom.
+ };
+
+ beforeEach(() => {
+ Cypress.env('disabledUILoginProp', true);
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: 'proj1'
+ });
+ })
+
+ it('subject\'s markdown', () => {
+ const markdown = "# Title1\n## Title2\n### Title 3\n#### Title 4\n##### Title 5\nTitle 6\n\n" +
+ "---\n" +
+ "# Emphasis\n" +
+ "italics: *italicized* or _italicized_\n\n" +
+ "bold: **bolded** or __bolded__\n\n" +
+ "combination **_bolded & italicized_**\n\n" +
+ "strikethrough: ~~struck~~\n\n" +
+ "---\n" +
+ "# Inline\n" +
+ "Inline `code` has `back-ticks around` it\n\n" +
+ "---\n" +
+ "# Multiline\n" +
+ "\n" +
+ "\n" +
+ "```\n" +
+ "import { SkillsDirective } from '@skilltree/skills-client-vue';\n" +
+ "Vue.use(SkillsDirective);\n" +
+ "```\n" +
+ "# Lists\n" +
+ "Ordered Lists:\n" +
+ "1. Item one\n" +
+ "1. Item two\n" +
+ "1. Item three (actual number does not matter)\n\n" +
+ "If List item has multiple lines of text, subsequent lines must be idented four spaces, otherwise list item numbers will reset, e.g.,\n" +
+ "1. item one\n" +
+ " paragrah one\n" +
+ "1. item two\n" +
+ "1. item three\n" +
+ "\n" +
+ "Unordered Lists\n" +
+ "* Item\n" +
+ "* Item\n" +
+ "* Item\n" +
+ "___\n" +
+ "# Links\n" +
+ "[in line link](https://www.somewebsite.com)\n" +
+ "___\n" +
+ "# Blockquotes\n" +
+ "> Blockquotes are very handy to emulate reply text.\n" +
+ "> This line is part of the same quote.\n\n" +
+ "# Horizontal rule\n" +
+ "Use three or more dashes, asterisks, or underscores to generate a horizontal rule line\n" +
+ "\n" +
+ "Separate me\n\n" +
+ "___\n\n" +
+ "Separate me\n\n" +
+ "---\n\n" +
+ "Separate me\n\n" +
+ "***\n\n" +
+ "# Emojis\n" +
+ ":star: :star: :star: :star:\n" +
+ "";
+
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ name: 'Subject 1',
+ helpUrl: 'http://doHelpOnThisSubject.com',
+ description: markdown
+ });
+
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill1`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: `skill1`,
+ name: `This is 1`,
+ type: 'Skill',
+ pointIncrement: 50,
+ numPerformToCompletion: 2,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ description: markdown,
+ version: 0,
+ helpUrl: 'http://doHelpOnThisSkill.com'
+ });
+
+ cy.request('POST', '/admin/projects/proj1/badges/badge1', {
+ projectId: 'proj1',
+ badgeId: 'badge1',
+ name: 'Badge 1',
+ "iconClass":"fas fa-ghost",
+ description: markdown,
+ });
+
+ cy.cdVisit('/');
+ cy.contains('Overall Points');
+
+ // check subject
+ cy.cdClickSubj(0, 'Subject 1');
+ cy.contains('Emphasis');
+ cy.matchImageSnapshot(`Markdown-subject`, snapshotOptions);
+
+ // check skill
+ cy.cdClickSkill(0);
+ cy.contains('This is 1');
+ cy.contains('Emphasis');
+ cy.matchImageSnapshot(`Markdown-skill`, snapshotOptions);
+
+ // check expanded skill
+ cy.cdBack('Subject 1');
+ cy.get('[data-cy=toggleSkillDetails]').click()
+ cy.contains('Overall Points Earned');
+ cy.matchImageSnapshot(`Markdown-Skill-Preview`, snapshotOptions);
+
+ cy.cdVisit('/');
+ cy.contains('Overall Points');
+
+ // check badge
+ cy.cdClickBadges();
+ cy.contains('Badges');
+ cy.contains('Emphasis');
+ cy.matchImageSnapshot(`Markdown-Badge`, snapshotOptions);
+ });
+});
diff --git a/e2e-tests/cypress/integration/client-display/client-display-theme_spec.js b/e2e-tests/cypress/integration/client-display/client-display-theme_spec.js
index c954aa3e..6a80c8fd 100644
--- a/e2e-tests/cypress/integration/client-display/client-display-theme_spec.js
+++ b/e2e-tests/cypress/integration/client-display/client-display-theme_spec.js
@@ -14,16 +14,16 @@
* limitations under the License.
*/
import moment from 'moment';
-const dateFormatter = value => moment(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
+const dateFormatter = value => moment.utc(value).format('YYYY-MM-DD[T]HH:mm:ss[Z]');
describe('Client Display Tests', () => {
const snapshotOptions = {
- blackout: ['[data-cy=pointHistoryChart]'],
+ blackout: ['[data-cy=pointHistoryChart]', '#dependent-skills-network', '[data-cy=achievementDate]'],
failureThreshold: 0.03, // threshold for entire image
failureThresholdType: 'percent', // percent of image or number of pixels
- customDiffConfig: { threshold: 0.1 }, // threshold for each pixel
- // capture: 'viewport', // capture viewport in screenshot
+ customDiffConfig: { threshold: 0.01 }, // threshold for each pixel
+ capture: 'fullPage', // When fullPage, the application under test is captured in its entirety from top to bottom.
};
const sizes = [
'iphone-6',
@@ -33,8 +33,6 @@ describe('Client Display Tests', () => {
];
before(() => {
- cy.disableUILogin();
-
Cypress.Commands.add("cdInitProjWithSkills", () => {
cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
projectId: 'proj1',
@@ -155,10 +153,6 @@ describe('Client Display Tests', () => {
});
- after(function () {
- cy.enableUILogin();
- });
-
beforeEach(() => {
Cypress.env('disabledUILoginProp', true);
cy.request('POST', '/app/projects/proj1', {
@@ -239,8 +233,6 @@ describe('Client Display Tests', () => {
cy.contains('This is 4');
cy.contains('Lorem ipsum dolor sit amet');
cy.contains('Achieved Dependencies');
- // wait for graph to finish animating
- cy.wait(4000);
cy.matchImageSnapshot(`Subject0-Skill3-Details_${size}`, snapshotOptions);
});
diff --git a/e2e-tests/cypress/integration/client-display/client-display_point_history_chart_spec.js b/e2e-tests/cypress/integration/client-display/client-display_point_history_chart_spec.js
new file mode 100644
index 00000000..0ef19ff4
--- /dev/null
+++ b/e2e-tests/cypress/integration/client-display/client-display_point_history_chart_spec.js
@@ -0,0 +1,736 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var moment = require('moment-timezone');
+
+describe('Client Display Tests', () => {
+
+ const waitForAnimation = 3000;
+
+ beforeEach(() => {
+ Cypress.env('disabledUILoginProp', true);
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: 'proj1'
+ });
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1', subjectId: 'subj1', name: 'Subject 1',
+ });
+ });
+
+ it('multiple achievements in the middle', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-13T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-14T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-15T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-16T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-17T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-18T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-19T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-20T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-21T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-22T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500,
+ 'name': 'Levels 1, 2, 3'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+
+ it('point history with data from server', () => {
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill1`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill1',
+ name: `This is 1`,
+ type: 'Skill',
+ pointIncrement: 100,
+ numPerformToCompletion: 5,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+ version: 0,
+ helpUrl: 'http://doHelpOnThisSkill.com'
+ });
+
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill2`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill2',
+ name: `This is 2`,
+ type: 'Skill',
+ pointIncrement: 100,
+ numPerformToCompletion: 5,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ version: 0,
+ });
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill3`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill3',
+ name: `This is 3`,
+ type: 'Skill',
+ pointIncrement: 100,
+ numPerformToCompletion: 2,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ version: 0,
+ });
+
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill4`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill4',
+ name: `This is 4`,
+ type: 'Skill',
+ pointIncrement: 100,
+ numPerformToCompletion: 2,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ version: 0,
+ });
+
+ const m = moment('2020-09-12 11', 'YYYY-MM-DD HH');
+ const orig = m.clone()
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(4, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(3, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(2, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(1, 'day').format('x')})
+
+ cy.server().route('/api/projects/proj1/pointHistory').as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ cy.contains('Levels 1, 2');
+
+ });
+
+
+ it('multiple achievements at the last date', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500,
+ 'name': 'Levels 1, 2, 3'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+ it('multiple achievements on first date', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-02T00:00:00.000+00:00',
+ 'points': 400,
+ 'name': 'Levels 1, 2, 3'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+ it('single achievements on the first date', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 500
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-02T00:00:00.000+00:00',
+ 'points': 400,
+ 'name': 'Level 1'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+ it('single achievement on the last date', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500,
+ 'name': 'Level 1'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+
+ it('achievements throughout time', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100,
+ 'name': 'Level 1'
+ },{
+ 'achievedOn': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300,
+ 'name': 'Level 2'
+ },{
+ 'achievedOn': '2020-09-08T00:00:00.000+00:00',
+ 'points': 400,
+ 'name': 'Level 3'
+ },{
+ 'achievedOn': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400,
+ 'name': 'Level 4'
+ },{
+ 'achievedOn': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500,
+ 'name': 'Level 5'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+
+ it('levels achieved on subsequent days', () => {
+ const data = {
+ 'pointsHistory': [{
+ 'dayPerformed': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100
+ }, {
+ 'dayPerformed': '2020-09-03T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-04T00:00:00.000+00:00',
+ 'points': 200
+ }, {
+ 'dayPerformed': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-06T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-07T00:00:00.000+00:00',
+ 'points': 300
+ }, {
+ 'dayPerformed': '2020-09-08T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-09T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-10T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400
+ }, {
+ 'dayPerformed': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500
+ }],
+ 'achievements': [{
+ 'achievedOn': '2020-09-02T00:00:00.000+00:00',
+ 'points': 100,
+ 'name': 'Level 1'
+ },{
+ 'achievedOn': '2020-09-05T00:00:00.000+00:00',
+ 'points': 300,
+ 'name': 'Level 2'
+ },{
+ 'achievedOn': '2020-09-06T00:00:00.000+00:00',
+ 'points': 300,
+ 'name': 'Level 3'
+ },{
+ 'achievedOn': '2020-09-11T00:00:00.000+00:00',
+ 'points': 400,
+ 'name': 'Level 4'
+ },{
+ 'achievedOn': '2020-09-12T00:00:00.000+00:00',
+ 'points': 500,
+ 'name': 'Level 5'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+
+ function createTimeline(start, numDays, startScore, increaseBy, increaseEvery, stopIncreasingAfterDays = -1) {
+ const m = moment.utc(start, 'YYYY-MM-DD HH');
+ const pointHistory = [];
+ let score = startScore;
+ for( let i=0; i i)) {
+ score += increaseBy;
+ }
+ pointHistory.push({
+ 'dayPerformed': m.clone().add(i, 'day').tz('UTC').format(),
+ 'points': score,
+ });
+ }
+ return pointHistory;
+ }
+
+ it('levels achieved on subsequent days with many days in the timeline', () => {
+ const pointHistory = createTimeline('2019-09-12', 120, 10, 10, 10);
+ pointHistory.forEach((value) => {
+ cy.log(value);
+ });
+ const data = {
+ 'pointsHistory': pointHistory,
+ 'achievements': [{
+ 'achievedOn': pointHistory[50].dayPerformed,
+ 'points': pointHistory[50].points,
+ 'name': 'Level 1'
+ },{
+ 'achievedOn': pointHistory[51].dayPerformed,
+ 'points': pointHistory[51].points,
+ 'name': 'Level 2'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+ it('rapid growth of points af start followed by no activity', () => {
+ const pointHistory = createTimeline('2019-09-12', 240, 10, 100, 7, 30);
+ cy.log(`Generated ${pointHistory.length} points`);
+ const data = {
+ 'pointsHistory': pointHistory,
+ 'achievements': [{
+ 'achievedOn': pointHistory[2].dayPerformed,
+ 'points': pointHistory[2].points,
+ 'name': 'Level 1'
+ },{
+ 'achievedOn': pointHistory[7].dayPerformed,
+ 'points': pointHistory[7].points,
+ 'name': 'Level 2'
+ },{
+ 'achievedOn': pointHistory[12].dayPerformed,
+ 'points': pointHistory[12].points,
+ 'name': 'Level 3'
+ },{
+ 'achievedOn': pointHistory[23].dayPerformed,
+ 'points': pointHistory[23].points,
+ 'name': 'Level 4'
+ },{
+ 'achievedOn': pointHistory[30].dayPerformed,
+ 'points': pointHistory[30].points,
+ 'name': 'Level 5'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+
+ cy.cdVisit('/');
+ cy.wait('@getPointHistory');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+
+ cy.contains('Reset Zoom').click();
+ cy.wait(waitForAnimation);
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot('PointHistoryChart-Reset');
+ });
+
+
+ it('subject: rapid growth of points af start followed by no activity', () => {
+ const pointHistory = createTimeline('2019-09-12', 240, 10, 100, 7, 30);
+ cy.log(`Generated ${pointHistory.length} points`);
+ const data = {
+ 'pointsHistory': pointHistory,
+ 'achievements': [{
+ 'achievedOn': pointHistory[2].dayPerformed,
+ 'points': pointHistory[2].points,
+ 'name': 'Level 1'
+ },{
+ 'achievedOn': pointHistory[7].dayPerformed,
+ 'points': pointHistory[7].points,
+ 'name': 'Level 2'
+ },{
+ 'achievedOn': pointHistory[12].dayPerformed,
+ 'points': pointHistory[12].points,
+ 'name': 'Level 3'
+ },{
+ 'achievedOn': pointHistory[23].dayPerformed,
+ 'points': pointHistory[23].points,
+ 'name': 'Level 4'
+ },{
+ 'achievedOn': pointHistory[30].dayPerformed,
+ 'points': pointHistory[30].points,
+ 'name': 'Level 5'
+ }]
+ }
+
+ cy.server().route({
+ url: '/api/projects/proj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistory');
+ cy.server().route({
+ url: '/api/projects/proj1/subjects/subj1/pointHistory',
+ status: 200,
+ response: data,
+ }).as('getPointHistorySubject');
+
+
+ cy.cdVisit('/');
+ cy.cdClickSubj(0);
+ cy.wait('@getPointHistorySubject');
+
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+ it('empty point history', () => {
+ cy.cdVisit('/');
+ cy.server().route('/api/projects/proj1/pointHistory').as('getPointHistory');
+ cy.wait('@getPointHistory')
+ // let's wait for animation to complete
+ cy.wait(waitForAnimation);
+
+ cy.get('[data-cy=pointHistoryChart]').matchImageSnapshot();
+ });
+
+
+});
+
diff --git a/e2e-tests/cypress/integration/client-display/client-display_spec.js b/e2e-tests/cypress/integration/client-display/client-display_spec.js
index 6c5ced61..9b0205a1 100644
--- a/e2e-tests/cypress/integration/client-display/client-display_spec.js
+++ b/e2e-tests/cypress/integration/client-display/client-display_spec.js
@@ -13,17 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import moment from 'moment';
+
describe('Client Display Tests', () => {
- const cssAttachedToNavigableCards = 'skills-navigable-item';
+ const snapshotOptions = {
+ blackout: ['[data-cy=pointHistoryChart]', '[data-cy=timePassed]'],
+ failureThreshold: 0.03, // threshold for entire image
+ failureThresholdType: 'percent', // percent of image or number of pixels
+ customDiffConfig: { threshold: 0.01 }, // threshold for each pixel
+ capture: 'fullPage', // When fullPage, the application under test is captured in its entirety from top to bottom.
+ };
- before(() => {
- cy.disableUILogin();
- });
-
- after(function () {
- cy.enableUILogin();
- });
+ const cssAttachedToNavigableCards = 'skills-navigable-item';
beforeEach(() => {
Cypress.env('disabledUILoginProp', true);
@@ -229,5 +231,71 @@ describe('Client Display Tests', () => {
cy.get('[data-cy=subjectTile]').should('not.exist');
});
+ it('display achieved date on skill overview page', () => {
+ const m = moment('2020-09-12 11', 'YYYY-MM-DD HH');
+ const orig = m.clone()
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(4, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(3, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(2, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(1, 'day').format('x')})
+ cy.cdVisit('/');
+ cy.cdClickSubj(0);
+ cy.cdClickSkill(1);
+
+ cy.get('[data-cy=achievementDate]').contains(`Achieved on ${orig.format("MMMM Do YYYY")}`);
+ cy.get('[data-cy=achievementDate]').contains(`${orig.fromNow()}`);
+
+ cy.matchImageSnapshot(`Skill-Overview-Achieved`, snapshotOptions);
+
+ cy.cdVisit('/?enableTheme=true');
+ cy.cdClickSubj(0);
+ cy.cdClickSkill(1);
+
+ cy.get('[data-cy=achievementDate]').contains(`Achieved on ${orig.format("MMMM Do YYYY")}`);
+ cy.get('[data-cy=achievementDate]').contains(`${orig.fromNow()}`);
+
+ cy.matchImageSnapshot(`Skill-Overview-Achieved-Themed`, snapshotOptions);
+
+ cy.setResolution('iphone-6');
+
+ cy.cdVisit('/');
+ cy.cdClickSubj(0);
+ cy.cdClickSkill(1);
+
+ cy.get('[data-cy=achievementDate]').contains(`Achieved on ${orig.format("MMMM Do YYYY")}`);
+ cy.get('[data-cy=achievementDate]').contains(`${orig.fromNow()}`);
+
+ cy.matchImageSnapshot(`Skill-Overview-Achieved-iphone6`, snapshotOptions);
+
+ cy.setResolution('ipad-2');
+
+ cy.cdVisit('/');
+ cy.cdClickSubj(0);
+ cy.cdClickSkill(1);
+
+ cy.get('[data-cy=achievementDate]').contains(`Achieved on ${orig.format("MMMM Do YYYY")}`);
+ cy.get('[data-cy=achievementDate]').contains(`${orig.fromNow()}`);
+
+ cy.matchImageSnapshot(`Skill-Overview-Achieved-ipad2`, snapshotOptions);
+
+ });
+
+ it('display achieved date on subject page when skill details are expanded', () => {
+ const m = moment('2020-09-12 11', 'YYYY-MM-DD HH');
+ const orig = m.clone()
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(4, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(3, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(2, 'day').format('x')})
+ cy.request('POST', `/api/projects/proj1/skills/skill2`, {userId: 'user0', timestamp: m.subtract(1, 'day').format('x')})
+ cy.cdVisit('/');
+ cy.cdClickSubj(0);
+
+ cy.get('[data-cy=toggleSkillDetails]').click();
+ cy.get('[data-cy=skillProgress]:nth-child(2) [data-cy=achievementDate]').contains(`Achieved on ${orig.format("MMMM Do YYYY")}`);
+ cy.get('[data-cy=skillProgress]:nth-child(2) [data-cy=achievementDate]').contains(`${orig.fromNow()}`);
+ });
+
});
diff --git a/e2e-tests/cypress/integration/error_pages_spec.js b/e2e-tests/cypress/integration/error_pages_spec.js
new file mode 100644
index 00000000..1d0ece23
--- /dev/null
+++ b/e2e-tests/cypress/integration/error_pages_spec.js
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe('Error Pages Tests', () => {
+
+ beforeEach(() => {
+ cy.server();
+ });
+
+ it('Project Does Not Exist', () => {
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/fake'
+ }).as('loadProject');
+ cy.visit('/projects/fake');
+ cy.wait('@loadProject');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/fake/subjects/fake'
+ }).as('loadSubject');
+ cy.visit('/projects/fake/subjects/fake');
+ cy.wait('@loadSubject');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/fake/subjects/fake/skills/fake'
+ }).as('loadSkill');
+
+ cy.visit('/projects/fake/subjects/fake/skills/fake');
+ cy.wait('@loadSkill');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/fake/badges/fake'
+ }).as('loadBadge');
+
+ cy.visit('/projects/fake/badges/fake');
+ cy.wait('@loadBadge');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+ });
+
+ it( 'User Not Authorized For Project', () => {
+ cy.register('user1', 'password1', false);
+ cy.register('user2', 'password2', false);
+ cy.logout();
+ cy.login('user1', 'password1');
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "proj1"
+ });
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ name: "Subject 1"
+ });
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1/skills/skill1', {
+ projectId: 'proj1',
+ subjectId: "subj1",
+ skillId: "skill1",
+ name: "Skill 1",
+ pointIncrement: '50',
+ numPerformToCompletion: '5'
+ });
+
+ cy.request('POST', '/admin/projects/proj1/badges/badge1', {
+ enabled:false,
+ projectId:"proj1",
+ name:"Badge1",
+ badgeId:"badge1",
+ description:"",
+ iconClass:"fas fa-award"
+ });
+ cy.logout();
+ cy.login('user2', 'password2');
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1'
+ }).as('loadProject');
+ cy.visit('/projects/proj1');
+ cy.wait('@loadProject');
+
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1/subjects/subj1'
+ }).as('loadSubject');
+ cy.visit('/projects/proj1/subjects/subj1');
+ cy.wait('@loadSubject');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1/subjects/subj1/skills/skill1'
+ }).as('loadSkill');
+ cy.visit('/projects/proj1/subjects/subj1/skills/skill1');
+ cy.wait('@loadSkill');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1/badges/badge1'
+ }).as('loadBadge');
+ cy.visit('/projects/proj1/badges/badge1');
+ cy.wait('@loadBadge');
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Project OR this Project does not exist');
+ });
+
+ it('Subject Not Found', () => {
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "proj1"
+ });
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1/subjects/fake'
+ }).as('loadSubject');
+
+ cy.visit('/projects/proj1/subjects/fake');
+ cy.wait('@loadSubject');
+
+ cy.get('[data-cy=notFoundExplanation]').should('be.visible');
+ cy.get('[data-cy=notFoundExplanation]').contains('Subject [fake] doesn\'t exist in project [proj1]');
+ })
+
+ it('Skill Not Found', () => {
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "proj1"
+ });
+
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ name: "Subject 1"
+ });
+
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1/subjects/subj1/skills/skill1'
+ }).as('loadSkill');
+ cy.visit('/projects/proj1/subjects/subj1/skills/skill1');
+ cy.wait('@loadSkill');
+
+ cy.get('[data-cy=notFoundExplanation]').should('be.visible');
+ cy.get('[data-cy=notFoundExplanation]').contains('Skill [skill1] doesn\'t exist.');
+ });
+
+ it('Badge Not Found', () => {
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "proj1"
+ });
+ cy.route({
+ method: 'GET',
+ url: '/admin/projects/proj1/badges/fake'
+ }).as('loadBadge');
+
+ cy.visit('/projects/proj1/badges/fake');
+ cy.wait('@loadBadge');
+
+ cy.get('[data-cy=notFoundExplanation]').should('be.visible');
+ cy.get('[data-cy=notFoundExplanation]').contains('Badge [fake] doesn\'t exist');
+ });
+
+ it('Global Badge Not Found', () => {
+ const supervisorUser = 'supervisor@skills.org';
+ cy.register(supervisorUser, 'password');
+ cy.login('root@skills.org', 'password');
+ cy.request('PUT', `/root/users/${supervisorUser}/roles/ROLE_SUPERVISOR`);
+ cy.logout();
+ cy.login(supervisorUser, 'password');
+
+ cy.route({
+ method: 'GET',
+ url: '/supervisor/badges/fake'
+ }).as('loadGlobalBadge');
+ cy.visit('/globalBadges/fake');
+ cy.wait('@loadGlobalBadge');
+
+ cy.get('[data-cy=notFoundExplanation]').should('be.visible');
+ cy.get('[data-cy=notFoundExplanation]').contains('GlobalBadge [fake] doesn\'t exist.');
+ });
+
+ it('Global Badge Not Authorized', () => {
+ const supervisorUser = 'supervisor@skills.org';
+ cy.register(supervisorUser, 'password');
+ cy.login('root@skills.org', 'password');
+ cy.request('PUT', `/root/users/${supervisorUser}/roles/ROLE_SUPERVISOR`);
+ cy.logout();
+ cy.login(supervisorUser, 'password');
+ cy.request('POST', '/supervisor/badges/globalBadge1', {
+ "enabled":false,
+ "originalBadgeId":"",
+ "name":"globalBadge1",
+ "badgeId":"globalBadge1",
+ "description":"",
+ "iconClass":"fas fa-award"
+ });
+ cy.register('user1', 'password1', false);
+ cy.logout();
+ cy.login('user1', 'password1');
+
+ cy.route('GET', '/supervisor/badges/globalBadge1').as('loadGlobalBadge');
+ cy.visit('/globalBadges/globalBadge1');
+ cy.wait('@loadGlobalBadge');
+
+ cy.get('[data-cy=notAuthorizedExplanation]').should('be.visible');
+ cy.get('[data-cy=notAuthorizedExplanation]').contains('You do not have permission to view/manage this Global Badge OR this Global Badge does not exist');
+ });
+
+
+});
diff --git a/e2e-tests/cypress/integration/global_badges_spec.js b/e2e-tests/cypress/integration/global_badges_spec.js
index f2301a7c..b9ebe09a 100644
--- a/e2e-tests/cypress/integration/global_badges_spec.js
+++ b/e2e-tests/cypress/integration/global_badges_spec.js
@@ -16,6 +16,7 @@
describe('Global Badges Tests', () => {
beforeEach(() => {
+ cy.server();
cy.logout();
const supervisorUser = 'supervisor@skills.org';
cy.register(supervisorUser, 'password');
@@ -27,12 +28,14 @@ describe('Global Badges Tests', () => {
it('Create badge with special chars', () => {
- const expectedId = 'JustABadgeBadge';
- const providedName = "JustABadge";
+ const expectedId = 'LotsofspecialPcharsBadge';
+ const providedName = "!L@o#t$s of %s^p&e*c/?#(i)a_l++_|}{P c'ha'rs";
cy.server();
cy.route('GET', `/supervisor/badges`).as('getGlobalBadges');
- cy.route('GET', '/app/userInfo/hasRole/ROLE_SUPERVISOR').as('checkSupervisorRole')
cy.route('PUT', `/supervisor/badges/${expectedId}`).as('postGlobalBadge');
+ cy.route('GET', `/supervisor/badges/id/${expectedId}/exists`).as('idExists');
+ cy.route('POST', '/supervisor/badges/name/exists').as('nameExists');
+ cy.route('GET', '/app/userInfo/hasRole/ROLE_SUPERVISOR').as('checkSupervisorRole')
cy.visit('/globalBadges');
cy.wait('@getGlobalBadges');
@@ -41,8 +44,9 @@ describe('Global Badges Tests', () => {
cy.clickButton('Badge');
cy.get('#badgeName').type(providedName);
-
+ cy.wait('@nameExists');
cy.clickSave();
+ cy.wait('@idExists');
cy.wait('@postGlobalBadge');
cy.contains(`ID: ${expectedId}`);
@@ -133,12 +137,13 @@ describe('Global Badges Tests', () => {
originalBadgeId: ''
});
- cy.contains('Badges').click();
+ cy.visit('/');
+ cy.clickNav('Badges');
cy.contains('Manage').click();
cy.get('.multiselect__tags').click();
cy.get('.multiselect__tags input').type('{enter}');
cy.get('div.table-responsive').should('be.visible');
- cy.get('li').contains('Levels').click();
+ cy.clickNav('Levels');
cy.get('.multiselect__tags').first().click();
cy.get('.multiselect__tags input').first().type('proj2{enter}');
@@ -154,28 +159,165 @@ describe('Global Badges Tests', () => {
cy.server();
cy.route('GET', `/supervisor/badges`).as('getGlobalBadges');
- cy.contains('Badges').click();
+ cy.visit('/');
+ cy.clickNav('Badges');
+ cy.wait('@getGlobalBadges');
+ });
+
+ it('Global Badge is disabled when created, can only be enabled once', () => {
+ const expectedId = 'TestBadgeBadge';
+ cy.route('GET', `/supervisor/badges`).as('getGlobalBadges');
+ cy.route('PUT', `/supervisor/badges/${expectedId}`).as('postGlobalBadge');
+ cy.route('GET', `/supervisor/badges/id/${expectedId}/exists`).as('idExists');
+ cy.route('POST', '/supervisor/badges/name/exists').as('nameExists');
+ cy.route('GET', '/app/userInfo/hasRole/ROLE_SUPERVISOR').as('checkSupervisorRole');
+ cy.route('GET', `/supervisor/badges/${expectedId}`).as('getExpectedBadge');
+
+ cy.visit('/globalBadges');
+ cy.wait('@getGlobalBadges');
+ cy.wait('@checkSupervisorRole');
+
+ cy.clickButton('Badge');
+
+ cy.get('#badgeName').type('Test Badge');
+ cy.wait('@nameExists');
+ cy.clickSave();
+ cy.wait('@postGlobalBadge');
+
+ cy.contains('Test Badge').should('exist');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=goLive]').click();
+ cy.contains('Please Confirm!').should('exist');
+ cy.contains('Yes, Go Live!').click();
+ cy.wait('@postGlobalBadge');
+ cy.wait('@getExpectedBadge');
cy.wait('@getGlobalBadges');
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Live').should('exist');
+ cy.get('[data-cy=goLive]').should('not.exist');
});
+ it('Canceling go live dialog should leave global badge disabled', () => {
+ const expectedId = 'TestBadgeBadge';
+ cy.route('GET', `/supervisor/badges`).as('getGlobalBadges');
+ cy.route('PUT', `/supervisor/badges/${expectedId}`).as('postGlobalBadge');
+ cy.route('GET', `/supervisor/badges/id/${expectedId}/exists`).as('idExists');
+ cy.route('POST', '/supervisor/badges/name/exists').as('nameExists');
+ cy.route('GET', '/app/userInfo/hasRole/ROLE_SUPERVISOR').as('checkSupervisorRole');
+
+ cy.visit('/globalBadges');
+ cy.wait('@getGlobalBadges');
+ cy.wait('@checkSupervisorRole');
+
+ cy.clickButton('Badge');
+
+ cy.get('#badgeName').type('Test Badge');
+ cy.wait('@nameExists');
+ cy.clickSave();
+ cy.wait('@postGlobalBadge');
+
+ cy.contains('Test Badge').should('exist');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=goLive]').click();
+ cy.contains('Please Confirm!').should('exist');
+ cy.contains('Cancel').click();
+ cy.contains('Test Badge');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Live').should('not.exist');
+ cy.get('[data-cy=goLive]').should('exist');
+ });
+
+ it('Can add Skill and Level requirements to disabled Global Badge', () => {
+ cy.route('GET', `/supervisor/badges`).as('getGlobalBadges');
+ cy.route('PUT', `/supervisor/badges/ABadgeBadge`).as('postGlobalBadge');
+ cy.route('GET', `/supervisor/badges/id/ABadgeBadge/exists`).as('idExists');
+ cy.route('POST', '/supervisor/badges/name/exists').as('nameExists');
+ //proj/subj/skill1
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "proj1"
+ });
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ name: "Subject 1"
+ });
+ cy.request('POST', `/admin/projects/proj1/subjects/subj1/skills/skill1`, {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ skillId: 'skill1',
+ name: `This is 1`,
+ type: 'Skill',
+ pointIncrement: 100,
+ numPerformToCompletion: 5,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ version: 0,
+ });
+ //proj/subj/skill2
+ cy.request('POST', '/app/projects/proj2', {
+ projectId: 'proj2',
+ name: "proj2"
+ });
+ cy.request('POST', '/admin/projects/proj2/subjects/subj1', {
+ projectId: 'proj2',
+ subjectId: 'subj1',
+ name: "Subject 1"
+ });
+ cy.request('POST', `/admin/projects/proj2/subjects/subj1/skills/skill1`, {
+ projectId: 'proj2',
+ subjectId: 'subj1',
+ skillId: 'skill1',
+ name: `This is 1`,
+ type: 'Skill',
+ pointIncrement: 100,
+ numPerformToCompletion: 5,
+ pointIncrementInterval: 0,
+ numMaxOccurrencesIncrementInterval: -1,
+ version: 0,
+ });
+
+ cy.visit('/');
+
+ cy.clickNav('Badges');
+ cy.wait('@getGlobalBadges');
+
+ cy.clickButton('Badge');
+
+ cy.get('#badgeName').type('A Badge');
+ cy.wait('@nameExists');
+ cy.clickSave();
+ cy.wait('@idExists');
+ cy.wait('@postGlobalBadge');
+
+ cy.contains('A Badge').should('exist');
+ cy.contains('Manage').click();
+ cy.get('.multiselect__tags').click();
+ cy.get('.multiselect__tags input').type('{enter}');
+ cy.get('div.table-responsive').should('be.visible');
+ cy.clickNav('Levels');
+
+ cy.get('.multiselect__tags').first().click();
+ cy.get('.multiselect__tags input').first().type('proj2{enter}');
+
+ cy.get('.multiselect__tags').last().click();
+ cy.get('.multiselect__tags input').last().type('5{enter}');
+
+ cy.contains('Add').click();
+ cy.get('#simple-levels-table').should('be.visible');
+
+ cy.contains('.router-link-active', 'Badges').click();
+ cy.wait('@getGlobalBadges');
+
+ cy.contains('A Badge').should('exist');
+
+ cy.get('[data-cy=badgeStatus]').contains('Status: Disabled').should('exist');
+ cy.get('[data-cy=goLive]').click();
+ cy.contains('Please Confirm!').should('exist');
+ cy.contains('Yes, Go Live!').click();
+ cy.wait('@getGlobalBadges');
+ cy.contains('A Badge').should('exist');
+ cy.get('[data-cy=badgeStatus]').contains('Status: Live').should('exist');
+ cy.get('[data-cy=goLive]').should('not.exist');
+ });
- // THIS DOES NOT PASS: will be handled bia #449
- // it('create badge with special chars', () => {
- // const expectedId = 'LotsofspecialPcharsBadge';
- // const providedName = "!L@o#t$s of %s^p&e*c(i)a_l++_|}{P c'ha'rs";
- // cy.server().route('GET', `/supervisor/badges`).as('getGlobalBadges');
- //
- // cy.visit('/globalBadges');
- // cy.wait('@getGlobalBadges')
- // cy.clickButton('Badge')
- //
- // cy.get('#badgeName').type(providedName)
- // cy.getIdField().should('have.value', expectedId)
- //
- // // cy.clickSave()
- // // cy.wait('@postNewBadge');
- // //
- // // cy.contains('ID: Lotsofspecial')
- // });
-
-})
+});
diff --git a/e2e-tests/cypress/integration/login_spec.js b/e2e-tests/cypress/integration/login_spec.js
index fd863efd..55c6f0f7 100644
--- a/e2e-tests/cypress/integration/login_spec.js
+++ b/e2e-tests/cypress/integration/login_spec.js
@@ -22,6 +22,7 @@ describe('Login Tests', () => {
.route('GET', '/app/projects').as('getProjects')
.route('GET', '/api/icons/customIconCss').as('getProjectsCustomIcons')
.route('GET', '/app/userInfo').as('getUserInfo')
+ .route('GET', '/app/oAuthProviders').as('getOAuthProviders')
.route('POST', '/performLogin').as('postPerformLogin');
});
@@ -32,12 +33,12 @@ describe('Login Tests', () => {
cy.get('#inputPassword').type('password');
cy.contains('Login').click();
- cy.wait('@getProjects').its('status').should('be', 200)
- .wait('@getUserInfo').its('status').should('be', 200);
+ cy.wait('@getProjects').its('status').should('equal', 200)
+ .wait('@getUserInfo').its('status').should('equal', 200);
cy.contains('Project');
cy.contains('My Projects');
- cy.contains('Inception');
+ cy.get('[data-cy=projectSearch]').should('be.visible');
});
it('form: bad password', () => {
@@ -102,7 +103,7 @@ describe('Login Tests', () => {
cy.visit('/');
cy.contains('Login').should('be.disabled');
- const expectedText = 'Email cannot be less than 5 characters.';
+ const expectedText = 'Email Address cannot be less than 5 characters.';
cy.get('#username').type('v@s');
cy.get('#inputPassword').type('12345678');
@@ -115,33 +116,11 @@ describe('Login Tests', () => {
cy.contains(expectedText).should('not.exist');
})
- it('disabled login - email must not exceed 73 chars', () => {
- cy.visit('/');
- cy.contains('Login').should('be.disabled');
-
- // valid email must be less than 73 chars
- const invalidEmail = Array(74-9).fill('a').join('');
- const validEmail = Array(73-9).fill('a').join('');
-
- // will be taken care of by email validator
- const expectedText = 'The Email field must be a valid email';
-
- cy.get('#username').type(`${invalidEmail}@mail.org`);
- cy.get('#inputPassword').type('12345678');
- cy.contains('Login').should('be.disabled');
- cy.contains(expectedText);
-
- cy.get('#username').clear();
- cy.get('#username').type(`${validEmail}@mail.org`);
- cy.contains('Login').should('be.enabled');
- cy.contains(expectedText).should('not.exist');
- })
-
it('disabled login - valid email format', () => {
cy.visit('/');
cy.contains('Login').should('be.disabled');
- const expectedText = 'The Email field must be a valid email';
+ const expectedText = 'Email Address must be a valid email';
cy.get('#username').type('notvalid');
cy.get('#inputPassword').type('12345678');
@@ -158,4 +137,12 @@ describe('Login Tests', () => {
cy.contains('Login').should('be.enabled');
cy.contains(expectedText).should('not.exist');
})
+
+ it('OAuth login is not enabled', () => {
+ cy.visit('/');
+ cy.contains('Login').should('be.disabled');
+
+ cy.wait('@getOAuthProviders').its('status').should('equal', 200)
+ cy.get('[data-cy=oAuthProviders]').should('not.exist');
+ })
});
diff --git a/e2e-tests/cypress/integration/markdown_spec.js b/e2e-tests/cypress/integration/markdown_spec.js
new file mode 100644
index 00000000..bf354b90
--- /dev/null
+++ b/e2e-tests/cypress/integration/markdown_spec.js
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe('Markdown Tests', () => {
+
+ const snapshotOptions = {
+ blackout: ['[data-cy=skillTableCellCreatedDate]'],
+ failureThreshold: 0.03, // threshold for entire image
+ failureThresholdType: 'percent', // percent of image or number of pixels
+ customDiffConfig: { threshold: 0.01 }, // threshold for each pixel
+ capture: 'fullPage', // When fullPage, the application under test is captured in its entirety from top to bottom.
+ };
+
+ beforeEach(() => {
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "proj1"
+ })
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1', {
+ projectId: 'proj1',
+ subjectId: 'subj1',
+ name: "Subject 1",
+ })
+ });
+
+ it('markdown features', () => {
+ cy.visit('/projects/proj1/');
+
+ const markdownInput = '[data-cy=markdownEditorInput]';
+ cy.get('[data-cy=cardSettingsButton]').click();
+ cy.contains('Edit').click();
+
+ const validateMarkdown = (markdown, snapshotName, expectedText = null, clickWrite = true) => {
+ if (clickWrite) {
+ cy.contains('Write').click();
+ }
+ cy.get(markdownInput).clear().type(markdown);
+ cy.contains('Preview').click();
+ // move focus away from Preview
+ cy.contains('Description').click();
+ if (expectedText) {
+ cy.contains(expectedText);
+ }
+ cy.matchImageSnapshot(snapshotName);
+ }
+ validateMarkdown('# Title1\n## Title2\n### Title 3\n#### Title 4\n##### Title 5\nTitle 6\n\n', 'Markdown-Titles', null,false);
+
+ const emphasisMarkdown = "italics: *italicized* or _italicized_\n\n" +
+ "bold: **bolded** or __bolded__\n\n" +
+ "combination **_bolded & italicized_**\n\n" +
+ "strikethrough: ~~struck~~\n\n";
+ validateMarkdown(emphasisMarkdown, 'Markdown-Emphasis');
+
+ validateMarkdown("Inline `code` has `back-ticks around` it\n\n", 'Markdown-Inline')
+
+ const multiLineCode = "Some text followed by code\n" +
+ "```\n" +
+ "const validateMarkdown = (markdown, snapshotName) => {\n" +
+ "}\n" +
+ "```";
+ validateMarkdown(multiLineCode, 'Markdown-MultiLineCode')
+
+ validateMarkdown('Some text:\n1. Item one\n1. Item two\n1. Item three (actual number does not matter)', 'Markdown-NumberedList')
+
+ validateMarkdown('List:\n* Item\n* Item\n* Item\n', 'Markdown-UnorderedList')
+
+ validateMarkdown('[in line link](https://www.somewebsite.com)', 'Markdown-Link')
+
+ const blockQuote = "# Blockquote:\n" +
+ "> Blockquotes are very handy to emulate reply text.\n" +
+ "> This line is part of the same quote.\n\n";
+ validateMarkdown(blockQuote, 'Markdown-blockquote');
+
+ validateMarkdown('Separate me\n\n___\n\nSeparate me\n\n---\n\nSeparate me\n\n***', 'Markdown-Separator')
+
+ validateMarkdown(':star: :star: :star: :star:', 'Markdown-emoji', '⭐ ⭐ ⭐ ⭐')
+ });
+
+ it('on skills pages', () => {
+ const markdown = "# Title1\n## Title2\n### Title 3\n#### Title 4\n##### Title 5\nTitle 6\n\n" +
+ "---\n" +
+ "# Emphasis\n" +
+ "italics: *italicized* or _italicized_\n\n" +
+ "bold: **bolded** or __bolded__\n\n" +
+ "combination **_bolded & italicized_**\n\n" +
+ "strikethrough: ~~struck~~\n\n" +
+ "---\n" +
+ "# Inline\n" +
+ "Inline `code` has `back-ticks around` it\n\n" +
+ "---\n" +
+ "# Multiline\n" +
+ "\n" +
+ "\n" +
+ "```\n" +
+ "import { SkillsDirective } from '@skilltree/skills-client-vue';\n" +
+ "Vue.use(SkillsDirective);\n" +
+ "```\n" +
+ "# Lists\n" +
+ "Ordered Lists:\n" +
+ "1. Item one\n" +
+ "1. Item two\n" +
+ "1. Item three (actual number does not matter)\n\n" +
+ "If List item has multiple lines of text, subsequent lines must be idented four spaces, otherwise list item numbers will reset, e.g.,\n" +
+ "1. item one\n" +
+ " paragrah one\n" +
+ "1. item two\n" +
+ "1. item three\n" +
+ "\n" +
+ "Unordered Lists\n" +
+ "* Item\n" +
+ "* Item\n" +
+ "* Item\n" +
+ "___\n" +
+ "# Links\n" +
+ "[in line link](https://www.somewebsite.com)\n" +
+ "___\n" +
+ "# Blockquotes\n" +
+ "> Blockquotes are very handy to emulate reply text.\n" +
+ "> This line is part of the same quote.\n\n" +
+ "# Horizontal rule\n" +
+ "Use three or more dashes, asterisks, or underscores to generate a horizontal rule line\n" +
+ "\n" +
+ "Separate me\n\n" +
+ "___\n\n" +
+ "Separate me\n\n" +
+ "---\n\n" +
+ "Separate me\n\n" +
+ "***\n\n" +
+ "# Emojis\n" +
+ ":star: :star: :star: :star:\n" +
+ "";
+ cy.request('POST', '/admin/projects/proj1/subjects/subj1/skills/skill1', {
+ projectId: 'proj1',
+ subjectId: "subj1",
+ skillId: "skill1",
+ name: "Skill 1",
+ pointIncrement: '50',
+ numPerformToCompletion: '5',
+ description: markdown
+ });
+ cy.visit('/projects/proj1/subjects/subj1/skills/skill1');
+
+ cy.contains('Description');
+ cy.contains('Level 0');
+ cy.contains('Emojis')
+ cy.contains('⭐ ⭐ ⭐ ⭐');
+ cy.matchImageSnapshot('Markdown-SkillsPage-Overview', snapshotOptions);
+
+ cy.visit('/projects/proj1/subjects/subj1');
+ cy.contains('Level 0');
+ const selectorSkillsRowToggle = 'table .VueTables__child-row-toggler';
+ cy.get(selectorSkillsRowToggle).click();
+ cy.contains('Description');
+ cy.contains('Emojis')
+ cy.contains('⭐ ⭐ ⭐ ⭐');
+ cy.matchImageSnapshot('Markdown-SubjectPage-SkillPreview', snapshotOptions);
+ });
+
+})
diff --git a/e2e-tests/cypress/integration/metrics_spec.js b/e2e-tests/cypress/integration/metrics_spec.js
new file mode 100644
index 00000000..fe5033ee
--- /dev/null
+++ b/e2e-tests/cypress/integration/metrics_spec.js
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe('Metrics Specs', () => {
+ beforeEach(() => {
+ cy.server()
+ .route('GET', '/metrics/global').as('getMetrics')
+ .route('GET', '/app/userInfo').as('getUserInfo')
+ });
+
+ it('global metrics page loads', function () {
+ cy.visit('/metrics');
+ cy.contains('No Metrics Yet').should('be.visible');
+ });
+
+});
diff --git a/e2e-tests/cypress/integration/password_reset_spec.js b/e2e-tests/cypress/integration/password_reset_spec.js
new file mode 100644
index 00000000..9cfd7467
--- /dev/null
+++ b/e2e-tests/cypress/integration/password_reset_spec.js
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe('Password Reset Tests', () => {
+
+ beforeEach(() => {
+ cy.logout();
+ cy.resetEmail();
+
+ cy.fixture('vars.json').then((vars) => {
+ cy.register(vars.rootUser, vars.defaultPass, true);
+ });
+
+ cy.login('root@skills.org', 'password');
+
+ cy.request({
+ method: 'POST',
+ url: '/root/saveEmailSettings',
+ body: {
+ host: 'localhost',
+ port: 1026,
+ 'protocol': 'smtp'
+ },
+ });
+
+ cy.request({
+ method: 'POST',
+ url: '/root/saveSystemSettings',
+ body: {
+ publicUrl: 'http://localhost:8082/',
+ resetTokenExpiration: 'PT2H'
+ }
+ });
+
+ cy.logout();
+
+ cy.server();
+ cy.route({
+ method: 'POST',
+ url: '/performPasswordReset'
+ }).as('performReset');
+ cy.route('GET', '/app/projects').as('getProjects')
+ cy.route('GET', '/app/userInfo').as('getUserInfo')
+ });
+
+ it('reset password', () => {
+ cy.register("test@skills.org", "apassword", false);
+ cy.visit('/');
+ cy.get('[data-cy=forgotPassword]').click();
+ cy.get('[data-cy=forgotPasswordEmail]').should('exist');
+ cy.get('[data-cy=forgotPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPassword').click();
+ cy.get('[data-cy=resetRequestConfirmation').should('exist');
+ cy.wait(11*1000); //request rest page redirects to login after 30 seconds
+ cy.get('[data-cy=login]').should('exist');
+ cy.getResetLink().then((resetLink) => {
+ cy.visit(resetLink);
+ cy.get('[data-cy=resetPasswordSubmit]').should('exist');
+ cy.get('[data-cy=resetPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPasswordNewPassword]').type('password2')
+ cy.get('[data-cy=resetPasswordConfirm]').type('password2');
+ cy.get('[data-cy=resetPasswordSubmit]').click();
+
+ cy.wait('@performReset');
+ cy.get('[data-cy=resetConfirmation]').should('exist');
+ cy.wait(11*1000) //will redirect to login page after 30 seconds
+ cy.get('[data-cy=login]').should('exist');
+ cy.get('#username').type('test@skills.org');
+ cy.get('#inputPassword').type('password2');
+ cy.get('[data-cy=login]').click();
+ cy.wait('@getProjects');
+ cy.wait('@getUserInfo');
+
+ cy.contains('Project');
+ cy.contains('My Projects');
+ });
+
+ });
+
+ it('reset password - wrong user', () => {
+ cy.register("test@skills.org", "apassword", false);
+ cy.visit('/');
+ cy.get('[data-cy=forgotPassword]').click();
+ cy.get('[data-cy=forgotPasswordEmail]').should('exist');
+ cy.get('[data-cy=forgotPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPassword').click();
+ cy.get('[data-cy=resetRequestConfirmation').should('exist');
+ cy.wait(11*1000); //request rest page redirects to login after 30 seconds
+ cy.get('[data-cy=login]').should('exist');
+ cy.getResetLink().then((resetLink) => {
+ cy.visit(resetLink);
+ cy.get('[data-cy=resetPasswordSubmit]').should('exist');
+ cy.get('[data-cy=resetPasswordEmail]').type('test2@skills.org');
+ cy.get('[data-cy=resetPasswordNewPassword]').type('password2')
+ cy.get('[data-cy=resetPasswordConfirm]').type('password2');
+ cy.get('[data-cy=resetPasswordSubmit]').click();
+
+ cy.wait('@performReset');
+ cy.get('[data-cy=resetError]').should('be.visible');
+ cy.get('[data-cy=resetPasswordSubmit]').should('be.disabled');
+ });
+ });
+
+ it('reset password - password confirmation mismatch', () => {
+ cy.register("test@skills.org", "apassword", false);
+ cy.visit('/');
+ cy.get('[data-cy=forgotPassword]').click();
+ cy.get('[data-cy=forgotPasswordEmail]').should('exist');
+ cy.get('[data-cy=forgotPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPassword').click();
+ cy.get('[data-cy=resetRequestConfirmation').should('exist');
+ cy.wait(11*1000); //request rest page redirects to login after 30 seconds
+ cy.get('[data-cy=login]').should('exist');
+ cy.getResetLink().then((resetLink) => {
+ cy.visit(resetLink);
+ cy.get('[data-cy=resetPasswordSubmit]').should('exist');
+ cy.get('[data-cy=resetPasswordEmail]').type('test2@skills.org');
+ cy.get('[data-cy=resetPasswordNewPassword]').type('password2')
+ cy.get('[data-cy=resetPasswordConfirm]').type('password');
+ cy.get('[data-cy=resetPasswordSubmit]').should('be.disabled');
+ });
+ });
+
+ it('reset password - user does not exist', () => {
+ cy.register("test@skills.org", "apassword", false);
+ cy.visit('/');
+ cy.get('[data-cy=forgotPassword]').click();
+ cy.get('[data-cy=forgotPasswordEmail]').should('exist');
+ cy.get('[data-cy=forgotPasswordEmail]').type('fake@skills.org');
+ cy.get('[data-cy=resetPassword').click();
+ cy.get('[data-cy=resetFailedError]').should('be.visible');
+ });
+
+ it('cannot use reset link twice', () => {
+ cy.register("test@skills.org", "apassword", false);
+ cy.visit('/');
+ cy.get('[data-cy=forgotPassword]').click();
+ cy.get('[data-cy=forgotPasswordEmail]').should('exist');
+ cy.get('[data-cy=forgotPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPassword').click();
+ cy.get('[data-cy=resetRequestConfirmation').should('exist');
+ cy.get('[data-cy=loginPage]').click();
+ cy.get('[data-cy=login]').should('exist');
+ cy.getResetLink().then((resetLink) => {
+ cy.visit(resetLink);
+ cy.get('[data-cy=resetPasswordSubmit]').should('exist');
+ cy.get('[data-cy=resetPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPasswordNewPassword]').type('password2')
+ cy.get('[data-cy=resetPasswordConfirm]').type('password2');
+ cy.get('[data-cy=resetPasswordSubmit]').click();
+
+ cy.wait('@performReset');
+ cy.get('[data-cy=resetConfirmation]').should('exist');
+ cy.get('[data-cy=loginPage]').click();
+ cy.get('[data-cy=login]').should('exist');
+
+ cy.visit(resetLink);
+ cy.get('[data-cy=resetPasswordSubmit]').should('exist');
+ cy.get('[data-cy=resetPasswordEmail]').type('test@skills.org');
+ cy.get('[data-cy=resetPasswordNewPassword]').type('password3')
+ cy.get('[data-cy=resetPasswordConfirm]').type('password3');
+ cy.get('[data-cy=resetPasswordSubmit]').click();
+
+ cy.wait('@performReset');
+ cy.get('[data-cy=resetError]').should('be.visible');
+ cy.get('[data-cy=resetPasswordSubmit]').should('be.disabled');
+ });
+ });
+
+ it('reset not enabled if required configurations not set', ()=>{
+ cy.login('root@skills.org', 'password');
+
+ cy.request({
+ method: 'POST',
+ url: '/root/saveSystemSettings',
+ body: {
+ publicUrl: '',
+ }
+ });
+ cy.logout();
+ cy.route('GET', '/public/isFeatureSupported?feature=passwordreset').as('isEnabled');
+ cy.visit('/');
+ cy.get('[data-cy=forgotPassword]').click();
+ cy.wait('@isEnabled');
+ cy.get('[data-cy=resetNotSupported]').should('be.visible');
+ cy.get('[data-cy=forgotPasswordEmail').should('have.length.lte', 0);
+ });
+});
diff --git a/e2e-tests/cypress/integration/projects_spec.js b/e2e-tests/cypress/integration/projects_spec.js
index 3a5156ba..81cb3ee3 100644
--- a/e2e-tests/cypress/integration/projects_spec.js
+++ b/e2e-tests/cypress/integration/projects_spec.js
@@ -19,15 +19,21 @@ describe('Projects Tests', () => {
.route('GET', '/app/projects').as('getProjects')
.route('GET', '/api/icons/customIconCss').as('getProjectsCustomIcons')
.route('GET', '/app/userInfo').as('getUserInfo')
+ .route('/admin/projects/proj1/users/root@skills.org/roles').as('getRolesForRoot');
});
it('Create new projects', function () {
- cy.visit('/');
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
cy.route('POST', '/app/projects/MyNewtestProject').as('postNewProject');
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
+
cy.clickButton('Project');
- cy.get('[data-vv-name="projectName"]').type("My New test Project")
+ cy.get('[data-cy="projectName"]').type("My New test Project")
cy.clickSave();
cy.wait('@postNewProject');
@@ -36,19 +42,37 @@ describe('Projects Tests', () => {
cy.contains('ID: MyNewtestProject')
});
+ it('Close new project dialog', () => {
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
+ cy.route('POST', '/app/projects/MyNewtestProject').as('postNewProject');
+
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
+
+ cy.clickButton('Project');
+ cy.get('[data-cy=closeProjectButton]').click();
+ cy.get('[data-cy="projectName"]').should('not.be.visible');
+ });
+
it('Duplicate project names are not allowed', () => {
cy.request('POST', '/app/projects/MyNewtestProject', {
projectId: 'MyNewtestProject',
name: "My New test Project"
})
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');
- cy.get('[data-vv-name="projectName"]').type("My New test Project")
-
- cy.contains('The value for the Project Name is already taken')
- cy.clickSave();
- cy.contains('***Form did NOT pass validation, please fix and try to Save again***')
+ cy.get('[data-cy="projectName"]').type("My New test Project")
+ cy.get('[data-cy=projectNameError]').contains('The value for the Project Name is already taken').should('be.visible')
+ cy.get('[data-cy=saveProjectButton]').should('be.disabled');
});
@@ -57,26 +81,36 @@ describe('Projects Tests', () => {
projectId: 'MyNewtestProject',
name: "My New test Project"
})
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');
- cy.get('[data-vv-name="projectName"]').type("Other Project Name")
+ cy.get('[data-cy="projectName"]').type("Other Project Name")
cy.contains('Enable').click();
cy.getIdField().clear().type("MyNewtestProject")
- cy.contains('The value for the Project ID is already taken')
- cy.clickSave();
- cy.contains('***Form did NOT pass validation, please fix and try to Save again***')
+ cy.get('[data-cy=idError]').contains('The value for the Project ID is already taken').should('be.visible');
+ cy.get('[data-cy=saveProjectButton]').should('be.disabled');
});
it('Project id autofill strips out special characters and spaces', () => {
const expectedId = 'LotsofspecialPchars';
- const providedName = "!L@o#t$s of %s^p&e*c(i)a_l++_|}{P c'ha'rs";
+ const providedName = "!L@o#t$s of %s^p&e*c(i)a_l++_|}/[]#?{P c'ha'rs";
cy.route('POST', `/app/projects/${expectedId}`).as('postNewProject');
+ cy.route('POST', '/app/projectExist').as('projectExists');
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');
- cy.get('[data-vv-name="projectName"]').type(providedName)
+ cy.get('[data-cy="projectName"]').type(providedName);
+ cy.wait('@projectExists');
cy.getIdField().should('have.value', expectedId)
cy.clickSave();
@@ -92,69 +126,77 @@ describe('Projects Tests', () => {
cy.route('POST', `/app/projects/${expectedId}`)
.as('postNewProject');
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');
- cy.get('[data-vv-name="projectName"]').type(providedName)
+ cy.get('[data-cy="projectName"]').type(providedName)
cy.getIdField().should('have.value', expectedId)
cy.clickSave();
cy.wait('@postNewProject');
cy.clickButton('Project');
- cy.get('[data-vv-name="projectName"]').type(providedName.toLowerCase())
+ cy.get('[data-cy="projectName"]').type(providedName.toLowerCase())
- cy.contains('The value for the Project Name is already taken')
+ cy.get('[data-cy=projectNameError').contains('The value for the Project Name is already taken').should('be.visible');
- cy.clickSave();
- cy.contains('***Form did NOT pass validation, please fix and try to Save again***')
+ cy.get('[data-cy=saveProjectButton]').should('be.disabled');
});
it('Once project id is enabled name-to-id autofill should be turned off', () => {
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');;
- cy.get('[data-vv-name="projectName"]').type('InitValue');
+ cy.get('[data-cy="projectName"]').type('InitValue');
cy.getIdField().should('have.value', 'InitValue');
cy.contains('Enable').click();
cy.contains('Enabled').not('a');
- cy.get('[data-vv-name="projectName"]').type('MoreValue');
+ cy.get('[data-cy="projectName"]').type('MoreValue');
cy.getIdField().should('have.value', 'InitValue');
- cy.get('[data-vv-name="projectName"]').clear();
+ cy.get('[data-cy="projectName"]').clear();
cy.getIdField().should('have.value', 'InitValue');
});
it('Project name is required', () => {
cy.server();
- cy.route({
- method: 'GET',
- url: '/app/projects'
- }).as('loadProjects');
- cy.visit('/')
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
cy.wait('@loadProjects');
- cy.clickButton('Project');;
+ cy.clickButton('Project');
cy.contains('Enable').click();
cy.getIdField().type('InitValue');
- cy.clickSave();
-
- cy.contains('The Project Name field is required')
- cy.contains('***Form did NOT pass validation, please fix and try to Save again***')
+ cy.get('[data-cy=saveProjectButton').should('be.disabled');
})
it('Project id is required', () => {
- cy.visit('/')
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');;
- cy.get('[data-vv-name="projectName"]').type('New Project');
+ cy.get('[data-cy="projectName"]').type('New Project');
cy.contains('Enable').click();
cy.getIdField().clear()
-
- cy.clickSave();
-
- cy.contains('The Project ID field is required')
- cy.contains('***Form did NOT pass validation, please fix and try to Save again***')
+ cy.get('[data-cy=idError]').contains('Project ID is required').should('be.visible');
+ cy.get('[data-cy=saveProjectButton').should('be.disabled');
})
@@ -163,24 +205,29 @@ describe('Projects Tests', () => {
const maxLenMsg = 'Project Name cannot exceed 50 characters';
const projId = 'ProjectId'
cy.route('POST', `/app/projects/${projId}`).as('postNewProject');
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
- cy.visit('/')
cy.clickButton('Project');;
cy.contains('Enable').click();
cy.getIdField().type('ProjectId')
- cy.get('[data-vv-name="projectName"]').type('12');
+ cy.get('[data-cy="projectName"]').type('12');
cy.contains(minLenMsg)
- cy.get('[data-vv-name="projectName"]').type('3');
+ cy.get('[data-cy="projectName"]').type('3');
cy.contains(minLenMsg).should('not.exist')
const longInvalid = Array(51).fill('a').join('');
const longValid = Array(50).fill('a').join('');
- cy.get('[data-vv-name="projectName"]').clear().type(longInvalid);
+ cy.get('[data-cy="projectName"]').clear().type(longInvalid);
cy.contains(maxLenMsg)
- cy.get('[data-vv-name="projectName"]').clear().type(longValid);
+ cy.get('[data-cy="projectName"]').clear().type(longValid);
cy.contains(maxLenMsg).should('not.exist')
cy.clickSave();
@@ -198,21 +245,26 @@ describe('Projects Tests', () => {
const longInvalid = Array(51).fill('a').join('');
const longValid = Array(50).fill('a').join('');
cy.route('POST', `/app/projects/${longValid}`).as('postNewProject');
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
- cy.visit('/')
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
cy.clickButton('Project');;
cy.contains('Enable').click();
cy.getIdField().type('12')
- cy.get('[data-vv-name="projectName"]').type(projName);
+ cy.get('[data-cy="projectName"]').type(projName);
cy.contains(minLenMsg)
cy.getIdField().type('3');
cy.contains(minLenMsg).should('not.exist')
- cy.getIdField().clear().type(longInvalid);
+ cy.getIdField().clear().click()
+ cy.getIdField().invoke('val', longInvalid).trigger('input');
cy.contains(maxLenMsg)
- cy.getIdField().clear().type(longValid);
+ cy.getIdField().clear().click().invoke('val', longValid).trigger('input');
cy.contains(maxLenMsg).should('not.exist')
cy.clickSave();
@@ -240,8 +292,12 @@ describe('Projects Tests', () => {
status: 200,
response: [{userId:'foo', userIdForDisplay: 'foo', first: 'foo', last: 'foo', dn: 'foo'}]
}).as('suggest');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/admin/projects/proj1').as('loadProject');
cy.visit('/projects/proj1/access');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProject');
cy.contains('Enter user id').type('foo');
cy.wait('@suggest');
@@ -270,8 +326,12 @@ describe('Projects Tests', () => {
status: 200,
response: [{userId:'foo', userIdForDisplay: 'foo', first: 'foo', last: 'foo', dn: 'foo'}]
}).as('suggest');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/admin/projects/proj1').as('loadProject');
cy.visit('/projects/proj1/access');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProject');
cy.contains('Enter user id').type('foo');
cy.wait('@suggest');
@@ -296,14 +356,19 @@ describe('Projects Tests', () => {
method: 'POST',
url: '/app/users/suggestDashboardUsers*',
}).as('suggest');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/admin/projects/proj1').as('loadProject');
cy.visit('/projects/proj1/access');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProject');
cy.contains('Enter user id').type('{enter}');
cy.wait('@suggest');
cy.contains('root@skills.org').click();
cy.clickButton('Add');
cy.wait('@addAdmin');
+ cy.wait('@getRolesForRoot');
cy.contains('Firstname LastName (root@skills.org)').should('exist');
});
@@ -322,15 +387,20 @@ describe('Projects Tests', () => {
method: 'POST',
url: '/app/users/suggestDashboardUsers*',
}).as('suggest');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/admin/projects/proj1').as('loadProject');
cy.visit('/projects/proj1/access');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProject');
cy.contains('Enter user id').type('root{enter}');
cy.wait('@suggest');
cy.contains('root@skills.org').click();
cy.clickButton('Add');
cy.wait('@addAdmin');
- cy.contains('Firstname LastName (root@skills.org)').should('be.visible');
+ cy.wait('@getRolesForRoot');
+ cy.contains('Firstname LastName (root@skills.org)')
});
it('Add Admin - forward slash character does not cause error', () => {
@@ -348,12 +418,98 @@ describe('Projects Tests', () => {
method: 'POST',
url: '/app/users/suggestDashboardUsers*',
}).as('suggest');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/admin/projects/proj1').as('loadProject');
cy.visit('/projects/proj1/access');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProject');
cy.contains('Enter user id').type('root/foo{enter}');
cy.wait('@suggest');
});
+ it('Root User Project Display', () => {
+
+ cy.request('POST', '/app/projects/proj1', {
+ projectId: 'proj1',
+ name: "one"
+ });
+
+ cy.request('POST', '/app/projects/proj2', {
+ projectId: 'proj2',
+ name: "two"
+ });
+
+ cy.request('POST', '/app/projects/proj3', {
+ projectId: 'proj3',
+ name: "three"
+ });
+
+ cy.request('POST', '/app/projects/proj4', {
+ projectId: 'proj4',
+ name: "four"
+ });
+ cy.logout();
+ cy.fixture('vars.json').then((vars) => {
+ cy.login(vars.rootUser, vars.defaultPass);
+ cy.route('GET', '/app/projects').as('default');
+ cy.route('GET', '/app/projects?search=one').as('searchOne');
+ cy.route('POST', '/root/pin/proj1').as('pinOne');
+ cy.route('DELETE', '/root/pin/proj1').as('unpinOne');
+ cy.route('GET', '/admin/projects/proj1/subjects').as('loadSubjects');
+
+ cy.visit('/');
+ //confirm that default project loading returns no projects for root user
+ cy.wait('@default');
+ cy.contains('No Projects Yet...').should('be.visible');
+ cy.get('[data-cy=projectSearch]').should('be.visible');
+
+ //search should return matching project based on name
+ cy.get('[data-cy=projectSearch]').type('one');
+ cy.wait('@searchOne');
+ cy.contains('ID: proj1').should('be.visible');
+
+ //pin project resulting from search
+ cy.get('[data-cy=pin]').click();
+ cy.wait('@pinOne');
+ cy.get('[data-cy=pinIcon]').should('have.class', 'pinned').and('not.have.class', 'notpinned');
+
+ //make sure that the root user can view a pinned project belonging to another user
+ cy.contains('Manage').click();
+ cy.wait('@loadSubjects');
+
+ cy.contains('Home').click();
+ //project search should retain the last searched value until it's cleared or the page is refreshed
+ cy.get('[data-cy=projectSearch]').should('have.value', 'one');
+ cy.wait('@searchOne');
+ cy.contains('ID: proj1').should('be.visible');
+
+ //clearing the search should result in the default load projects being called
+ cy.get('[data-cy=projectSearch]').click().clear();
+ cy.wait('@default');
+ //root user should only have pinned projects returned by default
+ cy.contains('ID: proj1').should('be.visible');
+
+ //search results should contain already pinned projects
+ cy.get('[data-cy=projectSearch]').click().type('o');
+ cy.contains('ID: proj1').should('be.visible');
+ cy.contains('ID: proj2').should('be.visible');
+ cy.contains('ID: proj4').should('be.visible');
+
+ cy.get('[data-cy=projectSearch]').click().clear();
+ cy.wait('@default');
+ //unpin a project
+ cy.get('[data-cy=pin]').click();
+ cy.wait('@unpinOne');
+ cy.get('[data-cy=pinIcon]').should('have.class', 'notpinned').and('not.have.class', 'pinned')
+ cy.get('[data-cy=nav-Metrics]').click();
+ cy.get('[data-cy=nav-Projects]').click();
+ cy.wait('@default');
+ //after removing all pinned projects, default load projects should return no results for root user
+ cy.contains('No Projects Yet...').should('be.visible');
+ });
+ });
+
});
diff --git a/e2e-tests/cypress/integration/register_root_user_spec.js b/e2e-tests/cypress/integration/register_root_user_spec.js
new file mode 100644
index 00000000..d818e6fa
--- /dev/null
+++ b/e2e-tests/cypress/integration/register_root_user_spec.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe('Register Root Users', () => {
+
+ beforeEach(() => {
+ cy.logout();
+ cy.clearDb();
+ });
+
+ afterEach(() => {
+ cy.logout();
+ cy.clearDb();
+
+ cy.fixture('vars.json').then((vars) => {
+ cy.register(vars.rootUser, vars.defaultPass, true);
+ cy.register(vars.defaultUser, vars.defaultPass);
+ })
+ });
+
+ it('register root user', () => {
+ cy.visit('/');
+ cy.contains('New SkillTree Root Account')
+ cy.get('#firstName').type("Robert")
+ cy.get('#lastName').type("Smith")
+ cy.get('#email').type("rob.smith@madeup.org")
+ cy.get('#password').type("password")
+ cy.get('#password_confirmation').type("password")
+ cy.contains('Create Account').click()
+
+ cy.get('[data-cy=projectSearch]').should('be.visible');
+ });
+});
diff --git a/e2e-tests/cypress/integration/register_user_spec.js b/e2e-tests/cypress/integration/register_user_spec.js
new file mode 100644
index 00000000..d215ae9f
--- /dev/null
+++ b/e2e-tests/cypress/integration/register_user_spec.js
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe('Register Dashboard Users', () => {
+
+ beforeEach(() => {
+ cy.logout();
+ });
+
+ it('navigate between login and sign up page', () => {
+ cy.visit('/');
+ cy.contains('Sign in to SkillTree Dashboard')
+ cy.contains('Sign up').click()
+ cy.contains('New SkillTree Account')
+ cy.contains('Sign in').click()
+ cy.contains('Sign in to SkillTree Dashboard')
+ });
+
+ it('register dashboard user', () => {
+ cy.visit('/request-account');
+ cy.contains('New SkillTree Account')
+ cy.get('#firstName').type("Robert")
+ cy.get('#lastName').type("Smith")
+ cy.get('#email').type("rob.smith@madeup.org")
+ cy.get('#password').type("password")
+ cy.get('#password_confirmation').type("password")
+ cy.contains('Create Account').click()
+
+ cy.contains('No Projects Yet')
+ });
+
+ it('register dashboard validation', () => {
+ cy.visit('/request-account');
+ cy.contains('New SkillTree Account')
+ cy.get('#firstName').type("Robert")
+ cy.get('#lastName').type("Smith")
+ cy.get('#email').type("rob.smith@madeup.org")
+ cy.get('#password').type("password")
+ cy.get('#password_confirmation').type("password")
+
+
+ // password mismatch via confirmation pass
+ cy.get('#password_confirmation').clear().type("password1")
+ cy.contains('Create Account').should('be.disabled');
+ cy.contains('Password confirmation does not match')
+ cy.get('#password_confirmation').clear().type("password")
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('Password confirmation does not match').should('not.exist')
+
+ // password mismatch via main pass
+ cy.get('#password').clear().type("password1")
+ cy.contains('Create Account').should('be.disabled');
+ cy.contains('Password confirmation does not match')
+ cy.get('#password').clear().type("password")
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('Password confirmation does not match').should('not.exist')
+
+ // password must be at least 8 chars
+ cy.get('#password').clear().type("passwor")
+ cy.contains('Create Account').should('be.disabled');
+ cy.contains('Password cannot be less than 8 characters')
+ cy.get('#password').clear().type("password")
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('Password cannot be less than 8 characters').should('not.exist')
+
+ // password must not exceed 40 characters
+ const invalidPassword = Array(41).fill('a').join('');
+ const validPassword = Array(40).fill('a').join('');
+ cy.get('#password').clear().type(invalidPassword)
+ cy.get('#password_confirmation').clear().type(invalidPassword)
+ cy.contains('Password cannot exceed 40 characters')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#password').type('{backspace}')
+ cy.get('#password_confirmation').type('{backspace}')
+ cy.contains('Password cannot exceed 40 characters').should('not.exist')
+ cy.contains('Create Account').should('be.enabled');
+
+ // email must be at least 5 chars
+ cy.get('#email').clear().type("1234")
+ cy.contains('Email must be a valid email')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#email').clear().type("rob.smith@madeup.org")
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('Email must be a valid email').should('not.exist')
+
+ /*
+ emails over 73 characters are considered valid in vee-validate 3+
+ // email must not exceed 73 chars
+ const invalidEmail = Array(74-9).fill('a').join('');
+ const validEmail = Array(73-9).fill('a').join('');
+ cy.get('#email').clear().type(`${invalidEmail}@mail.org`)
+ cy.contains('The Email field must be a valid email').should('be.visible')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#email').clear().type(`${validEmail}@mail.org`)
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('The Email field must be a valid email').should('not.exist')
+ */
+
+ // email already taken
+ cy.get('#email').clear().type('skills@skills.org')
+ cy.contains('The email address is already used for another account')
+ cy.contains('Create Account').should('be.disabled');
+
+ cy.get('#email').clear().type('skills1@skills.org')
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('The email address is already used for another account').should('not.exist')
+
+ // valid email
+ cy.get('#email').clear().type("rob.smithmadeup.org")
+ cy.contains('Email must be a valid email')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#email').clear().type("rob.smith@madeup.org")
+ cy.contains('Email must be a valid email').should('not.exist')
+ cy.contains('Create Account').should('be.enabled');
+
+ // first name must not be empty
+ cy.get('#firstName').clear()
+ cy.contains('First Name is required')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#firstName').type('Robert')
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('First Name is required').should('not.exist')
+
+ // first name must not exceed 30 characters
+ const thirtyOneChars = Array(31).fill('a').join('');
+ cy.get('#firstName').clear().type(thirtyOneChars)
+ cy.contains('First Name cannot exceed 30 characters')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#firstName').type('{backspace}')
+ cy.contains('First Name exceed 30 characters').should('not.exist')
+ cy.contains('Create Account').should('be.enabled');
+
+ // last name must not be empty
+ cy.get('#lastName').clear()
+ cy.contains('Last Name is required')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#lastName').type('Smith')
+ cy.contains('Create Account').should('be.enabled');
+ cy.contains('Last Name is required').should('not.exist')
+
+ // last name must not exceed 30 characters
+ cy.get('#lastName').clear().type(thirtyOneChars)
+ cy.contains('Last Name cannot exceed 30 characters')
+ cy.contains('Create Account').should('be.disabled');
+ cy.get('#lastName').type('{backspace}')
+ cy.contains('Last Name cannot exceed 30 characters').should('not.exist')
+ cy.contains('Create Account').should('be.enabled');
+ });
+
+});
diff --git a/e2e-tests/cypress/integration/settings_spec.js b/e2e-tests/cypress/integration/settings_spec.js
index 5b8249a2..25f9762b 100644
--- a/e2e-tests/cypress/integration/settings_spec.js
+++ b/e2e-tests/cypress/integration/settings_spec.js
@@ -50,6 +50,10 @@ describe('Settings Tests', () => {
cy.server();
cy.route('POST', '/root/users/without/role/ROLE_SUPER_DUPER_USER?userSuggestOption=ONE').as('getEligibleForRoot');
cy.route('PUT', '/root/users/skills@skills.org/roles/ROLE_SUPER_DUPER_USER').as('addRoot');
+ cy.route('GET', '/app/projects').as('loadProjects');
+ cy.route('GET', '/app/userInfo/hasRole/ROLE_SUPERVISOR').as('isSupervisor');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/public/config').as('loadConfig');
cy.route({
method: 'GET',
url: '/app/projects'
@@ -57,6 +61,10 @@ describe('Settings Tests', () => {
cy.route({method: 'GET', url: '/root/isRoot'}).as('checkRoot');
cy.visit('/');
+ cy.wait('@loadConfig');
+ cy.wait('@loadUserInfo');
+ cy.wait('@loadProjects');
+ cy.wait('@isSupervisor');
cy.contains('My Projects');
cy.get('button.dropdown-toggle').first().click({force: true});
cy.contains('Settings').click();
@@ -66,6 +74,7 @@ describe('Settings Tests', () => {
cy.wait('@getEligibleForRoot');
});
+
it('Add Root User With No Query', () => {
cy.server();
cy.route('POST', '/root/users/without/role/ROLE_SUPER_DUPER_USER?userSuggestOption=ONE').as('getEligibleForRoot');
@@ -119,6 +128,33 @@ describe('Settings Tests', () => {
cy.get('[data-cy=navigationmenu]').contains('Badges', {timeout: 5000}).should('be.visible');
});
+ it('Add Supervisor User Not Found', () => {
+ cy.server();
+ // cy.route('PUT', '/root/users/root@skills.org/roles/ROLE_SUPERVISOR').as('addSupervisor');
+ cy.route('POST', 'root/users/without/role/ROLE_SUPERVISOR?userSuggestOption=ONE', [{"userId":"blah@skills.org","userIdForDisplay":"blah@skills.org","first":"Firstname","last":"LastName","nickname":"Firstname LastName","dn":null}]).as('getEligibleForSupervisor');
+ cy.route({
+ method: 'GET',
+ url: '/app/projects'
+ }).as('loadProjects');
+ cy.route({method: 'GET', url: '/root/isRoot'}).as('checkRoot');
+
+ cy.visit('/');
+ cy.contains('My Projects');
+
+ cy.get('li').contains('Badges').should('not.exist');
+ cy.vuex().its('state.access.isSupervisor').should('equal', false);
+ cy.get('button.dropdown-toggle').first().click({force: true});
+ cy.contains('Settings').click();
+ cy.wait('@checkRoot');
+ cy.contains('Security').click();
+ cy.get('[data-cy=supervisorrm] div.multiselect__tags').type('root');
+ cy.wait('@getEligibleForSupervisor');
+ cy.get('[data-cy=supervisorrm]').contains('blah@skills.org').click();
+ cy.get('[data-cy=supervisorrm]').contains('Add').click();
+ cy.get('[data-cy=error-msg]').contains('Error! Request could not be completed! User [blah@skills.org] does not exist')
+ cy.vuex().its('state.access.isSupervisor').should('equal', false);
+ });
+
it('Add Supervisor User No Query', () => {
cy.server();
@@ -142,4 +178,290 @@ describe('Settings Tests', () => {
cy.get('[data-cy=supervisorrm] div.multiselect__tags').type('foo/bar{enter}');
cy.wait('@getEligibleForSupervisor');
});
+
+ it('Email Server Settings', () => {
+ cy.server();
+ cy.route('GET', '/root/getEmailSettings').as('loadEmailSettings');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.get('.userName').parent().click();
+ cy.contains('Settings').click();
+ cy.contains('Email').click();
+ cy.wait('@loadEmailSettings');
+ cy.get$('[data-cy=hostInput]').clear();
+ cy.get$('[data-cy=hostError]').contains('Host is required');
+ cy.get$('[data-cy=emailSettingsSave]').should('be.disabled');
+ cy.get$('[data-cy=hostInput]').type('{selectall}localhost');
+ cy.get$('[data-cy=portInput]').clear();
+ cy.get$('[data-cy=portError]').contains('Port is required');
+ cy.get$('[data-cy=emailSettingsSave]').should('be.disabled');
+ cy.get$('[data-cy=portInput]').type('{selectall}-55');
+ cy.get$('[data-cy=portError]').contains('Port must be 1 or greater');
+ cy.get$('[data-cy=portInput]').type('{selectall}65536');
+ cy.get$('[data-cy=portError]').contains('Port must be 65535 or less');
+ cy.get$('[data-cy=portInput]').type('{selectall}1026');
+ cy.get$('[data-cy=protocolInput]').clear();
+ cy.get$('[data-cy=protocolError').contains('Protocol is required');
+ cy.get$('[data-cy=emailSettingsSave]').should('be.disabled');
+ cy.get$('[data-cy=protocolInput]').type('{selectall}smtp');
+
+
+ cy.get$('[data-cy=tlsSwitch]').next('.custom-control-label').click();
+ cy.get$('[data-cy=authSwitch]').next('.custom-control-label').click();
+ cy.get('[data-cy=emailUsername]').should('be.visible');
+ cy.get('[data-cy=emailPassword]').should('be.visible');
+ cy.get$('[data-cy=emailUsername]').type('username');
+ cy.get$('[data-cy=emailPassword]').type('password');
+ cy.get$('[data-cy=emailSettingsTest]').click();
+ cy.get$('[data-cy=emailSettingsSave]').click();
+ //verify that appropriate saved data is loaded when form is loaded again
+ cy.contains('System').click();
+ cy.visit('/settings/email');
+ cy.wait('@loadEmailSettings');
+ cy.get('[data-cy=hostInput]').should('have.value', 'localhost');
+ cy.get('[data-cy=portInput]').should('have.value', '1026');
+ cy.get('[data-cy=protocolInput]').should('have.value', 'smtp');
+ cy.get('[data-cy=tlsSwitch]').should('have.value', 'true');
+ cy.get('[data-cy=authSwitch]').should('have.value', 'true');
+ cy.get('[data-cy=emailUsername]').should('have.value', 'username');
+ cy.get('[data-cy=emailPassword]').should('have.value', 'password');
+ });
+
+ it('Email Settings reasonable timeout', () => {
+ cy.server();
+ cy.route('GET', '/root/getEmailSettings').as('loadEmailSettings');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.get('.userName').parent().click();
+ cy.contains('Settings').click();
+ cy.contains('Email').click();
+ cy.wait('@loadEmailSettings');
+ cy.get$('[data-cy=hostInput]').type('{selectall}localhost');
+ //this needs to be an open port that is NOT an smtp server for the purposes of this test
+ cy.get$('[data-cy=portInput]').type('{selectall}8080');
+ cy.get$('[data-cy=protocolInput]').type('{selectall}smtp');
+
+ cy.get$('[data-cy=emailSettingsSave]').click();
+ cy.wait(12*1000);
+ cy.get('[data-cy=connectionError]').should('be.visible');
+ });
+
+ it('System Settings', () => {
+ cy.server();
+ cy.route('GET', '/root/getSystemSettings').as('loadSystemSettings');
+ cy.route('GET', '/app/userInfo').as('loadUserInfo');
+ cy.route('GET', '/public/config').as('loadConfig');
+ cy.visit('/');
+ cy.wait('@loadUserInfo');
+ cy.get('.userName').parent().click();
+ cy.contains('Settings').click();
+ cy.contains('System').click();
+
+ cy.wait('@loadSystemSettings');
+ cy.get('[data-cy=resetTokenExpiration]').should('have.value', '2H');
+ cy.get$('[data-cy=publicUrl]').type('{selectall}http://localhost:8082');
+ cy.get$('[data-cy=resetTokenExpiration]').type('{selectall}2H25M22S');
+ cy.get$('[data-cy=fromEmail]').type('{selectall}foo@skilltree.madeup');
+ cy.get$('[data-cy=customHeader').type('{selectall}
');
+ cy.get('[data-cy=customHeaderError]').should('be.visible');
+ cy.get('[data-cy=customHeaderError]').contains('
-
-
diff --git a/frontend/src/components/access/RequestAccess.vue b/frontend/src/components/access/RequestAccess.vue
deleted file mode 100644
index c1ebe176..00000000
--- a/frontend/src/components/access/RequestAccess.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
-Copyright 2020 SkillTree
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-
-
-
-
-
Create Skills Dashboard Account
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/components/levels/NewLevel.vue b/frontend/src/components/levels/NewLevel.vue
deleted file mode 100644
index 8b3040cc..00000000
--- a/frontend/src/components/levels/NewLevel.vue
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
-Copyright 2020 SkillTree
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/components/settings/EmailServerSettings.vue b/frontend/src/components/settings/EmailServerSettings.vue
deleted file mode 100644
index 93e3887e..00000000
--- a/frontend/src/components/settings/EmailServerSettings.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
-Copyright 2020 SkillTree
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-
-
-
-
-
diff --git a/frontend/src/components/skills/EditSkill.vue b/frontend/src/components/skills/EditSkill.vue
deleted file mode 100644
index 5866faf9..00000000
--- a/frontend/src/components/skills/EditSkill.vue
+++ /dev/null
@@ -1,480 +0,0 @@
-/*
-Copyright 2020 SkillTree
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-
-
-
-
-
-
-
-
-
- {{ errors[0] }}
-
-
-
-
-
-
-
-
-
-
-
- {{ errors[0]}}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ errors[0]}}
-
-
-
-
-
-
-
-
- {{ errors[0]}}
-
-
-
-
-
-
-
-
-
-
-
{{ totalPoints | number }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Hours
-
-
- {{ errors[0] }}
-
-
-
-
-
-
-
- Minutes
-
-
- {{ errors[0] }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ errors[0]}}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ errors[0] }}
-
-
-
-
-
-
-
- {{ errors.first('helpUrl')}}
-
-
-
***{{ overallErrMsg }}***
-
-
-
-
-
-
- Save
-
-
- Cancel
-
-
-
-
-
-
-
-
diff --git a/frontend/src/components/utils/MarkdownEditor.vue b/frontend/src/components/utils/MarkdownEditor.vue
deleted file mode 100644
index 5790d6f3..00000000
--- a/frontend/src/components/utils/MarkdownEditor.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
-Copyright 2020 SkillTree
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-
-
-
- Write
-
-
-
-
-
-
-
- Preview
-
-
-
-
-
-
-
Markdown is supported
-
-
-
-
-
-
diff --git a/frontend/src/components/utils/UserDnInput.vue b/frontend/src/components/utils/UserDnInput.vue
deleted file mode 100644
index c4fd9324..00000000
--- a/frontend/src/components/utils/UserDnInput.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
-Copyright 2020 SkillTree
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-
- selected = option">
- {{ emptySlot }}
-
-
-
-
- validating user...
-
-
{{ errors.first('user')}}
-
-
-
-
diff --git a/frontend/src/validators/RegisterValidators.js b/frontend/src/validators/RegisterValidators.js
deleted file mode 100644
index 73549c42..00000000
--- a/frontend/src/validators/RegisterValidators.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2020 SkillTree
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { Validator } from 'vee-validate';
-import './OptionalNumericValidator';
-import './CustomDescriptionValidator';
-import './CustomNameValidator';
-import ValidatorFactory from './ValidatorFactory';
-import store from '../store/store';
-
-export default {
- init() {
- Validator.extend('maxDescriptionLength', ValidatorFactory.newCharLengthValidator(store.getters.config.descriptionMaxLength));
- Validator.extend('maxFirstNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxFirstNameLength));
- Validator.extend('maxLastNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxLastNameLength));
- Validator.extend('maxNicknameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxNicknameLength));
-
- Validator.extend('minUsernameLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minUsernameLength));
-
- Validator.extend('minPasswordLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minPasswordLength));
- Validator.extend('maxPasswordLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxPasswordLength));
-
- Validator.extend('minIdLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minIdLength));
- Validator.extend('maxIdLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxIdLength));
-
- Validator.extend('minNameLength', ValidatorFactory.newCharMinLengthValidator(store.getters.config.minNameLength));
-
- Validator.extend('maxBadgeNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxBadgeNameLength));
- Validator.extend('maxProjectNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxProjectNameLength));
- Validator.extend('maxSkillNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxSkillNameLength));
- Validator.extend('maxSubjectNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxSubjectNameLength));
- Validator.extend('maxLevelNameLength', ValidatorFactory.newCharLengthValidator(store.getters.config.maxLevelNameLength));
-
- Validator.extend('maxSkillVersion', ValidatorFactory.newMaxNumValidator(store.getters.config.maxSkillVersion));
- Validator.extend('maxPointIncrement', ValidatorFactory.newMaxNumValidator(store.getters.config.maxPointIncrement));
- Validator.extend('maxNumPerformToCompletion', ValidatorFactory.newMaxNumValidator(store.getters.config.maxNumPerformToCompletion));
- Validator.extend('maxNumPointIncrementMaxOccurrences', ValidatorFactory.newMaxNumValidator(store.getters.config.maxNumPointIncrementMaxOccurrences));
-
- Validator.extend('userNoSpaceInUserIdInNonPkiMode', ValidatorFactory.newUserObjNoSpacesValidatorInNonPkiMode(store.getters.isPkiAuthenticated));
- },
-};
diff --git a/license-add/license-add-config.json b/license-add/license-add-config.json
index 5abe5488..fdddad9c 100644
--- a/license-add/license-add-config.json
+++ b/license-add/license-add-config.json
@@ -1,7 +1,7 @@
{
"ignore": [ ".git", "coverage", ".*/**", ".*", "*.iml", "babel.config.js", "rollup.config.js", "app/assets/**", "target/**",
"pom.xml", "build/**", "**/robots.txt", "**.svg", "**.config.js", "**/**/*.conf.js", "**/**/.*",
- "cypress/videos/**", "**/*.jar", "**.xml", "cypress/fonts/**", "cypress/fixtures/**"
+ "cypress/videos/**", "**/*.jar", "**.xml", "cypress/fonts/**", "cypress/fixtures/**", "logs/*"
],
"license": "../license-add/LICENSE-HEADER.txt",
"licenseFormats": {
diff --git a/pom.xml b/pom.xml
index 60174165..86115510 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,15 +4,15 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- skills
- skills-service
+ skill-tree
+ skills-service-parentpom
- 1.1.4-SNAPSHOT
+ 1.3.0-SNAPSHOT
- frontend
+ call-stack-profiler
+ dashboardclient-display
- skills-bootstrap
- backend
+ service
@@ -21,34 +21,36 @@
1.11
- 2.5.9
- 3.0.0-01
- 2.5.1-02
+ 3.0.5
+ 3.6.0-03
+ 3.0.5-01
- 1.3-groovy-2.5
- 1.8
+ 2.0-M3-groovy-3.0
+ 11
- 5.2.1
+ 6.5.5
- 3.7
- 3.2.2
- 2.6
+ 3.11
+ 4.4
+ 2.7
- 1.7.6
- v12.16.1
+ 1.10.0
+ v12.18.3
- 4.5.6
- 28.1-jre
+ 4.5.12
+ 29.0-jre
- 2.1.13.RELEASE
+ 2.3.3.RELEASE
+
+ 1.6.0org.springframework.bootspring-boot-starter-parent
- 2.1.13.RELEASE
+ 2.3.3.RELEASE
diff --git a/backend/pom.xml b/service/pom.xml
similarity index 87%
rename from backend/pom.xml
rename to service/pom.xml
index c116f691..42b209ce 100644
--- a/backend/pom.xml
+++ b/service/pom.xml
@@ -3,14 +3,14 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- skills-service
- skills
- 1.1.4-SNAPSHOT
+ skills-service-parent
+ skill-tree
+ 1.3.0-SNAPSHOT4.0.0
- backend
+ skills-servicejavax.xml.bindjaxb-api
- 2.3.0
+ 2.3.1com.sun.xml.bind
@@ -56,7 +56,7 @@
com.sun.xml.bindjaxb-impl
- 2.3.0
+ 2.3.3
@@ -70,7 +70,7 @@
org.springframework.security.oauth.bootspring-security-oauth2-autoconfigure
- 2.1.12.RELEASE
+ 2.3.3.RELEASE
@@ -78,6 +78,10 @@
org.springframework.bootspring-boot-starter-mail
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+ org.codehaus.groovy
@@ -85,10 +89,6 @@
${groovy.version}pom
-
- org.codehaus.groovy
- groovy-test-junit5
- org.codehaus.groovygroovy-test
@@ -143,6 +143,14 @@
org.springframework.bootspring-boot-starter-reactor-netty
+
+ org.springframework.session
+ spring-session-data-redis
+
+
+ io.lettuce
+ lettuce-core
+ ch.qos.logback
@@ -162,7 +170,7 @@
mysqlmysql-connector-java
- 8.0.16
+ 8.0.21org.postgresql
@@ -179,8 +187,8 @@
${commons.lang.version}
- commons-collections
- commons-collections
+ org.apache.commons
+ commons-collections4${commons.collections.version}
@@ -193,19 +201,20 @@
org.spockframeworkspock-core${spock.myVersion}
-
-
- org.codehaus.groovy
- groovy-all
-
- test
- profile
+ org.codehaus.groovy
+ groovy
+ ${groovy.version}
+ test
+
+
+
+ skill-treecall-stack-profiler
- 1.0.2
+ ${project.version}org.slf4j
@@ -237,6 +246,12 @@
org.springframework.bootspring-boot-starter-testtest
+
+
+ junit
+ junit
+
+ org.spockframework
@@ -245,6 +260,17 @@
test
+
+ com.icegreen
+ greenmail
+ ${greenmail.version}
+ test
+
+
+ org.codehaus.groovy
+ groovy
+
+
@@ -280,6 +306,12 @@
groovy-all${groovy.version}pom
+
+
+ org.codehaus.groovy
+ groovy-testng
+
+ org.springframework.boot
@@ -305,6 +337,16 @@
+
+ maven-failsafe-plugin
+ 2.22.2
+
+
+ **/*DBIT
+
+
+
+
maven-clean-plugin
@@ -321,7 +363,7 @@
maven-resources-plugin
- copy Vue.js frontend content
+ copy Vue.js dashboard contentgenerate-resourcescopy-resources
@@ -331,7 +373,7 @@
true
- ${project.parent.basedir}/frontend/dist
+ ${project.parent.basedir}/dashboard/dist
@@ -355,25 +397,6 @@
-
- copy Vue.js boostrap content
- generate-resources
-
- copy-resources
-
-
- ${basedir}/src/main/resources/public/bootstrap
- true
-
-
- ${project.parent.basedir}/skills-bootstrap/dist/
-
- /*/
-
-
-
-
-
@@ -435,6 +458,8 @@
The MIT LicenseThe GNU General Public License (GPL) Version 2 with the Classpath ExceptionEclipse Public License - Version 1.0
+ Eclipse Public License - Version 2.0
+ Eclipse Distribution License - Version 1.0Dual license: Common Development and Distribution License 1.1 (CDDL-1.1) and The GNU General Public License (GPL) Version 2Common Development and Distribution License 1.1 (CDDL-1.1) + The GNU General Public License (GPL) Version 2Common Development and Distribution License 1.1 (CDDL-1.1) + The GNU General Public License (GPL) Version 2 with the Classpath Exception
@@ -479,6 +504,8 @@
Common Development and Distribution License (CDDL) + The GNU General Public License (GPL) Version 2 with the Classpath Exception|CDDL + GPLv2 with classpath exceptionThe GNU General Public License (GPL) Version 2 with FOSS exception|The GNU General Public License, v2 with FOSS exceptionMozilla Public License, Version 2.0 or Eclipse Public License - Version 1.0|MPL 2.0 or EPL 1.0
+ Eclipse Public License - Version 2.0|Eclipse Public License v. 2.0|Eclipse Public License v2.0
+ Eclipse Distribution License - Version 1.0|EDL 1.0|Eclipse Distribution License v. 1.0|Eclipse Distribution License - v 1.0
@@ -498,6 +525,7 @@
**/*.jks**/*.ftlsrc/main/resources/public/**
+ src/main/resources/templates/****/license/*.properties
diff --git a/backend/src/license/override-THIRD-PARTY.properties b/service/src/license/override-THIRD-PARTY.properties
similarity index 100%
rename from backend/src/license/override-THIRD-PARTY.properties
rename to service/src/license/override-THIRD-PARTY.properties
diff --git a/backend/src/main/java/skills/CachingConfiguration.groovy b/service/src/main/java/skills/CachingConfiguration.groovy
similarity index 100%
rename from backend/src/main/java/skills/CachingConfiguration.groovy
rename to service/src/main/java/skills/CachingConfiguration.groovy
diff --git a/service/src/main/java/skills/EmailTemplatingConfig.groovy b/service/src/main/java/skills/EmailTemplatingConfig.groovy
new file mode 100644
index 00000000..9cd0bf88
--- /dev/null
+++ b/service/src/main/java/skills/EmailTemplatingConfig.groovy
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.thymeleaf.spring5.SpringTemplateEngine
+import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver
+import org.thymeleaf.templatemode.TemplateMode
+
+@Configuration
+class EmailTemplatingConfig {
+
+ @Bean
+ public SpringResourceTemplateResolver thymeleafTemplateResolver() {
+ SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
+ templateResolver.setPrefix("classpath:/templates/");
+ templateResolver.setSuffix(".html");
+ templateResolver.setCacheable(false)
+ templateResolver.setTemplateMode(TemplateMode.HTML);
+ templateResolver.setCharacterEncoding("UTF-8");
+ return templateResolver;
+ }
+
+ @Bean
+ public SpringTemplateEngine thymeleafTemplateEngine() {
+ SpringTemplateEngine templateEngine = new SpringTemplateEngine();
+ templateEngine.setTemplateResolver(thymeleafTemplateResolver());
+ templateEngine.setEnableSpringELCompiler(true);
+ return templateEngine;
+ }
+}
diff --git a/backend/src/main/java/skills/HealthChecker.groovy b/service/src/main/java/skills/HealthChecker.groovy
similarity index 89%
rename from backend/src/main/java/skills/HealthChecker.groovy
rename to service/src/main/java/skills/HealthChecker.groovy
index 43dc6908..d3cdf64a 100644
--- a/backend/src/main/java/skills/HealthChecker.groovy
+++ b/service/src/main/java/skills/HealthChecker.groovy
@@ -18,6 +18,7 @@ package skills
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import skills.auth.AuthMode
import skills.auth.pki.PkiUserLookup
@@ -27,6 +28,10 @@ import javax.annotation.PostConstruct
@Slf4j
@Component
+@ConditionalOnProperty(
+ name = "skills.db.startup",
+ havingValue = "true",
+ matchIfMissing = true)
class HealthChecker {
@Value('#{securityConfig.authMode}}')
diff --git a/backend/src/main/java/skills/PublicProps.groovy b/service/src/main/java/skills/PublicProps.groovy
similarity index 100%
rename from backend/src/main/java/skills/PublicProps.groovy
rename to service/src/main/java/skills/PublicProps.groovy
diff --git a/service/src/main/java/skills/RedisConfig.java b/service/src/main/java/skills/RedisConfig.java
new file mode 100644
index 00000000..34d29856
--- /dev/null
+++ b/service/src/main/java/skills/RedisConfig.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
+
+/**
+ * Redis based HttpSession is optional and only enabled in password auth clustered install
+ */
+@Configuration
+@ConditionalOnProperty(value = "spring.session.store-type", havingValue = "redis", matchIfMissing = false)
+@EnableRedisHttpSession
+public class RedisConfig {
+
+ @Bean
+ public LettuceConnectionFactory connectionFactory() {
+ return new LettuceConnectionFactory();
+ }
+
+}
diff --git a/backend/src/main/java/skills/SpringBootApp.java b/service/src/main/java/skills/SpringBootApp.java
similarity index 86%
rename from backend/src/main/java/skills/SpringBootApp.java
rename to service/src/main/java/skills/SpringBootApp.java
index 17163b34..381874a0 100644
--- a/backend/src/main/java/skills/SpringBootApp.java
+++ b/service/src/main/java/skills/SpringBootApp.java
@@ -15,24 +15,25 @@
*/
package skills;
-import groovy.util.logging.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.ImportResource;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import skills.utils.SecretsUtil;
-import javax.annotation.PostConstruct;
import javax.net.ssl.HttpsURLConnection;
import java.util.TimeZone;
+@EnableAsync
@EnableScheduling
@EnableWebSecurity
-@SpringBootApplication
+@SpringBootApplication(exclude = { RedisRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class})
@EnableJpaRepositories(basePackages = {"skills.storage.repos"})
public class SpringBootApp {
diff --git a/backend/src/main/java/skills/TomcatConfig.groovy b/service/src/main/java/skills/TomcatConfig.groovy
similarity index 86%
rename from backend/src/main/java/skills/TomcatConfig.groovy
rename to service/src/main/java/skills/TomcatConfig.groovy
index 1f1c2bbc..dfc9bb14 100644
--- a/backend/src/main/java/skills/TomcatConfig.groovy
+++ b/service/src/main/java/skills/TomcatConfig.groovy
@@ -34,7 +34,12 @@ class TomcatConfig implements WebServerFactoryCustomizer ui = [:]
+ Map client = [:]
+
+ @PostConstruct
+ void init() {
+ if(client['loggingEnabled']) {
+ log.info("Client Logging Enabled: ${client}")
+ }
+ }
}
diff --git a/backend/src/main/java/skills/auth/AuthMode.groovy b/service/src/main/java/skills/auth/AuthMode.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/AuthMode.groovy
rename to service/src/main/java/skills/auth/AuthMode.groovy
diff --git a/backend/src/main/java/skills/auth/AuthUtils.groovy b/service/src/main/java/skills/auth/AuthUtils.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/AuthUtils.groovy
rename to service/src/main/java/skills/auth/AuthUtils.groovy
diff --git a/service/src/main/java/skills/auth/PortalWebSecurityHelper.groovy b/service/src/main/java/skills/auth/PortalWebSecurityHelper.groovy
new file mode 100644
index 00000000..491e947f
--- /dev/null
+++ b/service/src/main/java/skills/auth/PortalWebSecurityHelper.groovy
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.auth
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.stereotype.Component
+import skills.storage.model.auth.RoleName
+
+@Component
+class PortalWebSecurityHelper {
+ HttpSecurity configureHttpSecurity(HttpSecurity http) {
+ http
+ .csrf().disable()
+ .authorizeRequests()
+ .antMatchers("/", "/favicon.ico",
+ "/icons/**", "/static/**",
+ "/skills.ico", "/skills.jpeg",
+ "/error", "/oauth/**",
+ "/app/oAuthProviders", "/login*", "/login/**",
+ "/performLogin", "/createAccount",
+ "/createRootAccount", '/grantFirstRoot',
+ '/userExists/**', "/app/userInfo",
+ "/app/users/validExistingDashboardUserId/*", "/app/oAuthProviders",
+ "index.html", "/public/**",
+ "/skills-websocket/**", "/requestPasswordReset",
+ "/resetPassword/**", "/performPasswordReset").permitAll()
+ .antMatchers('/admin/**').hasAnyAuthority(RoleName.ROLE_PROJECT_ADMIN.name(), RoleName.ROLE_SUPER_DUPER_USER.name())
+ .antMatchers('/supervisor/**').hasAnyAuthority(RoleName.ROLE_SUPERVISOR.name(), RoleName.ROLE_SUPER_DUPER_USER.name())
+ .antMatchers('/root/isRoot').hasAnyAuthority(RoleName.values().collect {it.name()}.toArray(new String[0]))
+ .antMatchers('/root/**').hasRole('SUPER_DUPER_USER')
+ .anyRequest().authenticated()
+ http.headers().frameOptions().disable()
+
+ return http
+ }
+}
diff --git a/backend/src/main/java/skills/auth/SecurityConfiguration.groovy b/service/src/main/java/skills/auth/SecurityConfiguration.groovy
similarity index 76%
rename from backend/src/main/java/skills/auth/SecurityConfiguration.groovy
rename to service/src/main/java/skills/auth/SecurityConfiguration.groovy
index 3085a565..38d3e583 100644
--- a/backend/src/main/java/skills/auth/SecurityConfiguration.groovy
+++ b/service/src/main/java/skills/auth/SecurityConfiguration.groovy
@@ -15,23 +15,20 @@
*/
package skills.auth
+import com.fasterxml.jackson.databind.ObjectMapper
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Condition
-import org.springframework.context.annotation.ConditionContext
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
-import org.springframework.core.type.AnnotatedTypeMetadata
+import org.springframework.http.MediaType
import org.springframework.security.access.AccessDeniedException
-import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.AuthenticationException
-import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
@@ -41,7 +38,8 @@ import org.springframework.security.web.access.AccessDeniedHandlerImpl
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextListener
-import skills.storage.model.auth.RoleName
+import skills.auth.util.AccessDeniedExplanation
+import skills.auth.util.AccessDeniedExplanationGenerator
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
@@ -56,6 +54,9 @@ class SecurityConfiguration {
@Value('${skills.authorization.authMode:#{T(skills.auth.AuthMode).DEFAULT_AUTH_MODE}}')
AuthMode authMode
+ @Autowired
+ ObjectMapper objectMapper
+
@Component
@Configuration
@Order(99)
@@ -79,6 +80,8 @@ class SecurityConfiguration {
@Autowired
UserDetailsService userDetailsService
+ AccessDeniedExplanationGenerator explanationGenerator = new AccessDeniedExplanationGenerator()
+
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**").cors()
@@ -120,14 +123,21 @@ class SecurityConfiguration {
@Bean
AccessDeniedHandler accessDeniedHandler() {
- return new CustomHandler()
- }
-
- static class CustomHandler extends AccessDeniedHandlerImpl {
- @Override
- void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
- log.warn("Received AccessDeniedException for [${request.getRequestURI()}]", accessDeniedException)
- super.handle(request, response, accessDeniedException)
+ return new AccessDeniedHandlerImpl() {
+ @Override
+ void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
+ log.warn("Received AccessDeniedException for [${request.getRequestURI()}]", accessDeniedException)
+ super.handle(request, response, accessDeniedException)
+ AccessDeniedExplanation explanation = new AccessDeniedExplanationGenerator().generateExplanation(request.getServerName())
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN)
+ if(explanation) {
+ String asJson = objectMapper.writeValueAsString(explanation)
+ response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
+ response.setContentLength(asJson.bytes.length)
+ response.getWriter().print(asJson)
+ response.getWriter().flush()
+ }
+ }
}
}
}
diff --git a/backend/src/main/java/skills/auth/SecurityMode.groovy b/service/src/main/java/skills/auth/SecurityMode.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/SecurityMode.groovy
rename to service/src/main/java/skills/auth/SecurityMode.groovy
diff --git a/backend/src/main/java/skills/auth/SkillsAuthorizationException.groovy b/service/src/main/java/skills/auth/SkillsAuthorizationException.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/SkillsAuthorizationException.groovy
rename to service/src/main/java/skills/auth/SkillsAuthorizationException.groovy
diff --git a/backend/src/main/java/skills/auth/UserAuthService.groovy b/service/src/main/java/skills/auth/UserAuthService.groovy
similarity index 98%
rename from backend/src/main/java/skills/auth/UserAuthService.groovy
rename to service/src/main/java/skills/auth/UserAuthService.groovy
index 90f43644..f20fa7e0 100644
--- a/backend/src/main/java/skills/auth/UserAuthService.groovy
+++ b/service/src/main/java/skills/auth/UserAuthService.groovy
@@ -17,8 +17,9 @@ package skills.auth
import callStack.profiler.Profile
import groovy.util.logging.Slf4j
-import org.apache.commons.collections.CollectionUtils
+import org.apache.commons.collections4.CollectionUtils
import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Lazy
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.GrantedAuthority
@@ -55,6 +56,7 @@ class UserAuthService {
AccessSettingsStorageService accessSettingsStorageService
@Autowired
+ @Lazy
private AuthenticationManager authenticationManager
@Autowired
diff --git a/backend/src/main/java/skills/auth/UserInfo.groovy b/service/src/main/java/skills/auth/UserInfo.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/UserInfo.groovy
rename to service/src/main/java/skills/auth/UserInfo.groovy
diff --git a/backend/src/main/java/skills/auth/UserInfoService.groovy b/service/src/main/java/skills/auth/UserInfoService.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/UserInfoService.groovy
rename to service/src/main/java/skills/auth/UserInfoService.groovy
diff --git a/backend/src/main/java/skills/auth/UserSkillsGrantedAuthority.groovy b/service/src/main/java/skills/auth/UserSkillsGrantedAuthority.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/UserSkillsGrantedAuthority.groovy
rename to service/src/main/java/skills/auth/UserSkillsGrantedAuthority.groovy
diff --git a/backend/src/main/java/skills/auth/aop/AdminUsersOnlyWhenUserIdSupplied.java b/service/src/main/java/skills/auth/aop/AdminUsersOnlyWhenUserIdSupplied.java
similarity index 100%
rename from backend/src/main/java/skills/auth/aop/AdminUsersOnlyWhenUserIdSupplied.java
rename to service/src/main/java/skills/auth/aop/AdminUsersOnlyWhenUserIdSupplied.java
diff --git a/backend/src/main/java/skills/auth/aop/AuthorizationAspect.java b/service/src/main/java/skills/auth/aop/AuthorizationAspect.java
similarity index 100%
rename from backend/src/main/java/skills/auth/aop/AuthorizationAspect.java
rename to service/src/main/java/skills/auth/aop/AuthorizationAspect.java
diff --git a/backend/src/main/java/skills/auth/form/CreateAccountController.groovy b/service/src/main/java/skills/auth/form/CreateAccountController.groovy
similarity index 82%
rename from backend/src/main/java/skills/auth/form/CreateAccountController.groovy
rename to service/src/main/java/skills/auth/form/CreateAccountController.groovy
index c21a228c..790b3578 100644
--- a/backend/src/main/java/skills/auth/form/CreateAccountController.groovy
+++ b/service/src/main/java/skills/auth/form/CreateAccountController.groovy
@@ -17,10 +17,13 @@ package skills.auth.form
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome
+import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition
import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.context.annotation.ConditionContext
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Configuration
-import org.springframework.http.MediaType
+import org.springframework.core.type.AnnotatedTypeMetadata
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.client.registration.ClientRegistration
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
@@ -97,6 +100,7 @@ class CreateAccountController {
roleName: RoleName.ROLE_SUPER_DUPER_USER
))]
userAuthService.createUser(userInfo, true)
+ userAuthService.autologin(userInfo, password)
}
@Conditional(SecurityMode.PkiAuth)
@@ -117,7 +121,7 @@ class CreateAccountController {
@GetMapping("/app/oAuthProviders")
List getOAuthProviders() {
List providers = []
- if (clientRegistrationRepository && oAuth2ProviderProperties) {
+ if (clientRegistrationRepository && oAuth2ProviderProperties?.registration) {
clientRegistrationRepository.iterator().each { ClientRegistration clientRegistration ->
skills.controller.result.model.OAuth2Provider oAuth2Provider = oAuth2ProviderProperties.registration.get(clientRegistration.registrationId)
oAuth2Provider.registrationId = oAuth2Provider.registrationId ?: clientRegistration.registrationId
@@ -135,4 +139,31 @@ class CreateAccountController {
static class OAuth2ProviderProperties {
final Map registration = new HashMap<>()
}
+
+ @Component
+ @Configuration
+ @Conditional(NoClientsConfiguredCondition)
+ /*
+ EmptyOauth2ClientRegistrationRepository will only be created if there are not OAuth2 clients configured (i.e.
+ when there are no security.oauth2.client.registration.XXX properties configured)
+ */
+ static class EmptyOauth2ClientRegistrationRepository implements ClientRegistrationRepository, Iterable {
+
+ @Override
+ Iterator iterator() {
+ return Collections.emptyIterator()
+ }
+
+ @Override
+ ClientRegistration findByRegistrationId(String registrationId) {
+ return null
+ }
+ }
+
+ static class NoClientsConfiguredCondition extends ClientsConfiguredCondition {
+ @Override
+ ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata))
+ }
+ }
}
diff --git a/backend/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy b/service/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy
similarity index 72%
rename from backend/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy
rename to service/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy
index bff45c64..0b1a9e9b 100644
--- a/backend/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy
+++ b/service/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy
@@ -15,25 +15,23 @@
*/
package skills.auth.form
+
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.context.annotation.Bean
-import org.springframework.context.annotation.Conditional
-import org.springframework.context.annotation.Configuration
-import org.springframework.context.annotation.Primary
+import org.springframework.context.annotation.*
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
-import org.springframework.security.access.AccessDeniedException
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.Authentication
-import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.password.PasswordEncoder
-import org.springframework.security.web.access.AccessDeniedHandler
+import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository
+import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
@@ -54,6 +52,8 @@ import javax.servlet.http.HttpServletResponse
@Slf4j
class FormSecurityConfiguration extends WebSecurityConfigurerAdapter {
+ public static final String SKILLS_REDIRECT_URI = 'skillsRedirectUri'
+
@Autowired
private PortalWebSecurityHelper portalWebSecurityHelper
@@ -69,6 +69,12 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler
+ @Autowired
+ private SkillsClientOAuth2AuthenticationSuccessHandler oauthAuthenticationSuccessHandler
+
+ @Autowired
+ private SkillsClientOAuth2AuthorizationRequestRepository skillsClientOAuth2AuthorizationRequestRepository
+
@Autowired
private RestLogoutSuccessHandler restLogoutSuccessHandler
@@ -101,18 +107,22 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter {
.loginProcessingUrl("/performLogin")
.successHandler(restAuthenticationSuccessHandler)
.failureHandler(restAuthenticationFailureHandler)
+ .and()
+ .logout()
+ .logoutSuccessHandler(restLogoutSuccessHandler)
.and()
.oauth2Login()
.loginPage("/skills-login")
+ .successHandler(oauthAuthenticationSuccessHandler)
.failureHandler(restAuthenticationFailureHandler)
- .and()
- .logout()
- .logoutSuccessHandler(restLogoutSuccessHandler)
+ .authorizationEndpoint()
+ .authorizationRequestRepository(skillsClientOAuth2AuthorizationRequestRepository)
}
@Override
@Bean(name = 'defaultAuthManager')
@Primary
+ @Lazy
AuthenticationManager authenticationManagerBean() throws Exception {
// provides the default AuthenticationManager as a Bean
return super.authenticationManagerBean()
@@ -133,6 +143,9 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter {
return [
google: new OAuth2UserConverterService.GoogleUserConverter(),
github: new OAuth2UserConverterService.GitHubUserConverter(),
+ gitlab: new OAuth2UserConverterService.GitLabUserConverter(),
+ auth0: new OAuth2UserConverterService.Auth0UserConverter(),
+ hydra: new OAuth2UserConverterService.HydraUserConverter(),
]
}
@@ -142,21 +155,24 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter {
}
@Component
- static class RestAccessDeniedHandler implements AccessDeniedHandler {
+ static final class RestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
- void handle(final HttpServletRequest request, final HttpServletResponse response, final AccessDeniedException ex) throws IOException, ServletException {
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- log.warn("Access Denied User [${authentication}], reqested resource [${request.getServletPath()}]")
- response.setStatus(HttpServletResponse.SC_FORBIDDEN)
+ void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+ clearAuthenticationAttributes(request)
+ writeNullJson(response)
}
}
@Component
- static final class RestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
+ static final class SkillsClientOAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
- void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
- clearAuthenticationAttributes(request)
- writeNullJson(response)
+ protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
+ String targetUrl = request.getSession(false).getAttribute(SKILLS_REDIRECT_URI)
+ if (targetUrl) {
+ return targetUrl
+ } else {
+ return super.determineTargetUrl(request, response)
+ }
}
}
@@ -167,12 +183,27 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter {
}
@Override
- void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+ void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
writeNullJson(response)
super.onLogoutSuccess(request, response, authentication)
}
}
+ @Component
+ static final class SkillsClientOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository {
+ @Delegate
+ HttpSessionOAuth2AuthorizationRequestRepository httpSessionOAuth2AuthorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository()
+
+ @Override
+ void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
+ httpSessionOAuth2AuthorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response)
+ String skillsRedirectUri = request.getParameter(SKILLS_REDIRECT_URI)
+ if (skillsRedirectUri) {
+ request.getSession(false).setAttribute(SKILLS_REDIRECT_URI, skillsRedirectUri)
+ }
+ }
+ }
+
static final String NULL_JSON = 'null'
static writeNullJson(HttpServletResponse response) {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
diff --git a/backend/src/main/java/skills/auth/form/LocalUserDetailsService.groovy b/service/src/main/java/skills/auth/form/LocalUserDetailsService.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/LocalUserDetailsService.groovy
rename to service/src/main/java/skills/auth/form/LocalUserDetailsService.groovy
diff --git a/service/src/main/java/skills/auth/form/PasswordReset.groovy b/service/src/main/java/skills/auth/form/PasswordReset.groovy
new file mode 100644
index 00000000..719da2a3
--- /dev/null
+++ b/service/src/main/java/skills/auth/form/PasswordReset.groovy
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.auth.form
+
+class PasswordReset {
+ String userId
+ String password
+ String resetToken
+}
diff --git a/service/src/main/java/skills/auth/form/PasswordResetController.groovy b/service/src/main/java/skills/auth/form/PasswordResetController.groovy
new file mode 100644
index 00000000..da139df1
--- /dev/null
+++ b/service/src/main/java/skills/auth/form/PasswordResetController.groovy
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.auth.form
+
+import groovy.util.logging.Slf4j
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Conditional
+import org.springframework.context.annotation.Lazy
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.web.bind.annotation.PostMapping
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestPart
+import org.springframework.web.bind.annotation.RestController
+import skills.PublicProps
+import skills.auth.SecurityMode
+import skills.auth.UserAuthService
+import skills.auth.UserInfo
+import skills.controller.PublicPropsBasedValidator
+import skills.controller.exceptions.SkillException
+import skills.controller.result.model.RequestResult
+import skills.services.PasswordResetService
+import skills.storage.model.auth.PasswordResetToken
+import skills.storage.model.auth.User
+
+import javax.annotation.PostConstruct
+
+@Conditional(SecurityMode.FormAuth)
+@Slf4j
+@RestController()
+@RequestMapping("/")
+class PasswordResetController {
+
+ @Autowired
+ PasswordResetService resetService
+
+ @Autowired
+ UserAuthService userAuthService
+
+ @Autowired
+ PublicPropsBasedValidator propsBasedValidator
+
+ @Autowired
+ PasswordEncoder passwordEncoder
+
+ @PostMapping("resetPassword")
+ RequestResult requestPasswordReset(@RequestPart("userId") String userId) {
+ log.info("requesting password reset for [${userId}]")
+ User user = userAuthService.getUserRepository().findByUserId(userId)
+ if (!user) {
+ log.error("no user found for requested password reset")
+ throw new SkillException("No user found for id [${userId}]")
+ }
+ resetService.createTokenAndNotifyUser(user)
+ return RequestResult.success()
+ }
+
+ @PostMapping("performPasswordReset")
+ RequestResult resetPassword(@RequestBody PasswordReset reset) {
+ PasswordResetToken token = resetService.loadToken(reset.resetToken)
+ if (token?.getUser()?.getUserId() != reset.userId) {
+ throw new SkillException("Supplied reset token is not for the specified user")
+ }
+
+ if (!token.isValid()) {
+ throw new SkillException("Reset token has expired")
+ }
+
+ log.info("reseting password for user [${reset.userId}]")
+ propsBasedValidator.validateMinStrLength(PublicProps.UiProp.minPasswordLength, "new_password", reset.password)
+ propsBasedValidator.validateMaxStrLength(PublicProps.UiProp.maxPasswordLength, "new_password", reset.password)
+
+ UserInfo userInfo = userAuthService.loadByUserId(token.user.userId)
+ userInfo.password = passwordEncoder.encode(reset.password)
+ userAuthService.createOrUpdateUser(userInfo)
+ resetService.deleteToken(token.token)
+
+ return RequestResult.success()
+ }
+}
diff --git a/service/src/main/java/skills/auth/form/RestAccessDeniedHandler.groovy b/service/src/main/java/skills/auth/form/RestAccessDeniedHandler.groovy
new file mode 100644
index 00000000..4ac800f8
--- /dev/null
+++ b/service/src/main/java/skills/auth/form/RestAccessDeniedHandler.groovy
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.auth.form
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import groovy.util.logging.Slf4j
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Conditional
+import org.springframework.http.MediaType
+import org.springframework.security.access.AccessDeniedException
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.web.access.AccessDeniedHandler
+import org.springframework.stereotype.Component
+import skills.auth.SecurityMode
+import skills.auth.util.AccessDeniedExplanation
+import skills.auth.util.AccessDeniedExplanationGenerator
+
+import javax.servlet.ServletException
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+@Conditional(SecurityMode.FormAuth)
+@Slf4j
+@Component
+class RestAccessDeniedHandler implements AccessDeniedHandler {
+
+ @Autowired
+ ObjectMapper om
+
+ @Override
+ void handle(final HttpServletRequest request, final HttpServletResponse response, final AccessDeniedException ex) throws IOException, ServletException {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ log.warn("Access Denied User [${authentication}], reqested resource [${request.getServletPath()}]")
+ AccessDeniedExplanation explanation = new AccessDeniedExplanationGenerator().generateExplanation(request.getServletPath())
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN)
+ if(explanation) {
+ String asJson = om.writeValueAsString(explanation)
+ response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
+ response.setContentLength(asJson.bytes.length)
+ response.getWriter().print(asJson)
+ response.getWriter().flush()
+ }
+ }
+}
diff --git a/backend/src/main/java/skills/auth/form/SkillsHttpSessionSecurityContextRepository.groovy b/service/src/main/java/skills/auth/form/SkillsHttpSessionSecurityContextRepository.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/SkillsHttpSessionSecurityContextRepository.groovy
rename to service/src/main/java/skills/auth/form/SkillsHttpSessionSecurityContextRepository.groovy
diff --git a/backend/src/main/java/skills/auth/form/jwt/JwtAuthenticationFilter.groovy b/service/src/main/java/skills/auth/form/jwt/JwtAuthenticationFilter.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/jwt/JwtAuthenticationFilter.groovy
rename to service/src/main/java/skills/auth/form/jwt/JwtAuthenticationFilter.groovy
diff --git a/backend/src/main/java/skills/auth/form/jwt/JwtAuthorizationFilter.groovy b/service/src/main/java/skills/auth/form/jwt/JwtAuthorizationFilter.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/jwt/JwtAuthorizationFilter.groovy
rename to service/src/main/java/skills/auth/form/jwt/JwtAuthorizationFilter.groovy
diff --git a/backend/src/main/java/skills/auth/form/jwt/JwtHelper.groovy b/service/src/main/java/skills/auth/form/jwt/JwtHelper.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/jwt/JwtHelper.groovy
rename to service/src/main/java/skills/auth/form/jwt/JwtHelper.groovy
diff --git a/backend/src/main/java/skills/auth/form/oauth2/AuthorizationServerConfig.groovy b/service/src/main/java/skills/auth/form/oauth2/AuthorizationServerConfig.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/oauth2/AuthorizationServerConfig.groovy
rename to service/src/main/java/skills/auth/form/oauth2/AuthorizationServerConfig.groovy
diff --git a/backend/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy b/service/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy
similarity index 51%
rename from backend/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy
rename to service/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy
index e2db3a27..9d1e8163 100644
--- a/backend/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy
+++ b/service/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy
@@ -15,6 +15,7 @@
*/
package skills.auth.form.oauth2
+import org.apache.commons.lang3.StringUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Conditional
import org.springframework.security.oauth2.core.user.OAuth2User
@@ -41,6 +42,9 @@ class OAuth2UserConverterService {
OAuth2UserConverter converter = lookup.get(clientId.toLowerCase())
if (converter) {
userInfo = converter.convert(clientId, oAuth2User)
+ if (!userInfo.usernameForDisplay) {
+ userInfo.usernameForDisplay = userInfo.username
+ }
} else {
throw new SkillsAuthorizationException("No OAuth2UserConverter configured for clientId [${clientId}]")
}
@@ -107,4 +111,94 @@ class OAuth2UserConverterService {
)
}
}
+
+ static class GitLabUserConverter implements OAuth2UserConverter {
+ static final String NAME = 'name'
+ static final String EMAIL = 'email'
+
+ String clientId = 'gitlab'
+
+ @Override
+ UserInfo convert(String clientId, OAuth2User oAuth2User) {
+ String username = oAuth2User.getName()
+ assert username, "Error getting name attribute of oAuth2User [${oAuth2User}] from clientId [$clientId]"
+ String email = oAuth2User.attributes.get(EMAIL)
+ if (!email) {
+ throw new SkillsAuthorizationException("Email must be available in your public GitLab profile")
+ }
+ String name = oAuth2User.attributes.get(NAME)
+ if (!name) {
+ throw new SkillsAuthorizationException("Name must be available in your public GitLab profile")
+ }
+ String firstName = name?.tokenize()?.first()
+ List tokens = name?.tokenize()
+ tokens?.pop()
+ tokens?.remove(username)
+ String lastName = tokens?.join(' ')
+
+ return new UserInfo(
+ username: "${username}-${clientId}",
+ email:email,
+ firstName: firstName,
+ lastName: lastName,
+ )
+ }
+ }
+
+ static class Auth0UserConverter implements OAuth2UserConverter {
+ static final String USERNAME = 'nickname'
+ static final String NAME = 'name'
+ static final String EMAIL = 'email'
+
+ String clientId = 'auth0'
+
+ @Override
+ UserInfo convert(String clientId, OAuth2User oAuth2User) {
+ String username = oAuth2User.attributes.get(USERNAME)
+ assert username, "Error getting name attribute of oAuth2User [${oAuth2User}] from clientId [$clientId]"
+ String email = oAuth2User.attributes.get(EMAIL)
+ if (!email) {
+ throw new SkillsAuthorizationException("Email must be available in your public GitLab profile")
+ }
+ String name = oAuth2User.attributes.get(NAME)
+ if (!name) {
+ throw new SkillsAuthorizationException("Name must be available in your public GitLab profile")
+ }
+ String firstName = name?.tokenize()?.first()
+ List tokens = name?.tokenize()
+ tokens?.pop()
+ tokens?.remove(username)
+ String lastName = tokens?.join(' ')
+
+ return new UserInfo(
+ username: "${username}-${clientId}",
+ email:email,
+ firstName: firstName,
+ lastName: lastName,
+ )
+ }
+ }
+
+ static class HydraUserConverter implements OAuth2UserConverter {
+ String clientId = 'hydra'
+ @Override
+ UserInfo convert(String clientId, OAuth2User oAuth2User) {
+ String email = oAuth2User.getName()
+ List tokens = email.tokenize('@')
+ String username = tokens.first()
+ String firstName = username;
+ String lastName = StringUtils.substringBeforeLast(tokens.last(), '.')
+ assert email, "Error getting email attribute of oAuth2User [${oAuth2User}] from clientId [$clientId]"
+ assert username, "Error getting username attribute of oAuth2User [${oAuth2User}] from clientId [$clientId]"
+ assert firstName, "Error getting firstName attribute of oAuth2User [${oAuth2User}] from clientId [$clientId]"
+ assert lastName, "Error getting lastName attribute of oAuth2User [${oAuth2User}] from clientId [$clientId]"
+
+ return new UserInfo(
+ username: "${username}-${clientId}",
+ email:email,
+ firstName: firstName,
+ lastName: lastName,
+ )
+ }
+ }
}
diff --git a/backend/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy b/service/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy
similarity index 93%
rename from backend/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy
rename to service/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy
index 65a62610..401ff636 100644
--- a/backend/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy
+++ b/service/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy
@@ -33,8 +33,6 @@ import skills.auth.UserInfo
import javax.servlet.http.HttpServletRequest
import javax.transaction.Transactional
-import static AuthorizationServerConfig.SKILLS_PROXY_USER
-
@Component
@Conditional(SecurityMode.FormAuth)
@Slf4j
@@ -56,8 +54,8 @@ class OAuthUtils {
Authentication skillsAuth
OAuth2AuthenticationDetails oauthDetails = (OAuth2AuthenticationDetails) auth.getDetails()
Map claims = oauthDetails.getDecodedDetails()
- if (claims && claims.get(SKILLS_PROXY_USER)) {
- String proxyUserId = claims.get(SKILLS_PROXY_USER)
+ if (claims && claims.get(AuthorizationServerConfig.SKILLS_PROXY_USER)) {
+ String proxyUserId = claims.get(AuthorizationServerConfig.SKILLS_PROXY_USER)
log.info("Loading proxyUser [${proxyUserId}]")
UserInfo currentUser = new UserInfo(
username: proxyUserId,
@@ -67,7 +65,7 @@ class OAuthUtils {
// Create new Authentication using UserInfo
skillsAuth = new UsernamePasswordAuthenticationToken(currentUser, null, currentUser.authorities)
} else {
- throw new InvalidTokenException("client_credentials grant_type must specify $SKILLS_PROXY_USER field")
+ throw new InvalidTokenException("client_credentials grant_type must specify $AuthorizationServerConfig.SKILLS_PROXY_USER field")
}
return skillsAuth
}
@@ -85,7 +83,7 @@ class OAuthUtils {
currentUser = userAuthService.createOrUpdateUser(currentUser)
// Create new Authentication using UserInfo
- skillsAuth = new UsernamePasswordAuthenticationToken(currentUser, null, currentUser.authorities)
+ skillsAuth = new UsernamePasswordAuthenticationToken(currentUser, auth, currentUser.authorities)
}
return skillsAuth
}
diff --git a/backend/src/main/java/skills/auth/form/oauth2/ResourceServerConfig.groovy b/service/src/main/java/skills/auth/form/oauth2/ResourceServerConfig.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/form/oauth2/ResourceServerConfig.groovy
rename to service/src/main/java/skills/auth/form/oauth2/ResourceServerConfig.groovy
diff --git a/backend/src/main/java/skills/auth/form/oauth2/SkillsOAuth2AuthenticationManager.groovy b/service/src/main/java/skills/auth/form/oauth2/SkillsOAuth2AuthenticationManager.groovy
similarity index 91%
rename from backend/src/main/java/skills/auth/form/oauth2/SkillsOAuth2AuthenticationManager.groovy
rename to service/src/main/java/skills/auth/form/oauth2/SkillsOAuth2AuthenticationManager.groovy
index 9590f814..d69301ad 100644
--- a/backend/src/main/java/skills/auth/form/oauth2/SkillsOAuth2AuthenticationManager.groovy
+++ b/service/src/main/java/skills/auth/form/oauth2/SkillsOAuth2AuthenticationManager.groovy
@@ -27,11 +27,9 @@ import org.springframework.security.oauth2.provider.token.DefaultTokenServices
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
-import skills.WebSocketConfig
import skills.auth.SecurityMode
import skills.auth.UserInfo
-import javax.annotation.PostConstruct
import javax.servlet.http.HttpServletRequest
@Component('skillsOAuth2AuthManager')
@@ -42,19 +40,10 @@ class SkillsOAuth2AuthenticationManager extends OAuth2AuthenticationManager {
@Autowired
OAuthUtils oAuthUtils
- @Autowired
- WebSocketConfig webSocketConfig
-
SkillsOAuth2AuthenticationManager(DefaultTokenServices tokenServices) {
setTokenServices(tokenServices)
}
- @PostConstruct
- void postConstruct() {
- // inject into WebSocketConfig (@Autowired caused a circular reference)
- webSocketConfig.oAuth2AuthenticationManager = this
- }
-
@Override
Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication auth = super.authenticate(authentication)
diff --git a/backend/src/main/java/skills/auth/pki/HttpClientRestTemplateConfig.groovy b/service/src/main/java/skills/auth/pki/HttpClientRestTemplateConfig.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/pki/HttpClientRestTemplateConfig.groovy
rename to service/src/main/java/skills/auth/pki/HttpClientRestTemplateConfig.groovy
diff --git a/backend/src/main/java/skills/auth/pki/PkiSecurityConfiguration.groovy b/service/src/main/java/skills/auth/pki/PkiSecurityConfiguration.groovy
similarity index 99%
rename from backend/src/main/java/skills/auth/pki/PkiSecurityConfiguration.groovy
rename to service/src/main/java/skills/auth/pki/PkiSecurityConfiguration.groovy
index 4d766bd9..ad53e586 100644
--- a/backend/src/main/java/skills/auth/pki/PkiSecurityConfiguration.groovy
+++ b/service/src/main/java/skills/auth/pki/PkiSecurityConfiguration.groovy
@@ -70,6 +70,7 @@ class PkiSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
@Bean(name = 'defaultAuthManager')
@Primary
+ @Lazy
AuthenticationManager authenticationManagerBean() throws Exception {
// provides the default AuthenticationManager as a Bean
return super.authenticationManagerBean()
diff --git a/backend/src/main/java/skills/auth/pki/PkiUserDetailsService.groovy b/service/src/main/java/skills/auth/pki/PkiUserDetailsService.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/pki/PkiUserDetailsService.groovy
rename to service/src/main/java/skills/auth/pki/PkiUserDetailsService.groovy
diff --git a/backend/src/main/java/skills/auth/pki/PkiUserLookup.groovy b/service/src/main/java/skills/auth/pki/PkiUserLookup.groovy
similarity index 100%
rename from backend/src/main/java/skills/auth/pki/PkiUserLookup.groovy
rename to service/src/main/java/skills/auth/pki/PkiUserLookup.groovy
diff --git a/service/src/main/java/skills/auth/util/AccessDeniedExplanation.groovy b/service/src/main/java/skills/auth/util/AccessDeniedExplanation.groovy
new file mode 100644
index 00000000..62fb3b59
--- /dev/null
+++ b/service/src/main/java/skills/auth/util/AccessDeniedExplanation.groovy
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.auth.util
+
+class AccessDeniedExplanation {
+ String explanation;
+}
diff --git a/service/src/main/java/skills/auth/util/AccessDeniedExplanationGenerator.groovy b/service/src/main/java/skills/auth/util/AccessDeniedExplanationGenerator.groovy
new file mode 100644
index 00000000..87777a2e
--- /dev/null
+++ b/service/src/main/java/skills/auth/util/AccessDeniedExplanationGenerator.groovy
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.auth.util
+
+class AccessDeniedExplanationGenerator {
+
+ AccessDeniedExplanation generateExplanation(String requestPath) {
+ if (requestPath?.startsWith("/admin/projects")) {
+ return new AccessDeniedExplanation(explanation: "You do not have permission to view/manage this Project OR this Project does not exist")
+ } else if (requestPath?.startsWith("/supervisor/badges")) {
+ return new AccessDeniedExplanation(explanation: "You do not have permission to view/manage this Global Badge OR this Global Badge does not exist")
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/backend/src/main/java/skills/controller/AccessSettingsController.groovy b/service/src/main/java/skills/controller/AccessSettingsController.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/AccessSettingsController.groovy
rename to service/src/main/java/skills/controller/AccessSettingsController.groovy
diff --git a/backend/src/main/java/skills/controller/AdminController.groovy b/service/src/main/java/skills/controller/AdminController.groovy
similarity index 95%
rename from backend/src/main/java/skills/controller/AdminController.groovy
rename to service/src/main/java/skills/controller/AdminController.groovy
index 7d12bff4..4acdc2b0 100644
--- a/backend/src/main/java/skills/controller/AdminController.groovy
+++ b/service/src/main/java/skills/controller/AdminController.groovy
@@ -109,8 +109,8 @@ class AdminController {
projectRequest.name = InputSanitizer.sanitize(projectRequest.name)
projectRequest.projectId = InputSanitizer.sanitize(projectRequest.projectId)
- projAdminService.saveProject(projectId, projectRequest)
- return new skills.controller.result.model.RequestResult(success: true)
+ projAdminService.saveProject(InputSanitizer.sanitize(projectId), projectRequest)
+ return new RequestResult(success: true)
}
@RequestMapping(value = "/projects/{id}", method = RequestMethod.DELETE)
@@ -168,34 +168,37 @@ class AdminController {
subjectRequest.iconClass = InputSanitizer.sanitize(subjectRequest.iconClass)
subjectRequest.helpUrl = InputSanitizer.sanitize(subjectRequest.helpUrl)
- subjAdminService.saveSubject(projectId, subjectId, subjectRequest)
+ subjAdminService.saveSubject(InputSanitizer.sanitize(projectId), subjectId, subjectRequest)
return new RequestResult(success: true)
}
- @RequestMapping(value = "/projects/{projectId}/subjectNameExists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @RequestMapping(value = "/projects/{projectId}/subjectNameExists", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
boolean doesSubjectNameExist(@PathVariable("projectId") String projectId,
- @RequestParam(value = "subjectName", required = false) String subjectName) {
+ @RequestBody NameExistsRequest existsRequest) {
+ String subjectName = existsRequest.name
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(subjectName, "Subject Name")
- return subjAdminService.existsBySubjectName(projectId, subjectName)
+ return subjAdminService.existsBySubjectName(InputSanitizer.sanitize(projectId), InputSanitizer.sanitize(subjectName))
}
- @RequestMapping(value = "/projects/{projectId}/badgeNameExists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @RequestMapping(value = "/projects/{projectId}/badgeNameExists", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
boolean doesBadgeExist(@PathVariable("projectId") String projectId,
- @RequestParam(value = "badgeName", required = false) String badgeName) {
+ @RequestBody NameExistsRequest nameExistsRequest) {
+ String badgeName = nameExistsRequest.name
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(badgeName, "Badge Name")
- return badgeAdminService.existsByBadgeName(projectId, badgeName)
+ return badgeAdminService.existsByBadgeName(InputSanitizer.sanitize(projectId), InputSanitizer.sanitize(badgeName))
}
- @RequestMapping(value = "/projects/{projectId}/skillNameExists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @RequestMapping(value = "/projects/{projectId}/skillNameExists", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
boolean doesSkillNameExist(@PathVariable("projectId") String projectId,
- @RequestParam(value = "skillName", required = false) String skillName) {
+ @RequestBody NameExistsRequest existsRequest) {
+ String skillName = existsRequest.name
SkillsValidator.isNotBlank(projectId, "Project Id")
- SkillsValidator.isNotBlank(projectId, "Skill Name")
- return skillsAdminService.existsBySkillName(projectId, skillName)
+ SkillsValidator.isNotBlank(skillName, "Skill Name")
+ return skillsAdminService.existsBySkillName(InputSanitizer.sanitize(projectId), InputSanitizer.sanitize(skillName))
}
/**
@@ -211,7 +214,7 @@ class AdminController {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(id, "Entity Id")
- return skillsAdminService.existsBySkillId(projectId, id)
+ return skillsAdminService.existsBySkillId(InputSanitizer.sanitize(projectId), InputSanitizer.sanitize(id))
}
@RequestMapping(value = "/projects/{projectId}/subjects/{subjectId}", method = RequestMethod.DELETE)
@@ -643,7 +646,7 @@ class AdminController {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(userId, "User Id", projectId)
- PageRequest pageRequest = new PageRequest(page - 1, limit, ascending ? ASC : DESC, orderBy)
+ PageRequest pageRequest = PageRequest.of(page - 1, limit, ascending ? ASC : DESC, orderBy)
return userAdminService.loadUserPerformedSkillsPage(projectId, userId?.toLowerCase(), query, pageRequest)
}
@@ -669,7 +672,7 @@ class AdminController {
@RequestParam Boolean ascending) {
SkillsValidator.isNotBlank(projectId, "Project Id")
- PageRequest pageRequest = new PageRequest(page - 1, limit, ascending ? ASC : DESC, orderBy)
+ PageRequest pageRequest = PageRequest.of(page - 1, limit, ascending ? ASC : DESC, orderBy)
return adminUsersService.loadUsersPage(projectId, query, pageRequest)
}
@@ -692,7 +695,7 @@ class AdminController {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(subjectId, "Subject Id", projectId)
- PageRequest pageRequest = new PageRequest(page - 1, limit, ascending ? ASC : DESC, orderBy)
+ PageRequest pageRequest = PageRequest.of(page - 1, limit, ascending ? ASC : DESC, orderBy)
List subjectSkills = getSkills(projectId, subjectId)
List skillIds = subjectSkills.collect { it.skillId }
return adminUsersService.loadUsersPage(projectId, skillIds, query, pageRequest)
@@ -710,7 +713,7 @@ class AdminController {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(skillId, "Skill Id", projectId)
- PageRequest pageRequest = new PageRequest(page - 1, limit, ascending ? ASC : DESC, orderBy)
+ PageRequest pageRequest = PageRequest.of(page - 1, limit, ascending ? ASC : DESC, orderBy)
return adminUsersService.loadUsersPage(projectId, Collections.singletonList(skillId), query, pageRequest)
}
@@ -726,7 +729,7 @@ class AdminController {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(badgeId, "Badge Id", projectId)
- PageRequest pageRequest = new PageRequest(page - 1, limit, ascending ? ASC : DESC, orderBy)
+ PageRequest pageRequest = PageRequest.of(page - 1, limit, ascending ? ASC : DESC, orderBy)
List badgeSkills = getBadgeSkills(projectId, badgeId)
List skillIds = badgeSkills.collect { it.skillId }
if (!skillIds) {
@@ -856,7 +859,7 @@ class AdminController {
@PathVariable("skillId") String skillId) {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(skillId, "Skill Id")
- return globalBadgesService.isSkillUsedInGlobalBadge(projectId, skillId)
+ return globalBadgesService.isSkillUsedInGlobalBadge(InputSanitizer.sanitize(projectId), InputSanitizer.sanitize(skillId))
}
@RequestMapping(value = "/projects/{projectId}/subjects/{subjectId}/globalBadge/exists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@@ -865,14 +868,14 @@ class AdminController {
@PathVariable("subjectId") String subjectId) {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotBlank(subjectId, "Subject Id")
- return globalBadgesService.isSubjectUsedInGlobalBadge(projectId, subjectId)
+ return globalBadgesService.isSubjectUsedInGlobalBadge(InputSanitizer.sanitize(projectId), InputSanitizer.sanitize(subjectId))
}
@RequestMapping(value = "/projects/{projectId}/globalBadge/exists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
boolean isProjectReferencedByGlobalBadge(@PathVariable("projectId") String projectId) {
SkillsValidator.isNotBlank(projectId, "Project Id")
- return globalBadgesService.isProjectUsedInGlobalBadge(projectId)
+ return globalBadgesService.isProjectUsedInGlobalBadge(InputSanitizer.sanitize(projectId))
}
@RequestMapping(value = "/projects/{projectId}/levels/{level}/globalBadge/exists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@@ -881,6 +884,6 @@ class AdminController {
@PathVariable("level") Integer level) {
SkillsValidator.isNotBlank(projectId, "Project Id")
SkillsValidator.isNotNull(level, "Level")
- return globalBadgesService.isProjectLevelUsedInGlobalBadge(projectId, level)
+ return globalBadgesService.isProjectLevelUsedInGlobalBadge(InputSanitizer.sanitize(projectId), level)
}
}
diff --git a/backend/src/main/java/skills/controller/AdminMetricsController.groovy b/service/src/main/java/skills/controller/AdminMetricsController.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/AdminMetricsController.groovy
rename to service/src/main/java/skills/controller/AdminMetricsController.groovy
diff --git a/backend/src/main/java/skills/controller/AuthenticatedSettingsController.groovy b/service/src/main/java/skills/controller/AuthenticatedSettingsController.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/AuthenticatedSettingsController.groovy
rename to service/src/main/java/skills/controller/AuthenticatedSettingsController.groovy
diff --git a/service/src/main/java/skills/controller/ClientLoggingController.groovy b/service/src/main/java/skills/controller/ClientLoggingController.groovy
new file mode 100644
index 00000000..bd55fee7
--- /dev/null
+++ b/service/src/main/java/skills/controller/ClientLoggingController.groovy
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.controller
+
+import groovy.transform.ToString
+import groovy.util.logging.Slf4j
+import org.springframework.web.bind.annotation.*
+import skills.profile.EnableCallStackProf
+
+@RestController
+@RequestMapping("/public")
+@Slf4j
+@EnableCallStackProf
+class ClientLoggingController {
+ // Predefined logging levels.
+// Logger.TRACE = defineLogLevel(1, 'TRACE');
+// Logger.DEBUG = defineLogLevel(2, 'DEBUG');
+// Logger.INFO = defineLogLevel(3, 'INFO');
+// Logger.TIME = defineLogLevel(4, 'TIME');
+// Logger.WARN = defineLogLevel(5, 'WARN');
+// Logger.ERROR = defineLogLevel(8, 'ERROR');
+// Logger.OFF = defineLogLevel(99, 'OFF');
+
+ @CrossOrigin
+ @RequestMapping(value = "/log", method = [RequestMethod.PUT, RequestMethod.POST])
+ @ResponseBody
+ boolean writeLog(@RequestBody LogMessage logMessage) {
+ switch (logMessage.level.value) {
+ case 1:
+ log.trace(logMessage.message)
+ break
+ case 2:
+ log.debug(logMessage.message)
+ break
+ case 3:
+ log.info(logMessage.message)
+ break
+ case 5:
+ log.warn(logMessage.message)
+ break
+ case 8:
+ log.error(logMessage.message)
+ break
+ default:
+ throw new IllegalArgumentException("Unexpected log level [${logMessage}]")
+ }
+ return true
+ }
+
+ @ToString
+ static class LogMessage {
+ // {"message":"We are initialized!","level":{"value":3,"name":"INFO"}}
+ String message
+ LogLevel level
+ }
+
+ @ToString
+ static class LogLevel {
+ String name
+ Integer value
+ }
+}
diff --git a/backend/src/main/java/skills/controller/CustomIconAdminController.groovy b/service/src/main/java/skills/controller/CustomIconAdminController.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/CustomIconAdminController.groovy
rename to service/src/main/java/skills/controller/CustomIconAdminController.groovy
diff --git a/backend/src/main/java/skills/controller/CustomValidationController.groovy b/service/src/main/java/skills/controller/CustomValidationController.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/CustomValidationController.groovy
rename to service/src/main/java/skills/controller/CustomValidationController.groovy
diff --git a/backend/src/main/java/skills/controller/BootstrapController.groovy b/service/src/main/java/skills/controller/FeatureVerificationController.groovy
similarity index 55%
rename from backend/src/main/java/skills/controller/BootstrapController.groovy
rename to service/src/main/java/skills/controller/FeatureVerificationController.groovy
index 94bb111c..438882fe 100644
--- a/backend/src/main/java/skills/controller/BootstrapController.groovy
+++ b/service/src/main/java/skills/controller/FeatureVerificationController.groovy
@@ -17,30 +17,30 @@ package skills.controller
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.ui.ModelMap
import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
-import org.springframework.web.servlet.ModelAndView
-import skills.services.AccessSettingsStorageService
+import skills.profile.EnableCallStackProf
+import skills.services.FeatureService
@RestController
+@RequestMapping("/public")
@Slf4j
-@skills.profile.EnableCallStackProf
-class BootstrapController {
+@EnableCallStackProf
+class FeatureVerificationController {
@Autowired
- AccessSettingsStorageService accessSettingsStorageService
+ FeatureService featureService
- @GetMapping('/')
- ModelAndView handleBootstrap(ModelMap model) {
- if (!accessSettingsStorageService.rootAdminExists()) {
- return new ModelAndView('redirect:/bootstrap/index.html', model)
+ @GetMapping("/isFeatureSupported")
+ public boolean isFeatureSupported(@RequestParam("feature") String feature) {
+ if ("passwordreset" == feature?.toLowerCase()) {
+ return featureService.isPasswordResetFeatureEnabled()
+ } else if (feature) {
+ log.warn("Unrecognized feature requested [${feature}]")
}
- return new ModelAndView('/index.html', model)
- }
- @GetMapping('/bootstrap')
- ModelAndView handleBootstrapPart2(ModelMap modelMap) {
- return new ModelAndView('redirect:/bootstrap/index.html', modelMap)
+ return false
}
}
diff --git a/backend/src/main/java/skills/controller/GlobalMetricsController.groovy b/service/src/main/java/skills/controller/GlobalMetricsController.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/GlobalMetricsController.groovy
rename to service/src/main/java/skills/controller/GlobalMetricsController.groovy
diff --git a/backend/src/main/java/skills/controller/ProjectController.groovy b/service/src/main/java/skills/controller/ProjectController.groovy
similarity index 83%
rename from backend/src/main/java/skills/controller/ProjectController.groovy
rename to service/src/main/java/skills/controller/ProjectController.groovy
index 311c3382..dc517eb9 100644
--- a/backend/src/main/java/skills/controller/ProjectController.groovy
+++ b/service/src/main/java/skills/controller/ProjectController.groovy
@@ -24,6 +24,7 @@ import skills.auth.UserInfoService
import skills.controller.exceptions.ErrorCode
import skills.controller.exceptions.SkillException
import skills.controller.exceptions.SkillsValidator
+import skills.controller.request.model.ProjectExistsRequest
import skills.controller.request.model.ProjectRequest
import skills.controller.result.model.CustomIconResult
import skills.controller.result.model.ProjectResult
@@ -34,6 +35,7 @@ import skills.services.IdFormatValidator
import skills.services.admin.ProjAdminService
import skills.services.admin.ShareSkillsService
import skills.services.admin.SkillsAdminService
+import skills.utils.InputSanitizer
import java.nio.charset.StandardCharsets
@@ -65,8 +67,8 @@ class ProjectController {
@RequestMapping(value = "/projects", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
- List getProjects() {
- return projAdminService.getProjects()
+ List getProjects(@RequestParam(required = false, defaultValue = "", name = "search") String search) {
+ return projAdminService.getProjects(search)
}
@RequestMapping(value = "/projects/{id}", method = [RequestMethod.PUT, RequestMethod.POST], produces = "application/json")
@@ -99,22 +101,34 @@ class ProjectController {
throw new SkillException("Project name was not provided.", projectId, null, ErrorCode.BadParam)
}
+ projectRequest.projectId = InputSanitizer.sanitize(projectRequest.projectId)
+ projectRequest.name = InputSanitizer.sanitize(projectRequest.name)
+
projAdminService.saveProject(null, projectRequest)
return new RequestResult(success: true)
}
- @RequestMapping(value = "/projectExist", method = RequestMethod.GET, produces = "application/json")
+ //need pin/unpin controller. Only permit if:
+ /*
+ boolean isRoot = userInfo.authorities?.find() {
+ it instanceof UserSkillsGrantedAuthority && RoleName.ROLE_SUPER_DUPER_USER == it.role?.roleName
+ }
+ */
+
+ @RequestMapping(value = "/projectExist", method = RequestMethod.POST, produces = "application/json")
@ResponseBody
- boolean doesProjectExist(@RequestParam(value = "projectId", required = false) String projectId,
- @RequestParam(value = "projectName", required = false) String projectName) {
+ boolean doesProjectExist(@RequestBody ProjectExistsRequest existsRequest) {
+ String projectId = existsRequest.projectId
+ String projectName = existsRequest.name
+
SkillsValidator.isTrue((projectId || projectName), "One of Project Id or Project Name must be provided.")
SkillsValidator.isTrue(!(projectId && projectName), "Only Project Id or Project Name may be provided, not both.")
if (projectId) {
- return projAdminService.existsByProjectId(projectId)
+ return projAdminService.existsByProjectId(InputSanitizer.sanitize(projectId))
}
- return projAdminService.existsByProjectName(projectName)
+ return projAdminService.existsByProjectName(InputSanitizer.sanitize(projectName))
}
@RequestMapping(value = "/projects/{id}/customIcons", method = RequestMethod.GET, produces = "application/json")
diff --git a/service/src/main/java/skills/controller/PublicConfigController.groovy b/service/src/main/java/skills/controller/PublicConfigController.groovy
new file mode 100644
index 00000000..333b0265
--- /dev/null
+++ b/service/src/main/java/skills/controller/PublicConfigController.groovy
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.controller
+
+import groovy.util.logging.Slf4j
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
+import org.springframework.web.bind.annotation.*
+import skills.HealthChecker
+import skills.UIConfigProperties
+import skills.auth.AuthMode
+import skills.controller.result.model.SettingsResult
+import skills.profile.EnableCallStackProf
+import skills.services.AccessSettingsStorageService
+import skills.services.SystemSettingsService
+import skills.services.settings.Settings
+import skills.services.settings.SettingsService
+
+@RestController
+@RequestMapping("/public")
+@Slf4j
+@EnableCallStackProf
+class PublicConfigController {
+
+ @Autowired
+ HealthChecker healthChecker
+
+ @Autowired
+ UIConfigProperties uiConfigProperties
+
+ @Autowired
+ AccessSettingsStorageService accessSettingsStorageService
+
+ @Value('${skills.authorization.authMode:#{T(skills.auth.AuthMode).DEFAULT_AUTH_MODE}}')
+ AuthMode authMode
+
+ @Autowired
+ SettingsService settingsService
+
+ @Autowired
+ private ClientRegistrationRepository clientRegistrationRepository;
+
+ @RequestMapping(value = "/config", method = RequestMethod.GET, produces = "application/json")
+ @ResponseBody
+ Map getConfig(){
+ Map res = new HashMap<>(uiConfigProperties.ui)
+ res["authMode"] = authMode.name()
+ res["needToBootstrap"] = !accessSettingsStorageService.rootAdminExists()
+ List customizationSettings = settingsService.getGlobalSettingsByGroup(SystemSettingsService.CUSTOMIZATION)
+ customizationSettings?.each {
+ if (Settings.GLOBAL_CUSTOM_HEADER.settingName == it.setting) {
+ res["customHeader"] = it.value
+ } else if (Settings.GLOBAL_CUSTOM_FOOTER.settingName == it.setting) {
+ res["customFooter"] = it.value
+ }
+ }
+ return res
+ }
+
+ final private static Map statusRes = [
+ status: "OK",
+ ]
+
+ @CrossOrigin
+ @RequestMapping(value = "/status", method = RequestMethod.GET, produces = "application/json")
+ @ResponseBody
+ def status() {
+ healthChecker.checkRequiredServices()
+ Map res = new HashMap<>(statusRes)
+ res['clientLib'] = uiConfigProperties.client
+ def oAuthProviders = clientRegistrationRepository?.collect {it?.registrationId }
+ if (oAuthProviders) {
+ res['oAuthProviders'] = oAuthProviders
+ }
+ return res
+ }
+}
diff --git a/backend/src/main/java/skills/controller/PublicPropsBasedValidator.groovy b/service/src/main/java/skills/controller/PublicPropsBasedValidator.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/PublicPropsBasedValidator.groovy
rename to service/src/main/java/skills/controller/PublicPropsBasedValidator.groovy
diff --git a/backend/src/main/java/skills/controller/RootController.groovy b/service/src/main/java/skills/controller/RootController.groovy
similarity index 82%
rename from backend/src/main/java/skills/controller/RootController.groovy
rename to service/src/main/java/skills/controller/RootController.groovy
index 014f0f1c..73671c25 100644
--- a/backend/src/main/java/skills/controller/RootController.groovy
+++ b/service/src/main/java/skills/controller/RootController.groovy
@@ -30,16 +30,25 @@ import skills.controller.exceptions.SkillsValidator
import skills.controller.request.model.GlobalSettingsRequest
import skills.controller.request.model.SuggestRequest
import skills.controller.result.model.RequestResult
+import skills.controller.result.model.SettingsResult
import skills.controller.result.model.UserInfoRes
import skills.controller.result.model.UserRoleRes
import skills.profile.EnableCallStackProf
import skills.services.AccessSettingsStorageService
+import skills.services.SystemSettingsService
+import skills.services.admin.ProjAdminService
+import skills.services.settings.Settings
import skills.services.settings.SettingsService
+import skills.settings.EmailConfigurationResult
import skills.settings.EmailConnectionInfo
import skills.settings.EmailSettingsService
+import skills.settings.SystemSettings
+import skills.storage.model.Setting
import skills.storage.model.auth.RoleName
import java.security.Principal
+import java.time.Duration
+import java.time.format.DateTimeParseException
@RestController
@RequestMapping('/root')
@@ -65,6 +74,12 @@ class RootController {
@Autowired
SettingsService settingsService
+ @Autowired
+ SystemSettingsService systemSettingsService
+
+ @Autowired
+ ProjAdminService projAdminService
+
@GetMapping('/rootUsers')
@ResponseBody
List getRootUsers() {
@@ -168,8 +183,25 @@ class RootController {
}
@PostMapping('/saveEmailSettings')
- void saveEmailSettings(@RequestBody EmailConnectionInfo emailConnectionInfo) {
- emailSettingsService.updateConnectionInfo(emailConnectionInfo)
+ RequestResult saveEmailSettings(@RequestBody EmailConnectionInfo emailConnectionInfo) {
+ EmailConfigurationResult success = emailSettingsService.updateConnectionInfo(emailConnectionInfo)
+ return new RequestResult(success: success?.configurationSuccessful, explanation: success?.explanation)
+ }
+
+ @GetMapping('/getEmailSettings')
+ EmailConnectionInfo fetchEmailSettings(){
+ emailSettingsService.fetchEmailSettings()
+ }
+
+ @PostMapping('/saveSystemSettings')
+ RequestResult saveSystemSettings(@RequestBody SystemSettings settings){
+ systemSettingsService.save(settings)
+ return RequestResult.success()
+ }
+
+ @GetMapping('/getSystemSettings')
+ SystemSettings getSystemSettings(){
+ return systemSettingsService.get()
}
@RequestMapping(value = "/global/settings/{setting}", method = [RequestMethod.PUT, RequestMethod.POST], produces = MediaType.APPLICATION_JSON_VALUE)
@@ -181,6 +213,20 @@ class RootController {
return new RequestResult(success: true)
}
+ @PostMapping('/pin/{projectId}')
+ RequestResult pinProject(@PathVariable("projectId") String projectId) {
+ projAdminService.pinProjectForRootUser(projectId)
+ return new RequestResult(success: true)
+ }
+
+ @DeleteMapping('/pin/{projectId}')
+ RequestResult unpinProject(@PathVariable("projectId") String projectId) {
+ projAdminService.unpinProjectForRootUser(projectId)
+ return new RequestResult(success: true)
+ }
+
+
+
private String getUserId(String userKey) {
// userKey will be the userId when in FORM authMode, or the DN when in PKI auth mode.
// When in PKI auth mode, the userDetailsService implementation will create the user
diff --git a/backend/src/main/java/skills/controller/SupervisorController.groovy b/service/src/main/java/skills/controller/SupervisorController.groovy
similarity index 95%
rename from backend/src/main/java/skills/controller/SupervisorController.groovy
rename to service/src/main/java/skills/controller/SupervisorController.groovy
index 1854f5e0..d3c23454 100644
--- a/backend/src/main/java/skills/controller/SupervisorController.groovy
+++ b/service/src/main/java/skills/controller/SupervisorController.groovy
@@ -28,6 +28,7 @@ import skills.controller.exceptions.MaxIconSizeExceeded
import skills.controller.exceptions.SkillsValidator
import skills.controller.request.model.ActionPatchRequest
import skills.controller.request.model.BadgeRequest
+import skills.controller.request.model.NameExistsRequest
import skills.controller.result.model.*
import skills.icons.CustomIconFacade
import skills.icons.UploadedIcon
@@ -62,11 +63,12 @@ class SupervisorController {
@Autowired
PublicPropsBasedValidator propsBasedValidator
- @RequestMapping(value = "/badges/name/{badgeName}/exists", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @RequestMapping(value = "/badges/name/exists", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
- boolean doesBadgeNameExist(@PathVariable("badgeName") String badgeName) {
+ boolean doesBadgeNameExist(@RequestBody() NameExistsRequest nameExistsRequest) {
+ String badgeName = nameExistsRequest.name
SkillsValidator.isNotBlank(badgeName, "Badge Name")
- String decodedName = URLDecoder.decode(badgeName, StandardCharsets.UTF_8.toString())
+ String decodedName = InputSanitizer.sanitize(badgeName)
return globalBadgesService.existsByBadgeName(decodedName)
}
@@ -74,7 +76,7 @@ class SupervisorController {
@ResponseBody
boolean doesBadgeIdExist(@PathVariable("badgeId") String badgeId) {
SkillsValidator.isNotBlank(badgeId, "Badge Id")
- String decodedId = URLDecoder.decode(badgeId, StandardCharsets.UTF_8.toString())
+ String decodedId = InputSanitizer.sanitize(URLDecoder.decode(badgeId, StandardCharsets.UTF_8.toString()))
return globalBadgesService.existsByBadgeId(decodedId)
}
diff --git a/backend/src/main/java/skills/controller/UserInfoController.groovy b/service/src/main/java/skills/controller/UserInfoController.groovy
similarity index 97%
rename from backend/src/main/java/skills/controller/UserInfoController.groovy
rename to service/src/main/java/skills/controller/UserInfoController.groovy
index adc64d27..4faa7f62 100644
--- a/backend/src/main/java/skills/controller/UserInfoController.groovy
+++ b/service/src/main/java/skills/controller/UserInfoController.groovy
@@ -132,12 +132,12 @@ class UserInfoController {
@RequestMapping(value = "/users/projects/{projectId}/suggestClientUsers", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
List suggestExistingClientUsersForProject(@PathVariable("projectId") String projectId, @RequestBody SuggestRequest suggestRequest) {
- return userAdminService.suggestUsersForProject(projectId, suggestRequest.suggestQuery, new PageRequest(0, 5)).collect { new UserInfoRes(userId: it) }
+ return userAdminService.suggestUsersForProject(projectId, suggestRequest.suggestQuery, PageRequest.of(0, 5)).collect { new UserInfoRes(userId: it) }
}
@RequestMapping(value = "/users/suggestClientUsers/", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
List suggestExistingClientUsers(@RequestBody SuggestRequest suggestRequest) {
- return userAdminService.suggestUsers(suggestRequest.suggestQuery, new PageRequest(0, 5)).collect { new UserInfoRes(userId: it) }
+ return userAdminService.suggestUsers(suggestRequest.suggestQuery, PageRequest.of(0, 5)).collect { new UserInfoRes(userId: it) }
}
@RequestMapping(value = "/users/projects/{projectId}/validExistingClientUserId/{userId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
diff --git a/backend/src/main/java/skills/controller/UserSkillsController.java b/service/src/main/java/skills/controller/UserSkillsController.java
similarity index 93%
rename from backend/src/main/java/skills/controller/UserSkillsController.java
rename to service/src/main/java/skills/controller/UserSkillsController.java
index 78b36ae0..791bec09 100644
--- a/backend/src/main/java/skills/controller/UserSkillsController.java
+++ b/service/src/main/java/skills/controller/UserSkillsController.java
@@ -42,6 +42,8 @@
import skills.utils.RetryUtil;
import javax.servlet.http.HttpServletRequest;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -76,6 +78,9 @@ class UserSkillsController {
@Autowired
private PublicProps publicProps;
+ @Autowired
+ private SkillEventsService skillEventsService;
+
private int getProvidedVersionOrReturnDefault(Integer versionParam) {
if (versionParam != null) {
return versionParam;
@@ -111,14 +116,24 @@ public Integer getUserLevel(@PathVariable(name = "projectId") String projectId,
@ResponseBody
@CompileStatic
@Profile
- public OverallSkillSummary getSkillsSummary(@PathVariable("projectId") String projectId,
+ public OverallSkillSummary getSkillsSummary(HttpServletRequest request,
+ @PathVariable("projectId") String projectId,
@RequestParam(name = "userId", required = false) String userIdParam,
@RequestParam(name = "version", required = false) Integer version,
@RequestParam(name = "idType", required = false) String idType) {
String userId = userInfoService.getUserName(userIdParam, true, idType);
+
+ log.debug("userId is {} and userIdParam is {}", userId, userIdParam);
return skillsLoader.loadOverallSummary(projectId, userId, getProvidedVersionOrReturnDefault(version));
}
+ private boolean isRequestFromDashboard(HttpServletRequest request) throws UnknownHostException{
+ InetAddress requestorIp = InetAddress.getByName(request.getRemoteAddr());
+ log.debug("remote port: [{}], local port: [{}]", request.getRemotePort(), request.getLocalPort());
+ return (requestorIp.isAnyLocalAddress() || requestorIp.isLoopbackAddress())
+ && (request.getRemotePort() == request.getLocalPort());
+ }
+
@RequestMapping(value = "/projects/{projectId}/subjects/{subjectId}/summary", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
@CompileStatic
@@ -136,8 +151,10 @@ public SkillSubjectSummary getSubjectSummary(@PathVariable("projectId") String p
@CompileStatic
public List getSubjectSkillsDescriptions(@PathVariable("projectId") String projectId,
@PathVariable("subjectId") String subjectId,
+ @RequestParam(name = "userId", required = false) String userIdParam,
@RequestParam(name = "version", required = false) Integer version) {
- return skillsLoader.loadSubjectDescriptions(projectId, subjectId, getProvidedVersionOrReturnDefault(version));
+ String userId = userInfoService.getUserName(userIdParam, true);
+ return skillsLoader.loadSubjectDescriptions(projectId, subjectId, userId, getProvidedVersionOrReturnDefault(version));
}
/**
@@ -192,11 +209,13 @@ public List getAllBadgesSummary(@PathVariable("projectId") St
public List getBadgeSkillsDescriptions(@PathVariable("projectId") String projectId,
@PathVariable("badgeId") String badgeId,
@RequestParam(name = "version", required = false) Integer version,
+ @RequestParam(name = "userId", required = false) String userIdParam,
@RequestParam(name = "global", required = false) Boolean isGlobal) {
+ String userId = userInfoService.getUserName(userIdParam, true);
if (isGlobal != null && isGlobal) {
- return skillsLoader.loadGlobalBadgeDescriptions(badgeId, getProvidedVersionOrReturnDefault(version));
+ return skillsLoader.loadGlobalBadgeDescriptions(badgeId, userId, getProvidedVersionOrReturnDefault(version));
} else {
- return skillsLoader.loadBadgeDescriptions(projectId, badgeId, getProvidedVersionOrReturnDefault(version));
+ return skillsLoader.loadBadgeDescriptions(projectId, badgeId, userId, getProvidedVersionOrReturnDefault(version));
}
}
@@ -271,8 +290,6 @@ public SkillEventResult addSkill(@PathVariable("projectId") String projectId,
//let's account for some possible clock drift
SkillsValidator.isTrue(requestedTimestamp <= (System.currentTimeMillis() + 30000), "Skill Events may not be in the future", projectId, skillId);
incomingDate = new Date(requestedTimestamp);
- } else {
- incomingDate = new Date();
}
SkillEventResult result;
diff --git a/service/src/main/java/skills/controller/UserTokenController.groovy b/service/src/main/java/skills/controller/UserTokenController.groovy
new file mode 100644
index 00000000..6b244f3e
--- /dev/null
+++ b/service/src/main/java/skills/controller/UserTokenController.groovy
@@ -0,0 +1,122 @@
+/**
+ * Copyright 2020 SkillTree
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package skills.controller
+
+import groovy.util.logging.Slf4j
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Conditional
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
+import org.springframework.security.oauth2.common.OAuth2AccessToken
+import org.springframework.security.oauth2.provider.OAuth2Authentication
+import org.springframework.security.oauth2.provider.OAuth2Request
+import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint
+import org.springframework.security.oauth2.provider.token.DefaultTokenServices
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter
+import org.springframework.web.bind.annotation.*
+import skills.auth.SecurityMode
+import skills.auth.form.jwt.JwtHelper
+import skills.services.InceptionProjectService
+
+@Conditional(SecurityMode.FormAuth)
+@RestController
+@skills.profile.EnableCallStackProf
+@Slf4j
+class UserTokenController {
+
+ @Autowired
+ TokenEndpoint tokenEndpoint
+
+ @Autowired
+ InMemoryOAuth2AuthorizedClientService authorizedClientService
+
+ @Autowired
+ JwtHelper jwtHelper
+
+ @Autowired
+ private JwtAccessTokenConverter jwtAccessTokenConverter;
+
+ @Autowired
+ private DefaultTokenServices tokenServices
+
+// @PostConstruct
+// void afterPropertiesSet() {
+// tokenServices = new DefaultTokenServices()
+// tokenServices.setTokenStore(jwtAccessTokenConverter)
+// tokenServices.setTokenEnhancer(jwtAccessTokenConverter)
+// }
+
+ /**
+ * token for inception
+ * @param userId
+ * @return
+ */
+ @RequestMapping(value = "/app/projects/Inception/users/{userId}/token", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseBody
+ ResponseEntity getUserToken(@PathVariable("userId") String userId) {
+ return createSkillsProxyToken(InceptionProjectService.inceptionProjectId, userId)
+ }
+
+ /**
+ * token for current user already authenticated via third party OAuth2 provider (eg, google, gitlab, etc.)
+ * @param projectId
+ * @return
+ */
+ @RequestMapping(value = "/api/projects/{projectId}/token", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseBody
+ @CrossOrigin(allowCredentials = 'true')
+ ResponseEntity getOAuth2UserToken(@PathVariable("projectId") String projectId) {
+ Object authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication.credentials instanceof OAuth2AuthenticationToken && authentication.isAuthenticated()) {
+ OAuth2AuthenticationToken oAuth2AuthenticationToken = authentication.credentials
+ String oauthProvider = oAuth2AuthenticationToken.authorizedClientRegistrationId
+ String userId = authentication.name
+ log.debug("Creating self-proxy OAUth Token for [{}] or project [{}], authenticated via [{}] OAuth provider", userId, projectId, oauthProvider)
+ return createSkillsProxyToken(projectId, userId)
+ }
+ }
+
+ private OAuth2Authentication convertAuthentication(Authentication authentication, String clientId) {
+ OAuth2Request request = new OAuth2Request(null, clientId, null, true, null, null, null, null, null)
+ return new OAuth2Authentication(request, authentication)
+ }
+
+ /**
+ * utilized by client-display within a project that previews that project's points
+ * @param projectId
+ * @param userId
+ * @return
+ */
+ @RequestMapping(value = "/admin/projects/{projectId}/token/{userId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseBody
+ ResponseEntity getUserToken(@PathVariable("projectId") String projectId, @PathVariable("userId") String userId) {
+ return createSkillsProxyToken(projectId, userId)
+ }
+
+ private ResponseEntity createSkillsProxyToken(String projectId, String userId) {
+ skills.controller.exceptions.SkillsValidator.isNotBlank(projectId, "Project Id")
+ skills.controller.exceptions.SkillsValidator.isNotBlank(userId, "User Id")
+
+ UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken(projectId, null, [])
+ Map parameters = [grant_type: 'client_credentials', proxy_user: userId]
+ return tokenEndpoint.postAccessToken(principal, parameters)
+ }
+}
diff --git a/backend/src/main/java/skills/controller/exceptions/DataIntegrityViolationExceptionHandler.groovy b/service/src/main/java/skills/controller/exceptions/DataIntegrityViolationExceptionHandler.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/exceptions/DataIntegrityViolationExceptionHandler.groovy
rename to service/src/main/java/skills/controller/exceptions/DataIntegrityViolationExceptionHandler.groovy
diff --git a/backend/src/main/java/skills/controller/exceptions/ErrorCode.groovy b/service/src/main/java/skills/controller/exceptions/ErrorCode.groovy
similarity index 89%
rename from backend/src/main/java/skills/controller/exceptions/ErrorCode.groovy
rename to service/src/main/java/skills/controller/exceptions/ErrorCode.groovy
index f6c91885..1f9abc18 100644
--- a/backend/src/main/java/skills/controller/exceptions/ErrorCode.groovy
+++ b/service/src/main/java/skills/controller/exceptions/ErrorCode.groovy
@@ -23,5 +23,9 @@ enum ErrorCode {
ConstraintViolation,
BadParam,
AccessDenied,
- UserNotFound
+ UserNotFound,
+ SkillNotFound,
+ BadgeNotFound,
+ SubjectNotFound,
+ ProjectNotFound
}
diff --git a/backend/src/main/java/skills/controller/exceptions/InvalidContentTypeException.groovy b/service/src/main/java/skills/controller/exceptions/InvalidContentTypeException.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/exceptions/InvalidContentTypeException.groovy
rename to service/src/main/java/skills/controller/exceptions/InvalidContentTypeException.groovy
diff --git a/backend/src/main/java/skills/controller/exceptions/MaxIconSizeExceeded.groovy b/service/src/main/java/skills/controller/exceptions/MaxIconSizeExceeded.groovy
similarity index 100%
rename from backend/src/main/java/skills/controller/exceptions/MaxIconSizeExceeded.groovy
rename to service/src/main/java/skills/controller/exceptions/MaxIconSizeExceeded.groovy
diff --git a/backend/src/main/java/skills/controller/exceptions/RestExceptionHandler.groovy b/service/src/main/java/skills/controller/exceptions/RestExceptionHandler.groovy
similarity index 95%
rename from backend/src/main/java/skills/controller/exceptions/RestExceptionHandler.groovy
rename to service/src/main/java/skills/controller/exceptions/RestExceptionHandler.groovy
index d799c59f..7671fc35 100644
--- a/backend/src/main/java/skills/controller/exceptions/RestExceptionHandler.groovy
+++ b/service/src/main/java/skills/controller/exceptions/RestExceptionHandler.groovy
@@ -37,6 +37,8 @@ import skills.controller.exceptions.SkillException.SkillExceptionLogLevel
@Slf4j
class RestExceptionHandler extends ResponseEntityExceptionHandler {
+ static final List NOT_FOUND_CODES = [ErrorCode.SkillNotFound, ErrorCode.SubjectNotFound, ErrorCode.ProjectNotFound, ErrorCode.BadgeNotFound]
+
static class BasicErrBody {
String explanation
String errorCode = ErrorCode.InternalError
@@ -52,6 +54,7 @@ class RestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(SkillException)
protected ResponseEntity