diff --git a/.github/workflows/build-and-test-postgres.yml b/.github/workflows/build-and-test-postgres.yml new file mode 100644 index 00000000..247918e8 --- /dev/null +++ b/.github/workflows/build-and-test-postgres.yml @@ -0,0 +1,105 @@ +# 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: Run 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 + npm run cy:run:postgres + cd .. + + - 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/cypress + ./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..30172f01 --- /dev/null +++ b/.github/workflows/build-and-test-redis.yml @@ -0,0 +1,99 @@ +# 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: Run Cypress tests + run: | + cd e2e-tests + npm install + npm run cyServices:start:skills-service:redis + npm run cyServices:start:client-display + npm run cy:run + cd .. + + - 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/cypress + ./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..510aee41 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,177 @@ +# 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-and-ui-against-h2: + runs-on: ubuntu-latest + + 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 + + - name: Run Cypress tests + run: | + cd e2e-tests + npm install + npm run cyServices:start + npm run cy:run + cd .. + + - name: upload result artifacts + if: ${{ always() }} + uses: actions/upload-artifact@v2 + with: + name: CI result artifacts + path: | + ./service/target/*.log + ./e2e-tests/cypress + ./e2e-tests/logs + + - name: upload service jar + uses: actions/upload-artifact@v2 + with: + name: service jar + path: ./service/target/*.jar + + 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/cypress + ./e2e-tests/logs + + publish-snapshot-docker-image: + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: [service-and-ui-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..ac9a3c22 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ 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 --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..357542b4 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,16 @@ +# 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 + +[![CI Badge](https://github.com/NationalSecurityAgency/skills-service/workflows/Continuous%20Integration/badge.svg)](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Continuous+Integration%22) + + +[![DB Test Badge](https://github.com/NationalSecurityAgency/skills-service/workflows/Test%20against%20PostgreSQL/badge.svg)](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Test+against+PostgreSQL%22) + +[![Test storing HttpSession in Redis](https://github.com/NationalSecurityAgency/skills-service/workflows/Test%20storing%20HttpSession%20in%20Redis/badge.svg)](https://github.com/NationalSecurityAgency/skills-service/actions?query=workflow%3A%22Test+storing+HttpSession+in+Redis%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/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..c88d700f --- /dev/null +++ b/call-stack-profiler/pom.xml @@ -0,0 +1,304 @@ + + + 4.0.0 + + + skills-service-parent + skill-tree + 1.2.3-SNAPSHOT + + + call-stack-profiler + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 2.5.13 + 1.3-groovy-2.5 + + 3.4.0-01 + + 2.5.7-01 + + 3.7.0 + 3.11 + 2.10.6 + 2.13.3 + + 2.7 + 1.11.2 + + + + + org.codehaus.groovy + groovy-all + ${groovy.version} + provided + pom + + + + 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} + + + org.codehaus.groovy + groovy-all + + + + + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + maven-compiler-plugin + ${maven.compiler.plugin.version} + + 1.8 + 1.8 + 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.mojo + versions-maven-plugin + ${versions.maven.plugin} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18.1 + + + **/*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 + The 3-Clause BSD License + + + The Apache Software License, Version 2.0|Apache License, Version 2.0 + Eclipse Public License - Version 1.0|Eclipse Public License 1.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..0d794f60 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", @@ -24,9 +24,11 @@ "axios": "0.19.0", "axios-auth-refresh": "1.0.7", "bootstrap": "4.3.1", - "lodash": "4.17.15", + "dompurify": "2.0.3", + "lodash": "4.17.20", "marked": "0.7.0", "material-icons": "0.3.1", + "node-emoji": "1.10.0", "numeral": "2.0.6", "postmate": "1.5.1", "q": "1.5.1", @@ -36,13 +38,12 @@ "vue": "2.6.10", "vue-apexcharts": "1.5.0", "vue-js-toggle-button": "1.3.2", - "vue-moment": "4.0.0", + "vue-moment": "4.1.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" + "vuex": "3.1.1" }, "devDependencies": { "@vue/cli-plugin-babel": "4.1.0", diff --git a/client-display/pom.xml b/client-display/pom.xml index ae5d5fba..47f32606 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.2.3-SNAPSHOT 4.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/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/frontend/build/build.js b/dashboard/build/build.js similarity index 100% rename from frontend/build/build.js rename to dashboard/build/build.js diff --git a/frontend/build/check-versions.js b/dashboard/build/check-versions.js similarity index 100% rename from frontend/build/check-versions.js rename to dashboard/build/check-versions.js diff --git a/frontend/build/logo.png b/dashboard/build/logo.png similarity index 100% rename from frontend/build/logo.png rename to dashboard/build/logo.png diff --git a/frontend/build/utils.js b/dashboard/build/utils.js similarity index 100% rename from frontend/build/utils.js rename to dashboard/build/utils.js diff --git a/frontend/build/vue-loader.conf.js b/dashboard/build/vue-loader.conf.js similarity index 100% rename from frontend/build/vue-loader.conf.js rename to dashboard/build/vue-loader.conf.js diff --git a/frontend/build/webpack.base.conf.js b/dashboard/build/webpack.base.conf.js similarity index 100% rename from frontend/build/webpack.base.conf.js rename to dashboard/build/webpack.base.conf.js diff --git a/frontend/build/webpack.dev.conf.js b/dashboard/build/webpack.dev.conf.js similarity index 100% rename from frontend/build/webpack.dev.conf.js rename to dashboard/build/webpack.dev.conf.js diff --git a/frontend/build/webpack.prod.conf.js b/dashboard/build/webpack.prod.conf.js similarity index 100% rename from frontend/build/webpack.prod.conf.js rename to dashboard/build/webpack.prod.conf.js diff --git a/frontend/package.json b/dashboard/package.json similarity index 82% rename from frontend/package.json rename to dashboard/package.json index 961e3818..32cd3329 100644 --- a/frontend/package.json +++ b/dashboard/package.json @@ -1,27 +1,27 @@ { - "name": "frontend", + "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;Custom: https://travis-ci.org/component/emitter.png' --summary", + "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 ../backend/src/main/resources/public/static/js && cp -rT dist ../backend/src/main/resources/public/ && cp -rT dist ../backend/target/classes/public/", + "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.11.2", - "@skills/skills-client-vue": "2.0.0", + "@skilltree/skills-client-vue": "3.0.1", "animate.css": "3.7.2", "apexcharts": "3.8.6", - "axios": "0.19.0", + "axios": "0.19.2", "babel-polyfill": "6.26.0", "bootstrap": "4.3.1", "bootstrap-vue": "2.0.2", @@ -33,6 +33,7 @@ "matchmedia-polyfill": "0.3.2", "material-icons": "0.3.1", "moment": "2.24.0", + "node-emoji": "1.10.0", "numeral": "2.0.6", "sockjs-client": "1.4.0", "vee-validate": "2.2.15", 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..7fb9fea6 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.2.3-SNAPSHOT 4.0.0 - frontend + dashboard UTF-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/RequestAccess.vue b/dashboard/src/components/access/RequestAccess.vue similarity index 66% rename from frontend/src/components/access/RequestAccess.vue rename to dashboard/src/components/access/RequestAccess.vue index c1ebe176..97a0760e 100644 --- a/frontend/src/components/access/RequestAccess.vue +++ b/dashboard/src/components/access/RequestAccess.vue @@ -18,52 +18,59 @@ limitations under the License.
- -

Create Skills Dashboard Account

+ +

+ New SkillTree Root Account + New SkillTree Account +

- - + + {{ errors.first('firstName')}}
- - + + {{ errors.first('lastName')}}
- - Email + {{ errors.first('email')}}
- - Password + {{ errors.first('password')}}
- - Confirm Password + {{ errors.first('password_confirmation')}}
- +
+ Bootstrapping! May take a second... +
-
+

-

Already have a User Skills account? +

Already have an account? Sign in

@@ -102,6 +109,12 @@ limitations under the License. export default { name: 'RequestAccount', + props: { + isRootAccount: { + type: Boolean, + default: false, + }, + }, data() { return { loginFields: { @@ -110,13 +123,15 @@ limitations under the License. email: '', password: '', }, + createInProgress: false, }; }, methods: { login() { this.$validator.validate().then((valid) => { if (valid) { - this.$store.dispatch('signup', this.loginFields).then(() => { + this.createInProgress = true; + this.$store.dispatch('signup', Object.assign({ isRootAccount: this.isRootAccount }, this.loginFields)).then(() => { this.$router.push({ name: 'HomePage' }); }); } diff --git a/dashboard/src/components/access/RequestPasswordReset.vue b/dashboard/src/components/access/RequestPasswordReset.vue new file mode 100644 index 00000000..211e989a --- /dev/null +++ b/dashboard/src/components/access/RequestPasswordReset.vue @@ -0,0 +1,144 @@ +/* +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/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. +*/ + + + 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. +*/ + + + 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. +*/ + + + diff --git a/dashboard/src/components/access/ResetPassword.vue b/dashboard/src/components/access/ResetPassword.vue new file mode 100644 index 00000000..19f42a7f --- /dev/null +++ b/dashboard/src/components/access/ResetPassword.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. +*/ + + + + + diff --git a/frontend/src/components/access/RoleManager.vue b/dashboard/src/components/access/RoleManager.vue similarity index 98% rename from frontend/src/components/access/RoleManager.vue rename to dashboard/src/components/access/RoleManager.vue index e5718b4f..0bd21739 100644 --- a/frontend/src/components/access/RoleManager.vue +++ b/dashboard/src/components/access/RoleManager.vue @@ -28,7 +28,8 @@ limitations under the License.
- + Error! Request could not be completed! {{ errNotification.msg }} 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..97ba3346 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 + +
@@ -59,6 +71,11 @@ limitations under the License. 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 = Object.assign({}, 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 100% rename from frontend/src/components/badges/BadgePage.vue rename to dashboard/src/components/badges/BadgePage.vue diff --git a/frontend/src/components/badges/BadgeSkills.vue b/dashboard/src/components/badges/BadgeSkills.vue similarity index 98% rename from frontend/src/components/badges/BadgeSkills.vue rename to dashboard/src/components/badges/BadgeSkills.vue index 76069c9a..e91b7ffa 100644 --- a/frontend/src/components/badges/BadgeSkills.vue +++ b/dashboard/src/components/badges/BadgeSkills.vue @@ -35,7 +35,7 @@ limitations under the License. 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 100% rename from frontend/src/components/header/Breadcrumb.vue rename to dashboard/src/components/header/Breadcrumb.vue 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 100% rename from frontend/src/components/header/HelpButton.vue rename to dashboard/src/components/header/HelpButton.vue 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. + + diff --git a/frontend/src/components/skills/AddSkillEvent.vue b/dashboard/src/components/skills/AddSkillEvent.vue similarity index 100% rename from frontend/src/components/skills/AddSkillEvent.vue rename to dashboard/src/components/skills/AddSkillEvent.vue diff --git a/frontend/src/components/skills/ChildRowSkillsDisplay.vue b/dashboard/src/components/skills/ChildRowSkillsDisplay.vue similarity index 91% rename from frontend/src/components/skills/ChildRowSkillsDisplay.vue rename to dashboard/src/components/skills/ChildRowSkillsDisplay.vue index 90eb09cd..004aa6e9 100644 --- a/frontend/src/components/skills/ChildRowSkillsDisplay.vue +++ b/dashboard/src/components/skills/ChildRowSkillsDisplay.vue @@ -41,7 +41,7 @@ limitations under the License. Description
-

+

Not Specified

@@ -68,10 +68,11 @@ limitations under the License. import SkillsService from './SkillsService'; import MediaInfoCard from '../utils/cards/MediaInfoCard'; import NumberFilter from '../../filters/NumberFilter'; + import MarkdownText from '../utils/MarkdownText'; export default { name: 'ChildRowSkillsDisplay', - components: { MediaInfoCard, LoadingContainer }, + components: { MarkdownText, MediaInfoCard, LoadingContainer }, props: { projectId: { type: String, @@ -180,4 +181,23 @@ limitations under the License. padding: 0 1rem 0 0.5rem; } + .markdown blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 1rem; + border-left: 5px solid #eeeeee; + color: #888; + line-height: 1.5; + } + + .markdown pre { + border: 1px solid #dddddd !important; + margin: 1rem; + padding: 1rem; + overflow: auto; + font-size: 85%; + border-radius: 6px; + background-color: #f6f8fa; + } + diff --git a/frontend/src/components/skills/EditSkill.vue b/dashboard/src/components/skills/EditSkill.vue similarity index 100% rename from frontend/src/components/skills/EditSkill.vue rename to dashboard/src/components/skills/EditSkill.vue 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 100% rename from frontend/src/components/skills/SimpleSkillsTable.vue rename to dashboard/src/components/skills/SimpleSkillsTable.vue 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 100% rename from frontend/src/components/skills/Skills.vue rename to dashboard/src/components/skills/Skills.vue diff --git a/frontend/src/components/skills/SkillsSelector2.vue b/dashboard/src/components/skills/SkillsSelector2.vue similarity index 100% rename from frontend/src/components/skills/SkillsSelector2.vue rename to dashboard/src/components/skills/SkillsSelector2.vue diff --git a/frontend/src/components/skills/SkillsService.js b/dashboard/src/components/skills/SkillsService.js similarity index 94% rename from frontend/src/components/skills/SkillsService.js rename to dashboard/src/components/skills/SkillsService.js index 28764726..f8814c7d 100644 --- a/frontend/src/components/skills/SkillsService.js +++ b/dashboard/src/components/skills/SkillsService.js @@ -97,10 +97,10 @@ export default { }, assignDependency(projectId, skillId, dependentSkillId, dependentProjectId) { if (dependentProjectId) { - return axios.post(`/admin/projects/${projectId}/skills/${skillId}/dependency/projects/${dependentProjectId}/skills/${dependentSkillId}`, null, { headers: { 'x-handleError': false } }) + return axios.post(`/admin/projects/${projectId}/skills/${skillId}/dependency/projects/${dependentProjectId}/skills/${dependentSkillId}`, null, { handleError: false }) .then(createdRuleResult => createdRuleResult.data); } - return axios.post(`/admin/projects/${projectId}/skills/${skillId}/dependency/${dependentSkillId}`, null, { headers: { 'x-handleError': false } }) + return axios.post(`/admin/projects/${projectId}/skills/${skillId}/dependency/${dependentSkillId}`, null, { handleError: false }) .then(createdRuleResult => createdRuleResult.data); }, removeDependency(projectId, skillId, dependentSkillId, dependentProjectId) { @@ -112,7 +112,7 @@ export default { .then(createdRuleResult => createdRuleResult.data); }, skillWithNameExists(projectId, skillName) { - return axios.get(`/admin/projects/${projectId}/skillNameExists?skillName=${encodeURIComponent(skillName)}`) + return axios.post(`/admin/projects/${projectId}/skillNameExists`, { name: skillName }) .then(remoteRes => !remoteRes.data); }, skillWithIdExists(projectId, skillId) { @@ -125,7 +125,7 @@ export default { }, saveSkillEvent(projectId, skillId, user, timestamp) { const userId = user.dn ? user.dn : user.userId; - return axios.put(`/api/projects/${projectId}/skills/${skillId}`, { userId, timestamp }, { headers: { 'x-handleError': false } }) + return axios.put(`/api/projects/${projectId}/skills/${skillId}`, { userId, timestamp }, { handleError: false }) .then(remoteRes => remoteRes.data); }, checkIfSkillBelongsToGlobalBadge(projectId, skillId) { diff --git a/frontend/src/components/skills/SkillsTable.vue b/dashboard/src/components/skills/SkillsTable.vue similarity index 97% rename from frontend/src/components/skills/SkillsTable.vue rename to dashboard/src/components/skills/SkillsTable.vue index 8bf7542d..00e470f4 100644 --- a/frontend/src/components/skills/SkillsTable.vue +++ b/dashboard/src/components/skills/SkillsTable.vue @@ -48,14 +48,14 @@ limitations under the License.
-
+
{{ props.row.created | date }}
- - + + + + diff --git a/dashboard/src/components/utils/MarkdownText.vue b/dashboard/src/components/utils/MarkdownText.vue new file mode 100644 index 00000000..51675981 --- /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 98% rename from frontend/src/components/utils/Navigation.vue rename to dashboard/src/components/utils/Navigation.vue index ccde9f05..39bd078b 100644 --- a/frontend/src/components/utils/Navigation.vue +++ b/dashboard/src/components/utils/Navigation.vue @@ -28,6 +28,7 @@ limitations under the License.
    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. +*/ + + + + + + diff --git a/frontend/src/components/utils/RequestOrderMixin.vue b/dashboard/src/components/utils/RequestOrderMixin.vue similarity index 100% rename from frontend/src/components/utils/RequestOrderMixin.vue rename to dashboard/src/components/utils/RequestOrderMixin.vue 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/UserDnInput.vue b/dashboard/src/components/utils/UserDnInput.vue similarity index 100% rename from frontend/src/components/utils/UserDnInput.vue rename to dashboard/src/components/utils/UserDnInput.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/frontend/src/components/utils/iconPicker/IconManager.vue b/dashboard/src/components/utils/iconPicker/IconManager.vue similarity index 100% rename from frontend/src/components/utils/iconPicker/IconManager.vue rename to dashboard/src/components/utils/iconPicker/IconManager.vue diff --git a/frontend/src/components/utils/iconPicker/IconManagerService.js b/dashboard/src/components/utils/iconPicker/IconManagerService.js similarity index 100% rename from frontend/src/components/utils/iconPicker/IconManagerService.js rename to dashboard/src/components/utils/iconPicker/IconManagerService.js diff --git a/frontend/src/components/utils/iconPicker/IconPicker.vue b/dashboard/src/components/utils/iconPicker/IconPicker.vue similarity index 100% rename from frontend/src/components/utils/iconPicker/IconPicker.vue rename to dashboard/src/components/utils/iconPicker/IconPicker.vue 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 100% rename from frontend/src/components/utils/inputForm/IdInput.vue rename to dashboard/src/components/utils/inputForm/IdInput.vue 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 100% rename from frontend/src/components/utils/upload/FileUpload.vue rename to dashboard/src/components/utils/upload/FileUpload.vue 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 100% rename from frontend/src/filters/DateFilter.js rename to dashboard/src/filters/DateFilter.js diff --git a/frontend/src/filters/NumberFilter.js b/dashboard/src/filters/NumberFilter.js similarity index 100% rename from frontend/src/filters/NumberFilter.js rename to dashboard/src/filters/NumberFilter.js diff --git a/frontend/src/filters/TruncateFilter.js b/dashboard/src/filters/TruncateFilter.js similarity index 100% rename from frontend/src/filters/TruncateFilter.js rename to dashboard/src/filters/TruncateFilter.js 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 71% rename from frontend/src/interceptors/errorHandler.js rename to dashboard/src/interceptors/errorHandler.js index 5b0829ee..0c4d4e4d 100644 --- a/frontend/src/interceptors/errorHandler.js +++ b/dashboard/src/interceptors/errorHandler.js @@ -20,10 +20,7 @@ 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,11 +36,21 @@ 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 diff --git a/frontend/src/main.js b/dashboard/src/main.js similarity index 61% rename from frontend/src/main.js rename to dashboard/src/main.js index a8103b17..dbe45758 100644 --- a/frontend/src/main.js +++ b/dashboard/src/main.js @@ -18,7 +18,7 @@ import Vue from 'vue'; import BootstrapVue from 'bootstrap-vue'; import { ClientTable, ServerTable } from 'vue-tables-2'; -import { SkillsDirective } from '@skills/skills-client-vue'; +import { SkillsConfiguration, SkillsDirective, SkillsReporter } from '@skilltree/skills-client-vue'; import VeeValidate from 'vee-validate'; import VueApexCharts from 'vue-apexcharts'; import Vuex from 'vuex'; @@ -58,6 +58,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 100% rename from frontend/src/store/modules/access.js rename to dashboard/src/store/modules/access.js diff --git a/frontend/src/store/modules/auth.js b/dashboard/src/store/modules/auth.js similarity index 78% rename from frontend/src/store/modules/auth.js rename to dashboard/src/store/modules/auth.js index 2a0797cf..b6efd365 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,22 +65,40 @@ 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); + } }); } }) @@ -94,13 +109,7 @@ const actions = { 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); @@ -113,12 +122,12 @@ const actions = { 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,11 +154,7 @@ 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'); @@ -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 100% rename from frontend/src/store/modules/badges.js rename to dashboard/src/store/modules/badges.js diff --git a/frontend/src/store/modules/config.js b/dashboard/src/store/modules/config.js similarity index 94% rename from frontend/src/store/modules/config.js rename to dashboard/src/store/modules/config.js index 5eb4c176..4f866acf 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 = { 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 100% rename from frontend/src/store/modules/projects.js rename to dashboard/src/store/modules/projects.js diff --git a/frontend/src/store/modules/subjects.js b/dashboard/src/store/modules/subjects.js similarity index 100% rename from frontend/src/store/modules/subjects.js rename to dashboard/src/store/modules/subjects.js diff --git a/frontend/src/store/modules/users.js b/dashboard/src/store/modules/users.js similarity index 100% rename from frontend/src/store/modules/users.js rename to dashboard/src/store/modules/users.js diff --git a/frontend/src/store/store.js b/dashboard/src/store/store.js similarity index 100% rename from frontend/src/store/store.js rename to dashboard/src/store/store.js 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 100% rename from frontend/src/validators/CustomDescriptionValidator.js rename to dashboard/src/validators/CustomDescriptionValidator.js diff --git a/frontend/src/validators/CustomNameValidator.js b/dashboard/src/validators/CustomNameValidator.js similarity index 100% rename from frontend/src/validators/CustomNameValidator.js rename to dashboard/src/validators/CustomNameValidator.js diff --git a/frontend/src/validators/CustomValidatorsService.js b/dashboard/src/validators/CustomValidatorsService.js similarity index 100% rename from frontend/src/validators/CustomValidatorsService.js rename to dashboard/src/validators/CustomValidatorsService.js 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 100% rename from frontend/src/validators/OptionalNumericValidator.js rename to dashboard/src/validators/OptionalNumericValidator.js diff --git a/frontend/src/validators/RegisterValidators.js b/dashboard/src/validators/RegisterValidators.js similarity index 100% rename from frontend/src/validators/RegisterValidators.js rename to dashboard/src/validators/RegisterValidators.js diff --git a/frontend/src/validators/ValidatorFactory.js b/dashboard/src/validators/ValidatorFactory.js similarity index 100% rename from frontend/src/validators/ValidatorFactory.js rename to dashboard/src/validators/ValidatorFactory.js 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..23c97d1e --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,44 @@ +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 +RUN apt-get -y install wget + +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..b2bded7b --- /dev/null +++ b/docker/build-docker-image.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# exit if a command returns non-zero exit code +set -e + +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 + +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 "-------------------" + +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}:${VERSION}_${BUILD_DATE_TAG}" . 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..6afc3da2 100644 --- a/e2e-tests/cypress.json +++ b/e2e-tests/cypress.json @@ -1,4 +1,6 @@ { "projectId": "skillstests1", - "baseUrl": "http://localhost:8080" + "baseUrl": "http://localhost:8080", + "requestTimeout": 10000, + "defaultCommandTimeout": 10000 } 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..b3d58317 100644 --- a/e2e-tests/cypress/integration/badges_spec.js +++ b/e2e-tests/cypress/integration/badges_spec.js @@ -19,26 +19,324 @@ 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.getIdField().should('have.value', expectedId) + cy.wait('@nameExistsCheck'); - cy.clickSave() + cy.getIdField().should('have.value', expectedId); + + cy.clickSave(); cy.wait('@postNewBadge'); cy.contains('ID: Lotsofspecial') }); + 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('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.contains(msg); + cy.clickSave(); + cy.contains(overallFormValidationMsg); + cy.get('#badgeName').type('Tes'); + cy.contains(msg).should('not.exist'); + cy.contains(overallFormValidationMsg).should('not.exist'); + + // 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().type(invalidName); + cy.contains(msg); + cy.clickSave(); + cy.contains(overallFormValidationMsg); + cy.get('#badgeName').type('{backspace}'); + cy.contains(msg).should('not.exist'); + cy.contains(overallFormValidationMsg).should('not.exist'); + + // id too short + msg = 'Badge ID cannot be less than 3 characters'; + cy.getIdField().clear().type("aa"); + cy.contains(msg); + cy.clickSave(); + cy.contains(overallFormValidationMsg); + cy.getIdField().type("a"); + cy.contains(msg).should('not.exist'); + cy.contains(overallFormValidationMsg).should('not.exist'); + + // id too long + msg = 'Badge ID cannot exceed 50 characters'; + const invalidId = Array(51).fill('a').join(''); + cy.getIdField().clear().type(invalidId); + cy.contains(msg); + cy.getIdField().type('{backspace}'); + cy.contains(msg).should('not.exist'); + + // id must not have special chars + msg = 'The Badge ID field may only contain alpha-numeric characters'; + cy.getIdField().clear().type('With$Special'); + cy.contains(msg); + cy.getIdField().clear().type('GoodToGo'); + cy.contains(msg).should('not.exist'); + + 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.contains(msg); + cy.getIdField().type('{backspace}'); + cy.contains(msg).should('not.exist'); + }) + + // badge name must not be already taken + msg = 'The value for Badge Name is already taken'; + cy.get('#badgeName').clear().type('Badge Exist'); + cy.contains(msg); + cy.get('#badgeName').type('1'); + cy.contains(msg).should('not.exist'); + + // badge id must not already exist + msg = 'The value for Badge ID is already taken'; + cy.getIdField().clear().type('badgeExist'); + cy.contains(msg); + cy.getIdField().type('1'); + cy.contains(msg).should('not.exist'); + + // 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.contains(msg); + cy.get('#markdown-editor').type('{backspace}'); + cy.contains(msg).should('not.exist') + + // 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.contains(msg); + cy.gemEndSetDay(2); + cy.contains(msg).should('not.exist'); + + // start date should be before end date + msg = 'Start Date must come before End Date'; + cy.gemStartSetDay(3); + cy.contains(msg) + cy.gemEndSetDay(4); + cy.contains(msg).should('not.exist'); + + // dates should not be in the past + const overallFormValidationMsg = 'Form did NOT pass validation, please fix and try to Save again'; + msg = 'End Date cannot be in the past'; + cy.gemStartPrevMonth(); + cy.gemStartPrevMonth(); + cy.gemStartSetDay(1); + cy.gemEndPrevMonth(); + cy.gemEndPrevMonth(); + cy.gemEndSetDay(2); + cy.contains(msg); + + // should not save if there are validation errors + cy.clickSave(); + cy.contains(overallFormValidationMsg); + + // fix the errors and save + cy.gemStartNextMonth(); + cy.gemStartNextMonth(); + cy.gemEndNextMonth(); + cy.gemEndNextMonth(); + cy.gemStartSetDay(1); + cy.gemEndSetDay(2); + cy.contains(msg).should('not.exist'); + cy.contains(overallFormValidationMsg).should('not.exist'); + + 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..9854d08f 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,15 @@ * 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.1 }, // threshold for each pixel + }; beforeEach(() => { Cypress.env('disabledUILoginProp', true); @@ -39,6 +37,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 +92,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 +112,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 +124,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 +133,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 +149,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..e283019f --- /dev/null +++ b/e2e-tests/cypress/integration/client-display/client-display-markdown_spec.js @@ -0,0 +1,149 @@ +/* + * 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.1 }, // threshold for each pixel + }; + + 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..f5199bd2 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,15 +14,15 @@ * 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]'], - failureThreshold: 0.03, // threshold for entire image + blackout: ['[data-cy=pointHistoryChart]', '#dependent-skills-network'], + failureThreshold: 0.0005, // threshold for entire image failureThresholdType: 'percent', // percent of image or number of pixels - customDiffConfig: { threshold: 0.1 }, // threshold for each pixel + customDiffConfig: { threshold: 0.01 }, // threshold for each pixel // capture: 'viewport', // capture viewport in screenshot }; const sizes = [ @@ -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_spec.js b/e2e-tests/cypress/integration/client-display/client-display_spec.js index 6c5ced61..c8dcf2b5 100644 --- a/e2e-tests/cypress/integration/client-display/client-display_spec.js +++ b/e2e-tests/cypress/integration/client-display/client-display_spec.js @@ -17,14 +17,6 @@ describe('Client Display Tests', () => { const cssAttachedToNavigableCards = 'skills-navigable-item'; - before(() => { - cy.disableUILogin(); - }); - - after(function () { - cy.enableUILogin(); - }); - beforeEach(() => { Cypress.env('disabledUILoginProp', true); cy.request('POST', '/app/projects/proj1', { 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..f35738f6 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.only('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..746fd99c 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'); }); @@ -158,4 +159,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('be', 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..b01f056a --- /dev/null +++ b/e2e-tests/cypress/integration/markdown_spec.js @@ -0,0 +1,170 @@ +/* + * 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.1 }, // threshold for each pixel + }; + + 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..3a8d6af4 --- /dev/null +++ b/e2e-tests/cypress/integration/password_reset_spec.js @@ -0,0 +1,178 @@ +/* + * 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'); + }); + }); + + 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'); + }); + }); + + 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..cc6ae4c9 100644 --- a/e2e-tests/cypress/integration/projects_spec.js +++ b/e2e-tests/cypress/integration/projects_spec.js @@ -19,13 +19,19 @@ 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.clickSave(); @@ -41,7 +47,12 @@ 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("My New test Project") @@ -57,7 +68,12 @@ 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.contains('Enable').click(); @@ -70,13 +86,19 @@ describe('Projects Tests', () => { 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-vv-name="projectName"]').type(providedName); + cy.wait('@projectExists'); cy.getIdField().should('have.value', expectedId) cy.clickSave(); @@ -92,7 +114,12 @@ 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.getIdField().should('have.value', expectedId) @@ -110,7 +137,12 @@ describe('Projects Tests', () => { }); 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'); @@ -128,13 +160,13 @@ describe('Projects Tests', () => { 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'); @@ -145,7 +177,12 @@ describe('Projects Tests', () => { }) 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.contains('Enable').click(); @@ -163,8 +200,13 @@ 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') @@ -198,8 +240,12 @@ 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') @@ -240,8 +286,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 +320,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 +350,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 +381,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,8 +412,12 @@ 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'); 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..3633fe3c --- /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.contains('Inception') + }); +}); 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..22015397 --- /dev/null +++ b/e2e-tests/cypress/integration/register_user_spec.js @@ -0,0 +1,159 @@ +/* + * 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('The Password Confirmation confirmation does not match') + cy.get('#password_confirmation').clear().type("password") + cy.contains('Create Account').should('be.enabled'); + cy.contains('The Password Confirmation 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('The Password Confirmation confirmation does not match') + cy.get('#password').clear().type("password") + cy.contains('Create Account').should('be.enabled'); + cy.contains('The Password Confirmation 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('The Email field 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('The Email field must be a valid email').should('not.exist') + + // 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') + 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('The Email field must be a valid email') + cy.contains('Create Account').should('be.disabled'); + cy.get('#email').clear().type("rob.smith@madeup.org") + cy.contains('The Email field 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('The First Name field is required') + cy.contains('Create Account').should('be.disabled'); + cy.get('#firstName').type('Robert') + cy.contains('Create Account').should('be.enabled'); + cy.contains('The First Name field 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('The Last Name field is required') + cy.contains('Create Account').should('be.disabled'); + cy.get('#lastName').type('Smith') + cy.contains('Create Account').should('be.enabled'); + cy.contains('The Last Name field 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..70470a46 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,275 @@ 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]').type('{selectall}localhost'); + cy.get$('[data-cy=portInput]').type('{selectall}1026'); + 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'); + cy.get$('[data-cy=customHeader').type('{selectall}
    HEADER
    '); + cy.get$('[data-cy=customFooter').type('{selectall}
    FOOTER
    '); + cy.get$('[data-cy=saveSystemSettings]').click(); + cy.wait('@loadConfig'); + cy.get('#customHeader').contains('HEADER'); + cy.get('#customFooter').contains('FOOTER'); + cy.visit('/settings/system'); + cy.wait('@loadSystemSettings'); + cy.get('[data-cy=publicUrl]').should('have.value', 'http://localhost:8082'); + cy.get('[data-cy=resetTokenExpiration]').should('have.value', '2H25M22S'); + cy.get('[data-cy=fromEmail]').should('have.value', 'foo@skilltree'); + cy.get('[data-cy=customHeader').should('have.value','
    HEADER
    '); + cy.get('[data-cy=customFooter').should('have.value','
    FOOTER
    '); + + //confirm that header/footer persist after logging out + cy.logout(); + cy.visit('/'); + cy.wait('@loadConfig'); + cy.get('#customHeader').contains('HEADER'); + cy.get('#customFooter').contains('FOOTER'); + }); + + it('System Settings - script tags not allowed in footer/header', () => { + 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'); + cy.get$('[data-cy=customHeader]').type('{selectall}
    FOOTER
    '); + cy.get('[data-cy=customHeaderError]').should('be.visible'); + cy.get('[data-cy=customHeaderError]').contains(' - - 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..15dc36e9 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-parent pom - 1.1.4-SNAPSHOT + 1.2.3-SNAPSHOT - frontend + call-stack-profiler + dashboard client-display - skills-bootstrap - backend + service @@ -21,9 +21,9 @@ 1.11 - 2.5.9 - 3.0.0-01 - 2.5.1-02 + 2.5.13 + 3.6.0-03 + 2.5.12-02 1.3-groovy-2.5 1.8 @@ -41,14 +41,16 @@ 28.1-jre - 2.1.13.RELEASE + 2.3.3.RELEASE + + 1.5.14 org.springframework.boot spring-boot-starter-parent - 2.1.13.RELEASE + 2.3.3.RELEASE diff --git a/backend/pom.xml b/service/pom.xml similarity index 91% rename from backend/pom.xml rename to service/pom.xml index c116f691..05228704 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.2.3-SNAPSHOT 4.0.0 - backend + skills-service @@ -70,7 +70,7 @@ org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure - 2.1.12.RELEASE + 2.3.3.RELEASE @@ -78,6 +78,10 @@ org.springframework.boot spring-boot-starter-mail + + org.springframework.boot + spring-boot-starter-thymeleaf + org.codehaus.groovy @@ -143,6 +147,14 @@ org.springframework.boot spring-boot-starter-reactor-netty + + org.springframework.session + spring-session-data-redis + + + io.lettuce + lettuce-core + ch.qos.logback @@ -203,9 +215,9 @@ - profile + skill-tree call-stack-profiler - 1.0.2 + ${project.version} org.slf4j @@ -245,6 +257,13 @@ test + + com.icegreen + greenmail + ${greenmail.version} + test + + @@ -305,6 +324,16 @@ + + maven-failsafe-plugin + 2.22.2 + + + **/*DBIT + + + + maven-clean-plugin @@ -321,7 +350,7 @@ maven-resources-plugin - copy Vue.js frontend content + copy Vue.js dashboard content generate-resources copy-resources @@ -331,7 +360,7 @@ true - ${project.parent.basedir}/frontend/dist + ${project.parent.basedir}/dashboard/dist @@ -355,25 +384,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 +445,8 @@ The MIT License The GNU General Public License (GPL) Version 2 with the Classpath Exception Eclipse Public License - Version 1.0 + Eclipse Public License - Version 2.0 + Eclipse Distribution License - Version 1.0 Dual license: Common Development and Distribution License 1.1 (CDDL-1.1) and The GNU General Public License (GPL) Version 2 Common Development and Distribution License 1.1 (CDDL-1.1) + The GNU General Public License (GPL) Version 2 Common Development and Distribution License 1.1 (CDDL-1.1) + The GNU General Public License (GPL) Version 2 with the Classpath Exception @@ -479,6 +491,8 @@ Common Development and Distribution License (CDDL) + The GNU General Public License (GPL) Version 2 with the Classpath Exception|CDDL + GPLv2 with classpath exception The GNU General Public License (GPL) Version 2 with FOSS exception|The GNU General Public License, v2 with FOSS exception Mozilla 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 Distribution License - Version 1.0|EDL 1.0|Eclipse Distribution License v. 1.0|Eclipse Distribution License - v 1.0 @@ -498,6 +512,7 @@ **/*.jks **/*.ftl src/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..9da87a58 --- /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*", + "/performLogin", "/createAccount", + "/createRootAccount", '/grantFirstRoot', + '/userExists/**', "/app/userInfo", + "/app/users/validExistingDashboardUserId/*", "/app/oAuthProviders", + "index.html", "/public/**", + "/skills-websocket/**", "/requestPasswordReset", + "/resetPassword/**", "/performPasswordReset").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/auth/SecurityConfiguration.groovy b/service/src/main/java/skills/auth/SecurityConfiguration.groovy similarity index 85% rename from backend/src/main/java/skills/auth/SecurityConfiguration.groovy rename to service/src/main/java/skills/auth/SecurityConfiguration.groovy index 3085a565..de1fdb4e 100644 --- a/backend/src/main/java/skills/auth/SecurityConfiguration.groovy +++ b/service/src/main/java/skills/auth/SecurityConfiguration.groovy @@ -15,6 +15,7 @@ */ 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 @@ -25,6 +26,7 @@ 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 @@ -41,6 +43,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.auth.util.AccessDeniedExplanation +import skills.auth.util.AccessDeniedExplanationGenerator import skills.storage.model.auth.RoleName import javax.servlet.ServletException @@ -56,6 +60,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 +86,8 @@ class SecurityConfiguration { @Autowired UserDetailsService userDetailsService + AccessDeniedExplanationGenerator explanationGenerator = new AccessDeniedExplanationGenerator() + @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/api/**").cors() @@ -128,6 +137,15 @@ class SecurityConfiguration { 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 99% rename from backend/src/main/java/skills/auth/UserAuthService.groovy rename to service/src/main/java/skills/auth/UserAuthService.groovy index 90f43644..c9043aaa 100644 --- a/backend/src/main/java/skills/auth/UserAuthService.groovy +++ b/service/src/main/java/skills/auth/UserAuthService.groovy @@ -19,6 +19,7 @@ import callStack.profiler.Profile import groovy.util.logging.Slf4j import org.apache.commons.collections.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..94456eb2 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) @@ -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 null + } + + @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 86% rename from backend/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy rename to service/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy index bff45c64..bcc81aec 100644 --- a/backend/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy +++ b/service/src/main/java/skills/auth/form/FormSecurityConfiguration.groovy @@ -15,25 +15,20 @@ */ 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.web.authentication.AuthenticationFailureHandler import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler @@ -113,6 +108,7 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override @Bean(name = 'defaultAuthManager') @Primary + @Lazy AuthenticationManager authenticationManagerBean() throws Exception { // provides the default AuthenticationManager as a Bean return super.authenticationManagerBean() @@ -141,16 +137,6 @@ class FormSecurityConfiguration extends WebSecurityConfigurerAdapter { return new SimpleUrlAuthenticationFailureHandler() } - @Component - static class RestAccessDeniedHandler implements AccessDeniedHandler { - @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) - } - } - @Component static final class RestAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override @@ -167,7 +153,7 @@ 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) } 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 97% 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..57b19d7c 100644 --- a/backend/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy +++ b/service/src/main/java/skills/auth/form/oauth2/OAuth2UserConverterService.groovy @@ -41,6 +41,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}]") } 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 100% rename from backend/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy rename to service/src/main/java/skills/auth/form/oauth2/OAuthUtils.groovy 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 89% rename from backend/src/main/java/skills/controller/ProjectController.groovy rename to service/src/main/java/skills/controller/ProjectController.groovy index 311c3382..69c75bfb 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 @@ -99,22 +101,27 @@ 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") + @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/backend/src/main/java/skills/controller/PublicConfigController.groovy b/service/src/main/java/skills/controller/PublicConfigController.groovy similarity index 55% rename from backend/src/main/java/skills/controller/PublicConfigController.groovy rename to service/src/main/java/skills/controller/PublicConfigController.groovy index ba4e0ab8..eca0e831 100644 --- a/backend/src/main/java/skills/controller/PublicConfigController.groovy +++ b/service/src/main/java/skills/controller/PublicConfigController.groovy @@ -17,6 +17,7 @@ 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.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod @@ -24,7 +25,13 @@ import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.bind.annotation.RestController 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") @@ -38,10 +45,30 @@ class PublicConfigController { @Autowired UIConfigProperties uiConfigProperties + @Autowired + AccessSettingsStorageService accessSettingsStorageService + + @Value('${skills.authorization.authMode:#{T(skills.auth.AuthMode).DEFAULT_AUTH_MODE}}') + AuthMode authMode + + @Autowired + SettingsService settingsService + @RequestMapping(value = "/config", method = RequestMethod.GET, produces = "application/json") @ResponseBody - Map getConfig(){ - return uiConfigProperties.ui + 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 = [ @@ -53,6 +80,8 @@ class PublicConfigController { @ResponseBody def status() { healthChecker.checkRequiredServices() - return statusRes + Map res = new HashMap<>(statusRes) + res['clientLib'] = uiConfigProperties.client + 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 87% rename from backend/src/main/java/skills/controller/RootController.groovy rename to service/src/main/java/skills/controller/RootController.groovy index 014f0f1c..22de1c67 100644 --- a/backend/src/main/java/skills/controller/RootController.groovy +++ b/service/src/main/java/skills/controller/RootController.groovy @@ -30,16 +30,24 @@ 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.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 +73,9 @@ class RootController { @Autowired SettingsService settingsService + @Autowired + SystemSettingsService systemSettingsService + @GetMapping('/rootUsers') @ResponseBody List getRootUsers() { @@ -168,8 +179,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) 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 96% rename from backend/src/main/java/skills/controller/UserSkillsController.java rename to service/src/main/java/skills/controller/UserSkillsController.java index 78b36ae0..886369ad 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 diff --git a/backend/src/main/java/skills/controller/UserTokenController.groovy b/service/src/main/java/skills/controller/UserTokenController.groovy similarity index 100% rename from backend/src/main/java/skills/controller/UserTokenController.groovy rename to service/src/main/java/skills/controller/UserTokenController.groovy 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 handleSkillException(Exception ex, WebRequest webRequest) { Object body + HttpStatus status = HttpStatus.BAD_REQUEST if (ex instanceof SkillException) { body = new DomainSpecificErrBody(userId: ex.userId, projectId: ex.projectId, skillId: ex.skillId, explanation: ex.message, errorCode: ex.errorCode.name()) String msg = "Exception for: projectId=[${ex.projectId}], skillId=${ex.skillId}" @@ -79,10 +82,14 @@ class RestExceptionHandler extends ResponseEntityExceptionHandler { } } + if (NOT_FOUND_CODES.contains(ex.errorCode)) { + status = HttpStatus.NOT_FOUND + } + } else { log.error("Unexpected exception type [${ex?.class?.simpleName}]", ex) } - return new ResponseEntity(body, HttpStatus.BAD_REQUEST) + return new ResponseEntity(body, status) } @ExceptionHandler(DataIntegrityViolationException) diff --git a/backend/src/main/java/skills/controller/exceptions/SkillException.groovy b/service/src/main/java/skills/controller/exceptions/SkillException.groovy similarity index 100% rename from backend/src/main/java/skills/controller/exceptions/SkillException.groovy rename to service/src/main/java/skills/controller/exceptions/SkillException.groovy diff --git a/backend/src/main/java/skills/controller/exceptions/SkillExceptionBuilder.groovy b/service/src/main/java/skills/controller/exceptions/SkillExceptionBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/controller/exceptions/SkillExceptionBuilder.groovy rename to service/src/main/java/skills/controller/exceptions/SkillExceptionBuilder.groovy diff --git a/backend/src/main/java/skills/controller/exceptions/SkillsValidator.groovy b/service/src/main/java/skills/controller/exceptions/SkillsValidator.groovy similarity index 100% rename from backend/src/main/java/skills/controller/exceptions/SkillsValidator.groovy rename to service/src/main/java/skills/controller/exceptions/SkillsValidator.groovy diff --git a/backend/src/main/java/skills/controller/filters/ClientLibVersionFilter.java b/service/src/main/java/skills/controller/filters/ClientLibVersionFilter.java similarity index 100% rename from backend/src/main/java/skills/controller/filters/ClientLibVersionFilter.java rename to service/src/main/java/skills/controller/filters/ClientLibVersionFilter.java diff --git a/backend/src/main/java/skills/controller/filters/VueEntryPointFilter.java b/service/src/main/java/skills/controller/filters/VueEntryPointFilter.java similarity index 100% rename from backend/src/main/java/skills/controller/filters/VueEntryPointFilter.java rename to service/src/main/java/skills/controller/filters/VueEntryPointFilter.java diff --git a/backend/src/main/java/skills/controller/filters/VueEntryPointFilterUtils.groovy b/service/src/main/java/skills/controller/filters/VueEntryPointFilterUtils.groovy similarity index 90% rename from backend/src/main/java/skills/controller/filters/VueEntryPointFilterUtils.groovy rename to service/src/main/java/skills/controller/filters/VueEntryPointFilterUtils.groovy index 31ddf861..0c75084e 100644 --- a/backend/src/main/java/skills/controller/filters/VueEntryPointFilterUtils.groovy +++ b/service/src/main/java/skills/controller/filters/VueEntryPointFilterUtils.groovy @@ -21,7 +21,7 @@ import org.springframework.stereotype.Component @Component class VueEntryPointFilterUtils { private final List backendResources = - Collections.unmodifiableList("/api,/admin,/app,/static,/clientDisplay,/favicon.ico,/skills.ico,/icons,/performLogin,/createAccount,/createRootAccount,/grantFirstRoot,/userExists,/oauth,/login,/logout,/bootstrap,/root,/supervisor,/public,/metrics,/skills-websocket".split(",").toList()) + Collections.unmodifiableList("/api,/admin,/app,/static,/clientDisplay,/favicon.ico,/skills.ico,/icons,/performLogin,/createAccount,/createRootAccount,/grantFirstRoot,/userExists,/oauth,/login,/logout,/root,/supervisor,/public,/skills-websocket,/resetPassword,/performPasswordReset,/metrics/global".split(",").toList()) boolean isFrontendResource(String pathInfo) { return !isBackendResource(pathInfo) diff --git a/backend/src/main/java/skills/controller/request/model/AccessRequest.groovy b/service/src/main/java/skills/controller/request/model/AccessRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/AccessRequest.groovy rename to service/src/main/java/skills/controller/request/model/AccessRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/ActionPatchRequest.groovy b/service/src/main/java/skills/controller/request/model/ActionPatchRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/ActionPatchRequest.groovy rename to service/src/main/java/skills/controller/request/model/ActionPatchRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/AddSkillRequest.groovy b/service/src/main/java/skills/controller/request/model/AddSkillRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/AddSkillRequest.groovy rename to service/src/main/java/skills/controller/request/model/AddSkillRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/BadgeRequest.groovy b/service/src/main/java/skills/controller/request/model/BadgeRequest.groovy similarity index 97% rename from backend/src/main/java/skills/controller/request/model/BadgeRequest.groovy rename to service/src/main/java/skills/controller/request/model/BadgeRequest.groovy index 41053ff0..10c955e2 100644 --- a/backend/src/main/java/skills/controller/request/model/BadgeRequest.groovy +++ b/service/src/main/java/skills/controller/request/model/BadgeRequest.groovy @@ -28,4 +28,6 @@ class BadgeRequest { Date endDate String helpUrl + + String enabled } diff --git a/backend/src/main/java/skills/controller/request/model/DependencyOrigin.groovy b/service/src/main/java/skills/controller/request/model/DependencyOrigin.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/DependencyOrigin.groovy rename to service/src/main/java/skills/controller/request/model/DependencyOrigin.groovy diff --git a/backend/src/main/java/skills/controller/request/model/EditLevelRequest.groovy b/service/src/main/java/skills/controller/request/model/EditLevelRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/EditLevelRequest.groovy rename to service/src/main/java/skills/controller/request/model/EditLevelRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/GlobalSettingsRequest.groovy b/service/src/main/java/skills/controller/request/model/GlobalSettingsRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/GlobalSettingsRequest.groovy rename to service/src/main/java/skills/controller/request/model/GlobalSettingsRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/GraphCheckType.groovy b/service/src/main/java/skills/controller/request/model/GraphCheckType.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/GraphCheckType.groovy rename to service/src/main/java/skills/controller/request/model/GraphCheckType.groovy diff --git a/service/src/main/java/skills/controller/request/model/NameExistsRequest.groovy b/service/src/main/java/skills/controller/request/model/NameExistsRequest.groovy new file mode 100644 index 00000000..a98f9852 --- /dev/null +++ b/service/src/main/java/skills/controller/request/model/NameExistsRequest.groovy @@ -0,0 +1,23 @@ +/** + * 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.request.model + +import groovy.transform.Canonical + +@Canonical +class NameExistsRequest { + String name +} diff --git a/backend/src/main/java/skills/controller/request/model/NextLevelRequest.groovy b/service/src/main/java/skills/controller/request/model/NextLevelRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/NextLevelRequest.groovy rename to service/src/main/java/skills/controller/request/model/NextLevelRequest.groovy diff --git a/service/src/main/java/skills/controller/request/model/ProjectExistsRequest.groovy b/service/src/main/java/skills/controller/request/model/ProjectExistsRequest.groovy new file mode 100644 index 00000000..50e98bcd --- /dev/null +++ b/service/src/main/java/skills/controller/request/model/ProjectExistsRequest.groovy @@ -0,0 +1,24 @@ +/** + * 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.request.model + +import groovy.transform.Canonical + +@Canonical +class ProjectExistsRequest { + String projectId + String name +} diff --git a/backend/src/main/java/skills/controller/request/model/ProjectRequest.groovy b/service/src/main/java/skills/controller/request/model/ProjectRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/ProjectRequest.groovy rename to service/src/main/java/skills/controller/request/model/ProjectRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/ProjectSettingsRequest.groovy b/service/src/main/java/skills/controller/request/model/ProjectSettingsRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/ProjectSettingsRequest.groovy rename to service/src/main/java/skills/controller/request/model/ProjectSettingsRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SettingsRequest.groovy b/service/src/main/java/skills/controller/request/model/SettingsRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SettingsRequest.groovy rename to service/src/main/java/skills/controller/request/model/SettingsRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SkillDefForDependencyRes.groovy b/service/src/main/java/skills/controller/request/model/SkillDefForDependencyRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SkillDefForDependencyRes.groovy rename to service/src/main/java/skills/controller/request/model/SkillDefForDependencyRes.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SkillEventRequest.groovy b/service/src/main/java/skills/controller/request/model/SkillEventRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SkillEventRequest.groovy rename to service/src/main/java/skills/controller/request/model/SkillEventRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SkillRequest.groovy b/service/src/main/java/skills/controller/request/model/SkillRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SkillRequest.groovy rename to service/src/main/java/skills/controller/request/model/SkillRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SkillsClientVersionRequest.groovy b/service/src/main/java/skills/controller/request/model/SkillsClientVersionRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SkillsClientVersionRequest.groovy rename to service/src/main/java/skills/controller/request/model/SkillsClientVersionRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SkillsFilterType.groovy b/service/src/main/java/skills/controller/request/model/SkillsFilterType.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SkillsFilterType.groovy rename to service/src/main/java/skills/controller/request/model/SkillsFilterType.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SubjectRequest.groovy b/service/src/main/java/skills/controller/request/model/SubjectRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SubjectRequest.groovy rename to service/src/main/java/skills/controller/request/model/SubjectRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/SuggestRequest.groovy b/service/src/main/java/skills/controller/request/model/SuggestRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/SuggestRequest.groovy rename to service/src/main/java/skills/controller/request/model/SuggestRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/UserProjectSettingsRequest.groovy b/service/src/main/java/skills/controller/request/model/UserProjectSettingsRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/UserProjectSettingsRequest.groovy rename to service/src/main/java/skills/controller/request/model/UserProjectSettingsRequest.groovy diff --git a/backend/src/main/java/skills/controller/request/model/UserSettingsRequest.groovy b/service/src/main/java/skills/controller/request/model/UserSettingsRequest.groovy similarity index 100% rename from backend/src/main/java/skills/controller/request/model/UserSettingsRequest.groovy rename to service/src/main/java/skills/controller/request/model/UserSettingsRequest.groovy diff --git a/backend/src/main/java/skills/controller/result/model/BadgeResult.groovy b/service/src/main/java/skills/controller/result/model/BadgeResult.groovy similarity index 98% rename from backend/src/main/java/skills/controller/result/model/BadgeResult.groovy rename to service/src/main/java/skills/controller/result/model/BadgeResult.groovy index b6a24967..2d1da1c7 100644 --- a/backend/src/main/java/skills/controller/result/model/BadgeResult.groovy +++ b/service/src/main/java/skills/controller/result/model/BadgeResult.groovy @@ -45,4 +45,6 @@ class BadgeResult { List requiredSkills = [] String helpUrl + + String enabled } diff --git a/backend/src/main/java/skills/controller/result/model/CountItem.java b/service/src/main/java/skills/controller/result/model/CountItem.java similarity index 100% rename from backend/src/main/java/skills/controller/result/model/CountItem.java rename to service/src/main/java/skills/controller/result/model/CountItem.java diff --git a/backend/src/main/java/skills/controller/result/model/CustomIconResult.groovy b/service/src/main/java/skills/controller/result/model/CustomIconResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/CustomIconResult.groovy rename to service/src/main/java/skills/controller/result/model/CustomIconResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/DependencyCheckResult.groovy b/service/src/main/java/skills/controller/result/model/DependencyCheckResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/DependencyCheckResult.groovy rename to service/src/main/java/skills/controller/result/model/DependencyCheckResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/GlobalBadgeLevelRes.groovy b/service/src/main/java/skills/controller/result/model/GlobalBadgeLevelRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/GlobalBadgeLevelRes.groovy rename to service/src/main/java/skills/controller/result/model/GlobalBadgeLevelRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/GlobalBadgeResult.groovy b/service/src/main/java/skills/controller/result/model/GlobalBadgeResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/GlobalBadgeResult.groovy rename to service/src/main/java/skills/controller/result/model/GlobalBadgeResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/LabelCountItem.groovy b/service/src/main/java/skills/controller/result/model/LabelCountItem.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/LabelCountItem.groovy rename to service/src/main/java/skills/controller/result/model/LabelCountItem.groovy diff --git a/backend/src/main/java/skills/controller/result/model/LevelDefinitionRes.groovy b/service/src/main/java/skills/controller/result/model/LevelDefinitionRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/LevelDefinitionRes.groovy rename to service/src/main/java/skills/controller/result/model/LevelDefinitionRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/NumUsersRes.groovy b/service/src/main/java/skills/controller/result/model/NumUsersRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/NumUsersRes.groovy rename to service/src/main/java/skills/controller/result/model/NumUsersRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/OAuth2Provider.groovy b/service/src/main/java/skills/controller/result/model/OAuth2Provider.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/OAuth2Provider.groovy rename to service/src/main/java/skills/controller/result/model/OAuth2Provider.groovy diff --git a/backend/src/main/java/skills/controller/result/model/ProjectResult.groovy b/service/src/main/java/skills/controller/result/model/ProjectResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/ProjectResult.groovy rename to service/src/main/java/skills/controller/result/model/ProjectResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/ProjectUser.groovy b/service/src/main/java/skills/controller/result/model/ProjectUser.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/ProjectUser.groovy rename to service/src/main/java/skills/controller/result/model/ProjectUser.groovy diff --git a/backend/src/main/java/skills/controller/result/model/RequestResult.groovy b/service/src/main/java/skills/controller/result/model/RequestResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/RequestResult.groovy rename to service/src/main/java/skills/controller/result/model/RequestResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SettingsResult.groovy b/service/src/main/java/skills/controller/result/model/SettingsResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SettingsResult.groovy rename to service/src/main/java/skills/controller/result/model/SettingsResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SharedSkillResult.groovy b/service/src/main/java/skills/controller/result/model/SharedSkillResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SharedSkillResult.groovy rename to service/src/main/java/skills/controller/result/model/SharedSkillResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SimpleProjectResult.groovy b/service/src/main/java/skills/controller/result/model/SimpleProjectResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SimpleProjectResult.groovy rename to service/src/main/java/skills/controller/result/model/SimpleProjectResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SkillDefGraphRes.groovy b/service/src/main/java/skills/controller/result/model/SkillDefGraphRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SkillDefGraphRes.groovy rename to service/src/main/java/skills/controller/result/model/SkillDefGraphRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SkillDefPartialRes.groovy b/service/src/main/java/skills/controller/result/model/SkillDefPartialRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SkillDefPartialRes.groovy rename to service/src/main/java/skills/controller/result/model/SkillDefPartialRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SkillDefRes.groovy b/service/src/main/java/skills/controller/result/model/SkillDefRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SkillDefRes.groovy rename to service/src/main/java/skills/controller/result/model/SkillDefRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SkillDefSkinnyRes.groovy b/service/src/main/java/skills/controller/result/model/SkillDefSkinnyRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SkillDefSkinnyRes.groovy rename to service/src/main/java/skills/controller/result/model/SkillDefSkinnyRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SkillsGraphRes.groovy b/service/src/main/java/skills/controller/result/model/SkillsGraphRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SkillsGraphRes.groovy rename to service/src/main/java/skills/controller/result/model/SkillsGraphRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/SubjectResult.groovy b/service/src/main/java/skills/controller/result/model/SubjectResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/SubjectResult.groovy rename to service/src/main/java/skills/controller/result/model/SubjectResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/TableResult.groovy b/service/src/main/java/skills/controller/result/model/TableResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/TableResult.groovy rename to service/src/main/java/skills/controller/result/model/TableResult.groovy diff --git a/backend/src/main/java/skills/controller/result/model/TimestampCountItem.groovy b/service/src/main/java/skills/controller/result/model/TimestampCountItem.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/TimestampCountItem.groovy rename to service/src/main/java/skills/controller/result/model/TimestampCountItem.groovy diff --git a/backend/src/main/java/skills/controller/result/model/UserInfoRes.groovy b/service/src/main/java/skills/controller/result/model/UserInfoRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/UserInfoRes.groovy rename to service/src/main/java/skills/controller/result/model/UserInfoRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/UserRoleRes.groovy b/service/src/main/java/skills/controller/result/model/UserRoleRes.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/UserRoleRes.groovy rename to service/src/main/java/skills/controller/result/model/UserRoleRes.groovy diff --git a/backend/src/main/java/skills/controller/result/model/UserSkillsStats.groovy b/service/src/main/java/skills/controller/result/model/UserSkillsStats.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/UserSkillsStats.groovy rename to service/src/main/java/skills/controller/result/model/UserSkillsStats.groovy diff --git a/backend/src/main/java/skills/controller/result/model/ValidationResult.groovy b/service/src/main/java/skills/controller/result/model/ValidationResult.groovy similarity index 100% rename from backend/src/main/java/skills/controller/result/model/ValidationResult.groovy rename to service/src/main/java/skills/controller/result/model/ValidationResult.groovy diff --git a/backend/src/main/java/skills/icons/CssGenerator.groovy b/service/src/main/java/skills/icons/CssGenerator.groovy similarity index 100% rename from backend/src/main/java/skills/icons/CssGenerator.groovy rename to service/src/main/java/skills/icons/CssGenerator.groovy diff --git a/backend/src/main/java/skills/icons/CustomIconFacade.groovy b/service/src/main/java/skills/icons/CustomIconFacade.groovy similarity index 100% rename from backend/src/main/java/skills/icons/CustomIconFacade.groovy rename to service/src/main/java/skills/icons/CustomIconFacade.groovy diff --git a/backend/src/main/java/skills/icons/IconCssNameUtil.groovy b/service/src/main/java/skills/icons/IconCssNameUtil.groovy similarity index 100% rename from backend/src/main/java/skills/icons/IconCssNameUtil.groovy rename to service/src/main/java/skills/icons/IconCssNameUtil.groovy diff --git a/backend/src/main/java/skills/icons/UploadedIcon.groovy b/service/src/main/java/skills/icons/UploadedIcon.groovy similarity index 100% rename from backend/src/main/java/skills/icons/UploadedIcon.groovy rename to service/src/main/java/skills/icons/UploadedIcon.groovy diff --git a/backend/src/main/java/skills/metrics/ChartParams.groovy b/service/src/main/java/skills/metrics/ChartParams.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/ChartParams.groovy rename to service/src/main/java/skills/metrics/ChartParams.groovy diff --git a/backend/src/main/java/skills/metrics/MetricsService.groovy b/service/src/main/java/skills/metrics/MetricsService.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/MetricsService.groovy rename to service/src/main/java/skills/metrics/MetricsService.groovy diff --git a/backend/src/main/java/skills/metrics/builders/MetricsChartBuilder.java b/service/src/main/java/skills/metrics/builders/MetricsChartBuilder.java similarity index 100% rename from backend/src/main/java/skills/metrics/builders/MetricsChartBuilder.java rename to service/src/main/java/skills/metrics/builders/MetricsChartBuilder.java diff --git a/backend/src/main/java/skills/metrics/builders/badges/AchievedPerMonthChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/badges/AchievedPerMonthChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/badges/AchievedPerMonthChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/badges/AchievedPerMonthChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/badges/DistinctUsersOverTimeChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/badges/DistinctUsersOverTimeChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/badges/DistinctUsersOverTimeChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/badges/DistinctUsersOverTimeChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/global/NumUsersPerProjectBuilder.groovy b/service/src/main/java/skills/metrics/builders/global/NumUsersPerProjectBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/global/NumUsersPerProjectBuilder.groovy rename to service/src/main/java/skills/metrics/builders/global/NumUsersPerProjectBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/global/SkillCountPerProjectBuilder.groovy b/service/src/main/java/skills/metrics/builders/global/SkillCountPerProjectBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/global/SkillCountPerProjectBuilder.groovy rename to service/src/main/java/skills/metrics/builders/global/SkillCountPerProjectBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/AchievedSkillsChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/AchievedSkillsChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/AchievedSkillsChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/AchievedSkillsChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/ContinuedUsageChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/ContinuedUsageChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/ContinuedUsageChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/ContinuedUsageChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/DistinctUsersOverTimeChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/DistinctUsersOverTimeChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/DistinctUsersOverTimeChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/DistinctUsersOverTimeChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/NumUsersPerBadgeChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/NumUsersPerBadgeChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/NumUsersPerBadgeChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/NumUsersPerBadgeChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/NumUsersPerLevelChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/NumUsersPerLevelChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/NumUsersPerLevelChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/NumUsersPerLevelChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/OverlookedSkillsChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/OverlookedSkillsChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/OverlookedSkillsChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/OverlookedSkillsChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/TimeToAchieveChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/TimeToAchieveChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/TimeToAchieveChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/TimeToAchieveChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/projects/TopAchievedChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/projects/TopAchievedChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/projects/TopAchievedChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/projects/TopAchievedChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/skills/DistinctUsersOverTimeChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/skills/DistinctUsersOverTimeChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/skills/DistinctUsersOverTimeChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/skills/DistinctUsersOverTimeChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/subjects/AchievedSkillsChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/subjects/AchievedSkillsChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/subjects/AchievedSkillsChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/subjects/AchievedSkillsChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/subjects/DistinctUsersOverTimeChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/subjects/DistinctUsersOverTimeChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/subjects/DistinctUsersOverTimeChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/subjects/DistinctUsersOverTimeChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/subjects/NumUsersPerLevelChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/subjects/NumUsersPerLevelChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/subjects/NumUsersPerLevelChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/subjects/NumUsersPerLevelChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/builders/users/PointHistoryChartBuilder.groovy b/service/src/main/java/skills/metrics/builders/users/PointHistoryChartBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/builders/users/PointHistoryChartBuilder.groovy rename to service/src/main/java/skills/metrics/builders/users/PointHistoryChartBuilder.groovy diff --git a/backend/src/main/java/skills/metrics/model/ChartOption.java b/service/src/main/java/skills/metrics/model/ChartOption.java similarity index 100% rename from backend/src/main/java/skills/metrics/model/ChartOption.java rename to service/src/main/java/skills/metrics/model/ChartOption.java diff --git a/backend/src/main/java/skills/metrics/model/ChartType.java b/service/src/main/java/skills/metrics/model/ChartType.java similarity index 100% rename from backend/src/main/java/skills/metrics/model/ChartType.java rename to service/src/main/java/skills/metrics/model/ChartType.java diff --git a/backend/src/main/java/skills/metrics/model/MetricsChart.groovy b/service/src/main/java/skills/metrics/model/MetricsChart.groovy similarity index 100% rename from backend/src/main/java/skills/metrics/model/MetricsChart.groovy rename to service/src/main/java/skills/metrics/model/MetricsChart.groovy diff --git a/backend/src/main/java/skills/metrics/model/Section.java b/service/src/main/java/skills/metrics/model/Section.java similarity index 100% rename from backend/src/main/java/skills/metrics/model/Section.java rename to service/src/main/java/skills/metrics/model/Section.java diff --git a/backend/src/main/java/skills/profile/CallStackProfAspect.groovy b/service/src/main/java/skills/profile/CallStackProfAspect.groovy similarity index 100% rename from backend/src/main/java/skills/profile/CallStackProfAspect.groovy rename to service/src/main/java/skills/profile/CallStackProfAspect.groovy diff --git a/backend/src/main/java/skills/profile/EnableCallStackProf.java b/service/src/main/java/skills/profile/EnableCallStackProf.java similarity index 100% rename from backend/src/main/java/skills/profile/EnableCallStackProf.java rename to service/src/main/java/skills/profile/EnableCallStackProf.java diff --git a/backend/src/main/java/skills/services/AccessSettingsStorageService.groovy b/service/src/main/java/skills/services/AccessSettingsStorageService.groovy similarity index 99% rename from backend/src/main/java/skills/services/AccessSettingsStorageService.groovy rename to service/src/main/java/skills/services/AccessSettingsStorageService.groovy index 6bfaf624..5f20c715 100644 --- a/backend/src/main/java/skills/services/AccessSettingsStorageService.groovy +++ b/service/src/main/java/skills/services/AccessSettingsStorageService.groovy @@ -174,7 +174,7 @@ class AccessSettingsStorageService { UserRole existingUserRole = user?.roles?.find {it.projectId == projectId && it.roleName == roleName} assert !existingUserRole, "CREATE FAILED -> user-role with project id [$projectId], userId [$userId] and roleName [$roleName] already exists" } else { - throw new SkillException("User [$userId] does not exist", (String) projectId) + throw new SkillException("User [$userId] does not exist", (String) projectId ?: SkillException.NA, SkillException.NA, ErrorCode.UserNotFound) } UserRole userRole = new UserRole(userId: userId, roleName: roleName, projectId: projectId) diff --git a/backend/src/main/java/skills/services/AdminUsersService.groovy b/service/src/main/java/skills/services/AdminUsersService.groovy similarity index 98% rename from backend/src/main/java/skills/services/AdminUsersService.groovy rename to service/src/main/java/skills/services/AdminUsersService.groovy index f7d80c38..05db6bcb 100644 --- a/backend/src/main/java/skills/services/AdminUsersService.groovy +++ b/service/src/main/java/skills/services/AdminUsersService.groovy @@ -138,7 +138,7 @@ class AdminUsersService { } List getAchievementCountsPerSubject(String projectId, int topNToLoad =5) { - List res = userAchievedRepo.getUsageFacetedViaSubject(projectId, SkillDef.ContainerType.Subject, new PageRequest(0, topNToLoad, Sort.Direction.DESC, "countRes")) + List res = userAchievedRepo.getUsageFacetedViaSubject(projectId, SkillDef.ContainerType.Subject, PageRequest.of(0, topNToLoad, Sort.Direction.DESC, "countRes")) return res.collect { new LabelCountItem(value: it.label, count: it.countRes) @@ -146,7 +146,7 @@ class AdminUsersService { } List getAchievementCountsPerSkill(String projectId, String subjectId, int topNToLoad =5) { - List res = userAchievedRepo.getSubjectUsageFacetedViaSkill(projectId, subjectId, SkillDef.ContainerType.Subject, new PageRequest(0, topNToLoad, Sort.Direction.DESC, "countRes")) + List res = userAchievedRepo.getSubjectUsageFacetedViaSkill(projectId, subjectId, SkillDef.ContainerType.Subject, PageRequest.of(0, topNToLoad, Sort.Direction.DESC, "countRes")) return res.collect { new LabelCountItem(value: it.label, count: it.countRes) diff --git a/service/src/main/java/skills/services/BadgeUtils.groovy b/service/src/main/java/skills/services/BadgeUtils.groovy new file mode 100644 index 00000000..ec253872 --- /dev/null +++ b/service/src/main/java/skills/services/BadgeUtils.groovy @@ -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. + */ +package skills.services + +import skills.storage.model.SkillDef +import skills.storage.model.SkillDefParent +import skills.storage.model.SkillDefWithExtra +import skills.storage.repos.SkillEventsSupportRepo + +class BadgeUtils { + + public static boolean withinActiveTimeframe(SkillEventsSupportRepo.SkillDefMin skillDef) { + boolean withinActiveTimeframe = true; + if (skillDef.startDate && skillDef.endDate) { + Date now = new Date() + withinActiveTimeframe = skillDef.startDate.before(now) && skillDef.endDate.after(now) + } + return withinActiveTimeframe + } + + public static boolean withinActiveTimeframe(SkillDefParent skillDef) { + boolean withinActiveTimeframe = true; + if (skillDef.startDate && skillDef.endDate) { + Date now = new Date() + withinActiveTimeframe = skillDef.startDate.before(now) && skillDef.endDate.after(now) + } + return withinActiveTimeframe + } + + public static boolean afterStartTime(SkillDefWithExtra skillDef) { + boolean withinActiveTimeframe = true; + if (skillDef.startDate) { + Date now = new Date() + withinActiveTimeframe = skillDef.startDate.before(now) + } + return withinActiveTimeframe + } + +} diff --git a/backend/src/main/java/skills/services/CreatedResourceLimitsValidator.groovy b/service/src/main/java/skills/services/CreatedResourceLimitsValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/CreatedResourceLimitsValidator.groovy rename to service/src/main/java/skills/services/CreatedResourceLimitsValidator.groovy diff --git a/backend/src/main/java/skills/services/CustomValidationResult.groovy b/service/src/main/java/skills/services/CustomValidationResult.groovy similarity index 100% rename from backend/src/main/java/skills/services/CustomValidationResult.groovy rename to service/src/main/java/skills/services/CustomValidationResult.groovy diff --git a/backend/src/main/java/skills/services/CustomValidator.groovy b/service/src/main/java/skills/services/CustomValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/CustomValidator.groovy rename to service/src/main/java/skills/services/CustomValidator.groovy diff --git a/backend/src/main/java/skills/services/DependencyValidator.groovy b/service/src/main/java/skills/services/DependencyValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/DependencyValidator.groovy rename to service/src/main/java/skills/services/DependencyValidator.groovy diff --git a/service/src/main/java/skills/services/EmailSendingService.groovy b/service/src/main/java/skills/services/EmailSendingService.groovy new file mode 100644 index 00000000..8bd07af9 --- /dev/null +++ b/service/src/main/java/skills/services/EmailSendingService.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 skills.services + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.mail.javamail.MimeMessageHelper +import org.springframework.stereotype.Component +import org.thymeleaf.context.Context +import org.thymeleaf.spring5.SpringTemplateEngine +import skills.settings.EmailSettingsService + +import javax.mail.internet.MimeMessage + +@Slf4j +@Component +class EmailSendingService { + + private static final String FROM = "no_reply@skilltree" + + @Autowired + EmailSettingsService emailSettings + + @Autowired + SpringTemplateEngine thymeleafTemplateEngine; + + @Autowired + SystemSettingsService systemSettingsService + + public void sendEmail(String subject, String to, String htmlBody) { + + String fromEmail = systemSettingsService.get()?.fromEmail + if (!fromEmail) { + fromEmail = FROM + } + + MimeMessage message = emailSettings.mailSender.createMimeMessage() + MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8") + helper.setSubject(subject) + helper.setTo(to) + helper.setFrom(fromEmail) + helper.setText(htmlBody, true) + log.info("sending email to [${to}]") + emailSettings.mailSender.send(message) + } + + public void sendEmailWithThymeleafTemplate(String subject, String to, String templateFileName, Context thymeleafContext) { + log.info("sending email with thymeleaf template") + String htmlBody = thymeleafTemplateEngine.process(templateFileName, thymeleafContext) + sendEmail(subject, to, htmlBody) + } +} diff --git a/service/src/main/java/skills/services/FeatureService.groovy b/service/src/main/java/skills/services/FeatureService.groovy new file mode 100644 index 00000000..4b3e874a --- /dev/null +++ b/service/src/main/java/skills/services/FeatureService.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 skills.services + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.JavaMailSenderImpl +import org.springframework.stereotype.Component +import org.thymeleaf.util.StringUtils +import skills.controller.result.model.SettingsResult +import skills.services.settings.Settings +import skills.services.settings.SettingsService +import skills.settings.EmailSettingsService + +@Slf4j +@Component +class FeatureService { + @Autowired + SettingsService settingsService + + @Autowired + EmailSettingsService emailSettingsService + + boolean isPasswordResetFeatureEnabled() { + JavaMailSender mailSender = emailSettingsService.getMailSender() + SettingsResult publicUrl = settingsService.getGlobalSetting(Settings.GLOBAL_PUBLIC_URL.settingName) + boolean publicUrlConfigured = true + boolean mailSenderConfigured = true + + if (mailSender == null) { + log.warn("Email Settings are not configured or are invalid, please configure through Dashboard for Password Reset feature to function") + mailSenderConfigured = false; + } else { + if (mailSender instanceof JavaMailSenderImpl){ + try { + mailSender.testConnection() + } catch (Exception e) { + mailSenderConfigured = false + log.warn('Email Settings are invalid, please configure valid settings through the Dashboard for Password Reset feature to function') + } + } + } + + if (StringUtils.isEmpty(publicUrl?.value)) { + log.warn("Public URL setting is not configured, please configure through Dashboard for Password Reset feature to function") + publicUrlConfigured = false + } + + return mailSenderConfigured && publicUrlConfigured + } +} diff --git a/backend/src/main/java/skills/services/GlobalBadgesService.groovy b/service/src/main/java/skills/services/GlobalBadgesService.groovy similarity index 99% rename from backend/src/main/java/skills/services/GlobalBadgesService.groovy rename to service/src/main/java/skills/services/GlobalBadgesService.groovy index e9405b99..0036496e 100644 --- a/backend/src/main/java/skills/services/GlobalBadgesService.groovy +++ b/service/src/main/java/skills/services/GlobalBadgesService.groovy @@ -288,7 +288,8 @@ class GlobalBadgesService { iconClass: skillDef.iconClass, startDate: skillDef.startDate, endDate: skillDef.endDate, - helpUrl: skillDef.helpUrl + helpUrl: skillDef.helpUrl, + enabled: skillDef.enabled ) if (loadRequiredSkills) { diff --git a/backend/src/main/java/skills/services/IconService.groovy b/service/src/main/java/skills/services/IconService.groovy similarity index 100% rename from backend/src/main/java/skills/services/IconService.groovy rename to service/src/main/java/skills/services/IconService.groovy diff --git a/backend/src/main/java/skills/services/IdFormatValidator.groovy b/service/src/main/java/skills/services/IdFormatValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/IdFormatValidator.groovy rename to service/src/main/java/skills/services/IdFormatValidator.groovy diff --git a/backend/src/main/java/skills/services/InceptionProjectService.groovy b/service/src/main/java/skills/services/InceptionProjectService.groovy similarity index 100% rename from backend/src/main/java/skills/services/InceptionProjectService.groovy rename to service/src/main/java/skills/services/InceptionProjectService.groovy diff --git a/backend/src/main/java/skills/services/LevelDefinitionStorageService.groovy b/service/src/main/java/skills/services/LevelDefinitionStorageService.groovy similarity index 100% rename from backend/src/main/java/skills/services/LevelDefinitionStorageService.groovy rename to service/src/main/java/skills/services/LevelDefinitionStorageService.groovy diff --git a/backend/src/main/java/skills/services/LevelUtils.groovy b/service/src/main/java/skills/services/LevelUtils.groovy similarity index 100% rename from backend/src/main/java/skills/services/LevelUtils.groovy rename to service/src/main/java/skills/services/LevelUtils.groovy diff --git a/backend/src/main/java/skills/services/LevelValidator.groovy b/service/src/main/java/skills/services/LevelValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/LevelValidator.groovy rename to service/src/main/java/skills/services/LevelValidator.groovy diff --git a/backend/src/main/java/skills/services/LockingService.groovy b/service/src/main/java/skills/services/LockingService.groovy similarity index 100% rename from backend/src/main/java/skills/services/LockingService.groovy rename to service/src/main/java/skills/services/LockingService.groovy diff --git a/service/src/main/java/skills/services/PasswordResetService.groovy b/service/src/main/java/skills/services/PasswordResetService.groovy new file mode 100644 index 00000000..cd13d498 --- /dev/null +++ b/service/src/main/java/skills/services/PasswordResetService.groovy @@ -0,0 +1,133 @@ +/** + * 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 + +import groovy.util.logging.Slf4j +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.time.DurationFormatUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Conditional +import org.springframework.security.access.method.P +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import org.thymeleaf.context.Context +import org.thymeleaf.spring5.SpringTemplateEngine +import skills.auth.SecurityMode +import skills.controller.exceptions.SkillException +import skills.controller.result.model.SettingsResult +import skills.services.settings.Settings +import skills.services.settings.SettingsService +import skills.settings.EmailSettingsService +import skills.storage.model.UserAttrs +import skills.storage.model.auth.PasswordResetToken +import skills.storage.model.auth.User +import skills.storage.repos.PasswordResetTokenRepo + +import javax.annotation.PostConstruct +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneId + +@Slf4j +@Component +@Conditional(SecurityMode.FormAuth) +class PasswordResetService { + + public static final String DEFAULT_TOKEN_EXPIRATION = "PT2H" + + @Autowired + PasswordResetTokenRepo tokenRepo + + @Autowired + UserAttrsService attrsService + + @Autowired + EmailSendingService emailService + + @Autowired + SettingsService settingsService + + @Transactional(readOnly = true) + PasswordResetToken loadToken(String token) { + return tokenRepo.findByToken(token) + } + + @Transactional() + void createTokenAndNotifyUser(User user) { + PasswordResetToken token = tokenRepo.findByUserId(user.userId) + + SettingsResult expirationSetting = settingsService.getGlobalSetting(Settings.GLOBAL_RESET_TOKEN_EXPIRATION.settingName) + Duration expirationDuration = null + if(expirationSetting) { + expirationDuration = Duration.parse(expirationSetting.value) + } else { + expirationDuration = Duration.parse(DEFAULT_TOKEN_EXPIRATION) + } + + String validFor = DurationFormatUtils.formatDurationWords(expirationDuration.toMillis(), true, true) + LocalDateTime expires = LocalDateTime.now() + expires = expirationDuration.addTo(expires) + + if (!token) { + token = new PasswordResetToken() + token.user = user + } + + token.setToken(UUID.randomUUID().toString()) + token.setExpires(Date.from(expires.atZone(ZoneId.systemDefault()).toInstant())) + tokenRepo.save(token) + UserAttrs attrs = attrsService.findByUserId(user.userId) + + if (StringUtils.isEmpty(attrs.email)) { + throw new SkillException("User [${user.userId}] has no email configured, unable to create reset token") + } + + String email = attrs.email + String name = "${attrs.firstName} ${attrs.lastName}" + + SettingsResult settingsResult = settingsService.getGlobalSetting(Settings.GLOBAL_PUBLIC_URL.settingName) + + if (!settingsResult) { + throw new SkillException("No public URL is configured for the system, unable to send password reset email") + } + + String publicUrl = settingsResult.value + if (!publicUrl.endsWith("/")){ + publicUrl += "/" + } + + String url = "${publicUrl}" + + Context templateContext = new Context() + templateContext.setVariable("recipientName", name) + templateContext.setVariable("senderName", "The team") + templateContext.setVariable("validTime", validFor) + templateContext.setVariable("publicUrl", url) + templateContext.setVariable("resetToken", token.token) + + emailService.sendEmailWithThymeleafTemplate("SkillTree Password Reset", email, "password_reset.html", templateContext) + } + + @Transactional(readOnly = true) + boolean isTokenForUserId(String token, String userId) { + tokenRepo.findByTokenAndUserId(token, userId) != null + } + + @Transactional + void deleteToken(String token) { + tokenRepo.deleteByToken(token) + } +} diff --git a/backend/src/main/java/skills/services/ProjectSortingService.groovy b/service/src/main/java/skills/services/ProjectSortingService.groovy similarity index 100% rename from backend/src/main/java/skills/services/ProjectSortingService.groovy rename to service/src/main/java/skills/services/ProjectSortingService.groovy diff --git a/backend/src/main/java/skills/services/RuleSetDefGraphService.groovy b/service/src/main/java/skills/services/RuleSetDefGraphService.groovy similarity index 100% rename from backend/src/main/java/skills/services/RuleSetDefGraphService.groovy rename to service/src/main/java/skills/services/RuleSetDefGraphService.groovy diff --git a/backend/src/main/java/skills/services/RuleSetDefinitionScoreUpdater.groovy b/service/src/main/java/skills/services/RuleSetDefinitionScoreUpdater.groovy similarity index 100% rename from backend/src/main/java/skills/services/RuleSetDefinitionScoreUpdater.groovy rename to service/src/main/java/skills/services/RuleSetDefinitionScoreUpdater.groovy diff --git a/backend/src/main/java/skills/services/SkillEventAdminService.groovy b/service/src/main/java/skills/services/SkillEventAdminService.groovy similarity index 100% rename from backend/src/main/java/skills/services/SkillEventAdminService.groovy rename to service/src/main/java/skills/services/SkillEventAdminService.groovy diff --git a/service/src/main/java/skills/services/SystemSettingsService.groovy b/service/src/main/java/skills/services/SystemSettingsService.groovy new file mode 100644 index 00000000..a5329cb5 --- /dev/null +++ b/service/src/main/java/skills/services/SystemSettingsService.groovy @@ -0,0 +1,112 @@ +/** + * 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 + +import groovy.util.logging.Slf4j +import org.apache.commons.lang3.StringUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import skills.controller.exceptions.SkillException +import skills.controller.request.model.GlobalSettingsRequest +import skills.controller.result.model.SettingsResult +import skills.services.settings.Settings +import skills.services.settings.SettingsService +import skills.settings.SystemSettings + +import java.time.Duration +import java.time.format.DateTimeParseException +import java.util.regex.Pattern + +@Slf4j +@Component +class SystemSettingsService { + + private static final int MAX_SETTING_VALUE = 3000 + + public static final String CUSTOMIZATION = 'customization' + + private static final Pattern SCRIPT = ~/.*<[^>]*script.*/ + + + @Autowired + SettingsService settingsService + + SystemSettings get(){ + SystemSettings settings = new SystemSettings() + SettingsResult result = settingsService.getGlobalSetting(Settings.GLOBAL_PUBLIC_URL.settingName) + if (result) { + settings.publicUrl = result.value + } + result = settingsService.getGlobalSetting(Settings.GLOBAL_RESET_TOKEN_EXPIRATION.settingName) + if (result) { + settings.resetTokenExpiration = result.value + } + result = settingsService.getGlobalSetting(Settings.GLOBAL_FROM_EMAIL.settingName) + if (result) { + settings.fromEmail = result.value + } + + List headerFooter = settingsService.getGlobalSettingsByGroup(CUSTOMIZATION) + headerFooter?.each { + if (Settings.GLOBAL_CUSTOM_HEADER.settingName == it.setting) { + settings.customHeader = it.value + } else if (Settings.GLOBAL_CUSTOM_FOOTER.settingName == it.setting) { + settings.customFooter = it.value + } + } + + return settings + } + + void save(SystemSettings settings) { + List toSave = [] + toSave << new GlobalSettingsRequest(setting: Settings.GLOBAL_PUBLIC_URL.settingName, value: settings.publicUrl) + + if (settings.resetTokenExpiration) { + try { + Duration.parse(settings.resetTokenExpiration); + } catch (DateTimeParseException dtpe) { + throw new SkillException("${settings.resetTokenExpiration} is not a valid duration"); + } + toSave << new GlobalSettingsRequest(setting: Settings.GLOBAL_RESET_TOKEN_EXPIRATION.settingName, value: settings.resetTokenExpiration) + } + + if (settings.fromEmail) { + toSave << new GlobalSettingsRequest(setting: Settings.GLOBAL_FROM_EMAIL.settingName, value: settings.fromEmail) + } + + if (settings.customHeader ==~ SCRIPT) { + throw new SkillException("Script tags are not allowed in custom header") + } + if (MAX_SETTING_VALUE < settings.customHeader?.length()) { + throw new SkillException("Custom Header may not be longer than [${MAX_SETTING_VALUE}]") + } + if (settings.customFooter ==~ SCRIPT) { + throw new SkillException("Script tags are not allowed in custom footer") + } + if (MAX_SETTING_VALUE < settings.customFooter?.length()) { + throw new SkillException("Custom Footer may not be longer than [${MAX_SETTING_VALUE}]") + } + + settings.customFooter = StringUtils.defaultString(settings.customFooter) + settings.customHeader = StringUtils.defaultString(settings.customHeader) + toSave << new GlobalSettingsRequest(setting: Settings.GLOBAL_CUSTOM_HEADER.settingName, value: settings.customHeader, settingGroup: CUSTOMIZATION) + toSave << new GlobalSettingsRequest(setting: Settings.GLOBAL_CUSTOM_FOOTER.settingName, value: settings.customFooter, settingGroup: CUSTOMIZATION) + + settingsService.saveSettings(toSave) + } + +} diff --git a/backend/src/main/java/skills/services/UserAchievementsAndPointsManagement.groovy b/service/src/main/java/skills/services/UserAchievementsAndPointsManagement.groovy similarity index 85% rename from backend/src/main/java/skills/services/UserAchievementsAndPointsManagement.groovy rename to service/src/main/java/skills/services/UserAchievementsAndPointsManagement.groovy index 07f4628d..eadf8eaa 100644 --- a/backend/src/main/java/skills/services/UserAchievementsAndPointsManagement.groovy +++ b/service/src/main/java/skills/services/UserAchievementsAndPointsManagement.groovy @@ -20,8 +20,13 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import skills.controller.exceptions.SkillsValidator +import skills.controller.result.model.SettingsResult +import skills.services.settings.Settings +import skills.services.settings.SettingsService import skills.storage.model.SkillDef +import skills.storage.model.SkillRelDef import skills.storage.repos.SkillDefRepo +import skills.storage.repos.SkillRelDefRepo import skills.storage.repos.UserAchievedLevelRepo import skills.storage.repos.UserPerformedSkillRepo import skills.storage.repos.UserPointsRepo @@ -49,6 +54,12 @@ class UserAchievementsAndPointsManagement { @Autowired SkillDefRepo skillDefRepo + @Autowired + SkillRelDefRepo skillRelDefRepo + + @Autowired + SettingsService settingsService + @Transactional void handleSkillRemoval(SkillDef skillDef) { SkillDef subject = ruleSetDefGraphService.getParentSkill(skillDef) @@ -110,7 +121,17 @@ class UserAchievementsAndPointsManagement { if (log.isDebugEnabled()){ log.debug("Insert User Achievements. projectId=[${projectId}], skillId=[${skillId}], skillRefId=[${skillRefId}], numOfOccurrences=[$numOfOccurrences]") } - userAchievedLevelRepo.insertUserAchievementWhenDecreaseOfOccurrencesCausesUsersToAchieve(projectId, skillId, skillRefId, numOfOccurrences) + userAchievedLevelRepo.insertUserAchievementWhenDecreaseOfOccurrencesCausesUsersToAchieve(projectId, skillId, skillRefId, numOfOccurrences, Boolean.FALSE.toString()) + + List parent = skillRelDefRepo.findAllByChildIdAndType(skillRefId, SkillRelDef.RelationshipType.RuleSetDefinition) + assert parent.size() == 1 + + SettingsResult settingsResult = settingsService.getProjectSetting(projectId, Settings.LEVEL_AS_POINTS.settingName) + + boolean pointsBased = settingsResult ? settingsResult.isEnabled() : false + + nativeQueriesRepo.identifyAndAddProjectLevelAchievements(projectId, pointsBased) + nativeQueriesRepo.identifyAndAddSubjectLevelAchievements(projectId, parent[0].parent.skillId, pointsBased) } @Transactional diff --git a/backend/src/main/java/skills/services/UserAdminService.groovy b/service/src/main/java/skills/services/UserAdminService.groovy similarity index 99% rename from backend/src/main/java/skills/services/UserAdminService.groovy rename to service/src/main/java/skills/services/UserAdminService.groovy index b0237cc3..d39b04cc 100644 --- a/backend/src/main/java/skills/services/UserAdminService.groovy +++ b/service/src/main/java/skills/services/UserAdminService.groovy @@ -69,7 +69,7 @@ class UserAdminService { List suggestDashboardUsers(String query, boolean includeSelf) { query = query ? query.toLowerCase() : "" - List userAttrs = userAttrsRepo.searchForUser(query, new PageRequest(0, 6)) + List userAttrs = userAttrsRepo.searchForUser(query, PageRequest.of(0, 6)) List results = userAttrs.collect { new UserInfoRes(it) } if (!includeSelf) { diff --git a/backend/src/main/java/skills/services/UserAttrsService.groovy b/service/src/main/java/skills/services/UserAttrsService.groovy similarity index 100% rename from backend/src/main/java/skills/services/UserAttrsService.groovy rename to service/src/main/java/skills/services/UserAttrsService.groovy diff --git a/backend/src/main/java/skills/services/UserInfoValidator.groovy b/service/src/main/java/skills/services/UserInfoValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/UserInfoValidator.groovy rename to service/src/main/java/skills/services/UserInfoValidator.groovy diff --git a/backend/src/main/java/skills/services/admin/BadgeAdminService.groovy b/service/src/main/java/skills/services/admin/BadgeAdminService.groovy similarity index 78% rename from backend/src/main/java/skills/services/admin/BadgeAdminService.groovy rename to service/src/main/java/skills/services/admin/BadgeAdminService.groovy index 793bd973..c72c987e 100644 --- a/backend/src/main/java/skills/services/admin/BadgeAdminService.groovy +++ b/service/src/main/java/skills/services/admin/BadgeAdminService.groovy @@ -17,6 +17,7 @@ package skills.services.admin import callStack.profiler.Profile import groovy.util.logging.Slf4j +import org.apache.commons.lang3.StringUtils import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -26,13 +27,11 @@ import skills.controller.request.model.ActionPatchRequest import skills.controller.request.model.BadgeRequest import skills.controller.result.model.BadgeResult import skills.services.* -import skills.storage.model.ProjDef -import skills.storage.model.SkillDef -import skills.storage.model.SkillDefWithExtra -import skills.storage.model.SkillRelDef import skills.storage.accessors.ProjDefAccessor -import skills.storage.repos.SkillDefRepo -import skills.storage.repos.SkillDefWithExtraRepo +import skills.storage.model.* +import skills.storage.repos.* +import skills.storage.repos.nativeSql.NativeQueriesRepo +import skills.utils.InputSanitizer import skills.utils.Props @Service @@ -66,6 +65,18 @@ class BadgeAdminService { @Autowired SkillsAdminService skillsAdminService + @Autowired + NativeQueriesRepo nativeQueriesRepo + + @Autowired + UserAchievedLevelRepo achievedLevelRepo + + @Autowired + GlobalBadgeLevelDefRepo globalBadgeLevelDefRepo + + @Autowired + SkillRelDefRepo skillRelDefRepo + @Transactional() void saveBadge(String projectId, String originalBadgeId, BadgeRequest badgeRequest, SkillDef.ContainerType type = SkillDef.ContainerType.Badge, boolean performCustomValidation=true) { CustomValidationResult customValidationResult = customValidator.validate(badgeRequest) @@ -96,7 +107,16 @@ class BadgeAdminService { } } + boolean identifyEligibleUsers = false + if (skillDefinition) { + String existingEnabled = skillDefinition.enabled; + if (StringUtils.isNotBlank(existingEnabled) && StringUtils.equals(existingEnabled, Boolean.TRUE.toString()) && StringUtils.equals(badgeRequest.enabled, Boolean.FALSE.toString())){ + throw new SkillException("Once a Badge has been published, the only allowable value for enabled is [${Boolean.TRUE.toString()}]", projectId, null, ErrorCode.BadParam) + } + if (!Boolean.valueOf(skillDefinition.enabled) && Boolean.valueOf(badgeRequest.enabled)) { + identifyEligibleUsers = true + } Props.copy(badgeRequest, skillDefinition) skillDefinition.skillId = badgeRequest.badgeId } else { @@ -119,7 +139,8 @@ class BadgeAdminService { endDate: badgeRequest.endDate, projDef: projDef, displayOrder: displayOrder, - helpUrl: badgeRequest.helpUrl + helpUrl: badgeRequest.helpUrl, + enabled: badgeRequest.enabled ) log.debug("Saving [{}]", skillDefinition) } @@ -130,9 +151,32 @@ class BadgeAdminService { savedSkill = skillDefWithExtraRepo.save(skillDefinition) } + if (identifyEligibleUsers) { + awardBadgeToUsersMeetingRequirements(savedSkill) + } + log.debug("Saved [{}]", savedSkill) } + @Transactional + public void awardBadgeToUsersMeetingRequirements(SkillDefParent badge) { + if(!badge.projectId) { + nativeQueriesRepo.addGlobalBadgeAchievementForEligibleUsers(badge.skillId, + badge.id, + Boolean.FALSE, + countNumberOfRequiredSkills(badge.skillId), + countNumberOfRequiredLevels(badge.skillId), + badge.startDate, + badge.endDate) + } else { + nativeQueriesRepo.addBadgeAchievementForEligibleUsers(badge.projectId, + badge.skillId, + badge.id, + Boolean.FALSE, + badge.startDate, + badge.endDate) + } + } @Transactional void deleteBadge(String projectId, String badgeId, SkillDef.ContainerType type = SkillDef.ContainerType.Badge) { @@ -164,6 +208,9 @@ class BadgeAdminService { @Transactional(readOnly = true) BadgeResult getBadge(String projectId, String badgeId) { SkillDefWithExtra skillDef = skillDefWithExtraRepo.findByProjectIdAndSkillIdIgnoreCaseAndType(projectId, badgeId, SkillDef.ContainerType.Badge) + if (!skillDef) { + throw new SkillException("Badge [${badgeId}] doesn't exist.", projectId, null, ErrorCode.BadgeNotFound) + } return convertToBadge(skillDef, true) } @@ -192,16 +239,17 @@ class BadgeAdminService { @Profile private BadgeResult convertToBadge(SkillDefWithExtra skillDef, boolean loadRequiredSkills = false) { - skills.controller.result.model.BadgeResult res = new skills.controller.result.model.BadgeResult( + BadgeResult res = new BadgeResult( badgeId: skillDef.skillId, projectId: skillDef.projectId, name: skillDef.name, - description: skillDef.description, + description: InputSanitizer.unsanitizeForMarkdown(skillDef.description), displayOrder: skillDef.displayOrder, iconClass: skillDef.iconClass, startDate: skillDef.startDate, endDate: skillDef.endDate, - helpUrl: skillDef.helpUrl + helpUrl: skillDef.helpUrl, + enabled: skillDef.enabled, ) if (loadRequiredSkills) { @@ -235,4 +283,14 @@ class BadgeAdminService { } return badges } + + private Integer countNumberOfRequiredSkills(String badgeId){ + Integer badgeSkillCount = skillRelDefRepo.getGlobalBadgeSkillCount(badgeId) + return badgeSkillCount + } + + private Integer countNumberOfRequiredLevels(String badgeId){ + Integer badgeLevelCount = globalBadgeLevelDefRepo.countByBadgeId(badgeId) + return badgeLevelCount + } } diff --git a/backend/src/main/java/skills/services/admin/DataIntegrityExceptionHandlers.groovy b/service/src/main/java/skills/services/admin/DataIntegrityExceptionHandlers.groovy similarity index 100% rename from backend/src/main/java/skills/services/admin/DataIntegrityExceptionHandlers.groovy rename to service/src/main/java/skills/services/admin/DataIntegrityExceptionHandlers.groovy diff --git a/backend/src/main/java/skills/services/admin/DisplayOrderService.groovy b/service/src/main/java/skills/services/admin/DisplayOrderService.groovy similarity index 100% rename from backend/src/main/java/skills/services/admin/DisplayOrderService.groovy rename to service/src/main/java/skills/services/admin/DisplayOrderService.groovy diff --git a/backend/src/main/java/skills/services/admin/ProjAdminService.groovy b/service/src/main/java/skills/services/admin/ProjAdminService.groovy similarity index 99% rename from backend/src/main/java/skills/services/admin/ProjAdminService.groovy rename to service/src/main/java/skills/services/admin/ProjAdminService.groovy index 13167a50..c99a76bb 100644 --- a/backend/src/main/java/skills/services/admin/ProjAdminService.groovy +++ b/service/src/main/java/skills/services/admin/ProjAdminService.groovy @@ -221,7 +221,7 @@ class ProjAdminService { @Transactional() List searchProjects(String projectId, String nameQuery) { - List projDefs = projDefRepo.queryProjectsByNameQueryAndNotProjectId(nameQuery.toLowerCase(), projectId, new PageRequest(0, 5, Sort.Direction.ASC, "name")) + List projDefs = projDefRepo.queryProjectsByNameQueryAndNotProjectId(nameQuery.toLowerCase(), projectId, PageRequest.of(0, 5, Sort.Direction.ASC, "name")) return projDefs.collect { new SimpleProjectResult(name: it.name, projectId: it.projectId) } diff --git a/backend/src/main/java/skills/services/admin/ShareSkillsService.groovy b/service/src/main/java/skills/services/admin/ShareSkillsService.groovy similarity index 100% rename from backend/src/main/java/skills/services/admin/ShareSkillsService.groovy rename to service/src/main/java/skills/services/admin/ShareSkillsService.groovy diff --git a/backend/src/main/java/skills/services/admin/SkillsAdminService.groovy b/service/src/main/java/skills/services/admin/SkillsAdminService.groovy similarity index 86% rename from backend/src/main/java/skills/services/admin/SkillsAdminService.groovy rename to service/src/main/java/skills/services/admin/SkillsAdminService.groovy index a52fc9b8..28c46254 100644 --- a/backend/src/main/java/skills/services/admin/SkillsAdminService.groovy +++ b/service/src/main/java/skills/services/admin/SkillsAdminService.groovy @@ -47,6 +47,7 @@ import skills.storage.repos.SkillDefRepo import skills.storage.repos.SkillDefWithExtraRepo import skills.storage.repos.SkillRelDefRepo import skills.storage.repos.UserPointsRepo +import skills.utils.InputSanitizer import skills.utils.Props @Service @@ -92,6 +93,9 @@ class SkillsAdminService { @Autowired SkillDefAccessor skillDefAccessor + @Autowired + BadgeAdminService badgeAdminService + @Transactional() void saveSkill(String originalSkillId, SkillRequest skillRequest, boolean performCustomValidation=true) { lockingService.lockProject(skillRequest.projectId) @@ -197,7 +201,16 @@ class SkillsAdminService { // Must update points prior removal of UserPerformedSkill events as the removal relies on the existence of those extra events userPointsManagement.updatePointsWhenOccurrencesAreDecreased(savedSkill.projectId, skillRequest.subjectId, savedSkill.skillId, savedSkill.pointIncrement, newOccurrences) userPointsManagement.removeExtraEntriesOfUserPerformedSkillByUser(savedSkill.projectId, savedSkill.skillId, currentOccurrences + occurrencesDelta) + //identify what badge (or badges) this skill belongs to. + //if any, look for users who qualify for the badge now after this change is persisted See BadgeAdminService.identifyUsersMeetingBadgeRequirements userPointsManagement.insertUserAchievementWhenDecreaseOfOccurrencesCausesUsersToAchieve(savedSkill.projectId, savedSkill.skillId, savedSkill.id, newOccurrences) + + //identify any badges that this skill belongs to and award the badge if any users now qualify for this badge + List badges = findAllBadgesSkillBelongsTo(savedSkill.skillId) + badges?.each { + badgeAdminService.awardBadgeToUsersMeetingRequirements(it) + } + } else if (occurrencesDelta > 0) { userPointsManagement.removeUserAchievementsThatDoNotMeetNewNumberOfOccurrences(savedSkill.projectId, savedSkill.skillId, newOccurrences) } @@ -218,15 +231,45 @@ class SkillsAdminService { SkillDef parentSkill = ruleSetDefGraphService.getParentSkill(skillDefinition) + //we need to check to see if this skill belongs to any badges, if so we need to look for any users who now qualify + //for those badges ruleSetDefinitionScoreUpdater.skillToBeRemoved(skillDefinition) userPointsManagement.handleSkillRemoval(skillDefinition) + //identify any badges that this skill belonged to and award the badge if any users now qualify for this badge + List badges = findAllBadgesSkillBelongsTo(skillDefinition.skillId) + ruleSetDefGraphService.deleteSkillWithItsDescendants(skillDefinition) log.debug("Deleted skill [{}]", skillDefinition.skillId) + badges?.each { + badgeAdminService.awardBadgeToUsersMeetingRequirements(it) + } + + List siblings = ruleSetDefGraphService.getChildrenSkills(parentSkill) + displayOrderService.resetDisplayOrder(siblings) + } + + @Transactional(readOnly = true) + private List findAllBadgesSkillBelongsTo(String skillId, boolean includeGlobal = true) { + List badges = [] + def res = skillRelDefRepo.findAllChildrenByChildSkillIdAndRelationshipTypeAndParentType(skillId, + SkillRelDef.RelationshipType.BadgeRequirement, + SkillDef.ContainerType.Badge) + if (res) { + badges.addAll(res) + } + + if (includeGlobal) { + res = skillRelDefRepo.findAllChildrenByChildSkillIdAndRelationshipTypeAndParentType(skillId, + SkillRelDef.RelationshipType.BadgeRequirement, + SkillDef.ContainerType.GlobalBadge) + if (res) { + badges.addAll(res) + } + } - List ciblings = ruleSetDefGraphService.getChildrenSkills(parentSkill) - displayOrderService.resetDisplayOrder(ciblings) + return badges } @Transactional(readOnly = true) @@ -239,11 +282,17 @@ class SkillsAdminService { return getSkillsByProjectSkillAndType(projectId, subjectId, SkillDef.ContainerType.Subject, SkillRelDef.RelationshipType.RuleSetDefinition) } + def errorCodeMapping = [(SkillDef.ContainerType.Badge) : ErrorCode.BadgeNotFound, + (SkillDef.ContainerType.GlobalBadge) : ErrorCode.BadgeNotFound, + (SkillDef.ContainerType.Subject) : ErrorCode.SubjectNotFound, + (SkillDef.ContainerType.Skill) : ErrorCode.SkillNotFound] + @Transactional(readOnly = true) List getSkillsByProjectSkillAndType(String projectId, String skillId, SkillDef.ContainerType type, SkillRelDef.RelationshipType relationshipType) { SkillDef parent = skillDefRepo.findByProjectIdAndSkillIdIgnoreCaseAndType(projectId, skillId, type) if (!parent) { - throw new SkillException("There is no skill id [${skillId}] doesn't exist.", projectId, null) + ErrorCode code = errorCodeMapping.get(type) + throw new SkillException("${type} [${skillId}] doesn't exist.", projectId, null, code) } List res @@ -265,7 +314,9 @@ class SkillsAdminService { @Transactional(readOnly = true) SkillDefRes getSkill(String projectId, String subjectId, String skillId) { SkillDefWithExtra res = skillDefWithExtraRepo.findByProjectIdAndSkillIdIgnoreCaseAndType(projectId, skillId, SkillDef.ContainerType.Skill) - assert res + if (!res) { + throw new SkillException("Skill [${skillId}] doesn't exist.", projectId, null, ErrorCode.SkillNotFound) + } return convertToSkillDefRes(res) } @@ -293,11 +344,11 @@ class SkillsAdminService { SkillDef switchWith switch (patchRequest.action) { case ActionPatchRequest.ActionType.DisplayOrderDown: - List foundSkills = skillDefRepo.findNextSkillDefs(projectId, parent.skillId, moveMe.displayOrder, SkillRelDef.RelationshipType.RuleSetDefinition, new PageRequest(0, 1)) + List foundSkills = skillDefRepo.findNextSkillDefs(projectId, parent.skillId, moveMe.displayOrder, SkillRelDef.RelationshipType.RuleSetDefinition, PageRequest.of(0, 1)) switchWith = foundSkills ? foundSkills?.first() : null break; case ActionPatchRequest.ActionType.DisplayOrderUp: - List foundSkills = skillDefRepo.findPreviousSkillDefs(projectId, parent.skillId, moveMe.displayOrder, SkillRelDef.RelationshipType.RuleSetDefinition, new PageRequest(0, 1)) + List foundSkills = skillDefRepo.findPreviousSkillDefs(projectId, parent.skillId, moveMe.displayOrder, SkillRelDef.RelationshipType.RuleSetDefinition, PageRequest.of(0, 1)) switchWith = foundSkills ? foundSkills?.first() : null break; default: @@ -331,6 +382,7 @@ class SkillsAdminService { private SkillDefRes convertToSkillDefRes(SkillDefWithExtra skillDef) { SkillDefRes res = new SkillDefRes() Props.copy(skillDef, res) + res.description = InputSanitizer.unsanitizeForMarkdown(res.description) res.numPerformToCompletion = skillDef.totalPoints / res.pointIncrement return res } @@ -387,7 +439,7 @@ class SkillsAdminService { @Transactional(readOnly = true) boolean existsBySkillId(String projectId, String skillId) { - return skillDefRepo.existsByProjectIdAndSkillIdAllIgnoreCase(projectId, skillId) + return skillDefRepo.existsByProjectIdIgnoreCaseAndSkillId(projectId, skillId) } @Profile diff --git a/backend/src/main/java/skills/services/admin/SkillsDepsService.groovy b/service/src/main/java/skills/services/admin/SkillsDepsService.groovy similarity index 100% rename from backend/src/main/java/skills/services/admin/SkillsDepsService.groovy rename to service/src/main/java/skills/services/admin/SkillsDepsService.groovy diff --git a/backend/src/main/java/skills/services/admin/SubjAdminService.groovy b/service/src/main/java/skills/services/admin/SubjAdminService.groovy similarity index 97% rename from backend/src/main/java/skills/services/admin/SubjAdminService.groovy rename to service/src/main/java/skills/services/admin/SubjAdminService.groovy index 7133b71f..63d8f7f1 100644 --- a/backend/src/main/java/skills/services/admin/SubjAdminService.groovy +++ b/service/src/main/java/skills/services/admin/SubjAdminService.groovy @@ -36,6 +36,7 @@ import skills.storage.accessors.ProjDefAccessor import skills.storage.repos.ProjDefRepo import skills.storage.repos.SkillDefRepo import skills.storage.repos.SkillDefWithExtraRepo +import skills.utils.InputSanitizer import skills.utils.Props @Service @@ -170,6 +171,9 @@ class SubjAdminService { @Transactional(readOnly = true) SubjectResult getSubject(String projectId, String subjectId) { SkillDefWithExtra skillDef = skillDefWithExtraRepo.findByProjectIdAndSkillIdIgnoreCaseAndType(projectId, subjectId, SkillDef.ContainerType.Subject) + if (!skillDef) { + throw new SkillException("Subject [${subjectId}] doesn't exist in project [${projectId}]", projectId, null, ErrorCode.SubjectNotFound) + } convertToSubject(skillDef) } @@ -188,7 +192,7 @@ class SubjAdminService { subjectId: skillDef.skillId, projectId: skillDef.projectId, name: skillDef.name, - description: skillDef.description, + description: InputSanitizer.unsanitizeForMarkdown(skillDef.description), displayOrder: skillDef.displayOrder, totalPoints: skillDef.totalPoints, iconClass: skillDef.iconClass, diff --git a/backend/src/main/java/skills/services/events/AchievedBadgeHandler.groovy b/service/src/main/java/skills/services/events/AchievedBadgeHandler.groovy similarity index 86% rename from backend/src/main/java/skills/services/events/AchievedBadgeHandler.groovy rename to service/src/main/java/skills/services/events/AchievedBadgeHandler.groovy index a48b0682..b5af6594 100644 --- a/backend/src/main/java/skills/services/events/AchievedBadgeHandler.groovy +++ b/service/src/main/java/skills/services/events/AchievedBadgeHandler.groovy @@ -20,6 +20,7 @@ import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component +import skills.services.BadgeUtils import skills.storage.model.SkillDef import skills.storage.model.SkillRelDef import skills.storage.model.UserAchievement @@ -46,7 +47,8 @@ class AchievedBadgeHandler { List parents = skillEventsSupportRepo.findParentSkillsByChildIdAndType(currentSkillDef.id, SkillRelDef.RelationshipType.BadgeRequirement) parents.each { SkillEventsSupportRepo.SkillDefMin skillDefMin -> - if (skillDefMin.type == SkillDef.ContainerType.Badge && withinActiveTimeframe(skillDefMin)) { + if (skillDefMin.type == SkillDef.ContainerType.Badge && BadgeUtils.withinActiveTimeframe(skillDefMin) && + (skillDefMin.enabled == null || Boolean.valueOf(skillDefMin.enabled)) ) { SkillEventsSupportRepo.SkillDefMin badge = skillDefMin Long nonAchievedChildren = achievedLevelRepo.countNonAchievedChildren(userId, badge.projectId, badge.skillId, SkillRelDef.RelationshipType.BadgeRequirement) if (nonAchievedChildren == 0) { @@ -61,13 +63,4 @@ class AchievedBadgeHandler { } } - private boolean withinActiveTimeframe(SkillEventsSupportRepo.SkillDefMin skillDef) { - boolean withinActiveTimeframe = true; - if (skillDef.startDate && skillDef.endDate) { - Date now = new Date() - withinActiveTimeframe = skillDef.startDate.before(now) && skillDef.endDate.after(now) - } - return withinActiveTimeframe - } - } diff --git a/backend/src/main/java/skills/services/events/AchievedGlobalBadgeHandler.groovy b/service/src/main/java/skills/services/events/AchievedGlobalBadgeHandler.groovy similarity index 94% rename from backend/src/main/java/skills/services/events/AchievedGlobalBadgeHandler.groovy rename to service/src/main/java/skills/services/events/AchievedGlobalBadgeHandler.groovy index 8bd59103..e7cb2d3d 100644 --- a/backend/src/main/java/skills/services/events/AchievedGlobalBadgeHandler.groovy +++ b/service/src/main/java/skills/services/events/AchievedGlobalBadgeHandler.groovy @@ -62,6 +62,10 @@ class AchievedGlobalBadgeHandler { ?.collectEntries {String key, List val -> [key,val.collect{it.level}.max()]} badgeCheckLoop: for (SkillDefMin globalBadge : globalBadges) { + if(globalBadge.enabled != null && !Boolean.valueOf(globalBadge.enabled)) { + log.debug("global badge [{}] isn't enabled yet, cannot be checked for achievement", globalBadge.skillId) + continue; + } // first check required project levels List requiredLevels = globalBadgesService.getGlobalBadgeLevels(globalBadge.skillId) for (GlobalBadgeLevelRes requiredLevel : requiredLevels) { diff --git a/backend/src/main/java/skills/services/events/CheckDependenciesHelper.groovy b/service/src/main/java/skills/services/events/CheckDependenciesHelper.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/CheckDependenciesHelper.groovy rename to service/src/main/java/skills/services/events/CheckDependenciesHelper.groovy diff --git a/backend/src/main/java/skills/services/events/CompletionItem.groovy b/service/src/main/java/skills/services/events/CompletionItem.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/CompletionItem.groovy rename to service/src/main/java/skills/services/events/CompletionItem.groovy diff --git a/backend/src/main/java/skills/services/events/CompletionTypeUtil.groovy b/service/src/main/java/skills/services/events/CompletionTypeUtil.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/CompletionTypeUtil.groovy rename to service/src/main/java/skills/services/events/CompletionTypeUtil.groovy diff --git a/backend/src/main/java/skills/services/events/RecommendationItem.groovy b/service/src/main/java/skills/services/events/RecommendationItem.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/RecommendationItem.groovy rename to service/src/main/java/skills/services/events/RecommendationItem.groovy diff --git a/backend/src/main/java/skills/services/events/SkillEventPublisher.groovy b/service/src/main/java/skills/services/events/SkillEventPublisher.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/SkillEventPublisher.groovy rename to service/src/main/java/skills/services/events/SkillEventPublisher.groovy diff --git a/backend/src/main/java/skills/services/events/SkillEventResult.groovy b/service/src/main/java/skills/services/events/SkillEventResult.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/SkillEventResult.groovy rename to service/src/main/java/skills/services/events/SkillEventResult.groovy diff --git a/backend/src/main/java/skills/services/events/SkillEventsService.groovy b/service/src/main/java/skills/services/events/SkillEventsService.groovy similarity index 75% rename from backend/src/main/java/skills/services/events/SkillEventsService.groovy rename to service/src/main/java/skills/services/events/SkillEventsService.groovy index 7da3d204..72f21516 100644 --- a/backend/src/main/java/skills/services/events/SkillEventsService.groovy +++ b/service/src/main/java/skills/services/events/SkillEventsService.groovy @@ -19,6 +19,7 @@ import callStack.profiler.Profile import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import skills.controller.exceptions.SkillException @@ -32,6 +33,7 @@ import skills.storage.model.UserPoints import skills.storage.repos.SkillEventsSupportRepo import skills.storage.repos.UserAchievedLevelRepo import skills.storage.repos.UserPerformedSkillRepo +import skills.storage.repos.UserPointsRepo import static skills.services.events.CompletionItem.CompletionItemType @@ -40,6 +42,9 @@ import static skills.services.events.CompletionItem.CompletionItemType @Slf4j class SkillEventsService { + private static final String PENDING_NOTIFICATION_EXPLANATION = "Achieved due to a modification " + + "in the training profile (such as: skill deleted, occurrences modified, badge published, etc..)" + @Autowired UserPerformedSkillRepo performedSkillRepository @@ -70,6 +75,9 @@ class SkillEventsService { @Autowired SkillEventPublisher skillEventPublisher + @Autowired + UserPointsRepo userPointsRepo + @Transactional @Profile SkillEventResult reportSkill(String projectId, String skillId, String userId, Boolean notifyIfNotApplied, Date incomingSkillDate = new Date()) { @@ -80,6 +88,64 @@ class SkillEventsService { return result } + @Transactional + protected void notifyUserOfAchievements(String userId){ + try { + List pendingNotificationAchievements = achievedLevelRepo.findAllByUserIdAndNotifiedOrderByCreatedAsc(userId, Boolean.FALSE.toString()) + + SkillEventResult ser + + pendingNotificationAchievements?.each { + SkillEventsSupportRepo.SkillDefMin skill = skillEventsSupportRepo.findByProjectIdAndSkillId(it.projectId, it.skillId) + + if (!ser) { + ser = new SkillEventResult(projectId: it.projectId, skillId: it.skillId, name: skill.name) + ser.completed = [] + } + + CompletionItem completionItem + if (it.level != null) { + Date day = it.created.clearTime() + UserPoints points = userPointsRepo.findByProjectIdAndUserIdAndSkillIdAndDay(it.projectId, userId, it.skillId, day) + + if (points) { + completionItem = new CompletionItem( + level: it.level, name: skill.name, + id: points.skillId ?: "OVERALL", + type: points.skillId ? CompletionItemType.Subject : CompletionItemType.Overall) + } + } else { + if(SkillDef.ContainerType.Skill == skill.type) { + completionItem = new CompletionItem(type: CompletionItemType.Skill, id: skill.skillId, name: skill.name) + } else { + //why doesn't CompletionTypeUtil support Skill? + completionItem = new CompletionItem(type: CompletionTypeUtil.getCompletionType(skill.type), id: skill.skillId, name: skill.name) + } + } + + if (completionItem) { + ser.completed.add(completionItem) + } + + it.notified = Boolean.TRUE.toString() + } + + if (ser) { + ser.explanation = PENDING_NOTIFICATION_EXPLANATION + skillEventPublisher.publishSkillUpdate(ser, userId) + achievedLevelRepo.saveAll(pendingNotificationAchievements) + } + } catch (Exception e) { + log.error("unable to notify user [${userId}] of pending achievements", e) + throw e + } + } + + @Async + public void identifyPendingNotifications(String userId) { + notifyUserOfAchievements(userId) + } + private SkillEventResult reportSkillInternal(String projectId, String skillId, String userId, Date incomingSkillDate) { assert projectId assert skillId diff --git a/backend/src/main/java/skills/services/events/TimeWindowHelper.groovy b/service/src/main/java/skills/services/events/TimeWindowHelper.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/TimeWindowHelper.groovy rename to service/src/main/java/skills/services/events/TimeWindowHelper.groovy diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/DataToSave.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/DataToSave.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/DataToSave.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/DataToSave.groovy diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/LoadedData.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/LoadedData.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/LoadedData.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/LoadedData.groovy diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/LoadedDataValidator.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/LoadedDataValidator.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/LoadedDataValidator.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/LoadedDataValidator.groovy diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsBuilder.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsBuilder.groovy similarity index 99% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsBuilder.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsBuilder.groovy index 0dc90e71..f88d6de7 100644 --- a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsBuilder.groovy +++ b/service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsBuilder.groovy @@ -48,7 +48,7 @@ class PointsAndAchievementsBuilder { List completionItems = [] @Profile - PointsAndAchievementsResult build() { + PointsAndAchievementsResult build() { dataToSave = new DataToSave(pointIncrement: pointIncrement) // any parent that exist must get points added diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsDataLoader.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsDataLoader.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsDataLoader.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsDataLoader.groovy diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsHandler.groovy diff --git a/backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsSaver.groovy b/service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsSaver.groovy similarity index 100% rename from backend/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsSaver.groovy rename to service/src/main/java/skills/services/events/pointsAndAchievements/PointsAndAchievementsSaver.groovy diff --git a/backend/src/main/java/skills/services/settings/DefaultSettingsToInit.groovy b/service/src/main/java/skills/services/settings/DefaultSettingsToInit.groovy similarity index 100% rename from backend/src/main/java/skills/services/settings/DefaultSettingsToInit.groovy rename to service/src/main/java/skills/services/settings/DefaultSettingsToInit.groovy diff --git a/backend/src/main/java/skills/services/settings/SettingChangedListener.java b/service/src/main/java/skills/services/settings/SettingChangedListener.java similarity index 100% rename from backend/src/main/java/skills/services/settings/SettingChangedListener.java rename to service/src/main/java/skills/services/settings/SettingChangedListener.java diff --git a/backend/src/main/java/skills/services/settings/SettingTypeUtil.groovy b/service/src/main/java/skills/services/settings/SettingTypeUtil.groovy similarity index 100% rename from backend/src/main/java/skills/services/settings/SettingTypeUtil.groovy rename to service/src/main/java/skills/services/settings/SettingTypeUtil.groovy diff --git a/backend/src/main/java/skills/services/settings/Settings.java b/service/src/main/java/skills/services/settings/Settings.java similarity index 75% rename from backend/src/main/java/skills/services/settings/Settings.java rename to service/src/main/java/skills/services/settings/Settings.java index 65c1eec5..3c7be5d1 100644 --- a/backend/src/main/java/skills/services/settings/Settings.java +++ b/service/src/main/java/skills/services/settings/Settings.java @@ -16,7 +16,13 @@ package skills.services.settings; public enum Settings { - LEVEL_AS_POINTS("level.points.enabled"); + LEVEL_AS_POINTS("level.points.enabled"), + GLOBAL_PUBLIC_URL("public_url"), + GLOBAL_RESET_TOKEN_EXPIRATION("password_reset_token_expiration"), + GLOBAL_FROM_EMAIL("from_email"), + GLOBAL_CUSTOM_HEADER("custom_header"), + GLOBAL_CUSTOM_FOOTER("custom_footer"); + private String settingName; diff --git a/backend/src/main/java/skills/services/settings/SettingsDataAccessor.groovy b/service/src/main/java/skills/services/settings/SettingsDataAccessor.groovy similarity index 98% rename from backend/src/main/java/skills/services/settings/SettingsDataAccessor.groovy rename to service/src/main/java/skills/services/settings/SettingsDataAccessor.groovy index 79ce81d2..1ef7e666 100644 --- a/backend/src/main/java/skills/services/settings/SettingsDataAccessor.groovy +++ b/service/src/main/java/skills/services/settings/SettingsDataAccessor.groovy @@ -100,6 +100,10 @@ class SettingsDataAccessor { settingRepo.saveAll(settings) } + void deleteGlobalSetting(String setting) { + settingRepo.deleteGlobalSetting(setting) + } + Setting loadSetting(SettingsRequest request){ if(request instanceof UserProjectSettingsRequest){ return getUserProjectSetting(request.userId, request.projectId, request.setting, request.settingGroup) diff --git a/backend/src/main/java/skills/services/settings/SettingsInitializingBean.groovy b/service/src/main/java/skills/services/settings/SettingsInitializingBean.groovy similarity index 91% rename from backend/src/main/java/skills/services/settings/SettingsInitializingBean.groovy rename to service/src/main/java/skills/services/settings/SettingsInitializingBean.groovy index 7413a9b8..1705f9a9 100644 --- a/backend/src/main/java/skills/services/settings/SettingsInitializingBean.groovy +++ b/service/src/main/java/skills/services/settings/SettingsInitializingBean.groovy @@ -17,6 +17,7 @@ package skills.services.settings import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.event.EventListener import org.springframework.stereotype.Component @@ -34,6 +35,10 @@ import skills.controller.request.model.SettingsRequest */ @Component @Slf4j +@ConditionalOnProperty( + name = "skills.db.startup", + havingValue = "true", + matchIfMissing = true) class SettingsInitializingBean { @Autowired diff --git a/backend/src/main/java/skills/services/settings/SettingsService.groovy b/service/src/main/java/skills/services/settings/SettingsService.groovy similarity index 98% rename from backend/src/main/java/skills/services/settings/SettingsService.groovy rename to service/src/main/java/skills/services/settings/SettingsService.groovy index 1ab5d8aa..cae9df92 100644 --- a/backend/src/main/java/skills/services/settings/SettingsService.groovy +++ b/service/src/main/java/skills/services/settings/SettingsService.groovy @@ -180,6 +180,11 @@ class SettingsService { return convertToResList(settings) } + @Transactional() + void deleteGlobalSetting(String setting) { + settingsDataAccessor.deleteGlobalSetting(setting) + } + /** * Private helper methods */ diff --git a/backend/src/main/java/skills/services/settings/listeners/LevelPointsSettingListener.groovy b/service/src/main/java/skills/services/settings/listeners/LevelPointsSettingListener.groovy similarity index 100% rename from backend/src/main/java/skills/services/settings/listeners/LevelPointsSettingListener.groovy rename to service/src/main/java/skills/services/settings/listeners/LevelPointsSettingListener.groovy diff --git a/backend/src/main/java/skills/services/settings/listeners/ValidationRes.groovy b/service/src/main/java/skills/services/settings/listeners/ValidationRes.groovy similarity index 100% rename from backend/src/main/java/skills/services/settings/listeners/ValidationRes.groovy rename to service/src/main/java/skills/services/settings/listeners/ValidationRes.groovy diff --git a/backend/src/main/java/skills/settings/CommonSettings.groovy b/service/src/main/java/skills/settings/CommonSettings.groovy similarity index 100% rename from backend/src/main/java/skills/settings/CommonSettings.groovy rename to service/src/main/java/skills/settings/CommonSettings.groovy diff --git a/service/src/main/java/skills/settings/EmailConfigurationResult.groovy b/service/src/main/java/skills/settings/EmailConfigurationResult.groovy new file mode 100644 index 00000000..a24f8ec4 --- /dev/null +++ b/service/src/main/java/skills/settings/EmailConfigurationResult.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 skills.settings + +class EmailConfigurationResult { + boolean configurationSuccessful + String explanation +} diff --git a/backend/src/main/java/skills/settings/EmailConnectionInfo.groovy b/service/src/main/java/skills/settings/EmailConnectionInfo.groovy similarity index 100% rename from backend/src/main/java/skills/settings/EmailConnectionInfo.groovy rename to service/src/main/java/skills/settings/EmailConnectionInfo.groovy diff --git a/backend/src/main/java/skills/settings/EmailSettingsService.groovy b/service/src/main/java/skills/settings/EmailSettingsService.groovy similarity index 58% rename from backend/src/main/java/skills/settings/EmailSettingsService.groovy rename to service/src/main/java/skills/settings/EmailSettingsService.groovy index fac0992c..ff96ffe6 100644 --- a/backend/src/main/java/skills/settings/EmailSettingsService.groovy +++ b/service/src/main/java/skills/settings/EmailSettingsService.groovy @@ -18,22 +18,30 @@ package skills.settings import groovy.transform.WithWriteLock import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.JavaMailSenderImpl import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional import skills.controller.exceptions.SkillException import skills.controller.request.model.GlobalSettingsRequest import skills.controller.request.model.SettingsRequest +import skills.controller.result.model.SettingsResult import skills.services.settings.SettingsService import javax.annotation.PostConstruct import javax.mail.MessagingException +import java.util.concurrent.TimeUnit @Service @Slf4j +@ConditionalOnProperty( + name = "skills.db.startup", + havingValue = "true", + matchIfMissing = true) class EmailSettingsService { + private static final String SMTP_CONNECTION_TIMEOUT = Long.toString(TimeUnit.SECONDS.toMillis(10)) + static final String settingsGroup = 'GLOBAL.EMAIL' static final String hostSetting = 'email.host' static final String portSetting = 'email.port' @@ -51,39 +59,51 @@ class EmailSettingsService { @PostConstruct void init() { EmailConnectionInfo emailConnectionInfo = new EmailConnectionInfo( - host: settingsService.getGlobalSetting(hostSetting)?.value, - port: settingsService.getGlobalSetting(portSetting)?.value?.toInteger() ?: -1, - protocol: settingsService.getGlobalSetting(protocolSetting)?.value, - username: settingsService.getGlobalSetting(usernameSetting)?.value, - password: settingsService.getGlobalSetting(passwordSetting)?.value, - authEnabled: settingsService.getGlobalSetting(authSetting)?.value?.toBoolean() ?: false, - tlsEnabled: settingsService.getGlobalSetting(tlsEnableSetting)?.value?.toBoolean() ?: false, + host: settingsService.getGlobalSetting(hostSetting, settingsGroup)?.value, + port: settingsService.getGlobalSetting(portSetting, settingsGroup)?.value?.toInteger() ?: -1, + protocol: settingsService.getGlobalSetting(protocolSetting, settingsGroup)?.value, + username: settingsService.getGlobalSetting(usernameSetting, settingsGroup)?.value, + password: settingsService.getGlobalSetting(passwordSetting, settingsGroup)?.value, + authEnabled: settingsService.getGlobalSetting(authSetting,settingsGroup)?.value?.toBoolean() ?: false, + tlsEnabled: settingsService.getGlobalSetting(tlsEnableSetting, settingsGroup)?.value?.toBoolean() ?: false, ) try { - configureMailSender(emailConnectionInfo) + configureMailSender(emailConnectionInfo, true) } catch (SkillException e) { log.warn('Email connection failed. No email can be sent without updating the configuration', e) } } - void updateConnectionInfo(EmailConnectionInfo emailConnectionInfo) { - configureMailSender(emailConnectionInfo) + EmailConfigurationResult updateConnectionInfo(EmailConnectionInfo emailConnectionInfo) { + EmailConfigurationResult configurationSuccessful = configureMailSender(emailConnectionInfo) storeSettings(emailConnectionInfo) + return configurationSuccessful; } - void configureMailSender(EmailConnectionInfo emailConnectionInfo) { + EmailConfigurationResult configureMailSender(EmailConnectionInfo emailConnectionInfo, boolean isInit = false) { log.info('Configuring the email sender with properties [{}]', emailConnectionInfo) JavaMailSenderImpl tmpMailSender = createJavaMailSender(emailConnectionInfo) - try { tmpMailSender.testConnection() log.info('Refreshing the email sender') updateMailSender(tmpMailSender) + return new EmailConfigurationResult(configurationSuccessful: true) } catch (MessagingException e) { - log.warn('Email connection failed!', e) + if (isInit) { + log.info("Email connection failed [${e.message}] . This is normal on start-up if email connection info was not configured!") + } else { + log.warn('Email connection failed!', e) + } + + String msg = e.message + Exception next = e.nextException + if (next instanceof SocketTimeoutException) { + msg = next.message + } // throw new SkillException('Could not connect with the email settings ' + emailConnectionInfo, e) + return new EmailConfigurationResult(configurationSuccessful: false, explanation: msg) } } @@ -102,6 +122,9 @@ class EmailSettingsService { props.put('mail.smtp.starttls.enable', emailConnectionInfo.tlsEnabled) props.put('mail.debug', 'false') + //connectiontimeout doesn't work if connecting to an open socket that is not an smtp server + props.put("mail.smtp.timeout", SMTP_CONNECTION_TIMEOUT) + return tmpMailSender } @@ -131,16 +154,62 @@ class EmailSettingsService { ]) } + EmailConnectionInfo fetchEmailSettings(){ + List emailSettings = settingsService.getGlobalSettingsByGroup(settingsGroup) + EmailConnectionInfo info = new EmailConnectionInfo() + if (emailSettings) { + def mappedSettings = emailSettings.collectEntries() { + [it.setting, it.value] + } + if(mappedSettings[(hostSetting)]) { + info.host = mappedSettings[(hostSetting)] + } + if(mappedSettings[(portSetting)]) { + info.port = Integer.valueOf(mappedSettings[(portSetting)]) + } + if(mappedSettings[(protocolSetting)]) { + info.protocol = mappedSettings[(protocolSetting)] + } + if(mappedSettings[(usernameSetting)]) { + info.username = mappedSettings[(usernameSetting)] + } + if(mappedSettings[(passwordSetting)]) { + info.password = mappedSettings[(passwordSetting)] + } + if (mappedSettings[(authSetting)]) { + info.authEnabled = Boolean.valueOf(mappedSettings[(authSetting)]) + } + if (mappedSettings[(tlsEnableSetting)]) { + info.tlsEnabled = Boolean.valueOf(mappedSettings[(tlsEnableSetting)]) + } + } + return info + } + private void saveOrUpdateGlobalGroup(String settingsGroup, Map settingsMap) { List settingsRequests = [] + List deleteIfExist = [] settingsMap.each { String setting, String value -> - settingsRequests << new GlobalSettingsRequest(settingGroup: settingsGroup, setting: setting, value: value) + GlobalSettingsRequest gsr = new GlobalSettingsRequest(settingGroup: settingsGroup, setting: setting, value: value) + if (value) { + settingsRequests << gsr + } else { + //if there's not value specified, we need to check if that setting previously had a value and delete it + deleteIfExist << gsr + } } settingsService.saveSettings(settingsRequests) + deleteIfExist.each { + settingsService.deleteGlobalSetting(it.setting) + } } @WithWriteLock void updateMailSender(JavaMailSender mailSender) { this.mailSender = mailSender } + + JavaMailSender getMailSender() { + return this.mailSender; + } } diff --git a/service/src/main/java/skills/settings/SystemSettings.groovy b/service/src/main/java/skills/settings/SystemSettings.groovy new file mode 100644 index 00000000..8e7befed --- /dev/null +++ b/service/src/main/java/skills/settings/SystemSettings.groovy @@ -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 skills.settings + +import groovy.transform.Canonical + + +@Canonical +class SystemSettings { + + String publicUrl + //iso 8601 period/duration string, e.g., PT2H30M45S + String resetTokenExpiration + //from address used in all outgoing emails from the system + String fromEmail + //html and in-line css to display a custom header in the dashboard application + String customHeader + //html and in-line css to display a custom footer in the dashboard application + String customFooter +} diff --git a/backend/src/main/java/skills/shutdown/GracefulShutdown.java b/service/src/main/java/skills/shutdown/GracefulShutdown.java similarity index 100% rename from backend/src/main/java/skills/shutdown/GracefulShutdown.java rename to service/src/main/java/skills/shutdown/GracefulShutdown.java diff --git a/backend/src/main/java/skills/shutdown/GracefulShutdownBeans.java b/service/src/main/java/skills/shutdown/GracefulShutdownBeans.java similarity index 100% rename from backend/src/main/java/skills/shutdown/GracefulShutdownBeans.java rename to service/src/main/java/skills/shutdown/GracefulShutdownBeans.java diff --git a/backend/src/main/java/skills/skillLoading/DependencySummaryLoader.groovy b/service/src/main/java/skills/skillLoading/DependencySummaryLoader.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/DependencySummaryLoader.groovy rename to service/src/main/java/skills/skillLoading/DependencySummaryLoader.groovy diff --git a/backend/src/main/java/skills/skillLoading/PointsHistoryBuilder.groovy b/service/src/main/java/skills/skillLoading/PointsHistoryBuilder.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/PointsHistoryBuilder.groovy rename to service/src/main/java/skills/skillLoading/PointsHistoryBuilder.groovy diff --git a/backend/src/main/java/skills/skillLoading/RankingLoader.groovy b/service/src/main/java/skills/skillLoading/RankingLoader.groovy similarity index 98% rename from backend/src/main/java/skills/skillLoading/RankingLoader.groovy rename to service/src/main/java/skills/skillLoading/RankingLoader.groovy index d0efb9e2..74767bf4 100644 --- a/backend/src/main/java/skills/skillLoading/RankingLoader.groovy +++ b/service/src/main/java/skills/skillLoading/RankingLoader.groovy @@ -134,14 +134,14 @@ class RankingLoader { @CompileStatic @Profile private List findLowestUserPoints(String projectId, int points, String subjectId) { - List previous = userPointsRepository.findByProjectIdAndSkillIdAndPointsLessThanAndDayIsNull(projectId, subjectId, points, new PageRequest(0, 1, Sort.Direction.DESC, "points")) + List previous = userPointsRepository.findByProjectIdAndSkillIdAndPointsLessThanAndDayIsNull(projectId, subjectId, points, PageRequest.of(0, 1, Sort.Direction.DESC, "points")) previous } @CompileStatic @Profile private List findHighestUserPoints(String projectId, int points, String subjectId) { - List next = userPointsRepository.findByProjectIdAndSkillIdAndPointsGreaterThanAndDayIsNull(projectId, subjectId, points, new PageRequest(0, 1, Sort.Direction.ASC, "points")) + List next = userPointsRepository.findByProjectIdAndSkillIdAndPointsGreaterThanAndDayIsNull(projectId, subjectId, points, PageRequest.of(0, 1, Sort.Direction.ASC, "points")) next } diff --git a/backend/src/main/java/skills/skillLoading/SkillsLoader.groovy b/service/src/main/java/skills/skillLoading/SkillsLoader.groovy similarity index 97% rename from backend/src/main/java/skills/skillLoading/SkillsLoader.groovy rename to service/src/main/java/skills/skillLoading/SkillsLoader.groovy index ac9918e5..f601f817 100644 --- a/backend/src/main/java/skills/skillLoading/SkillsLoader.groovy +++ b/service/src/main/java/skills/skillLoading/SkillsLoader.groovy @@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional import skills.controller.exceptions.SkillExceptionBuilder import skills.controller.result.model.GlobalBadgeLevelRes import skills.controller.result.model.SettingsResult +import skills.services.BadgeUtils import skills.services.DependencyValidator import skills.services.GlobalBadgesService import skills.services.LevelDefinitionStorageService @@ -37,6 +38,7 @@ import skills.storage.model.* import skills.storage.repos.* import skills.storage.repos.nativeSql.GraphRelWithAchievement import skills.storage.repos.nativeSql.NativeQueriesRepo +import skills.utils.InputSanitizer @Component @CompileStatic @@ -210,7 +212,9 @@ class SkillsLoader { if ( version >= 0 ) { badgeDefs = badgeDefs.findAll { it.version <= version } } - List badges = badgeDefs.sort({ it.displayOrder }).collect { SkillDefWithExtra badgeDefinition -> + List badges = badgeDefs.sort({ it.displayOrder }).findAll { + (it.enabled == null || Boolean.valueOf(it.enabled)) && BadgeUtils.afterStartTime(it) + }.collect { SkillDefWithExtra badgeDefinition -> loadBadgeSummary(projDef, userId, badgeDefinition, version) } return badges @@ -265,7 +269,7 @@ class SkillsLoader { totalPoints: skillDef.totalPoints, description: new SkillDescription( skillId: skillDef.skillId, - description: skillDef.description, + description: InputSanitizer.unsanitizeForMarkdown(skillDef.description), href: getHelpUrl(helpUrlRootSetting, skillDef.helpUrl)), dependencyInfo: skillDependencySummary, crossProject: crossProjectId != null @@ -309,7 +313,7 @@ class SkillsLoader { List res = dbRes.collect { new SkillDescription( skillId: it.getSkillId(), - description: it.getDescription(), + description: InputSanitizer.unsanitizeForMarkdown(it.getDescription()), href: getHelpUrl(helpUrlRootSetting, it.getHelpUrl()) ) } @@ -389,10 +393,13 @@ class SkillsLoader { helpUrl = getHelpUrl(helpUrlRootSetting, subjectDefinition.helpUrl) } + String description = subjectDefinition instanceof SkillDefWithExtra ? subjectDefinition.description : null + description = InputSanitizer.unsanitizeForMarkdown(description) + return new SkillSubjectSummary( subject: subjectDefinition.name, subjectId: subjectDefinition.skillId, - description: subjectDefinition instanceof SkillDefWithExtra ? subjectDefinition.description : null, + description: description, points: points, skillsLevel: levelInfo.level, @@ -459,7 +466,7 @@ class SkillsLoader { return new SkillBadgeSummary( badge: badgeDefinition.name, badgeId: badgeDefinition.skillId, - description: badgeDefinition.description, + description: InputSanitizer.unsanitizeForMarkdown(badgeDefinition.description), badgeAchieved: achievements?.size() > 0, dateAchieved: achievements ? achievements.first().created : null, numSkillsAchieved: numAchievedSkills, @@ -535,7 +542,7 @@ class SkillsLoader { return new SkillGlobalBadgeSummary( badge: badgeDefinition.name, badgeId: badgeDefinition.skillId, - description: badgeDefinition.description, + description: InputSanitizer.unsanitizeForMarkdown(badgeDefinition.description), badgeAchieved: achievements?.size() > 0, dateAchieved: achievements ? achievements.first().created : null, numSkillsAchieved: numAchievedSkills, @@ -614,6 +621,7 @@ class SkillsLoader { if (lastAchievedLevel.level >= maxLevel) { res.currentPoints = 0 res.nextLevelPoints = -1 + res.level = maxLevel } else { int nextLevelPointsToAchievel diff --git a/backend/src/main/java/skills/skillLoading/SubjectDataLoader.groovy b/service/src/main/java/skills/skillLoading/SubjectDataLoader.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/SubjectDataLoader.groovy rename to service/src/main/java/skills/skillLoading/SubjectDataLoader.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/OverallSkillSummary.groovy b/service/src/main/java/skills/skillLoading/model/OverallSkillSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/OverallSkillSummary.groovy rename to service/src/main/java/skills/skillLoading/model/OverallSkillSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/ProjectLevelSummary.groovy b/service/src/main/java/skills/skillLoading/model/ProjectLevelSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/ProjectLevelSummary.groovy rename to service/src/main/java/skills/skillLoading/model/ProjectLevelSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/ProjectLevelsAndSkillsSummary.groovy b/service/src/main/java/skills/skillLoading/model/ProjectLevelsAndSkillsSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/ProjectLevelsAndSkillsSummary.groovy rename to service/src/main/java/skills/skillLoading/model/ProjectLevelsAndSkillsSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillBadgeSummary.groovy b/service/src/main/java/skills/skillLoading/model/SkillBadgeSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillBadgeSummary.groovy rename to service/src/main/java/skills/skillLoading/model/SkillBadgeSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillDependencyInfo.groovy b/service/src/main/java/skills/skillLoading/model/SkillDependencyInfo.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillDependencyInfo.groovy rename to service/src/main/java/skills/skillLoading/model/SkillDependencyInfo.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillDependencySummary.groovy b/service/src/main/java/skills/skillLoading/model/SkillDependencySummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillDependencySummary.groovy rename to service/src/main/java/skills/skillLoading/model/SkillDependencySummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillDescription.groovy b/service/src/main/java/skills/skillLoading/model/SkillDescription.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillDescription.groovy rename to service/src/main/java/skills/skillLoading/model/SkillDescription.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillGlobalBadgeSummary.groovy b/service/src/main/java/skills/skillLoading/model/SkillGlobalBadgeSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillGlobalBadgeSummary.groovy rename to service/src/main/java/skills/skillLoading/model/SkillGlobalBadgeSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillHistoryPoints.groovy b/service/src/main/java/skills/skillLoading/model/SkillHistoryPoints.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillHistoryPoints.groovy rename to service/src/main/java/skills/skillLoading/model/SkillHistoryPoints.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillPerfomed.groovy b/service/src/main/java/skills/skillLoading/model/SkillPerfomed.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillPerfomed.groovy rename to service/src/main/java/skills/skillLoading/model/SkillPerfomed.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillSubjectSummary.groovy b/service/src/main/java/skills/skillLoading/model/SkillSubjectSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillSubjectSummary.groovy rename to service/src/main/java/skills/skillLoading/model/SkillSubjectSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillSummary.groovy b/service/src/main/java/skills/skillLoading/model/SkillSummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillSummary.groovy rename to service/src/main/java/skills/skillLoading/model/SkillSummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillsLevelDefinition.groovy b/service/src/main/java/skills/skillLoading/model/SkillsLevelDefinition.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillsLevelDefinition.groovy rename to service/src/main/java/skills/skillLoading/model/SkillsLevelDefinition.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillsRanking.groovy b/service/src/main/java/skills/skillLoading/model/SkillsRanking.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillsRanking.groovy rename to service/src/main/java/skills/skillLoading/model/SkillsRanking.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/SkillsRankingDistribution.groovy b/service/src/main/java/skills/skillLoading/model/SkillsRankingDistribution.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/SkillsRankingDistribution.groovy rename to service/src/main/java/skills/skillLoading/model/SkillsRankingDistribution.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/UserPointHistorySummary.groovy b/service/src/main/java/skills/skillLoading/model/UserPointHistorySummary.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/UserPointHistorySummary.groovy rename to service/src/main/java/skills/skillLoading/model/UserPointHistorySummary.groovy diff --git a/backend/src/main/java/skills/skillLoading/model/UsersPerLevel.groovy b/service/src/main/java/skills/skillLoading/model/UsersPerLevel.groovy similarity index 100% rename from backend/src/main/java/skills/skillLoading/model/UsersPerLevel.groovy rename to service/src/main/java/skills/skillLoading/model/UsersPerLevel.groovy diff --git a/backend/src/main/java/skills/storage/InMemoryH2.groovy b/service/src/main/java/skills/storage/InMemoryH2.groovy similarity index 100% rename from backend/src/main/java/skills/storage/InMemoryH2.groovy rename to service/src/main/java/skills/storage/InMemoryH2.groovy diff --git a/backend/src/main/java/skills/storage/JpaConfig.java b/service/src/main/java/skills/storage/JpaConfig.java similarity index 100% rename from backend/src/main/java/skills/storage/JpaConfig.java rename to service/src/main/java/skills/storage/JpaConfig.java diff --git a/backend/src/main/java/skills/storage/accessors/ProjDefAccessor.groovy b/service/src/main/java/skills/storage/accessors/ProjDefAccessor.groovy similarity index 100% rename from backend/src/main/java/skills/storage/accessors/ProjDefAccessor.groovy rename to service/src/main/java/skills/storage/accessors/ProjDefAccessor.groovy diff --git a/backend/src/main/java/skills/storage/accessors/SkillDefAccessor.groovy b/service/src/main/java/skills/storage/accessors/SkillDefAccessor.groovy similarity index 100% rename from backend/src/main/java/skills/storage/accessors/SkillDefAccessor.groovy rename to service/src/main/java/skills/storage/accessors/SkillDefAccessor.groovy diff --git a/backend/src/main/java/skills/storage/model/CustomIcon.groovy b/service/src/main/java/skills/storage/model/CustomIcon.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/CustomIcon.groovy rename to service/src/main/java/skills/storage/model/CustomIcon.groovy diff --git a/backend/src/main/java/skills/storage/model/DayCountItem.java b/service/src/main/java/skills/storage/model/DayCountItem.java similarity index 100% rename from backend/src/main/java/skills/storage/model/DayCountItem.java rename to service/src/main/java/skills/storage/model/DayCountItem.java diff --git a/backend/src/main/java/skills/storage/model/GlobalBadgeLevelDef.groovy b/service/src/main/java/skills/storage/model/GlobalBadgeLevelDef.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/GlobalBadgeLevelDef.groovy rename to service/src/main/java/skills/storage/model/GlobalBadgeLevelDef.groovy diff --git a/backend/src/main/java/skills/storage/model/LevelDef.groovy b/service/src/main/java/skills/storage/model/LevelDef.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/LevelDef.groovy rename to service/src/main/java/skills/storage/model/LevelDef.groovy diff --git a/backend/src/main/java/skills/storage/model/LevelDefInterface.groovy b/service/src/main/java/skills/storage/model/LevelDefInterface.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/LevelDefInterface.groovy rename to service/src/main/java/skills/storage/model/LevelDefInterface.groovy diff --git a/backend/src/main/java/skills/storage/model/ProjDef.groovy b/service/src/main/java/skills/storage/model/ProjDef.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/ProjDef.groovy rename to service/src/main/java/skills/storage/model/ProjDef.groovy diff --git a/backend/src/main/java/skills/storage/model/Setting.groovy b/service/src/main/java/skills/storage/model/Setting.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/Setting.groovy rename to service/src/main/java/skills/storage/model/Setting.groovy diff --git a/backend/src/main/java/skills/storage/model/SkillDef.groovy b/service/src/main/java/skills/storage/model/SkillDef.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/SkillDef.groovy rename to service/src/main/java/skills/storage/model/SkillDef.groovy diff --git a/backend/src/main/java/skills/storage/model/SkillDefParent.groovy b/service/src/main/java/skills/storage/model/SkillDefParent.groovy similarity index 98% rename from backend/src/main/java/skills/storage/model/SkillDefParent.groovy rename to service/src/main/java/skills/storage/model/SkillDefParent.groovy index e74cd5da..1ee9855a 100644 --- a/backend/src/main/java/skills/storage/model/SkillDefParent.groovy +++ b/service/src/main/java/skills/storage/model/SkillDefParent.groovy @@ -71,4 +71,6 @@ class SkillDefParent { @Temporal(TemporalType.TIMESTAMP) @LastModifiedDate Date updated + + String enabled } diff --git a/backend/src/main/java/skills/storage/model/SkillDefWithExtra.groovy b/service/src/main/java/skills/storage/model/SkillDefWithExtra.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/SkillDefWithExtra.groovy rename to service/src/main/java/skills/storage/model/SkillDefWithExtra.groovy diff --git a/backend/src/main/java/skills/storage/model/SkillRelDef.groovy b/service/src/main/java/skills/storage/model/SkillRelDef.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/SkillRelDef.groovy rename to service/src/main/java/skills/storage/model/SkillRelDef.groovy diff --git a/backend/src/main/java/skills/storage/model/SkillShareDef.groovy b/service/src/main/java/skills/storage/model/SkillShareDef.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/SkillShareDef.groovy rename to service/src/main/java/skills/storage/model/SkillShareDef.groovy diff --git a/backend/src/main/java/skills/storage/model/SkillsDBLock.groovy b/service/src/main/java/skills/storage/model/SkillsDBLock.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/SkillsDBLock.groovy rename to service/src/main/java/skills/storage/model/SkillsDBLock.groovy diff --git a/backend/src/main/java/skills/storage/model/UserAchievement.groovy b/service/src/main/java/skills/storage/model/UserAchievement.groovy similarity index 98% rename from backend/src/main/java/skills/storage/model/UserAchievement.groovy rename to service/src/main/java/skills/storage/model/UserAchievement.groovy index fb5e1043..391e0288 100644 --- a/backend/src/main/java/skills/storage/model/UserAchievement.groovy +++ b/service/src/main/java/skills/storage/model/UserAchievement.groovy @@ -66,4 +66,6 @@ class UserAchievement { @Temporal(TemporalType.TIMESTAMP) @LastModifiedDate Date updated + + String notified } diff --git a/backend/src/main/java/skills/storage/model/UserAttrs.groovy b/service/src/main/java/skills/storage/model/UserAttrs.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/UserAttrs.groovy rename to service/src/main/java/skills/storage/model/UserAttrs.groovy diff --git a/backend/src/main/java/skills/storage/model/UserPerformedSkill.groovy b/service/src/main/java/skills/storage/model/UserPerformedSkill.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/UserPerformedSkill.groovy rename to service/src/main/java/skills/storage/model/UserPerformedSkill.groovy diff --git a/backend/src/main/java/skills/storage/model/UserPoints.groovy b/service/src/main/java/skills/storage/model/UserPoints.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/UserPoints.groovy rename to service/src/main/java/skills/storage/model/UserPoints.groovy diff --git a/service/src/main/java/skills/storage/model/auth/PasswordResetToken.groovy b/service/src/main/java/skills/storage/model/auth/PasswordResetToken.groovy new file mode 100644 index 00000000..c2ce1a3b --- /dev/null +++ b/service/src/main/java/skills/storage/model/auth/PasswordResetToken.groovy @@ -0,0 +1,43 @@ +/** + * 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.storage.model.auth + +import groovy.transform.ToString + +import javax.persistence.* + +@ToString +@Table(name="password_reset_token") +@Entity +class PasswordResetToken implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Integer id + + String token + + Date expires + + @OneToOne + User user + + boolean isValid() { + expires?.after(new Date()) + } +} diff --git a/backend/src/main/java/skills/storage/model/auth/RoleName.groovy b/service/src/main/java/skills/storage/model/auth/RoleName.groovy similarity index 100% rename from backend/src/main/java/skills/storage/model/auth/RoleName.groovy rename to service/src/main/java/skills/storage/model/auth/RoleName.groovy diff --git a/backend/src/main/java/skills/storage/model/auth/User.groovy b/service/src/main/java/skills/storage/model/auth/User.groovy similarity index 92% rename from backend/src/main/java/skills/storage/model/auth/User.groovy rename to service/src/main/java/skills/storage/model/auth/User.groovy index d0f88de8..e5029400 100644 --- a/backend/src/main/java/skills/storage/model/auth/User.groovy +++ b/service/src/main/java/skills/storage/model/auth/User.groovy @@ -15,16 +15,16 @@ */ package skills.storage.model.auth - import groovy.transform.ToString -import skills.storage.model.Setting import javax.persistence.* @ToString(excludes = ['roles']) @Entity @Table(name = 'users') -class User { +class User implements Serializable { + + private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/skills/storage/model/auth/UserRole.groovy b/service/src/main/java/skills/storage/model/auth/UserRole.groovy similarity index 91% rename from backend/src/main/java/skills/storage/model/auth/UserRole.groovy rename to service/src/main/java/skills/storage/model/auth/UserRole.groovy index 98439957..a055a2e2 100644 --- a/backend/src/main/java/skills/storage/model/auth/UserRole.groovy +++ b/service/src/main/java/skills/storage/model/auth/UserRole.groovy @@ -24,7 +24,9 @@ import javax.persistence.* @Entity @Table(name = 'user_roles') @Canonical -class UserRole { +class UserRole implements Serializable { + + private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/skills/storage/repos/CustomIconRepo.groovy b/service/src/main/java/skills/storage/repos/CustomIconRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/CustomIconRepo.groovy rename to service/src/main/java/skills/storage/repos/CustomIconRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/GlobalBadgeLevelDefRepo.groovy b/service/src/main/java/skills/storage/repos/GlobalBadgeLevelDefRepo.groovy similarity index 96% rename from backend/src/main/java/skills/storage/repos/GlobalBadgeLevelDefRepo.groovy rename to service/src/main/java/skills/storage/repos/GlobalBadgeLevelDefRepo.groovy index 4ab765f8..23bade65 100644 --- a/backend/src/main/java/skills/storage/repos/GlobalBadgeLevelDefRepo.groovy +++ b/service/src/main/java/skills/storage/repos/GlobalBadgeLevelDefRepo.groovy @@ -27,4 +27,6 @@ interface GlobalBadgeLevelDefRepo extends CrudRepository { + + @Nullable + @Query("select p from PasswordResetToken p where p.token = ?1") + PasswordResetToken findByToken(String token) + + @Nullable + @Query("select p from PasswordResetToken p where p.user.userId = ?1") + PasswordResetToken findByUserId(String userId) + + @Nullable + @Query("select p from PasswordResetToken p where p.token = ?1 and p.user.userId = ?2") + PasswordResetToken findByTokenAndUserId(String token, String userId) + + void deleteByToken(String token) + +} diff --git a/backend/src/main/java/skills/storage/repos/ProjDefRepo.groovy b/service/src/main/java/skills/storage/repos/ProjDefRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/ProjDefRepo.groovy rename to service/src/main/java/skills/storage/repos/ProjDefRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/SettingRepo.groovy b/service/src/main/java/skills/storage/repos/SettingRepo.groovy similarity index 84% rename from backend/src/main/java/skills/storage/repos/SettingRepo.groovy rename to service/src/main/java/skills/storage/repos/SettingRepo.groovy index 83bd398a..1bb47dcf 100644 --- a/backend/src/main/java/skills/storage/repos/SettingRepo.groovy +++ b/service/src/main/java/skills/storage/repos/SettingRepo.groovy @@ -15,7 +15,8 @@ */ package skills.storage.repos - +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.CrudRepository import org.springframework.lang.Nullable import skills.storage.model.Setting @@ -33,4 +34,10 @@ interface SettingRepo extends CrudRepository { @Nullable List findAllByTypeAndSettingGroup(Setting.SettingType type, String settingGroup) + + + @Modifying + @Query("delete from Setting s where s.setting = ?1 AND s.type = 'Global'") + void deleteGlobalSetting(String setting) + } diff --git a/backend/src/main/java/skills/storage/repos/SkillDefRepo.groovy b/service/src/main/java/skills/storage/repos/SkillDefRepo.groovy similarity index 98% rename from backend/src/main/java/skills/storage/repos/SkillDefRepo.groovy rename to service/src/main/java/skills/storage/repos/SkillDefRepo.groovy index ea3f5c6a..c7563278 100644 --- a/backend/src/main/java/skills/storage/repos/SkillDefRepo.groovy +++ b/service/src/main/java/skills/storage/repos/SkillDefRepo.groovy @@ -139,6 +139,7 @@ interface SkillDefRepo extends PagingAndSortingRepository { boolean existsByProjectIdAndSkillIdAndTypeAllIgnoreCase(String id, String skillId, SkillDef.ContainerType type) boolean existsByProjectIdAndSkillIdAllIgnoreCase(@Nullable String id, String skillId) + boolean existsByProjectIdIgnoreCaseAndSkillId(@Nullable String id, String skillId) boolean existsByProjectIdAndNameAndTypeAllIgnoreCase(@Nullable String id, String name, SkillDef.ContainerType type) diff --git a/backend/src/main/java/skills/storage/repos/SkillDefWithExtraRepo.groovy b/service/src/main/java/skills/storage/repos/SkillDefWithExtraRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/SkillDefWithExtraRepo.groovy rename to service/src/main/java/skills/storage/repos/SkillDefWithExtraRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/SkillEventsSupportRepo.groovy b/service/src/main/java/skills/storage/repos/SkillEventsSupportRepo.groovy similarity index 90% rename from backend/src/main/java/skills/storage/repos/SkillEventsSupportRepo.groovy rename to service/src/main/java/skills/storage/repos/SkillEventsSupportRepo.groovy index 69f19dc6..fd0a5584 100644 --- a/backend/src/main/java/skills/storage/repos/SkillEventsSupportRepo.groovy +++ b/service/src/main/java/skills/storage/repos/SkillEventsSupportRepo.groovy @@ -96,6 +96,7 @@ interface SkillEventsSupportRepo extends CrudRepository { SkillDef.ContainerType getType() Date getStartDate() Date getEndDate() + String getEnabled() } @Query('''SELECT @@ -107,11 +108,27 @@ interface SkillEventsSupportRepo extends CrudRepository { s.pointIncrementInterval as pointIncrementInterval, s.numMaxOccurrencesIncrementInterval as numMaxOccurrencesIncrementInterval, s.totalPoints as totalPoints, - s.type as type + s.type as type, + s.enabled as enabled from SkillDef s where s.projectId = ?1 and s.skillId=?2 and s.type = ?3''') @Nullable SkillDefMin findByProjectIdAndSkillIdAndType(String projectId, String skillId, SkillDef.ContainerType type) + @Query('''SELECT + s.id as id, + s.projectId as projectId, + s.skillId as skillId, + s.name as name, + s.pointIncrement as pointIncrement, + s.pointIncrementInterval as pointIncrementInterval, + s.numMaxOccurrencesIncrementInterval as numMaxOccurrencesIncrementInterval, + s.totalPoints as totalPoints, + s.type as type, + s.enabled as enabled + from SkillDef s where s.projectId = ?1 and s.skillId=?2''') + @Nullable + SkillDefMin findByProjectIdAndSkillId(String projectId, String skillId) + @Query('''SELECT badge.id as id, badge.projectId as projectId, @@ -121,7 +138,8 @@ interface SkillEventsSupportRepo extends CrudRepository { badge.pointIncrementInterval as pointIncrementInterval, badge.numMaxOccurrencesIncrementInterval as numMaxOccurrencesIncrementInterval, badge.totalPoints as totalPoints, - badge.type as type + badge.type as type, + badge.enabled as enabled from SkillDef badge where badge.projectId is null and @@ -199,7 +217,8 @@ interface SkillEventsSupportRepo extends CrudRepository { s.totalPoints as totalPoints, s.type as type, s.startDate as startDate, - s.endDate as endDate + s.endDate as endDate, + s.enabled as enabled from SkillDef s, SkillRelDef srd where s.id = srd.parent and diff --git a/backend/src/main/java/skills/storage/repos/SkillRelDefRepo.groovy b/service/src/main/java/skills/storage/repos/SkillRelDefRepo.groovy similarity index 91% rename from backend/src/main/java/skills/storage/repos/SkillRelDefRepo.groovy rename to service/src/main/java/skills/storage/repos/SkillRelDefRepo.groovy index 1029749c..44cc80ad 100644 --- a/backend/src/main/java/skills/storage/repos/SkillRelDefRepo.groovy +++ b/service/src/main/java/skills/storage/repos/SkillRelDefRepo.groovy @@ -32,6 +32,9 @@ interface SkillRelDefRepo extends CrudRepository { @Query(value = '''select count(srd.id) from SkillRelDef srd where srd.child.skillId=?1 and srd.type='BadgeRequirement' and srd.parent.type = 'GlobalBadge' ''') Integer getSkillUsedInGlobalBadgeCount(String skillId) + @Query(value = '''select count(srd.id) from SkillRelDef srd where srd.type='BadgeRequirement' and srd.parent.type = 'GlobalBadge' and srd.parent.skillId=?1''') + Integer getGlobalBadgeSkillCount(String badgeId) + @Query(value = '''select count(srd1.id) from SkillRelDef srd1, SkillRelDef srd2 where srd1.parent.skillId=?1 @@ -94,6 +97,11 @@ interface SkillRelDefRepo extends CrudRepository { @Query(value = '''select srd.parent from SkillRelDef srd where srd.child.skillId=?1 and srd.type='BadgeRequirement' and srd.parent.type = 'GlobalBadge' ''') SkillDef findGlobalBadgeByChildSkillId(String skillId) + @Nullable + @Query(value= '''select srd.parent from SkillRelDef srd where srd.child.skillId=?1 and srd.type=?2 and srd.parent.type=?3 ''') + List findAllChildrenByChildSkillIdAndRelationshipTypeAndParentType(String skillId, SkillRelDef.RelationshipType relType, SkillDef.ContainerType parentType) + + static interface SkillDefSkinny { Integer getId() String getProjectId() diff --git a/backend/src/main/java/skills/storage/repos/SkillShareDefRepo.groovy b/service/src/main/java/skills/storage/repos/SkillShareDefRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/SkillShareDefRepo.groovy rename to service/src/main/java/skills/storage/repos/SkillShareDefRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/SkillsDBLockRepo.groovy b/service/src/main/java/skills/storage/repos/SkillsDBLockRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/SkillsDBLockRepo.groovy rename to service/src/main/java/skills/storage/repos/SkillsDBLockRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/UserAchievedLevelRepo.groovy b/service/src/main/java/skills/storage/repos/UserAchievedLevelRepo.groovy similarity index 93% rename from backend/src/main/java/skills/storage/repos/UserAchievedLevelRepo.groovy rename to service/src/main/java/skills/storage/repos/UserAchievedLevelRepo.groovy index 58188eda..2b0d2e44 100644 --- a/backend/src/main/java/skills/storage/repos/UserAchievedLevelRepo.groovy +++ b/service/src/main/java/skills/storage/repos/UserAchievedLevelRepo.groovy @@ -32,6 +32,8 @@ interface UserAchievedLevelRepo extends CrudRepository List findAllByUserIdAndProjectIdAndSkillId(String userId, @Nullable String projectId, @Nullable String skillId) + List findAllByUserIdAndNotifiedOrderByCreatedAsc(String userId, String notified) + @Query('''select ua from UserAchievement ua where ua.userId = ?1 and ua.projectId in ?2''') List findAllByUserAndProjectIds(String userId, Collection projectId) @@ -171,8 +173,8 @@ interface UserAchievedLevelRepo extends CrudRepository List countAchievementsForProjectPerMonth(@Param('projectId') String projectId, @Param('badgeId') String badgeId, @Param('type') SkillDef.ContainerType containerType, @Param('date') Date mustBeAfterThisDate) - @Query(value = '''INSERT INTO user_achievement(user_id, project_id, skill_id, skill_ref_id, points_when_achieved) - SELECT eventsByUserId.user_id, :projectId, :skillId, :skillRefId, -1 + @Query(value = '''INSERT INTO user_achievement(user_id, project_id, skill_id, skill_ref_id, points_when_achieved, notified) + SELECT eventsByUserId.user_id, :projectId, :skillId, :skillRefId, -1, :notified FROM ( SELECT user_id, count(id) eventCount FROM user_performed_skill @@ -187,5 +189,9 @@ interface UserAchievedLevelRepo extends CrudRepository SELECT id FROM user_achievement WHERE project_id = :projectId and skill_id = :skillId and user_id = eventsByUserId.user_id )''', nativeQuery = true) @Modifying - void insertUserAchievementWhenDecreaseOfOccurrencesCausesUsersToAchieve(@Param('projectId') String projectId, @Param('skillId') String skillId, @Param('skillRefId') Integer skillRefId, @Param('numOfOccurrences') int numOfOccurrences) + void insertUserAchievementWhenDecreaseOfOccurrencesCausesUsersToAchieve(@Param('projectId') String projectId, + @Param('skillId') String skillId, + @Param('skillRefId') Integer skillRefId, + @Param('numOfOccurrences') int numOfOccurrences, + @Param('notified') String notified) } diff --git a/backend/src/main/java/skills/storage/repos/UserAttrsRepo.groovy b/service/src/main/java/skills/storage/repos/UserAttrsRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/UserAttrsRepo.groovy rename to service/src/main/java/skills/storage/repos/UserAttrsRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/UserPerformedSkillRepo.groovy b/service/src/main/java/skills/storage/repos/UserPerformedSkillRepo.groovy similarity index 99% rename from backend/src/main/java/skills/storage/repos/UserPerformedSkillRepo.groovy rename to service/src/main/java/skills/storage/repos/UserPerformedSkillRepo.groovy index 827ee59d..eb6489cb 100644 --- a/backend/src/main/java/skills/storage/repos/UserPerformedSkillRepo.groovy +++ b/service/src/main/java/skills/storage/repos/UserPerformedSkillRepo.groovy @@ -25,8 +25,6 @@ import skills.storage.model.DayCountItem import skills.storage.model.SkillDef import skills.storage.model.UserPerformedSkill -import javax.validation.constraints.Null - @CompileStatic interface UserPerformedSkillRepo extends JpaRepository { diff --git a/backend/src/main/java/skills/storage/repos/UserPointsRepo.groovy b/service/src/main/java/skills/storage/repos/UserPointsRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/UserPointsRepo.groovy rename to service/src/main/java/skills/storage/repos/UserPointsRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/UserRepo.groovy b/service/src/main/java/skills/storage/repos/UserRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/UserRepo.groovy rename to service/src/main/java/skills/storage/repos/UserRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/UserRoleRepo.groovy b/service/src/main/java/skills/storage/repos/UserRoleRepo.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/UserRoleRepo.groovy rename to service/src/main/java/skills/storage/repos/UserRoleRepo.groovy diff --git a/backend/src/main/java/skills/storage/repos/nativeSql/DBConditions.groovy b/service/src/main/java/skills/storage/repos/nativeSql/DBConditions.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/nativeSql/DBConditions.groovy rename to service/src/main/java/skills/storage/repos/nativeSql/DBConditions.groovy diff --git a/backend/src/main/java/skills/storage/repos/nativeSql/GraphRelWithAchievement.groovy b/service/src/main/java/skills/storage/repos/nativeSql/GraphRelWithAchievement.groovy similarity index 100% rename from backend/src/main/java/skills/storage/repos/nativeSql/GraphRelWithAchievement.groovy rename to service/src/main/java/skills/storage/repos/nativeSql/GraphRelWithAchievement.groovy diff --git a/backend/src/main/java/skills/storage/repos/nativeSql/H2NativeRepo.groovy b/service/src/main/java/skills/storage/repos/nativeSql/H2NativeRepo.groovy similarity index 50% rename from backend/src/main/java/skills/storage/repos/nativeSql/H2NativeRepo.groovy rename to service/src/main/java/skills/storage/repos/nativeSql/H2NativeRepo.groovy index 84820d06..3f8b090d 100644 --- a/backend/src/main/java/skills/storage/repos/nativeSql/H2NativeRepo.groovy +++ b/service/src/main/java/skills/storage/repos/nativeSql/H2NativeRepo.groovy @@ -44,9 +44,9 @@ class H2NativeRepo implements NativeQueriesRepo { and a.project_id = :projectId'''.toString() Query query = entityManager.createNativeQuery(q); - query.setParameter("projectId", projectId); - query.setParameter("parentSubjectSkillId", parentSubjectSkillId) - query.setParameter("deletedSkillId", deletedSkillId) + query.setParameter('projectId', projectId); + query.setParameter('parentSubjectSkillId', parentSubjectSkillId) + query.setParameter('deletedSkillId', deletedSkillId) query.executeUpdate(); } @@ -154,7 +154,6 @@ class H2NativeRepo implements NativeQueriesRepo { new PerformedSkillEventCount(userId: it[0], eventCount: it[1]) } - String updateSql = ''' UPDATE user_points points @@ -378,4 +377,370 @@ class H2NativeRepo implements NativeQueriesRepo { updateQ.executeUpdate() } } + + @Override + int addBadgeAchievementForEligibleUsers(String projectId, String badgeId, Integer badgeRowId, Boolean notified, Date start, Date end) { + String badgeSkillsQ = ''' + SELECT sr.child_ref_id + FROM skill_relationship_definition sr + INNER JOIN skill_definition sd ON sr.parent_ref_id = sd.id AND + sd.skill_id = :badgeId AND + sd.project_id = :projectId + ''' + + Query selectBadgeSkills = entityManager.createNativeQuery(badgeSkillsQ) + selectBadgeSkills.setParameter('badgeId', badgeId) + selectBadgeSkills.setParameter('projectId', projectId) + List badgeSkillIds = selectBadgeSkills.getResultList() + + String selectUsersQ = ''' + SELECT ua.user_id + FROM user_achievement ua + WHERE ua.skill_ref_id IN (:badgeSkillIds) AND + NOT EXISTS ( + SELECT 1 + FROM user_achievement + WHERE skill_id = :badgeId AND + user_id = ua.user_id AND + project_id = :projectId + ) + GROUP BY ua.user_id + HAVING COUNT(*) = :numBadgeSkills''' + + String dateFrag = ''' + AND + ( + SELECT MAX(performed_on) + FROM user_performed_skill + WHERE user_id=ua.user_id AND + skill_ref_id IN (:badgeSkillIds) + ) BETWEEN :start AND :end + ''' + + List results = [] + if(badgeSkillIds) { + boolean dateCheck = start != null && end != null + if(dateCheck) { + selectUsersQ += dateFrag + } + + Query getUsers = entityManager.createNativeQuery(selectUsersQ) + getUsers.setParameter('badgeSkillIds', badgeSkillIds) + getUsers.setParameter('projectId', projectId) + getUsers.setParameter('badgeId', badgeId) + getUsers.setParameter('numBadgeSkills', badgeSkillIds.size()) + if(dateCheck) { + getUsers.setParameter('start', start) + getUsers.setParameter('end', end) + } + List r = getUsers.getResultList() + + if(r) { + results.addAll(r) + } + } + + int updated=0 + results?.each{ + String insert = ''' + INSERT INTO user_achievement (user_id, project_id, skill_id, skill_ref_id, notified, points_when_achieved) + VALUES (:userId, :projectId, :skillId, :skillRefId, :notified, :pointsWhenAchieved ) + ''' + Query insertAchievement = entityManager.createNativeQuery(insert) + insertAchievement.setParameter("userId", it) + insertAchievement.setParameter("projectId", projectId) + insertAchievement.setParameter("skillId", badgeId) + insertAchievement.setParameter("skillRefId", badgeRowId) + insertAchievement.setParameter("notified", Boolean.FALSE.toString()) + insertAchievement.setParameter("pointsWhenAchieved", -1) + insertAchievement.executeUpdate() + updated++ + } + return updated + } + + int addGlobalBadgeAchievementForEligibleUsers(String badgeId, + Integer badgeRowId, + Boolean notified, + Integer requiredSklls, + Integer requiredLevels, + Date start, + Date end) { + + boolean requireSkills = requiredSklls != null && requiredSklls > 0 + boolean requireLevels = requiredLevels != null && requiredLevels > 0 + + String badgeSkillsQ = ''' + SELECT sr.child_ref_id + FROM skill_relationship_definition sr + INNER JOIN skill_definition sd ON sr.parent_ref_id = sd.id AND + sd.skill_id = :badgeId AND + sd.project_id is null + ''' + + String levelsQ = ''' + SELECT + ua.user_id + FROM USER_ACHIEVEMENT ua + INNER JOIN GLOBAL_BADGE_LEVEL_DEFINITION g ON g.level=ua.level + AND g.project_id = ua.project_id + WHERE ua.SKILL_ID is null + GROUP BY ua.user_id having count(ua.project_id) >= (SELECT count(*) FROM global_badge_level_definition WHERE skill_id = :badgeId) + ''' + + List usersWithRequiredLevel = [] + if (requireLevels) { + Query selectBadgeLevels = entityManager.createNativeQuery(levelsQ) + selectBadgeLevels.setParameter("badgeId", badgeId) + usersWithRequiredLevel = selectBadgeLevels.getResultList() + } + + List usersWithRequiredSkills = [] + if (requireSkills) { + Query skills = entityManager.createNativeQuery(badgeSkillsQ) + skills.setParameter("badgeId", badgeId) + List badgeSkills = skills.getResultList() + + if (badgeSkills) { + String usersWithSkills = ''' + SELECT ua.user_id + FROM user_achievement ua + WHERE ua.skill_ref_id IN (:badgeSkillIds) AND + NOT EXISTS ( + SELECT 1 + FROM user_achievement + WHERE skill_id = :badgeId AND + user_id = ua.user_id AND + project_id is null + ) + GROUP BY ua.user_id + HAVING COUNT(*) = :numBadgeSkills''' + + boolean dateRange = start != null && end != null + if (dateRange) { + usersWithSkills += ''' + AND + ( + SELECT MAX(performed_on) + FROM user_performed_skill + WHERE user_id=ua.user_id AND + skill_ref_id IN (:badgeSkillIds) + ) BETWEEN :start AND :end + ''' + } + + Query usersWithBadgeSkills = entityManager.createNativeQuery(usersWithSkills) + usersWithBadgeSkills.setParameter("badgeSkillIds", badgeSkills) + usersWithBadgeSkills.setParameter("badgeId", badgeId) + usersWithBadgeSkills.setParameter("numBadgeSkills", badgeSkills.size()) + usersWithBadgeSkills.setParameter("badgeSkillIds", badgeSkills) + usersWithRequiredSkills = usersWithBadgeSkills.getResultList() + } + } + + def users = [] + if (!requireLevels) { + users = usersWithRequiredSkills + } else if (!requireSkills) { + users = usersWithRequiredLevel + } else { + users = usersWithRequiredSkills.intersect(usersWithRequiredLevel) + } + + int updated=0 + users?.each{ + String insert = ''' + INSERT INTO user_achievement (user_id, project_id, skill_id, skill_ref_id, notified, points_when_achieved) + VALUES (:userId, :projectId, :skillId, :skillRefId, :notified, :pointsWhenAchieved ) + ''' + Query insertAchievement = entityManager.createNativeQuery(insert) + insertAchievement.setParameter("userId", it) + insertAchievement.setParameter("projectId", null) + insertAchievement.setParameter("skillId", badgeId) + insertAchievement.setParameter("skillRefId", badgeRowId) + insertAchievement.setParameter("notified", false) + insertAchievement.setParameter("pointsWhenAchieved", -1) + insertAchievement.executeUpdate() + updated++ + } + return updated + } + + @Override + void identifyAndAddSubjectLevelAchievements(String projectId, String subjectId, boolean pointsBasedLevels) { + + Query subjectScoreQ = entityManager.createNativeQuery(''' + select id, skill_id, total_points + from skill_definition + WHERE type = 'Subject' AND project_id = :projectId AND skill_id = :skillId ''') + + subjectScoreQ.setParameter("projectId", projectId) + subjectScoreQ.setParameter("skillId", subjectId) + Object score = subjectScoreQ.getSingleResult() //0: id, 1: skill_id, 2: total_points + + Number id = score[0] + String skillId = score[1] + Number totalSubjectPoints = score[2] + + String pointsRequiredFragment = '((CAST(ld.percent AS FLOAT)/100)*'+totalSubjectPoints+')' + if (pointsBasedLevels) { + pointsRequiredFragment = 'points_from' + } + + Query subjectLevelsQ = entityManager.createNativeQuery( + ''' + SELECT ld.id, ld.level, '''+pointsRequiredFragment+''' AS pointsRequired + FROM level_definition ld + WHERE ld.skill_ref_id = :subjectId + ''' + ) + subjectLevelsQ.setParameter("subjectId", id) + List subjectLevels = subjectLevelsQ.getResultList() //0: id, 1: level, 2: pointsRequired + if(!subjectLevels) { + log.warn("unable to retrieve subject levels for [${projectId} - ${subjectId}]") + return + } + + Query userTotalsQ = entityManager.createNativeQuery(''' + SELECT user_id, MAX(points) as totalPoints + FROM user_points + WHERE project_id = :projectId AND skill_id = :skillId AND day is null + GROUP BY user_id + ''') + userTotalsQ.setParameter("projectId", projectId) + userTotalsQ.setParameter("skillId", skillId) + List usersTotals = userTotalsQ.getResultList() //0: user_id, 1: totalPoints + if (!usersTotals) { + log.warn("unable to retrieve user point totals for [${projectId} - ${subjectId}]") + return + } + + usersTotals?.each { + String userId = it[0] + Number userPoints = it[1] + + subjectLevels?.each { Object[] level -> + Number levelId = level[0] + String levelValue = level[1] + Number pointsRequired = level[2] + + if (userPoints > pointsRequired) { + Query alreadyExists = entityManager.createNativeQuery(''' + SELECT 1 + FROM user_achievement + WHERE user_id = :userId and project_id = :projectId AND skill_id = :skillId AND level = :level + ''') + alreadyExists.setParameter("userId", userId) + alreadyExists.setParameter("projectId", projectId) + alreadyExists.setParameter("skillId", skillId) + alreadyExists.setParameter("level", levelValue) + def exists = alreadyExists.getResultList() + + if(exists.isEmpty() || exists[0] < 1) { + Query insertAchievement = entityManager.createNativeQuery(''' + INSERT INTO user_achievement (user_id, skill_id, level, points_when_achieved, project_id, notified) + VALUES (:userId, :skillId, :level, :userPoints, :projectId, 'false') + ''') + insertAchievement.setParameter("userId", userId) + insertAchievement.setParameter("skillId", skillId) + insertAchievement.setParameter("level", levelValue) + insertAchievement.setParameter("userPoints", userPoints.toInteger()) + insertAchievement.setParameter("projectId", projectId) + insertAchievement.executeUpdate() + } + } + } + } + + } + + @Override + void identifyAndAddProjectLevelAchievements(String projectId, boolean pointsBasedLevels) { + Query projectScore = entityManager.createNativeQuery(''' + SELECT SUM(total_points) AS totalPoints + FROM skill_definition WHERE type = 'Skill' AND project_id = :projectId + ''') + + projectScore.setParameter("projectId", projectId) + final Number totalPoints = projectScore.getSingleResult() + if (totalPoints == null) { + log.warn("unable to retrieve project total points for [${projectId}]") + return + } else if (totalPoints == 0) { + log.warn("received project total points of zero for [${projectId}]") + return + } + + String pointsRequiredFragment = '((CAST(percent AS FLOAT)/100)*'+totalPoints+')' + if (pointsBasedLevels) { + pointsRequiredFragment = 'points_from' + } + + Query projectRef = entityManager.createNativeQuery(''' + SELECT MAX(proj_ref_id) + FROM skill_definition + WHERE project_id = :projectId AND proj_ref_id IS NOT null + ''') + projectRef.setParameter("projectId", projectId) + final String projRefId = projectRef.getSingleResult() + if (!projRefId) { + log.warn("unable to retrieve project ref id for [${projectId}]") + return + } + + Query projectLevels = entityManager.createNativeQuery( + '''SELECT id, level, '''+pointsRequiredFragment+''' AS pointsRequired + FROM level_definition + WHERE project_ref_id=:projRefId + ''') + projectLevels.setParameter("projRefId", projRefId) + List levels = projectLevels.getResultList() + if (!levels) { + log.warn("unable to retrieve project levels for [${projectId}]") + return + } + + Query userTotals = entityManager.createNativeQuery(''' + SELECT user_id, MAX(points) as totalPoints + FROM user_points + WHERE project_id = :projectId AND skill_id is null AND day is null GROUP BY user_id + ''') + userTotals.setParameter("projectId", projectId) + List users = userTotals.getResultList() + + users?.each { + String userId = it[0] + Number userPoints = it[1] + + levels.each { Object[] level -> + Number levelId = level[0] + String levelValue = level[1] + Number pointsRequired = level[2] + + if (userPoints > pointsRequired) { + Query alreadyExists = entityManager.createNativeQuery(''' + SELECT 1 + FROM user_achievement + WHERE user_id = :userId AND project_id = :projectId AND level = :level AND skill_id IS NULL + ''') + alreadyExists.setParameter("userId", userId) + alreadyExists.setParameter("projectId", projectId) + alreadyExists.setParameter("level", levelValue) + List exists = alreadyExists.getResultList() + + if(exists.isEmpty() || exists[0] < 1) { + Query insertAchievement = entityManager.createNativeQuery(''' + INSERT INTO user_achievement (user_id, level, points_when_achieved, project_id, notified) + VALUES (:userId, :level, :userPoints, :projectId, 'false') + ''') + insertAchievement.setParameter("userId", userId) + insertAchievement.setParameter("level", levelValue) + insertAchievement.setParameter("userPoints", userPoints.toInteger()) + insertAchievement.setParameter("projectId", projectId) + insertAchievement.executeUpdate() + } + } + } + } + } } diff --git a/backend/src/main/java/skills/storage/repos/nativeSql/MySQLNativeRepo.groovy b/service/src/main/java/skills/storage/repos/nativeSql/MySQLNativeRepo.groovy similarity index 86% rename from backend/src/main/java/skills/storage/repos/nativeSql/MySQLNativeRepo.groovy rename to service/src/main/java/skills/storage/repos/nativeSql/MySQLNativeRepo.groovy index 4440a3b8..4d1b77ba 100644 --- a/backend/src/main/java/skills/storage/repos/nativeSql/MySQLNativeRepo.groovy +++ b/service/src/main/java/skills/storage/repos/nativeSql/MySQLNativeRepo.groovy @@ -154,4 +154,30 @@ class MySQLNativeRepo implements NativeQueriesRepo { void removeUserAchievementsThatDoNotMeetNewNumberOfOccurrences(String projectId, String skillId, int numOfOccurrences) { throw new UnsupportedOperationException("Sorry!") } + + @Override + int addBadgeAchievementForEligibleUsers(String projectId, String badgeId, Integer badgeRowId, Boolean notified, Date start, Date end){ + throw new UnsupportedOperationException("Sorry!") + } + + @Override + int addGlobalBadgeAchievementForEligibleUsers(String badgeId, + Integer badgeRowId, + Boolean notified, + Integer requiredSklls, + Integer requiredLevels, + Date start, + Date end) { + throw new UnsupportedOperationException("Sorry!") + } + + @Override + void identifyAndAddSubjectLevelAchievements(String projectId, String subjectId, boolean pointsBasedLevels) { + throw new UnsupportedOperationException("Sorry!") + } + + @Override + void identifyAndAddProjectLevelAchievements(String projectId, boolean pointsBasedLevels) { + throw new UnsupportedOperationException("Sorry!") + } } diff --git a/backend/src/main/java/skills/storage/repos/nativeSql/NativeQueriesRepo.groovy b/service/src/main/java/skills/storage/repos/nativeSql/NativeQueriesRepo.groovy similarity index 68% rename from backend/src/main/java/skills/storage/repos/nativeSql/NativeQueriesRepo.groovy rename to service/src/main/java/skills/storage/repos/nativeSql/NativeQueriesRepo.groovy index f9e30e0d..baf70389 100644 --- a/backend/src/main/java/skills/storage/repos/nativeSql/NativeQueriesRepo.groovy +++ b/service/src/main/java/skills/storage/repos/nativeSql/NativeQueriesRepo.groovy @@ -35,5 +35,19 @@ interface NativeQueriesRepo { void removeExtraEntriesOfUserPerformedSkillByUser(String projectId, String skillId, int numEventsToKeep) void removeUserAchievementsThatDoNotMeetNewNumberOfOccurrences(String projectId, String skillId, int numOfOccurrences) + + int addBadgeAchievementForEligibleUsers(String projectId, String badgeId, Integer badgeRowId, Boolean notified, Date start, Date end) + + int addGlobalBadgeAchievementForEligibleUsers(String globalBadgeId, + Integer globalBadgeRowId, + Boolean notified, + Integer requiredSklls, + Integer requiredLevels, + Date start, + Date end) + + void identifyAndAddSubjectLevelAchievements(String projectId, String subjectId, boolean pointsBasedLevels); + + void identifyAndAddProjectLevelAchievements(String projectId, boolean pointsBasedLevels); } diff --git a/backend/src/main/java/skills/storage/repos/nativeSql/PostgresQlNativeRepo.groovy b/service/src/main/java/skills/storage/repos/nativeSql/PostgresQlNativeRepo.groovy similarity index 61% rename from backend/src/main/java/skills/storage/repos/nativeSql/PostgresQlNativeRepo.groovy rename to service/src/main/java/skills/storage/repos/nativeSql/PostgresQlNativeRepo.groovy index 1f42a767..16545b39 100644 --- a/backend/src/main/java/skills/storage/repos/nativeSql/PostgresQlNativeRepo.groovy +++ b/service/src/main/java/skills/storage/repos/nativeSql/PostgresQlNativeRepo.groovy @@ -319,4 +319,250 @@ where sum.sumUserId = points.user_id and (sum.sumDay = points.day OR (sum.sumDay query.setParameter("numOfOccurrences", numOfOccurrences) query.executeUpdate() } + + @Override + int addBadgeAchievementForEligibleUsers(String projectId, String badgeId, Integer badgeRowId, Boolean notified, Date start, Date end) { + + String q = ''' + WITH badgeSkills AS ( + SELECT sr.child_ref_id childId + FROM skill_relationship_definition sr + INNER JOIN skill_definition sd ON sr.parent_ref_id = sd.id AND + sd.skill_id = :badgeId AND + sd.project_id = :projectId + ) + INSERT INTO user_achievement (user_id, project_id, skill_id, skill_ref_id, notified, points_when_achieved) + SELECT ua.user_id, ''' +"'$projectId', '$badgeId', $badgeRowId, '${notified.toString()}', -1"+ + ''' + FROM user_achievement ua + WHERE ua.skill_ref_id IN (SELECT childId FROM badgeSkills) AND + NOT EXISTS ( + SELECT 1 + FROM user_achievement + WHERE skill_id = :badgeId AND + user_id = ua.user_id AND + project_id = :projectId + ) + GROUP BY ua.user_id + HAVING COUNT(*) = (SELECT COUNT(*) FROM badgeSkills)''' + + final String dateFrag = ''' + AND + ( + SELECT MAX(performed_on) + FROM user_performed_skill + WHERE user_id=ua.user_id AND + skill_ref_id IN (SELECT childId FROM badgeSkills) + ) BETWEEN :start AND :end + ''' + + boolean dateCheck = start != null && end != null + + if (dateCheck) { + q += dateFrag + } + + Query query = entityManager.createNativeQuery(q); + query.setParameter('projectId', projectId) + query.setParameter('badgeId', badgeId) + + if (dateCheck) { + query.setParameter('start', start) + query.setParameter('end', end) + } + + return query.executeUpdate() + } + + int addGlobalBadgeAchievementForEligibleUsers(String badgeId, + Integer badgeRowId, + Boolean notified, + Integer requiredSklls, + Integer requiredLevels, + Date start, + Date end) { + + final String cteFrag = ''' + WITH badgeSkills AS ( + SELECT sr.child_ref_id childId + FROM skill_relationship_definition sr + INNER JOIN skill_definition sd ON sr.parent_ref_id = sd.id AND + sd.skill_id = :badgeId AND + sd.project_id is null + ) + ''' + + final String insertStatement = ''' + INSERT INTO user_achievement (user_id, skill_id, skill_ref_id, notified, points_when_achieved) + ''' + + final String selectFrag = '''SELECT ua.user_id, ''' +"'$badgeId', $badgeRowId, '${notified.toString()}', -1 " + + final String skillDateFrag = ''' + AND + ( + SELECT MAX(performed_on) + FROM user_performed_skill + WHERE user_id=ua.user_id AND + skill_ref_id IN (SELECT childId FROM badgeSkills) + ) BETWEEN :start AND :end + ''' + + boolean includeLevels = requiredLevels != null && requiredLevels > 0 + boolean includeSkills = requiredSklls != null && requiredSklls > 0 + boolean includeDates = start != null && end != null + + String q = insertStatement + String levels = '' + String skills = '' + if (includeLevels) { + levels = selectFrag + + ''' + FROM USER_ACHIEVEMENT ua + INNER JOIN GLOBAL_BADGE_LEVEL_DEFINITION g ON g.level=ua.level + AND g.project_id = ua.project_id + WHERE ua.SKILL_ID is null + GROUP BY ua.user_id having count(ua.project_id) >= (SELECT count(*) FROM global_badge_level_definition WHERE skill_id = :badgeId) + ''' + } + + if (includeSkills) { + skills = selectFrag + + ''' + FROM USER_ACHIEVEMENT ua + WHERE ua.skill_ref_id IN (SELECT childId FROM badgeSkills) AND + NOT EXISTS ( + SELECT 1 + FROM user_achievement + WHERE skill_id = :badgeId AND + user_id = ua.user_id AND + project_id is null + ) + GROUP BY ua.user_id + HAVING COUNT(*) = (SELECT COUNT(*) FROM badgeSkills) + ''' + } + + if (includeSkills) { + q = cteFrag + q + skills + if (includeDates) { + q += skillDateFrag + } + } + + if (includeLevels) { + if (includeSkills) { + q += ''' + INTERSECT + ''' + } + + q += levels + } + + if(!includeSkills && !includeLevels){ + return 0; + } + + Query query = entityManager.createNativeQuery(q); + if (includeSkills || includeLevels) { + query.setParameter('badgeId', badgeId) + } + if (includeDates) { + query.setParameter('start', start) + query.setParameter('end', end) + } + + return query.executeUpdate() + } + + @Override + void identifyAndAddSubjectLevelAchievements(String projectId, String subjectId, boolean pointsBasedLevels) { + + String pointsRequiredFragment = '((CAST(ld.percent AS FLOAT)/100)*subject_score.total_points)' + if (pointsBasedLevels) { + pointsRequiredFragment = 'points_from' + } + String SQL = ''' + WITH subject_score AS ( + SELECT id, skill_id, total_points + FROM skill_definition + WHERE type = 'Subject' AND project_id = :projectId AND skill_id = :skillId + ), + subject_levels AS ( + SELECT ld.id, ld.level, '''+pointsRequiredFragment+''' AS pointsRequired, subject_score.skill_id + FROM level_definition ld, subject_score + WHERE ld.skill_ref_id IN (SELECT max(id) from subject_score) + ), + user_totals AS ( + SELECT user_id, MAX(points) as totalPoints + FROM user_points + WHERE project_id = :projectId AND skill_id = :skillId AND day is null + GROUP BY user_id + ) + INSERT INTO user_achievement (user_id, skill_id, level, points_when_achieved, project_id, notified) + SELECT user_totals.user_id, subject_score.skill_id, subject_levels.level, user_totals.totalPoints, ''' + "'$projectId', 'false'" + + ''' + FROM user_totals, subject_score, subject_levels + WHERE user_totals.totalPoints > subject_levels.pointsRequired + AND NOT EXISTS + ( + SELECT 1 + FROM user_achievement + WHERE user_id = user_totals.user_id and project_id = :projectId AND skill_id = :skillId AND level = subject_levels.level + ) + ''' + + Query query = entityManager.createNativeQuery(SQL) + query.setParameter('projectId', projectId) + query.setParameter('skillId', subjectId) + + query.executeUpdate() + } + + @Override + void identifyAndAddProjectLevelAchievements(String projectId, boolean pointsBasedLevels){ + String pointsRequiredFragment = '((CAST(percent AS FLOAT)/100)*project_score.totalPoints)' + if (pointsBasedLevels) { + pointsRequiredFragment = 'points_from' + } + + String SQL = ''' + WITH project_score AS ( + SELECT SUM(total_points) AS totalPoints + FROM skill_definition WHERE type = 'Skill' AND project_id = :projectId + ), + project_ref AS ( + SELECT MAX(proj_ref_id) + FROM skill_definition + WHERE project_id = :projectId AND proj_ref_id IS NOT null + ), + project_levels AS ( + SELECT id, level, '''+pointsRequiredFragment+''' AS pointsRequired + FROM level_definition, project_score + WHERE project_ref_id IN (SELECT max from project_ref) + ), + user_totals AS ( + SELECT user_id, MAX(points) AS totalPoints + FROM user_points + WHERE project_id = :projectId AND skill_id is null AND day is null GROUP BY user_id + ) + INSERT INTO user_achievement (user_id, level, points_when_achieved, project_id, notified) + SELECT user_totals.user_id, project_levels.level, user_totals.totalPoints, '''+"'$projectId', 'false'"+ + ''' + FROM project_levels, user_totals + WHERE user_totals.totalPoints > project_levels.pointsRequired + AND NOT EXISTS + ( + SELECT 1 + FROM user_achievement + WHERE user_id = user_totals.user_id AND project_id = :projectId AND skill_id is null AND level = project_levels.level + ) + ''' + + Query query = entityManager.createNativeQuery(SQL) + query.setParameter('projectId', projectId) + query.executeUpdate() + } + } diff --git a/backend/src/main/java/skills/storage/repos/package-info.java b/service/src/main/java/skills/storage/repos/package-info.java similarity index 100% rename from backend/src/main/java/skills/storage/repos/package-info.java rename to service/src/main/java/skills/storage/repos/package-info.java diff --git a/backend/src/main/java/skills/utils/ArtificialDelay.java b/service/src/main/java/skills/utils/ArtificialDelay.java similarity index 100% rename from backend/src/main/java/skills/utils/ArtificialDelay.java rename to service/src/main/java/skills/utils/ArtificialDelay.java diff --git a/backend/src/main/java/skills/utils/ArtificialDelayAspect.groovy b/service/src/main/java/skills/utils/ArtificialDelayAspect.groovy similarity index 100% rename from backend/src/main/java/skills/utils/ArtificialDelayAspect.groovy rename to service/src/main/java/skills/utils/ArtificialDelayAspect.groovy diff --git a/backend/src/main/java/skills/utils/ClientSecretGenerator.groovy b/service/src/main/java/skills/utils/ClientSecretGenerator.groovy similarity index 100% rename from backend/src/main/java/skills/utils/ClientSecretGenerator.groovy rename to service/src/main/java/skills/utils/ClientSecretGenerator.groovy diff --git a/backend/src/main/java/skills/utils/InputSanitizer.groovy b/service/src/main/java/skills/utils/InputSanitizer.groovy similarity index 71% rename from backend/src/main/java/skills/utils/InputSanitizer.groovy rename to service/src/main/java/skills/utils/InputSanitizer.groovy index 6b05fbaf..68832d4d 100644 --- a/backend/src/main/java/skills/utils/InputSanitizer.groovy +++ b/service/src/main/java/skills/utils/InputSanitizer.groovy @@ -21,14 +21,25 @@ import org.jsoup.safety.Whitelist class InputSanitizer { - public static final Document.OutputSettings print = new Document.OutputSettings().prettyPrint(false) - public static String sanitize(String input) { + static String sanitize(String input) { if (!input) { return input; } return Jsoup.clean(input, "", Whitelist.basic(), print) } + + /** + * is #sanitize method is used for markdown before returning the payload back + * to the client remove sanitizion that breaks proper display of markdown + */ + static String unsanitizeForMarkdown(String input) { + if (!input) { + return input; + } + + return input.replaceAll(">", ">") + } } diff --git a/backend/src/main/java/skills/utils/Props.groovy b/service/src/main/java/skills/utils/Props.groovy similarity index 100% rename from backend/src/main/java/skills/utils/Props.groovy rename to service/src/main/java/skills/utils/Props.groovy diff --git a/backend/src/main/java/skills/utils/RetryUtil.groovy b/service/src/main/java/skills/utils/RetryUtil.groovy similarity index 100% rename from backend/src/main/java/skills/utils/RetryUtil.groovy rename to service/src/main/java/skills/utils/RetryUtil.groovy diff --git a/backend/src/main/java/skills/utils/SecretsUtil.groovy b/service/src/main/java/skills/utils/SecretsUtil.groovy similarity index 100% rename from backend/src/main/java/skills/utils/SecretsUtil.groovy rename to service/src/main/java/skills/utils/SecretsUtil.groovy diff --git a/service/src/main/java/skills/websocket/ChainedChannelInterceptor.groovy b/service/src/main/java/skills/websocket/ChainedChannelInterceptor.groovy new file mode 100644 index 00000000..47387166 --- /dev/null +++ b/service/src/main/java/skills/websocket/ChainedChannelInterceptor.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.websocket + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Lazy +import org.springframework.lang.Nullable +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.support.ChannelInterceptor +import org.springframework.stereotype.Component + +@Slf4j +@Component +class ChainedChannelInterceptor implements ChannelInterceptor{ + + @Qualifier("WebSocketConfig") + @Lazy + @Autowired + List chainedInterceptors + + + @Override + public Message preSend(Message message, MessageChannel channel) { + Message messageToUse = message; + for (ChannelInterceptor interceptor : chainedInterceptors) { + Message resolvedMessage = interceptor.preSend(messageToUse, channel); + if (resolvedMessage == null) { + String name = interceptor.getClass().getSimpleName(); + if (log.isDebugEnabled()) { + log.debug(name + " returned null from preSend, i.e. precluding the send."); + } + return null; + } + messageToUse = resolvedMessage; + } + return messageToUse; + } + + @Override + public void afterSendCompletion(Message message, MessageChannel channel, boolean sent, @Nullable Exception ex) { + for (ChannelInterceptor interceptor : chainedInterceptors) { + interceptor.afterSendCompletion(message, channel, sent, ex) + } + } + + @Override + public void postSend(Message message, MessageChannel channel, boolean sent) { + for (ChannelInterceptor interceptor : chainedInterceptors) { + interceptor.postSend(message, channel, sent); + } + } + + @Override + public boolean preReceive(MessageChannel channel) { + for (ChannelInterceptor interceptor : chainedInterceptors) { + if (!interceptor.preReceive(channel)) { + return false; + } + } + return true; + } + + @Override + public Message postReceive(Message message, MessageChannel channel) { + Message messageToUse = message; + for (ChannelInterceptor interceptor : chainedInterceptors) { + messageToUse = interceptor.postReceive(messageToUse, channel); + if (messageToUse == null) { + return null; + } + } + return messageToUse; + } + +} diff --git a/service/src/main/java/skills/websocket/FormAuthenticationChannelInterceptor.groovy b/service/src/main/java/skills/websocket/FormAuthenticationChannelInterceptor.groovy new file mode 100644 index 00000000..a689d0ef --- /dev/null +++ b/service/src/main/java/skills/websocket/FormAuthenticationChannelInterceptor.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.websocket + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Conditional +import org.springframework.context.annotation.DependsOn +import org.springframework.context.annotation.Lazy +import org.springframework.core.annotation.Order +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +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.stereotype.Component +import skills.auth.SecurityMode +import skills.auth.form.oauth2.SkillsOAuth2AuthenticationManager + +import javax.annotation.PostConstruct +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletRequestWrapper +import javax.servlet.http.HttpSession +import java.lang.reflect.Proxy + +@Qualifier("WebSocketConfig") +@Lazy +@Slf4j +@Component +@Conditional(SecurityMode.FormAuth) +@Order(-100) +class FormAuthenticationChannelInterceptor implements ChannelInterceptor { + + static final String AUTHORIZATION = 'Authorization' + + TokenExtractor tokenExtractor + AuthenticationDetailsSource authenticationDetailsSource + + SkillsOAuth2AuthenticationManager oAuth2AuthenticationManager + + @PostConstruct + public void init() { + log.info("Registering FormAuthenticationChannelInterceptor") + tokenExtractor = new BearerTokenExtractor() + authenticationDetailsSource = new OAuth2AuthenticationDetailsSource(); + } + + @Autowired + void setoAuth2AuthenticationManager(SkillsOAuth2AuthenticationManager oAuth2AuthenticationManager) { + this.oAuth2AuthenticationManager = oAuth2AuthenticationManager + } + + @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/service/src/main/java/skills/websocket/PendingNotificationsChannelInterceptor.groovy b/service/src/main/java/skills/websocket/PendingNotificationsChannelInterceptor.groovy new file mode 100644 index 00000000..08228b5e --- /dev/null +++ b/service/src/main/java/skills/websocket/PendingNotificationsChannelInterceptor.groovy @@ -0,0 +1,67 @@ +/** + * 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.websocket + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Lazy +import org.springframework.core.annotation.Order +import org.springframework.lang.Nullable +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +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.core.Authentication +import org.springframework.stereotype.Component +import skills.services.events.SkillEventsService + +import java.security.Principal + +@Qualifier("WebSocketConfig") +@Lazy +@Component +@Slf4j +@Order(Integer.MAX_VALUE) +class PendingNotificationsChannelInterceptor implements ChannelInterceptor { + + @Lazy + @Autowired + SkillEventsService skillEventsService + + @Override + void afterSendCompletion(Message message, MessageChannel channel, boolean sent, @Nullable Exception ex) { + if(ex == null) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class) + if (StompCommand.SUBSCRIBE == accessor.getCommand()) { + Principal user = accessor.getUser() + if (user != null && ((Authentication)user).isAuthenticated()) { + log.debug("sending any pending notifications to user [${user.getName()}]") + try { + skillEventsService.identifyPendingNotifications(user.getName()) + } catch (Exception e) { + log.error("unable to notify user [${user.getName()}] of pending notifications", e) + } + } else { + log.warn("unable to notify user of pending notifications as there is no Authentication or the user is not yet authenticated") + } + } + } + } + +} diff --git a/service/src/main/java/skills/websocket/WebSocketConfig.groovy b/service/src/main/java/skills/websocket/WebSocketConfig.groovy new file mode 100644 index 00000000..3983145e --- /dev/null +++ b/service/src/main/java/skills/websocket/WebSocketConfig.groovy @@ -0,0 +1,80 @@ +/** + * 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.websocket + +import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.messaging.simp.config.ChannelRegistration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +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 + +@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 { + + @Value('#{"${skills.websocket.enableStompBrokerRelay:false}"}') + Boolean enableStompBrokerRelay + + @Value('#{"${skills.websocket.relayHost:skills-stomp-broker}"}') + String relayHost + + @Value('#{"${skills.websocket.relayPort:61613}"}') + Integer relayPort + + @Value('${skills.authorization.authMode:#{T(skills.auth.AuthMode).DEFAULT_AUTH_MODE}}') + AuthMode authMode + + @Autowired + ChainedChannelInterceptor chainedChannelInterceptor + + + @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(chainedChannelInterceptor) { + registration.interceptors(chainedChannelInterceptor) + } + } + +} diff --git a/backend/src/main/resources/application.yml b/service/src/main/resources/application.yml similarity index 68% rename from backend/src/main/resources/application.yml rename to service/src/main/resources/application.yml index 1c0384e4..e9d95c10 100644 --- a/backend/src/main/resources/application.yml +++ b/service/src/main/resources/application.yml @@ -26,7 +26,23 @@ server: session: timeout: 24h persistent: false + http2: + enabled: false + compression: + enabled: true + min-response-size: 2048 + mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,images/x-icon spring: + #logging: + resources: + cache: + cachecontrol: + cache-private: true + #in seconds + max-age: 172800 + no-cache: false + no-store: false + must-revalidate: true main: allow-bean-definition-overriding: true jpa: @@ -41,20 +57,20 @@ spring: liquibase: "change-log": "classpath:db/changelog/db.changelog-master.xml" - security: - oauth2: - client: - registration: - google: - client-id: 584882456739-26jqdi7pd7v6vn4s3vuihvncp40e6nts.apps.googleusercontent.com - client-secret: IGi3Nu2kU4gI582OXc6pn7uz - redirectUriTemplate: 'http://localhost:8080/{action}/oauth2/code/{registrationId}' - iconClass: fab fa-google - github: - client-id: 6915872bda33a22ad6ef - client-secret: d79a5c4c063f4afae5963c50b73e828251ca3ae2 - redirectUriTemplate: 'http://localhost:8080/{action}/oauth2/code/{registrationId}' - iconClass: fab fa-github +# security: +# oauth2: +# client: +# registration: +# google: +# client-id: 12345 +# client-secret: 67890 +# redirectUriTemplate: 'http://localhost:8080/{action}/oauth2/code/{registrationId}' +# iconClass: fab fa-google +# github: +# client-id: 98765 +# client-secret: ABCDEF +# redirectUriTemplate: 'http://localhost:8080/{action}/oauth2/code/{registrationId}' +# iconClass: fab fa-github skills: gracefulShutdown: timeoutSecs: 80 @@ -63,11 +79,13 @@ skills: clientLibVersion: @maven.build.timestamp@ config: ui: + dashboardVersion: @pom.version@ + buildTimestamp: @maven.build.timestamp@ minimumSubjectPoints: 100 minimumProjectPoints: 100 descriptionMaxLength: 2000 maxTimeWindowInMinutes: 43200 - docsHost: http://skillsasaservicedocs.com + docsHost: https://code.nsa.gov/skills-docs maxProjectsPerAdmin: 25 maxSubjectsPerProject: 25 maxBadgesPerProject: 25 @@ -96,9 +114,11 @@ skills: maxNumPerformToCompletion: 10000 maxNumPointIncrementMaxOccurrences: 999 userSuggestOptions: + client: + loggingEnabled: false + loggingLevel: DEBUG profiles: active: default -#logging: # level: # org.hibernate: INFO # org.hibernate.type.descriptor.sql.BasicBinder: TRACE diff --git a/backend/src/main/resources/conf/logback-access.xml b/service/src/main/resources/conf/logback-access.xml similarity index 100% rename from backend/src/main/resources/conf/logback-access.xml rename to service/src/main/resources/conf/logback-access.xml diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.xml b/service/src/main/resources/db/changelog/db.changelog-master.xml similarity index 95% rename from backend/src/main/resources/db/changelog/db.changelog-master.xml rename to service/src/main/resources/db/changelog/db.changelog-master.xml index b58bca64..313cd3b6 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.xml +++ b/service/src/main/resources/db/changelog/db.changelog-master.xml @@ -940,4 +940,52 @@ + + + + + + + + + UPDATE user_achievement SET notified = 'true'; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/jwtkeys.jks b/service/src/main/resources/jwtkeys.jks similarity index 100% rename from backend/src/main/resources/jwtkeys.jks rename to service/src/main/resources/jwtkeys.jks diff --git a/service/src/main/resources/templates/password_reset.html b/service/src/main/resources/templates/password_reset.html new file mode 100644 index 00000000..c0c454be --- /dev/null +++ b/service/src/main/resources/templates/password_reset.html @@ -0,0 +1,33 @@ + + + + + + + +

    Hi [[${recipientName}]],

    +

    You have requested a password reset. Please use the link below to reset your password. The link will be valid for [[${validTime}]].

    +

    Reset Password

    +

    Regards,

    +

    + [[${senderName}]] at SkillTree
    +

    + + diff --git a/backend/src/test/java/skills/auth/AuthUtilsSpec.groovy b/service/src/test/java/skills/auth/AuthUtilsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/auth/AuthUtilsSpec.groovy rename to service/src/test/java/skills/auth/AuthUtilsSpec.groovy diff --git a/backend/src/test/java/skills/auth/UserInfoServiceSpec.groovy b/service/src/test/java/skills/auth/UserInfoServiceSpec.groovy similarity index 100% rename from backend/src/test/java/skills/auth/UserInfoServiceSpec.groovy rename to service/src/test/java/skills/auth/UserInfoServiceSpec.groovy diff --git a/backend/src/test/java/skills/auth/pki/PkiUserDetailsServiceSpecs.groovy b/service/src/test/java/skills/auth/pki/PkiUserDetailsServiceSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/auth/pki/PkiUserDetailsServiceSpecs.groovy rename to service/src/test/java/skills/auth/pki/PkiUserDetailsServiceSpecs.groovy diff --git a/backend/src/test/java/skills/icons/CssGeneratorSpec.groovy b/service/src/test/java/skills/icons/CssGeneratorSpec.groovy similarity index 100% rename from backend/src/test/java/skills/icons/CssGeneratorSpec.groovy rename to service/src/test/java/skills/icons/CssGeneratorSpec.groovy diff --git a/backend/src/test/java/skills/icons/CustomIconFacadeSpec.groovy b/service/src/test/java/skills/icons/CustomIconFacadeSpec.groovy similarity index 100% rename from backend/src/test/java/skills/icons/CustomIconFacadeSpec.groovy rename to service/src/test/java/skills/icons/CustomIconFacadeSpec.groovy diff --git a/backend/src/test/java/skills/intTests/AdminBadgesSpecs.groovy b/service/src/test/java/skills/intTests/AdminBadgesSpecs.groovy similarity index 65% rename from backend/src/test/java/skills/intTests/AdminBadgesSpecs.groovy rename to service/src/test/java/skills/intTests/AdminBadgesSpecs.groovy index 3d0e334f..e0c7271e 100644 --- a/backend/src/test/java/skills/intTests/AdminBadgesSpecs.groovy +++ b/service/src/test/java/skills/intTests/AdminBadgesSpecs.groovy @@ -16,6 +16,7 @@ package skills.intTests import skills.intTests.utils.DefaultIntSpec +import skills.intTests.utils.SkillsClientException import skills.intTests.utils.SkillsFactory class AdminBadgesSpecs extends DefaultIntSpec { @@ -50,6 +51,38 @@ class AdminBadgesSpecs extends DefaultIntSpec { res.projectId == proj.projectId } + def "assign skills to inactive badge"() { + def proj = SkillsFactory.createProject() + def subj = SkillsFactory.createSubject() + def skills = SkillsFactory.createSkills(4) + def badge = SkillsFactory.createBadge() + + skillsService.createProject(proj) + skillsService.createSubject(subj) + skillsService.createSkills(skills) + skillsService.assignDependency([projectId: proj.projectId, skillId: skills.get(0).skillId, dependentSkillId: skills.get(1).skillId]) + skillsService.assignDependency([projectId: proj.projectId, skillId: skills.get(0).skillId, dependentSkillId: skills.get(2).skillId]) + + badge.enabled = false + skillsService.createBadge(badge) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(0).skillId]) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(3).skillId]) + + when: + def res = skillsService.getBadge(badge) + + then: + res + res.numSkills == 2 + res.requiredSkills.size() == 2 + res.requiredSkills.collect { it.skillId }.sort() == ["skill1", "skill4"] + res.totalPoints == 20 + res.badgeId == badge.badgeId + res.name == badge.name + res.projectId == proj.projectId + res.enabled == 'false' + } + def "remove skills from a badge"() { def proj = SkillsFactory.createProject() def subj = SkillsFactory.createSubject() @@ -104,4 +137,27 @@ class AdminBadgesSpecs extends DefaultIntSpec { resAfterDeletion resAfterDeletion.collect { it.badgeId } == [badge.badgeId] } + + def "cannot disable a badge after it has been enabled"(){ + def proj = SkillsFactory.createProject() + def subj = SkillsFactory.createSubject() + def skills = SkillsFactory.createSkills(4) + def badge = SkillsFactory.createBadge() + badge.enabled = 'true' + + skillsService.createProject(proj) + skillsService.createSubject(subj) + skillsService.createSkills(skills) + + skillsService.createBadge(badge) + + when: + badge = skillsService.getBadge(badge) + badge.enabled = 'false' + skillsService.createBadge(badge) + + then: + SkillsClientException ex = thrown() + ex.getMessage().contains("Once a Badge has been published, the only allowable value for enabled is [true]") + } } diff --git a/backend/src/test/java/skills/intTests/AdminEditSpecs.groovy b/service/src/test/java/skills/intTests/AdminEditSpecs.groovy similarity index 99% rename from backend/src/test/java/skills/intTests/AdminEditSpecs.groovy rename to service/src/test/java/skills/intTests/AdminEditSpecs.groovy index a6632890..ac4bf489 100644 --- a/backend/src/test/java/skills/intTests/AdminEditSpecs.groovy +++ b/service/src/test/java/skills/intTests/AdminEditSpecs.groovy @@ -92,6 +92,7 @@ class AdminEditSpecs extends DefaultIntSpec { def res = skillsService.getBadge(badge) def originalBadgeId = res.badgeId res.badgeId = "TestBadge47" + res.enabled = 'true' skillsService.createBadge(res, originalBadgeId) def updatedResult = skillsService.getBadge(res) diff --git a/backend/src/test/java/skills/intTests/AdminSkillInfoSpecs.groovy b/service/src/test/java/skills/intTests/AdminSkillInfoSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/AdminSkillInfoSpecs.groovy rename to service/src/test/java/skills/intTests/AdminSkillInfoSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/AuthorizationSpecs.groovy b/service/src/test/java/skills/intTests/AuthorizationSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/AuthorizationSpecs.groovy rename to service/src/test/java/skills/intTests/AuthorizationSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/ClientVersionSpecs.groovy b/service/src/test/java/skills/intTests/ClientVersionSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/ClientVersionSpecs.groovy rename to service/src/test/java/skills/intTests/ClientVersionSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/ConcurrencySpecs.groovy b/service/src/test/java/skills/intTests/ConcurrencySpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/ConcurrencySpecs.groovy rename to service/src/test/java/skills/intTests/ConcurrencySpecs.groovy diff --git a/backend/src/test/java/skills/intTests/ConstraintViolationSpecs.groovy b/service/src/test/java/skills/intTests/ConstraintViolationSpecs.groovy similarity index 81% rename from backend/src/test/java/skills/intTests/ConstraintViolationSpecs.groovy rename to service/src/test/java/skills/intTests/ConstraintViolationSpecs.groovy index 220ae500..2bfd4991 100644 --- a/backend/src/test/java/skills/intTests/ConstraintViolationSpecs.groovy +++ b/service/src/test/java/skills/intTests/ConstraintViolationSpecs.groovy @@ -40,6 +40,24 @@ class ConstraintViolationSpecs extends DefaultIntSpec { exception.message.contains("errorCode:ConstraintViolation") } + def "special characters in project name"() { + Map proj = SkillsFactory.createProject() + proj.name = "special 123456789_-#()[]/*%;" + + skillsService.createProject(proj) + when: + def origIdExists = skillsService.projectIdExists([projectId: proj.projectId]) + def origNameExists = skillsService.projectNameExists([projectName: proj.name]) + def project = skillsService.getProject(proj.projectId) + + then: + origIdExists + origNameExists + project + project.name == "special 123456789_-#()[]/*%;" + + } + def "check for existing project name"(){ Map proj = SkillsFactory.createProject() proj.name = "Test Project 1" @@ -108,6 +126,25 @@ class ConstraintViolationSpecs extends DefaultIntSpec { upperExists } + def "special characters in subject name"() { + Map proj = SkillsFactory.createProject() + Map subject = SkillsFactory.createSubject() + subject.name = "special 123456789_-#()[]/*%;" + skillsService.createProject(proj) + skillsService.createSubject(subject) + + when: + def existsOriginal = skillsService.subjectNameExists([projectId: proj.projectId, subjectName: subject.name]) + def existsWithoutRestrictedCharacters = skillsService.subjectNameExists([projectId: proj.projectId, subjectName: "special 123456789_-#()[]/"]) + def subj = skillsService.getSubject([projectId: proj.projectId, subjectId: subject.subjectId]) + + then: + existsOriginal + existsWithoutRestrictedCharacters + subj + subj.name == 'special 123456789_-#()[]/*%;' + } + def "duplicate subject id - will belong to another user and will fail auth"() { Map proj = SkillsFactory.createProject() Map subject = SkillsFactory.createSubject() @@ -185,6 +222,26 @@ class ConstraintViolationSpecs extends DefaultIntSpec { upperExists } + def "skill name with special characters"() { + Map proj = SkillsFactory.createProject() + Map subject = SkillsFactory.createSubject() + Map skill = SkillsFactory.createSkill() + skill.name = "special 123456789_-#()[]/*%;" + + skillsService.createProject(proj) + skillsService.createSubject(subject) + skillsService.createSkill(skill) + + when: + def originalNameExists = skillsService.skillNameExists([projectId: proj.projectId, skillName: skill.name.toLowerCase()]) + def retrievedSkill = skillsService.getSkill([projectId: proj.projectId, subjectId: subject.subjectId, skillId: skill.skillId]) + + then: + originalNameExists + retrievedSkill + retrievedSkill.name == 'special 123456789_-#()[]/*%;' + } + def "duplicate skill id"() { Map proj = SkillsFactory.createProject() Map subject = SkillsFactory.createSubject() @@ -245,6 +302,27 @@ class ConstraintViolationSpecs extends DefaultIntSpec { upper } + def "badge name with special characters"() { + Map proj = SkillsFactory.createProject() + Map subject = SkillsFactory.createSubject() + Map badge = SkillsFactory.createBadge() + String badgeName = "foo 123456789_-#()[]/*%;" + badge.name = badgeName + + skillsService.createProject(proj) + skillsService.createSubject(subject) + skillsService.createBadge(badge) + + when: + def exists = skillsService.badgeNameExists([projectId: proj.projectId, badgeName: badgeName]) + def existingBadge = skillsService.getBadge([projectId: proj.projectId, badgeId: badge.badgeId]) + + then: + exists + existingBadge + existingBadge.name == 'foo 123456789_-#()[]/*%;' + } + def "duplicate badge id - project belongs to another user and will fail auth"() { Map proj = SkillsFactory.createProject() Map subject = SkillsFactory.createSubject() diff --git a/backend/src/test/java/skills/intTests/CustomIconsSpec.groovy b/service/src/test/java/skills/intTests/CustomIconsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/CustomIconsSpec.groovy rename to service/src/test/java/skills/intTests/CustomIconsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/CustomValidationSpecs.groovy b/service/src/test/java/skills/intTests/CustomValidationSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/CustomValidationSpecs.groovy rename to service/src/test/java/skills/intTests/CustomValidationSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/DataCleanupSpecs.groovy b/service/src/test/java/skills/intTests/DataCleanupSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/DataCleanupSpecs.groovy rename to service/src/test/java/skills/intTests/DataCleanupSpecs.groovy diff --git a/service/src/test/java/skills/intTests/DataMigrationDBIT.groovy b/service/src/test/java/skills/intTests/DataMigrationDBIT.groovy new file mode 100644 index 00000000..70cfe736 --- /dev/null +++ b/service/src/test/java/skills/intTests/DataMigrationDBIT.groovy @@ -0,0 +1,224 @@ +/** + * 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 + +import groovy.util.logging.Slf4j +import liquibase.Contexts +import liquibase.LabelExpression +import liquibase.Liquibase +import liquibase.database.Database +import liquibase.database.DatabaseFactory +import liquibase.database.jvm.JdbcConnection +import liquibase.resource.ClassLoaderResourceAccessor +import org.apache.commons.lang3.StringUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Conditional +import org.springframework.core.io.ClassPathResource +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Component +import skills.SpringBootApp +import skills.intTests.utils.DefaultIntSpec +import skills.intTests.utils.SkillsFactory +import skills.storage.repos.LevelDefRepo +import skills.storage.repos.ProjDefRepo +import skills.storage.repos.SkillRelDefRepo +import skills.storage.repos.nativeSql.DBConditions + +import javax.annotation.PostConstruct +import javax.sql.DataSource + +@Slf4j +@SpringBootTest(properties = ['skills.db.startup=false', 'spring.liquibase.enabled=false'], webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SpringBootApp) +class DataMigrationDBIT extends DefaultIntSpec { + + @Autowired + LevelDefRepo levelDefRepo + + @Autowired + ProjDefRepo projDefRepo + + @Autowired + SkillRelDefRepo relDefRepo + + @Autowired + DataSource dataSource + + @Autowired + JdbcTemplate jdbcTemplate + + @Autowired + @Value('${spring.liquibase.change-log}') + ClassPathResource changeLog + + @Autowired + DbDropper dbDropper + + @Override + def doSetup() { + // disable initial db setup in the base class + dbDropper.dropDb() + } + + def "validate migration1"() { + Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())) + Liquibase liquibase = new Liquibase(StringUtils.removeStart(changeLog.getPath(), "classpath:"), new ClassLoaderResourceAccessor(), database) + liquibase.update(new Contexts('!migration1'), new LabelExpression()) + + insertPreMigration1TestData() + skillsService = createService() + when: + String projectId = 'TestProject1' + String userId = 'user1' + + // get skill_definition, settings, and user_achievement rows pre migration1 + def existingSkillDefsPreMigration = getSkillDefinitionFromDB(projectId) + def existingUserAchievementsPreMigration = getUserAchievementsFromDB(projectId) + def existingSettingsPreMigration = getSettingsFromDB() + + // apply migration1 changes + liquibase.update(new Contexts('migration1'), new LabelExpression()) + + // get skill_definition, settings, and user_achievement rows post migration1 + def existingSkillDefsPostMigration = getSkillDefinitionFromDB(projectId) + def existingUserAchievementsPostMigration = getUserAchievementsFromDB(projectId) + def settingsPostMigration = getSettingsFromDB() + + // also, create a new subject, skill and user_achievement post migration + // and make sure everything works as expected + def subject2 = SkillsFactory.createSubject(1, 2) + List subjectSkills2 = SkillsFactory.createSkills(1, 1, 2) + subjectSkills2.each { + it.pointIncrement = 100 + } + def expectedSkill2 = subjectSkills2.get(0) + skillsService.createSubject(subject2) + skillsService.createSkills(subjectSkills2) + def res = skillsService.addSkill([projectId: projectId, skillId: expectedSkill2.skillId], userId, new Date()) + def resetTokensPostMigration = getPasswordResetTokens() + + then: + existingSkillDefsPreMigration.findAll { it.containsKey('ENABLED') }.size() == 0 + existingUserAchievementsPreMigration.findAll { it.containsKey('NOTIFIED') }.size() == 0 + //make sure existing setting value wasn't changed by column size alteration + existingSettingsPreMigration.find {it['id'] == 2}.value == settingsPostMigration.find {it['id'] ==2}.value + + existingSkillDefsPreMigration.size() == existingSkillDefsPostMigration.size() + existingUserAchievementsPreMigration.size() == existingUserAchievementsPreMigration.size() + + existingSkillDefsPostMigration.findAll { it.containsKey('ENABLED') }.size() == existingSkillDefsPostMigration.size() + existingUserAchievementsPostMigration.findAll { it.containsKey('NOTIFIED') }.size() == existingUserAchievementsPostMigration.size() + existingSkillDefsPostMigration.findAll { new Boolean(it.ENABLED) == true }.size() == existingSkillDefsPostMigration.size() + existingUserAchievementsPostMigration.findAll { new Boolean(it.NOTIFIED) == true }.size() == existingUserAchievementsPostMigration.size() + !resetTokensPostMigration + + + res.body.skillApplied + res.body.explanation == "Skill event was applied" + + res.body.completed.size() == 6 + res.body.completed.find({ it.type == "Skill" }).id == expectedSkill2.skillId + res.body.completed.find({ it.type == "Skill" }).name == expectedSkill2.name + + res.body.completed.findAll({ it.type == "Subject" && it.id == subject2.subjectId }).size() == 5 + res.body.completed.findAll({ it.type == "Subject" && it.name == subject2.name }).size() == 5 + res.body.completed.findAll({ it.type == "Subject" && it.level == 1 }) + res.body.completed.findAll({ it.type == "Subject" && it.level == 2 }) + res.body.completed.findAll({ it.type == "Subject" && it.level == 3 }) + res.body.completed.findAll({ it.type == "Subject" && it.level == 4 }) + res.body.completed.findAll({ it.type == "Subject" && it.level == 5 }) + } + + private insertPreMigration1TestData() { + new ClassPathResource("migration1.sql").getFile().eachLine { sqlStmt -> + jdbcTemplate.execute(sqlStmt) + } + } + + private List> getSkillDefinitionFromDB(String projectId) { + jdbcTemplate.queryForList("select * from skill_definition where project_id='${projectId}'") + } + + private List> getUserAchievementsFromDB(String projectId) { + jdbcTemplate.queryForList("select * from user_achievement where project_id='${projectId}'") + } + + private List> getSettingsFromDB() { + jdbcTemplate.queryForList("select * from settings"); + } + + private List> getPasswordResetTokens() { + jdbcTemplate.queryForList("select * from password_reset_token") + } + + // over-ride beans that interact with the database during spring context initialization + @ConditionalOnProperty( + name = "skills.db.startup", + havingValue = "false", + matchIfMissing = false) + @Component + static class HealthChecker extends skills.HealthChecker { + @PostConstruct + @Override + void checkRequiredServices() { } + } + + @ConditionalOnProperty( + name = "skills.db.startup", + havingValue = "false", + matchIfMissing = false) + @Component + static class EmailSettingsService extends skills.settings.EmailSettingsService { + @PostConstruct + @Override + void init() { } + } + + static interface DbDropper { + void dropDb() + } + + @Conditional(DBConditions.PostgresQL) + @Component + static class PostgresqlDropper implements DbDropper { + + @Autowired + JdbcTemplate jdbcTemplate + + @Override + void dropDb() { + jdbcTemplate.execute('drop schema public cascade') + jdbcTemplate.execute('create schema public') + log.info("Dropped PostgreSQL database") + } + } + + @Conditional(DBConditions.H2_IN_MEMORY) + @Component + static class H2Dropper implements DbDropper { + + @Autowired + JdbcTemplate jdbcTemplate + + @Override + void dropDb() { + jdbcTemplate.execute('DROP ALL OBJECTS') + log.info("Dropped H2 database") + } + } +} diff --git a/backend/src/test/java/skills/intTests/DeleteSkillEventSpecs.groovy b/service/src/test/java/skills/intTests/DeleteSkillEventSpecs.groovy similarity index 99% rename from backend/src/test/java/skills/intTests/DeleteSkillEventSpecs.groovy rename to service/src/test/java/skills/intTests/DeleteSkillEventSpecs.groovy index d35bb875..0f160628 100644 --- a/backend/src/test/java/skills/intTests/DeleteSkillEventSpecs.groovy +++ b/service/src/test/java/skills/intTests/DeleteSkillEventSpecs.groovy @@ -195,6 +195,9 @@ class DeleteSkillEventSpecs extends DefaultIntSpec { skillsService.createSkill(skill3) skillsService.createSkill(skill4) skillsService.createBadge(badge) + badge = skillsService.getBadge(badge) + badge.enabled = 'true' + skillsService.createBadge(badge) requiredSkillsIds.each { skillId -> skillsService.assignSkillToBadge(projectId: projId, badgeId: badge.badgeId, skillId: skillId) } diff --git a/backend/src/test/java/skills/intTests/EntityExistsTests.groovy b/service/src/test/java/skills/intTests/EntityExistsTests.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/EntityExistsTests.groovy rename to service/src/test/java/skills/intTests/EntityExistsTests.groovy diff --git a/service/src/test/java/skills/intTests/FeatureVerificationSpecs.groovy b/service/src/test/java/skills/intTests/FeatureVerificationSpecs.groovy new file mode 100644 index 00000000..5f903248 --- /dev/null +++ b/service/src/test/java/skills/intTests/FeatureVerificationSpecs.groovy @@ -0,0 +1,90 @@ +/** + * 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 + +import com.icegreen.greenmail.util.GreenMail +import com.icegreen.greenmail.util.ServerSetupTest +import skills.intTests.utils.DefaultIntSpec +import skills.intTests.utils.SkillsService + +class FeatureVerificationSpecs extends DefaultIntSpec { + + GreenMail greenMail = new GreenMail(ServerSetupTest.SMTP) + + SkillsService rootSkillsService + + + def setup() { + rootSkillsService = createService("rootUser", 'aaaaaaaa') + if (!rootSkillsService.isRoot()) { + rootSkillsService.grantRoot() + } + } + + def cleanup(){ + greenMail?.stop() + } + + def "password reset feature enabled if mail settings and public url are configured" () { + greenMail.start() + rootSkillsService.getWsHelper().rootPost("/saveEmailSettings", [ + "host" : "localhost", + "port" : ServerSetupTest.SMTP.port, + "protocol" : "smtp", + "authEnabled": false, + "tlsEnabled" : false + ]) + rootSkillsService.addOrUpdateGlobalSetting("public_url", + ["setting": "public_url", "value": "http://localhost:${localPort}/".toString()]) + + when: + + def enabled = skillsService.isFeatureEnabled("passwordReset") + + then: + enabled == true + } + + def "password reset disabled if email is not configured"(){ + rootSkillsService.addOrUpdateGlobalSetting("public_url", + ["setting": "public_url", "value": "http://localhost:${localPort}/".toString()]) + + when: + + def enabled = skillsService.isFeatureEnabled("passwordReset") + + then: + enabled == false + } + + def "password reset disabled if public url is not configured"() { + greenMail.start() + rootSkillsService.getWsHelper().rootPost("/saveEmailSettings", [ + "host" : "localhost", + "port" : ServerSetupTest.SMTP.port, + "protocol" : "smtp", + "authEnabled": false, + "tlsEnabled" : false + ]) + + when: + + def enabled = skillsService.isFeatureEnabled("passwordReset") + + then: + enabled == false + } +} diff --git a/backend/src/test/java/skills/intTests/LevelsSpec.groovy b/service/src/test/java/skills/intTests/LevelsSpec.groovy similarity index 92% rename from backend/src/test/java/skills/intTests/LevelsSpec.groovy rename to service/src/test/java/skills/intTests/LevelsSpec.groovy index a3d345d7..53b63384 100644 --- a/backend/src/test/java/skills/intTests/LevelsSpec.groovy +++ b/service/src/test/java/skills/intTests/LevelsSpec.groovy @@ -201,6 +201,94 @@ class LevelsSpec extends DefaultIntSpec{ userLevelBeforeEdit == userLevelAfterEdit } + def "user retains achieved level if points range changes (because new skill added to subject) after achieving max level"(){ + when: + + def levels = skillsService.getLevels(projId, null).sort {it.level} + + skillsService.addSkill([projectId: projId, skillId: skill1], "thing1", new Date()) + skillsService.addSkill([projectId: projId, skillId: skill2], "thing1", new Date()) + skillsService.addSkill([projectId: projId, skillId: skill3], "thing1", new Date()) + + def userLevelBeforeEdit = skillsService.getSkillSummary("thing1", projId).skillsLevel + //should be level 3 or 4? + + skillsService.createSkill( + [ + projectId: projId, + subjectId: subject, + skillId: 'skill4', + name: 'Test Skill 4', + pointIncrement: 100, + numPerformToCompletion: 10, + pointIncrementInterval: 60, numMaxOccurrencesIncrementInterval: 1 + ] + ) + + def skillSummary = skillsService.getSkillSummary("thing1", projId) + def userLevelAfterEdit = skillSummary.skillsLevel + def levelsAfterEdit = skillsService.getLevels(projId, null).sort {it.level} + + then: + userLevelBeforeEdit == userLevelAfterEdit + } + + def "user retains achieved level if points range changes after achieving max level"(){ + when: + + def settingResult = skillsService.changeSetting(projId, projectPointsSetting, [projectId: projId, setting: projectPointsSetting, value: "true"]) + def levels = skillsService.getLevels(projId, null).sort {it.level} + + skillsService.addSkill([projectId: projId, skillId: skill1], "thing1", new Date()) + skillsService.addSkill([projectId: projId, skillId: skill2], "thing1", new Date()) + skillsService.addSkill([projectId: projId, skillId: skill3], "thing1", new Date()) + + def userLevelBeforeEdit = skillsService.getSkillSummary("thing1", projId).skillsLevel + //should be level5 + + + def props = [:] + props.projectId = levels.last().projectId + props.skillId = levels.last().skillId + props.level = levels.last().level + props.percent = levels.last().percent + props.pointsFrom = levels.last().pointsFrom+50 + props.pointsTo = levels.last().pointsTo + props.name = "TwoEagles" + props.iconClass = "two-eagles" + skillsService.editLevel(projId, null, props.level as String, props) + + props = [:] + props.projectId = levels.get(levels.size()-2).projectId + props.skillId = levels.get(levels.size()-2).skillId + props.level = levels.get(levels.size()-2).level + props.percent = levels.get(levels.size()-2).percent + props.pointsFrom = levels.get(levels.size()-2).pointsFrom+50 + props.pointsTo = levels.get(levels.size()-2).pointsTo+50 + props.name = "TwoEagles" + props.iconClass = "two-eagles" + skillsService.editLevel(projId, null, props.level as String, props) + + props = [:] + props.projectId = levels.get(levels.size()-3).projectId + props.skillId = levels.get(levels.size()-3).skillId + props.level = levels.get(levels.size()-3).level + props.percent = levels.get(levels.size()-3).percent + props.pointsFrom = levels.get(levels.size()-3).pointsFrom+50 + props.pointsTo = levels.get(levels.size()-3).pointsTo+50 + props.name = "TwoEagles" + props.iconClass = "two-eagles" + skillsService.editLevel(projId, null, props.level as String, props) + + def skillSummary = skillsService.getSkillSummary("thing1", projId) + def userLevelAfterEdit = skillSummary.skillsLevel + def levelsAfterEdit = skillsService.getLevels(projId, null).sort {it.level} + + then: + userLevelBeforeEdit == userLevelAfterEdit + } + + def "switch to points based, edit level points, user should not achieve based on old percentage"(){ when: diff --git a/service/src/test/java/skills/intTests/PasswordResetSpec.groovy b/service/src/test/java/skills/intTests/PasswordResetSpec.groovy new file mode 100644 index 00000000..b429d080 --- /dev/null +++ b/service/src/test/java/skills/intTests/PasswordResetSpec.groovy @@ -0,0 +1,188 @@ +/** + * 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 + +import com.icegreen.greenmail.util.GreenMail +import com.icegreen.greenmail.util.ServerSetupTest +import org.apache.commons.lang3.time.DurationFormatUtils +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.HttpClientErrorException +import skills.intTests.utils.DefaultIntSpec +import skills.intTests.utils.SkillsService + +import javax.mail.internet.MimeMessage +import java.time.Duration + +class PasswordResetSpec extends DefaultIntSpec { + + GreenMail greenMail = new GreenMail(ServerSetupTest.SMTP) + + SkillsService rootSkillsService + + + def setup() { + greenMail.start() + + rootSkillsService = createService("rootUser", 'aaaaaaaa') + if (!rootSkillsService.isRoot()) { + rootSkillsService.grantRoot() + } + + rootSkillsService.getWsHelper().rootPost("/saveEmailSettings", [ + "host" : "localhost", + "port" : ServerSetupTest.SMTP.port, + "protocol" : "smtp", + "authEnabled": false, + "tlsEnabled" : false + ]) + rootSkillsService.addOrUpdateGlobalSetting("public_url", + ["setting": "public_url", "value": "http://localhost:${localPort}/".toString()]) + + rootSkillsService.addOrUpdateGlobalSetting("from_email", + ["setting": "from_email", "value": "resetspec@skilltreetests".toString()]) + } + + def cleanup(){ + greenMail.stop() + } + + def "password reset request sends email"() { + SkillsService aUser = createService("randomuser@skills.org", "somepassword",) + + when: + //post request with an unauthenticated client to ensure that the url is publicly available + RestTemplate template = new RestTemplate() + HttpHeaders headers = new HttpHeaders() + headers.setContentType(MediaType.MULTIPART_FORM_DATA) + MultiValueMap body = new LinkedMultiValueMap<>() + body.add("userId", "randomuser@skills.org") + HttpEntity entity = new HttpEntity(body, headers) + + String url = "http://localhost:${localPort}/resetPassword" + + template.postForEntity(url, entity, String.class) + + then: + greenMail.getReceivedMessages().length == 1 + MimeMessage msg = greenMail.getReceivedMessages()[0] + msg.getAllRecipients()[0].toString() == "randomuser@skills.org" + msg.getSubject() == "SkillTree Password Reset" + msg.getContent().toString().contains('href="http://localhost:' + localPort + '/reset-password/') + msg.getFrom()[0].toString() == "resetspec@skilltreetests" + } + + def "reset password with token from email"() { + SkillsService aUser = createService("randomuser@skills.org", "somepassword") + //post request with an unauthenticated client to ensure that the url is publicly available + RestTemplate template = new RestTemplate() + HttpHeaders headers = new HttpHeaders() + headers.setContentType(MediaType.MULTIPART_FORM_DATA) + MultiValueMap body = new LinkedMultiValueMap<>() + body.add("userId", "randomuser@skills.org") + HttpEntity entity = new HttpEntity(body, headers) + String url = "http://localhost:${localPort}/resetPassword" + template.postForEntity(url, entity, String.class) + + MimeMessage msg = greenMail.getReceivedMessages()[0] + + def match = msg.content.toString() =~ /href=".*\/reset-password\/([^"]+)"/ + String token = match[0][1] + + when: + + url = "http://localhost:${localPort}/performPasswordReset" + def reset = ["userId": "randomuser@skills.org", "password": "newpassword", "resetToken": token] + headers = new HttpHeaders() + headers.setContentType(MediaType.APPLICATION_JSON) + entity = new HttpEntity(reset, headers) + template.postForEntity(url, entity, String.class) + //we expect this to fail as it is using the old password + createService("randomuser@skills.org", "somepassword") + + then: + AssertionError e = thrown(AssertionError) + e.message.contains("401 UNAUTHORIZED") + //we expect this to succeed as it uses the password specified in the reset + createService("randomuser@skills.org", "newpassword") + } + + def "reset password with invalid token fails"() { + SkillsService aUser = createService("randomuser@skills.org", "somepassword") + //post request with an unauthenticated client to ensure that the url is publicly available + RestTemplate template = new RestTemplate() + HttpHeaders headers = new HttpHeaders() + headers.setContentType(MediaType.MULTIPART_FORM_DATA) + MultiValueMap body = new LinkedMultiValueMap<>() + body.add("userId", "randomuser@skills.org") + HttpEntity entity = new HttpEntity(body, headers) + String url = "http://localhost:${localPort}/resetPassword" + template.postForEntity(url, entity, String.class) + + MimeMessage msg = greenMail.getReceivedMessages()[0] + String token = "fake" + + when: + + url = "http://localhost:${localPort}/performPasswordReset" + def reset = ["userId": "randomuser@skills.org", "password": "newpassword", "resetToken": token] + headers = new HttpHeaders() + headers.setContentType(MediaType.APPLICATION_JSON) + entity = new HttpEntity(reset, headers) + template.postForEntity(url, entity, String.class) + + then: + thrown(HttpClientErrorException) + } + + def "reset with expired token fails"(){ + rootSkillsService.addOrUpdateGlobalSetting("password_reset_token_expiration", + ["setting": "password_reset_token_expiration", "value": "PT0.001S"]) + + SkillsService aUser = createService("randomuser@skills.org", "somepassword") + //post request with an unauthenticated client to ensure that the url is publicly available + RestTemplate template = new RestTemplate() + HttpHeaders headers = new HttpHeaders() + headers.setContentType(MediaType.MULTIPART_FORM_DATA) + MultiValueMap body = new LinkedMultiValueMap<>() + body.add("userId", "randomuser@skills.org") + HttpEntity entity = new HttpEntity(body, headers) + String url = "http://localhost:${localPort}/resetPassword" + template.postForEntity(url, entity, String.class) + + MimeMessage msg = greenMail.getReceivedMessages()[0] + + def match = msg.content.toString() =~ /href=".*\/reset-password\/([^"]+)"/ + String token = match[0][1] + Thread.currentThread().sleep(500) //wait for token to become invalid + + when: + url = "http://localhost:${localPort}/performPasswordReset" + def reset = ["userId": "randomuser@skills.org", "password": "newpassword", "resetToken": token] + headers = new HttpHeaders() + headers.setContentType(MediaType.APPLICATION_JSON) + entity = new HttpEntity(reset, headers) + template.postForEntity(url, entity, String.class) + + then: + thrown(HttpClientErrorException) + } + +} diff --git a/backend/src/test/java/skills/intTests/PointHistorySpecs.groovy b/service/src/test/java/skills/intTests/PointHistorySpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/PointHistorySpecs.groovy rename to service/src/test/java/skills/intTests/PointHistorySpecs.groovy diff --git a/backend/src/test/java/skills/intTests/ProjectNameSearchSpecs.groovy b/service/src/test/java/skills/intTests/ProjectNameSearchSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/ProjectNameSearchSpecs.groovy rename to service/src/test/java/skills/intTests/ProjectNameSearchSpecs.groovy diff --git a/service/src/test/java/skills/intTests/PublicConfigSpecs.groovy b/service/src/test/java/skills/intTests/PublicConfigSpecs.groovy new file mode 100644 index 00000000..6e5b482b --- /dev/null +++ b/service/src/test/java/skills/intTests/PublicConfigSpecs.groovy @@ -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. + */ +package skills.intTests + +import skills.intTests.utils.DefaultIntSpec + +class PublicConfigSpecs extends DefaultIntSpec { + + def "retrieve public configs"() { + when: + def config = skillsService.getPublicConfigs() + then: + config + config.descriptionMaxLength == "2000" + } + + def "public configs should return install mode"() { + when: + def config = skillsService.getPublicConfigs() + then: + config + config.authMode == "FORM" + } + + def "needToBootstrap should be true if root user doesn't exist"() { + when: + def noRootConfig = skillsService.getPublicConfigs() + skillsService.grantRoot() + def withRootConfig = skillsService.getPublicConfigs() + then: + noRootConfig + noRootConfig.needToBootstrap == true + + withRootConfig + withRootConfig.needToBootstrap == false + } + + def "public configs should include custom header/footer"() { + skillsService.grantRoot() + skillsService.saveSystemSettings("http://not-real.fakefakefake", "PT1H", "noreply@fakefakefake", "
    header
    ", "
    footer
    ") + when: + def configs = skillsService.getPublicConfigs() + + then: + configs.customHeader == "
    header
    " + configs.customFooter == "
    footer
    " + } +} diff --git a/backend/src/test/java/skills/intTests/RuleSetDeletionsSpecs.groovy b/service/src/test/java/skills/intTests/RuleSetDeletionsSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/RuleSetDeletionsSpecs.groovy rename to service/src/test/java/skills/intTests/RuleSetDeletionsSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/RuleSetManagementSpecs.groovy b/service/src/test/java/skills/intTests/RuleSetManagementSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/RuleSetManagementSpecs.groovy rename to service/src/test/java/skills/intTests/RuleSetManagementSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/SettingsSpecs.groovy b/service/src/test/java/skills/intTests/SettingsSpecs.groovy similarity index 61% rename from backend/src/test/java/skills/intTests/SettingsSpecs.groovy rename to service/src/test/java/skills/intTests/SettingsSpecs.groovy index 24642b99..e691f435 100644 --- a/backend/src/test/java/skills/intTests/SettingsSpecs.groovy +++ b/service/src/test/java/skills/intTests/SettingsSpecs.groovy @@ -16,9 +16,13 @@ package skills.intTests import org.springframework.http.HttpStatus +import org.springframework.web.client.HttpClientErrorException import skills.intTests.utils.DefaultIntSpec import skills.intTests.utils.SkillsClientException import skills.intTests.utils.SkillsFactory +import spock.lang.Timeout + +import java.util.regex.Pattern class SettingsSpecs extends DefaultIntSpec { @@ -191,4 +195,146 @@ class SettingsSpecs extends DefaultIntSpec { res1.settingGroup == "public_groupName1" } + def "save and retrieve email settings"() { + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + when: + skillsService.saveEmailSettings("somehost", "smtp", 1026, false, true, "fakeuser", "fakepassword") + def emailSettings = skillsService.getEmailSettings() + + then: + emailSettings.host == "somehost" + emailSettings.protocol == "smtp" + emailSettings.port == 1026 + emailSettings.tlsEnabled == false + emailSettings.authEnabled == true + emailSettings.username == "fakeuser" + emailSettings.password == "fakepassword" + } + + @Timeout(12) + def "save and retrieve email settings with invalid smtp server"() { + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + when: + skillsService.saveEmailSettings("somehost", "smtp", 1026, false, true, "fakeuser", "fakepassword") + def emailSettings = skillsService.getEmailSettings() + + then: + !emailSettings.success + } + + def "save and retrieve system settings"() { + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + when: + skillsService.saveSystemSettings("http://public", + "PT1H30M20S", + "foo@skilltree", "
    header
    ", "
    footer
    ") + def systemSettings = skillsService.getSystemSettings() + + then: + systemSettings.publicUrl == "http://public" + systemSettings.resetTokenExpiration == "PT1H30M20S" + systemSettings.fromEmail == "foo@skilltree" + systemSettings.customHeader == "
    header
    " + systemSettings.customFooter == "
    footer
    " + } + + def "save system settings with invalid token expiration duration"() { + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + when: + skillsService.saveSystemSettings("http://public", "1H30M20S", "foo@skilltree", "
    ", "
    ") + def systemSettings = skillsService.getSystemSettings() + + then: + def ex = thrown(SkillsClientException) + ex.message.contains("1H30M20S is not a valid duration") + } + + def "save custom header setting with script tag"(){ + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + when: + skillsService.saveSystemSettings("http://public", + "PT1H30M20S", + "foo@skilltree", + '
    ', + "
    ") + def systemSettings = skillsService.getSystemSettings() + + then: + def ex = thrown(SkillsClientException) + ex.message.contains("Script tags are not allowed in custom header") + } + + def "save custom footer setting with script tag"(){ + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + when: + skillsService.saveSystemSettings("http://public", + "PT1H30M20S", + "foo@skilltree", + '
    ', + "
    ") + def systemSettings = skillsService.getSystemSettings() + + then: + def ex = thrown(SkillsClientException) + ex.message.contains("Script tags are not allowed in custom footer") + } + + def "save custom header setting with value exceeding max"(){ + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + def header = (1..3001).collect{"A"}.join() + + when: + skillsService.saveSystemSettings("http://public", + "PT1H30M20S", + "foo@skilltree", + header, + "
    ") + def systemSettings = skillsService.getSystemSettings() + + then: + def ex = thrown(SkillsClientException) + ex.message.contains("Custom Header may not be longer than [3000]") + } + + def "save custom footer setting with value exceeding max"(){ + if (!skillsService.isRoot()) { + skillsService.grantRoot() + } + + def header = (1..3001).collect{"A"}.join() + + when: + skillsService.saveSystemSettings("http://public", + "PT1H30M20S", + "foo@skilltree", + '
    ', + header) + def systemSettings = skillsService.getSystemSettings() + + then: + def ex = thrown(SkillsClientException) + ex.message.contains("Custom Footer may not be longer than [3000]") + } + } diff --git a/backend/src/test/java/skills/intTests/SkillOccurrencesSpecs.groovy b/service/src/test/java/skills/intTests/SkillOccurrencesSpecs.groovy similarity index 90% rename from backend/src/test/java/skills/intTests/SkillOccurrencesSpecs.groovy rename to service/src/test/java/skills/intTests/SkillOccurrencesSpecs.groovy index 3015a578..0fa45baf 100644 --- a/backend/src/test/java/skills/intTests/SkillOccurrencesSpecs.groovy +++ b/service/src/test/java/skills/intTests/SkillOccurrencesSpecs.groovy @@ -893,7 +893,9 @@ class SkillOccurrencesSpecs extends DefaultIntSpec { List afterAchievements = userAchievementRepo.findAll() then: - beforeAchievements.size() == afterAchievements.size() - 1 // 1 achievement should be added + //1 skill achievement should be added, 1 subject level achievement should be added, one project level achievement should be added + beforeAchievements.size() == afterAchievements.size() - 3 + afterAchievements.findAll { it.notified == 'false' }.size() == 3 !beforeAchievements.findAll { it.projectId == proj1.projectId && it.skillId == proj1_skills.get(1).skillId && it.userId == userId1 } afterAchievements.find { it.projectId == proj1.projectId && it.skillId == proj1_skills.get(1).skillId && it.userId == userId1 } @@ -987,7 +989,9 @@ class SkillOccurrencesSpecs extends DefaultIntSpec { List afterAchievements = userAchievementRepo.findAll() then: - beforeAchievements.size() == afterAchievements.size() - 1 // 1 achievement should be added + // 1 skill achievement should be added, 2 subject level achievements, and 2 project level achievements + beforeAchievements.size() == afterAchievements.size() - 5 + afterAchievements.findAll { it.notified == 'false' }.size() == 5 !beforeAchievements.findAll { it.projectId == proj1.projectId && it.skillId == proj1_skills.get(1).skillId && it.userId == userId1 } afterAchievements.find { it.projectId == proj1.projectId && it.skillId == proj1_skills.get(1).skillId && it.userId == userId1 } @@ -1022,4 +1026,106 @@ class SkillOccurrencesSpecs extends DefaultIntSpec { getPointHistory(userId4, proj2.projectId) == [10, 20, 50] getPointHistory(userId4, proj2.projectId, proj2_subj.subjectId) == [10, 20, 50] } + + def "decrease in occurrences causes project and subject level achievements"() { + def project = SkillsFactory.createProject() + skillsService.createProject(project) + def subject = SkillsFactory.createSubject(1) + skillsService.createSubject(subject) + def subject2 = SkillsFactory.createSubject(1, 2) + skillsService.createSubject(subject2) + + + def skill1_1 = SkillsFactory.createSkill(1, 1, 1, 0, 10, 0, 100) + skillsService.createSkill(skill1_1) + def skill1_2 = SkillsFactory.createSkill(1, 1, 2, 0, 10, 0, 10) + skillsService.createSkill(skill1_2) + def skill2_1 = SkillsFactory.createSkill(1, 2, 1, 0, 1, 0, 100) + skillsService.createSkill(skill2_1) + + when: + skillsService.addSkill([projectId: project.projectId, skillId: skill1_1.skillId], "user1", new Date()) + skillsService.addSkill([projectId: project.projectId, skillId: skill1_2.skillId], "user2", new Date()) + def beforeEditAchievements = userAchievementRepo.findAllByUserAndProjectIds("user1", [project.projectId]) + def beforeEditAchievementsU2 = userAchievementRepo.findAllByUserAndProjectIds("user2", [project.projectId]) + skill1_1.numPerformToCompletion = 1 + skillsService.updateSkill(skill1_1, skill1_1.skillId) + def afterEditAchievements = userAchievementRepo.findAllByUserAndProjectIds("user1", [project.projectId]) + def afterEditAchievementsU2 = userAchievementRepo.findAllByUserAndProjectIds("user2", [project.projectId]) + //1300 total, levels: + //1: 130 + //2: 325 + //3: 585 + //4: 871 + //5: 1196 + + //300 total, levels: + //1: 30 + //2: 75 + //3: 135 + //4: 201 + //5: 276 + + then: + beforeEditAchievements.size() == 0 + afterEditAchievements.size() == 6 + afterEditAchievements.findAll { it.notified == 'false' }.size() == 6 + afterEditAchievements.find { it.projectId == project.projectId && it.skillId == 'skill1' } + afterEditAchievements.find { it.projectId == project.projectId && !it.skillId && it.level == 1 } + afterEditAchievements.find { it.projectId == project.projectId && !it.skillId && it.level == 2 } + afterEditAchievements.find { it.projectId == project.projectId && !it.skillId && it.level == 1 } + afterEditAchievements.find { it.projectId == project.projectId && it.skillId == subject.subjectId && it.level == 1 } + afterEditAchievements.find { it.projectId == project.projectId && it.skillId == subject.subjectId && it.level == 2 } + afterEditAchievements.find { it.projectId == project.projectId && it.skillId == subject.subjectId && it.level == 3 } + beforeEditAchievementsU2.size() == 0 + afterEditAchievementsU2.size() == 0 + } + + def "decrease in occurrences causes project and subject level achievements for points based levels"() { + def project = SkillsFactory.createProject() + skillsService.createProject(project) + + def subject2 = SkillsFactory.createSubject(1, 2) + skillsService.createSubject(subject2) + def skill2_1 = SkillsFactory.createSkill(1, 2, 1, 0, 1, 0, 100) + skillsService.createSkill(skill2_1) + + def subject = SkillsFactory.createSubject(1) + skillsService.createSubject(subject) + def skill1_1 = SkillsFactory.createSkill(1, 1, 1, 0, 10, 0, 100) + skillsService.createSkill(skill1_1) + def skill1_2 = SkillsFactory.createSkill(1, 1, 2, 0, 10, 0, 10) + skillsService.createSkill(skill1_2) + + String projectPointsSetting = "level.points.enabled" + skillsService.changeSetting(project.projectId, projectPointsSetting, [projectId: project.projectId, setting: projectPointsSetting, value: "true"]) + + + when: + skillsService.addSkill([projectId: project.projectId, skillId: skill1_1.skillId], "user1", new Date()) + skillsService.addSkill([projectId: project.projectId, skillId: skill1_2.skillId], "user2", new Date()) + def beforeEditAchievements = userAchievementRepo.findAllByUserAndProjectIds("user1", [project.projectId]) + def beforeEditAchievementsU2 = userAchievementRepo.findAllByUserAndProjectIds("user2", [project.projectId]) + skill1_1.numPerformToCompletion = 1 + skillsService.updateSkill(skill1_1, skill1_1.skillId) + + def afterEditAchievements = userAchievementRepo.findAllByUserAndProjectIds("user1", [project.projectId]) + def afterEditAchievementsU2 = userAchievementRepo.findAllByUserAndProjectIds("user2", [project.projectId]) + + then: + beforeEditAchievements.size() == 0 + afterEditAchievements.size() == 1 + afterEditAchievements.findAll { it.notified == 'false' }.size() == 1 + afterEditAchievements.find { it.projectId == project.projectId && it.skillId == 'skill1' } + //with change to points based levels, once the occurrences are deleted, the pointsFrom for levels still exceed the user's + //points + !afterEditAchievements.find { it.projectId == project.projectId && !it.skillId && it.level == 1 } + !afterEditAchievements.find { it.projectId == project.projectId && !it.skillId && it.level == 2 } + !afterEditAchievements.find { it.projectId == project.projectId && !it.skillId && it.level == 1 } + !afterEditAchievements.find { it.projectId == project.projectId && it.skillId == subject.subjectId && it.level == 1 } + !afterEditAchievements.find { it.projectId == project.projectId && it.skillId == subject.subjectId && it.level == 2 } + !afterEditAchievements.find { it.projectId == project.projectId && it.skillId == subject.subjectId && it.level == 3 } + beforeEditAchievementsU2.size() == 0 + afterEditAchievementsU2.size() == 0 + } } diff --git a/backend/src/test/java/skills/intTests/StatusCheckSpecs.groovy b/service/src/test/java/skills/intTests/StatusCheckSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/StatusCheckSpecs.groovy rename to service/src/test/java/skills/intTests/StatusCheckSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/SuggestUsersSpecs.groovy b/service/src/test/java/skills/intTests/SuggestUsersSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/SuggestUsersSpecs.groovy rename to service/src/test/java/skills/intTests/SuggestUsersSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/TinyAmountOfPointsSpecs.groovy b/service/src/test/java/skills/intTests/TinyAmountOfPointsSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/TinyAmountOfPointsSpecs.groovy rename to service/src/test/java/skills/intTests/TinyAmountOfPointsSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/UserAdminMetricsSpec.groovy b/service/src/test/java/skills/intTests/UserAdminMetricsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/UserAdminMetricsSpec.groovy rename to service/src/test/java/skills/intTests/UserAdminMetricsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/UserInfoSettingsSpecs.groovy b/service/src/test/java/skills/intTests/UserInfoSettingsSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/UserInfoSettingsSpecs.groovy rename to service/src/test/java/skills/intTests/UserInfoSettingsSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/UserInfoSpecs.groovy b/service/src/test/java/skills/intTests/UserInfoSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/UserInfoSpecs.groovy rename to service/src/test/java/skills/intTests/UserInfoSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/UserPointsSpecs.groovy b/service/src/test/java/skills/intTests/UserPointsSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/UserPointsSpecs.groovy rename to service/src/test/java/skills/intTests/UserPointsSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/UserRoleSpecs.groovy b/service/src/test/java/skills/intTests/UserRoleSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/UserRoleSpecs.groovy rename to service/src/test/java/skills/intTests/UserRoleSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/UserTokenEndpointsSpec.groovy b/service/src/test/java/skills/intTests/UserTokenEndpointsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/UserTokenEndpointsSpec.groovy rename to service/src/test/java/skills/intTests/UserTokenEndpointsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/ValidationSpecs.groovy b/service/src/test/java/skills/intTests/ValidationSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/ValidationSpecs.groovy rename to service/src/test/java/skills/intTests/ValidationSpecs.groovy diff --git a/service/src/test/java/skills/intTests/WebsocketSpecs.groovy b/service/src/test/java/skills/intTests/WebsocketSpecs.groovy new file mode 100644 index 00000000..c55f4534 --- /dev/null +++ b/service/src/test/java/skills/intTests/WebsocketSpecs.groovy @@ -0,0 +1,300 @@ +/** + * 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 + +import groovy.util.logging.Slf4j +import org.springframework.lang.Nullable +import org.springframework.messaging.converter.MappingJackson2MessageConverter +import org.springframework.messaging.simp.stomp.StompHeaders +import org.springframework.messaging.simp.stomp.StompSession +import org.springframework.messaging.simp.stomp.StompSessionHandler +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter +import org.springframework.web.socket.WebSocketHttpHeaders +import org.springframework.web.socket.client.standard.StandardWebSocketClient +import org.springframework.web.socket.messaging.WebSocketStompClient +import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport +import org.springframework.web.socket.sockjs.client.SockJsClient +import org.springframework.web.socket.sockjs.client.Transport +import org.springframework.web.socket.sockjs.client.WebSocketTransport +import skills.intTests.utils.DefaultIntSpec +import skills.intTests.utils.SkillsFactory +import skills.intTests.utils.TestUtils +import skills.services.events.CompletionItem +import skills.services.events.SkillEventResult + +import java.lang.reflect.Type +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Slf4j +class WebsocketSpecs extends DefaultIntSpec { + + TestUtils testUtils = new TestUtils() + String projId = SkillsFactory.defaultProjId + List sampleUserIds // loaded from system props + StompSession stompSession + List subj1, subj2, subj3 + + def setup() { + skillsService.deleteProjectIfExist(projId) + sampleUserIds = System.getProperty("sampleUserIds", "tom|||dick|||harry")?.split("\\|\\|\\|").sort() + subj1 = (1..5).collect { [projectId: projId, subjectId: "subj1", skillId: "s1${it}".toString(), name: "subj1 ${it}".toString(), type: "Skill", pointIncrement: 10, numPerformToCompletion: 10, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } + subj2 = (1..4).collect { [projectId: projId, subjectId: "subj2", skillId: "s2${it}".toString(), name: "subj2 ${it}".toString(), type: "Skill", pointIncrement: 5, numPerformToCompletion: 10, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } + subj3 = (1..5).collect { [projectId: projId, subjectId: "subj3", skillId: "s3${it}".toString(), name: "subj3 ${it}".toString(), type: "Skill", pointIncrement: 20, numPerformToCompletion: 10, totalPoints: 200, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } + skillsService.createSchema([subj1, subj2, subj3]) + } + + def cleanup() { + stompSession?.disconnect() + } + + def "achieve subject's level - validate via websocket"(){ + given: + List subjSummaryRes = [] + List wsResults = [] + boolean skillsAdded = false + CountDownLatch messagesReceived = setupWebsocketConnection(wsResults) + + when: + List dates = testUtils.getLastNDays(5) + List addSkillRes = [] + + (0..4).each { + addSkillRes << skillsService.addSkill([projectId: projId, skillId: subj1.get(it).skillId], sampleUserIds.get(0), dates.get(it)) + subjSummaryRes << skillsService.getSkillSummary(sampleUserIds.get(0), projId, subj1.get(it).subjectId) + } + skillsAdded = true + messagesReceived.await(30, TimeUnit.SECONDS) + then: + interaction { + if (skillsAdded) { // interaction closure seemed to be getting called before the "when:" block + validateResults(subjSummaryRes, wsResults) + } + } + } + + def "Non-notified badge achievements are notified when user connects to websocket" () { + given: + + def badge = SkillsFactory.createBadge() + badge.enabled = false + skillsService.createBadge(badge) + skillsService.assignSkillToBadge([projectId: projId, badgeId: badge.badgeId, skillId: subj1.get(0).skillId]) + skillsService.assignSkillToBadge([projectId: projId, badgeId: badge.badgeId, skillId: subj2.get(0).skillId]) + + (0..9).each { + skillsService.addSkill([projectId: projId, skillId: subj1.get(0).skillId], 'skills@skills.org', new Date()-it) + skillsService.addSkill([projectId: projId, skillId: subj2.get(0).skillId], 'skills@skills.org', new Date()-it) + } + + skillsService.updateBadge([projectId: projId, badgeId: badge.badgeId, enabled: true, name: badge.name], badge.badgeId) + + List wsResults = [] + + when: + CountDownLatch messagesReceived = setupWebsocketConnection(wsResults, false, false, 1, 'skills@skills.org') + messagesReceived.await(30, TimeUnit.SECONDS) + + then: + wsResults.find{it.skillId=='badge1'}.success + wsResults.find{it.skillId=='badge1'}.completed + wsResults.find{it.skillId=='badge1'}.completed.size() == 1 + wsResults.find{it.skillId=='badge1'}.completed[0].type == CompletionItem.CompletionItemType.Badge + wsResults.find{it.skillId=='badge1'}.completed[0].name == badge.name + } + + def "non-notified skill achievements are notified when user connects to websocket" () { + def subj = SkillsFactory.createSubject(1, 1) + def skill = SkillsFactory.createSkill(1, 1, 1, 0, 4, 0, 150) + skillsService.createSubject(subj) + skillsService.createSkill(skill) + + skillsService.addSkill([projectId: projId, skillId: skill.skillId], 'skills@skills.org', new Date()) + + + skill.numPerformToCompletion = 1 + skillsService.updateSkill(skill, skill.skillId) + + List wsResults = [] + + when: + CountDownLatch messagesReceived = setupWebsocketConnection(wsResults, false, false, 1, 'skills@skills.org') + messagesReceived.await(30, TimeUnit.SECONDS) + + then: + wsResults[0].success + wsResults[0].completed + wsResults[0].explanation == 'Achieved due to a modification in the training profile (such as: skill deleted, occurrences modified, badge published, etc..)' + wsResults[0].completed.size() == 4 + wsResults[0].completed?.find{it.id=='skill1'}.type == CompletionItem.CompletionItemType.Skill + wsResults[0].completed?.find{it.id=='skill1'}.name == skill.name + wsResults[0].completed?.findAll { it.type == CompletionItem.CompletionItemType.Subject }.size() == 3 + } + + def "achieve subject's level - validate via xhr streaming"(){ + given: + List subjSummaryRes = [] + List wsResults = [] + boolean skillsAdded = false + CountDownLatch messagesReceived = setupWebsocketConnection(wsResults, true) + + when: + List dates = testUtils.getLastNDays(5) + List addSkillRes = [] + (0..4).each { + addSkillRes << skillsService.addSkill([projectId: projId, skillId: subj1.get(it).skillId], sampleUserIds.get(0), dates.get(it)) + subjSummaryRes << skillsService.getSkillSummary(sampleUserIds.get(0), projId, subj1.get(it).subjectId) + } + skillsAdded = true + messagesReceived.await(30, TimeUnit.SECONDS) + + then: + interaction { + if (skillsAdded) { // interaction closure seemed to be getting called before the "when:" block + validateResults(subjSummaryRes, wsResults) + } + } + } + + def "achieve subject's level - validate via xhr polling"(){ + given: + List subjSummaryRes = [] + List wsResults = [] + boolean skillsAdded = false + CountDownLatch messagesReceived = setupWebsocketConnection(wsResults, true, true) + + when: + List dates = testUtils.getLastNDays(5) + List addSkillRes = [] + (0..4).each { + addSkillRes << skillsService.addSkill([projectId: projId, skillId: subj1.get(it).skillId], sampleUserIds.get(0), dates.get(it)) + subjSummaryRes << skillsService.getSkillSummary(sampleUserIds.get(0), projId, subj1.get(it).subjectId) + } + skillsAdded = true + messagesReceived.await(30, TimeUnit.SECONDS) + + then: + interaction { + if (skillsAdded) { // interaction closure seemed to be getting called before the "when:" block + validateResults(subjSummaryRes, wsResults) + } + } + } + + + + private CountDownLatch setupWebsocketConnection(List wsResults, boolean xhr=false, boolean xhrPolling=false, int count=5, String userId=null) { + CountDownLatch messagesReceived = new CountDownLatch(count) + List transports = [] + if (xhr) { + RestTemplateXhrTransport xhrTransport = new RestTemplateXhrTransport() + xhrTransport.xhrStreamingDisabled = xhrPolling + transports.add(xhrTransport) + } else { + transports.add(new WebSocketTransport(new StandardWebSocketClient())) + } + SockJsClient sockJsClient = new SockJsClient(transports) + WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient) + stompClient.setMessageConverter(new MappingJackson2MessageConverter()) + StompSessionHandler sessionHandler = new StompSessionHandlerAdapter() { + @Override + Type getPayloadType(StompHeaders headers) { + return SkillEventResult + } + + @Override + void handleFrame(StompHeaders headers, @Nullable Object payload) { + SkillEventResult result = (SkillEventResult) payload + wsResults.add(result) + messagesReceived.countDown() + } + + @Override + void afterConnected(StompSession session, StompHeaders connectedHeaders) { + session.subscribe("/user/queue/${projId}-skill-updates", this) + } + } + + // setup websocket connection for sampleUserIds[0] + if(userId == null) { + userId = sampleUserIds.get(0) + } + String secret = skillsService.getClientSecret(projId) + skillsService.setProxyCredentials(projId, secret) + String token = skillsService.wsHelper.getTokenForUser(userId) + WebSocketHttpHeaders headers = new WebSocketHttpHeaders() + headers.add('Authorization', "Bearer ${token}") + String protocol = xhr ? 'http' : 'ws' + stompSession = stompClient.connect("${protocol}://localhost:${localPort}/skills-websocket", headers, sessionHandler).get() + return messagesReceived + } + + private validateResults(List subjSummaryRes, List wsResults) { + wsResults.sort {it.skillId} + wsResults.each { + assert it.skillApplied + assert it.explanation == "Skill event was applied" + } + !wsResults.get(0).completed + wsResults.get(0).skillId == "${subj1.get(0).skillId}" + wsResults.get(0).name == "${subj1.get(0).name}" + wsResults.get(0).pointsEarned == 10 + subjSummaryRes.get(0).skillsLevel == 0 + subjSummaryRes.get(0).points == 10 + subjSummaryRes.get(0).todaysPoints == 0 + subjSummaryRes.get(0).levelPoints == 10 + + !wsResults.get(1).completed + wsResults.get(1).skillId == "${subj1.get(1).skillId}" + wsResults.get(1).name == "${subj1.get(1).name}" + wsResults.get(1).pointsEarned == 10 + subjSummaryRes.get(1).skillsLevel == 0 + subjSummaryRes.get(1).points == 20 + subjSummaryRes.get(1).todaysPoints == 0 + subjSummaryRes.get(1).levelPoints == 20 + + !wsResults.get(2).completed + wsResults.get(2).skillId == "${subj1.get(2).skillId}" + wsResults.get(2).name == "${subj1.get(2).name}" + wsResults.get(2).pointsEarned == 10 + subjSummaryRes.get(2).skillsLevel == 0 + subjSummaryRes.get(2).points == 30 + subjSummaryRes.get(2).todaysPoints == 0 + subjSummaryRes.get(2).levelPoints == 30 + + !wsResults.get(3).completed + wsResults.get(3).skillId == "${subj1.get(3).skillId}" + wsResults.get(3).name == "${subj1.get(3).name}" + wsResults.get(3).pointsEarned == 10 + subjSummaryRes.get(3).skillsLevel == 0 + subjSummaryRes.get(3).points == 40 + subjSummaryRes.get(3).todaysPoints == 0 + subjSummaryRes.get(3).levelPoints == 40 + + wsResults.get(4).completed.size() == 1 + wsResults.get(4).skillId == "${subj1.get(4).skillId}" + wsResults.get(4).name == "${subj1.get(4).name}" + wsResults.get(4).pointsEarned == 10 + wsResults.get(4).completed.get(0).type == CompletionItem.CompletionItemType.Subject + wsResults.get(4).completed.get(0).level == 1 + wsResults.get(4).completed.get(0).id == "subj1" + + subjSummaryRes.get(4).skillsLevel == 1 + subjSummaryRes.get(4).points == 50 + subjSummaryRes.get(4).todaysPoints == 10 + subjSummaryRes.get(4).levelPoints == 0 + } +} diff --git a/backend/src/test/java/skills/intTests/adminDisplayOrder/BadgesOrderSpecs.groovy b/service/src/test/java/skills/intTests/adminDisplayOrder/BadgesOrderSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/adminDisplayOrder/BadgesOrderSpecs.groovy rename to service/src/test/java/skills/intTests/adminDisplayOrder/BadgesOrderSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/adminDisplayOrder/GlobalBadgeOrderSpecs.groovy b/service/src/test/java/skills/intTests/adminDisplayOrder/GlobalBadgeOrderSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/adminDisplayOrder/GlobalBadgeOrderSpecs.groovy rename to service/src/test/java/skills/intTests/adminDisplayOrder/GlobalBadgeOrderSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/adminDisplayOrder/SkillsOrderSpecs.groovy b/service/src/test/java/skills/intTests/adminDisplayOrder/SkillsOrderSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/adminDisplayOrder/SkillsOrderSpecs.groovy rename to service/src/test/java/skills/intTests/adminDisplayOrder/SkillsOrderSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/adminDisplayOrder/SubjectsOrderSpecs.groovy b/service/src/test/java/skills/intTests/adminDisplayOrder/SubjectsOrderSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/adminDisplayOrder/SubjectsOrderSpecs.groovy rename to service/src/test/java/skills/intTests/adminDisplayOrder/SubjectsOrderSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/badges/GlobalBadgeSpecs.groovy b/service/src/test/java/skills/intTests/badges/GlobalBadgeSpecs.groovy similarity index 80% rename from backend/src/test/java/skills/intTests/badges/GlobalBadgeSpecs.groovy rename to service/src/test/java/skills/intTests/badges/GlobalBadgeSpecs.groovy index 3335218e..e4dec0ce 100644 --- a/backend/src/test/java/skills/intTests/badges/GlobalBadgeSpecs.groovy +++ b/service/src/test/java/skills/intTests/badges/GlobalBadgeSpecs.groovy @@ -17,6 +17,7 @@ package skills.intTests.badges import skills.intTests.utils.DefaultIntSpec +import skills.intTests.utils.SkillsClientException import skills.intTests.utils.SkillsFactory import skills.intTests.utils.SkillsService @@ -77,6 +78,7 @@ class GlobalBadgeSpecs extends DefaultIntSpec { skillsService.createSubject(subj2) skillsService.createSkills(skills) skillsService.createSkills(subj2Skills) + badge.enabled = 'true' supervisorService.createGlobalBadge(badge) supervisorService.assignProjectLevelToGlobalBadge(projectId: proj.projectId, badgeId: badge.badgeId, level: "1") @@ -90,4 +92,27 @@ class GlobalBadgeSpecs extends DefaultIntSpec { result.body.completed.find{ it.type == 'GlobalBadge' } } + + def "cannot disable a badge after it has been enabled"(){ + def proj = SkillsFactory.createProject() + def subj = SkillsFactory.createSubject() + def skills = SkillsFactory.createSkills(4) + def badge = SkillsFactory.createBadge() + badge.enabled = 'true' + + skillsService.createProject(proj) + skillsService.createSubject(subj) + skillsService.createSkills(skills) + + supervisorService.createGlobalBadge(badge) + + when: + badge = supervisorService.getGlobalBadge(badge.badgeId) + badge.enabled = 'false' + supervisorService.createGlobalBadge(badge) + + then: + SkillsClientException ex = thrown() + ex.getMessage().contains("Once a Badge has been published, the only allowable value for enabled is [true]") + } } diff --git a/backend/src/test/java/skills/intTests/badges/IsReferencesByGlobalBadgeSpecs.groovy b/service/src/test/java/skills/intTests/badges/IsReferencesByGlobalBadgeSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/badges/IsReferencesByGlobalBadgeSpecs.groovy rename to service/src/test/java/skills/intTests/badges/IsReferencesByGlobalBadgeSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayBadgesSpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplayBadgesSpec.groovy similarity index 83% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayBadgesSpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplayBadgesSpec.groovy index 50a33ab0..6ed17a4e 100644 --- a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayBadgesSpec.groovy +++ b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplayBadgesSpec.groovy @@ -18,9 +18,25 @@ package skills.intTests.clientDisplay import skills.intTests.utils.DefaultIntSpec import skills.intTests.utils.SkillsFactory +import skills.intTests.utils.SkillsService class ClientDisplayBadgesSpec extends DefaultIntSpec { + String ultimateRoot = 'jh@dojo.com' + SkillsService rootSkillsService + String supervisorUserId = 'foo@bar.com' + SkillsService supervisorSkillsService + + def setup(){ + rootSkillsService = createService(ultimateRoot, 'aaaaaaaa') + supervisorSkillsService = createService(supervisorUserId) + + if (!rootSkillsService.isRoot()) { + rootSkillsService.grantRoot() + } + rootSkillsService.grantSupervisorRole(supervisorUserId) + } + def "badges summary for a project - one badge"() { String userId = "user1" @@ -503,4 +519,83 @@ class ClientDisplayBadgesSpec extends DefaultIntSpec { summaries.get(0).numSkillsAchieved == 1 summaries.get(0).numTotalSkills == 5 } + + def "user badge achievement should not leak into another project"() { + String userId = "user1" + String userId2 = "user2" + String badge1 = "badge1" + + // proj1 + def proj1 = SkillsFactory.createProject(1) + def proj1_subj = SkillsFactory.createSubject(1, 1) + List proj1_skills = SkillsFactory.createSkills(2, 1, 1) + proj1_skills.get(0).pointIncrement=100 + + skillsService.createProject(proj1) + skillsService.createSubject(proj1_subj) + skillsService.createSkills(proj1_skills) + + skillsService.addBadge([projectId: proj1.projectId, badgeId: badge1, name: 'Badge 1', description: 'This is a first badge', iconClass: "fa fa-seleted-icon",]) + skillsService.assignSkillToBadge([projectId: proj1.projectId, badgeId: badge1, skillId: proj1_skills.get(0).skillId]) + + // proj2 + def proj2 = SkillsFactory.createProject(2) + def proj2_subj = SkillsFactory.createSubject(2, 1) + List proj2_skills = SkillsFactory.createSkills(2, 2, 1) + proj2_skills.get(0).pointIncrement=100 + + skillsService.createProject(proj2) + skillsService.createSubject(proj2_subj) + skillsService.createSkills(proj2_skills) + + skillsService.addBadge([projectId: proj2.projectId, badgeId: badge1, name: 'Badge 1', description: 'This is a first badge', iconClass: "fa fa-seleted-icon",]) + skillsService.assignSkillToBadge([projectId: proj2.projectId, badgeId: badge1, skillId: proj2_skills.get(0).skillId]) + + // global badge + Map badge = [badgeId: "globalBadge", name: 'Badge 1', description: 'This is a first badge', iconClass: "fa fa-seleted-icon"] + badge.helpUrl = "http://foo.org" + supervisorSkillsService.createGlobalBadge(badge) + supervisorSkillsService.assignSkillToGlobalBadge(projectId: proj1.projectId, badgeId: badge.badgeId, skillId: proj1_skills.get(0).skillId) + + // add skill + skillsService.addSkill([projectId: proj1.projectId, skillId: proj1_skills.get(0).skillId], userId, new Date()) + skillsService.addSkill([projectId: proj2.projectId, skillId: proj2_skills.get(1).skillId], userId, new Date()) + + skillsService.addSkill([projectId: proj1.projectId, skillId: proj1_skills.get(1).skillId], userId2, new Date()) + skillsService.addSkill([projectId: proj2.projectId, skillId: proj2_skills.get(0).skillId], userId2, new Date()) + + when: + def summary = skillsService.getSkillSummary(userId, proj1.projectId) + def summaries = skillsService.getBadgesSummary(userId, proj1.projectId) + + def summary2 = skillsService.getSkillSummary(userId, proj2.projectId) + def summaries2 = skillsService.getBadgesSummary(userId, proj2.projectId) + + def summaryUser2 = skillsService.getSkillSummary(userId2, proj1.projectId) + def summariesUser2 = skillsService.getBadgesSummary(userId2, proj1.projectId) + + def summary2User2 = skillsService.getSkillSummary(userId2, proj2.projectId) + def summaries2User2 = skillsService.getBadgesSummary(userId2, proj2.projectId) + + then: + // user 1 + summary.badges.numBadgesCompleted == 2 + summaries.size() == 2 + summaries.find { it.badgeId == "globalBadge" }.numSkillsAchieved == 1 + summaries.find { it.badgeId == "badge1" }.numSkillsAchieved == 1 + + summary2.badges.numBadgesCompleted == 1 + summaries2.size() == 1 + summaries2.find { it.badgeId == "badge1" }.numSkillsAchieved == 0 + + // user 2 + summaryUser2.badges.numBadgesCompleted == 0 + summariesUser2.size() == 2 + summariesUser2.find { it.badgeId == "globalBadge" }.numSkillsAchieved == 0 + summariesUser2.find { it.badgeId == "badge1" }.numSkillsAchieved == 0 + + summary2User2.badges.numBadgesCompleted == 1 + summaries2User2.size() == 1 + summaries2User2.find { it.badgeId == "badge1" }.numSkillsAchieved == 1 + } } diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayGlobalBadgesSpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplayGlobalBadgesSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayGlobalBadgesSpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplayGlobalBadgesSpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayLevelsSpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplayLevelsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayLevelsSpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplayLevelsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankDistributionSpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankDistributionSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankDistributionSpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankDistributionSpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankSpecs.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankSpecs.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplayRankSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplaySpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplaySpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplaySpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplaySpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/ClientDisplaySubjSummarySpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/ClientDisplaySubjSummarySpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/ClientDisplaySubjSummarySpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/ClientDisplaySubjSummarySpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/SingleSkillSummarySpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/SingleSkillSummarySpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/SingleSkillSummarySpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/SingleSkillSummarySpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/SkillsDescriptionSpec.groovy b/service/src/test/java/skills/intTests/clientDisplay/SkillsDescriptionSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/SkillsDescriptionSpec.groovy rename to service/src/test/java/skills/intTests/clientDisplay/SkillsDescriptionSpec.groovy diff --git a/backend/src/test/java/skills/intTests/clientDisplay/UserLevelSpecs.groovy b/service/src/test/java/skills/intTests/clientDisplay/UserLevelSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/clientDisplay/UserLevelSpecs.groovy rename to service/src/test/java/skills/intTests/clientDisplay/UserLevelSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/crossProject/CrossProjectClientDisplaySpec.groovy b/service/src/test/java/skills/intTests/crossProject/CrossProjectClientDisplaySpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/crossProject/CrossProjectClientDisplaySpec.groovy rename to service/src/test/java/skills/intTests/crossProject/CrossProjectClientDisplaySpec.groovy diff --git a/backend/src/test/java/skills/intTests/crossProject/CrossProjectDepsAndAchievementsSpec.groovy b/service/src/test/java/skills/intTests/crossProject/CrossProjectDepsAndAchievementsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/crossProject/CrossProjectDepsAndAchievementsSpec.groovy rename to service/src/test/java/skills/intTests/crossProject/CrossProjectDepsAndAchievementsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/crossProject/CrossProjectSkillsManagementSpec.groovy b/service/src/test/java/skills/intTests/crossProject/CrossProjectSkillsManagementSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/crossProject/CrossProjectSkillsManagementSpec.groovy rename to service/src/test/java/skills/intTests/crossProject/CrossProjectSkillsManagementSpec.groovy diff --git a/backend/src/test/java/skills/intTests/dependentSkills/AdminDepManagementSpecs.groovy b/service/src/test/java/skills/intTests/dependentSkills/AdminDepManagementSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/dependentSkills/AdminDepManagementSpecs.groovy rename to service/src/test/java/skills/intTests/dependentSkills/AdminDepManagementSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/dependentSkills/AdminGraphDisplaySpec.groovy b/service/src/test/java/skills/intTests/dependentSkills/AdminGraphDisplaySpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/dependentSkills/AdminGraphDisplaySpec.groovy rename to service/src/test/java/skills/intTests/dependentSkills/AdminGraphDisplaySpec.groovy diff --git a/backend/src/test/java/skills/intTests/dependentSkills/ClientDisplayOfDependentSkillsSpec.groovy b/service/src/test/java/skills/intTests/dependentSkills/ClientDisplayOfDependentSkillsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/dependentSkills/ClientDisplayOfDependentSkillsSpec.groovy rename to service/src/test/java/skills/intTests/dependentSkills/ClientDisplayOfDependentSkillsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/dependentSkills/ReportSkills_DependentSkillsSpecs.groovy b/service/src/test/java/skills/intTests/dependentSkills/ReportSkills_DependentSkillsSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/dependentSkills/ReportSkills_DependentSkillsSpecs.groovy rename to service/src/test/java/skills/intTests/dependentSkills/ReportSkills_DependentSkillsSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/metrics/MetricsSpec.groovy b/service/src/test/java/skills/intTests/metrics/MetricsSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/metrics/MetricsSpec.groovy rename to service/src/test/java/skills/intTests/metrics/MetricsSpec.groovy diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkillsEdgeCasesSpecs.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkillsEdgeCasesSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/reportSkills/ReportSkillsEdgeCasesSpecs.groovy rename to service/src/test/java/skills/intTests/reportSkills/ReportSkillsEdgeCasesSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkillsSpecs.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkillsSpecs.groovy similarity index 88% rename from backend/src/test/java/skills/intTests/reportSkills/ReportSkillsSpecs.groovy rename to service/src/test/java/skills/intTests/reportSkills/ReportSkillsSpecs.groovy index dfd799ff..85772b98 100644 --- a/backend/src/test/java/skills/intTests/reportSkills/ReportSkillsSpecs.groovy +++ b/service/src/test/java/skills/intTests/reportSkills/ReportSkillsSpecs.groovy @@ -17,31 +17,10 @@ package skills.intTests.reportSkills import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired -import org.springframework.lang.Nullable -import org.springframework.messaging.converter.MappingJackson2MessageConverter -import org.springframework.messaging.simp.stomp.StompHeaders -import org.springframework.messaging.simp.stomp.StompSession -import org.springframework.messaging.simp.stomp.StompSessionHandler -import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter -import org.springframework.web.socket.WebSocketHttpHeaders -import org.springframework.web.socket.client.WebSocketClient -import org.springframework.web.socket.client.standard.StandardWebSocketClient -import org.springframework.web.socket.messaging.WebSocketStompClient -import org.springframework.web.socket.sockjs.client.SockJsClient -import org.springframework.web.socket.sockjs.client.Transport -import org.springframework.web.socket.sockjs.client.WebSocketTransport -import skills.intTests.utils.DefaultIntSpec -import skills.intTests.utils.SkillsClientException -import skills.intTests.utils.SkillsFactory -import skills.intTests.utils.SkillsService -import skills.intTests.utils.TestUtils -import skills.services.events.CompletionItem -import skills.services.events.SkillEventResult +import skills.intTests.utils.* import skills.storage.model.UserAchievement import skills.storage.repos.UserAchievedLevelRepo -import java.lang.reflect.Type - @Slf4j class ReportSkillsSpecs extends DefaultIntSpec { @@ -282,115 +261,6 @@ class ReportSkillsSpecs extends DefaultIntSpec { subjSummaryRes.get(4).levelPoints == 0 } - def "achieve subject's level by progressing through several skill results via websocket connection"(){ - List subj1 = (1..5).collect { [projectId: projId, subjectId: "subj1", skillId: "s1${it}".toString(), name: "subj1 ${it}".toString(), type: "Skill", pointIncrement: 10, numPerformToCompletion: 10, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } - List subj2 = (1..4).collect { [projectId: projId, subjectId: "subj2", skillId: "s2${it}".toString(), name: "subj2 ${it}".toString(), type: "Skill", pointIncrement: 5, numPerformToCompletion: 10, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } - List subj3 = (1..5).collect { [projectId: projId, subjectId: "subj3", skillId: "s3${it}".toString(), name: "subj3 ${it}".toString(), type: "Skill", pointIncrement: 20, numPerformToCompletion: 10, totalPoints: 200, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } - - List wsResults = [] - WebSocketClient client = new StandardWebSocketClient() - List transports = [] - transports.add(new WebSocketTransport(client)) - SockJsClient sockJsClient = new SockJsClient(transports) - WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient) - stompClient.setMessageConverter(new MappingJackson2MessageConverter()) - StompSessionHandler sessionHandler = new StompSessionHandlerAdapter() { - @Override - Type getPayloadType(StompHeaders headers) { - return SkillEventResult - } - - @Override - void handleFrame(StompHeaders headers, @Nullable Object payload) { - SkillEventResult result = (SkillEventResult) payload - wsResults.add(result) - } - - @Override - void afterConnected(StompSession session, StompHeaders connectedHeaders) { - session.subscribe("/user/queue/${projId}-skill-updates", this) - } - } - - when: - skillsService.createSchema([subj1, subj2, subj3]) - - // setup websocket connection for sampleUserIds[0] - String userId = sampleUserIds.get(0) - String secret = skillsService.getClientSecret(projId) - skillsService.setProxyCredentials(projId, secret) - String token = skillsService.wsHelper.getTokenForUser(userId) - WebSocketHttpHeaders headers = new WebSocketHttpHeaders() - headers.add('Authorization', "Bearer ${token}") - stompClient.connect("ws://localhost:${localPort}/skills-websocket", headers, sessionHandler) - Thread.sleep(2000) - - List dates = testUtils.getLastNDays(5) - List addSkillRes = [] - List subjSummaryRes = [] - - (0..4).each { - addSkillRes << skillsService.addSkill([projectId: projId, skillId: subj1.get(it).skillId], userId, dates.get(it)) - subjSummaryRes << skillsService.getSkillSummary(userId, projId, subj1.get(it).subjectId) - } - Thread.sleep(2000) - - then: - wsResults.sort {it.skillId} - wsResults.each { - assert it.skillApplied - assert it.explanation == "Skill event was applied" - } - !wsResults.get(0).completed - wsResults.get(0).skillId == "${subj1.get(0).skillId}" - wsResults.get(0).name == "${subj1.get(0).name}" - wsResults.get(0).pointsEarned == 10 - subjSummaryRes.get(0).skillsLevel == 0 - subjSummaryRes.get(0).points == 10 - subjSummaryRes.get(0).todaysPoints == 0 - subjSummaryRes.get(0).levelPoints == 10 - - !wsResults.get(1).completed - wsResults.get(1).skillId == "${subj1.get(1).skillId}" - wsResults.get(1).name == "${subj1.get(1).name}" - wsResults.get(1).pointsEarned == 10 - subjSummaryRes.get(1).skillsLevel == 0 - subjSummaryRes.get(1).points == 20 - subjSummaryRes.get(1).todaysPoints == 0 - subjSummaryRes.get(1).levelPoints == 20 - - !wsResults.get(2).completed - wsResults.get(2).skillId == "${subj1.get(2).skillId}" - wsResults.get(2).name == "${subj1.get(2).name}" - wsResults.get(2).pointsEarned == 10 - subjSummaryRes.get(2).skillsLevel == 0 - subjSummaryRes.get(2).points == 30 - subjSummaryRes.get(2).todaysPoints == 0 - subjSummaryRes.get(2).levelPoints == 30 - - !wsResults.get(3).completed - wsResults.get(3).skillId == "${subj1.get(3).skillId}" - wsResults.get(3).name == "${subj1.get(3).name}" - wsResults.get(3).pointsEarned == 10 - subjSummaryRes.get(3).skillsLevel == 0 - subjSummaryRes.get(3).points == 40 - subjSummaryRes.get(3).todaysPoints == 0 - subjSummaryRes.get(3).levelPoints == 40 - - wsResults.get(4).completed.size() == 1 - wsResults.get(4).skillId == "${subj1.get(4).skillId}" - wsResults.get(4).name == "${subj1.get(4).name}" - wsResults.get(4).pointsEarned == 10 - wsResults.get(4).completed.get(0).type == CompletionItem.CompletionItemType.Subject - wsResults.get(4).completed.get(0).level == 1 - wsResults.get(4).completed.get(0).id == "subj1" - - subjSummaryRes.get(4).skillsLevel == 1 - subjSummaryRes.get(4).points == 50 - subjSummaryRes.get(4).todaysPoints == 10 - subjSummaryRes.get(4).levelPoints == 0 - } - def "fully achieve a subject"(){ List subj1 = (1..2).collect { [projectId: projId, subjectId: "subj1", skillId: "s1${it}".toString(), name: "subj1 ${it}".toString(), type: "Skill", pointIncrement: 15, numPerformToCompletion: 4, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } List subj2 = (1..4).collect { [projectId: projId, subjectId: "subj2", skillId: "s2${it}".toString(), name: "subj2 ${it}".toString(), type: "Skill", pointIncrement: 5, numPerformToCompletion: 5, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] } diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionController.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionController.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionController.groovy rename to service/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionController.groovy diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionSpecs.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionSpecs.groovy rename to service/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionalService.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionalService.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionalService.groovy rename to service/src/test/java/skills/intTests/reportSkills/ReportSkillsTransactionalService.groovy diff --git a/service/src/test/java/skills/intTests/reportSkills/ReportSkills_BadgeSkillsSpecs.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkills_BadgeSkillsSpecs.groovy new file mode 100644 index 00000000..ecb6ad73 --- /dev/null +++ b/service/src/test/java/skills/intTests/reportSkills/ReportSkills_BadgeSkillsSpecs.groovy @@ -0,0 +1,458 @@ +/** + * 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 "gem not awarded if achieved after end date"(){ + 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 fiveWeeksAgo = new Date()-35 + + Date threeWeeksAgo = new Date() -21 + + Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1', startDate: fiveWeeksAgo, 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) + + List requiredSkillsIds = [skill1.skillId, skill2.skillId, skill3.skillId, skill4.skillId] + requiredSkillsIds.each { String skillId -> + skillsService.assignSkillToBadge(projectId: projId, badgeId: badge.badgeId, skillId: skillId) + } + + def resSkill1 = skillsService.addSkill([projectId: projId, skillId: skill1.skillId], "someuser", threeWeeksAgo).body + def resSkill3 = skillsService.addSkill([projectId: projId, skillId: skill3.skillId], "someuser",threeWeeksAgo).body + def resSkill2 = skillsService.addSkill([projectId: projId, skillId: skill2.skillId], "someuser",threeWeeksAgo).body + def resSkill4 = skillsService.addSkill([projectId: projId, skillId: skill4.skillId], "someuser", new Date()).body + + def badgeSummary = skillsService.getBadgeSummary("someuser", projId, badge.badgeId) + + 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'} + !badgeSummary.badgeAchieved + } + + 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") + } + + def 'badge not awarded if inactive'() { + String subj = "testSubj" + + Map skill1 = [projectId: projId, subjectId: subj, skillId: "skill1", name : "Test Skill 1", type: "Skill", + pointIncrement: 100, numPerformToCompletion: 1, pointIncrementInterval: 8*60, numMaxOccurrencesIncrementInterval: 1] + + Map badge = [projectId: projId, badgeId: 'badge1', name: 'Test Badge 1'] + badge.enabled = false + List requiredSkillsIds = [skill1.skillId] + + + when: + skillsService.createProject([projectId: projId, name: "Test Project"]) + skillsService.createSubject([projectId: projId, subjectId: subj, name: "Test Subject"]) + skillsService.createSkill(skill1) + 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 + + then: + resSkill1.skillApplied && !resSkill1.completed.find { it.id == 'badge1'} + } + + def "badge awarded to users with requirements after enabling"() { + def proj = SkillsFactory.createProject() + def subj = SkillsFactory.createSubject() + def skills = SkillsFactory.createSkills(20) + def badge = SkillsFactory.createBadge() + + skillsService.createProject(proj) + skillsService.createSubject(subj) + skillsService.createSkills(skills) + + badge.enabled = false + skillsService.createBadge(badge) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(0).skillId]) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(1).skillId]) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user2", new Date()) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj.projectId], "user1", new Date()) + + when: + def user1SummaryBeforeEnable = skillsService.getBadgeSummary("user1", proj.projectId, badge.badgeId) + skillsService.updateBadge([projectId: proj.projectId, badgeId: badge.badgeId, enabled: true, name: badge.name], badge.badgeId) + + def user1Summary = skillsService.getBadgeSummary("user1", proj.projectId, badge.badgeId) + def user2Summary = skillsService.getBadgeSummary("user2", proj.projectId, badge.badgeId) + + then: + !user1SummaryBeforeEnable.badgeAchieved + user1Summary.badgeAchieved + !user2Summary.badgeAchieved + } + + def "badge awarded to users with requirements after enabling, does not impact other badges"() { + def proj = SkillsFactory.createProject() + def subj = SkillsFactory.createSubject() + def skills = SkillsFactory.createSkills(20) + def badge = SkillsFactory.createBadge() + + def badge2 = SkillsFactory.createBadge(1, 2) + + skillsService.createProject(proj) + skillsService.createSubject(subj) + skillsService.createSkills(skills) + + badge.enabled = false + skillsService.createBadge(badge) + skillsService.createBadge(badge2) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(0).skillId]) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(1).skillId]) + + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge2.badgeId, skillId: skills[0].skillId]) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge2.badgeId, skillId: skills[5].skillId]) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user2", new Date()) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj.projectId], "user1", new Date()) + + skillsService.addSkill([skillId: skills[0].skillId, projectId: proj.projectId], "user3", new Date()) + + when: + def user1SummaryBeforeEnable = skillsService.getBadgeSummary("user1", proj.projectId, badge.badgeId) + skillsService.updateBadge([projectId: proj.projectId, badgeId: badge.badgeId, enabled: true, name: badge.name], badge.badgeId) + + def user1Summary = skillsService.getBadgeSummary("user1", proj.projectId, badge.badgeId) + def user1SummaryBadge2 = skillsService.getBadgeSummary("user1", proj.projectId, badge2.badgeId) + def user2Summary = skillsService.getBadgeSummary("user2", proj.projectId, badge.badgeId) + def user3SummaryBadge1 = skillsService.getBadgeSummary("user3", proj.projectId, badge.badgeId) + def user3SummaryBadge2 = skillsService.getBadgeSummary("user3", proj.projectId, badge2.badgeId) + + then: + !user1SummaryBeforeEnable.badgeAchieved + user1Summary.badgeAchieved + !user1SummaryBadge2.badgeAchieved + !user2Summary.badgeAchieved + !user3SummaryBadge1.badgeAchieved + !user3SummaryBadge2.badgeAchieved + } + + def "gem awarded to users with requirements after enabling"() { + def proj = SkillsFactory.createProject() + def subj = SkillsFactory.createSubject() + def skills = SkillsFactory.createSkills(20) + def badge = SkillsFactory.createBadge() + + skillsService.createProject(proj) + skillsService.createSubject(subj) + skillsService.createSkills(skills) + + Date twoWeeksAgo = new Date() - 14 + Date nextWeek = new Date() + 7 + + badge.enabled = false + badge.startDate = twoWeeksAgo + badge.endDate = nextWeek + + //add start/end dates + skillsService.createBadge(badge) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(0).skillId]) + skillsService.assignSkillToBadge([projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills.get(1).skillId]) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user2", new Date()-60) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj.projectId], "user1", new Date()-7) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj.projectId], "user2", new Date()-35) + + when: + def user1SummaryBeforeEnable = skillsService.getBadgeSummary("user1", proj.projectId, badge.badgeId) + + skillsService.updateBadge([projectId: proj.projectId, + badgeId: badge.badgeId, + enabled: true, + name: badge.name, + startDate: twoWeeksAgo, + endDate: nextWeek], badge.badgeId) + + def user1Summary = skillsService.getBadgeSummary("user1", proj.projectId, badge.badgeId) + def user2Summary = skillsService.getBadgeSummary("user2", proj.projectId, badge.badgeId) + + then: + !user1SummaryBeforeEnable.badgeAchieved + user1Summary.badgeAchieved + !user2Summary.badgeAchieved + } + + def "changes to skill occurrence causes badge to be awarded"() { + def proj1 = SkillsFactory.createProject(1) + skillsService.createProject(proj1) + def subj = SkillsFactory.createSubject(1) + skillsService.createSubject(subj) + + def skill1 = SkillsFactory.createSkill(1, 1, 1, 0, 3, 90, 100) + def skill2 = SkillsFactory.createSkill(1, 1, 2, 0, 1, 0, 100) + + skillsService.createSkills([skill1, skill2]) + + def badge = SkillsFactory.createBadge() + skillsService.createBadge(badge) + skillsService.assignSkillToBadge([projectId: proj1.projectId, badgeId: badge.badgeId, skillId: skill1.skillId]) + skillsService.assignSkillToBadge([projectId: proj1.projectId, badgeId: badge.badgeId, skillId: skill2.skillId]) + + skillsService.addSkill([projectId: proj1.projectId, skillId: skill1.skillId], "u123", new Date()) + skillsService.addSkill([projectId: proj1.projectId, skillId: skill2.skillId], "u123", new Date()) + + skillsService.addSkill([projectId: proj1.projectId, skillId: skill1.skillId], "u124", new Date()) + + when: + //get history for user123 and assert that badge is not awarded + def u123SummaryBeforeEdit = skillsService.getBadgeSummary("u123", proj1.projectId, badge.badgeId) + def u124SummaryBeforeEdit = skillsService.getBadgeSummary("u124", proj1.projectId, badge.badgeId) + + + skillsService.updateSkill([projectId: proj1.projectId, + subjectId: subj.subjectId, + skillId: skill1.skillId, + numPerformToCompletion: 1, + pointIncrement: skill1.pointIncrement, + pointIncrementInterval: skill1.pointIncrementInterval, + numMaxOccurrencesIncrementInterval: skill1.numMaxOccurrencesIncrementInterval, + version: skill1.version, + name: skill1.name], skill1.skillId) + + def u123SummaryAfterEditOccurrences = skillsService.getBadgeSummary("u123", proj1.projectId, badge.badgeId) + def u124SummaryAfterEditOccurrences = skillsService.getBadgeSummary("u124", proj1.projectId, badge.badgeId) + + then: + !u123SummaryBeforeEdit.badgeAchieved + u123SummaryAfterEditOccurrences.badgeAchieved + !u124SummaryBeforeEdit.badgeAchieved + !u124SummaryAfterEditOccurrences.badgeAchieved + } + + def "deletion of a skill causes badge to be awarded"() { + def proj1 = SkillsFactory.createProject(1) + skillsService.createProject(proj1) + def subj = SkillsFactory.createSubject(1) + skillsService.createSubject(subj) + + def skill1 = SkillsFactory.createSkill(1, 1, 1, 0, 1, 90, 100) + def skill2 = SkillsFactory.createSkill(1, 1, 2, 0, 1, 0, 100) + + skillsService.createSkills([skill1, skill2]) + + def badge = SkillsFactory.createBadge() + skillsService.createBadge(badge) + skillsService.assignSkillToBadge([projectId: proj1.projectId, badgeId: badge.badgeId, skillId: skill1.skillId]) + skillsService.assignSkillToBadge([projectId: proj1.projectId, badgeId: badge.badgeId, skillId: skill2.skillId]) + + skillsService.addSkill([projectId: proj1.projectId, skillId: skill1.skillId], "u123", new Date()) + skillsService.addSkill([projectId: proj1.projectId, skillId: skill2.skillId], "u124", new Date()) + + when: + //get history for user123 and assert that badge is not awarded + def u123SummaryBeforeEdit = skillsService.getBadgeSummary("u123", proj1.projectId, badge.badgeId) + def u124SummaryBeforeEdit = skillsService.getBadgeSummary("u124", proj1.projectId, badge.badgeId) + + skillsService.deleteSkill([projectId: proj1.projectId, subjectId: subj.subjectId, skillId: skill2.skillId]) + + def u123SummaryAfterSkillDeletion = skillsService.getBadgeSummary("u123", proj1.projectId, badge.badgeId) + def u124SummaryAfterEdit = skillsService.getBadgeSummary("u124", proj1.projectId, badge.badgeId) + + then: + !u123SummaryBeforeEdit.badgeAchieved + u123SummaryAfterSkillDeletion.badgeAchieved + !u124SummaryBeforeEdit.badgeAchieved + !u124SummaryAfterEdit.badgeAchieved + } +} diff --git a/service/src/test/java/skills/intTests/reportSkills/ReportSkills_GlobalBadgeSkillsSpecs.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkills_GlobalBadgeSkillsSpecs.groovy new file mode 100644 index 00000000..0b185156 --- /dev/null +++ b/service/src/test/java/skills/intTests/reportSkills/ReportSkills_GlobalBadgeSkillsSpecs.groovy @@ -0,0 +1,344 @@ +/** + * 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.SkillsClientException +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) + } + + def "global badge awarded to users meeting level requirements after enabling"() { + def proj = SkillsFactory.createProject() + def proj2 = SkillsFactory.createProject(2) + def subj = SkillsFactory.createSubject() + def subj2 = SkillsFactory.createSubject(2) + def skills = SkillsFactory.createSkills(20) + def skills2 = SkillsFactory.createSkills(10, 2) + def badge = [badgeId: badgeId, name: 'Test Global Badge 1', enabled: 'false'] + + skillsService.createProject(proj) + skillsService.createProject(proj2) + skillsService.createSubject(subj) + skillsService.createSubject(subj2) + skillsService.createSkills(skills) + skillsService.createSkills(skills2) + + skillsService.createGlobalBadge(badge) + skillsService.assignProjectLevelToGlobalBadge(projectId: proj.projectId, badgeId: badge.badgeId, level: "1") + skillsService.assignProjectLevelToGlobalBadge(projectId: proj2.projectId, badgeId: badge.badgeId, level: "1") + + (0..9).each { + if (it == 0) { + skillsService.addSkill([skillId: skills.get(it).skillId, projectId: proj.projectId], "user2", new Date()) + skillsService.addSkill([skillId: skills2.get(it).skillId, projectId: proj2.projectId], "user2", new Date()) + } + skillsService.addSkill([skillId: skills.get(it).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills2.get(it).skillId, projectId: proj2.projectId], "user1", new Date()) + } + + def proj1Level = skillsService.getUserLevel(proj.projectId, "user1") + def proj2Level = skillsService.getUserLevel(proj2.projectId, "user1") + + assert proj1Level > 0 + assert proj2Level > 0 + + when: + def user1SummaryBeforeEnable = skillsService.getBadgesSummary("user1", proj.projectId) + badge.enabled = true + skillsService.createGlobalBadge(badge, badge.badgeId) + + def user1Summary = skillsService.getBadgesSummary("user1", proj.projectId) + def user2Summary = skillsService.getBadgesSummary("user2", proj.projectId) + + then: + !user1SummaryBeforeEnable[0].badgeAchieved + user1Summary[0].badgeId == 'GlobalBadge1' + user1Summary[0].badgeAchieved + user2Summary[0].badgeId == 'GlobalBadge1' + !user2Summary[0].badgeAchieved + } + + def "global badge awarded to users meeting skill requirements after enabling"() { + def proj = SkillsFactory.createProject() + def proj2 = SkillsFactory.createProject(2) + def subj = SkillsFactory.createSubject() + def subj2 = SkillsFactory.createSubject(2) + def skills = SkillsFactory.createSkills(20) + def skills2 = SkillsFactory.createSkills(10, 2) + def badge = [badgeId: badgeId, name: 'Test Global Badge 1', enabled: 'false'] + + def badge2 = [badgeId: 'GlobalBadge2', name: 'Test Global Badge 2', enabled: 'false'] + + skillsService.createProject(proj) + skillsService.createProject(proj2) + skillsService.createSubject(subj) + skillsService.createSubject(subj2) + skillsService.createSkills(skills) + skillsService.createSkills(skills2) + + skillsService.createGlobalBadge(badge) + skillsService.createGlobalBadge(badge2) + skillsService.assignSkillToGlobalBadge(projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills[0].skillId) + skillsService.assignSkillToGlobalBadge(projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills[1].skillId) + skillsService.assignSkillToGlobalBadge(projectId: proj2.projectId, badgeId: badge.badgeId, skillId: skills2[0].skillId) + + skillsService.assignSkillToGlobalBadge(projectId: proj.projectId, badgeId: badge2.badgeId, skillId: skills[0].skillId) + skillsService.assignSkillToGlobalBadge(projectId: proj.projectId, badgeId: badge2.badgeId, skillId: skills[1].skillId) + skillsService.assignSkillToGlobalBadge(projectId: proj2.projectId, badgeId: badge2.badgeId, skillId: skills2[0].skillId) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user2", new Date()) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj2.projectId], "user2", new Date()) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills2.get(0).skillId, projectId: proj2.projectId], "user1", new Date()) + + when: + def user1SummaryBeforeEnable = skillsService.getBadgesSummary("user1", proj.projectId) + badge.enabled = true + skillsService.createGlobalBadge(badge, badge.badgeId) + + def user1Summary = skillsService.getBadgesSummary("user1", proj.projectId) + def user2Summary = skillsService.getBadgesSummary("user2", proj.projectId) + + then: + !user1SummaryBeforeEnable.find{it.badgeId=='GlobalBadge1'}.badgeAchieved + user1Summary.find{ it.badgeId == 'GlobalBadge1'} + user1Summary.find{it.badgeId == 'GlobalBadge2'} + !user1Summary.find{it.badgeId == 'GlobalBadge2'}.badgeAchieved + user1Summary.find{ it.badgeId == 'GlobalBadge1'}.badgeAchieved + user2Summary.find{it.badgeId == 'GlobalBadge1'} + !user2Summary.find{it.badgeId == 'GlobalBadge1'}.badgeAchieved + !user2Summary.find{it.badgeId == 'GlobalBadge2'}.badgeAchieved + } + + def "global badge awarded to users meeting skill and level requirements after enabling"() { + def proj = SkillsFactory.createProject() + def proj2 = SkillsFactory.createProject(2) + def proj3 = SkillsFactory.createProject(3) + def subj = SkillsFactory.createSubject() + def subj2 = SkillsFactory.createSubject(2) + def subj3 = SkillsFactory.createSubject(3) + + def skills = SkillsFactory.createSkills(20) + def skills2 = SkillsFactory.createSkills(10, 2) + def skills3 = SkillsFactory.createSkills(10, 3) + + def badge = [badgeId: badgeId, name: 'Test Global Badge 1', enabled: 'false'] + + skillsService.createProject(proj) + skillsService.createProject(proj2) + skillsService.createProject(proj3) + skillsService.createSubject(subj) + skillsService.createSubject(subj2) + skillsService.createSubject(subj3) + skillsService.createSkills(skills) + skillsService.createSkills(skills2) + skillsService.createSkills(skills3) + + skillsService.createGlobalBadge(badge) + skillsService.assignSkillToGlobalBadge(projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills[0].skillId) + skillsService.assignSkillToGlobalBadge(projectId: proj.projectId, badgeId: badge.badgeId, skillId: skills[1].skillId) + skillsService.assignSkillToGlobalBadge(projectId: proj2.projectId, badgeId: badge.badgeId, skillId: skills2[0].skillId) + skillsService.assignProjectLevelToGlobalBadge([projectId: proj3.projectId, badgeId: badge.badgeId, level: "1"]) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user2", new Date()) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj2.projectId], "user2", new Date()) + skillsService.addSkill([skillId: skills3.get(0).skillId, projectId: proj3.projectId], "user2", new Date()) + + skillsService.addSkill([skillId: skills.get(0).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills.get(1).skillId, projectId: proj.projectId], "user1", new Date()) + skillsService.addSkill([skillId: skills2.get(0).skillId, projectId: proj2.projectId], "user1", new Date()) + + (0..6).each { + skillsService.addSkill([skillId: skills3.get(it).skillId, projectId: proj3.projectId], "user1", new Date()) + } + + when: + def user1SummaryBeforeEnable = skillsService.getBadgesSummary("user1", proj.projectId) + + badge.enabled = true + skillsService.createGlobalBadge(badge, badge.badgeId) + + def user1Summary = skillsService.getBadgesSummary("user1", proj.projectId) + def user2Summary = skillsService.getBadgesSummary("user2", proj.projectId) + + then: + !user1SummaryBeforeEnable[0].badgeAchieved + user1Summary[0].badgeId == 'GlobalBadge1' + user1Summary[0].badgeAchieved + user2Summary[0].badgeId == 'GlobalBadge1' + !user2Summary[0].badgeAchieved + } + + def "changes to skill occurrence causes global badge to be awarded"() { + def proj1 = SkillsFactory.createProject(1) + skillsService.createProject(proj1) + def subj = SkillsFactory.createSubject(1) + skillsService.createSubject(subj) + + def proj2 = SkillsFactory.createProject(2) + skillsService.createProject(proj2) + def subj2 = SkillsFactory.createSubject(2) + skillsService.createSubject(subj2) + + + def skill1 = SkillsFactory.createSkill(1, 1, 1, 0, 3, 90, 100) + def skill2 = SkillsFactory.createSkill(2, 1, 2, 0, 1, 0, 100) + + + skillsService.createSkills([skill1, skill2]) + + def badge = SkillsFactory.createBadge() + skillsService.createGlobalBadge(badge) + skillsService.assignSkillToGlobalBadge([projectId: proj1.projectId, badgeId: badge.badgeId, skillId: skill1.skillId]) + skillsService.assignSkillToGlobalBadge([projectId: proj2.projectId, badgeId: badge.badgeId, skillId: skill2.skillId]) + + skillsService.addSkill([projectId: proj1.projectId, skillId: skill1.skillId], "u123", new Date()) + skillsService.addSkill([projectId: proj2.projectId, skillId: skill2.skillId], "u123", new Date()) + + when: + //get history for user123 and assert that badge is not awarded + def u123SummaryBeforeEdit = skillsService.getBadgesSummary("u123", proj1.projectId) + + + skillsService.updateSkill([projectId: proj1.projectId, + subjectId: subj.subjectId, + skillId: skill1.skillId, + numPerformToCompletion: 1, + pointIncrement: skill1.pointIncrement, + pointIncrementInterval: skill1.pointIncrementInterval, + numMaxOccurrencesIncrementInterval: skill1.numMaxOccurrencesIncrementInterval, + version: skill1.version, + name: skill1.name], skill1.skillId) + + def u123SummaryAfterEditOccurrences = skillsService.getBadgesSummary("u123", proj1.projectId) + + then: + !u123SummaryBeforeEdit[0].badgeAchieved + u123SummaryAfterEditOccurrences[0].badgeAchieved + } + + def "deletion of a skill causes global badge to be awarded"() { + def proj1 = SkillsFactory.createProject(1) + skillsService.createProject(proj1) + def subj = SkillsFactory.createSubject(1) + skillsService.createSubject(subj) + + def proj2 = SkillsFactory.createProject(2) + skillsService.createProject(proj2) + def subj2 = SkillsFactory.createSubject(2) + skillsService.createSubject(subj2) + + + def skill1 = SkillsFactory.createSkill(1, 1, 1, 0, 1, 90, 100) + def skill2 = SkillsFactory.createSkill(2, 1, 2, 0, 1, 0, 100) + + + skillsService.createSkills([skill1, skill2]) + + def badge = SkillsFactory.createBadge() + skillsService.createGlobalBadge(badge) + skillsService.assignSkillToGlobalBadge([projectId: proj1.projectId, badgeId: badge.badgeId, skillId: skill1.skillId]) + skillsService.assignSkillToGlobalBadge([projectId: proj2.projectId, badgeId: badge.badgeId, skillId: skill2.skillId]) + + skillsService.addSkill([projectId: proj1.projectId, skillId: skill1.skillId], "u123", new Date()) + + when: + //get history for user123 and assert that badge is not awarded + def u123SummaryBeforeEdit = skillsService.getBadgesSummary("u123", proj1.projectId) + + skillsService.deleteSkill([projectId: proj2.projectId, subjectId: subj.subjectId, skillId: skill2.skillId]) + + def u123SummaryAfterSkillDeletion = skillsService.getBadgesSummary("u123", proj1.projectId) + + then: + SkillsClientException ex = thrown(SkillsClientException) + ex.message.contains('cannot be deleted as it is currently referenced by one or more global badges') + } + +} diff --git a/backend/src/test/java/skills/intTests/reportSkills/ReportSkills_TimeWindowSpecs.groovy b/service/src/test/java/skills/intTests/reportSkills/ReportSkills_TimeWindowSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/reportSkills/ReportSkills_TimeWindowSpecs.groovy rename to service/src/test/java/skills/intTests/reportSkills/ReportSkills_TimeWindowSpecs.groovy diff --git a/backend/src/test/java/skills/intTests/root/RootAccessSpec.groovy b/service/src/test/java/skills/intTests/root/RootAccessSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/root/RootAccessSpec.groovy rename to service/src/test/java/skills/intTests/root/RootAccessSpec.groovy diff --git a/backend/src/test/java/skills/intTests/skillsVersioning/ClientDisplaySkillVersioningSpec.groovy b/service/src/test/java/skills/intTests/skillsVersioning/ClientDisplaySkillVersioningSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/skillsVersioning/ClientDisplaySkillVersioningSpec.groovy rename to service/src/test/java/skills/intTests/skillsVersioning/ClientDisplaySkillVersioningSpec.groovy diff --git a/backend/src/test/java/skills/intTests/skillsVersioning/SkillVersionManagementSpec.groovy b/service/src/test/java/skills/intTests/skillsVersioning/SkillVersionManagementSpec.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/skillsVersioning/SkillVersionManagementSpec.groovy rename to service/src/test/java/skills/intTests/skillsVersioning/SkillVersionManagementSpec.groovy diff --git a/backend/src/test/java/skills/intTests/supervisor/SupervisorEditSpecs.groovy b/service/src/test/java/skills/intTests/supervisor/SupervisorEditSpecs.groovy similarity index 97% rename from backend/src/test/java/skills/intTests/supervisor/SupervisorEditSpecs.groovy rename to service/src/test/java/skills/intTests/supervisor/SupervisorEditSpecs.groovy index e4f4a952..b11f6262 100644 --- a/backend/src/test/java/skills/intTests/supervisor/SupervisorEditSpecs.groovy +++ b/service/src/test/java/skills/intTests/supervisor/SupervisorEditSpecs.groovy @@ -197,6 +197,23 @@ class SupervisorEditSpecs extends DefaultIntSpec { skillsService.deleteGlobalBadge(badgeId) } + def 'global badge name special characters'() { + String badgeName = "foo 123456789_-#()[]/*%;" + Map badge = [badgeId: badgeId, name: badgeName] + skillsService.createGlobalBadge(badge) + + when: + def res = skillsService.doesGlobalBadgeNameExists(badge.name) + def globalBadge = skillsService.getGlobalBadge(badge.badgeId) + + then: + res + globalBadge.name == 'foo 123456789_-#()[]/*%;' + + cleanup: + skillsService.deleteGlobalBadge(badgeId) + } + def 'cannot create global badge where the name already exists'() { Map badge = [badgeId: badgeId, name: 'Test Global Badge 1'] skillsService.createGlobalBadge(badge) @@ -535,4 +552,6 @@ class SupervisorEditSpecs extends DefaultIntSpec { cleanup: skillsService.deleteGlobalBadge(badgeId) } + + } diff --git a/backend/src/test/java/skills/intTests/utils/DefaultIntSpec.groovy b/service/src/test/java/skills/intTests/utils/DefaultIntSpec.groovy similarity index 90% rename from backend/src/test/java/skills/intTests/utils/DefaultIntSpec.groovy rename to service/src/test/java/skills/intTests/utils/DefaultIntSpec.groovy index 5ced109b..1bf7f064 100644 --- a/backend/src/test/java/skills/intTests/utils/DefaultIntSpec.groovy +++ b/service/src/test/java/skills/intTests/utils/DefaultIntSpec.groovy @@ -54,6 +54,10 @@ class DefaultIntSpec extends Specification { SettingRepo settingRepo def setup() { + // allows for over-ridding the setup method + doSetup(); + } + def doSetup() { String msg = "\n-------------------------------------------------------------\n" + "START: [${specificationContext.currentIteration.name}]\n" + "-------------------------------------------------------------" @@ -61,13 +65,13 @@ class DefaultIntSpec extends Specification { /** * deleting projects and users will wipe the entire db clean due to cascading */ - projDefRepo.deleteAll() - userAttrsRepo.deleteAll() - // global badges don't have references to a project so must delete those manually - skillDefRepo.deleteAll() + projDefRepo.deleteAll() + userAttrsRepo.deleteAll() + // global badges don't have references to a project so must delete those manually + skillDefRepo.deleteAll() settingRepo.findAll().each { - if (!it.settingGroup.startsWith("public_")){ + if (!it.settingGroup?.startsWith("public_")){ settingRepo.delete(it) } } diff --git a/backend/src/test/java/skills/intTests/utils/Props.groovy b/service/src/test/java/skills/intTests/utils/Props.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/utils/Props.groovy rename to service/src/test/java/skills/intTests/utils/Props.groovy diff --git a/backend/src/test/java/skills/intTests/utils/RestTemplateWrapper.groovy b/service/src/test/java/skills/intTests/utils/RestTemplateWrapper.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/utils/RestTemplateWrapper.groovy rename to service/src/test/java/skills/intTests/utils/RestTemplateWrapper.groovy diff --git a/backend/src/test/java/skills/intTests/utils/SkillsClientException.groovy b/service/src/test/java/skills/intTests/utils/SkillsClientException.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/utils/SkillsClientException.groovy rename to service/src/test/java/skills/intTests/utils/SkillsClientException.groovy diff --git a/backend/src/test/java/skills/intTests/utils/SkillsFactory.groovy b/service/src/test/java/skills/intTests/utils/SkillsFactory.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/utils/SkillsFactory.groovy rename to service/src/test/java/skills/intTests/utils/SkillsFactory.groovy diff --git a/backend/src/test/java/skills/intTests/utils/SkillsService.groovy b/service/src/test/java/skills/intTests/utils/SkillsService.groovy similarity index 92% rename from backend/src/test/java/skills/intTests/utils/SkillsService.groovy rename to service/src/test/java/skills/intTests/utils/SkillsService.groovy index 7ea76cc8..75dde24d 100644 --- a/backend/src/test/java/skills/intTests/utils/SkillsService.groovy +++ b/service/src/test/java/skills/intTests/utils/SkillsService.groovy @@ -160,12 +160,12 @@ class SkillsService { def projectIdExists(Map props){ String id = props.projectId - wsHelper.appGet("/projectExist?projectId=${id}") + wsHelper.appPost("/projectExist", props) } def projectNameExists(Map props){ String name = props.projectName - wsHelper.appGet("/projectExist?projectName=${name}".toString()) + wsHelper.appPost("/projectExist", [name: name])?.body } def getProjects() { @@ -177,8 +177,9 @@ class SkillsService { } def deleteProjectIfExist(String projectId) { - Boolean res = wsHelper.appGet("/projectExist", [projectId: projectId]) - if(res ) { + def res = wsHelper.appPost("/projectExist", [projectId: projectId]) + Boolean exists = res.body + if(exists) { deleteProject(projectId) } } @@ -218,8 +219,7 @@ class SkillsService { } def subjectNameExists(Map props){ - def subjName = URLEncoder.encode(props.subjectName, 'UTF-8') - wsHelper.adminGet("/projects/${props.projectId}/subjectNameExists?subjectName=${subjName}") + wsHelper.adminPost("/projects/${props.projectId}/subjectNameExists", [name:props.subjectName]) } def getSubjectDescriptions(String projectId, String subjectId) { @@ -236,13 +236,11 @@ class SkillsService { } def badgeNameExists(Map props){ - def badgeName = URLEncoder.encode(props.badgeName, 'UTF-8') - wsHelper.adminGet("/projects/${props.projectId}/badgeNameExists?badgeName=${badgeName}") + wsHelper.adminPost("/projects/${props.projectId}/badgeNameExists", [name:props.badgeName])?.body } def skillNameExists(Map props){ - def skillName = URLEncoder.encode(props.skillName, 'UTF-8') - wsHelper.adminGet("/projects/${props.projectId}/skillNameExists?skillName=${skillName}") + wsHelper.adminPost("/projects/${props.projectId}/skillNameExists", [name: props.skillName])?.body } def deleteSubject(Map props) { @@ -391,7 +389,7 @@ class SkillsService { } def doesGlobalBadgeNameExists(String name) { - wsHelper.supervisorGet("/badges/name/${name}/exists") + wsHelper.supervisorPost("/badges/name/exists", [name:name])?.body } def doesGlobalBadgeIdExists(String id) { wsHelper.supervisorGet("/badges/id/${id}/exists") @@ -498,6 +496,14 @@ class SkillsService { wsHelper.apiGet(url) } + def getSkillsSummaryForCurrentUser(String projId, int version = -1) { + String url = "/projects/${projId}/summary" + if (version >= 0) { + url += "&version=${version}" + } + wsHelper.apiGet(url) + } + def getDependencyGraph(String projId, String skillId=null) { String url = skillId ? "/projects/${projId}/skills/${skillId}/dependency/graph" : "/projects/${projId}/dependency/graph" wsHelper.adminGet(url) @@ -788,17 +794,15 @@ class SkillsService { boolean doesSubjectNameExist(String projectId, String subjectName) { // String encoded = URLEncoder.encode(subjectName, StandardCharsets.UTF_8.toString()) - return wsHelper.adminGet("/projects/${projectId}/subjectNameExists?subjectName=${subjectName}") + return wsHelper.adminPost("/projects/${projectId}/subjectNameExists", [name:subjectName])?.body } - boolean doesBadgeNameExist(String projectId, String subjectName) { - String encoded = URLEncoder.encode(subjectName, StandardCharsets.UTF_8.toString()) - return wsHelper.adminGet("/projects/${projectId}/badgeNameExists?badgeName=${encoded}") + boolean doesBadgeNameExist(String projectId, String badgeName) { + return wsHelper.adminPost("/projects/${projectId}/badgeNameExists",[name:badgeName])?.body } boolean doesSkillNameExist(String projectId, String skillName) { - String encoded = URLEncoder.encode(skillName, StandardCharsets.UTF_8.toString()) - return wsHelper.adminGet("/projects/${projectId}/skillNameExists?skillName=${encoded}") + return wsHelper.adminPost("/projects/${projectId}/skillNameExists", [name:skillName])?.body } boolean doesEntityExist(String projectId, String id) { @@ -825,6 +829,43 @@ class SkillsService { return wsHelper.apiPost("/projects/${projectId}/skillsClientVersion", [ skillsClientVersion: version ]) } + def requestPasswordReset(String userId) { + return wsHelper.post("/resetPassword", "", ["userId", userId]) + } + + def saveEmailSettings(String host, String protocol, Integer port, boolean tlsEnabled, boolean authEnabled, String username, String password) { + return wsHelper.rootPost("/saveEmailSettings", [ + host: host, + protocol: protocol, + port: port, + tlsEnabled: tlsEnabled, + authEnabled: authEnabled, + username: username, + password: password + ]) + } + + def getEmailSettings() { + return wsHelper.rootGet('/getEmailSettings') + } + + def saveSystemSettings(String publicUrl, String resetTokenExpiration, String fromEmail, String customHeader, String customFooter) { + return wsHelper.rootPost('/saveSystemSettings', + [publicUrl: publicUrl, + resetTokenExpiration: resetTokenExpiration, + fromEmail: fromEmail, + customHeader: customHeader, + customFooter: customFooter]) + } + + def getSystemSettings() { + return wsHelper.rootGet('/getSystemSettings') + } + + def isFeatureEnabled(String featureName) { + return wsHelper.isFeatureEnabled(featureName) + } + private String getProjectUrl(String project) { return "/projects/${project}".toString() } diff --git a/backend/src/test/java/skills/intTests/utils/SkillsServiceFactory.groovy b/service/src/test/java/skills/intTests/utils/SkillsServiceFactory.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/utils/SkillsServiceFactory.groovy rename to service/src/test/java/skills/intTests/utils/SkillsServiceFactory.groovy diff --git a/backend/src/test/java/skills/intTests/utils/SpringBootAppManager.java b/service/src/test/java/skills/intTests/utils/SpringBootAppManager.java similarity index 100% rename from backend/src/test/java/skills/intTests/utils/SpringBootAppManager.java rename to service/src/test/java/skills/intTests/utils/SpringBootAppManager.java diff --git a/backend/src/test/java/skills/intTests/utils/TestUtils.groovy b/service/src/test/java/skills/intTests/utils/TestUtils.groovy similarity index 100% rename from backend/src/test/java/skills/intTests/utils/TestUtils.groovy rename to service/src/test/java/skills/intTests/utils/TestUtils.groovy diff --git a/backend/src/test/java/skills/intTests/utils/WSHelper.groovy b/service/src/test/java/skills/intTests/utils/WSHelper.groovy similarity index 98% rename from backend/src/test/java/skills/intTests/utils/WSHelper.groovy rename to service/src/test/java/skills/intTests/utils/WSHelper.groovy index 902beb99..18097fee 100644 --- a/backend/src/test/java/skills/intTests/utils/WSHelper.groovy +++ b/service/src/test/java/skills/intTests/utils/WSHelper.groovy @@ -56,7 +56,8 @@ class WSHelper { } def appPost(String endpoint, def params) { - post(endpoint, "app", params) + def res = post(endpoint, "app", params) + return res } def appGet(String endpoint, Map params = null) { @@ -142,6 +143,10 @@ class WSHelper { return post('/grantFirstRoot', '', null) } + def isFeatureEnabled(String featureName) { + return get('/isFeatureSupported', 'public', [feature: featureName]) + } + def serverPut(String endpoint, def params) { put(endpoint, "server", params) } diff --git a/backend/src/test/java/skills/services/CustomValidatorSpec.groovy b/service/src/test/java/skills/services/CustomValidatorSpec.groovy similarity index 100% rename from backend/src/test/java/skills/services/CustomValidatorSpec.groovy rename to service/src/test/java/skills/services/CustomValidatorSpec.groovy diff --git a/backend/src/test/java/skills/services/IdFormatValidatorSpec.groovy b/service/src/test/java/skills/services/IdFormatValidatorSpec.groovy similarity index 100% rename from backend/src/test/java/skills/services/IdFormatValidatorSpec.groovy rename to service/src/test/java/skills/services/IdFormatValidatorSpec.groovy diff --git a/backend/src/test/java/skills/services/SkillEventServiceUnitSpecs.groovy b/service/src/test/java/skills/services/SkillEventServiceUnitSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/services/SkillEventServiceUnitSpecs.groovy rename to service/src/test/java/skills/services/SkillEventServiceUnitSpecs.groovy diff --git a/backend/src/test/java/skills/services/UserAttrsServiceSpec.groovy b/service/src/test/java/skills/services/UserAttrsServiceSpec.groovy similarity index 100% rename from backend/src/test/java/skills/services/UserAttrsServiceSpec.groovy rename to service/src/test/java/skills/services/UserAttrsServiceSpec.groovy diff --git a/backend/src/test/java/skills/stressTests/CreateSkillsDef.groovy b/service/src/test/java/skills/stressTests/CreateSkillsDef.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/CreateSkillsDef.groovy rename to service/src/test/java/skills/stressTests/CreateSkillsDef.groovy diff --git a/backend/src/test/java/skills/stressTests/HitSkillsHard.groovy b/service/src/test/java/skills/stressTests/HitSkillsHard.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/HitSkillsHard.groovy rename to service/src/test/java/skills/stressTests/HitSkillsHard.groovy diff --git a/backend/src/test/java/skills/stressTests/ReportEvents.groovy b/service/src/test/java/skills/stressTests/ReportEvents.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/ReportEvents.groovy rename to service/src/test/java/skills/stressTests/ReportEvents.groovy diff --git a/backend/src/test/java/skills/stressTests/SkillServiceFactory.groovy b/service/src/test/java/skills/stressTests/SkillServiceFactory.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/SkillServiceFactory.groovy rename to service/src/test/java/skills/stressTests/SkillServiceFactory.groovy diff --git a/backend/src/test/java/skills/stressTests/StatsHelper.groovy b/service/src/test/java/skills/stressTests/StatsHelper.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/StatsHelper.groovy rename to service/src/test/java/skills/stressTests/StatsHelper.groovy diff --git a/backend/src/test/java/skills/stressTests/StressTestRunner.groovy b/service/src/test/java/skills/stressTests/StressTestRunner.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/StressTestRunner.groovy rename to service/src/test/java/skills/stressTests/StressTestRunner.groovy diff --git a/backend/src/test/java/skills/stressTests/UserAndDateFactory.groovy b/service/src/test/java/skills/stressTests/UserAndDateFactory.groovy similarity index 100% rename from backend/src/test/java/skills/stressTests/UserAndDateFactory.groovy rename to service/src/test/java/skills/stressTests/UserAndDateFactory.groovy diff --git a/backend/src/test/java/skills/utils/RetryUtilSpecs.groovy b/service/src/test/java/skills/utils/RetryUtilSpecs.groovy similarity index 100% rename from backend/src/test/java/skills/utils/RetryUtilSpecs.groovy rename to service/src/test/java/skills/utils/RetryUtilSpecs.groovy diff --git a/backend/src/test/resources/application.properties b/service/src/test/resources/application.properties similarity index 100% rename from backend/src/test/resources/application.properties rename to service/src/test/resources/application.properties diff --git a/backend/src/test/resources/dot.png b/service/src/test/resources/dot.png similarity index 100% rename from backend/src/test/resources/dot.png rename to service/src/test/resources/dot.png diff --git a/backend/src/test/resources/dot2.png b/service/src/test/resources/dot2.png similarity index 100% rename from backend/src/test/resources/dot2.png rename to service/src/test/resources/dot2.png diff --git a/backend/src/test/resources/logback.xml b/service/src/test/resources/logback.xml similarity index 100% rename from backend/src/test/resources/logback.xml rename to service/src/test/resources/logback.xml diff --git a/service/src/test/resources/migration1.sql b/service/src/test/resources/migration1.sql new file mode 100644 index 00000000..13dda218 --- /dev/null +++ b/service/src/test/resources/migration1.sql @@ -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. +-- + +INSERT INTO public.user_attrs (id, user_id, first_name, last_name, dn, email, nickname, user_id_for_display, created, updated) VALUES (1, 'skills@skills.org', 'Skills', 'Test', null, 'skills@skills.org', 'Skills Test', 'skills@skills.org', '2020-07-08 21:18:39.684787', '2020-07-08 21:18:39.684787'); +INSERT INTO public.user_attrs (id, user_id, first_name, last_name, dn, email, nickname, user_id_for_display, created, updated) VALUES (2, 'user1', null, null, null, null, '', 'user1', '2020-07-08 21:18:41.109754', '2020-07-08 21:18:41.109754'); +INSERT INTO public.users (id, user_id, password, created, updated) VALUES (1, 'skills@skills.org', '$2a$10$N60RpYKz02mseEd/zBVkSOQ0A7fZHPhIrKGZ2zNRN/xXoSwXB4m.W', '2020-07-08 18:23:24.040326', '2020-07-08 18:23:24.040326'); +INSERT INTO public.project_definition (id, project_id, name, client_secret, total_points, created, updated) VALUES (1, 'TestProject1', 'Test Project--1', 'ubZE327Jx78p4mZ0Mzbx35D6e9OWnN8F', 0, '2020-07-08 18:23:27.136735', '2020-07-08 18:23:27.136735'); +INSERT INTO public.user_roles (id, user_ref_id, user_id, role_name, project_id, created, updated) VALUES (1, 1, 'skills@skills.org', 'ROLE_APP_USER', null, '2020-07-08 18:23:24.040326', '2020-07-08 18:23:24.040326'); +INSERT INTO public.user_roles (id, user_ref_id, user_id, role_name, project_id, created, updated) VALUES (2, 1, 'skills@skills.org', 'ROLE_PROJECT_ADMIN', 'TestProject1', '2020-07-08 18:23:27.136735', '2020-07-08 18:23:27.136735'); +INSERT INTO public.skill_definition (id, project_id, skill_id, proj_ref_id, name, point_increment, point_increment_interval, increment_interval_max_occurrences, total_points, description, help_url, display_order, type, custom_icon_ref_id, icon_class, start_date, end_date, version, created, updated) VALUES (92, 'TestProject1', 'skill1', null, 'Test Skill 1', 50, 480, 1, 50, '75540', '75541', 1, 'Skill', null, null, null, null, 0, '2020-07-08 21:18:40.801000', '2020-07-08 21:18:40.801000'); +INSERT INTO public.skill_definition (id, project_id, skill_id, proj_ref_id, name, point_increment, point_increment_interval, increment_interval_max_occurrences, total_points, description, help_url, display_order, type, custom_icon_ref_id, icon_class, start_date, end_date, version, created, updated) VALUES (93, 'TestProject1', 'skill2', null, 'Test Skill 2', 50, 480, 1, 50, '75542', '75543', 2, 'Skill', null, null, null, null, 0, '2020-07-08 21:18:40.919000', '2020-07-08 21:18:40.919000'); +INSERT INTO public.skill_definition (id, project_id, skill_id, proj_ref_id, name, point_increment, point_increment_interval, increment_interval_max_occurrences, total_points, description, help_url, display_order, type, custom_icon_ref_id, icon_class, start_date, end_date, version, created, updated) VALUES (94, 'TestProject1', 'skill3', null, 'Test Skill 3', 50, 480, 1, 50, '75544', '75545', 3, 'Skill', null, null, null, null, 0, '2020-07-08 21:18:41.018000', '2020-07-08 21:18:41.018000'); +INSERT INTO public.skill_definition (id, project_id, skill_id, proj_ref_id, name, point_increment, point_increment_interval, increment_interval_max_occurrences, total_points, description, help_url, display_order, type, custom_icon_ref_id, icon_class, start_date, end_date, version, created, updated) VALUES (99, 'TestProject1', 'TestSubject1', 1, 'Test Subject --1', 0, 0, 0, 150, null, null, 0, 'Subject', null, 'fa fa-question-circle', null, null, 0, '2020-07-08 21:18:40.617000', '2020-07-08 21:18:41.038000'); +INSERT INTO public.skill_relationship_definition (id, parent_ref_id, child_ref_id, type, created, updated) VALUES (91, 99, 92, 'RuleSetDefinition', '2020-07-08 21:18:40.830000', '2020-07-08 21:18:40.830000'); +INSERT INTO public.skill_relationship_definition (id, parent_ref_id, child_ref_id, type, created, updated) VALUES (92, 99, 93, 'RuleSetDefinition', '2020-07-08 21:18:40.927000', '2020-07-08 21:18:40.927000'); +INSERT INTO public.skill_relationship_definition (id, parent_ref_id, child_ref_id, type, created, updated) VALUES (93, 99, 94, 'RuleSetDefinition', '2020-07-08 21:18:41.026000', '2020-07-08 21:18:41.026000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (81, 'user1', 'TestProject1', 'TestSubject1', 99, 1, 50, '2020-07-08 21:18:41.401000', '2020-07-08 21:18:41.401000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (82, 'user1', 'TestProject1', 'TestSubject1', 99, 2, 50, '2020-07-08 21:18:41.406000', '2020-07-08 21:18:41.406000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (83, 'user1', 'TestProject1', null, null, 1, 50, '2020-07-08 21:18:41.411000', '2020-07-08 21:18:41.411000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (84, 'user1', 'TestProject1', null, null, 2, 50, '2020-07-08 21:18:41.415000', '2020-07-08 21:18:41.415000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (85, 'user1', 'TestProject1', 'skill1', 92, null, 50, '2020-07-08 21:18:41.419000', '2020-07-08 21:18:41.419000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (86, 'user1', 'TestProject1', 'TestSubject1', 99, 3, 100, '2020-07-08 21:18:41.554000', '2020-07-08 21:18:41.554000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (87, 'user1', 'TestProject1', 'TestSubject1', 99, 4, 100, '2020-07-08 21:18:41.556000', '2020-07-08 21:18:41.556000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (88, 'user1', 'TestProject1', null, null, 3, 100, '2020-07-08 21:18:41.558000', '2020-07-08 21:18:41.558000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (89, 'user1', 'TestProject1', null, null, 4, 100, '2020-07-08 21:18:41.560000', '2020-07-08 21:18:41.560000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (90, 'user1', 'TestProject1', 'skill2', 93, null, 50, '2020-07-08 21:18:41.562000', '2020-07-08 21:18:41.562000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (91, 'user1', 'TestProject1', 'TestSubject1', 99, 5, 150, '2020-07-08 21:18:41.647000', '2020-07-08 21:18:41.647000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (92, 'user1', 'TestProject1', null, null, 5, 150, '2020-07-08 21:18:41.649000', '2020-07-08 21:18:41.649000'); +INSERT INTO public.user_achievement (id, user_id, project_id, skill_id, skill_ref_id, level, points_when_achieved, created, updated) VALUES (93, 'user1', 'TestProject1', 'skill3', 94, null, 50, '2020-07-08 21:18:41.650000', '2020-07-08 21:18:41.650000'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (91, 1, null, 1, 10, null, null, null, 'fas fa-user-ninja', 'White Belt', '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (92, 1, null, 2, 25, null, null, null, 'fas fa-user-ninja', 'Blue Belt', '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (93, 1, null, 3, 45, null, null, null, 'fas fa-user-ninja', 'Purple Belt', '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (94, 1, null, 4, 67, null, null, null, 'fas fa-user-ninja', 'Brown Belt', '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (95, 1, null, 5, 92, null, null, null, 'fas fa-user-ninja', 'Black Belt', '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (96, null, 99, 1, 10, null, null, null, 'fas fa-user-ninja', 'White Belt', '2020-07-08 21:18:40.528126', '2020-07-08 21:18:40.528126'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (97, null, 99, 2, 25, null, null, null, 'fas fa-user-ninja', 'Blue Belt', '2020-07-08 21:18:40.528126', '2020-07-08 21:18:40.528126'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (98, null, 99, 3, 45, null, null, null, 'fas fa-user-ninja', 'Purple Belt', '2020-07-08 21:18:40.528126', '2020-07-08 21:18:40.528126'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (99, null, 99, 4, 67, null, null, null, 'fas fa-user-ninja', 'Brown Belt', '2020-07-08 21:18:40.528126', '2020-07-08 21:18:40.528126'); +INSERT INTO public.level_definition (id, project_ref_id, skill_ref_id, level, percent, points_from, points_to, custom_icon_ref_id, icon_class, logical_name, created, updated) VALUES (100, null, 99, 5, 92, null, null, null, 'fas fa-user-ninja', 'Black Belt', '2020-07-08 21:18:40.528126', '2020-07-08 21:18:40.528126'); +INSERT INTO public.settings (id, setting, value, type, project_id, setting_group, user_ref_id, created, updated) VALUES (1, 'project', '1', 'UserProject', 'TestProject1', 'project_sort_order', 1, '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); +INSERT INTO public.settings (id, setting, value, type, created, updated) VALUES (2, 'foo', '123456abcdef', 'Global', '2020-07-08 21:18:40.161689', '2020-07-08 21:18:40.161689'); diff --git a/skills-bootstrap/.gitignore b/skills-bootstrap/.gitignore deleted file mode 100644 index a0dddc6f..00000000 --- a/skills-bootstrap/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -.DS_Store -node_modules -/dist - -# local env files -.env.local -.env.*.local - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/skills-bootstrap/README.md b/skills-bootstrap/README.md deleted file mode 100644 index fa4f0b8a..00000000 --- a/skills-bootstrap/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# skills-bootstrap-cli3 - -## Project setup -``` -npm install -``` - -### Compiles and hot-reloads for development -``` -npm run serve -``` - -### Compiles and minifies for production -``` -npm run build -``` - -### Run your tests -``` -npm run test -``` - -### Lints and fixes files -``` -npm run lint -``` - -### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/skills-bootstrap/babel.config.js b/skills-bootstrap/babel.config.js deleted file mode 100644 index ba179669..00000000 --- a/skills-bootstrap/babel.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - presets: [ - '@vue/app' - ] -} diff --git a/skills-bootstrap/package.json b/skills-bootstrap/package.json deleted file mode 100644 index 8b74521d..00000000 --- a/skills-bootstrap/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "skills-bootstrap-cli3", - "version": "0.1.0", - "private": true, - "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "lint": "vue-cli-service lint", - "deploy": "npm run build && rm -rf ../backend/src/main/resources/public/bootstrap && rm -rf ../backend/target/classes/public/bootstrap && cp -rT dist ../backend/src/main/resources/public/bootstrap/ && cp -rT dist ../backend/target/classes/public/bootstrap" - }, - "dependencies": { - "@fortawesome/fontawesome-free": "^5.8.2", - "animate.css": "^3.7.0", - "axios": "^0.18.0", - "bootstrap": "4.3.1", - "bootstrap-vue": "2.0.0-rc.19", - "core-js": "^2.6.5", - "css-loader": "^2.1.1", - "material-icons": "^0.3.1", - "vee-validate": "^2.2.7", - "vue": "^2.6.10", - "vue-loader": "^15.7.0" - }, - "devDependencies": { - "@vue/cli-plugin-babel": "^3.7.0", - "@vue/cli-plugin-eslint": "^3.7.0", - "@vue/cli-service": "^3.7.0", - "autoprefixer": "^9.6.0", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-vue": "^5.0.0", - "node-sass": "^4.12.0", - "sass-loader": "^7.1.0", - "vue-template-compiler": "^2.6.10", - "webpack": "^4.31.0" - }, - "eslintConfig": { - "root": true, - "env": { - "node": true - }, - "extends": [ - "plugin:vue/essential", - "eslint:recommended" - ], - "rules": {}, - "parserOptions": { - "parser": "babel-eslint" - } - }, - "postcss": { - "plugins": { - "autoprefixer": {} - } - }, - "browserslist": [ - "> 1%", - "last 2 versions" - ] -} diff --git a/skills-bootstrap/pom.xml b/skills-bootstrap/pom.xml deleted file mode 100644 index 8b54e72e..00000000 --- a/skills-bootstrap/pom.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - skills-service - skills - 1.1.4-SNAPSHOT - - 4.0.0 - - skills-bootstrap - - - UTF-8 - UTF-8 - 1.8 - 1.6 - - - - - - - maven-jar-plugin - - - default-jar - none - - unwanted - unwanted - - - - - - maven-install-plugin - - - default-install - none - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - com.github.eirslett - frontend-maven-plugin - 1.6 - - target - - - - install node and npm - - install-node-and-npm - - - ${node.version} - - - - npm install - - npm - - generate-resources - - install - - - - npm run build - - npm - - - run build - - - - - - - - - - - - - - - - - - - - - diff --git a/skills-bootstrap/public/index.html b/skills-bootstrap/public/index.html deleted file mode 100644 index ba5e5ec7..00000000 --- a/skills-bootstrap/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - User Skills - - - - - - -
    - - - diff --git a/skills-bootstrap/public/skills.ico b/skills-bootstrap/public/skills.ico deleted file mode 100644 index 59111183..00000000 Binary files a/skills-bootstrap/public/skills.ico and /dev/null differ diff --git a/skills-bootstrap/public/skills.jpeg b/skills-bootstrap/public/skills.jpeg deleted file mode 100644 index 7402133b..00000000 Binary files a/skills-bootstrap/public/skills.jpeg and /dev/null differ diff --git a/skills-bootstrap/src/App.vue b/skills-bootstrap/src/App.vue deleted file mode 100644 index e4d6a81d..00000000 --- a/skills-bootstrap/src/App.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - diff --git a/skills-bootstrap/src/components/BootstrapContainer.vue b/skills-bootstrap/src/components/BootstrapContainer.vue deleted file mode 100644 index c983df39..00000000 --- a/skills-bootstrap/src/components/BootstrapContainer.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/components/BootstrapService.js b/skills-bootstrap/src/components/BootstrapService.js deleted file mode 100644 index 1ee25388..00000000 --- a/skills-bootstrap/src/components/BootstrapService.js +++ /dev/null @@ -1,16 +0,0 @@ -import axios from 'axios'; - -export default { - registerUser(loginFields) { - return axios.put('/createRootAccount', loginFields).then(response => response.data); - }, - grantRoot() { - return axios.post('/grantFirstRoot').then(response => response.data); - }, - isLoggedIn() { - return axios.get('/app/userInfo').then(response => response.data); - }, - userWithEmailExists(email) { - return axios.get(`/userExists/${email}`).then(response => !response.data); - }, -}; diff --git a/skills-bootstrap/src/components/ErrorPage.vue b/skills-bootstrap/src/components/ErrorPage.vue deleted file mode 100644 index 4587a16f..00000000 --- a/skills-bootstrap/src/components/ErrorPage.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/components/HomePage.vue b/skills-bootstrap/src/components/HomePage.vue deleted file mode 100644 index 0f4b6704..00000000 --- a/skills-bootstrap/src/components/HomePage.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/components/Initializing.vue b/skills-bootstrap/src/components/Initializing.vue deleted file mode 100644 index ed1a0ff7..00000000 --- a/skills-bootstrap/src/components/Initializing.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/components/RootPki.vue b/skills-bootstrap/src/components/RootPki.vue deleted file mode 100644 index 1d5d6e53..00000000 --- a/skills-bootstrap/src/components/RootPki.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/components/RootRegistration.vue b/skills-bootstrap/src/components/RootRegistration.vue deleted file mode 100644 index 45f3809f..00000000 --- a/skills-bootstrap/src/components/RootRegistration.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/components/Success.vue b/skills-bootstrap/src/components/Success.vue deleted file mode 100644 index 3225d8a7..00000000 --- a/skills-bootstrap/src/components/Success.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - - - diff --git a/skills-bootstrap/src/main.js b/skills-bootstrap/src/main.js deleted file mode 100644 index 4bd8fdd9..00000000 --- a/skills-bootstrap/src/main.js +++ /dev/null @@ -1,12 +0,0 @@ -import Vue from 'vue'; -import VeeValidate from 'vee-validate'; -import App from './App'; - -Vue.use(VeeValidate); -Vue.config.productionTip = false; - -new Vue({ // eslint-disable-line no-new - el: '#app', - components: { App }, - template: '', -}); diff --git a/skills-bootstrap/src/styles/palette.scss b/skills-bootstrap/src/styles/palette.scss deleted file mode 100644 index 38713136..00000000 --- a/skills-bootstrap/src/styles/palette.scss +++ /dev/null @@ -1,27 +0,0 @@ -// Set your brand colors -$blue-palette-color1: #36899b; -$blue-palette-color2: #3a98c4; -$blue-palette-color3: #59a1ea; -$blue-palette-color4: #64bde0; -$blue-palette-color5: #4b71d1; - -$green-palette-color1: #0ba08c; -$green-palette-color2: #0e933b; -$green-palette-color3: #0A9B77; -$green-palette-color4: #0b7c79; -$green-palette-color5: #008215; - -$monochrome-palette-color1: #333333; -$monochrome-palette-color2: #141414; -$monochrome-palette-color3: #161616; -$monochrome-palette-color4: #262626; -$monochrome-palette-color5: #282828; - -$red-palette-color1: #8c0009; -$red-palette-color2: #9e032c; -$red-palette-color3: #a0170b; -$red-palette-color4: #b50c03; -$red-palette-color5: #b20c1d; - -$background-color1: #f5f6f1; -$background-color2: #FFFFFF; diff --git a/skills-bootstrap/vue.config.js b/skills-bootstrap/vue.config.js deleted file mode 100644 index 4b71315f..00000000 --- a/skills-bootstrap/vue.config.js +++ /dev/null @@ -1,48 +0,0 @@ -const path = require('path'); - -const resolve = dir => path.join(__dirname, dir); - -const proxyConf = { - target: 'http://localhost:8080', - changeOrigin: true, - logLevel: 'debug', -}; - -module.exports = { - devServer: { - host: 'localhost', - port: 8082, - overlay: true, - proxy: { - '/admin': proxyConf, - '/app': proxyConf, - '/api': proxyConf, - '/server': proxyConf, - '/icons': proxyConf, - '/performLogin': proxyConf, - '/logout': proxyConf, - '/createAccount': proxyConf, - '/oauth2': proxyConf, - '/login': proxyConf, - '/static': proxyConf, - '/root': proxyConf, - '/createRootAccount': proxyConf, - '/userExists': proxyConf, - }, - }, - configureWebpack: { - resolve: { - alias: { - '@$': resolve('src'), - }, - }, - devtool: 'cheap-module-eval-source-map', - }, - publicPath: '.', - // outputDir: undefined, - // assetsDir: 'static', - runtimeCompiler: true, - // productionSourceMap: undefined, - // parallel: undefined, - // css: undefined, -};