From 4e1fa0f5d825fba3e269411f0e92654815dffa26 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sat, 3 Oct 2026 23:53:10 +0800 Subject: [PATCH 001/122] chore(build): prepare shared validation tools - manage compiler, surefire and coverage plugin versions - keep Java 8 output and use compatible Mockito 4 - resolve optional server repositories, refs and pins - verify checkout and package servers without install - share authenticated startup and filter AppleDouble files --- .../actions/get-hugegraph-commit/action.yml | 62 +++--- .../actions/get-hugegraph-commit/resolve.cjs | 52 +++++ .../get-hugegraph-commit/resolve.test.cjs | 73 +++++++ .../actions/setup-hugegraph-server/action.yml | 17 +- .../assembly/travis/checkout-server.sh | 47 +++++ .../assembly/travis/checkout-server.test.cjs | 58 ++++++ .../travis/install-hugegraph-from-source.sh | 59 ++---- .../assembly/travis/install-server.test.cjs | 196 ++++++++++++++++++ .../travis/start-hugegraph-servers.sh | 67 ++++++ pom.xml | 17 +- 10 files changed, 565 insertions(+), 83 deletions(-) create mode 100644 .github/actions/get-hugegraph-commit/resolve.cjs create mode 100644 .github/actions/get-hugegraph-commit/resolve.test.cjs create mode 100644 hugegraph-client/assembly/travis/checkout-server.sh create mode 100644 hugegraph-client/assembly/travis/checkout-server.test.cjs create mode 100644 hugegraph-client/assembly/travis/install-server.test.cjs create mode 100644 hugegraph-client/assembly/travis/start-hugegraph-servers.sh diff --git a/.github/actions/get-hugegraph-commit/action.yml b/.github/actions/get-hugegraph-commit/action.yml index b73b49451..da195ee84 100644 --- a/.github/actions/get-hugegraph-commit/action.yml +++ b/.github/actions/get-hugegraph-commit/action.yml @@ -6,6 +6,15 @@ inputs: description: 'HugeGraph Server PR number to use as integration baseline' required: false default: '' + repository: + description: 'GitHub owner/repository containing the server source' + default: 'apache/hugegraph' + ref: + description: 'Branch, tag or commit; mutually exclusive with server-pr-number' + default: '' + expected-commit: + description: 'Optional full commit SHA; fail if the selected ref has moved' + default: '' outputs: commit_id: @@ -23,44 +32,25 @@ runs: uses: actions/github-script@v9 with: script: | - const owner = 'apache'; - const repo = 'hugegraph'; - const serverPrNumber = '${{ inputs.server-pr-number }}'; + const resolve = require(process.env.RESOLVER_PATH); try { - if (serverPrNumber) { - const pull_number = Number(serverPrNumber); - if (!Number.isInteger(pull_number) || pull_number <= 0) { - throw new Error(`Invalid server PR number: ${serverPrNumber}`); - } - const { data: pull } = await github.rest.pulls.get({ - owner, repo, pull_number - }); - const sha = pull.head.sha; - const fetchRef = `refs/pull/${pull_number}/head`; - core.exportVariable('COMMIT_ID', sha); - core.exportVariable('COMMIT_REF', fetchRef); - core.setOutput('commit_id', sha); - core.setOutput('fetch_ref', fetchRef); - console.log(`Using HugeGraph Server PR #${pull_number} ${fetchRef} (${sha})`); - return; - } - const { data: release } = await github.rest.repos.getLatestRelease({ owner, repo }); - const tagName = release.tag_name; - const { data: ref } = await github.rest.git.getRef({ - owner, repo, ref: `tags/${tagName}` + const result = await resolve(github, { + repository: process.env.SERVER_REPOSITORY, + ref: process.env.SERVER_REF, + pr: process.env.SERVER_PR, + expectedCommit: process.env.SERVER_EXPECTED_COMMIT }); - let sha = ref.object.sha; - if (ref.object.type === 'tag') { - const { data: tag } = await github.rest.git.getTag({ owner, repo, tag_sha: sha }); - sha = tag.object.sha; - } else if (ref.object.type !== 'commit') { - throw new Error(`Unexpected ref type: ${ref.object.type}`); - } - core.exportVariable('COMMIT_ID', sha); - core.exportVariable('COMMIT_REF', `refs/tags/${tagName}`); - core.setOutput('commit_id', sha); - core.setOutput('fetch_ref', `refs/tags/${tagName}`); - console.log(`Using HugeGraph release ${tagName} (${sha})`); + core.exportVariable('COMMIT_ID', result.sha); + core.exportVariable('COMMIT_REF', result.fetchRef); + core.setOutput('commit_id', result.sha); + core.setOutput('fetch_ref', result.fetchRef); + core.info(`Server baseline: ${process.env.SERVER_REPOSITORY} ${result.fetchRef} (${result.sha})`); } catch (error) { core.setFailed(`Failed to get HugeGraph commit: ${error.message}`); } + env: + RESOLVER_PATH: ${{ github.action_path }}/resolve.cjs + SERVER_REPOSITORY: ${{ inputs.repository }} + SERVER_REF: ${{ inputs.ref }} + SERVER_PR: ${{ inputs.server-pr-number }} + SERVER_EXPECTED_COMMIT: ${{ inputs.expected-commit }} diff --git a/.github/actions/get-hugegraph-commit/resolve.cjs b/.github/actions/get-hugegraph-commit/resolve.cjs new file mode 100644 index 000000000..050637950 --- /dev/null +++ b/.github/actions/get-hugegraph-commit/resolve.cjs @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +module.exports = async function resolve(github, { repository, ref = '', pr = '', expectedCommit = '' }) { + if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(repository)) { + throw new Error('Invalid server repository'); + } + if (expectedCommit && !/^[0-9a-f]{40}$/i.test(expectedCommit)) { + throw new Error('Expected commit must be a full SHA'); + } + if (ref && pr) throw new Error('Select either ref or server-pr-number'); + const [owner, repo] = repository.split('/'); + let sha; + let fetchRef; + if (pr) { + if (!/^[1-9][0-9]*$/.test(pr) || !Number.isSafeInteger(Number(pr))) { + throw new Error('Invalid server PR number'); + } + const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number: Number(pr) }); + sha = pull.head.sha; + fetchRef = `refs/pull/${pr}/head`; + } else { + if (ref) { + fetchRef = ref; + } else { + const { data: release } = await github.rest.repos.getLatestRelease({ owner, repo }); + fetchRef = `refs/tags/${release.tag_name}`; + } + // The commits API peels annotated tags, including nested tags, to a commit. + const { data: commit } = await github.rest.repos.getCommit({ owner, repo, ref: fetchRef }); + sha = commit.sha; + } + if (!/^[0-9a-f]{40}$/i.test(sha)) throw new Error('Server API returned an invalid commit SHA'); + if (expectedCommit && sha.toLowerCase() !== expectedCommit.toLowerCase()) { + throw new Error(`Server ref moved: expected ${expectedCommit}, resolved ${sha}`); + } + return { sha, fetchRef }; +}; diff --git a/.github/actions/get-hugegraph-commit/resolve.test.cjs b/.github/actions/get-hugegraph-commit/resolve.test.cjs new file mode 100644 index 000000000..788be95a5 --- /dev/null +++ b/.github/actions/get-hugegraph-commit/resolve.test.cjs @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const resolve = require('./resolve.cjs'); +const sha = 'a'.repeat(40); + +function client() { + const calls = []; + const github = { rest: { + pulls: { get: async args => { calls.push(['pr', args]); return { data: { head: { sha } } }; } }, + repos: { + getLatestRelease: async args => { calls.push(['release', args]); return { data: { tag_name: 'v1.7.0' } }; }, + getCommit: async args => { calls.push(['commit', args]); return { data: { sha } }; } + } + } }; + return { github, calls }; +} + +test('resolves explicit organization branch and verifies the pin', async () => { + const { github, calls } = client(); + assert.deepEqual(await resolve(github, { repository: 'example/server', + ref: 'integration-check', expectedCommit: sha }), + { sha, fetchRef: 'integration-check' }); + assert.deepEqual(calls, [['commit', { owner: 'example', repo: 'server', ref: 'integration-check' }]]); +}); + +test('retains latest-release selection for existing callers', async () => { + const { github, calls } = client(); + assert.deepEqual(await resolve(github, { repository: 'apache/hugegraph' }), + { sha, fetchRef: 'refs/tags/v1.7.0' }); + assert.deepEqual(calls, [['release', { owner: 'apache', repo: 'hugegraph' }], + ['commit', { owner: 'apache', repo: 'hugegraph', ref: 'refs/tags/v1.7.0' }]]); +}); + +test('resolves PR head from the selected repository', async () => { + const { github, calls } = client(); + assert.deepEqual(await resolve(github, { repository: 'example/server', pr: '42' }), + { sha, fetchRef: 'refs/pull/42/head' }); + assert.deepEqual(calls, [['pr', { owner: 'example', repo: 'server', pull_number: 42 }]]); +}); + +test('rejects invalid or ambiguous selectors before calling GitHub', async () => { + for (const input of [{ repository: '../repo' }, { ref: 'master', pr: '42' }, + { pr: '1.5' }, { pr: '0' }, { expectedCommit: 'short' }]) { + const { github, calls } = client(); + await assert.rejects(resolve(github, { repository: 'apache/hugegraph', ...input })); + assert.equal(calls.length, 0); + } +}); + +test('fails closed for a moved branch or API error', async () => { + const { github } = client(); + await assert.rejects(resolve(github, { repository: 'example/server', ref: 'master', + expectedCommit: '0'.repeat(40) }), /Server ref moved/); + github.rest.repos.getCommit = async () => { throw new Error('Not Found'); }; + await assert.rejects(resolve(github, { repository: 'example/server', ref: 'missing' }), /Not Found/); +}); diff --git a/.github/actions/setup-hugegraph-server/action.yml b/.github/actions/setup-hugegraph-server/action.yml index ff142ed1a..cabd8f8b0 100644 --- a/.github/actions/setup-hugegraph-server/action.yml +++ b/.github/actions/setup-hugegraph-server/action.yml @@ -9,10 +9,25 @@ inputs: description: 'HugeGraph commit SHA to install' required: true + repository: + description: 'GitHub owner/repository containing the server source' + default: 'apache/hugegraph' + fetch-ref: + description: 'Fetchable ref; the installer verifies its HEAD against commit-id' + default: '' + runs: using: composite steps: - name: Prepare env and service shell: bash + env: + TRAVIS_SCRIPT_DIR: ${{ inputs.travis-dir }} + SERVER_COMMIT: ${{ inputs.commit-id }} + SERVER_REPOSITORY: ${{ inputs.repository }} + SERVER_FETCH_REF: ${{ inputs.fetch-ref }} run: | - ${{ inputs.travis-dir }}/install-hugegraph-from-source.sh ${{ inputs.commit-id }} + installer=$(realpath "${TRAVIS_SCRIPT_DIR}/install-hugegraph-from-source.sh") + server_build=$(mktemp -d "$RUNNER_TEMP/hugegraph-build.XXXXXX") + cd "$server_build" + bash "$installer" "$SERVER_COMMIT" diff --git a/hugegraph-client/assembly/travis/checkout-server.sh b/hugegraph-client/assembly/travis/checkout-server.sh new file mode 100644 index 000000000..53b543f0f --- /dev/null +++ b/hugegraph-client/assembly/travis/checkout-server.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. +# +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: checkout-server.sh " >&2 + exit 1 +fi + +COMMIT_ID=$1 +CHECKOUT_DIR=$2 +SERVER_REPOSITORY=${SERVER_REPOSITORY:-apache/hugegraph} +SERVER_FETCH_REF=${SERVER_FETCH_REF:-$COMMIT_ID} +if [[ ! "$COMMIT_ID" =~ ^[0-9a-fA-F]{40}$ ]] || + [[ ! "$SERVER_REPOSITORY" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]*/[a-zA-Z0-9][a-zA-Z0-9_.-]*$ ]]; then + echo "Expected a full commit SHA and GitHub owner/repository" >&2 + exit 1 +fi + +# Fetch a reachable ref, then verify it against the resolved SHA before building. +# A moving branch or PR must fail closed rather than silently changing the baseline. +[[ ! -e "$CHECKOUT_DIR" ]] || { echo "Checkout directory already exists" >&2; exit 1; } +git init "$CHECKOUT_DIR" +git -C "$CHECKOUT_DIR" remote add origin "https://github.com/${SERVER_REPOSITORY}.git" +git -C "$CHECKOUT_DIR" fetch --depth 1 -- origin "$SERVER_FETCH_REF" +git -C "$CHECKOUT_DIR" checkout --detach FETCH_HEAD +ACTUAL_COMMIT=$(git -C "$CHECKOUT_DIR" rev-parse HEAD) +if [[ "$ACTUAL_COMMIT" != "$(printf '%s' "$COMMIT_ID" | tr '[:upper:]' '[:lower:]')" ]]; then + echo "Server checkout mismatch: expected $COMMIT_ID, got $ACTUAL_COMMIT" >&2 + exit 1 +fi +printf 'Server source: %s@%s\n' "$SERVER_REPOSITORY" "$ACTUAL_COMMIT" diff --git a/hugegraph-client/assembly/travis/checkout-server.test.cjs b/hugegraph-client/assembly/travis/checkout-server.test.cjs new file mode 100644 index 000000000..36a83e6e1 --- /dev/null +++ b/hugegraph-client/assembly/travis/checkout-server.test.cjs @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { execFileSync, spawnSync } = require('node:child_process'); +const { mkdtempSync, rmSync, writeFileSync, existsSync } = require('node:fs'); +const { tmpdir } = require('node:os'); +const { join } = require('node:path'); + +test('checkout verifies actual git HEAD and refuses missing/moving refs and existing directories', t => { + const root = mkdtempSync(join(tmpdir(), 'server-checkout-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + const source = join(root, 'source'); + const git = (...args) => execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); + git('init', source); + git('-C', source, 'config', 'user.name', 'Fixture'); + git('-C', source, 'config', 'user.email', 'fixture@example.invalid'); + writeFileSync(join(source, 'file'), 'one'); + git('-C', source, 'add', 'file'); + git('-C', source, 'commit', '-m', 'initial'); + git('-C', source, 'branch', '-M', 'candidate'); + const first = git('-C', source, 'rev-parse', 'HEAD'); + const env = { ...process.env, SERVER_REPOSITORY: 'example/server', SERVER_FETCH_REF: 'candidate', + GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: `url.file://${source}.insteadOf`, + GIT_CONFIG_VALUE_0: 'https://github.com/example/server.git' }; + const checkout = (name, sha, extra = {}) => spawnSync('bash', + [join(__dirname, 'checkout-server.sh'), sha, join(root, name)], + { env: { ...env, ...extra }, encoding: 'utf8' }); + const result = checkout('success', first); + assert.equal(result.status, 0, result.stderr); + assert.equal(git('-C', join(root, 'success'), 'rev-parse', 'HEAD'), first); + assert.notEqual(checkout('success', first).status, 0); + assert.notEqual(checkout('missing', first, { SERVER_FETCH_REF: 'absent' }).status, 0); + writeFileSync(join(source, 'file'), 'two'); + git('-C', source, 'commit', '-am', 'advance'); + const moved = checkout('moved', first); + assert.notEqual(moved.status, 0); + assert.match(moved.stderr, /Server checkout mismatch/); + assert.notEqual(checkout('invalid', 'short').status, 0); + assert.equal(existsSync(join(root, 'invalid')), false); + assert.notEqual(checkout('bad-repo', first, { SERVER_REPOSITORY: '../server' }).status, 0); + assert.equal(existsSync(join(root, 'bad-repo')), false); +}); diff --git a/hugegraph-client/assembly/travis/install-hugegraph-from-source.sh b/hugegraph-client/assembly/travis/install-hugegraph-from-source.sh index d7b74aff9..1c2b0e876 100755 --- a/hugegraph-client/assembly/travis/install-hugegraph-from-source.sh +++ b/hugegraph-client/assembly/travis/install-hugegraph-from-source.sh @@ -15,51 +15,20 @@ # License for the specific language governing permissions and limitations # under the License. # -set -ev +set -euo pipefail -if [[ $# -ne 1 ]]; then - echo "Must input an existing commit id of hugegraph server" && exit 1 +if [[ $# -lt 1 || $# -gt 2 || ( $# -eq 2 && "$2" != "--build-only" ) ]]; then + echo "Usage: install-hugegraph-from-source.sh [--build-only]" >&2 + exit 1 fi -COMMIT_ID=$1 -HUGEGRAPH_GIT_URL="https://github.com/apache/hugegraph.git" - -git clone --depth 150 ${HUGEGRAPH_GIT_URL} hugegraph -cd hugegraph -git checkout "${COMMIT_ID}" -mvn package -DskipTests -Dmaven.javadoc.skip=true -ntp -cd hugegraph-server -mv apache-hugegraph-*.tar.gz ../../ -cd ../../ -rm -rf hugegraph -tar zxf apache-hugegraph-*.tar.gz - -HTTPS_SERVER_DIR="hugegraph_https" -mkdir ${HTTPS_SERVER_DIR} -cp -r apache-hugegraph-*/. ${HTTPS_SERVER_DIR} -cd "$(find apache-hugegraph-* | head -1)" -# start HugeGraphServer with http protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh || exit 1 - -# Wait for server to initialize -echo "Waiting 5 seconds for HugeGraph server to initialize..." -sleep 5 - -cd ../${HTTPS_SERVER_DIR} -REST_SERVER_CONFIG="conf/rest-server.properties" -GREMLIN_SERVER_CONFIG="conf/gremlin-server.yaml" -sed -i "s?http://127.0.0.1:8080?https://127.0.0.1:8443?g" "$REST_SERVER_CONFIG" -sed -i "s/#port: 8182/port: 8282/g" "$GREMLIN_SERVER_CONFIG" -echo "gremlinserver.url=http://127.0.0.1:8282" >> ${REST_SERVER_CONFIG} - -# start HugeGraphServer with https protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh -cd ../ +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +bash "$SCRIPT_DIR/checkout-server.sh" "$1" hugegraph +# Build the server distribution without replacing published client dependencies +# in the caller's Maven repository. --build-only skips server startup. +(cd hugegraph && mvn package -DskipTests -Dmaven.javadoc.skip=true -ntp) +if [[ "${2:-}" != "--build-only" ]]; then + ARCHIVES=(hugegraph/hugegraph-server/apache-hugegraph-*.tar.gz) + [[ ${#ARCHIVES[@]} -eq 1 && -f "${ARCHIVES[0]}" ]] + bash "$SCRIPT_DIR/start-hugegraph-servers.sh" "${ARCHIVES[0]}" +fi diff --git a/hugegraph-client/assembly/travis/install-server.test.cjs b/hugegraph-client/assembly/travis/install-server.test.cjs new file mode 100644 index 000000000..6a6a11f0c --- /dev/null +++ b/hugegraph-client/assembly/travis/install-server.test.cjs @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { execFileSync, spawnSync } = require('node:child_process'); +const { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, readdirSync, existsSync } = require('node:fs'); +const { tmpdir } = require('node:os'); +const { join } = require('node:path'); + +function temp(t) { + const root = mkdtempSync(join(tmpdir(), 'server-install-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + return root; +} + +test('build-only packages the server without installing reactor dependencies or starting it', t => { + const root = temp(t); + const source = join(root, 'source'); + const git = (...args) => execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); + git('init', source); + git('-C', source, 'config', 'user.name', 'Fixture'); + git('-C', source, 'config', 'user.email', 'fixture@example.invalid'); + writeFileSync(join(source, 'pom.xml'), 'fixture'); + git('-C', source, 'add', 'pom.xml'); + git('-C', source, 'commit', '-m', 'initial'); + const sha = git('-C', source, 'rev-parse', 'HEAD'); + const bin = join(root, 'bin'); + mkdirSync(bin); + writeFileSync(join(bin, 'mvn'), '#!/bin/bash\nprintf "%s\\n" "$@" > "$BUILD_ARGS"\nprintf "%s" "$MAVEN_ARGS" > "$BUILD_REPO"\n', { mode: 0o755 }); + const env = { ...process.env, SERVER_REPOSITORY: 'example/server', SERVER_FETCH_REF: sha, + PATH: `${bin}:${process.env.PATH}`, BUILD_ARGS: join(root, 'args'), BUILD_REPO: join(root, 'repo'), + MAVEN_ARGS: `-Dmaven.repo.local=${root}/isolated-m2`, GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: `url.file://${source}.insteadOf`, GIT_CONFIG_VALUE_0: 'https://github.com/example/server.git' }; + const run = args => spawnSync('bash', [join(__dirname, 'install-hugegraph-from-source.sh'), ...args], + { cwd: root, env, encoding: 'utf8' }); + assert.notEqual(run([sha, '--unknown']).status, 0); + assert.equal(existsSync(join(root, 'hugegraph')), false); + const result = run([sha, '--build-only']); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(readFileSync(env.BUILD_ARGS, 'utf8').trim().split('\n'), + ['package', '-DskipTests', '-Dmaven.javadoc.skip=true', '-ntp']); + assert.equal(readFileSync(env.BUILD_REPO, 'utf8'), env.MAVEN_ARGS); + // No archive exists: success proves build-only did not attempt extraction/startup. + assert.equal(readdirSync(root).some(name => name.startsWith('hugegraph-servers.')), false); +}); + +function serverFixture(fixture) { + mkdirSync(join(fixture, 'conf/graphs'), { recursive: true }); + mkdirSync(join(fixture, 'bin')); + writeFileSync(join(fixture, 'conf/graphs/hugegraph.properties'), + 'gremlin.graph=org.apache.hugegraph.HugeFactory\n'); + writeFileSync(join(fixture, 'conf/rest-server.properties'), + 'restserver.url=http://127.0.0.1:8080\n#auth.authenticator=none\n#auth.admin_pa=none\n'); + writeFileSync(join(fixture, 'conf/gremlin-server.yaml'), '#port: 8182\n'); + writeFileSync(join(fixture, 'bin/init-store.sh'), + '#!/bin/bash\nset -e\nshopt -s dotglob\nread -r password\n[[ "$password" == pa ]]\n' + + 'for file in conf/graphs/*.properties; do\n' + + ' name=$(basename "$file" .properties)\n' + + ' if [[ ! "$name" =~ ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ ]]; then\n' + + ' echo "Invalid graph name: $name" >&2\n exit 1\n fi\n' + + 'done\nprintf "%s" "$JAVA_HOME" > initialized-with-jvm\n', { mode: 0o755 }); + writeFileSync(join(fixture, 'bin/start-hugegraph.sh'), + '#!/bin/bash\nprintf "%s" "$JAVA_HOME" > started-with-jvm\n', { mode: 0o755 }); +} + +function runStarter(root, entries) { + const archive = join(root, 'server.tar.gz'); + // Keep metadata entries as files on macOS too, matching the Linux CI extractor. + const env = { ...process.env, COPYFILE_DISABLE: '1', RUNNER_TEMP: root, JAVA_HOME: '/fixture/java11' }; + if (process.platform === 'darwin') { + const bin = join(root, 'tar-bin'); + mkdirSync(bin); + writeFileSync(join(bin, 'tar'), + '#!/bin/bash\n[[ "$1" == -* ]] || set -- "-$1" "${@:2}"\n' + + 'exec /usr/bin/tar --no-mac-metadata "$@"\n', { mode: 0o755 }); + env.PATH = `${bin}:${env.PATH}`; + } + execFileSync('tar', ['czf', archive, '-C', root, ...entries], { env }); + const result = spawnSync('bash', [join(__dirname, 'start-hugegraph-servers.sh'), archive], + { env, encoding: 'utf8' }); + const serverRoot = join(root, readdirSync(root).find(name => name.startsWith('hugegraph-servers.'))); + return { result, serverRoot }; +} + +for (const layout of [ + { name: 'direct candidate', top: 'apache-hugegraph-fixture', server: 'apache-hugegraph-fixture' }, + { name: 'official aggregate', top: 'apache-hugegraph-incubating-1.7.0', + server: 'apache-hugegraph-incubating-1.7.0/apache-hugegraph-server-incubating-1.7.0' } +]) { + test(`shared starter launches ${layout.name} HTTP/auth and HTTPS with caller JVM`, t => { + const root = temp(t); + serverFixture(join(root, layout.server)); + if (layout.server !== layout.top) { + // Sibling components in the real release are not server candidates. + mkdirSync(join(root, layout.top, 'apache-hugegraph-pd-incubating-1.7.0')); + mkdirSync(join(root, layout.top, 'apache-hugegraph-store-incubating-1.7.0')); + } + const { result, serverRoot } = runStarter(root, [layout.top]); + assert.equal(result.status, 0, result.stderr); + for (const dir of [layout.server, 'hugegraph_https']) { + const deployed = join(serverRoot, dir); + assert.equal(readFileSync(join(deployed, 'started-with-jvm'), 'utf8'), '/fixture/java11'); + assert.equal(readFileSync(join(deployed, 'initialized-with-jvm'), 'utf8'), '/fixture/java11'); + assert.match(readFileSync(join(deployed, 'conf/graphs/hugegraph.properties'), 'utf8'), /HugeFactoryAuthProxy/); + const rest = readFileSync(join(deployed, 'conf/rest-server.properties'), 'utf8'); + assert.match(rest, /auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator/); + assert.match(rest, /auth.admin_pa=pa/); + assert.match(rest, /batch.max_vertices_per_batch=500/); + assert.match(rest, /batch.max_edges_per_batch=500/); + assert.match(rest, dir === 'hugegraph_https' ? /https:\/\/127.0.0.1:8443/ : /http:\/\/127.0.0.1:8080/); + } + assert.match(readFileSync(join(serverRoot, 'hugegraph_https/conf/gremlin-server.yaml'), 'utf8'), /^port: 8282/m); + assert.equal(existsSync(join(serverRoot, 'hugegraph_https/apache-hugegraph-pd-incubating-1.7.0')), false); + }); +} + +test('shared starter rejects multiple runnable server distributions before starting either', t => { + const root = temp(t); + const top = 'apache-hugegraph-ambiguous'; + const paths = [top, `${top}/apache-hugegraph-server-nested`]; + paths.forEach(path => serverFixture(join(root, path))); + const { result, serverRoot } = runStarter(root, [top]); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Expected exactly one runnable server distribution, found 2/); + for (const path of paths) { + assert.equal(existsSync(join(serverRoot, path, 'initialized-with-jvm')), false); + assert.equal(existsSync(join(serverRoot, path, 'started-with-jvm')), false); + } + assert.equal(existsSync(join(serverRoot, 'hugegraph_https')), false); +}); + +test('shared starter rejects an incomplete server before initialization', t => { + const root = temp(t); + const top = 'apache-hugegraph-incomplete'; + serverFixture(join(root, top)); + rmSync(join(root, top, 'conf/graphs/hugegraph.properties')); + const { result, serverRoot } = runStarter(root, [top]); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Expected exactly one runnable server distribution, found 0/); + assert.equal(existsSync(join(serverRoot, top, 'initialized-with-jvm')), false); + assert.equal(existsSync(join(serverRoot, top, 'started-with-jvm')), false); + assert.equal(existsSync(join(serverRoot, 'hugegraph_https')), false); +}); + + +test('shared starter removes AppleDouble sidecars without replacing released content', t => { + const root = temp(t); + const top = 'apache-hugegraph-incubating-1.7.0'; + const server = `${top}/apache-hugegraph-server-incubating-1.7.0`; + serverFixture(join(root, server)); + mkdirSync(join(root, server, 'lib')); + // GNU tar retains these macOS sidecars; ConfigUtil scans every *.properties. + writeFileSync(join(root, server, 'conf/graphs/._hugegraph.properties'), 'metadata sidecar'); + writeFileSync(join(root, server, 'lib/._released.jar'), 'metadata sidecar'); + writeFileSync(join(root, server, 'lib/released.jar'), 'original released binary'); + writeFileSync(join(root, server, 'conf/.keep'), 'ordinary dotfile'); + writeFileSync(join(root, '._outside.properties'), 'outside the extracted distribution'); + const { result, serverRoot } = runStarter(root, [top]); + assert.equal(result.status, 0, result.stderr); + for (const dir of [server, 'hugegraph_https']) { + const deployed = join(serverRoot, dir); + assert.equal(readFileSync(join(deployed, 'started-with-jvm'), 'utf8'), '/fixture/java11'); + assert.equal(existsSync(join(deployed, 'conf/graphs/._hugegraph.properties')), false); + assert.equal(existsSync(join(deployed, 'lib/._released.jar')), false); + assert.equal(readFileSync(join(deployed, 'lib/released.jar'), 'utf8'), 'original released binary'); + assert.equal(readFileSync(join(deployed, 'conf/.keep'), 'utf8'), 'ordinary dotfile'); + assert.match(readFileSync(join(deployed, 'conf/graphs/hugegraph.properties'), 'utf8'), /HugeFactoryAuthProxy/); + } + assert.equal(readFileSync(join(root, '._outside.properties'), 'utf8'), 'outside the extracted distribution'); +}); + +test('shared starter still rejects invalid non-metadata graph names', t => { + const root = temp(t); + const top = 'apache-hugegraph-invalid-graph'; + serverFixture(join(root, top)); + writeFileSync(join(root, top, 'conf/graphs/9invalid.properties'), 'invalid graph configuration'); + const { result, serverRoot } = runStarter(root, [top]); + assert.notEqual(result.status, 0); + assert.equal(existsSync(join(serverRoot, top, 'conf/graphs/9invalid.properties')), true); + assert.equal(existsSync(join(serverRoot, top, 'started-with-jvm')), false); +}); diff --git a/hugegraph-client/assembly/travis/start-hugegraph-servers.sh b/hugegraph-client/assembly/travis/start-hugegraph-servers.sh new file mode 100644 index 000000000..0418f9c80 --- /dev/null +++ b/hugegraph-client/assembly/travis/start-hugegraph-servers.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. +# +set -euo pipefail + +if [[ $# -ne 1 || ! -f "$1" ]]; then + echo "Usage: start-hugegraph-servers.sh " >&2 + exit 1 +fi + +# Both baselines use the same authenticated HTTP and HTTPS test configuration. +# JAVA_HOME belongs to the server process only; callers retain their client JVM. +SERVER_ROOT=$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/hugegraph-servers.XXXXXX") +tar zxf "$1" -C "$SERVER_ROOT" +# macOS-built release archives include AppleDouble sidecars. GNU tar retains +# them, and graph discovery mistakes ._hugegraph.properties for a graph. +find "$SERVER_ROOT" -type f -name '._*' -delete +# Candidate archives contain the server directly; official aggregate releases +# put it one level below the distribution root, alongside PD and Store. +SERVER_DIRS=() +for dir in "$SERVER_ROOT"/apache-hugegraph-*/ "$SERVER_ROOT"/apache-hugegraph-*/*/; do + if [[ -f "$dir/conf/graphs/hugegraph.properties" && + -f "$dir/conf/rest-server.properties" && + -f "$dir/conf/gremlin-server.yaml" && + -x "$dir/bin/init-store.sh" && -x "$dir/bin/start-hugegraph.sh" ]]; then + SERVER_DIRS+=("$dir") + fi +done +if [[ ${#SERVER_DIRS[@]} -ne 1 ]]; then + printf 'Expected exactly one runnable server distribution, found %s\n' "${#SERVER_DIRS[@]}" >&2 + exit 1 +fi +cp -r "${SERVER_DIRS[0]}" "$SERVER_ROOT/hugegraph_https" +printf 'Server directories: %s\n' "$SERVER_ROOT" + +start_server() ( + cd "$1" + sed -i.bak 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties + sed -i.bak 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties + sed -i.bak 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties + # Boundary tests submit 501 edges / 1000 vertices and expect rejection. + # Pin their fixture limit instead of depending on changing server defaults. + printf '\nbatch.max_vertices_per_batch=500\nbatch.max_edges_per_batch=500\n' >> conf/rest-server.properties + printf 'pa\n' | bin/init-store.sh + bin/start-hugegraph.sh +) + +start_server "${SERVER_DIRS[0]}" +HTTPS_DIR="$SERVER_ROOT/hugegraph_https" +sed -i.bak 's?http://127.0.0.1:8080?https://127.0.0.1:8443?g' "$HTTPS_DIR/conf/rest-server.properties" +sed -i.bak 's/#port: 8182/port: 8282/g' "$HTTPS_DIR/conf/gremlin-server.yaml" +printf '\ngremlinserver.url=http://127.0.0.1:8282\n' >> "$HTTPS_DIR/conf/rest-server.properties" +start_server "$HTTPS_DIR" diff --git a/pom.xml b/pom.xml index 92c95ecdf..279663f67 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,9 @@ 1.8 1.8 1.8 + 3.15.0 + 3.5.6 + 0.8.15 1.13.5 2.2.1 3.2.2 @@ -128,7 +131,7 @@ 3.3.1 3.6.2 4.12 - 2.25.1 + 4.11.0 1.18.32 2.8.0 3.9 @@ -595,6 +598,7 @@ **/*.txt **/.flattened-pom.xml **/node_modules/**/* + **/__pycache__/** **/dist/**/* **/.vinext/**/* **/.wrangler/**/* @@ -619,6 +623,7 @@ org.apache.maven.plugins maven-compiler-plugin + ${maven.compiler.plugin.version} ${compiler.source} ${compiler.target} @@ -630,6 +635,16 @@ + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + From ef29d9c8efd75fa9e8618186311810cc542757c2 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 00:42:08 +0800 Subject: [PATCH 002/122] fix(ci): forward server source selection - connect repository and fetch refs to all server installers - reuse the shared installer for Tools and Spark validation - preserve Loader source caching with verified checkouts - isolate fixture builds and retain separate HTTPS RPC ports --- .../actions/get-hugegraph-commit/action.yml | 3 + .github/workflows/client-ci.yml | 2 + .github/workflows/client-go-ci.yml | 2 + .github/workflows/loader-ci.yml | 7 +- .github/workflows/spark-connector-ci.yml | 2 + .github/workflows/tools-ci.yml | 2 + .../assembly/travis/install-server.test.cjs | 65 ++++++++++++++++++- .../travis/start-hugegraph-servers.sh | 1 + .../travis/install-hugegraph-from-source.sh | 42 +++--------- .../travis/install-hugegraph-from-source.sh | 47 +------------- .../travis/install-hugegraph-from-source.sh | 47 +------------- 11 files changed, 95 insertions(+), 125 deletions(-) diff --git a/.github/actions/get-hugegraph-commit/action.yml b/.github/actions/get-hugegraph-commit/action.yml index da195ee84..b07b45c9e 100644 --- a/.github/actions/get-hugegraph-commit/action.yml +++ b/.github/actions/get-hugegraph-commit/action.yml @@ -17,6 +17,9 @@ inputs: default: '' outputs: + repository: + description: 'GitHub repository selected as the server baseline' + value: ${{ inputs.repository }} commit_id: description: 'The HugeGraph commit SHA selected as integration baseline' value: ${{ steps.get-commit.outputs.commit_id }} diff --git a/.github/workflows/client-ci.yml b/.github/workflows/client-ci.yml index 90d7e8110..4f1f6bcaa 100644 --- a/.github/workflows/client-ci.yml +++ b/.github/workflows/client-ci.yml @@ -52,6 +52,8 @@ jobs: with: travis-dir: ${{ env.TRAVIS_DIR }} commit-id: ${{ steps.get-commit.outputs.commit_id }} + repository: ${{ steps.get-commit.outputs.repository }} + fetch-ref: ${{ steps.get-commit.outputs.fetch_ref }} - name: Compile run: | diff --git a/.github/workflows/client-go-ci.yml b/.github/workflows/client-go-ci.yml index 520d92d95..11d5f36a4 100644 --- a/.github/workflows/client-go-ci.yml +++ b/.github/workflows/client-go-ci.yml @@ -50,6 +50,8 @@ jobs: with: travis-dir: ${{ env.TRAVIS_DIR }} commit-id: ${{ steps.get-commit.outputs.commit_id }} + repository: ${{ steps.get-commit.outputs.repository }} + fetch-ref: ${{ steps.get-commit.outputs.fetch_ref }} - name: Init Go env uses: actions/setup-go@v6 diff --git a/.github/workflows/loader-ci.yml b/.github/workflows/loader-ci.yml index 8389ecab6..351b97ddd 100644 --- a/.github/workflows/loader-ci.yml +++ b/.github/workflows/loader-ci.yml @@ -77,9 +77,14 @@ jobs: - name: Prepare env and service env: COMMIT_ID: ${{ steps.get-commit.outputs.commit_id }} + SERVER_REPOSITORY: ${{ steps.get-commit.outputs.repository }} + SERVER_FETCH_REF: ${{ steps.get-commit.outputs.fetch_ref }} run: | $TRAVIS_DIR/install-hadoop.sh - $TRAVIS_DIR/install-hugegraph-from-source.sh $COMMIT_ID + installer=$(realpath "$TRAVIS_DIR/install-hugegraph-from-source.sh") + server_build=$(mktemp -d "$RUNNER_TEMP/hugegraph-build.XXXXXX") + cd "$server_build" + bash "$installer" "$COMMIT_ID" - name: Run test run: | diff --git a/.github/workflows/spark-connector-ci.yml b/.github/workflows/spark-connector-ci.yml index 50693024a..99316c708 100644 --- a/.github/workflows/spark-connector-ci.yml +++ b/.github/workflows/spark-connector-ci.yml @@ -55,6 +55,8 @@ jobs: with: travis-dir: ${{ env.TRAVIS_DIR }} commit-id: ${{ steps.get-commit.outputs.commit_id }} + repository: ${{ steps.get-commit.outputs.repository }} + fetch-ref: ${{ steps.get-commit.outputs.fetch_ref }} - name: Run test run: | diff --git a/.github/workflows/tools-ci.yml b/.github/workflows/tools-ci.yml index 4613bcd0f..98475c03d 100644 --- a/.github/workflows/tools-ci.yml +++ b/.github/workflows/tools-ci.yml @@ -55,6 +55,8 @@ jobs: with: travis-dir: ${{ env.TRAVIS_DIR }} commit-id: ${{ steps.get-commit.outputs.commit_id }} + repository: ${{ steps.get-commit.outputs.repository }} + fetch-ref: ${{ steps.get-commit.outputs.fetch_ref }} - name: Run test run: | diff --git a/hugegraph-client/assembly/travis/install-server.test.cjs b/hugegraph-client/assembly/travis/install-server.test.cjs index 6a6a11f0c..1c234d802 100644 --- a/hugegraph-client/assembly/travis/install-server.test.cjs +++ b/hugegraph-client/assembly/travis/install-server.test.cjs @@ -28,7 +28,12 @@ function temp(t) { return root; } -test('build-only packages the server without installing reactor dependencies or starting it', t => { +for (const installer of [ + join(__dirname, 'install-hugegraph-from-source.sh'), + join(__dirname, '../../../hugegraph-tools/assembly/travis/install-hugegraph-from-source.sh'), + join(__dirname, '../../../hugegraph-spark-connector/assembly/travis/install-hugegraph-from-source.sh') +]) { +test(`build-only ${installer} preserves source selection and packages without dependency installation`, t => { const root = temp(t); const source = join(root, 'source'); const git = (...args) => execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); @@ -46,7 +51,7 @@ test('build-only packages the server without installing reactor dependencies or PATH: `${bin}:${process.env.PATH}`, BUILD_ARGS: join(root, 'args'), BUILD_REPO: join(root, 'repo'), MAVEN_ARGS: `-Dmaven.repo.local=${root}/isolated-m2`, GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: `url.file://${source}.insteadOf`, GIT_CONFIG_VALUE_0: 'https://github.com/example/server.git' }; - const run = args => spawnSync('bash', [join(__dirname, 'install-hugegraph-from-source.sh'), ...args], + const run = args => spawnSync('bash', [installer, ...args], { cwd: root, env, encoding: 'utf8' }); assert.notEqual(run([sha, '--unknown']).status, 0); assert.equal(existsSync(join(root, 'hugegraph')), false); @@ -58,6 +63,7 @@ test('build-only packages the server without installing reactor dependencies or // No archive exists: success proves build-only did not attempt extraction/startup. assert.equal(readdirSync(root).some(name => name.startsWith('hugegraph-servers.')), false); }); +} function serverFixture(fixture) { mkdirSync(join(fixture, 'conf/graphs'), { recursive: true }); @@ -65,7 +71,7 @@ function serverFixture(fixture) { writeFileSync(join(fixture, 'conf/graphs/hugegraph.properties'), 'gremlin.graph=org.apache.hugegraph.HugeFactory\n'); writeFileSync(join(fixture, 'conf/rest-server.properties'), - 'restserver.url=http://127.0.0.1:8080\n#auth.authenticator=none\n#auth.admin_pa=none\n'); + 'restserver.url=http://127.0.0.1:8080\nrpc.server_port=8091\n#auth.authenticator=none\n#auth.admin_pa=none\n'); writeFileSync(join(fixture, 'conf/gremlin-server.yaml'), '#port: 8182\n'); writeFileSync(join(fixture, 'bin/init-store.sh'), '#!/bin/bash\nset -e\nshopt -s dotglob\nread -r password\n[[ "$password" == pa ]]\n' + @@ -122,6 +128,7 @@ for (const layout of [ assert.match(rest, /auth.admin_pa=pa/); assert.match(rest, /batch.max_vertices_per_batch=500/); assert.match(rest, /batch.max_edges_per_batch=500/); + assert.match(rest, dir === 'hugegraph_https' ? /rpc.server_port=8092/ : /rpc.server_port=8091/); assert.match(rest, dir === 'hugegraph_https' ? /https:\/\/127.0.0.1:8443/ : /http:\/\/127.0.0.1:8080/); } assert.match(readFileSync(join(serverRoot, 'hugegraph_https/conf/gremlin-server.yaml'), 'utf8'), /^port: 8282/m); @@ -194,3 +201,55 @@ test('shared starter still rejects invalid non-metadata graph names', t => { assert.equal(existsSync(join(serverRoot, top, 'conf/graphs/9invalid.properties')), true); assert.equal(existsSync(join(serverRoot, top, 'started-with-jvm')), false); }); + + +test('Loader retains its source cache while using the selected repository and shared startup', t => { + const root = temp(t); + const source = join(root, 'source'); + const git = (...args) => execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); + git('init', source); + git('-C', source, 'config', 'user.name', 'Fixture'); + git('-C', source, 'config', 'user.email', 'fixture@example.invalid'); + writeFileSync(join(source, 'pom.xml'), 'fixture'); + git('-C', source, 'add', 'pom.xml'); + git('-C', source, 'commit', '-m', 'initial'); + const sha = git('-C', source, 'rev-parse', 'HEAD'); + const top = 'apache-hugegraph-loader-fixture'; + serverFixture(join(root, top)); + const archive = join(root, 'fixture.tar.gz'); + execFileSync('tar', ['czf', archive, '-C', root, top], + { env: { ...process.env, COPYFILE_DISABLE: '1' } }); + const bin = join(root, 'bin'); + mkdirSync(bin); + writeFileSync(join(bin, 'mvn'), '#!/bin/bash\nprintf "%s\\n" "$@" > "$BUILD_ARGS"\n' + + 'mkdir -p hugegraph-server\ncp "$FIXTURE_ARCHIVE" hugegraph-server/apache-hugegraph-fixture.tar.gz\n', + { mode: 0o755 }); + const env = { ...process.env, SERVER_REPOSITORY: 'example/server', SERVER_FETCH_REF: sha, + SERVER_CACHE_DIR: join(root, 'cache'), FIXTURE_ARCHIVE: archive, BUILD_ARGS: join(root, 'args'), + PATH: `${bin}:${process.env.PATH}`, RUNNER_TEMP: root, JAVA_HOME: '/fixture/java11', + GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: `url.file://${source}.insteadOf`, + GIT_CONFIG_VALUE_0: 'https://github.com/example/server.git' }; + const installer = join(__dirname, '../../../hugegraph-loader/assembly/travis/install-hugegraph-from-source.sh'); + for (const pass of ['build', 'cache']) { + const cwd = join(root, `run-${pass}`); + mkdirSync(cwd); + const result = spawnSync('bash', [installer, sha], { cwd, env, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(readFileSync(env.BUILD_ARGS, 'utf8').trim().split('\n'), + ['package', '-DskipTests', '-Dmaven.javadoc.skip=true', '-ntp']); + assert.equal(existsSync(join(cwd, 'hugegraph')), false); + if (pass === 'build') { + assert.equal(readdirSync(env.SERVER_CACHE_DIR).length, 1); + // A cache hit must not execute Maven or reach the source repository again. + writeFileSync(join(bin, 'mvn'), '#!/bin/bash\nexit 91\n', { mode: 0o755 }); + rmSync(source, { recursive: true, force: true }); + } + } + const deployments = readdirSync(root).filter(name => name.startsWith('hugegraph-servers.')); + assert.equal(deployments.length, 2); + for (const deployment of deployments) { + for (const dir of [top, 'hugegraph_https']) { + assert.equal(readFileSync(join(root, deployment, dir, 'started-with-jvm'), 'utf8'), '/fixture/java11'); + } + } +}); diff --git a/hugegraph-client/assembly/travis/start-hugegraph-servers.sh b/hugegraph-client/assembly/travis/start-hugegraph-servers.sh index 0418f9c80..310868e5d 100644 --- a/hugegraph-client/assembly/travis/start-hugegraph-servers.sh +++ b/hugegraph-client/assembly/travis/start-hugegraph-servers.sh @@ -62,6 +62,7 @@ start_server() ( start_server "${SERVER_DIRS[0]}" HTTPS_DIR="$SERVER_ROOT/hugegraph_https" sed -i.bak 's?http://127.0.0.1:8080?https://127.0.0.1:8443?g' "$HTTPS_DIR/conf/rest-server.properties" +sed -i.bak 's/rpc.server_port=8091/rpc.server_port=8092/g' "$HTTPS_DIR/conf/rest-server.properties" sed -i.bak 's/#port: 8182/port: 8282/g' "$HTTPS_DIR/conf/gremlin-server.yaml" printf '\ngremlinserver.url=http://127.0.0.1:8282\n' >> "$HTTPS_DIR/conf/rest-server.properties" start_server "$HTTPS_DIR" diff --git a/hugegraph-loader/assembly/travis/install-hugegraph-from-source.sh b/hugegraph-loader/assembly/travis/install-hugegraph-from-source.sh index 8884c1268..af965a6d2 100755 --- a/hugegraph-loader/assembly/travis/install-hugegraph-from-source.sh +++ b/hugegraph-loader/assembly/travis/install-hugegraph-from-source.sh @@ -15,15 +15,17 @@ # License for the specific language governing permissions and limitations # under the License. # -set -ev +set -euo pipefail if [[ $# -ne 1 ]]; then echo "Must input an existing commit id of hugegraph server" && exit 1 fi COMMIT_ID=$1 -HUGEGRAPH_GIT_URL="https://github.com/apache/hugegraph.git" -CACHE_DIR="${HOME}/hugegraph-cache-${COMMIT_ID}" +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +SHARED_DIR="$SCRIPT_DIR/../../../hugegraph-client/assembly/travis" +[[ "$COMMIT_ID" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "Expected a full commit SHA" >&2; exit 1; } +CACHE_DIR=${SERVER_CACHE_DIR:-"${HOME}/hugegraph-cache-${COMMIT_ID}"} mkdir -p "${CACHE_DIR}" @@ -34,9 +36,8 @@ if [[ -f "${CACHED_TARBALL}" ]]; then cp "${CACHED_TARBALL}" ./ else echo "Building HugeGraph server from source (commit ${COMMIT_ID})..." - git clone --depth 150 ${HUGEGRAPH_GIT_URL} hugegraph + bash "$SHARED_DIR/checkout-server.sh" "$COMMIT_ID" hugegraph cd hugegraph - git checkout "${COMMIT_ID}" mvn package -DskipTests -Dmaven.javadoc.skip=true -ntp cd hugegraph-server mv apache-hugegraph-*.tar.gz ../../ @@ -45,31 +46,6 @@ else cp apache-hugegraph-*.tar.gz "${CACHE_DIR}"/ fi -tar zxf apache-hugegraph-*.tar.gz - -HTTPS_SERVER_DIR="hugegraph_https" -mkdir ${HTTPS_SERVER_DIR} -cp -r apache-hugegraph-*/. ${HTTPS_SERVER_DIR} -cd "$(find apache-hugegraph-* | head -1)" -# start HugeGraphServer with http protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh || exit 1 - -cd ../${HTTPS_SERVER_DIR} -REST_SERVER_CONFIG="conf/rest-server.properties" -GREMLIN_SERVER_CONFIG="conf/gremlin-server.yaml" -sed -i "s?http://127.0.0.1:8080?https://127.0.0.1:8443?g" "$REST_SERVER_CONFIG" -sed -i "s/rpc.server_port=8091/rpc.server_port=8092/g" "$REST_SERVER_CONFIG" -sed -i "s/#port: 8182/port: 8282/g" "$GREMLIN_SERVER_CONFIG" -echo "gremlinserver.url=http://127.0.0.1:8282" >> ${REST_SERVER_CONFIG} - -# start HugeGraphServer with https protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh -cd ../ +ARCHIVES=(apache-hugegraph-*.tar.gz) +[[ ${#ARCHIVES[@]} -eq 1 && -f "${ARCHIVES[0]}" ]] +exec bash "$SHARED_DIR/start-hugegraph-servers.sh" "${ARCHIVES[0]}" diff --git a/hugegraph-spark-connector/assembly/travis/install-hugegraph-from-source.sh b/hugegraph-spark-connector/assembly/travis/install-hugegraph-from-source.sh index 354acd958..6988aa291 100755 --- a/hugegraph-spark-connector/assembly/travis/install-hugegraph-from-source.sh +++ b/hugegraph-spark-connector/assembly/travis/install-hugegraph-from-source.sh @@ -15,48 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. # -set -ev +set -euo pipefail -if [[ $# -ne 1 ]]; then - echo "Must input an existing commit id of hugegraph server" && exit 1 -fi - -COMMIT_ID=$1 -HUGEGRAPH_GIT_URL="https://github.com/apache/hugegraph.git" - -git clone --depth 150 ${HUGEGRAPH_GIT_URL} hugegraph -cd hugegraph -git checkout "${COMMIT_ID}" -mvn package -DskipTests -Dmaven.javadoc.skip=true -ntp -cd hugegraph-server -mv apache-hugegraph-*.tar.gz ../../ -cd ../../ -rm -rf hugegraph -tar zxf apache-hugegraph-*.tar.gz - -HTTPS_SERVER_DIR="hugegraph_https" -mkdir ${HTTPS_SERVER_DIR} -cp -r apache-hugegraph-*/. ${HTTPS_SERVER_DIR} -cd "$(find apache-hugegraph-* | head -1)" -# start HugeGraphServer with http protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh || exit 1 - -cd ../${HTTPS_SERVER_DIR} -REST_SERVER_CONFIG="conf/rest-server.properties" -GREMLIN_SERVER_CONFIG="conf/gremlin-server.yaml" -sed -i "s?http://127.0.0.1:8080?https://127.0.0.1:8443?g" "$REST_SERVER_CONFIG" -sed -i "s/rpc.server_port=8091/rpc.server_port=8092/g" "$REST_SERVER_CONFIG" -sed -i "s/#port: 8182/port: 8282/g" "$GREMLIN_SERVER_CONFIG" -echo "gremlinserver.url=http://127.0.0.1:8282" >> ${REST_SERVER_CONFIG} - -# start HugeGraphServer with https protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh -cd ../ +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +exec bash "$SCRIPT_DIR/../../../hugegraph-client/assembly/travis/install-hugegraph-from-source.sh" "$@" diff --git a/hugegraph-tools/assembly/travis/install-hugegraph-from-source.sh b/hugegraph-tools/assembly/travis/install-hugegraph-from-source.sh index 354acd958..6988aa291 100755 --- a/hugegraph-tools/assembly/travis/install-hugegraph-from-source.sh +++ b/hugegraph-tools/assembly/travis/install-hugegraph-from-source.sh @@ -15,48 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. # -set -ev +set -euo pipefail -if [[ $# -ne 1 ]]; then - echo "Must input an existing commit id of hugegraph server" && exit 1 -fi - -COMMIT_ID=$1 -HUGEGRAPH_GIT_URL="https://github.com/apache/hugegraph.git" - -git clone --depth 150 ${HUGEGRAPH_GIT_URL} hugegraph -cd hugegraph -git checkout "${COMMIT_ID}" -mvn package -DskipTests -Dmaven.javadoc.skip=true -ntp -cd hugegraph-server -mv apache-hugegraph-*.tar.gz ../../ -cd ../../ -rm -rf hugegraph -tar zxf apache-hugegraph-*.tar.gz - -HTTPS_SERVER_DIR="hugegraph_https" -mkdir ${HTTPS_SERVER_DIR} -cp -r apache-hugegraph-*/. ${HTTPS_SERVER_DIR} -cd "$(find apache-hugegraph-* | head -1)" -# start HugeGraphServer with http protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh || exit 1 - -cd ../${HTTPS_SERVER_DIR} -REST_SERVER_CONFIG="conf/rest-server.properties" -GREMLIN_SERVER_CONFIG="conf/gremlin-server.yaml" -sed -i "s?http://127.0.0.1:8080?https://127.0.0.1:8443?g" "$REST_SERVER_CONFIG" -sed -i "s/rpc.server_port=8091/rpc.server_port=8092/g" "$REST_SERVER_CONFIG" -sed -i "s/#port: 8182/port: 8282/g" "$GREMLIN_SERVER_CONFIG" -echo "gremlinserver.url=http://127.0.0.1:8282" >> ${REST_SERVER_CONFIG} - -# start HugeGraphServer with https protocol -sed -i 's|gremlin.graph=org.apache.hugegraph.HugeFactory|gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy|' conf/graphs/hugegraph.properties -sed -i 's|#auth.authenticator=.*|auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator|' conf/rest-server.properties -sed -i 's|#auth.admin_pa=.*|auth.admin_pa=pa|' conf/rest-server.properties -echo -e "pa" | bin/init-store.sh || exit 1 -bin/start-hugegraph.sh -cd ../ +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +exec bash "$SCRIPT_DIR/../../../hugegraph-client/assembly/travis/install-hugegraph-from-source.sh" "$@" From e9bb4d033361d8e4e9f05641ac4b4dcd82637a3d Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 01:14:41 +0800 Subject: [PATCH 003/122] fix(hubble): retain Mockito test compatibility - preserve Mockito 2 interaction assertions with the equivalent API - align test-only Byte Buddy dependencies with Mockito 4 - retain the Spring Boot 2 and Java 11 shared baseline --- hugegraph-hubble/hubble-be/pom.xml | 14 ++++++++++++++ .../auth/AccountMutationAuthorizationTest.java | 6 +++--- .../auth/GraphSpaceAuthOwnershipTest.java | 4 ++-- .../controller/space/GraphSpaceControllerTest.java | 6 +++--- .../SchemaTemplateControllerSecurityTest.java | 2 +- .../service/auth/AuthContextServiceTest.java | 2 +- .../service/auth/GraphSpaceUserServiceTest.java | 10 +++++----- .../service/space/GraphSpaceServiceTest.java | 4 ++-- .../apache/hugegraph/unit/AuthSecurityTest.java | 2 +- .../unit/GraphsControllerCanonicalTest.java | 2 +- .../hugegraph/unit/GraphsServiceDefaultTest.java | 4 ++-- .../hugegraph/unit/LoaderScopeControllerTest.java | 2 +- .../hugegraph/unit/OltpAlgoControllerTest.java | 2 +- .../apache/hugegraph/unit/UserApiContractTest.java | 2 +- .../unit/UserServiceCompatibilityTest.java | 2 +- 15 files changed, 39 insertions(+), 25 deletions(-) diff --git a/hugegraph-hubble/hubble-be/pom.xml b/hugegraph-hubble/hubble-be/pom.xml index ff96b6031..162f24a1d 100644 --- a/hugegraph-hubble/hubble-be/pom.xml +++ b/hugegraph-hubble/hubble-be/pom.xml @@ -37,6 +37,7 @@ localhost 36320 7.16.1 + 1.12.19 @@ -62,6 +63,19 @@ spring-boot-starter-test test + + + net.bytebuddy + byte-buddy + ${mockito.bytebuddy.version} + test + + + net.bytebuddy + byte-buddy-agent + ${mockito.bytebuddy.version} + test + + + org.apache.logging.log4j + log4j-slf4j-impl + + @@ -132,7 +139,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl ${log4j.version} @@ -156,9 +163,9 @@ - org.scala-tools - maven-scala-plugin - 2.15.2 + net.alchim31.maven + scala-maven-plugin + 4.9.2 ${scala.minor.version} @@ -166,6 +173,8 @@ -Xss4096K + -Xmx512m + -XX:ActiveProcessorCount=2 @@ -190,6 +199,32 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Xmx512m -XX:ActiveProcessorCount=2 + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + --add-opens=java.base/java.nio=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED + --add-opens=java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens=java.base/sun.nio.ch=ALL-UNNAMED + --add-opens=java.base/sun.nio.cs=ALL-UNNAMED + --add-opens=java.base/sun.security.action=ALL-UNNAMED + --add-opens=java.base/sun.util.calendar=ALL-UNNAMED + --add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED + -Djdk.reflect.useDirectMethodHandle=false + + + org.apache.maven.plugins maven-assembly-plugin diff --git a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/EdgeBuilderTest.java b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/EdgeBuilderTest.java index 91d119f05..90dcddaa9 100644 --- a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/EdgeBuilderTest.java +++ b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/EdgeBuilderTest.java @@ -85,8 +85,10 @@ public void testEdgeBuild() { @NotNull private static HGLoadContext getEdgeLoadContext() { Map configs = new HashMap<>(); - configs.put("host", HGEnvUtils.DEFAULT_HOST); - configs.put("port", HGEnvUtils.DEFAULT_PORT); + configs.put("host", HGEnvUtils.HOST); + configs.put("port", HGEnvUtils.PORT); + configs.put("graph", HGEnvUtils.GRAPH); + configs.put("protocol", HGEnvUtils.PROTOCOL); configs.put("username", "admin"); configs.put("token", "pa"); configs.put("data-type", "edge"); diff --git a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/VertexBuilderTest.java b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/VertexBuilderTest.java index 25b08d205..89119dc18 100644 --- a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/VertexBuilderTest.java +++ b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/builder/VertexBuilderTest.java @@ -75,8 +75,10 @@ public void testCustomizeWithNullIdField() { @NotNull private static HGLoadContext getCustomizeIdVertexContext() { Map configs = new HashMap<>(); - configs.put("host", HGEnvUtils.DEFAULT_HOST); - configs.put("port", HGEnvUtils.DEFAULT_PORT); + configs.put("host", HGEnvUtils.HOST); + configs.put("port", HGEnvUtils.PORT); + configs.put("graph", HGEnvUtils.GRAPH); + configs.put("protocol", HGEnvUtils.PROTOCOL); configs.put("username", "admin"); configs.put("token", "pa"); configs.put("data-type", "vertex"); @@ -137,8 +139,10 @@ public void testPrimaryWithNullIdField() { @NotNull private static HGLoadContext getPrimaryIdVertexContext() { Map configs = new HashMap<>(); - configs.put("host", HGEnvUtils.DEFAULT_HOST); - configs.put("port", HGEnvUtils.DEFAULT_PORT); + configs.put("host", HGEnvUtils.HOST); + configs.put("port", HGEnvUtils.PORT); + configs.put("graph", HGEnvUtils.GRAPH); + configs.put("protocol", HGEnvUtils.PROTOCOL); configs.put("username", "admin"); configs.put("token", "pa"); configs.put("data-type", "vertex"); diff --git a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/client/HGClientHolderTest.java b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/client/HGClientHolderTest.java index c00094185..9784d6827 100644 --- a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/client/HGClientHolderTest.java +++ b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/client/HGClientHolderTest.java @@ -31,8 +31,10 @@ public class HGClientHolderTest { @Test public void testHGClientHolder() { Map configs = new HashMap<>(); - configs.put("host", HGEnvUtils.DEFAULT_HOST); - configs.put("port", HGEnvUtils.DEFAULT_PORT); + configs.put("host", HGEnvUtils.HOST); + configs.put("port", HGEnvUtils.PORT); + configs.put("graph", HGEnvUtils.GRAPH); + configs.put("protocol", HGEnvUtils.PROTOCOL); configs.put("data-type", "vertex"); configs.put("label", "person"); diff --git a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/utils/HGEnvUtils.java b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/utils/HGEnvUtils.java index 95efb52d6..f3276469f 100644 --- a/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/utils/HGEnvUtils.java +++ b/hugegraph-spark-connector/src/test/java/org/apache/hugegraph/spark/connector/utils/HGEnvUtils.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.spark.connector.utils; +import java.net.URI; + import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.driver.SchemaManager; @@ -28,15 +30,23 @@ public class HGEnvUtils { public static final String DEFAULT_GRAPHSPACE = "DEFAULT"; public static final String DEFAULT_URL = "http://" + DEFAULT_HOST + ":" + DEFAULT_PORT; + public static final String URL = System.getProperty("hugegraph.test.url", DEFAULT_URL); + public static final String GRAPH = System.getProperty("hugegraph.test.graph", DEFAULT_GRAPH); + public static final String HOST = URI.create(URL).getHost(); + public static final String PORT = String.valueOf(URI.create(URL).getPort() == -1 ? + (URI.create(URL).getScheme().equals("https") ? 443 : 80) : + URI.create(URL).getPort()); + public static final String PROTOCOL = URI.create(URL).getScheme(); + private static HugeClient hugeClient; public static void createEnv() { hugeClient = - HugeClient.builder(DEFAULT_URL, DEFAULT_GRAPH) + HugeClient.builder(URL, GRAPH) .configUser("admin", "pa").build(); - hugeClient.graphs().clearGraph(DEFAULT_GRAPH, "I'm sure to delete all data"); + hugeClient.graphs().clearGraph(GRAPH, "I'm sure to delete all data"); SchemaManager schema = hugeClient.schema(); diff --git a/hugegraph-spark-connector/src/test/scala/org/apache/hugegraph/spark/connector/SinkExampleTest.scala b/hugegraph-spark-connector/src/test/scala/org/apache/hugegraph/spark/connector/SinkExampleTest.scala index 62724f73e..8558a39d1 100644 --- a/hugegraph-spark-connector/src/test/scala/org/apache/hugegraph/spark/connector/SinkExampleTest.scala +++ b/hugegraph-spark-connector/src/test/scala/org/apache/hugegraph/spark/connector/SinkExampleTest.scala @@ -33,7 +33,9 @@ object SinkExampleTest { var client: HugeClient = _ val sparkSession: SparkSession = SparkSession.builder() - .master("local[*]") + .master(sys.props.getOrElse("spark.test.master", "local[2]")) + .config("spark.ui.enabled", "false") + .config("spark.sql.shuffle.partitions", "2") .appName(this.getClass.getSimpleName) .getOrCreate() @@ -57,9 +59,9 @@ class SinkExampleTest { val sparkSession: SparkSession = SinkExampleTest.sparkSession val DEFAULT_ENTRANCE: String = "org.apache.hugegraph.spark.connector.DataSource" - val DEFAULT_HOST: String = HGEnvUtils.DEFAULT_HOST - val DEFAULT_PORT: String = HGEnvUtils.DEFAULT_PORT - val DEFAULT_GRAPH: String = HGEnvUtils.DEFAULT_GRAPH + val DEFAULT_HOST: String = HGEnvUtils.HOST + val DEFAULT_PORT: String = HGEnvUtils.PORT + val DEFAULT_GRAPH: String = HGEnvUtils.GRAPH @Test def testFirstInsertVertexPerson(): Unit = { @@ -69,7 +71,7 @@ class SinkExampleTest { Tuple3("Josh", 32, "Beijing"), Tuple3("peter", 35, "ShangHai"), Tuple3("li,nary", 26, "Wu,han"), - Tuple3("Bob", 18, "HangZhou"), + Tuple3("张三🙂", 18, "杭州"), )).toDF("name", "age", "city") df.show() @@ -79,6 +81,7 @@ class SinkExampleTest { .option("host", DEFAULT_HOST) .option("port", DEFAULT_PORT) .option("graph", DEFAULT_GRAPH) + .option("protocol", HGEnvUtils.PROTOCOL) .option("username", "admin") .option("token", "pa") .option("data-type", "vertex") @@ -90,6 +93,9 @@ class SinkExampleTest { val vertices: util.List[Vertex] = client.graph().listVertices("person") assertEquals(6, vertices.size()) + val unicodeVertex = client.graph().getVertex("张三🙂") + assertEquals("张三🙂", unicodeVertex.properties().get("name")) + assertEquals("杭州", unicodeVertex.properties().get("city")) } @Test @@ -106,6 +112,7 @@ class SinkExampleTest { .option("host", DEFAULT_HOST) .option("port", DEFAULT_PORT) .option("graph", DEFAULT_GRAPH) + .option("protocol", HGEnvUtils.PROTOCOL) .option("username", "admin") .option("token", "pa") .option("data-type", "vertex") @@ -139,6 +146,7 @@ class SinkExampleTest { .option("host", DEFAULT_HOST) .option("port", DEFAULT_PORT) .option("graph", DEFAULT_GRAPH) + .option("protocol", HGEnvUtils.PROTOCOL) .option("username", "admin") .option("token", "pa") .option("data-type", "edge") @@ -169,6 +177,7 @@ class SinkExampleTest { .option("host", DEFAULT_HOST) .option("port", DEFAULT_PORT) .option("graph", DEFAULT_GRAPH) + .option("protocol", HGEnvUtils.PROTOCOL) .option("username", "admin") .option("token", "pa") .option("data-type", "edge") From f974e7920119451d6342a8e38feb98fbf4622dd0 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 01:53:43 +0800 Subject: [PATCH 008/122] chore(spark): align runtime license inventory - update the shared inventory from the published full reactor runtime - retain complete bundled license texts and required dependency notices - cover Jackson suffixed legal entries and the protobuf release license --- hugegraph-dist/release-docs/LICENSE | 41 ++- hugegraph-dist/release-docs/NOTICE | 239 ++++++++++++++++++ .../LICENSE-spark-java17-third-party.txt | 124 +++++++++ .../scripts/dependency/known-dependencies.txt | 14 +- 4 files changed, 414 insertions(+), 4 deletions(-) create mode 100644 hugegraph-dist/release-docs/licenses/LICENSE-spark-java17-third-party.txt diff --git a/hugegraph-dist/release-docs/LICENSE b/hugegraph-dist/release-docs/LICENSE index 69e048027..27074c456 100644 --- a/hugegraph-dist/release-docs/LICENSE +++ b/hugegraph-dist/release-docs/LICENSE @@ -477,7 +477,6 @@ See licenses/ for text of these licenses. (Apache License, Version 2.0) * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.40.v20210413 - https://eclipse.org/jetty/jetty-xml) (Apache License, Version 2.0) * JSQLParser library (com.github.jsqlparser:jsqlparser:3.1 - https://github.com/JSQLParser/JSqlParser) (Apache License, Version 2.0) * Javassist (org.javassist:javassist:3.24.0-GA - http://www.javassist.org/) - (Apache License, Version 2.0) * Javassist (org.javassist:javassist:3.25.0-GA - http://www.javassist.org/) (Apache License, Version 2.0) * Javassist (org.javassist:javassist:3.28.0-GA - http://www.javassist.org/) (Apache License, Version 2.0) * less (less:3.13.1 - https://github.com/less/less.js) (Apache License, Version 2.0) * typescript (typescript:3.9.10 - https://github.com/Microsoft/TypeScript) @@ -590,7 +589,6 @@ See licenses/ for text of these licenses. (The MIT License) * Microsoft JDBC Driver for SQL Server (com.microsoft.sqlserver:mssql-jdbc:6.4.0.jre8 - https://github.com/Microsoft/mssql-jdbc) (The MIT License) * Microsoft JDBC Driver for SQL Server (com.microsoft.sqlserver:mssql-jdbc:7.2.0.jre8 - https://github.com/Microsoft/mssql-jdbc) (The MIT License) * Project Lombok (org.projectlombok:lombok:1.18.8 - https://projectlombok.org) - (The MIT License) * SLF4J API Module (org.slf4j:slf4j-api:1.7.25 - http://www.slf4j.org) (The MIT License) * SLF4J API Module (org.slf4j:slf4j-api:1.7.28 - http://www.slf4j.org) (The MIT License) * SLF4J API Module (org.slf4j:slf4j-api:1.7.30 - http://www.slf4j.org) (The MIT License) * SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org) @@ -680,3 +678,42 @@ See licenses/ for text of these licenses. (Public Domain) * XZ for Java (org.tukaani:xz:1.8 - https://tukaani.org/xz/java.html) (Public Domain, per Creative Commons CC0) * HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.9 - http://hdrhistogram.github.io/HdrHistogram/) (Public Domain, per Creative Commons CC0) * LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) + +======================================================================== +Spark Connector runtime dependency updates +======================================================================== + +These versions are additions to the runtime dependency set for the Spark +Connector upgrade. Other modules retain their existing runtime versions. +The Spark framework is provided by the deployment environment and is not +bundled in the Connector assembly. Required attribution notices are in the +distribution NOTICE. Full bundled terms and copyright statements are in +licenses/LICENSE-spark-java17-third-party.txt and the component files below. +Apache-2.0 terms are reproduced at the beginning of this LICENSE. +Javassist is distributed under its Apache-2.0 option; the EDL-1.0 option +is used for Jakarta Activation and XML Binding APIs. + + (Apache-2.0) com.fasterxml.jackson.core:jackson-annotations:2.15.2 + Apache-2.0 terms above + (Apache-2.0) com.fasterxml.jackson.core:jackson-core:2.15.2 + Apache-2.0 terms above, licenses/LICENSE-spark-java17-third-party.txt + (Apache-2.0) com.fasterxml.jackson.core:jackson-databind:2.15.2 + Apache-2.0 terms above + (Apache-2.0) com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.15.2 + licenses/LICENSE-spark-java17-third-party.txt + (Apache-2.0) com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.15.2 + licenses/LICENSE-spark-java17-third-party.txt + (Apache-2.0) com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.15.2 + licenses/LICENSE-spark-java17-third-party.txt + (BSD-3-Clause) com.google.protobuf:protobuf-java:3.19.6 + licenses/LICENSE-spark-java17-third-party.txt + (EDL-1.0) jakarta.activation:jakarta.activation-api:1.2.2 + licenses/LICENSE-jakarta.activation-api.txt + (EDL-1.0) jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 + licenses/LICENSE-jakarta.xml.bind-api.txt + (Apache-2.0) org.apache.logging.log4j:log4j-slf4j2-impl:2.25.5 + Apache-2.0 terms above + (Apache-2.0) org.javassist:javassist:3.30.2-GA + Apache-2.0 terms above + (MIT) org.slf4j:slf4j-api:2.0.7 + licenses/LICENSE-spark-java17-third-party.txt diff --git a/hugegraph-dist/release-docs/NOTICE b/hugegraph-dist/release-docs/NOTICE index d268ded9b..ab05aa461 100644 --- a/hugegraph-dist/release-docs/NOTICE +++ b/hugegraph-dist/release-docs/NOTICE @@ -2550,5 +2550,244 @@ Apache Hadoop Third-party Libs Notices Apache Hadoop Third-party Libs Copyright 2020 and onwards The Apache Software Foundation. +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== +Spark Connector runtime dependency notices +======================================================================== + +======================================================================== +com.fasterxml.jackson.core:jackson-annotations:2.15.2 / META-INF/NOTICE +com.fasterxml.jackson.core:jackson-databind:2.15.2 / META-INF/NOTICE +======================================================================== + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +======================================================================== +com.fasterxml.jackson.core:jackson-core:2.15.2 / META-INF/NOTICE +======================================================================== + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +## FastDoubleParser + +jackson-core bundles a shaded copy of FastDoubleParser . +That code is available under an MIT license +under the following copyright. + +Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + +See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser +and the licenses and copyrights that apply to that code. + +======================================================================== +com.fasterxml.jackson.core:jackson-core:2.15.2 / META-INF/FastDoubleParser-NOTICE +======================================================================== + +# FastDoubleParser + +This is a Java port of Daniel Lemire's fast_float project. +This project provides parsers for double, float, BigDecimal and BigInteger values. + +## Copyright + +Copyright © 2023 Werner Randelshofer, Switzerland. + +## Licensing + +This code is licensed under MIT License. +https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE +(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +Some portions of the code have been derived from other projects. +All these projects require that we include a copyright notice, and some require that we also include some text of their +license file. + +fast_double_parser, Copyright (c) 2022 Daniel Lemire. Apache 2.0 License. +https://github.com/fastfloat/fast_float +https://github.com/fastfloat/fast_float/blob/dc88f6f882ac7eb8ec3765f633835cb76afa0ac2/LICENSE-APACHE + +fast_float, Copyright (c) 2021 The fast_float authors. Apache 2.0 License. +https://github.com/fastfloat/fast_float +https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE + +bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. +https://github.com/tbuktu/bigint/tree/floatfft +https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE +https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE +(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) +(The file 'bigint-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +======================================================================== +jakarta.activation:jakarta.activation-api:1.2.2 / META-INF/NOTICE.md +======================================================================== + +# Notices for Jakarta Activation + +This content is produced and maintained by Jakarta Activation project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jaf + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Distribution License v. 1.0, +which is available at http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: BSD-3-Clause + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jaf + +## Third-party Content + +This project leverages the following third party content. + +JUnit (4.12) + +* License: Eclipse Public License + +======================================================================== +jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 / META-INF/NOTICE.md +======================================================================== + +[//]: # " Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " +[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " +[//]: # " " +[//]: # " SPDX-License-Identifier: BSD-3-Clause " + +# Notices for Jakarta XML Binding + +This content is produced and maintained by the Jakarta XML Binding +project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jaxb + +## Trademarks + +Jakarta XML Binding is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Distribution License v. 1.0 which is available at +http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: BSD-3-Clause + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jaxb-api +* https://github.com/eclipse-ee4j/jaxb-tck + +## Third-party Content + +This project leverages the following third party content. + +Apache River (3.0.0) + +* License: Apache-2.0 AND BSD-3-Clause + +ASM 7 (n/a) + +* License: BSD-3-Clause +* Project: https://asm.ow2.io/ +* Source: + https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand + +JTHarness (5.0) + +* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) +* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness +* Source: http://hg.openjdk.java.net/code-tools/jtharness/ + +normalize.css (3.0.2) + +* License: MIT + +SigTest (n/a) + +* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +======================================================================== +org.apache.logging.log4j:log4j-slf4j2-impl:2.25.5 / META-INF/NOTICE +======================================================================== + +SLF4J 2 Provider for Log4j API +Copyright 1999-2026 The Apache Software Foundation + + This product includes software developed at The Apache Software Foundation (http://www.apache.org/). diff --git a/hugegraph-dist/release-docs/licenses/LICENSE-spark-java17-third-party.txt b/hugegraph-dist/release-docs/licenses/LICENSE-spark-java17-third-party.txt new file mode 100644 index 000000000..6784afc73 --- /dev/null +++ b/hugegraph-dist/release-docs/licenses/LICENSE-spark-java17-third-party.txt @@ -0,0 +1,124 @@ +Additional license texts for the Spark Connector runtime dependency updates. + +Headings identify exact published artifacts and their original JAR entries, +or upstream release sources. Identical complete texts are reproduced once. +Required attribution notices are reproduced in the distribution NOTICE. +Other complete terms remain in the existing component license files. + +======================================================================== +com.fasterxml.jackson.core:jackson-core:2.15.2 / META-INF/bigint-LICENSE +======================================================================== + +Copyright 2022 Tim Buktu + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:2.15.2 / META-INF/LICENSE +com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.15.2 / META-INF/LICENSE +======================================================================== + +This copy of Jackson JSON processor databind module is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivative works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 + +======================================================================== +com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.15.2 / META-INF/LICENSE +======================================================================== + +This copy of Jackson JSON processor `jackson-module-jaxb-annotations` module is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivative works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 + +======================================================================== +org.slf4j:slf4j-api:2.0.7 / META-INF/LICENSE.txt +======================================================================== + +Copyright (c) 2004-2023 QOS.ch +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +======================================================================== +https://raw.githubusercontent.com/protocolbuffers/protobuf/v3.19.6/LICENSE +======================================================================== + +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. diff --git a/hugegraph-dist/scripts/dependency/known-dependencies.txt b/hugegraph-dist/scripts/dependency/known-dependencies.txt index 2d97a6ba4..5ac793a64 100644 --- a/hugegraph-dist/scripts/dependency/known-dependencies.txt +++ b/hugegraph-dist/scripts/dependency/known-dependencies.txt @@ -204,23 +204,31 @@ ivy-2.4.0.jar ivy-2.5.0.jar j2objc-annotations-1.3.jar jackson-annotations-2.12.3.jar +jackson-annotations-2.15.2.jar jackson-core-2.12.3.jar +jackson-core-2.15.2.jar jackson-core-asl-1.9.13.jar jackson-databind-2.12.3.jar +jackson-databind-2.15.2.jar jackson-dataformat-yaml-2.12.3.jar jackson-datatype-jdk8-2.12.3.jar jackson-datatype-jsr310-2.12.3.jar jackson-jaxrs-1.9.2.jar jackson-jaxrs-base-2.12.3.jar +jackson-jaxrs-base-2.15.2.jar jackson-jaxrs-json-provider-2.12.3.jar +jackson-jaxrs-json-provider-2.15.2.jar jackson-mapper-asl-1.9.13.jar jackson-module-jaxb-annotations-2.12.3.jar +jackson-module-jaxb-annotations-2.15.2.jar jackson-module-parameter-names-2.12.3.jar jackson-xc-1.9.2.jar jakarta.activation-2.0.1.jar jakarta.activation-api-1.2.1.jar +jakarta.activation-api-1.2.2.jar jakarta.json-2.0.1.jar jakarta.xml.bind-api-2.3.2.jar +jakarta.xml.bind-api-2.3.3.jar jamon-runtime-2.4.1.jar janino-3.0.15.jar java-util-1.9.0.jar @@ -228,8 +236,8 @@ javacsv-2.0.jar javapoet-1.8.0.jar javassist-3.22.0-CR2.jar javassist-3.24.0-GA.jar -javassist-3.25.0-GA.jar javassist-3.28.0-GA.jar +javassist-3.30.2-GA.jar javatuples-1.2.jar javax.activation-api-1.2.0.jar javax.annotation-api-1.3.2.jar @@ -385,6 +393,7 @@ log4j-1.2.17.jar log4j-api-2.25.5.jar log4j-core-2.25.5.jar log4j-slf4j-impl-2.25.5.jar +log4j-slf4j2-impl-2.25.5.jar log4j-to-slf4j-2.11.2.jar log4j-web-2.11.2.jar logback-classic-1.2.3.jar @@ -455,6 +464,7 @@ proto-google-common-protos-2.0.1.jar protobuf-java-2.5.0.jar protobuf-java-3.17.2.jar protobuf-java-3.17.3.jar +protobuf-java-3.19.6.jar protobuf-java-util-3.17.2.jar re2j-1.1.jar sfm-converter-8.2.1.jar @@ -464,10 +474,10 @@ sfm-reflect-8.2.1.jar sfm-tuples-8.2.1.jar sfm-util-8.2.1.jar sketches-core-0.9.0.jar -slf4j-api-1.7.25.jar slf4j-api-1.7.28.jar slf4j-api-1.7.30.jar slf4j-api-1.7.36.jar +slf4j-api-2.0.7.jar slf4j-log4j12-1.7.30.jar smiley-http-proxy-servlet-1.12.1.jar snakeyaml-1.23.jar From 812cb89125ff7e06b9a8092435671140b540af98 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 02:42:07 +0800 Subject: [PATCH 009/122] fix(spark): use the host logging provider - keep the Spark 3.5 SLF4J provider in provided scope - retain the exclusion of the inherited SLF4J 1 binding - document exact Guava acquisition and driver/executor paths - state the application classpath override and runtime boundary --- hugegraph-spark-connector/README.md | 31 ++++++++++++++++++++++++++++- hugegraph-spark-connector/pom.xml | 4 +++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/hugegraph-spark-connector/README.md b/hugegraph-spark-connector/README.md index 366042e45..1b704cab1 100644 --- a/hugegraph-spark-connector/README.md +++ b/hugegraph-spark-connector/README.md @@ -47,9 +47,38 @@ set `-Dhugegraph.test.url=... -Dhugegraph.test.graph=...` to select another targ The default Spark master is `local[2]`; set `-Dspark.test.master=...` to test another master. Run applications with `spark-submit` from the matching Spark distribution. Spark supplies its -Java module options; the Maven test configuration supplies these options for embedded Spark on Java 17. +SLF4J 2 provider and Java module options; the connector assembly does not bundle an SLF4J provider. +The Maven test configuration supplies the module options for embedded Spark on Java 17. The connector continues to emit Java 8 bytecode. +Keep Spark's default class loading order for its logging classes. The Java Client needs +`com.google.guava:guava:30.0-jre`; an older Guava from Spark can fail with a missing +`Preconditions.checkNotNull` overload. Fetch the exact application dependency on the driver: + +```bash +GUAVA_DIR="$PWD/spark-runtime" +mvn org.apache.maven.plugins:maven-dependency-plugin:3.7.0:copy \ + -Dartifact=com.google.guava:guava:30.0-jre -DoutputDirectory="$GUAVA_DIR" +DRIVER_GUAVA_JAR="$GUAVA_DIR/guava-30.0-jre.jar" +``` + +Before submission, copy that JAR to each executor host, or run the same Maven copy command there. +Use one consistent absolute executor path across workers, and set `EXECUTOR_GUAVA_JAR` to it. +For example, after placing the JAR at `/opt/hugegraph-spark/lib/guava-30.0-jre.jar` on every worker: + +```bash +EXECUTOR_GUAVA_JAR=/opt/hugegraph-spark/lib/guava-30.0-jre.jar +spark-submit --driver-class-path "$DRIVER_GUAVA_JAR" \ + --conf "spark.executor.extraClassPath=$EXECUTOR_GUAVA_JAR" \ + --jars /path/to/hugegraph-spark-connector-1.8.0-jar-with-dependencies.jar \ + /path/to/your-application.jar +``` + +Supply the class and application arguments required by your application. This setting gives the +application's Guava priority on both sides; it changes the effective Guava classpath while keeping +Spark's logging provider. It is not a guarantee of compatibility with every other Spark application +or library in the same host. + ## How to use If we have a graph, the schema is defined as follows: diff --git a/hugegraph-spark-connector/pom.xml b/hugegraph-spark-connector/pom.xml index a8ed45284..519a05d05 100644 --- a/hugegraph-spark-connector/pom.xml +++ b/hugegraph-spark-connector/pom.xml @@ -140,7 +140,9 @@ org.apache.logging.log4j log4j-slf4j2-impl - ${log4j.version} + + 2.20.0 + provided From 00d84bfa9b550fb8dcf0734290bad89456a7b2ce Mon Sep 17 00:00:00 2001 From: imbajin Date: Sat, 3 Oct 2026 23:54:56 +0800 Subject: [PATCH 010/122] fix(loader): prepare tests and close clients - Inherit coverage tooling and keep scoped ORC access on Java 17. - Update test-only container support and include existing unit coverage. - Close both distinct clients and cover shared, separate and failing closes. - Preserve the current Java baseline; shared-base runtime validation is pending. --- .../assembly/static/bin/hugegraph-loader.sh | 12 +- .../assembly/static/bin/mapping-convert.sh | 7 +- hugegraph-loader/pom.xml | 7 +- .../loader/executor/LoadContext.java | 8 +- .../loader/test/unit/LoadContextTest.java | 122 ++++++++++++++++++ .../loader/test/unit/UnitTestSuite.java | 4 + 6 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java diff --git a/hugegraph-loader/assembly/static/bin/hugegraph-loader.sh b/hugegraph-loader/assembly/static/bin/hugegraph-loader.sh index b0646aba7..2ca0ae208 100755 --- a/hugegraph-loader/assembly/static/bin/hugegraph-loader.sh +++ b/hugegraph-loader/assembly/static/bin/hugegraph-loader.sh @@ -57,7 +57,7 @@ else JAVA=java fi -if [ -z ${JAVA} ] ; then +if [ -z "${JAVA:-}" ] ; then echo Unable to find java executable. Check JAVA_HOME and PATH environment variables. > /dev/stderr exit 1; fi @@ -73,12 +73,20 @@ CP="$CP":$(find -L ${LIB} -name '*.jar' \ export LOADER_CLASSPATH="${CLASSPATH:-}:$CP" +# Hive ORC interns URI fields reflectively; Java 17 requires this opening. +# Keep the existing Java baseline usable while preparing for the upgrade. +JAVA_VERSION=$("$JAVA" -version 2>&1 | awk -F '"' '/version/ {print $2; exit}') +JAVA_MAJOR=${JAVA_VERSION%%[.+-]*} +if [ "$JAVA_MAJOR" -ge 17 ] 2>/dev/null; then + JVM_OPTS="$JVM_OPTS --add-opens=java.base/java.net=ALL-UNNAMED" +fi + # Xmx needs to be set so that it is big enough to cache all the vertexes in the run export JVM_OPTS="$JVM_OPTS -Xmx10g -cp $LOADER_CLASSPATH" # Uncomment to enable debugging #JVM_OPTS="$JVM_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1414" -exec ${JAVA} -Dname="HugeGraphLoader" -Dloader.home.path=${TOP} -Dlog4j.configurationFile=${CONF}/log4j2.xml \ +exec "$JAVA" -Dname="HugeGraphLoader" -Dloader.home.path=${TOP} -Dlog4j.configurationFile=${CONF}/log4j2.xml \ -Djava.library.path=${NATIVE} \ ${JVM_OPTS} org.apache.hugegraph.loader.HugeGraphLoader ${VARS} diff --git a/hugegraph-loader/assembly/static/bin/mapping-convert.sh b/hugegraph-loader/assembly/static/bin/mapping-convert.sh index 75dede275..4c64bd593 100755 --- a/hugegraph-loader/assembly/static/bin/mapping-convert.sh +++ b/hugegraph-loader/assembly/static/bin/mapping-convert.sh @@ -42,7 +42,12 @@ for jar in "$LIB_PATH"/*.jar; do class_path=${class_path}:${jar} done +JAVA=java +if [ -n "$JAVA_HOME" ]; then + JAVA="$JAVA_HOME/bin/java" +fi + args=$1 main_class="org.apache.hugegraph.loader.MappingConverter" -exec java -Dlog4j.configurationFile="${CONF_PATH}"/log4j2.xml \ +exec "$JAVA" -Dlog4j.configurationFile="${CONF_PATH}"/log4j2.xml \ -cp "${class_path}" ${main_class} "${args}" diff --git a/hugegraph-loader/pom.xml b/hugegraph-loader/pom.xml index 29272b2b5..7179cb59c 100644 --- a/hugegraph-loader/pom.xml +++ b/hugegraph-loader/pom.xml @@ -38,6 +38,8 @@ + + ${project.artifactId} apache-${release.name}-${project.version} ${project.basedir}/assembly @@ -51,7 +53,7 @@ 8.0.28 42.4.1 7.2.0.jre8 - 1.19.0 + 1.21.4 1.1.8 @@ -747,7 +749,6 @@ org.jacoco jacoco-maven-plugin - 0.8.4 pre-unit-test @@ -801,6 +802,8 @@ org.apache.maven.plugins maven-surefire-plugin + + @{argLine} --add-opens=java.base/java.net=ALL-UNNAMED ${test-classes} diff --git a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java index 6e3aaf445..7a000acda 100644 --- a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java +++ b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java @@ -212,7 +212,13 @@ public void close() { } LOG.info("Write load progress successfully"); - this.client.close(); + try { + this.client.close(); + } finally { + if (this.indirectClient != this.client) { + this.indirectClient.close(); + } + } LOG.info("Close HugeClient successfully"); this.closed = true; } diff --git a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java new file mode 100644 index 000000000..cba21a922 --- /dev/null +++ b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.loader.test.unit; + +import java.lang.reflect.Field; +import java.util.HashMap; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.HugeClientBuilder; +import org.apache.hugegraph.loader.executor.LoadContext; +import org.apache.hugegraph.loader.executor.LoadOptions; +import org.apache.hugegraph.loader.metrics.LoadSummary; +import org.apache.hugegraph.loader.progress.LoadProgress; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class LoadContextTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void testCloseDistinctClientsOnce() throws Exception { + RecordingClient client = allocate(RecordingClient.class); + RecordingClient indirect = allocate(RecordingClient.class); + LoadContext context = this.context(client, indirect); + context.close(); + context.close(); + Assert.assertEquals(1, client.closeCalls); + Assert.assertEquals(1, indirect.closeCalls); + Assert.assertTrue(context.closed()); + } + + @Test + public void testCloseSharedClientOnce() throws Exception { + RecordingClient client = allocate(RecordingClient.class); + LoadContext context = this.context(client, client); + context.close(); + context.close(); + Assert.assertEquals(1, client.closeCalls); + } + + @Test + public void testCloseIndirectClientWhenPrimaryCloseFails() throws Exception { + RecordingClient client = allocate(RecordingClient.class); + RecordingClient indirect = allocate(RecordingClient.class); + client.failClose = true; + LoadContext context = this.context(client, indirect); + try { + context.close(); + Assert.fail("Expected primary close failure"); + } catch (IllegalStateException expected) { + Assert.assertEquals("Primary close failed", expected.getMessage()); + } + Assert.assertEquals(1, client.closeCalls); + Assert.assertEquals(1, indirect.closeCalls); + } + + private LoadContext context(HugeClient client, HugeClient indirect) throws Exception { + // Match the existing TaskManagerTest allocation seam: no network clients. + LoadContext context = allocate(LoadContext.class); + LoadOptions options = new LoadOptions(); + options.file = this.folder.newFile("mapping.json").getAbsolutePath(); + set(context, "timestamp", "test"); + set(context, "options", options); + set(context, "summary", new LoadSummary()); + set(context, "newProgress", new LoadProgress()); + set(context, "loggers", new HashMap<>()); + set(context, "client", client); + set(context, "indirectClient", indirect); + return context; + } + + private static T allocate(Class type) throws Exception { + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + Field field = unsafeClass.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return type.cast(unsafeClass.getMethod("allocateInstance", Class.class) + .invoke(field.get(null), type)); + } + + private static void set(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + + private static class RecordingClient extends HugeClient { + + private int closeCalls; + private boolean failClose; + + private RecordingClient() { + super((HugeClientBuilder) null); + } + + @Override + public void close() { + this.closeCalls++; + if (this.failClose) { + throw new IllegalStateException("Primary close failed"); + } + } + } +} diff --git a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/UnitTestSuite.java b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/UnitTestSuite.java index b9d453cfe..aafc4b2e7 100644 --- a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/UnitTestSuite.java +++ b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/UnitTestSuite.java @@ -23,6 +23,10 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ LineTest.class, + FileLineFetcherTest.class, + LoadOptionsTest.class, + LoadContextTest.class, + TaskManagerTest.class, DateUtilTest.class, MappingConverterTest.class, LoadProgressTest.class, From 7816bfe9acae79eab0640597fb5a3d3f3e4027bc Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 02:44:19 +0800 Subject: [PATCH 011/122] chore(spark): match host-provided logging licenses - remove the provided logging bridge from the runtime inventory - retain the exact published baseline Log4j API notice - verify the reactor copy and provider-free assembly legal coverage --- hugegraph-dist/release-docs/LICENSE | 2 -- hugegraph-dist/release-docs/NOTICE | 4 ++-- hugegraph-dist/scripts/dependency/known-dependencies.txt | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/hugegraph-dist/release-docs/LICENSE b/hugegraph-dist/release-docs/LICENSE index 27074c456..61298a499 100644 --- a/hugegraph-dist/release-docs/LICENSE +++ b/hugegraph-dist/release-docs/LICENSE @@ -711,8 +711,6 @@ is used for Jakarta Activation and XML Binding APIs. licenses/LICENSE-jakarta.activation-api.txt (EDL-1.0) jakarta.xml.bind:jakarta.xml.bind-api:2.3.3 licenses/LICENSE-jakarta.xml.bind-api.txt - (Apache-2.0) org.apache.logging.log4j:log4j-slf4j2-impl:2.25.5 - Apache-2.0 terms above (Apache-2.0) org.javassist:javassist:3.30.2-GA Apache-2.0 terms above (MIT) org.slf4j:slf4j-api:2.0.7 diff --git a/hugegraph-dist/release-docs/NOTICE b/hugegraph-dist/release-docs/NOTICE index ab05aa461..b2cf1eb69 100644 --- a/hugegraph-dist/release-docs/NOTICE +++ b/hugegraph-dist/release-docs/NOTICE @@ -2782,10 +2782,10 @@ possession, or use, and re-export of encryption software, to see if this is permitted. ======================================================================== -org.apache.logging.log4j:log4j-slf4j2-impl:2.25.5 / META-INF/NOTICE +Retained published baseline: org.apache.logging.log4j:log4j-api:2.25.5 / META-INF/NOTICE ======================================================================== -SLF4J 2 Provider for Log4j API +Apache Log4j API Copyright 1999-2026 The Apache Software Foundation diff --git a/hugegraph-dist/scripts/dependency/known-dependencies.txt b/hugegraph-dist/scripts/dependency/known-dependencies.txt index 5ac793a64..2e73a657f 100644 --- a/hugegraph-dist/scripts/dependency/known-dependencies.txt +++ b/hugegraph-dist/scripts/dependency/known-dependencies.txt @@ -393,7 +393,6 @@ log4j-1.2.17.jar log4j-api-2.25.5.jar log4j-core-2.25.5.jar log4j-slf4j-impl-2.25.5.jar -log4j-slf4j2-impl-2.25.5.jar log4j-to-slf4j-2.11.2.jar log4j-web-2.11.2.jar logback-classic-1.2.3.jar From 38f373438f014486b45106f66f595e037c34e6f2 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 00:36:51 +0800 Subject: [PATCH 012/122] fix(loader): preserve JVM and close failures - Apply the ORC module opening only on JDK 17 and later. - Keep default file-test selection when the JDK profile is active. - Preserve the primary close exception and suppress secondary failures. - Validate JVM option boundaries and unit/file suites on JDK 11 and 17. --- hugegraph-loader/pom.xml | 18 +++++++++++-- .../loader/executor/LoadContext.java | 6 +---- .../loader/test/unit/LoadContextTest.java | 27 ++++++++++++++++--- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/hugegraph-loader/pom.xml b/hugegraph-loader/pom.xml index 7179cb59c..77a1890bf 100644 --- a/hugegraph-loader/pom.xml +++ b/hugegraph-loader/pom.xml @@ -40,6 +40,11 @@ + + + file + files + **/FileLoadTest.java ${project.artifactId} apache-${release.name}-${project.version} ${project.basedir}/assembly @@ -553,6 +558,16 @@ + + java17-orc-access + + [17,) + + + + --add-opens=java.base/java.net=ALL-UNNAMED + + unit @@ -802,8 +817,7 @@ org.apache.maven.plugins maven-surefire-plugin - - @{argLine} --add-opens=java.base/java.net=ALL-UNNAMED + @{argLine} ${loader.test.jvm.options} ${test-classes} diff --git a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java index 7a000acda..e11f42b75 100644 --- a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java +++ b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java @@ -212,12 +212,8 @@ public void close() { } LOG.info("Write load progress successfully"); - try { + try (HugeClient secondary = this.indirectClient != this.client ? this.indirectClient : null) { this.client.close(); - } finally { - if (this.indirectClient != this.client) { - this.indirectClient.close(); - } } LOG.info("Close HugeClient successfully"); this.closed = true; diff --git a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java index cba21a922..0d81532b0 100644 --- a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java +++ b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java @@ -61,7 +61,7 @@ public void testCloseSharedClientOnce() throws Exception { public void testCloseIndirectClientWhenPrimaryCloseFails() throws Exception { RecordingClient client = allocate(RecordingClient.class); RecordingClient indirect = allocate(RecordingClient.class); - client.failClose = true; + client.closeFailure = "Primary close failed"; LoadContext context = this.context(client, indirect); try { context.close(); @@ -73,6 +73,25 @@ public void testCloseIndirectClientWhenPrimaryCloseFails() throws Exception { Assert.assertEquals(1, indirect.closeCalls); } + @Test + public void testPreservePrimaryFailureWhenBothClosesFail() throws Exception { + RecordingClient client = allocate(RecordingClient.class); + RecordingClient indirect = allocate(RecordingClient.class); + client.closeFailure = "Primary close failed"; + indirect.closeFailure = "Indirect close failed"; + LoadContext context = this.context(client, indirect); + try { + context.close(); + Assert.fail("Expected primary close failure"); + } catch (IllegalStateException expected) { + Assert.assertEquals("Primary close failed", expected.getMessage()); + Assert.assertEquals(1, expected.getSuppressed().length); + Assert.assertEquals("Indirect close failed", expected.getSuppressed()[0].getMessage()); + } + Assert.assertEquals(1, client.closeCalls); + Assert.assertEquals(1, indirect.closeCalls); + } + private LoadContext context(HugeClient client, HugeClient indirect) throws Exception { // Match the existing TaskManagerTest allocation seam: no network clients. LoadContext context = allocate(LoadContext.class); @@ -105,7 +124,7 @@ private static void set(Object target, String name, Object value) throws Excepti private static class RecordingClient extends HugeClient { private int closeCalls; - private boolean failClose; + private String closeFailure; private RecordingClient() { super((HugeClientBuilder) null); @@ -114,8 +133,8 @@ private RecordingClient() { @Override public void close() { this.closeCalls++; - if (this.failClose) { - throw new IllegalStateException("Primary close failed"); + if (this.closeFailure != null) { + throw new IllegalStateException(this.closeFailure); } } } From 3e5cfe10637a579a77ac94a8bb049763af917259 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sat, 3 Oct 2026 23:48:34 +0800 Subject: [PATCH 013/122] fix(tools): preserve plain HDFS backups - read plain backup streams without ZIP decoding - close invalid compressed streams before reporting errors - cover ZIP and plain backup restore round trips - allow a dedicated test server URL --- .../apache/hugegraph/base/HdfsDirectory.java | 6 +- .../hugegraph/test/functional/AuthTest.java | 2 +- .../test/functional/BackupRestoreTest.java | 118 ++++++++++++++++++ .../test/functional/FuncTestSuite.java | 4 +- .../test/functional/HdfsDirectoryTest.java | 67 ++++++++++ 5 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/BackupRestoreTest.java create mode 100644 hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/HdfsDirectoryTest.java diff --git a/hugegraph-tools/src/main/java/org/apache/hugegraph/base/HdfsDirectory.java b/hugegraph-tools/src/main/java/org/apache/hugegraph/base/HdfsDirectory.java index 7dddb02db..456d7d672 100644 --- a/hugegraph-tools/src/main/java/org/apache/hugegraph/base/HdfsDirectory.java +++ b/hugegraph-tools/src/main/java/org/apache/hugegraph/base/HdfsDirectory.java @@ -135,10 +135,14 @@ public InputStream inputStream(String file) { Path source = new Path(path); try { is = fs.open(source); + // Plain JSON backups are also supported by LocalDirectory. + if (!file.endsWith(this.suffix(true))) { + return is; + } zis = new ZipInputStream(is); E.checkState(zis.getNextEntry() != null, "Invalid zip file '%s'", file); - } catch (IOException e) { + } catch (IOException | IllegalStateException e) { closeAndIgnoreException(is); throw new ClientException("Failed to read from %s", e, path); } diff --git a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthTest.java b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthTest.java index cf8b847f7..f620d7c1b 100644 --- a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthTest.java +++ b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthTest.java @@ -23,7 +23,7 @@ public class AuthTest { public static final String DEFAULT_TEST_URL = "/auth/"; public static final String USER_NAME = "admin"; public static final String USER_PASSWORD = "pa"; - public static final String URL = "http://127.0.0.1:8080"; + public static final String URL = System.getProperty("tools.test.url", "http://127.0.0.1:8080"); public static final String GRAPH = "hugegraph"; public static final Integer TIME_OUT = 30; public static final String TRUST_STORE_FILE = ""; diff --git a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/BackupRestoreTest.java b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/BackupRestoreTest.java new file mode 100644 index 000000000..b78b157f3 --- /dev/null +++ b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/BackupRestoreTest.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.test.functional; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.apache.hugegraph.base.Directory; +import org.apache.hugegraph.base.HdfsDirectory; +import org.apache.hugegraph.base.LocalDirectory; +import org.apache.hugegraph.cmd.HugeGraphCommand; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.driver.SchemaManager; +import org.apache.hugegraph.structure.constant.GraphMode; +import org.apache.hugegraph.structure.constant.T; +import org.apache.hugegraph.structure.graph.Vertex; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class BackupRestoreTest extends AuthTest { + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void testCompressedBackupRestore() throws Exception { + this.backupRestore(true); + } + + @Test + public void testUncompressedBackupRestore() throws Exception { + // Preserve the plain JSON backup format used by older Tools releases. + this.backupRestore(false); + } + + private void backupRestore(boolean compress) throws Exception { + String hdfs = System.getProperty("tools.test.hdfs"); + String path = hdfs == null ? this.temporary.newFolder("backup").getAbsolutePath() : + hdfs + "/tools-backup-" + UUID.randomUUID(); + Directory backup = hdfs == null ? new LocalDirectory(path) : + new HdfsDirectory(path, Collections.singletonMap("fs.default.name", hdfs)); + backup.ensureDirectoryExist(true); + File logs = this.temporary.newFolder("logs"); + try (HugeClient client = HugeClient.builder(URL, GRAPH) + .configUser(USER_NAME, USER_PASSWORD) + .build()) { + client.graphs().clearGraph(GRAPH, "I'm sure to delete all data"); + try { + SchemaManager schema = client.schema(); + schema.propertyKey("name").asText().create(); + schema.vertexLabel("person").properties("name") + .primaryKeys("name").create(); + schema.edgeLabel("knows").sourceLabel("person") + .targetLabel("person").create(); + Vertex first = client.graph().addVertex(T.LABEL, "person", "name", "Alice"); + Vertex second = client.graph().addVertex(T.LABEL, "person", "name", "李四"); + first.addEdge("knows", second); + Assert.assertEquals("李四", client.graph().getVertex(second.id()).property("name")); + + this.command("backup", path, logs, "--compress", String.valueOf(compress)); + Assert.assertFalse(backup.files().isEmpty()); + client.graphs().clearGraph(GRAPH, "I'm sure to delete all data"); + Assert.assertTrue(client.graph().listVertices().isEmpty()); + client.graphs().mode(GRAPH, GraphMode.RESTORING); + this.command("restore", path, logs); + client.graphs().mode(GRAPH, GraphMode.NONE); + + Assert.assertEquals(1, client.schema().getPropertyKeys().size()); + Assert.assertEquals(1, client.schema().getVertexLabels().size()); + Assert.assertEquals(1, client.schema().getEdgeLabels().size()); + Assert.assertEquals(2, client.graph().listVertices().size()); + Assert.assertEquals(1, client.graph().listEdges().size()); + Assert.assertEquals("Alice", client.graph().getVertex(first.id()).property("name")); + Assert.assertEquals("李四", client.graph().getVertex(second.id()).property("name")); + Assert.assertFalse(backup.files().isEmpty()); + } finally { + client.graphs().mode(GRAPH, GraphMode.NONE); + client.graphs().clearGraph(GRAPH, "I'm sure to delete all data"); + backup.removeDirectory(); + } + } + } + + private void command(String operation, String backup, File logs, String... extra) { + List args = new ArrayList<>(Arrays.asList( + "--throw-mode", "true", "--url", URL, "--graph", GRAPH, + "--user", USER_NAME, "--password", USER_PASSWORD, operation, + "--directory", backup, "--log", logs.getAbsolutePath(), + "--thread-num", "2")); + String hdfs = System.getProperty("tools.test.hdfs"); + if (hdfs != null) { + args.add("-Dfs.default.name=" + hdfs); + } + args.addAll(Arrays.asList(extra)); + HugeGraphCommand.main(args.toArray(new String[0])); + } +} diff --git a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/FuncTestSuite.java b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/FuncTestSuite.java index 30517025e..7a08a756f 100644 --- a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/FuncTestSuite.java +++ b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/FuncTestSuite.java @@ -24,7 +24,9 @@ @Suite.SuiteClasses({ AuthBackupTest.class, AuthRestoreTest.class, - CommandTest.class + CommandTest.class, + BackupRestoreTest.class, + HdfsDirectoryTest.class }) public class FuncTestSuite { } diff --git a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/HdfsDirectoryTest.java b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/HdfsDirectoryTest.java new file mode 100644 index 000000000..457ad8128 --- /dev/null +++ b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/HdfsDirectoryTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.test.functional; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.apache.commons.io.IOUtils; +import org.apache.hugegraph.base.HdfsDirectory; +import org.apache.hugegraph.rest.ClientException; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class HdfsDirectoryTest { + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void testPlainAndCompressedBackupStreams() throws Exception { + HdfsDirectory directory = this.directory(); + byte[] expected = "{\"vertices\": [{\"name\": \"李四\"}]}" + .getBytes(StandardCharsets.UTF_8); + for (boolean compress : new boolean[]{false, true}) { + try (OutputStream out = directory.outputStream("vertices", compress, true)) { + out.write(expected); + } + try (InputStream in = directory.inputStream("vertices" + directory.suffix(compress))) { + Assert.assertArrayEquals(expected, IOUtils.toByteArray(in)); + } + } + } + + @Test + public void testInvalidCompressedBackup() throws Exception { + HdfsDirectory directory = this.directory(); + try (OutputStream out = directory.outputStream("broken.zip", false, true)) { + out.write("invalid zip".getBytes(StandardCharsets.UTF_8)); + } + Assert.assertThrows(ClientException.class, () -> directory.inputStream("broken.zip")); + } + + private HdfsDirectory directory() throws Exception { + // Exercise Hadoop's actual stream implementations without a remote server. + return new HdfsDirectory(this.temporary.newFolder().toURI().toString(), + Collections.singletonMap("fs.default.name", "file:///")); + } +} From 5875f81865a50e28facec14a4a3ae460d5ff3d92 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 02:55:35 +0800 Subject: [PATCH 014/122] docs(spark): clarify driver deployment paths - mark the validated submit example as client deploy mode - explain the remote driver Guava path in cluster deploy mode - distinguish driver provisioning from executor JAR copies --- hugegraph-spark-connector/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hugegraph-spark-connector/README.md b/hugegraph-spark-connector/README.md index 1b704cab1..a7d1a3c68 100644 --- a/hugegraph-spark-connector/README.md +++ b/hugegraph-spark-connector/README.md @@ -68,12 +68,15 @@ For example, after placing the JAR at `/opt/hugegraph-spark/lib/guava-30.0-jre.j ```bash EXECUTOR_GUAVA_JAR=/opt/hugegraph-spark/lib/guava-30.0-jre.jar -spark-submit --driver-class-path "$DRIVER_GUAVA_JAR" \ +spark-submit --deploy-mode client --driver-class-path "$DRIVER_GUAVA_JAR" \ --conf "spark.executor.extraClassPath=$EXECUTOR_GUAVA_JAR" \ --jars /path/to/hugegraph-spark-connector-1.8.0-jar-with-dependencies.jar \ /path/to/your-application.jar ``` +For cluster deploy mode, provision Guava on the remote driver and use its readable absolute path in +`--driver-class-path`; copying it to executors does not provision the driver. + Supply the class and application arguments required by your application. This setting gives the application's Guava priority on both sides; it changes the effective Guava classpath while keeping Spark's logging provider. It is not a guarantee of compatibility with every other Spark application From 38894b08eda4ea3484ea8ea09a69d5212b82ad01 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 02:57:15 +0800 Subject: [PATCH 015/122] fix(loader): retain profiles and close state - Restore profile test selection without global file-test defaults. - Keep scoped ORC access and late coverage on JDK 11/17 test JVMs. - Mark client cleanup closed even when either client throws. - Cover primary, secondary and dual failures followed by retry. --- hugegraph-loader/pom.xml | 18 ++-------- .../loader/executor/LoadContext.java | 3 +- .../loader/test/unit/LoadContextTest.java | 35 +++++++++++++++++-- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/hugegraph-loader/pom.xml b/hugegraph-loader/pom.xml index 77a1890bf..d56642309 100644 --- a/hugegraph-loader/pom.xml +++ b/hugegraph-loader/pom.xml @@ -40,11 +40,6 @@ - - - file - files - **/FileLoadTest.java ${project.artifactId} apache-${release.name}-${project.version} ${project.basedir}/assembly @@ -558,16 +553,6 @@ - - java17-orc-access - - [17,) - - - - --add-opens=java.base/java.net=ALL-UNNAMED - - unit @@ -817,7 +802,8 @@ org.apache.maven.plugins maven-surefire-plugin - @{argLine} ${loader.test.jvm.options} + + @{argLine} --add-opens=java.base/java.net=ALL-UNNAMED ${test-classes} diff --git a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java index e11f42b75..d7407103f 100644 --- a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java +++ b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadContext.java @@ -214,9 +214,10 @@ public void close() { try (HugeClient secondary = this.indirectClient != this.client ? this.indirectClient : null) { this.client.close(); + } finally { + this.closed = true; } LOG.info("Close HugeClient successfully"); - this.closed = true; } @Override diff --git a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java index 0d81532b0..992975cb2 100644 --- a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java +++ b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadContextTest.java @@ -22,6 +22,7 @@ import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.driver.HugeClientBuilder; +import org.apache.hugegraph.loader.constant.ElemType; import org.apache.hugegraph.loader.executor.LoadContext; import org.apache.hugegraph.loader.executor.LoadOptions; import org.apache.hugegraph.loader.metrics.LoadSummary; @@ -68,9 +69,25 @@ public void testCloseIndirectClientWhenPrimaryCloseFails() throws Exception { Assert.fail("Expected primary close failure"); } catch (IllegalStateException expected) { Assert.assertEquals("Primary close failed", expected.getMessage()); + Assert.assertEquals(0, expected.getSuppressed().length); } - Assert.assertEquals(1, client.closeCalls); - Assert.assertEquals(1, indirect.closeCalls); + assertClosedWithoutRetrying(context, client, indirect); + } + + @Test + public void testCloseRemainsClosedWhenSecondaryCloseFails() throws Exception { + RecordingClient client = allocate(RecordingClient.class); + RecordingClient indirect = allocate(RecordingClient.class); + indirect.closeFailure = "Indirect close failed"; + LoadContext context = this.context(client, indirect); + try { + context.close(); + Assert.fail("Expected indirect close failure"); + } catch (IllegalStateException expected) { + Assert.assertEquals("Indirect close failed", expected.getMessage()); + Assert.assertEquals(0, expected.getSuppressed().length); + } + assertClosedWithoutRetrying(context, client, indirect); } @Test @@ -88,8 +105,17 @@ public void testPreservePrimaryFailureWhenBothClosesFail() throws Exception { Assert.assertEquals(1, expected.getSuppressed().length); Assert.assertEquals("Indirect close failed", expected.getSuppressed()[0].getMessage()); } + assertClosedWithoutRetrying(context, client, indirect); + } + + private static void assertClosedWithoutRetrying(LoadContext context, RecordingClient client, + RecordingClient indirect) { + Assert.assertTrue(context.closed()); + context.close(); Assert.assertEquals(1, client.closeCalls); Assert.assertEquals(1, indirect.closeCalls); + Assert.assertEquals(2L, context.newProgress().vertexLoaded()); + Assert.assertEquals(3L, context.newProgress().edgeLoaded()); } private LoadContext context(HugeClient client, HugeClient indirect) throws Exception { @@ -99,7 +125,10 @@ private LoadContext context(HugeClient client, HugeClient indirect) throws Excep options.file = this.folder.newFile("mapping.json").getAbsolutePath(); set(context, "timestamp", "test"); set(context, "options", options); - set(context, "summary", new LoadSummary()); + LoadSummary summary = new LoadSummary(); + summary.plusLoaded(ElemType.VERTEX, 2); + summary.plusLoaded(ElemType.EDGE, 3); + set(context, "summary", summary); set(context, "newProgress", new LoadProgress()); set(context, "loggers", new HashMap<>()); set(context, "client", client); From aa59d485d9c4adbe68bd1defa16aa7f773641c7b Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 00:28:55 +0800 Subject: [PATCH 016/122] fix(tools): honor auth test server URL - pass the configured URL to auth backup commands - pass the same URL to auth restore commands - keep command and client requests on one test fixture --- .../hugegraph/test/functional/AuthBackupTest.java | 7 ++++--- .../hugegraph/test/functional/AuthRestoreTest.java | 11 ++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthBackupTest.java b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthBackupTest.java index d69711b1d..06af51be7 100644 --- a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthBackupTest.java +++ b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthBackupTest.java @@ -34,7 +34,7 @@ public void init() { @Test public void testAuthBackup() { - String[] args = new String[]{"--throw-mode", "true", "--user", USER_NAME, + String[] args = new String[]{"--throw-mode", "true", "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-backup" }; @@ -47,7 +47,7 @@ public void testAuthBackup() { @Test public void testAuthBackupByTypes() { - String[] args = new String[]{"--throw-mode", "true", "--user", USER_NAME, + String[] args = new String[]{"--throw-mode", "true", "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-backup", "--types", "user,group" }; @@ -61,7 +61,7 @@ public void testAuthBackupByTypes() { @Test public void testAuthBackupWithWrongType() { - String[] args = new String[]{"--throw-mode", "true", "--user", USER_NAME, + String[] args = new String[]{"--throw-mode", "true", "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-backup", "--types", "user,group,test" }; @@ -79,6 +79,7 @@ public void testAuthBackupByDirectory() { String directory = "./backup"; String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-backup", diff --git a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthRestoreTest.java b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthRestoreTest.java index 365d98f5b..2d1e05f32 100644 --- a/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthRestoreTest.java +++ b/hugegraph-tools/src/test/java/org/apache/hugegraph/test/functional/AuthRestoreTest.java @@ -58,7 +58,7 @@ public void testAuthRestoreForAllType() { this.loadData(HugeType.BELONG, "auth_belongs.txt"); this.loadData(HugeType.ACCESS, "auth_accesses.txt"); - String[] args = new String[]{"--throw-mode", "true", "--user", USER_NAME, + String[] args = new String[]{"--throw-mode", "true", "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", "--directory", DEFAULT_URL, "--init-password", "123456", "--strategy", "ignore" @@ -122,6 +122,7 @@ public void testAuthRestoreForUser() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -145,6 +146,7 @@ public void testAuthRestoreForUser() { public void testRestoreWithoutInitPassword() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -166,6 +168,7 @@ public void testAuthRestoreWithConflictAndStopStrategy() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -187,6 +190,7 @@ public void testAuthRestoreWithIgnoreStrategy() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -212,6 +216,7 @@ public void testAuthRestoreWithWrongDirectory() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -234,6 +239,7 @@ public void testAuthRestoreWithWrongType() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -257,6 +263,7 @@ public void testAuthRestoreByBelongWithoutDependency() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -280,6 +287,7 @@ public void testAuthRestoreByAccessWithoutDependency() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", @@ -303,6 +311,7 @@ public void testAuthRestoreWithWrongStrategy() { String[] args = new String[]{ "--throw-mode", "true", + "--url", URL, "--user", USER_NAME, "--password", USER_PASSWORD, "auth-restore", From 02b576e05bf6de1a5cb745dc599fd3e60ec6041e Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 4 Oct 2026 00:16:05 +0800 Subject: [PATCH 017/122] feat(hubble): prepare Java 17 runtime Upgrade the backend to Spring Boot 3 and Jakarta APIs. Use a fresh H2 database with complete schema initialization. Align JAXB and Avatica runtime dependencies. Preserve runtime data while packaging archives and images. Carry the verified graph context and Java 17 documentation. --- hugegraph-hubble/Dockerfile | 12 +- hugegraph-hubble/README.md | 43 +- .../legacy17-query-after.jpg | Bin 0 -> 96944 bytes hugegraph-hubble/hubble-be/pom.xml | 107 ++- .../config/DatabaseSchemaMigrator.java | 813 ------------------ .../hugegraph/config/H2DataSourceConfig.java | 45 + .../config/IngestionProxyServlet.java | 4 +- .../hugegraph/config/MybatisPlusConfig.java | 10 +- .../hugegraph/config/TomcatServletConfig.java | 7 - .../hugegraph/controller/BaseController.java | 4 +- .../controller/SettingController.java | 6 +- .../controller/op/LogController.java | 2 +- .../controller/schema/SchemaController.java | 3 +- .../entity/enums/AsyncTaskStatus.java | 2 +- .../hugegraph/entity/enums/ExecuteStatus.java | 2 +- .../hugegraph/entity/enums/ExecuteType.java | 2 +- .../entity/enums/FileMappingStatus.java | 2 +- .../hugegraph/entity/enums/JobStatus.java | 2 +- .../hugegraph/entity/enums/LoadAction.java | 2 +- .../hugegraph/entity/enums/LoadStatus.java | 2 +- .../hugegraph/handler/CustomInterceptor.java | 10 +- .../hugegraph/handler/ExceptionAdvisor.java | 2 +- .../hugegraph/handler/LoginInterceptor.java | 8 +- .../handler/MessageSourceHandler.java | 4 +- .../hugegraph/handler/ResponseAdvisor.java | 2 +- .../service/HugeClientPoolService.java | 4 +- .../hugegraph/service/auth/BelongService.java | 3 + .../service/graphs/GraphsService.java | 8 +- .../service/load/JobManagerService.java | 2 +- .../service/load/LoadTaskService.java | 4 +- .../service/op/LiveOperationsCollector.java | 2 +- .../query/GremlinCollectionService.java | 2 +- .../service/schema/SchemaService.java | 5 + .../src/main/resources/application.properties | 44 +- .../src/main/resources/database/schema.sql | 65 +- .../config/LegacySchemaStartupTest.java | 233 ----- .../AccountMutationAuthorizationTest.java | 6 +- .../auth/GraphSpaceAuthOwnershipTest.java | 4 +- .../space/GraphSpaceControllerTest.java | 6 +- .../SchemaTemplateControllerSecurityTest.java | 2 +- .../service/auth/AuthContextServiceTest.java | 2 +- .../auth/GraphSpaceUserServiceTest.java | 10 +- .../IngestTransactionIntegrationTest.java | 16 +- .../query/ExecuteHistoryRetentionTest.java | 14 +- .../service/space/GraphSpaceServiceTest.java | 4 +- .../hugegraph/unit/AuthSecurityTest.java | 2 +- .../hugegraph/unit/AvaticaRuntimeTest.java | 75 ++ .../hugegraph/unit/FileMappingSchemaTest.java | 394 +-------- .../unit/GraphsControllerCanonicalTest.java | 6 +- .../unit/GraphsServiceDefaultTest.java | 28 +- .../apache/hugegraph/unit/H2StartupTest.java | 122 +++ .../hugegraph/unit/JaxbRuntimeTest.java | 59 ++ .../hugegraph/unit/JobManagerServiceTest.java | 2 +- .../unit/LoaderScopeControllerTest.java | 2 +- .../unit/MessageSourceHandlerTest.java | 2 +- .../unit/OltpAlgoControllerTest.java | 2 +- .../apache/hugegraph/unit/UnitTestSuite.java | 3 + .../hugegraph/unit/UserApiContractTest.java | 6 +- .../unit/UserServiceCompatibilityTest.java | 2 +- .../src/test/resources/application.properties | 27 +- .../assembly/static/bin/common_functions | 7 + .../assembly/static/bin/start-hubble.sh | 3 +- .../assembly/travis/archive-hubble.sh | 44 + .../assembly/travis/test_archive_hubble.py | 114 +++ hugegraph-hubble/hubble-dist/pom.xml | 11 +- .../query-context.integration.test.js | 166 ++++ .../src/modules/analysis/Home/index.js | 10 +- .../src/modules/analysis/Home/index.test.js | 28 + .../analysis/QueryBar/ContentCommon/index.js | 7 +- .../QueryBar/ContentCommon/index.test.js | 8 + .../modules/analysis/QueryBar/Home/index.js | 5 +- .../analysis/QueryBar/Home/index.test.js | 18 + hugegraph-hubble/pom.xml | 72 +- 73 files changed, 1117 insertions(+), 1640 deletions(-) create mode 100644 hugegraph-hubble/docs/images/java17-validation/legacy17-query-after.jpg delete mode 100644 hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java create mode 100644 hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/H2DataSourceConfig.java delete mode 100644 hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/LegacySchemaStartupTest.java create mode 100644 hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AvaticaRuntimeTest.java create mode 100644 hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/H2StartupTest.java create mode 100644 hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JaxbRuntimeTest.java create mode 100644 hugegraph-hubble/hubble-dist/assembly/travis/archive-hubble.sh create mode 100644 hugegraph-hubble/hubble-dist/assembly/travis/test_archive_hubble.py create mode 100644 hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/query-context.integration.test.js diff --git a/hugegraph-hubble/Dockerfile b/hugegraph-hubble/Dockerfile index b1d5b3c19..9381953b4 100644 --- a/hugegraph-hubble/Dockerfile +++ b/hugegraph-hubble/Dockerfile @@ -15,7 +15,7 @@ # limitations under the License. # -FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build +FROM --platform=$BUILDPLATFORM maven:3.9.11-eclipse-temurin-17 AS build ENV NODE_OPTIONS=--dns-result-order=ipv4first @@ -34,16 +34,20 @@ COPY . /pkg RUN set -x \ && mvn install $MAVEN_ARGS -pl hugegraph-client,hugegraph-loader -am -Dmaven.javadoc.skip=true -DskipTests -ntp \ && cd /pkg/hugegraph-hubble/ \ - && mvn package $MAVEN_ARGS -e -B -ntp -DskipTests -Dmaven.javadoc.skip=true + && mvn package $MAVEN_ARGS -e -B -ntp -DskipTests -Dmaven.javadoc.skip=true \ + && set -- /pkg/hugegraph-hubble/target/apache-hugegraph-hubble-*.tar.gz \ + && test "$#" -eq 1 \ + && mkdir /hubble-release \ + && tar -xzf "$1" --strip-components=1 -C /hubble-release -FROM eclipse-temurin:11-jre-jammy +FROM eclipse-temurin:17-jre-jammy RUN apt-get -q update \ && apt-get install --no-install-recommends curl -yq \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -COPY --from=build /pkg/hugegraph-hubble/apache-hugegraph-hubble-*/ /hubble +COPY --from=build /hubble-release/ /hubble RUN sed -i \ -e 's/^server\.host=.*/server.host=0.0.0.0/' \ -e 's/^dashboard\.address=.*/dashboard.address=/' \ diff --git a/hugegraph-hubble/README.md b/hugegraph-hubble/README.md index 0b2b94e7d..20ceb949c 100644 --- a/hugegraph-hubble/README.md +++ b/hugegraph-hubble/README.md @@ -58,15 +58,16 @@ cd hubble-fe yarn dev ``` -Run the backend incrementally with Java 11 and the Maven daemon: +Run the backend incrementally with Java 17 and the Maven daemon: ```bash -export JAVA_HOME=$(/usr/libexec/java_home -v 11) +export JAVA_HOME=$(/usr/libexec/java_home -v 17) mvnd -pl hubble-be -DskipTests compile dependency:build-classpath \ -Dmdep.outputFile=/tmp/hubble-be-classpath mkdir -p /tmp/hubble-dev-home cd hubble-be "$JAVA_HOME/bin/java" -Dfile.encoding=UTF-8 \ + --add-opens=java.base/java.net=ALL-UNNAMED \ -Dhubble.home.path=/tmp/hubble-dev-home \ -cp "target/classes:$(^)`ATC--&%$nIVYfVmuPo@A@wAHoL z0jEv@0H-KFz{w~;1#tHCnX_k3pFMl#?74Gi&r@HaroM22n(@-*i&t0}uU=(gWM*dN z;N@gx<7Q`O{!#cxZa#iNpx`x5k((j{H+cmF1->CUb?)3b>hshL)YJ?Dtjw$e|I63O zF95ppRK4fEP@Uoe{6Ke#itg0O2LK0!$RDUqQ7*tA7uD$>D1@Fme}VGa=nCKz71gOT zRA(<-I7dZAb(#ur>IbURXXxn9GBDmS6gP@t;!?i%p!eJ@5X{5#aa=L8grqd&No>Zu z^W1l>i%LqN@tHhQHY#3yEWC}=U*3CDaI$^N?bqv%94R3 zWPM9i{T8I%n(de`q2ond5RzGd{_am+g|NePcH(`QdefQwWVFghwafD+(v z0igKw)t{*U>e&xVX!Lo!PVZ`alyU+HQJ({kH0(mpzvKJO^LzB2m;ZSQbj@4JZp~FO z4E}5l09+hp8%uU##|~UMsEXThJ7)Ufrq*vBwqzyy6Tq|q_vw`1e1`u86DSHBad}1_ zQ=k1SQ~x`Kq~YOq`GLZ;7Qn;Jg=CmCB8A<{%SoPIfdm6_7cH85fDQyVLRgAU01KaS zBybE;>EV|f@6$ge!4+=Y0oa)X0Q;Bjpu-bQNqLJ?uGs69sy9?&P*pM{!z{4H&M6P? z^pmPnlE8}@D-FqQQCi0?_TA=eqHeasGl#45AV<*}ZkV#_Qep>t>xT{!5efw(AT%zy zLyDa+I1#?OYy<1Ro_!$WL(s3E56*fL%L2Wn7M@dEJ9#%lf8=c`%lTNGY?TpO|3Mj@ zoC2nQU*$T>vyK$~Tp;dXK094&HY4YyL#Q{)cukjbB`HE^1p&(O3gc!1iSITWY+YWN zwXK-+X)aG(RHJovRO3!&k0hds+8~1gQIBTlp7pnbtZy~@B*i@)I=-ZM$xU?Eph9Z9 z1XQp7DXng@Uxur<`x;Ew=Iv9r<4cqEqe4g3+NMr+gsIX+?H73~qT-mun=+pk>rL>! zx8+D~Pld|tJBAAxACbs%XfZ4^Fw^xmFO`h_I2?Vl@K#li5^DP(X3BMcfb-!k&tU8$|V3_OR0 z95SfyxiS!4Ttd;EIzl&G{O33MJ&Fhx=hG?&qJrWrpM429KaoB9!slUhMw1%-P(K`G zzK|Qq#E{4!GZ!-dY&I#5(;d5@U|Ki@U+7G$G#7B)!R?QFGHoaBJ9>9qlEH}yBXz~Z z49Cb;?~a&zou%K6el936e!ax52QkbAp*6TAD`M@sEQ3F$uGbbHPFzZwrm~g;LYLGY2BjqCR}zBOZ^s0%GJ$^72QT zF?prp=v@Q%_2YJ>>AMwIcCmJeTGk0>U%dEa_8BU$PvF||g514W30r43u(~&w*#2YB zzfz(h-xZaMl2q;Is$yBuecjg~qE|0tn(thQ6ouUi+c_ou8#U~|WrTN&&2>=dSLC(1 zK6fu43TynVkTbCgfcx*HpV@4E z)f{^(S46ZgS+@p#*&<|_N6#OXkKpqvL|!*M%2R^Dl#V_sh0)&I%}A}*`I5(jiL^)G zP9c5ehLDXqm##Vz60SfT9iG}%cA*ych@d} z56(9{KKGkL%Vas&YcS9PhA>%%cyfDd-y!!Y3^xkZ$lx;Me9#k1_jA?aZovxZH8Q;2 z3}H;-JHTZ4=rhdGGpk>o{^o=xPL_GWl8hJVSzCEhOn=#%@??`jj|1!ba&TWCrnn$Q zl2}dskot7MdrlSvT_^Z#$4K|JMhDV4okqxUqu7-4Z;nb-K>^8+BcrrO#Jd>(G8y*e zR?Ld$k8aWp@Uj*sLm8i{OJnCB{Z`!Dzd0W8qatTX3E590{P@7W;wTgv1@fG+HLAmQ z5C?M+B_FDfO3Ot!E)w%`xWuA9DcSq?Jag0{AMoYwRA_IQ@4KU4$zGyQ-=Pcs;dh7q zP|m?Eul!bV6!lVKy{};G?ZvUe!H-MrestoCif!dSUDQwf;4oO5X|%E9R7h9SMRNsM z9X7{TG*JHM$@TaXz}tx3EAGF$#2y!g>T=_(u)d-`h$6P8SxA0RPA4wiI&&u@ht!F! zf~@2t($LwV+74as^nrOus4_Xz zt3cPIFCt_PDRQ88rt6DpyKEAVgc@+w;k#U&&6If%bEEb6bUtXd-5!m+4x;=M9VOkp9q%s-~_2fSz|go?&L z%;;GBrQqmNF#h5GsY7B)e0Phl?#21Tt#qwm;xClp@xRA>fMZ*<%PeaIui%v!(bQ<# zY9$<F~p?Ro+7(OPZo4K#m zNuz(0t)Q^%y+d7fRC2eM<7xWY<%EuDo1wibJwXG&sjbeQ*5)NIHzz+l>{5e$AG)bT zjC4(K3QIs?o@P3e2iUbEiF-`=HW;1hi-?Zo{=v!RYj$~sI>d@DQT0#+~=kBvg3 zq;({t)Qci@ao7uF&XMM}GkV^QYjSjJxgQK24n-}I^=Cag5=9o`oKI)iBK5Xana~+m>f#N@ zdN1Z?B=RYk=ffPNK^zIHOP)R>W}_udyE2{dD$^OLhbR+EPk;jF^hDd4bQfk*IL`xk zbKIi-2Osx_&daxDi&7#U=|Xz|0*g|RS!ywx6F_eZtTdgA7_|0DC1C^{FHjeK0?^=q z_XR{49yEkqVm|>eRu?T&1E_#gfO9<_ikZ`fkg`jm;h8l^*HTGoYK)>3fk7ZOh9Q0Q znM@y2Hu?UNO&-Rt*mwv6qrKO5t=XjMgM69$Ttv&4hv26}h2_8tE=6B1S`4sxOgx$i z8^$PsE7pyQZakS-y)N1JQI5SXG?ZM9U`wFFS`KI0^k0VaOW!bkvZe#RmzNnE!zrKq z;WZe%ITLtEZmHI_-~&-LSYm4LkfpZ-EfLM4lzY67cVYVEG~P4-0bPCFH^?=weLdj_ zB?>LCDCXoNt)7gy{F=0hCS{y?cC0?q4pBkFxzwfXMsZf4M z0zNmjfX~G6#?ULUe@R#z_9-Hg%5mfCF(vL30*+3|Re3eJ`53xb=?k4K8GAp|yGPAu z+a9Uu1(Z1Jw>k?8R~wCUx>m`(nC2+FrltnRATu3G>x!5I)4|WLt?i8GT z7{ZOW?;(79uN;;*6R_pxg3I^qjB!8tX|eV9y{ENvYvb?N=T$J-#I&xCkjz;T%#-d(%R&o1o7;UKM2e*4x-N* zDSRfxB(hJwPsj+5+=?q8q^nAz2I^fss#M|p4%UKui5k@=?0f74{{$G!AEv7_(&*W- zed&X9-^%sgfeDdOZ^!4h{0Trl^*TbVJs&K8E;R`)BbGgAA=Nits&p=xI^iv`g=Tp< z%k;kGj22I1O>SnUUoU3HNJ1KgKUZ z7c72Q8z#6W*~k~C?%lZ6UpFX$saz4^NSazrqRcn&j^wY@$bCnyEKJB!aXY~$TcgaM zBVM(obmo|4)yv!e!n(~F5%XJZU`?#V)h$@$1I}i!M)sHF>&2S$HD&F%T)r$?vP?Oq zFxasplDFHHylDVagRq+2ut{g=ZH>}U5=7T_-vA7X8|r=>8wLn)r2jIxtm2?_P<{f) zOuaXF$N+t>MQJXZ#Ma>qI|!`XW18+}U3Nz{LBXo7-TP)&nKPQUZyE)=jzgy#(fRuH zZzga~N>9kwIPSGaucfTr79{S8oGuLOLa6VJb7_)I)Hk-Swdn}=WbxQud{0`D0EUO{ zT;2N@I7U8PrgIy+i+V9*5GE^Nz}&~@O8r<~Tsi-uEy@y=05e>u>Mn`BEJ_OU$U2xi zXi0KWk}*8m4E*#g=Di`Wt`TvS6vMTs4kW+=fdLioVPHms)>rw?x=@+m5E1`@c zhK&nsKp8{t*7;mn;7sb!-=vY^^}YWoHlsvNHm*$J(qj5OvySrg)V$-p`Y*Gi;4Wxz zz~rNhBKf2PgCHC(_~3Og{q?v%Uii+O07%wfT=RA%P-t{D$|T*8Iq{aN2Z1ADMfzEA zyhM^#knu=eziY3KTuf181lO#;ds=9<35RfD%;DHqp(Ii+fb6N{dw41cEA?2qvQIN%bdJQ$B82)eMZT%gzkv4pS>D0IuenObbg*j=6>Ve+A%xlt)=mHd z&Xcl+ft!KzmkevGix#E}7usqDV^-E-7+8*BxU!~?qYHYqF-3D?4#-H z9Am*5RJty~X@I>7;UMhFUWbclyY)~hUdk;^pq^66dn)#OeG!yq%_zUqS;B%_<`v#2 zZ3TMU>BbL$V-xtC3oR}~?P21iNGp}96gj@;@tvT=euZ+s3j|3Q+!J-}LT4il$qwK> zcU(9{f{A4>|D!@a|5*ru2L~=LRq%deEVFIkh6x9Ii86h*OUOb&TzUo_EWm0-Z(|M`HUWGYUS%u|m zcjG|YF6K)yk#jG>Mg)naTQw7hNpd$_ia$fc$Ch$1h%yC}yHirCc!g4-{m=pPlUW(2>zo! z{;6~>r|^Fw{2>cd%1S6@3p&Oh^R$mrg7 zJptT}{l@R*p@?x;H^vHbPF|(i;!JK%zyl3-BOtKs8k@;WOs^z>SqcE)2bW%7%iQ48g#~NK0%^Z&lzmRb4TtR012fk9YNx#5D+Q{g8U0SDc@J`LM zpcGnnwd_$WFO2NrCx4lp=ZVRW$+3-JHMm(I=9F4b?znzGry1#TFg~!p%45U2HTc0b z9?8XZ;17Lb+AM2|nKtBgrL4(x^=}+KoyP6C<=BF@ddwyH=+-=nI>^v2ZGYH$O;Ej2 zePUYC=;Fj{kGr@+=wja7ibq9-vwAo2dpT}KLI@05^2-BFJ<|U821gP$vNB!;NamwNc`YayBVQ|PS^=xXYK3d z8y3gN!V^G6aHYw2nE!1a2-(ztZk1Qh=;QS%{Ev=V4VH!nzOVbm;LHhtd+gXX|KPd* z^Y5|W`S86xeD4?ECBb)z_Ti~8kCy)3ciMSd942yUgg-0_tb%@#k>J%Q!`lABe0D|E512=X>;bo_uc*-+RMqsFrs$`Qf?Rdh6@m2ZVjP;J5v0f9NsIiwFP!|5p4`MUu`c^RD7# zBXmU~OGk-?$x;5Wk>;Bd0g*vnyj&BDMlvq0J~@plT0)$|b>X$ImI%y_xFstg*Rmea z!ec38=9ZL$2m?kLFT4<`sYG0^6E|{LFjpM>i z^C&4HRvfX5+HT8R*i_wtrXLQ_uRohuF z2qtGbvjr66y(9WFJiH!dY67%L;5h9U)77}qPcm+H$khGdZF2UCcH4B6aT-p#jD5(P zvPIV9B1HU~a-Xtn*g^#IQHH)mp%Z}G!#=l9cdPOiK8E}@;xX#<9=p_kxF#%V=2n*2 z8Cj&|RjaT_FT)*myI(9}d3X?$<16;~gT{LOzTl!&85{q@Vl$xS%cyc^g-F=S;ENXp zCjg`8@0UmP$~ez4C0*o2vsE1Dbnixb;F)l1*4#rYkbCQs!S%+%8-wqaOJE1LUq+iF zI9wC8V23dUU{Ko^n1Vtb4ILZvLiy(O+3{j072Wu47P*4pg;Ho3c7xwv@!2-24!vs_ zg}?8Z@2_QDIIROl=ug>W(#IVSxzNlQGXl%S9%v9u!NHwFE%PZ#~dOL>yhVJpr5m3a=ri znWAe#=TTR2NGrB+ngK$8)FUX!kb9(}cXd$jvn0P$86N{#G5aMvD~1d&w4RSe zbCj2JBxlx|>}ZeQuN|L#!cV-_d&YJ3B5*Sx+HYRTvBlVhDai%3d$$;(47wbPV?(H^ zFGX(jE7T6;ZINfujUI_d>IBth7=uDGD+cE zv2Qs+H4xhXpDutND8)1zJ6E{iN3@qo&f69)tH>};4ze5VbN9K1SZ*d?E*tiacC+m` z^x&Zbmq#~f>#FP{6E@358z+w8Sr0BTQqZGHlO-wqw2H0BsG)G)UcSK69HjeMLhEch z(s~Ct?GdpcO0NF^)+y_)r=cfHbs8BIC$iZh&e)b3tEzc$5iC+2`fA(S%_fUqcfY|S zj9Xm>Ko9VC&f-K!&kdse^IC4=gnEcS(I%3bLk$ti6^?~;H+W7x^EW;3zfRPtFUz4a zke+8y7gY=rgWQBIY4OjEuRy55y;(b_x<+@pCQV8z*y@wH!$bOR8IOM{9+-{J>A2oE zew|3l&8UDv2UIlocSm^)y0)C&u6ECNkUV-;K<}cS@S>9iX4Jf$HE}g9WiT7)?6U3= z;|=Vv*^_LMN#bS=S?lzl-Zkp!G&rl%D05JnWO~nPd}qVbGAoTRvi#9wvi9|odND;gG0U&L zAoz8&_=*+CYxY{(*igDdc82qa+(5ND0u=AfBtE2NL~_K<_`_n=2MKm?;F6mDxO^|7 z% z>7|kLc*SS-h~5yDNS{_0D(CgyqAgR){dqk`kl5n_lUzhm>x^}kA)R~vELrq@2}|{D zdl&Y2Ey+QkzsJXUV7_NlU1O#YjD28Yr_f%G@lx}U_nR8kJ&`hZVo_AzY~)B^Wfu#E zSB=k_+sQI=Cdyg{G8C6QoWj&N;88PZ?gbJuI~&RiSz;fsmSGX~G#iayQ3Wse^DlO6 z2NTb`Aq0eXP&FZ@IAuN7)$d*+vfzjEZ|<4Llb%(3@#Fy7^? zx%o~)_dKf1x5!8Z#DxxluvE`m0dhK}Y!(_E8^ehQFj>V%>_a@^#S8ESPgG@#eCD*o z%Tr@5eid3w7hmF##Z@rDA)2E(I#?ik71vIJKzVVS8B9b0qOQXZCM zHlW5smpdI_1T8k-^iYy7+F>U$%7K+jJXSgt%F*`+qA{VL%Fe%Z)^A68isOyAcKCU% zv_v=vUrHX1@RXbOeb!tO5R4hqv4+AOWYoaMY=Nbu!nT&t;dVQ$w`y`S-SGI`@E40L z!vwcYMJur+&GOLjA=~V%Jg==ntmhK1CmkJo*t1442*P~?*qKv1CL}rlgD>#pXy4Ar zjfr?YaO+6Bz;XzPDY}AJEBZ>kzbv%2n#QJfXWxW3H$OnszbrSQgNez|P~vAbsk&cju~%yGCv5X=&Z^BsuziLVsm+~noe+vbr$ zvBpDOjK#m6YY-@O_2nLIgIt}B-+DaSRL1P+c3mf`3~y3kt-jzB;>QPjap;KPV-Zp7 z=A8+^kZMHKoa1BQnIAjnL6HkB_q$4+1|;juLfS!ytB~-b6}v#%=Ga&q$}im1^eg;U z!H^6+ub!(v?8e&OFm#1%zE9)RnOgha*9*EXQa-qi{bZwEhQ2_zKo(+PPmc7KVXtiT z!N@-z1_x!ha0vGh#h1;fFxk3dUklWox*bH#_ojMQcE%y*c;PLkn>rkJ-o-*sQ(MtR zW#)r~)X!HpYhVvLIC3*bZAjM`?6G*4t*!Nug`AvNIFb7+!C2bG6LKMxpdi1uA2BLt zT}={Y14%4qCf*i)Sw3J5RSQ?oc8Z^!=QGVa=cH_+=bmk+$JpDgf4CS6>OjzhG#(gg zwI^cq_s^#xbU1jH(+rxAT--p-EEiI9SxWlMevl0_J58^B0tj!$Ve`s+a{PF`o1~-S zQXkZODyolCZ(L)*loLO6zuW=dU<$=1z$=*|=Ed_`D*6WWjSUnBO$Bh7aK*Erz)Q$` z22-w#gv|WJwB-cr4a+<~bb)x%LAx->q@O;&#j1umSM`xrTZ*>kltA-D}f^-modeRJAos&3RNs z=@ub+>Mm7ImBY{JJPIR&rR1xiYj@t%;Q|y3k?FL>hso=m`y1buwptv!T=-kdIBRxa zEgb*rC*Re?cmN=18vqcGVK1uUhH6ZE;d0$`?_3rcsVP$norEY@Fo+^ZDbI)l4CNMV zSsuMu<7-%A4t5BlmTzypSqUr~Y=W#6vbr2t)Lrdp;7b=-MtRlQhFw^C< zewJ!-Z&o@6oeD(yJm^S{8-XA=Y!f!2<(V{|c1S4=37uXh!}b?-wXfC&===>CIg~8d zTe@c}g8knwp(SS5FrJUGpB8KoX7+i3YHAgJWDl98`Po}DUGA0L_a55t3|aBhnt?&G zjS)NsL@h;5^Z<3G5Lb}yY_Z`6-!05(s*85nVZG)k70H$8=dk)IKeFuvfOW`N znq05x$#eS!0AQl%P5%CIWc|kn6DsOut7Y`2%gzneUq82%W8a&VmeaWQjm(u>E}Fp9 zq?>K3?m>zSAt<1?%Jb3ynw7fxdQ`5%7~x`$56;hzln{G0sW)_52@TdgT@``>l^@XA zj>>A@tLvBPe(bF*4~FZ|p8yitj?nBm@dtR21~Vogxkm)uoC zFoq&uH15nt#?8(*yWArkb3JfNWOkLB89cAJKmn4a*2u-rvvZ?=CG=yja}L&JXAzhk zVrwD+0k*CX6oeLL+yGUq90emft^*o(JMeb!-mV03*GWvb{>N<<@|s;iMap>Qvgrt% zFvQiSUl2scVRtG@452Gg%R zr9a~KWc=*vY4gHnj~%Q#KtK$cei=`~ymxNk3y=t^#n(Tx`a<$d`CXGzRw>kNDB)pG z#Ic;d=kGfcp}aW&0B7A_sr(DclmLRVSum;95Jo*4PB8|u*h<-bML^0*-1v2|Erfn6 zIK{2#tj@)s&qDv7kpG_upHlXR?gmm;yx8n0`#UMyvld&F-JPiZ)IT4!2&1=UbYZD{tW_{7!r2-JQJqIc24ftos!Ok9)vV== zq!nN8>%9PYdiy)s=ipK2I^4D$to&Z>|53ZKiz_Qog<5LDuifn_ z`yibeb)$|EAHQ@fP0PS^=*mR+?a(bF0wk~9o9Rr!1KNeG7jA06S=pBu!|$)Zv*mkp z_}&e^ONZ}L@jrbus4sL}{~@|9{a?4PE=B*{s2}Ed2XJo9;McM4svegb&5Q>yiQpe@7mj@<>twOP$8V>+vFy<&%&5sa+jU_4OcwPlbzzI(+qk{%cb5Uv4Lu5 z>bMeCypT0RHJuNUTtW#9if?4npK@dpuqka7td3?Eiwe{jal@ntYSEho)&v&*EU|l; z#m)(_E%2oj3u6vgbM3uT*`EEOs3dXwLks7_GI?PK2pJburfIchy+++wg=6IX zRZX(R>uq$InGTZ+lbVQ%w~D-LxT?O1Qyk4h z5APl#+I{G>@)6LuEt5!{K18kh_tX*r|)I9@$$7Akzt z{S7W(`_YeCn9zRVKD`3E{KjfY2Yw|hR}XwgkH2x_qf_(cPOKJ%2L{V{9H*?*RhyuS zqWl3}qF-56BM?{kwzmz`wOBl=hJcm{qx&5bhhGyTc<*wp@u+Uivax2K0FuF$fJZq; zLbfEKEGN8)4&rqH>Z`2FCkYkb=D`h*<$-vs(viuj8j+p)5glHghoUrGC4mM~rz zk#ZOFgtKT4K%uHDb6SlxwSC%Ronn3b5-vb@!kXl_yZlUes!&ZQJLML6Y>lB_-PEAm zIzJg>Dam`kfebSX=+HO4#EJURF)s%R1?Jps{F{k=vxbmaJQ=16E^~LNOsVfaWizF? z%C}bXi|ew+MXJ?Lt_SjGzdg=G|9tRk`o9CZXzfw;0#Ewe4eY<6 z;XgZy>hpLGZg%U`7JZBQ*CO^k7_N4mxFRtXyq7+u!}QMP@d;oq_~&1P!iY0b=O`6_ zeA=_G{^~e44`NXl50f(}=l*reTmpj$SQ#wrX|mJ|>kRzqCmQz4Cjg1Vb5tjQk0*d1 zj0t~@IAvWa?5=d!z5ZLWGvQNd_Yv&5K7scUt{!uBH|Ge9<%&N8cIUZ+Zl!}T8o|_Y zJ!Kq*+zGb|R8_ZG+H4>NvU0mnC;g~nCaLzk7dYIDFcJ=ll?v}RfpLID+7nH~Br>HG zm@J;PPnm#G{aFnm>ZJ>7(-7w~@x>5PE^feL)=KJ1r8vG(VLidRHz zvCMXL2TXOJ;Y^ZK*nBF_c({LQt;Sq>E?rE6f0XhGpD2H1^`=48$AA9pBY|J@xi*EF z?@=VXPoe_qE?1PD0EB(_8E?87yty6p z30sw-Um5uY$;9*9Tv?m9i90dR^xY)loTI-bqYP~pIr#7E8Mf` z9@wY(9X7fn4ZZgcO&t_SwK_z*hw11!@VDA%Nyq<9an6ks*fzmQ89O@-v(ZY3#R$E* z$%i)SuDB>Kw`TQ1$(@Y1^5>@7bB(3iAIHcTmH>+u2P7#<8Y+{~okv2yXiycfKI;6? zRKM0cX8Q$YzNe8}7oo#0}h`?KfbeTofM%MK;akzNE)gU6EyDioJS&hqXt}OfmZy`V;p^7C~ zm*imQQ4Ntls_kG+6%*F|+z@OPoFN;aA=x4|Ss!y) zd6slRAm_Q!bfi7Zwp&kN0Gu0IKT<9&8*b*^P8dB6$5m%%5l3LQPN~~A7B6kIYc32W zzbzfk)hn;ILS|^P?(keukJdo0XlPfiAh?ZRFZ5d#k23J`o~cUGZhtP7_z;@Is=&;X zWQ4asPndMcJ_g%Mg)VBS=U~=t^ugS+{lS zF_8)6njNi^S^_d`C;F$n6Mi!im8vdiM()FSBw zQbgQfvArpr{~a67)`JU=NM!QeRL~r#e}8AYYK_~o@6(F}%3(w-7oN4;k&&|>9AC1@ zxBrB4w83cSDn$BUYYSKyFi)#oF)EmJbR7yzh$7z)fO>5Vs@^)p`ayH#H6eP?1Q9s>qN*>o=HmQ_ka;OAKGL}eLaZysa|Ojml2<%s#eS4bd7yGM zxbN4VELu|Zwx_?pb&i{NEt6|gchh|Tq5~$DT|>pOG7DLaC_{*Fc7d zlJlXTri-@9DsHb{scHGq$0K5@!4(p}0G_Po5a5kOZmTrj|Bxr+!+hUCqoP z@=%o=t|x+&A=|tq^r4fuVeV;qya(3fk_Hfx1(-t?;$G-gtiWE-j&lGvtF~`IFz8;e zLM)BbTta?GOj+!Qqsz#Wt5D0@pXXitwRZ)r82A(C;rog2E;49TC4|p(@pOybCFr~) zwK*6sI=U1F_S(l@6C6}kwJxG-udZRbiH&xY^U6* z?fVLRI>qHPn6Q{`7~_}L#Yc&HVdnFE?6?)rb`L7k8W)~U4TDjhrr7ZIvG9;9f1l5n^xsnQrV zEiGDk{o_?;DXCeHO4Y_wmX|yE%Yum?qbTm^adzl`bgu5d8LH6y%(S}8xwv$9??~qKmG)c@=bl8w~X@qO){zbg%Q9*2z!gQ=m36yBbp{E~LXLP%l z%P9c{0ls)`skI2X2QJ?nFy=roN*WGmz?ZZ*B-@95rQziS5an<@87_zPJQCd5o8vB2 zA+D2FXZiNRygf*bFOnwOs3K9po{5cveT(R~3Ue~kk(;-Jyd@B4`mir;uC7x-)G9zj znq)fDJi#1%K`emFeG7BO<9t*a}ZSC<#k;Thw2^+oMy zJaqA6akNw^w79~t@FF3vq1u?>8Y;@c;y%c2oEKi%=m~2p^BIE{Q&gnu)(sk7Bx>Gs zhTtcnoXqnI8*OMXJyp7CcDcQ@MM8ZnGHoVCOG!&xGMz{uKfY5%#ckMB8%PpNF@x^j z$8i{fsfu!gAv@P_evhg^iTq>}sKWnrwY1jr*&Z1++=Q1}6 z=1|ESN9YgpBDhOG3wz)^EQ8*==oOW>29!oQ%|tD?!)1KMWoFrWZ{g|(gj$Mj%F6WB z*AvP$`U`~Jk|E;ib$PG1HuQgT5)zZv=x~Yk)BLe8FFOx)+IAH#POqSa74OuBraX9% zF*3^FdO`Idr%_rnR`V;*{pNl1yyR}|6*B)@qi&e8WK_`osep^R8}O=Xgwj2n{fBo< zw2U{+fm!IVuIVKiq1P=wC7Ey7^kVCBdrY_p%{w>hG)Bve8O@n=d${3~-+Y(V zU9KBrWQ&*-H5>Ghe4ol0mE<}vr{_X+t+Pn6tlKRl%krLnv-_FZ86j``KcI}8cYb6B z%SSl6N`jIeWrPXYrQDC5q~Z-q{4kIcc2q~_(SSsUdbSnEI~p&g%~X$j>SSWFe5*B! zR^A)XUX*iq=wt=5BP7HVE11oYV{Hi*1zpzM!>{Ki*Vsn8OLvb-YlUKsT_n450^4?) zfgkg><4ns4md5=Q6;dw-=K}W9gwoYm3nth~r9z;<4xWil!D{@CeS4o2TEIxltHpgI zJkY-G(T?pUzX~_Zo19qcCDZ$a&cZ1=>8P=JpIE1Xm!Gw_y0ip#=5Y0~C~8&#o*FQG z30`K2rCziz^OL&GlG@>5JuCDAKI1C_~BB*`H<1V4sTIyf0RN*h+H~etflUNHusob7N`JD8)3e92?Zr zVyi%7G8KMszB4)pi#u=wKkyzgUA!`+AZ}OcIHR20T*3TXmyk= zROFnH*-_ydZD5+B98^jC>?rA+jMHdD+NBONyBc>7bD<@>aF(i7b#Bwy_Bum}4)(V` zDI7tEZ_4U3zVJZQgfe1SarrQ?4$R+B&(r66qAZ^>tqaukNyY$Uu^m&7Pze@0z@iZD z#q1OuC-9ZV5TrBEKCc7Z>w@k0{KyjNJednVXjpLm!()6Mv_7&yrmP=ObgDNXUD0a0 z&U-#jPXLtDh4uvJHMXKNP5^!VM@kjbij)IU-IRy`POV=ScZ`zl$}DEX(I~&yiHwvJ z%Zu`GE0km9=&gj;^S1>eqfms{$7M_g?>oapx7l)JeG8EX`a$SwO_6PiCQQCJu*stk z=?H_igaxK3OC9l|uI3_)a(G(Tj`0jvVcWTE-G_PZ4jBckCD@k-c_)Ci)s+^6w%6y2 zb!$q8#yM~A?At@3u=?ABkDV-XlA#|uuE(#p71oY^$_(UQ%=IeWHpuR+BKOVc=_4~S zWjJFj=|XN;uhe<#=x0;C`TCljbDJ%^)T9yMR94sm(M8xS*QO7Zx*F^hztz`bm69T} zwzzHs5fL?QR@&}Cbb}oVpNw;yk?bkIo#>$1gwQNR${-Y-TWOQzn@tRWZINR+VExK(Q;gV`*-B;q))SC4_a00<`d)#8A|FL_K)!tRUFzoa@*%f(`zV*ut!hLU{-Qur*zn;}4Jue{OEPrh ztC#IgjrVDKH#7Z@ZG366X$#;YCWGEIc_sO2zqG)^{aVl-H}E~CA?Xglzt)2P-O%xN zS)lbXlhR#I^zo&JuLbyJmUZm@l|%3eAom2|aC!d(;Bn;8!?|up@fo}V1bKhu{NJ+D znYF1mM>-*`9}l)>QB;PdSA7D;d^X8*mc?UBI9G2*ZzuKbD5=)yTo0V5`8^Meet_sB z)sJ80#XsF_N3#7w3sUbjQj^TrT(XN|>KE>9!M?Q8hkkhv7SSt;@N+=l4ia=#X7_C3 zzPh8?@TWNgy>zd@dV$4{%I{wV4jX3D=I{F#XI9}K@D{f6qw{~5pl z9fUer<(u&w_#f!MnV9}xjEbbAyP5jxKdlJ=_nGuPhyN>W@Bc4s6OJN;{Ih>M|JbueQu=WJA(`{jKf0fKBkiT5tV4A&lc71mNA6Y?|DPg*``)!D&8UP< z7_!in<@7%gotKo`iT=XMaiFmqxBgAs{0i~*sY=8B698StWNC1i(w~xq?LSCOD8g+Z z>L4V0qIxEm5B;Bb{wiTU$Mk3yI(dWIp5IW4XV?MX{F6AY&>#cwh`g_|lI`ZDKZT+8 zwLMD2m)X-taUXvbYQKrY^lz}&zomcwP3&oWqb~gaIBDz{1A9i0HW?w)?SCwhbbCSX zPobuLTj^Or9O?wX&Ljz(05anxtK6epJ$xHxcuxSw(I)^NrMrI$RfZM!biFU^qN$EH z5V}u$0pMO)cJ=Hfy9W|ehhO(D1fKx1D8``Y{uUI!CdM!wosNIMa-Fj|@VL7>_*K#g zU|;F*RnT3^35gWL#toINf5>%C-$&^e1CGFN9|d3gdZ46m{6`|?Kl8cV^N%4RaQ0Jx z_~y3ayXixR$(8>ZF?3&(!YG=~#DjCszoZA3Y)<}3G3Osd9bNd7MH&;9{O>uHApb8A zzESwJ^F_g%np>Z})SKVVKV%g}2z3Dc!v4V*MU89C$Oovj-2Byjb1|CdG|wG!0hbUe z8x9m(<*t9WRX(Pigr51$?se!NcCYCayH{&dL%^wnZ&s)0&N*XZbt3(W!xICDI$a1G z`Ozh^l)n6~8QAuTGtH16k%o1mZ%+=Bq;! z1=x@E;e59OIV{C4et8`+AvAFU=zk6l?Pkw(s1E!Z?1Z7?fR=Z_)M{XA3jVw?AA7-d zTD|SK*Dy6OIGg;gBddV*Ap;AWZ?I^jg<$t1R1&XCM)Fr*peW-quPXEb-jIP-<1>ms zOYx~1RgP(H8zT+1Y+?rA|6uRDeUIZfI$}eNg`!UAp(rIF%^8KzTS!761W+I% zp(&vVh#dz4L_tCc5G6oJAwf!jfZ$l@9TG}t3WO4hN>f0co0)xPpMCe;`+Uwm_niAV z=YBSSBr9)7vevtvcdhq*p6B;{EX>#EJWNtLBeCQ(AH6`5N&^t{0Ly9NAE}sqMaay` zdT_T%>)-?ky_fi~c^LWV0u5t(A@cByc1t=l27pXu$DDRAcdtHE8G89bs$nj@yjC(t z9XAtOg!$I)&S8jjSZRDRHq1<$-sFIkl67oaTRZ(02)Y#ct3;QjX;f%;5J0~5^4O;N z?e~W5*xCr8c#UB`6ZiFJV@-5ML+yya+H^vx4q3&5M*FC))Gurb)+^B}%FE;=1rR&1 z3b_JWE;X#W`lZ6>NIapg$DoJha2%KQtAu7zZ;s{!Hal$~DkzyYs;Q|Pt`QtuSy198 zMXN@vgr0k86yWhf&Bd~>4J_+&=E?OSh;=p{X0lRLH5h%=;DLOb)X~Y_XUwq9|XINQks?4?W+Jxj}?rg$61@QtG zJBTg3M9+g*@sW>kbLR7Lo|s43NcYEkuD&BYKk@nJdUA#`EaSbz{+&;MpGu08_>JYe zE>nxDnot!kNU#U|lN^f#Sp^^ICg)`6pS}MsFfSed0p8m9(-%GdiOK6Xl5b8sv~SwK zmyr0T?hkpq2J&AN^EhZv)-E|FJ*vdtP`t`H7F)!Aj3nnH!|MBnng0Hl0+&)L78dwM z3=3In8(QDzPmiXzE!M$+m`X4qvX1lAp|%?RS-T9#4UMmh4|`}cBo&A!H}BgAmWH5-M#q(e}2D* z*4k!38~~mu5B4zWg1}vx)X_A$AQPfQC!Q=yMPRu{`Jt2u_JI$%c=t%*dP`GaC9k_} zFZr^l*H!+ouemo!>xK?xrqizIY+uj1m;Z^I)#Iu{j*C;}sL@ZUV&yPr7p)$rlaMBy zH^~!~jpkEg0gS=~h3wE6pEz31a$RiZbnKmVK{B0Q{^D>!;q9_6ttfD(7r0tP!xyQS zt#48ny?fy2)GxF4k*pMPh@)R7HfoT4_mD>_F^OjH6FGWoa*t7iptoYWth5w^Z@#W& z;&?;BQQk1VY5eC$ZI&kGiEZQD5Vew^M_AB8Ii!|!#vLkM-s%E+i(K%un3~9GV1IHa zxEeMES29&XBd{qO0oSFKJ8EqeDur9dQ;gTi6(g1>3L`;v?UTp09CtE?OSCTaLj2QH z7RSx!d>*4fhO$XiQ~AOg#`-yTdoc!FF{>3HzY6j8F@$?!$*&+4I#-O{{HpEcItv1I z&wp%?E$E%`Z-mmA1l#sJ4xi_=2*D%pdA@fLVZD~N>Z_%$)vftR)4HEsjI?tmq5Ucw ziMY;!$w3_8n(#hMS~I5{A3&Q zom2M|ef%pe=68eK7e8K8CIT2dUq^x!v^B9qN?slL*xPr8_fq@SF~Zm9ZNq!0p_%D@ z!Ihokr4gO_Gkbm8dp7_T-Aq0!7;K0vmD0&&E@&*C?d=sV{6%+Ro{*5=WvQ6ZERPGJk%`2M7`a@r?w>+-FMoQ&@%lb5KUzVo2H1H3-UsDY<9yOBgY)}l z5s(lq_KR8BAI^bGd>1M=iS4UtPUnDYIG+II#uM2Kf4Qdp@3_6kHzIyy!OsRXNjtV3 z8jRvzC}M`Yd(_+89;7HlQK7PPrz^!WM};SWDHd}o37D9aL34BXYWOtDHuyFTUl!7G z4T~6}r{>n6>od4g(i;qEv~O0KQ*Q&Pv78I-^-;giL4H5)b&WE7AmA{ClGW3vkc}`N zS=0)hdj*xcz7QRi=k~D$kf^BFJgu)Pyx|pR?|{3KQzrvCJht7T%I#xz8r8cqwMNPQ zMu@V)N=_JD8^8R~>{;q}mTbjzJ>-lkBOy*W+_A$ts9P_^voN>VN=;A(s*@vxa-?m5 zo3OK~M)S{>o$QqqtwyD^mqKf^5u-)~H_hie)HyHGMV5A}IHIwX4D<1J$z)U^IhuqC zHIz+fR7no^K-4en)kB0IICtEKY^{`Xlt=3w!5ByRr6dOT)m$Tb2VAt83Zv&R)T4LX z2EWKBY$&FvXWP_dI3x+B!cQR_dWD@-Dp>}TP~5+PYNC{AsM0l`epe=&Jr0#II%MUW z(gWV)itN^l9BkCJbk2cc?vftzChkp!9&w(wEz%2z+%N6*u zG9E`@m*xvKk{HP+t9xt15#O9@h=l(t0V>jSGLUFQE=|M?kkb%nhRsezCc=@VnEZS) z#tm5LqAWZ{zrtUgIsVZ2CmeX#X)|ixKz~l9AP*h{UrdoUdy1#^PjR|QacE(QaAVH%`htrZYL{1G@56@z(Uta9*?4OaVwF@-% zaSX3Evw7Nd$;%J6@M=M?<`&-p(l zqz8NBe}m^7t+4=KnE|RQ`#)3|w;Yq5-D;paT>fCS>toNy`P3U?y4j#YXqe*fy#x!QIqT))*Z)H|AqiD z-AM6Xz;OgpF>bcUAV@fSN^i2q&^wxwUE(?#-NBZA@-j@GYU!5X4WqL2u;nt%xA)nT$kZVFLS9;@q)LvFOxu=sQ#6p)qo@BvPcekNn8(AAkpY)@RvEyjKS{q(lq+1gRihiW~y>yb|a9DVBO3!EGZWK zB-e1GtwnE*MXf$dzF*fY%9M&1N1uUA^s_$IgjCDG3bajY%SitRCtg0(*uejuf6uKm zg4m=mfZp;4!6Y?n#iFAR+KKdglq;w!h%qb&bX2)}*SJ`TV=%>^fgG--2w}%Hm0P)6 zDG}OM7PFNh<%N9it#1L;S1#=g=-W*Qjd=V0f~=t?gQ67cgSm599q4_&pZ#O|lpQ0l zM{H?-Axl*HO({43ud+l(ae;^5Wr?}}Dodn`fr(;S;sLQN(H<)N#vvt0F2nJ-XZ#Qc9+ZV+Td&_iEng z>FH(Z{@}Y-4%dy6gBSCJAu%Kh@Tg_Q>i)_Ww*)~->%$i5tOSKn_G$W~tFN%_=F6RP z!~Uc0(fgCp6i~z|-CTyC+Mn5*yxztviU?D<0O?N<&}VRFGSP0Lj*Ex6BA@#kw)x*4 z;J*SH><{pK_cfa+)m)zj7sUo4j0a~bujMqd0nMv$$gE%bb(!gVzr4UC%dy;!5-db; z#0;VS)pxvKK1CLjR3JG4mK17i0p0!HFSq$&i{7UE1NnfkeBxZv>Ux&F$?3UqTe;|r zz5P>mTRSyADZO-Z3hFfQQsm4=tfkF4m8@Pwvzn?>u5*3PtSP9rJ(19P^7}yF*5HA~ z+j=M0OG7PdK0dUGceF=}*M0J?Y0J_WW<5kKcdv|A&Wf?yS?_Z;DX| z`fbZzf6ED!|M#6h{n-kr=(&&2!fS7Bn-#CeuY3C6?Hb&ix38(}7X;*l2RLn9Af@Lj z`ihZHm{xqkeCPgiJ1y=%$6VFE3Z7c#_b!6Gv9V|^Ef?c71CYRt>4H+kTmIBx(<1#? z4;u;*#ba--A>A?469Tm?4G#2DN;0x}6qJPs)_weRO(JZvY57-)43&+b=P#8-crtF^Zv3-0bO~<~@CtdXRd{vo#i4aNwRG&v8Fj>?np^|x^A(Gs`F$X9$v)Lw z=Dh{eGcpef$_bebXvYukQMxAkbo*oKV$sTcf!bVAVffxX$yRS-WMx&);3Gi{En+|v zr)}&jMt0FO$nMblE{xUq6fZ!}*{Bfuc|}I+>TyGy=Z*5-bGbhj=)Baf=03O~P9VNi z{^mpLB`T|Np$_bRD(w;CoUxR~Q}dbT21iplE>@gyoyZqN{3KVi*~iMzYcO4{*fNRw zY1_o_GH?lreFrx-hlfh&k(*YLe&1jJ60|VDtNo&A{Pn@-s9z-pPNi|L)o1=HF?=G& z^za{?g}(sPE&Jq){$MUVZRuf|_da`QEh%v~-a$n8!I)t~ta$UsHptdt*O_}uvxeY&Q^_GZ>3@-VD?A-?V^F(fi@W6UcuK@!iQ+@iBOF#9@*mqf(u!(shFgeVYHh=)Ej=EE$*Xrib6G8^C{+UX)_q5uwUNA>XtOGV%u_x5 zFAY25D@x84MjyQGt*PGa?|JWu>p%bDpZ)Mpd-w;&g+%PxZ5yY=dh?mTq|Drm{P4r? zl$pO;JBukZxBgs0U|XYERSH+$q#yr@)#6;y=29c~I@H+VN@9*5Z|9|5@<@k%tB;N{ zItG`cnUD5*;{6$wDHWXeeLR@DI~dgmo>M@aPUh3EJgZA=-*-!<<8ghr*X$@PC`&0> z>|f&pj(qzF?JW6dZaTo{_R|8UZUGjEY0YfJ%`Pr{^GO{#(Cbp1U=2IeCiAvMHYKd2 zxNkIq=+Ew|9Ad^bb(a3DO=S*L7{E(FUEPNGny1wBjqhH{3Zg+3zLmvOdq1I8<@l{9@`un9Yy%orz zwJVm2&n5%N@$B_&frr$CD=J#l4_@|g?q?LJ1HqkWKe?^F0TE^6&tnzFl1nfrj?PLo zKP$}v7AZLy9l!=6H)ZSUcgOoBlw8-$>QV3UkOBE+>5MI29;j_SRO1@Y_LvCmx*#h7 zCkxFS<(wUn-v(w(7hp%K{6GLHr>BO0&v9BQ014U}~cT@JM8$yYC>xZme`)6(9-9QFZDrIsb z-QKg6LTwxBf$dJa^7UK&Tf4`I>*+SXJskXLFKmepSi)L+*)#)-#tqv}k#n{X^>PkPdRAy_5D11prmg~%YJM_<{3vbtt!UJ^5&UT` ztwsCL-`P(P1;|)GDdGP{5TFF{4SVXLrbtUc9Fz5hAVCq7W965F4#Ha;+KB4ZWVsS= zt1PU+q#sq%IbCoUYF$x@s)KHyw*129FHB!X6s%sCRC9AqEofodmUp<%21{3zLMjfE1lA&dExq2pU$W9I>Q292 z6CZfxt$b|l@Y09oEsQT%GdZlzCqg{>pF#f4(f=1VpTBE{tk>JYqW{4JqaY6w)h*TGmTvm@q+z!#oLKh2bT|Y1fqph~z8k`3Q zMi}9D9gfko1Ba?l1sQo?Vh9=dXGHBeP*5^n2!mbm@g~=LkU_D_y0QZWB1TT#88kUf zVoK8`L?x&g*VA1HmSqJJEG=cgOrYsV#_=3An+c-6X?}f|l4hfgak=;PaF>IiGIJm!GHFXNb^Zvep%-h$D$j4mi%Tg zba0h7gKGIIf5y{oW<9%>-C?hpAEbgQ^$I1=4$Uc!PFsSD@7iY8RYL-5jnwA!cu$=< zi$w{goMj`V0_+$rLraVzoXXti^LAf)y|DbNO(l3vsnG>yt7G77c5o#oSJaCq^UWUA zxrXr4WRW4RpFS<6xqr&FbtvVOgQf5thMvK}L`O;y^Oi|u&|8dNj2{3|2AUH+ue)E0 zZvSO;7t~mQl!SC{tmLfenQg-N3p`%LBIyk)@z6pQ+zej)U>|Qb3OqmO z;SsA?>5+Qh=u4*CLQla}_s8-M)v#Tkm8HHQt14EX?j+dyGK)%4h;@;WeLvmRwISao zlL%OX`(z15mRD+ymtd?@`z(m3E5Gkm&Y!>vk5(_7fAe>9ek6Y{X`q^s|9LnzUdPpU zuS-Ar@ui`dy3WHPiuncU5mgKhp7ywTV%lP1OA|n>J$Kdp-u$6QXU&l>bPS<#BfyI^ z?#nQt0OK***b`D+_kdl1_bWX;U}#7QYk}%}wk9rzNiIU*daE&xIQsoHc-oJcb5@8Jum zoF*;#Vv6U0Hu5OC2&*dpy}S7QwEXt)(kg0cFi_aK81<|sh2%r4RFrCF-b3b>7p$Jd zDyMpn1(o-=-K)i1uC~6oi~)ls=L!%m!Y*N7PxI=-Z#z;BN$h&^4_>qOpApDH=G$cL zk-_=C9Sj@z19#M75H@NP8ijSVaEQ*wlo0jY&;2U#BzGREsW3svbH%R)gZ&&8O`pgn zO&=3RL7FZF4}A10q*iVmQ?Ak>oEe#>w68yRX3PnbqPS}hzuKX(i;;BMb5{rqTkoDy z&Z%UnrB*GeV)LoYDJE~N+i*&V;55z#Rg_v~#sCA*xqt%|+wR}`n+LM}`{z316(aS* z{)0npnDG84Ylx+vh4-u|jti{X8{muZ@{HYS-jPzAfvHT-ttf5qaziMla0ekNSrBu? zY_;j}UEW0x%%#ed{fu0BSiix0_hBlMY=^wMgN12A>{P%K)yg=yTjPogeRjyNIy6 zJ{L)!OtngplA8fHv+dRvO;3K`>3~dM+}Gg|66*Qb-e%LBS=pzfZwfV8H-_b-Ll=bI zjSnt?)K%Z<@ZGf>+-8UDYiOi1qgwJgV3|z2Xh7`GjBQ;i>iAEl`L!^_$m1(<3;EF& z56@Ec^&>$*p&V<`T$r@k$=j~0U;0!6__urfZ=Qz#l*S*NhcTTB?1W4YFjGeA@bQ;V)$UMS1>~6eu7QcEy&;bkQZjv=MQvAyT97HTC->P8 z0~}3FV}b#EL^vQhFqjOnxA#G+^2~*TkYm`mj5#gZOvv~*FlMX_G&E4+o2tUt!qgh= zHlm%6((U)hg2#JIEEc~=zphL=m;T%HZw-)tZvV3%{%HgMw1NMbHb6iiMjoZU*`SNr z`T%K4;?WZlpdbIitc#!ibR#mHTc@VWKfUuysa09q!8*UL%z=pfBmH#Z_Pib6hv*M7 zd+*zbBKRe~Hmm%FCTDXlRO3K95vl<*HV@{v%y*?;Q$75GmH!;Y#HNx`-`eYmMy(pH z@QnNLDkjDwFfEL~y3DuCg=){T?ILbJ-KopjvUV%)2<#aece5!Wn9gIRGGoTXv%w<7 zd7do9QThkWB5%EQ2-#7T8o|Aq1DwgYEc=TSc_pZ*gn*A0T@y_;X6CF~YQ>nu%G*Tb z<`osADjwS#z4B6$legS} zfBzoV>aUB0=|~*=-5K%n6uzFXdpdEVg!5DTUwqI1{LSy~`YT`8Z*ehDNA%7i9CA9& zwsisdVtZ(9NJj3iPt(svEW3ZL9rIsz7asWnd;UZo6(Ny(YxvZEY!$u(4P*nZ&YHD> z^xZEr)I{&B;j>HoST=*Rk#>lcBYT7Ed2P74RWem#1;q?+sdeTYp7%fpK%HBPta}aB zMlz=ty^awa;xGDEwVWM9Yxc~g4*7=>52$;jjoIa7Er$Fk-DlLuc~x--_l&fpAk={OlUIh3Mr7KEK}B+hvnp8|%_h1HvEXmLZ>8 zBz?@JaU)7R_r2fNxwyF3zVVkqRf-$Nrp8~7Qq&OGj>~v#4uKIq*;I}P!#w9`G_0cU z>g86;KFh<$7uC~JmR3@rF;b?M^jjvai&$<$g*v!w6#3dwQ@1cIK`yN~9~wUcD#>XA zs(gx7&4pz9Ev?s;(4X0hu~lwMx}|EPMS2S{DeflMznBubOnma@p6ov}B#CJpuSn~8 z5zHv6w0u5RSz5{kBG6Ugz^G1$C(rBL?3=$d$A8eoztedhDLtMvhov)y0w`medy8Fr z!MKr8Rqg^>7F5!6X;bp+&j#=Pf0f`|+#LGxVD6c>Nwb*8*X?eG>?zOeZ#q7g@HN-E zE$GYiI}FE&(PQhVpzw;C9;qcmN;#eEAVyJH(**PA3|4M&)i3L1)&icTap~zPuUXuy z(ek3`7Dwq(!E}Dc;`#GN5gPCh7SH%Bi;}^6_x|#b{(ajryE_;Z>ND$Lo9H27Tzla1 zxqX%AOB&N6O;%bXfJBjg|G70i8?0_SC#lUyj9?s1S#!@Rt^7lXrMesr^T^pGuz4GalD zp!-{FrJ+f;TXXt4>`F>hh7Z4V>E>JNj$2AQe`=$OzV*=bcGdvB?kaMg0X`Y6FCI<| zl51_5nF(v+T7S8o4MMC#^ZCH_UVv6Mlq>1 zF3Ny#fna z1qIcJ%6u2#ojrA=8~VizppW1jtSW+*WizY5VxmYGW^%=Qk%vGFfCObW8t>h+B^7Q( z&-Y#B99m5EJ4?buTV~X%ezom@n(jsq1tl^oEPom*x;N0HlAP*lj6GA3DCI0^)kffdLwYa>VSPC;97XON+?Y<(zt2S)QUjjjNc>&E}&9KmAm(Sb8Uu~goBFKxs041D4Of&U~|aWIQnUjDM|hOE2yR^ zrBEfEVxZ`3tz@eh3M^{yuwf+hobBZ)+>91rH?zT-O&I^`>Pu9{jpGfxA)jQyQTnXM zWEdk{V==aOD}|` zWL)f49(Ff*tZA0V^6lX%fqII)#)}#8G+yx4jw7bkCm#iPh2}GC_iCOWaE6-!%)wIr zIH~!r^w?ckeQEo5)RmJ~F;^mO5 z0w#m~;fmC&?VhQX@-_rFMD@${<)(xoUZJ0??W?6TUdA_BN4)$r&l1L{ci(EC&}v5X zzBtt9=sY`-oT80p)Cit^7|f}KKU84|T0utZ&OF$F0=QBI5v;x2bTol!_#SfJCCHw; zTCu{p^mUVddHDDPQ$;%=z|{53eCJCKHd0M79c$>yxyoxYVXURj%@aC}ey*~8{5d7IkalEtot<7;dxt=(Ax9nk2&0`2@OX!Ic?chBGLPl_o`{yFr@l!eTOex9B_A`A1yPlim_< zwv>Aun!%Qq=fbDpg4~C(n>DN@n$<<}V1*Se3+MH$pRh)S8UmKKjAI1>J6;aoAm2GE z#OuElT;oH1edAYSh)5#@iq}g{N{(w~F;mMrBHh6Nc%)cmseV~B(fd)Myl@bLro0_{ zd_H3_5`Vub@%1UZKU=rAFJA!x@j)-{Lq~{{+*_BYod#Li={@d9aUH9mLXzvyZY3E* z1eC!LtFouFJgsMEc(e1VrI>;@RkC&OzUD{cnYF#okDQq3G>$!{NHChLh{W$LD4>$u zan+>$U)ln6=05IT2qEIycHx)WFKP>HD>AabPzwV9s>A0RA+~#g92eAmE($*t5v7H= zA`UU^*Og^=j%#tQo^vz(rnkYhHQl8W9AxVgNb1#V()d;4XH(N-fXd{8j6jbtTo6(3 z;oG#UtuxfOM<$KGZ}F^)-Q{urd4PU@{XleepCvS;@Xe6ylR_G{m?)^lC$>G-@_dy4 zq}JuDYm^bI!zP+^TuYTtBP29WbK1K;)f+79Wz?nAIC@=2IVyMU7(y{MVE|w1D`<7F zF)STwh(l;P4fn@ZKEb>2933xJEu}`evLLyLgq)H_57njiBJHP(|8)g`wsi&}TS7M9 zMHLN&qU1SKqosb}4Cl`yh=mQ8@kxGsO5 zulF)W9?i9$-njlleQEnCcZh+6*`M@&l@L7r68Jd5A(gr6R)0ra#j4}8%*{pD8l^gprrzuKVo*~Au9qn8ly7zJB1lmKkOL-2U& zKUjY4sF4xk9`y~zdkxj)1@&U_GN0Vcq6*QDmUWiPtEP!EyiiX znseBwMf5{qO|Pw{F?vI$Me0ibXUrNU`pS1xGDfD1vJpj+Y*1$^gL)?2gBJ>9P*kwt zTf2wVDi61PF;#Fw+6*W4gveVBK|4d>Ym>b@ZT4!4wM={KH}>wZl}b6AimKunehLgq zh8fz0p=Wr=h~b4-ZtDFT=i)yww|GKwLsOyQDLrA!PqViRs}~XX1u3j>0pU=petU=(hS8&*@gZJoVBOHH1krpPW37v;Q1g z=IW*qY-zEcPnue#P(v*(kvoEPv1$d@KSW%ag4n%99I5KZN3EGoAjfIGg>;%LgHe#s zPU0MA@GO-LR5QJSl#4Mjx&p{W8pgDKKocNVVLV37dO_;j^fU;(?E2TQt}nW7IkeX& zXEshp4sXqW4i9aTmo~N--{{+j2Fs%XPebSh)(X67p%jdy_1n)mMBU-@n{0y$@eq8v zzi8y65~^1eYE>QlK`J)jfy2IW-%B)d!*xY@PtiCQ-yYrcZ?#NkF-D5= zl`rhqs|~DM*1Wr{pwcC2g4U~?(Fkpm`4Lnm&OK`ai{tNG`%*N**JQn{amA@AYO27n zFj0rCNlkW~+mT)IVku3RZjr>1tZ5vo1b#R=`&*ZoWLRXcwWY_ot2Qn`Qv@uUl~Q64 zVzx*%7f~h7u*gr*i&a>z2mn%c4}>o;vNrpzQHeQ1#9$Zf5vi*pIMt}F5FF!EkXP)x zM8^6i$jxp9XB{y;I~EJ;jrsmVhb0&_1;J7QM@pUcpBfTLviir%wKo8lPgcPU+nmcS ziZfuRlar;wFUBC9^#p>5i2C%q-jgVc#w}b9$Lc}I$IHd=)l+A$Ewqf5{uUj^f@zuN z<}ZI(-KP?)5hy9ce(!XBPQ>~_=RuIq^KLjx;oz>oHj5cd zeZZQZh3?R2Li4t%h+VEH5Zd!)Kq=@_3#F#2&A4h12 z8j50tf3*qkXbA~dtl*Vf5UA0uWk9em4pqET!Jy2vJJ>|KEM|{R-+u?t6E&HHwLCn2 zv(#PlN`=S5ipz_GI+7SWN8E8jppLE6&_U%*gcR3~;?rSZe9Y1#xcGTre^33H!s<5U zj`OAj-}XmjDXDNE^)%Of^&))kmp?{^+*sd2?%6(MujW`}2lh6gvr7GD4aw1rT*nrd zh+R+_(j42?@FG`?9ZT0Zeu*C(qdO!n5%lja@u~pukz21r@ndj>VkeL3 zp4-Hq1V)!y%PPr-QrhUANp_62W&(j^=7gy~$-6ev9utl-vS*;#fox7ZJ&dRsN5->Cd>Y^U#aK&J`gjkZkl>dZrqQB2*T!PxJhEZT z$Yqc~Alv9GCiRO|kqld6D>$U#2sr9uj_>S*iXl(D;=9+eqtfxJ_J z{jKhI$JLdveI3pvE?T;B&?iiMycSwZT85Z6N|VD64zJf<_QK}Z_ge-m+hv?m5&Orz z>U%lr4ntCc(zFPo%f%>6XzrcVv4{H&9t2dzRe&?H@;MvIgn96{55XnlXZ3l-YCvNb zsg{3DoDx9bN;-37TF z3c=a_n8I|wmS7r%Nivj6vE7c9t-+zzIfJ3fzF6`1VOKo+f})0MTIW5;>FSAZQxot* zzLtgtOm0||@GHg=H>>7hWg@WQ79W|fVIe^F_WlLQN=#f8_A^(f8iGK0oyR%vs$5S9s3q6Oti0BfHf_O0oP4#(@7&7g5vQ}rR4Py_Fpa+K$ijo(+HG%^?WP8wz zXJaFV0Nt+C3(oF6w`Vf8gq~d^v&<0QDj0k~z(z1cE~ZRb;M;)+K2i%u4;4CGvjgHf zYM5WU2Q>f#xq0{@m^DC>k&vSD&g%DC zOFL)VmW$Eag3^x9rE~f&lJ}Bp0^|BQ01gtRbf+l11Ct`Hi6fB-Ub08+@S1Z^zES<- z-u$2Xygf%yk~+RNw&GYeuKHY*`(}aT+i%K9Qd`}Z!n^2<)KWKv%sKyIr>Xl0EQN#a>^dLeb7m&>*gB{O z+`ur|rZdxsEM65GpI!$Qj_ z6nlW$=AIs)*Y^P+EeL<#+X3Q^gD>pD@pQYK?(;(MfhT>N%rr5Bq z<4%1dy|a)`Rk>t%;JUg2mzu z)W`mxHj_>4!q)aB5M`p2D;)SQftb^b`1ogpd*A%+A{u%? zs{-kx8R{cfD!>P-RKo1TsnyK`n)b%>asgy3!s3*N1n%Ghq_9nz5{@xRVGf`H`+Ra9 zpt%gX0AHBs(jr(4Dhg5zrJS>?Na6!)w>SudBT5)!|4B1hG3Hc&ul{WMk+Yy(zIP6r z4%zQFw^XPRO6}2z(KNe6x299skIW9A$tM@~>Krb~VqLH zTH;U#04Z<(jR z7Cq5N(+?mf01;dFvz??k+1n!%|JEKCt8u8;#iii#Dg)OG9-VOjka0&y8!a8XYbDG2 zT<694kAZj8L_JBPW>6&yDY{_J*BnQT^>1E~D0N}qH)7!39xE<}8}k>fgf5&``U6Ah zM~`@g@+DRL(aPwu@t>aRqtcg%eTGmLcD+Oq%{Kk+&4Eya@eY56<%j*n7Og4oCh&Rf zcx0*cK9{1R4o7H}2f0eC+bqg8;{kr3JeybDL(a`w%f8?)SNGj3lxo1NC@4d_$JG|)7Bo&P)9&xcTu|~WlSr} zf4m&wS;Lpk4`4u9?OA>IVh}h&Y85peP(@>E<-& zE~lQ<$U{se-kTY2G0as}O4j_@Pc5+TtSeSsr0Ak(k78ul=7|eljeN=3ii*gVjN235 z5vPIdvS+|D7K1~e*kG@dfFq~usG?C9lCQX;u= zSPQWNho3Di` zEe}u9!;MMA6Psx--+JU9Z5qzmCb(;>&vVPG*)T%Q4kPJDS*r>erHs0)HJP^K!%i-6 zTU!r3_?kabN%vTnWMLFeR3|ZijvCxIG#a_=?b6hU?J4SE){@&d@ehV$$-ZsUY^XvH zdMxTC*BC{9?j(Ow?Nn>*T+ga8+^-F-1TG<}$pBXDldYkCg1 z1A+)P4$buZDdb#N_hRz>8b&%feezH$kDb}08y5ixo2hGwida6|8x)5xs{z@>+;b!& zYm`+?)O!#B?gGChYTNUjnjyox%`5E@;W27NbL~`!s?W%tc(e~-`q zmwNAhJH+iBrw&W9Vqa;yoh;xMo;TH6W5Z9Ndet{GsSitaaDN-=gM%-xU+VDyn?Nmzx8f4wly? zR!H-EzwX9ospiMfr@8h{pXR20lw#x(3Zb1-IV)+42ID*R@x#iUno1ot*uxA8I>dk+ zAjD;N^;W};ZtYChY8gvyx}3o4vR+s#t1G@!$U?=B5$x?lm$6iK!&IX$_#Uz1saPX_X6)mzQWh;zp z7_qrmT%(8m(KNFPH5^O;DUXh$M2izZPHDf9o&P5R-@V8*6V~6A@)-D6jTw}jQy*GKtKb5ok>8m+Ur^syw6P>fTW#dr^8^VttDhixv|gZFR(={}e2 zIFAfJMT@CMa*2e#-k*+pl@H5b$zxFpiVJBZGM>J+haT<|X`5l#OqWq+c}5&DskKjU zcMD#Xg-B-EY|6SHMx-C%>0i!acM=oM>PE4VXUpsVY}&{_N`eUSe=ZY(HPZbd`p1r{ z!MKCJ1Gc`i-E-?ZL{S}jx`6BM4-9Z^rJvrI`owB9;fX;`b>H1l=h&+U1V5edrB??l z6d@2{DW!TB$#qsjV{4zlVwXA^ectYEG?A4)tH~3~@2X%IAQs$_0Wm2pnvHxdnJ_3J z@%zZl%7lDC3Z7ir#;P9=ssu%Jgfj`b4?@8;8QDan+{3*OwF3mRz2;upFqc^Kuy7}r z-X@r}I_8c!uZa^%aCDOX^I`obZEUw$q*m(NRMz+@_+tVOS`~Y!0e_WwU3V<<{nv8SYE4bZ3~=`Z7irIK9E~m>b1MMn{vD2+e3)3HQt=5VlfGTBR%jI z>fmYgbr4AUmPFqT=i$Q??6qk(EYOxjN;XMzAI#v89b;iHzYyQjc*MDa({=I=SYOU9?;Ag|Lz6qT+w4w~u%{T=tXnA!; zSvd4^va0(EuV?&!vG?XtO=a7@KUMZ9Rv{t^q9r|{QILQEV%ZS-E+ip9Xq29W0ESLL ztkMz$lvo53ByaV5IRd#nqX)Xngmp&4+KkT>^;AnbMJkl&Kq^#JLh-bd1Kr$ zUjE3~StD85d#$zCUURPb{mheUc32jh)0DO)L8pTQG(ZI223o^nw2HuXW;`E|x^%6I ziy?eN$k`jodpRB?(Yq77@o(&w*pw20LRJ5cs=o^Sl4 zs%CG47uT9yAi;#b(^~v4`^9Gm8sza9)G}`?;Zno-V?*8v$$mxDhE}R=v<9`>K~cjK z#AC&N(WBkrgU?|pV)7AlN|vx}c?VV#a7jk|+D7~^@^p!j**i6{wj5WotZGyDJJ89Q z!7TQ|_?3RfJKnjxG4N5f0tuBRh;vZCV0@I&&0N%lK)MQl{3RtsBz8!6`_=@#$d1d{ zI;pQ##~4@EX;$CWQyh!K&sLQC2yd(;^r*cQYHT8uHjB^?Z75TsP95K2qaqXOFs5!2RTcP z7=pWir!ixXYdjoq=FkN2<-YIRv=ki|TD|VSGrB#{@jqO{+Z$UNsv>^=`mHX*FH$0F%=o70CTKg;|*qqSQV z7$|QJt(nicBD#D~@dDnS5~SEq91%F*(Vw;87VOwL^t2vG#G$Tbx07sp->pd!Gwyma z*6j@90hG9loO<^U$7gn;Egb-!S(Gv20dw!88{R16V&6~7(4?2EM_jgF;oeO*ubY|@ zpfyf%_Lw#Mv-FvV2SBrBhE+q~{3&(7B-c|aZpU;j4~9DbeAM=dI|Nrdb%NW&h}P$Q z5Zzg*pwT*`nYA^EbuhMI!pMYWlhE<>;){bGAlkfiLAGmdxq>YrPkW=Fj`bY$iw*+t zx_8aUoZAJq!c7ii4kv?_2F{WXjg*)eVMxPK&B*H4)176SBr3&zc4YiAjqw6B&~v#s z9l$EP`7Gai<>(8Ivv!(Vu|Rv-bEf~UF>F_DYl%uZbf|j{EwS;W%p{kh4r06SPuzFb zQM9sDxnK3Gdk^lxgIc6Gw3@sUfMLdo$GLr4tRPQo%3BDJa811)UC24@*?Qy%6rzDV zH>sU7h?^ZqV7uT#`ub~Vn@_SctM;KI=B9MRYP-*9WDdkZP4KvR-WUb&$Q80F#h*b zpnc*r=TZms8-t^8ft3NKJdSv9Y`y~yx#|gCS^%n>OOJN2&|FPk6#1zX`8@t(??!9Nj#k1;$@|Nr z9&JKo*cMs&t5wr`H5b8E_|OzU;_t^C7hRUIRYv;1?X(}UHBf%j=2Vy`g>2~SILdO; z?c8fICm!y7t_YA^MaWqEJ)J*wZ!Z2Ay1$Vg-V76Ahunuvyxrt4)0jjlk5N;JK){={ z&;5%m(La0e{L7zqySBM-)Git|u~)^Z+s7mk6+x7sqTKMHS{b&X;nslM>l=e`Y#ODw zae(9<=c1$|m?&DPW3M6sI!cY{`H}iMBBx^38|Us}n@P|3z-1gDU_Wjoc}OZv;ywt=+hbM^xj3=cijs; z0E=0Jqx7`j)P9fMlGZf%YUlha;L;|FtpW_Af`YRj9$7`Wft2zyLwg#{RH`}}jSFF3 zlZslI&jdXQAk(7ZbK;->&}DzdAQQxe{$~W)zgv#}p_>2xhyNFx`0K3(X9c5^E7f%# z;+ZNsIb`-wng2rFn(qu5R-!^*`uV$4B5-zxzz*D*#wXv1pxi7NWX8GNcDx*KqBJ;Q zg~Nu`2Artjo_#J!&<^R;L!EsC9d_)dBm|L9p1uA{t6WUVk%oL91uA**N_L%{wvzq^ z3&l8AV+UX`7`oary_HW?A|UI&=U`;3ha>-#gIOD0fBLR5(oy2PKK~0xE@{(vFklrI z9j@jSN2c5aI84I;-PI|IFl3+vz1Wa{SzV{!Khh{Nfdunx!oWS3kDD=%Nmr3GOZR=N zN+uB2Te1C*dg;t#A4p2A%b@$^29-S)sJ1>OuMoH`|ZE z>MTecftsxli@G(`vd}JAJ9iAzkfI(_5pQKc(^r;3m8$R&fVqNt?z3UuS^I8A0u?F1 zHmUZ+b9{ADoz(22X2eQ*Vz?x|BBOokDcnV&$4FOZYy9E${|5NKH^%>k0se<(|Ks65 zw@bBccPHQz{Mp|Wo7+y4#i`|tNr}Q2^?Jvt%Oha_yLfYn4X*}WbJjj1j+%>Q301K9 zHD%=(Kj>+nY&&D6+?)E+p`B4EJLYpm`M|-^Kj1!{6hd&vOz`aXF@Jt_&Yepw!`W6z}vM zoks(JOkqqqJz?R6L!(7X>d4jQR~3XqPc^;Xh16uT1)DZ-S>_Gi%>IPt;Apdh@bA56 za8sAc@CD7gF^9ASQ*TK?2xcOSyNZyy+RF4}7OG^U7Ie>EiLro>FP^j!%uz9*4Hx@L znz{ra(`n7Cko8WK2T>|+g3elbjn#0zzt8*mk3I6gJvLJT*VT$vsO9VybIbdPwlOWY z1nJQX8XFF=rK)g}4)`Uo`_Z&4Rmql}R<<3L1X>zn%>3<2RhYH6mAv{J+x(g~#@a%_ zLFmaHmVH{i{E=z}XRfk}(PELVX~*@hDLNBSco3liNEV%mD9M=$a2zwx9HMQw-4(iC z!Ut8sJYYlY$p1lU85+D9cec zsS4Yc%duc1zkq|8r?u>6j%PclmP1<*{?DImFm{ExG_XvG^C5vi#xGqNHqCQ z+`$8#f|WQk@kUNaY{@vYh?bEd7|AFRu39dL7;4SfR^VO7A9Z3FwD!%{@ftx?Q^y-a zKmR$|W~Zsiakcvl+Fz3g&A=ue;C_^`htd}j>PJ0bFr@auNHn|oGW6)C|FhWDM_`@F zA-l*5T03JHE~qYSjmv%u?(G5KY$=Sbu-cm6aZ7YMyD8uP4%E&M5Psbv>uDEfZG(;L zOsv}3H5*Zq+R*6ht#*h^-gAEwv5>VIwA(MI=u<$@&6}h;Veb+k$5UP4mNQ@@U6)BE zdaCq$KlnA7tLnYH_HO5LiTY<$%L|RxvwZt(h_7x{SU(0VeS8dV z;riRWVfL^}3mMxCS_59WmwBA z(-!!Q*qA-Q9^F#wXAG*Vveb_va4TW&+^*m1uHY^1ohbU+g{Hr(ppvKz`c?#BPv6S&R$MA(W1z|+m_8qt6V-+>%!6+<@cqXj zhy#52$7%~g)u5A)6=MXvr2hVbN2@Na((~byZS$Q|;+tm&L?HcAz=ijpH>x=DJlp zm93I=BMi1KtVB1+@IDoB&3Zo+GZ7G<#f7P-5;iM2No*EQ)Pbajz@M*7oUtebur*T9zByf z#JE4-XZEsULsNGVx-%!hf0mX~{b-?Dqw&2R5YR%L9>%JN?>HQ~rk{yfq0it6+i z?Ss(%!LzZ4GupqAfU0GPj5K;daiqn zB3zjcO86rGIn1^_LMRzQsGnfU>yks{|n}8|;HLnl@920I#8jB)7B3Y_asE04KEq_Y*Vw{6iReV1p zOK=UDU)Bv3Oa`|n84>wD{FDw$^~wNoSK) z{2A|wwCZl;NgjoQlUgJcXSI0v9dSZ9L7`6SkA&F_R8e-OqJIr!s_K1?EYpe6`6!Bz z0~7jDsnwL32ukpXWg=ik@t%#FU8Ty8m%(cv0*<=#gDNW+6>)prC9PT6Q$DG?voaWr z&Yd5bu!u^p%aQdFekRmhi_ae6JnqIxf*DN*vL5HGw-7uQ-h#1B?cuVcFAzEDf68XfxIU2Q+j%oQLD+(=qQ-G zgAZpES6TT*iO66)N2IUuYCZvlG5|pD1$!fgdO#qy^|YVDJmb};pK`FrY+p{hPpWYv zd*^Q4wOL?f4ubT$hdI@R7k;SZOrs%~rMV;l=}D!$bq+xq+&ux1`t5+nd58@z#o-V! zZ=1sBWLU(bQoSjS^4fxh;@i{BjY40VUPZGh2oTPgWkw#bata;E$`S_H>Eirp7=Y2c z!VW)bJwxnt@$r<{eQDCTd2K|u**M`8GhbJ0L+Ns2=+^V`YiVqBzw_8Tjx8{1 zjIro4joba^qXNt2E6pz+lvk!I8yFmixa_k=_arQ&MJU2N zcs^oFu*%A^)r6)TWx?$XkR{4W@;IecVEjPII|YUM+|VVa(Lj}>myRIUjm3_ton~xy`YcL8Tm4bJiz!$0t5k{+as>h$l?d;8JCJk~ zTYnQXEt>VldOEr6AHA|y!-^lP=5RF1v!-f4F9qt7c8oU;X%L&(F3kov0tZV{jo()z za>?-lpdIIyPKd#!li=-yAl|T#bHs3 zGLI^+mg<6V&bhb`${o0tl_79WM%X-+7oUzr@ze1y-;d}%d&bS++Q1Qz9;RO80P5vP zPQ;dL3DPj*V^wFF*H`;#SA-4br>^^5GnT~;|1!tTzurkx>LEQO;A2?rJqi>`Iyu5e z;#qIM;59K1fnTqTiYe&M+ctEb564!rO8`m!yv2)#hQ%XK9pBaKJUoo#D(w+LoSIJc zH#Vb!^WqJw%F-IHpsEafvkEqJ)2+@)Zcyu_>dLREK36rFH!0>?ca#xUGq^gLyJl{o zC>!fP{uJBhAK0p42eDZ?-1AhewS`@JvMHrN(#!VuI0cqxs=`wXA1)5V-BvxH54Ze*%l>Cm^i^^Fe}lP!Bx~0 z(G#zC+5I^te3jh=Jeshq3;*aa~ zeI~YxT)ybC>IvHu?}s3Q-FyfHXx+3~g_jSZu}aRWIbQy$H8A4(6%iwSB$eFJ<98*slR-M< z>2BxN>2PfF1TovjS;L|b$%eux#lLM!o8S|Daf^E?-$XjO&imvU0|9cJ|SL(N-{tU!(pLWpBSaWP?HwcI)romL*nkngHdHu zGYH@8;Cb{+q{sM#yw zI=U^SFFdHqwqsdMF+gL*rW;AnJzMa-l^hzdhA@k(_J*M5UaLJoG8=U>sE2#+6_@ z=BZ^bjMffH0&r83W|YQ!nxxr?S5juo8gL&+}i*eT?NhYy9NLo4f&+*caBF1GgCDX6tn#5*^Q$9X@I>b#?Wocaarmc>O z%o5h_O6Fr%X3mz7sJ8BpoebkPh6Mw;f(kd!j@F#I=Dl=L@(lQh44*#ysQW2rlZZs! zf*)!m-%Kwi9^iL8evZH61H9W~5k^vWr$>nw`vYrQg&rQQyETF)M2;0i-n%xZKpa4W zGwE67no6l?PTns+->kCN$cTT^GozeEEkoT>d<1O|9xl&eM^&N$AZ7kW5c#qTpYSCB z+IU)Iy!D>0>*zUlVTt-&vm9xFQ13f6=bI1$+R8%O%l+`DahP zpa0FfFt$UqhEz+96pQX;tsPsODYEh{A{S?tT>GN+W;(hlQB=*Y%$kj3MvfTP50+)y zw&@dX0xnbq9=aW~|Bzn`9i^-t7Z>N=uQ`Vgm36;!_A_6A#tdR|1Gn`u%c#A&me83!w)36c5=J2}} zW7=yOkcLkQU&QrbGTEHS zyyfYjQ9H_Mraf;YGrk(!C+856StJLL3=`6O*Ue|9#dgEZ?t}+w0ril#zL`u7$w=G-^ru09=)K(qS2{SXPPkgxI^VT9ic zD{lnbRu$r(q8I&B+%AYjR{U;4@fd)8eWQ_Ieq}tIrnER<8c{(l z@S)KjZPa{ed_KrDJ50Mj|H$I4g(CibAy$0=ul9NDs-Hrnk~?r2X1R>EuZGw*%V%^Z zsE8Q)*yRVaD@~o4VC2=cgU*g3fLB<$-aNuF-)VuvLfHI8PNLD=K>6qpaK7;X)>=6N zez)n3NuSp44Y>UX*_PW;%JCXsJ!n~r8*ix@_i?ls(v&vXK|Ln#FRL=%zdLPe5n@{O zyw2*_FTXw%jy>Ykxg*E0->wIBE=>cE5INQ9bn@zEE;mQORAe5!8-^a1&;UmQU!>c4Ppa-G?|=8HSa5%x;Y}6?5|9JzLrv z6r9b5&ma6q$q5-q8>3}q_Qm!O0h|(lm}zHS>*#zPiZx6SR~|U`WiECFGb~}WAZ+H- zKf0$*17a10me!dAnRZ&wtA?SEI(?KJ%Reo3Tzg`Fb z=_K|K6#h$1vYn=2&FsNM9gZ?`V&}Q_{W|NMgG3ADbFBuSvtZM%Eub>FE}siUdQd`S zrdRrS$Vm%%)Lg=y>J@J{&mO0X2n~)_tWQQuvIQd+GC%Pk@!YXQtO(PQDhf4k5^t|V zSSoXzp9V8kje!`1RHhX9dE;F8LxA4ZFsICT?XYcaql?j}C+{u5sbW>10nM8gEaKhB zLavAQutoz%@tj09c_c_cTT$Ww)I|>-Z>j=b*I=9j5v!$ zIlvqqo;fCTyFiuMf&D(_&!^)Ul&syDBkE{wt&0|53?y~t76dq6zP9h|_ggppN+}Nr zmiNP7m6RtMIoG|ma2c8Boz^d6Xe8I~@=`b}gK`;rq9cqIf)f?Qe%3B?)d*)QY&dW6 z7YFsOzsUX6GJP!*VRsR7-d$oF{nFdd`mT+D$v0~@xaatGTf;7;DDgzd1CBOA*@;c0 zdkh>ZxTE_n@2yOabJCd&MGpC27o)Bf%b+pwr{M?(Et3&b;0w*!fj7#X*=H8F;a-9wTAE>zf+sdPIMGCiK|2ZW$C=TIZyM4bid)b?3Wyt2&s%OcQPBY8C$7 zGOmu{<~GW%UFVx%QxRd;h6bairVB2NTN*iN7>ESXbfoq8a4zOUMAYnNoK2%qUab4?eg2i1)Pl&+&0R`fvMx`}nC}$JHgn(CFf| z;8=^*At7r82#K*)5&7k%j(e62D?26XHM;@bA)ts{;7!?8_?AraNF zqfXmLdTUXynyKxv!BM#Kj?U!b-9~+`mbYJiowVEjeb_k zG2hA4CD(CpwoypP0OOTJuUJ7X%$9OtJjkHWdJzCr0>r)a{aNnkzh2)z_}cahV?rYN zLKRt7`J%(a*;X{+3D33^Rhw*F?1FFrrv0%a>-HUQ7Gm>|fN3yXdTpKiHLb-53;lB=10bB-%Q>UA<-d5=vP%ntsjdGDbSXsZb01x zq^3Wh(nDOV>+Rf$KqdPPo39^o!GqV_BvUoU_b*m|+oKyc(aYM5`;I(|KssOtx48gx z?wbnHv93{%5zy017Ap7GOaiMh>2?UK{DmClD}#&~P`vriUP395r1qr*lWKD> z`yLzzzK_7<;O?Z&wU5@$uc>`shhmR3d9TH9X3EbDM#_)dGAq4k_tDg(u$<@L1o5j2 zm7p=?*WeGFx+_;8YKJIggaLfu{BcqeJ#E#-O1-p!k9l#HF$<0xxeT=Lu2an%R!x7o zhd@Lxhm=rA_L9o9+793M6PcMXbv!R2<*)4s^!y}r>9&%>reVf!L(l3MS8SX{qJiMu z@xPHkKKx&Oc*0jvLIO(N63$YprLiGA9riS6u|n*v(Nd4Ov*riNl(E6Ty&Uk zSSc)gZ%|?TYU|jvc1;;qhzg@+o}havGCR;=d-))cDLqf4W`bwQ!!(|x)1&haD7X#y z_CTsG<{&vZfq$6C4!q7v0|E)Io?K?AzVi^xBHiGNz%x}zjBch@F{b3PL*iuSih0_2vRa2rXW~dbQHnR`9v$Nj zKo%#s$;Eny&TLNjc}5`MaNpkYIPb^z=OH$UKdDzI_}J&GlPD>IizY(Kb&);Lk{i zA$)&P^*Ri+>YCn+`3`Fo*VLz9VCl${$`hT@R(gW@TLHGX z#Gm)elSV#1yhSF{3N^8fd=psl5g9= zd%6x&cb7#z#e7L?u^bK0vKw+kpuLaCgGbCkfr&{lvK1jg4RfrIAKfBCKX1J9do}HO z&oStG4T0fRaD3fG#CWC66b!!GLq*hN#nAsxy ztgQ%;e{p->>q-`6np@NDo0ZX$Xqb?o?!N8bd+=oP7xLRIr0^o*sr{tt0+TpQ0jlQA zr}zkLZ2X^>MUIOh-3woeNzXqz-Rk#PKrnMI5qmRaq>k1c#A!UE>XO{LUF7sgf9j-t zbxl?hJ;L}qT1gorr(di7+#(vzYlabdrFPqisBaKjaOHa>0#(N4c5!}2^zRLL2uA{y zJ8?l5PjyxfMl8MDf3~ka*Bv>DE|S>4Hy1_+-C)~tN0>b)3}4F>1;H^ zK{I_xv^DUqciHzzuIGx|Q?v#S;+m0sX4~!0gqEKq^f4+i;&@`#qEf9iJY$Rd%->4} za@5|;`D*X`&b9FkyySw9uv&OK`a@s4XWV;U`}%~e6G38a_a=S4s~PdFi#_PBS=Tq} zRpQ>P(vYZqBHEf24{+7{bY;piLV8W=-`!EFB9&TQl9SuXco-&qxA&>hK$A9^Ld=@* z+3Z&<8IM>Uto{^KJ2WxIGOBCUlZ1E|AF8fn`%mjH_5D?Y^8Y-Q|F&@WFMBG7J_CCv zI*%F@ZQ4ZLhZ(Hgpv9)R7^sC28FPY7G|+Ngwt&>GJSTU&?LCc0Ti1ZD$xrjSj^R8qS{8+ws7i>yI7l>F!U% zg01G>(=~^coue%J{hH%@#z!A@xHyk(xHt&qCT`XvM_Au_R@z+CX!da`wNS@}IU#nS z{3Q|I243?)L}n4l%6-p`?VB;@p>EsjHA9LKnrNAg05z?sv+pand$QGmUzN4Y!|)^D zvf{f4Hxx1Gz#Gc!WP@qEi!=B7lLc9{h631-c)E9Dc{Z^3!_gL7juL>;accX6a--)j zl(jZZ$IkJt9&aPqffZHPeDlZopyb}NhoUNqV+-bGswuA*$eDDREy~6qDGaWXbX%yb z2X9d#VP<=Fh)rRW=HwksoIJX#M4?C-`6+RsAV?#{2C)7{zRw8=gb zLKF_Iy^5-07{^hs5PQmXx{i38Ig@nDXOlI_3v4Jf^@sn%W9r|B;y)8W`etw|^GoFP z*IUO|Yu6Kd-_4??Rue*S$##Dzh8-AX%6;=m@Nd;`Oe+iJOO&8b4WzkLq-94kK-L zARbJ;-lW8O&Q=ASQ#%!--$eKd^QdnnQ75CZt(1CC0kBgBk9(XWv*ww-wO`s~p#>{D zIw^n&+6!;DgNzri@4s-W-)?FS5Y~A7KdPtV$ z?`CP}3cV(GnrW=~Fta=^V}YuPMB7$9eVn5t1z+eCyP)r1(J1sZ61}P5D5R@#3{sBua}`bN$XOoHk6|cibQe=QV57ugvTf zcpI$H%ufd|83{Qj(_0+~R96$txTruKO*@A1-Fc*X`@PW0MigB}SEEv@vZ!v^nqC?6 zpPY{eH=QN>B)Y>44?IlX3U>y5S&cW!TW82K=XuzoW`tj5T5i8=E2Xmd*oUBORQXAp z?ur3r*jO?X<^(lm-s*<4cC_M)^+)8xgB;`FcbuSR8e6mCo5MQTe@<7kixjYS`eB55 z2SIvtYfF4N3+;78!vq)wof^Jw3izlHA`Wj#Z|$TX2pc*}O5Zc%YSg$IJH>Zger0BRWB5yeIH%uP5k;-}`FWji{CREJu-^F5zUxVx6)NdPe^V zbTFZ&^Th&Lsts`Wc=Ems^ROinU&Z52FHqQ5BxW&KZJs!!y$*w(Z?%IQ6ct;c%g~lC z`wQ@{5w{S22K{_6wy_qt4zBIS7bt4^{iv>Zs$iu*$3JU0sw|jQL8(fsr)t@nD0##M zxYw5iG+HEoG6KFO52n&;vJF?fh~E*lJ3 z8w)HG5eZskq?-*KJ}Y>vVmlwMc3u87+7NVIgPCIPa>Fki=OE?`8_zDps zUBo{Qlw2)!@0(Wh4Am(2r}Gt4Vo;7r7Wpf>9CxT2j_uQJS$+~<*7&L0WWw$&#Px+o z!X4nq>&6(L11Jsx;1HJTe8>3<;DhQr4XYo+M9Gm`s92xr?gli7sPdL%@2 zWMvJwiKWshaTzR6W0q4nN12w<+TmYUkL-BBoid8g6Le@&-oD7UUTEe+_S(!4e{AhD{_?)Qcb3{%i{rbq1 zwsrC}3KcrR2sh#gP0!YZ1L%N~k22KRM6LjExxZL`9s@HFu{R67Ff|R2FwK2&q(e?A zGagsb9ubL0txBp>hEK8WG}fi7V3Vzs*jYvpjV>g*j$0uzGu4I3bRw9YPCl%0lW=(U z#;0D;*V-R?3g(b7^+|B~~??byuBVV1M* z#{!y(4zz1mypNqA@X2t_k?MkrdMV;xCNNlkzdXg%{SZ9?I2Kq1LCM*^tHUs#MF^@MIHbo=gdX`+4R=GUiHqyiYKT~b%Dz#A_csvK73`AZ&4?ERR?MS>9=e)PN+qcVPO&v9qo$~cjNA_6KM=u!7~4v1NxPDLl{dy znh3&nS?-~>nK!8jntfeMFHlUDCo7>$@p4bqE`zJgRKFe1Zw$G^d|z=zo<9AFXDX2J zI@K2co@U2+OTe**C_6VlXG!wk8mr!R{$Pz$Xw%3@B@nD^9iu7MNv~4`0-@070=U~Z z2lCcGrHxU0WYUVo)+wzl3uw%!!EgwsDptfATeB|+jP-v;rJ{TbD{h=`U=KJ7sf?J-Jb`-M#eX?cdepOp3xV!aI*a!%8t)7zXR+a{f~@$F`y;$4q0FEB7eSVn~1WSh?naV5<0Qthryt4q5SKWN5Mx+dB1VN0~$l7tkl zyws{zdF&bB@|BXQ2QJy{W@AH@!;SNp`{SOwY?uIY{L!;# zlUw3z<;OhubF7rg?s&=g-L2$%5V_zp$tG+aE2L|MtiKBT}^K-!dzfasNG1)Tv&!?YVaHW7SXRET6qSYjCfw zESvt;A7V+M>7ln!O#s9B!VAmVVED@70$2xAEIk_8Ba!jP6Psi3`0I zdtyDdq?dfVb7Hn5dkV)LGb zqkMVz7r9@cE}Bw7oxt|p$?tob-9m|wZkx?i@V-#9AG^>YA7W>C#TyUzAkTz!H#Hf~ z#QsHYG~$u^;A2O|=Mz&1qygpRmMG@YBUUOE`AYKJHw#}R|8~ybEVijM0db=NpF6iK z9rd-yyUL-hFYN#tI9CC@N3^SK+hEgo7`8I}U|*vlg2t>VDZUrvGxj7SW5EOpKI0ok z7owt#T15b6M9{S>bt9%0R(oq)VIIjucZC+~^02Lv`IcJvb(mcuv!+0^L{xVHAA|(O zCWhmqigdeq>gs2wE4DVOv1E=WIZ8FBbyf&7Mkupfd~?hgk_08WgrC=046)jsYGrrv z5`Y%1hRn*&RehPxp%kVR2Dt;Cn#^ zD;oU~SraO^aqdc;bW?5ZZdxPZ19%g2K{P#g)w`b6o$a$Bfkag&eB>7Yop<9OX5jx> z1i?TGW{X-H8d0d#NF+uls6ecfY@7ASMe~n+ioO^SH%<(Z6*|jlHIr)DMn_%A6kCpJ z_f}1b@N3$QW*KEQ#ynyHf5Fkl^xp;$FA5IYB1e&6GpN$ZZI~yDt^Y$xg&2lIuPlL7+~eTu`nI8ex<-d8DKO%{T^i2x=MY zYkcb)elq@GJUq_ffFXQiojc5XXPgdHN?zP}sZzQMdpbn-15nLY5x@F%I__ z5KS}FeCMIC{OWJ1?~0ccUqA~`@GL<+bP;9SmQu^2OaeJO1|COu0{P4humWP(`)SdY3W9z@_y>e zz$}$1dfClNkru=Udu8U>Q#2q1s$?wnrO}QR+x^rqHsXe~OK28&{MJGWXLxPdK3u>V zD@nDR{aE7`u3~JecF==_OplV#-nzE(T4vg&{`@D0aJK01Nus$}!LM`IWXq15;bJA( z9+hHm#u#J%umBg5vhqtqWn*jp%pSlpO{^dO-CWwmS(Zo~hR-a6bX zjElnfR^uoXkQHof*QqZxwstYa;aWbHK8+NI12#gc}lnongzUOa244Z7(G{NydwIP)ZVg?X)Sf!*tm%&o4>_H-a_n>CZ75_ zvMTkwJ_5pZAqlre)r#-pB<=n67KlVKM13ESljyixZ=riF34M6tMac>==HVq)r%$+M zFmc>bJ`_w_Xho-+9KAC^W3&&g+e;G}@;-nBpGACeJ)Gj?-h>JQzb zkl%}{OpoXnx^a2W=4=oIbFku%*-D}dQZ_Yeq;ue7lFt_0$cl=v2?U&*Y*>Aq1Frlu zZKP(zwwoO3v~W5U@hUTmL0c&IyA9ASycOVKkyGawZc)g8lJeBgr%@;NZrIrY|1N`_ z0W89@iMcLM(9v0sPb&KS8mOF>!-!3LZjmrib{;BlL z-q6A4`sgUL*y%Eix|6tXq;%%&OliYcT-_S7B zkV(BsFe3(MdK_vp<5}#UgWzh=GbynVM$2(xR3!W1Fp5Pq;|vSVLr*jG8ETqAUd0Ct zkgiGWO|W%sB<*AZ^A6y6GcQKHQ92+-^uYV2fL>f=AfOxcUotWQ4>l;bbYyP((H~jWsH$TyI7i@kZT)pF??$`_4Th8nWy^#xHgy8J6W64jQaem;8_S_w15`=}=#Yo;$FfGGP zAj2KzXhA{3Q4h8$CgBc7-;nM+pScMY*nGJE-Zx8VOF0=`1+s4K3v1~d59W?u^yD4Z zW(JitLliESBf&AO$ZSt$3M$WSF8TTs@A3ze3?DQ(>8&t@%p2>jH#y4fDpzt$`QF(^ zffX_)kMiZPF>}*T2-PM!bQN$$rqBgag9aD?&`VYILXRoR4UUP76Fr`)%2wJB=`x1N z-+T{7#D`V^Je97~x{+++>YZo7njpyX6>iYT=!~yZPD)?3Ojj2{Bw<@!t`f+sNatNkG=1XYbx9KWyU(z zL5L_t$IwGjkbpEBgkB6u2qXcNCL}beQXL&Z2!cZiB{U5rq>x~M00BWorAki%1Obtb zL`5lr?PbonckbNxxpVKipL6c#z5CwX|72%nua#Za-g~Xz?^^=0ny05dA>Ek9=`15c zXKLE1UT`mv)r->B?}!A(6u6lU5O)!=H@%>4Tgt+*Lxv}pV=uh@> zGbYjC0sE~{OacM`mF0fm0xOT7S0|06RgxbX1)i|A8KzyLhs}mT_1YICrA{*k=7hHl zs`MAGX)Q8)uv*oZy$LHU2m}&uaYV40qpwkre#fk)*$Hat&9c@M)t|q=5HAna=`x0$ z@>IN^DtthQSCVGbvUZu0Y|UuC;S6QH*r)KpFYoQB4}E7w*6s!I6OyKa>k)0zw|x}M zS9);lTuldOdO7V%`-rmMzA$eW#c{FAkh2QxaN8MYHg z$pN{%mNuOMLlO78KvI?(zF;%SGTZ&xY;#tR*1^u#O8WV4B6l$gj#k&yuHX-iD$*!k$g6GO z&OA$>2cJxO5^l_PERlTx5k_wF`5Gj)eil08h@i=x&L&j`N&T>P@NQkKM+|2)vnWUg z1cFmB=c{`J!Kc&3qZF8Lzf(s-U^6)FT|?SxATqi0zrD)9MKM z@yoK4F`S23+1!7ol0tv=76uRK_QATV1Jm;l=pWxlnObDxJ< zYspNS1IS&sRax?;<@T`<4I%CN;_IO@u?#DKKe zjP@5FAXsh0ezW0DtA|QU3=_!;W?72vERcRl5>|uQebBgz1+FtPTyPc-x1C#8tR+RE zh7CmV2p1cp)BZv}{(5@Jv4W_r>U)l-6pm`F`!y6YhN*@3^Hz(l|Cdm*e-C+O z`AU70c<~n#)wlm-P^aJZS&V1?%|w+>JoH-q=+-g3*pK#;VLquIMPvt!f{nCMGrnbx zR|dV<7rK>0BT4GCTded43nHD6&yMc~xF5~yS&}C0ji;IAdtyA+@R5~?jE1Qr-oK^i z%``9Po`&V7G903F=6F#O`*!%R z1(6i;S@-Ic#0h&&54}7`Me*uoF02QEXq7zKyP*)#Jlv@vuRl+athg0HlNwCNO%e1Z z_Ud;nebKtXx3}GWEUy2g8C@*NGY}^7_$JZ8_?+&t7K2YJe#sVG;AtVi&U0!*GqJA4 z{%kPPcH<>`DuSzMB%shDLe5?j|O@zN_NhKa#Ro>8*+0C zXi}NS%S4a<^<3lcpfUdIhW^HHJ4-1y)X79OFYiH+<{cbV{D-#UK2`0sRw;D(L#6NJE+$qtM?RCaSAj5=cgnw-ffR$m0BJV)e4psl$)C`{YH}A zqk=b_jDzV^nXfV2+bGcX2Lynrns+PbHtrA)hx*O#4|w#fcn(ihQxh$pHrnN)?iDMr z)IZdpJa-&Nkm4X|e?_W**U`%KH*788h#7{h?S;xatev=ug*=WIHzd#zyV0FU6E_up z`ez9V@Q@sUGzZ%Y=^Q$TG&xg{853zW8w-G`1wbXmgz=p}f9H9WBK27 z(*No<{c-M-`PbT@ZxZMCuen6i7CeNitJP7PKVGYP(rst!P$9A7?S;Rdg8jx!Aw9YF zOIo0}eYKf}pK>BXx7t)U{dGr3iJusjgD8L^VIf7eug&?+n=$FKs3g;q%+tE1u?Q)X zXZv`FApwKRl(NTK3rg^~K|@GOqh(OxrkI}8`lPlp(+Zn2su)lWf5#RCOM#Xq0j{HD zgia<8jg}KhN4z+h0m9WQ1;xr}+k1Sx&fAhwIvPC}q4~C^L!p7hG0ie@&NxrL-Ati3ptEtca>JC;!Y&xGA z4bni+i;FKtynIEgoy5(H76R%TNHa*ickez4N?o}atC?%Q#yUezKN?SNW0pOcR{%Wb z4_weKc17`ct%`*+QJz5Hmj?~k$G6-nAFXqS1o>>^d=T*A_BQjG&ZRCNhbujd*|43d zB`&q1=ltWPne=`wu>7hvDV#;1SLBPe)3P{RJMVoj<+bW2cNN!8ZaF}ZgTNFgZ+H_} zVTCU~`$m#Lq?b7(wH$D2P^PWbo?LnA#CXe z@$=y?Yb?3wabA|7< z_O;8$&jl&g4vpQlb@MUTXYOMZgR%PhhRGspLg%CmQT4QWU3V3}AD&{m?R>w=TQ_G4 zhsK0T?L~BD=;pVC0u&iLjzMd?y>mH9gJ@H5sT16Rk2UD_Gz}-QklmOsNsE~qj;=8g z4f(7Lv{h%F%?m%AJ3Y-2yAb;6#|y%rcmAgyA=5#P*leUNS^6M;@u3Ph6JdL>NYg7X zz0)UpQig5#{#aw&PowjcSpS8Y3jCqNFsM>6&gsw#VnzrL#@$&3o9a9 z?05$o>~9g`@f0sJ#c=TqVTC@GBbA?caCETiGK0(SgAepS-t8MeECAZ1W56^rav!3_ z-#xAmo?b8SV=)dnXC0dP7FThPT3R+0wcnXL@T?HTfnLeR0Lx3&0yPUP>UKX}>tw{+ zZ-f05Xcw4|YHTNTj#(#8Q)%5YB|2saf5KM>K&f=Pm==dPyCt!1Xq*K{+PscG-`Na( zaqdWV->)2)wgY$Wd9ebyeiT^Shhg#ob;O!{QM!VbISwNp$xIqvtU*`rP~Gqy30UFq zA{%G|pI8rB?(H#+XpmtK-0qtM&FxdXqh`26?I`wj5yFDQ48ZaRo+2Qf^tJb&Al5;B z@_`S7{XrgE33>>4I9P8$JYv$$_0Z;cLJOiNyEiIH4<%idF<(yuKr1Tfzl z0B*Xe7Y*&>f0Ky1aF6%PjgMy@!ZDwdI~Qp)bSlS)EPCQ(01`tknu{>YgWG%7Ij@5n zhtM=8m@o)+vPA2)FwL#Tt5nIc*Bag3OU~ZoZ^?KN_{_99Y1hYj5LRozNuR^l3Ud*( zPIq3Cr?N0%En!Q_kwayEPvW5~iNFoc7ryEyUCc zUxSY8g~Gd<(|)x)pcuzb%SRwEK-^Hb)zoN%KXbXm(F_WXT`aN?su>bh%cS_%;&|ImYdlgCKniN|gM}7lv@+M~L(8}831SrxYup;yEt|!QCo@U71 ze3rAp?qa`s4@*61boKUhzMH$y#t60P?{+1#XyCNDFVM35BO-oRxv70vlUU28m^kI3 z5OX>QMQryGny&|HvNAfJSX(i#!-@~fH|J7L zk6%V`jtdXIP<*e5su`_c^`SOLZcyKT2Jh11ZOJXbWoSo8ujDic8?dgK#U#2CBp?{$ zqa&>E1v@;k^x&+nAjGOzwDQV;b*`V!xM5mfa$BRV;Wvp~HO4$d8r}b0cL%4Y|FILR zyde3;%dW_&oN}v~AcCWFE^f^`X;i!4z;Ajgzrg7Vo`~+nEim~j+V@(>@s zvJmToSpDhV{7)aKk>gRYKBOt*3Cz`piy>iEm{TD<&u};Edb@B@{K{ci{z2;x02u!mG~_!B{Z6N$dkq zdD?nJXbXiU(3&4>J>2a}@PCm$kl#0ri1{iBY3K_>D|*0`BWB0koT|GlooKR6l1JEd zAHbE-K~NMwa|lO=|q!OM^;zBIV&@wjS|pqhDpKwqzi zJWH#rbBI4Ab}LfY_w>zd(l)NmYdx?$Jlwcu`tyvWMJoWfSfO(3Z4tX42cMjbK zvHO(*9YM4-8p3DAh=p0t5jC%x{ys7sFD6ZakCJs~cr$}bA5t67ct;RP(-n;H8h*ra zdVsd4%0h1_cQ}d+fWHvasA?Y%VD1tPv9q3C?u%`P$;l1wd2t?)?r91#FeQnD^8lcp zDXsP+*$M|tZ-^QW0TQb&4{4!R{0V=dNdEn%3<8wL$k&QOd zi*!cff-wAN@vMGC;Sz;L2}IM?Cr*-6Se{@n9-aPTGn&TzOZWUcVCue={vz90Hexi> zexN6rxLkOt{b8R_0vy2ugtc~FrRQ}*C~>+vu7of`y(Mr{=lN`Y_0Znp>-@<41C+X( zl)GMtd~=JgIn#I)rg=x`ZZAUR2-5i+M-!EetGuEVJ-G4Ue8$|+NS0;%N2B67C|YdX zTJC~%lUlXk#|1ho!fXMtvJGKtwE0Jv?7mU0z15I7Vju!4uv^SEZ}qv-h(l0|32tYe z^WzQ<(>2%By(_Kd5HJ=uarpe|G;y&p7vbXy}(>ODvs* zAdMSl2O6w6xS$zkSYA^sZ0RKHqp>H_2QHNHajFHFad$Du3{x0ghN}O>zrxmmpyrE$^H4uxw{`t!`YZoH{>2$UOjSSTj$^}^#O?Z+1;sck!oZn^&V=c+a8pw6rg<$R8B^qCbp1mjlMeTe`uP{ zHaYV4=;!1wuAlbSAC&kvJx{+|7Flvo(-15LATw~QZ^!ZbE7ii!v_N8x%cXSYdsySq zu?mM45;z{SAqVO@&kIy;e%;jK^h_y6XPM(w@?QT@t;yH;pG&?;*s+}-t)zX>D5a)i zu4asM8dN*QbQKt|H7@%|IWwz5*48h)_dS^lT3n4>Q@AUm?f@Q@loyDvn!QE6n!jpU z)(?ppRUBD*k@5D9fVf(lVKUKXN1Nk;X9pfzWwb9%6%-BqFmL>mwlg6J%D1(p+=x7P zx|UT}jq_RAEKuYGtRf((>?If zzrQC8MVtRn{MAjbNKBR3D%>3>y81kWIDR$*5X5kJ;LsUS(8g$UpvqZmkjx zyxf;uct7%@%Vgjufa{JdHP!m3pA@RXi;E9FO98-)P;=q-wj!fIB5@Lm^&Yx904i_l zYP6WL4Q+tNUwvHjeVbB&L0DOX^Xb8eK=8IeM}QvgAWe2g4CzIdve$g9Kx_I@maI%J zn{OEvAy75V55f$AU1&=&AB@jxmhT!>;!phWmaBR4Supu}Wx{fvH{UAV%YD2)trOqe zb1MvJ=oP91Wx+8ZN=SL1EkM-Idv?xIA-uboR$-pwY+z$LJWDP91rjmh_j;{oFu|2i zGj9XG^U7unerecU@l1uN~O=lQ2(-7Dj4%?WuA`M~*}1oJp`&SR+icCqz}Y zu@T;gby-{QnsDB-8CibD*-)(0O0Uiz|9-@NtFyZETXabAJ%*Os*6=$x^i4|xC&t6~ zhM>w{tY=8+gt}c(&mKX{XNvdYj@*Rvk3_44^$y2elu?x+GNq^Ksn#8}q6SpTm)$?*7zW-U6?8^5GJQ0EwM9ZH%K0b048k(K z`tmzpE}s3f$JXCh{+}e7;eRKYVT!Z9kRGB9g75A4q-nx?TJu^$B4PF4-I>2nN%P-r z`oGzazvmwQV>j8UJ0gG>Zu>7W%t}G`=Ju%FW>zp*CX}h2tOP`e3 z+@Bun@+`J6%~J)@S#R!8-d^V2Uu#u7)H>aT;dg>!viJNf@g2U2y2i%v!Wt!!#9x~m zGVVB^rf+YO*~QP0ys%rmkLl{9d~YkVm%zX;2Hf7@c6%~L8(?AwVyG`1RH6}2)v@}z zacK^%A;ryS9rhwr`dwd3zR>E-anlksC_!d;)!PP=qSal+oYZ(Y`Xjnf^R_tGyafOL zrJC*4yGgTMaU9J+uVo4tGFLby9$~3(a~>F803F?icr~%-ImXBkbZ7`1??>N04A81G zk}Go%%gH$+<&d^TTa#BH^Yp~~e|8@K6Xmy3%g39wKBHT`U|+z>)6+lm23(tqBrnUQ%pa;;IJ0jb)(6tfb1xboB>?PTHQFA~oWozp}7Sc{52 ztm$aSSqe3z7n99Sh&|=kIH>e6D5f3@TC1e)zk9<*=u%vfN!e~{j9QQc{Ll|=$K&y- z-2e%Riyfp)YsGYDBdcX5GZa*3n(uh_@Qqy!4KeyUt1#VzhV4(ohIX>tpQ^(|dy6T^ zwr!m`)`##coVOjt_^j~zYy<2MMK&3H3kDp2pTsiA6Sdl5E2X7#cT(wCR|PmV-Ai8l zM+m8s%UCs;$BYW0hSpxQyA_QW&KW1@h4u)zrc}CpP~#{oAY*ND6Gee^yxzl%F&Isp z-uCk1h*ub9w*46{76CAfb*%XPM4a04lUSZMR`?tjYM^nmb4sI6M-(ghJk*fUEv9te zjo*5OXQot%H}nd>{qZ?21JTS|IeHknWiC#WXkhoWtJD(6ZRUM{;gS-&wLa4Ve5J$? zy64gNrzm2z!{Oiho&RtB4oO;ZIxF#5InCpqlIO!}2iURqZL+T{+07+}A=@u9J>2Zi zeQ)o2(lZTn8{%P02uaQpIkkw{ppsC8as1i%ZL*`By?Zh(kr#$1A+D5rHvWbm9T;%ms>3tXr_w8To?qoK1Mz$Hx?*6)NNR+*ejq^A68wHgyQA ziLS>?DRAxN_x9qn;f-hZLaJEindw3y&7bC1Dkk>P3)&2!vV8NU;FEoo0t<|~d(9{n zH&C?lu2&JTAm^UvZbd7$XwpB>rUlRguiU1`!58e-8`YbzDkjqo6Lt65C(@MN`D4I% z$N|F?H@B8%VS`kbkK>u#a)*4G;ba6$*i{7nn!mD|g&3XY>R<1+vC!Q*%Y+LUOm}<{ ztt+$W^-|89-~!@w4w!lmTUE4JI0dl-sSaO=Y;$qDf|{vod-}FM%@w1R-&5o86yeTa zNxe>LL`{}`yfCsT=+Gko>==RtC4@Ov-vmq>PR{o@pvAe53R+*3(>u5rq+jcxU3>2k zam1kwSb4fKQf^61R>!KT(=})~l@zhJx@DQ8y8zo-DSzy}T)d{iF8ur}EP@BVo#|*x ze6ir@=@Vz|qa_xK4w12^)(-}(4I^s(Mop_`p1|mA6M@DtIK@(eO}hP1Y)uG49;u%GB;(7K$}>`y`UsT1nkQ0s(KNI8(m4w3g90;=dhuYlA&xSJ3pOzHItER12$D# zqJuy&uo6YkCxDH$;!>0p;@TyDN6D{d8sN_X?CZ_$y;*oroAHGUQUY88r^R~mmr z+jAvog52Fplj-phH1DM{~-3$_|nFtMWZ&9SBax)ah-$NhyZSz7y^!+Jid zQi=!ocy&p|up_^gKg%$g?g@$5GrVXNa~X0Dfv{Nsj@hgk%2!1T32%6K{1SuvO(NeG zAgAh2!wWm}6o6v&hF9Hxg)gZAeZAV~JUYz)?%S)!2ht-N~9TpP+oBfJ0B)kw0KXKuu~jU8?4yqNO{yx+n}pwmLhvqO`Sx@zLVA*cjp)9+=qR<&~m+H zFf=f6eDrcd^At>j_3I!+(I2OdJ|gPPI8=!nZt#%R$QtUZ%b%~P>8rA7t0kK7IfsqB zvV>^^$+SUvy#AN0stcAdd1*-vkm8HZOrOotxmhutU_W?fSbN9enpm@IBUIK(CPpk1 zT08JM&E)O%+b+dJr2u-God(hwHrNsCfbVdPB#)|BtlP{ELA18uyb@;E6+z>EVK7Hr zG>H=WpwYIbK*5ttsvzU97Z6*O!WI#sO|;<`l_siY!H;0k)*9>9&&rI`=E^N1yq@NM z-!6gw<5l?Q^+W9<*x z{91ZI_36`4MNA=I#^gh0JfTsvcbp^}TL3CK7h0Oplj#CX`6-liNpWBX35Ir!_GX67 zM(MbS`t=hl$e#Ks>FHZnbXew=s-rhGM%LVWeHjt<4Jp4qXF8Ke^aU&nA)7i}Tqd(k z&2beU{-*pxxi5voW@peZ0SxIn3H+1x1kXU*N?Z6B9PDWjyK5!#$(_$( zYXzfe+rXWK;1>rk0T9X~z z(NQA}`kUJ*!9RB@hEq;Ib=<1ea>|V)Y$R7rY1VKC1u&=p*5A_#W#3;6E5-G{2Q3cG z&FHA167RlYXuDIUjG`0;!FAF=h6q86;iJmdF1%WZ+`@a_hX@@+ z*bp(sdJQxMjvxQcGbACFEjEv;APs0%l?QA{681sUTPdd^AX379FC9IhSw81^OKJ*Mn z44ihY$`Klg$|sM#p#9wQ0OTiswN26F{=&O+4&5pPVjHIQmqOhuOhW%!#cGejtrhiLJ>L9X;Ud=C2ns!zA9ec8Qh z#v$PAjN;5>76NT9x>L=2m#cml8p24)0G(@&;R12tY3~aG{-J9p*0eMzmMD zp{iGSd-9qNLNY98Q#TJI_j)gh-eu-j`xq%V@2_68r(w9ULq2a{8uPxbsMuDR_*#pU z)Mc|68~%fIa7_DGEPegBCcP4j0bV`O8%!>4F0OUS;9t;LZc`;Q6TqsYd@eRgFA zbQ1x8jrob-T*@{ zFi+}AC;1U)yK+hiy`dk>=We!?dlqT;2y^<)G>_+B&P1hLj>G45HZ6*i&Kc0tExU_5 zM}ndb4mtO=Y1sgQ+|cCDVfNO;OEDc@y>I#8RgRNBvF^rLpwf@n8!FN5OwSnvtR%R+ zW_&1cuOM>@={2FVZV0*;sFW%jmbf5}7PG@A!$d|g-32uQ82=p1Qf`Wt4^oD#wC6o( zvh6V0m9aF=WZ~BlO2;St1;k8!3+gQR3Jo)Z;5}%9)Jr-ok(@+ZbNv>_s~W7y7KSD~ zBu8rFB7V7QaN;64zLiTu$ZMy(xlTkaab?ueJ&$^@jzfn84G*;0YDJ)F3k7<5eNN95FU>Lq^z}Mz1myMg>fK>(wS+;( zu%sA#AmT^C;1f9vZ+fUYa^^z@O#aIuqsH3j=RzNlJNR|G{0Fv{KLeYQAXr1UUPtgO zMSm=({9Z$cN9zl$Pn^6F+1{+oJO~jD`rt4y-u^1GDu57`i~(YRRB(!}AbC8sbyMZ$yE zT|QhpF47~I`JKu67~o|Ex~=m|iL?Ja+WLol{`Z)bKl+Cp z>;El{{7x2M`zNyapV|tY`r7ZoEU4@IHYz%Vkw-9&pUv$*Of@u~1BAE6kNA`D^ z?Vk9PCjWaD%g^#Y=ZJH;>%{n+4aheM_}lpig7h*Uq6ssA$~$4;Y= zi}~)7vBY06y8q|m+j@=mHET`88;06hTn2D5qxD(rSnuJM7%A~IeN4W$1+3f49@0xv zm|kqa{kQFmk8&bUhiX1a%X(VramDLb?dO+gIY4hw03ibpF;S_fCR2^mfz zy~u+O3)=f}1XL@CgXckB10+N8fFoE27SdF>EP@8mN&)vHqK#k@}S;t`&ckzAN$1`pB9 zOuO!6Z_H_7jAgGVN-oldY1u1y3)a}XG)MTb02g5{H=M5&opGm!c?dGVNPK$Rqh_qF zhMMi%Da0GPLXb9u19r~@zk)alw}4@-fas~3_pw@4*6O5;swa8Cv>r=i2kJ<@XZtgZ zVnLqU2;b;lKYb7l^9Dh)o|r<&iaFTGnQrsM`mjc*q!Epltkq}sF7X_qW*OyII??*& zaK6|Ol3((U9s`gXTsV!19H$dj({DUqSXfxBw-@tt5WO>B0dwh>&#Nj85oyxo<5oQ^OfCqiAf{fPwP6$}duI&Mh$>cYMBLyvQed&(N52E{O4A)h<~o{3qD*&9l3 z>Tu^p62c$7$$jR9K26yl4lfKk&qSUZH^mMUkzmq_FnKk)t&b;`RnBbi7 zOJVH*zqT#D^nSlm`b$!Gp!|V>4v1@))FvS*6oXAL2lb5i<8T zhd3Jp(~-s@+{dq?=pbxCWPgHX9}kA|xa?By9nzrETz-`p&VlN#1Tk%jV5(Pqs~`J* zG$Z6RILIcEcH8={u)#v;^awLL~j{YACQtuTmNAK7o=K zJQ}b>_|y-+@bdmpH3RB-I2o5rZOAbFVWj=WMs+qAed%IZ=JhJn)Qx*?2RP1}ESVmM zXR{0;>SmON*rPeLiOOF>Tv#|XaeJ+t<9LuLW9eTziW61DYlg!0598Els9q90#q@jjK(Qu5A}Q zeb3kBh`a{UmXS{s8eF2%F$vLcLQ9KN8M}pCD5MGaq(<7H`Di$k(2AQ|!v% zuiEF&bmFe%=JeCubyQw}{M?M{td9rxhf0oV)yBrN+-5Y{gB_Z(1B9bA3?JvZY&dy> zw{UYdz=?mB@Br)7_Do~F@kO+-M)9O*Ai3m{zH{W}EWhs@Ov%Zqg4wee2Ntbr1EO;{ zs;B+C#Lu_M6R#?L^nHnxi9Ho)n+91T-XEJWpg}MQNTVm;X#p-GdiC1MM$D|Q+U`x? zUd0>?gb*{6Y7r3r*5^1UYx(9|n#XfXT!e`;wy{T+i5U%}v{)$<-oQ{Gf}ZN9wp7(K zhXEDctMCG}KXYbep~_WeWb1o__G<~}r{Axl`bH1idRT{$y>|&+eTv?3`c-Oei*(v< zlQ_DmiM?Rc-2pVrr=Cvht)PaW#tuyDn(BU#&qRmGwu8lAYNZ9%p!%+ek&ET_PH;<$ zb;#|@Mp-%gl8C-M804g6`^cd?=AoCMfRo*(yTat`_tZ6hY3^vemR0&J5XUze;xJ|* zB@X3b@Ol1X_#0{>%eletFUc>+Wt!m!2e)QY6ByNtgl&K2H=uATW1i^UdF@uo-4B6f z6{uazs`5-(nsWW%(?!$g4l%Y#T!180rz6&WyrtF#&79=>%@aS;BpIqo2&hrpL@t?b?FyDBs;XT*P&U#OE}VQ4AYrXLpP!$(!CoY2}s zyYNI?6FoxsmB9-5k3py<4MK#$V8?K~Fm1G9Z+RUO{RaH2kz4nc2>TomNJXSHU0edb zKx|7OHR+O9f)a>PoJ@_;@?Epwh0(=O$!25IhDfx5PU|O7|a%(-s7LSZ}4ij?5cqy=KT+f0Qz)nND(9J zDKk7myIQQ1dzfBv!h`U>A2x7Wzr&$sq1+rqr$5bT7pWJnP37$3>s8x3)&x{TdoBHa z-emCg2G6c9P$mnqT{56Tum-@jYp=-8XLl!B7?g`b2j~s(3=sr?zH9!bhVtj{+w0+_iPiKd!bj5HAE zUA3*lECfmhAk7FT9%LW|DcSAhQ4Jsa0g%2fvx0aT_QdI<{I6XaVYIYso|Qz}>z0=@ z{;y4FwY5fK9QkzqofvqZ^J9u}n1O?(Aw4-cy+^LwEjx57LreJ${!8tzZB8Q#R3?8{ zCn)H!Xrj0KAX588njOoi61;LSHDc%srEZwwjd(*i^07$Q&$F=`%aPgO?(HgW<;u>1 z0kM*Bo~1&G--xostWjCwfD@zmnH|Uw%sDyV1$#8fs#%*AAw-eHF*MRulg(tmd%A9@ zI8aCmO8}N~#PI-Lhgwv%hrfR@lVzk~(BFWRY=tKOLq zX%=(>ka9Ts#b5j;^M#Y+AP7lM9Ip>8;y%7#KVYiQ(T^mg+xm=Ky*Z7&RFRfk(7IWj zG`Snnh;^gX!N-emDFavRGp+ald`b>bgDZ}iCJ1fg)2&7@3zEHQ11d=xTp;QA<~<)l zLAX%x5z^ZL$+;#8H14WRb8qQ>9f}&0^7iErID0YMk+r1DiC6?7nGic zWOvc!OLKp?Ih_YzIfnC&{85x>LQihb*x;U?n6*kxQ(GNcn_pTjxj1TbSiPK{sZJuN zgQ6oqK3z?bVv!*M^~yoIMO*ZZ%;MJ937d1K&oVW&QarYxfhy?b45_Q*JVz+^u6b3fy`dPxfbkJgi1mqALh=8u`Whd}o%-AtNdL z=;o;fO=)RC{Zu(Mb8gpQfN(NKdWBU?^M41LAFEt`0n82aqmJZsbbvlSAoaQN51*khefi8<3rcDnl0U0H|GWKkytR;~!T>PfkA>Y|qyUcVI8D)vz}-TL!yHTQ0F zXbqULH?QWo`gs$P8sqEccvdJ20UsMY2hh@kuPoP|0($D)#)_v!c0ko(D+$54rgP)K z(>m;~7*48j(`&GQ&&fMm-znBE%@Bt(mA6%8Zq3SCF)xl%c3eR$>p$AX*WUzD3jp>C zCjA_}Ql%7@xdY}tZKT6w_g*hla+=t$_e8Qd=C=Rt9woe?e4DkL{H(Uve35~qhfA4B zFIjZUDPcj{M|kkgNcFtm_W3_vzTfG$FiE3Q@hVj@i4ekk;W#zV?n`pA!@pY<;IFk( zlf|>Q-|Z!K(V20kPxdU*MoL`${!(kZlzGR!I47}DD}D$FDpy{;m-qcHS(}($r~K7k zdF%DD56cJGDWZy!Z@v_7uNTp%SiQ=3f(u;dXQ>&X8%Je9+<4!2-)-BgV7g5yyyimH zuB(H4(o$|>Bj2CrjCTb-Xp_^fxqzqwcR4vP4m6iul;EKwu^qVBiD1qEjJt44L(#lb zhEmtxyX9hWX0a8D=w$Gek5#n;U96I5JvEhb+dBYvNGv1D6Vsy2rZX(Hi#9SbrI(Xxoy}D# ziEU^?L*~qGAS+&zRdgAZ8gi z%{y1Leik3S#IZ8+jdTURraj)&At9$+lZ)tlh^5m`NbIVp8>o7@Ionc*9%*z%Adrqh zQl=?OTH-o70RgzeSvE6_%Y~j`h)zSKuPA1!4L=PdyctN&4_%{gK7Vv5sB|JYDqwtJ zu<{3n!pS~;wNVXnK!Ej;Ahd4pK)wgY%==4z}$1!On;s4!$-H&{FnY}r}-Vuj@k?6-h zBn2lpz5U`69k&;a}pY~XRl;fDo$B1mZrS%oD64$B_8kIv_Hd{n8&T%sT%nvF#-AF7`T7q z*bMWd()&F}R}m8lrR^uK@saVH1!0!44>$B{iACxv1m%p32DqIL{!LE;!Jxdjw(R|s zadoi~>Pqx^kDnfYlR&twFP>8{8m(M8@dSF|V>uoX>yuE{6@Cf!F?&K5|4rgcxBn*b z&Nm5x-fCFSWqHc0@)`Bw;!{r@bS93T3IJ6#Qb=^glEgMU&^L*5PYfQaF7GP2@VdX<^N%Y3|D^ZrXamdhErFop_pCl%z2-Y`VJpVuOts(SFoeH49Q^qYjG%ystm zPl1lzXSx(}j{G?E&xNMS&6ydCo7k&UF%$r*@EvMv5oQ+TYc<$vkQ^6_TAOUg7fIi_ zb!#n9dAl6s$&*j|=|_%^)MS5m^NKDQ;ROZ6hzE!N#f$hQ2%B7ug?|)FoBT&76jY~0%a)f1y-qxIp_len+T$M=7|^yHgF9tNOR z_s2~xg}_M@XCA~^yBFc@f7>ABo5ZZlw!g8O_}>n^6IYNwvyu4NDK+lvJ8@gK|7J5@ zuLj?LY2G|Q6c^k=Qn9Oh zZ;sBbCvALrrQm*#!PEbrYX5Ue{?dc~rXwVl1|Dj7Baf!36~@s(8e!5L0x|Mha zw}M8tBvr67?a7VjOk3Y>4LPmnGqSQZlw~qzN_R>PqMlTi=VrVaEqWVIbNi8ym4KfKt!(pS(rRJ~V>cD;1sFVQpms%)VOF z(b(m$BW0-&;f{Mj#Qck8qbq{A^0ChfXHPNj3XK-DBHofnnWuir zzi&j#qEMQS@FL~X1AAbGpY|5$rww=<4NA(W9J3mpM#90FVdiduX$dD`rrg)4LY<#ms-9*fkZGTfn8lVy*K(Fb7Sw+f_}ck{dGNEDwO^ru%s>E3TNU0e3RZv8vH$ zS0utlm23I~cN{*FtVk~l)pve;VPpyp@6>Ql&VOeH4fg59Dsu&v(Tut$+w8J=Y*^_D zqKs@wwwtSmrLV-B@7eADeM0xe|u*{S(Reh^2$Jz#ByW+Mzzc3DG zJ&ue%K4)gVCoq6R(B(1E9++XGz!Jnkc!x%`C_;@*e~ytj74`Y2$65*$>@Gj=_!dKV z9*q&8Lu#GIm7eeDbY1O_&yfE% z>pw_rv-}UIEB~8Ca2(t+l61Ugqw(^U=Iv*{SCRTb>{ZNGX)jz8cJckR+G8(wo>~`b`q--Yg_S~;yXb1c)75qN{{D&hwQug- zybFOh)RxR#)ArOl_w3ZvTIvNbgH z(E6<9rd9!VTUYP<7x?aTc7T{J}a)Mkh!lW zgnZO^qi%X?o&C(RUAIo1`X#k|Z(wHVtKMUQw*5-6XuF$X zk1U6+-S*XwKPA7qRmaP++uo*U+3ZurYmc$-eY>(a@ML=#@amR%o~N(WF1&f-WtOIv zY_Q5!L2sQ)wkJCGR$rd=>CKxr&NV!L)5BK%i+f$y{k7J5SFz}CtzT2tG-*${aN)|? zBX%sw;peaP#aLE83^i`5`a7qz^pNH9wbd&h&z!u?w0GC6wILyH`z76;THEe9*C6%l z-n!dA-FM$!AM$J4{PsBw%roCzZQxaof-t^nJs>mgK4sbW$iRbyz zT+_97ci%3WB%yupW@@iu^4ygxIy(A8PMJ?CvJPNtylv?7c4h6;OM62vW<=FRnI=ZB z2OcNMWinf8!&z^U?Uv8BUOcn;a^PyIS+Y|@PVbKCShd4>Ekkk7&5W6e3%3>OhXq|( z>SXVgda6G>`RCLuGsDTQ1+CdnJyvN=+9q;$YZ&LQMUT(<2hGi34o%p4KDRG*_1dVN zON+F2D&_cQsV|he@}D7Q&fo9J+FBN0Hr~(D%~>W7CXzvx;)0(#Ru{OT%?bJ?j*Ngo)dArT$*Iy<&Z&>N+>v$k$zHF{eUTgX!F^@@x z=dQM!R;G&2(vn@l?4Eb?+r>LCSBI^NT-r9dHMBa=^yjUqr(0akuN1hdcVwGd&`IZ* z7ulJ2?bK>?C$3A`lwB;k`t+p7+_z6(D^uW0+MlMSrMu-y&#wL6$|c1rS1z79xm7Rp z)Kwp)FKFn7njhuJ>3mRIeQ2($tl>-y$3&@ z;ek|N&xyGH&$-_enBjAe2-U1_7UTF-8 hugegraph-hubble ${release.name}-${project.version} - 11 + 17 true + localhost 36320 - 7.16.1 - 1.12.19 + + com.baomidou + mybatis-plus-jsqlparser + 3.5.17 + + + org.junit.vintage + junit-vintage-engine + test + org.mitre.dsmiley.httpproxy smiley-http-proxy-servlet - 1.12.1 + 2.0 org.springframework.boot @@ -63,19 +72,6 @@ spring-boot-starter-test test - - - net.bytebuddy - byte-buddy - ${mockito.bytebuddy.version} - test - - - net.bytebuddy - byte-buddy-agent - ${mockito.bytebuddy.version} - test - - co.elastic.clients - elasticsearch-java - ${es.version} + org.elasticsearch.client + elasticsearch-rest-client - org.mybatis.spring.boot - mybatis-spring-boot-starter - 2.1.0 + co.elastic.clients + elasticsearch-java com.baomidou - mybatis-plus-boot-starter - 3.3.0 + mybatis-plus-spring-boot3-starter + 3.5.17 com.h2database @@ -110,7 +104,6 @@ com.github.ben-manes.caffeine caffeine - 2.8.0 net.sourceforge.javacsv @@ -121,6 +114,10 @@ org.apache.hugegraph hugegraph-common + + com.sun.xml.bind + jaxb-impl + org.apache.logging.log4j log4j-api @@ -147,6 +144,10 @@ org.apache.hugegraph hg-pd-client + + com.sun.xml.bind + jaxb-impl + org.apache.logging.log4j log4j-api @@ -166,6 +167,15 @@ hugegraph-loader ${revision} + + com.sun.xml.bind + jaxb-impl + + + + org.glassfish.web + javax.servlet.jsp + org.apache.logging.log4j log4j-api @@ -197,6 +207,10 @@ hugegraph-client ${revision} + + com.sun.xml.bind + jaxb-impl + org.apache.logging.log4j log4j-api @@ -236,11 +250,6 @@ - - commons-fileupload - commons-fileupload - 1.4 - org.parboiled parboiled-core @@ -251,6 +260,10 @@ hugegraph-core compile + + com.sun.xml.bind + jaxb-impl + @@ -284,6 +297,14 @@ core compile + + com.sun.xml.bind + jaxb-impl + + + org.apache.calcite.avatica + avatica + com.google.guava guava @@ -301,6 +322,18 @@ + + + org.glassfish.jaxb + jaxb-runtime + runtime + + + + org.apache.calcite.avatica + avatica-core + com.squareup.okhttp3 @@ -348,16 +381,14 @@ --> maven-compiler-plugin - 3.8.0 - ${java.version} - ${java.version} + ${java.version} + true -Xmaxerrs 500 -Xlint:unchecked -Xlint:deprecation - --add-modules=java.base @@ -385,7 +416,10 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 + + + @{argLine} --add-opens=java.base/java.net=ALL-UNNAMED + org.apache.maven.plugins @@ -404,7 +438,6 @@ org.jacoco jacoco-maven-plugin - 0.8.4 pre-unit-test diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java deleted file mode 100644 index 235c36bb7..000000000 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/DatabaseSchemaMigrator.java +++ /dev/null @@ -1,813 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -package org.apache.hugegraph.config; - -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.sql.DataSource; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.core.Ordered; -import org.springframework.core.PriorityOrdered; -import org.springframework.stereotype.Component; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@Component -public class DatabaseSchemaMigrator implements BeanPostProcessor, - PriorityOrdered { - - private static final int FILE_MAPPING_PATH_LENGTH = 2048; - private static final int COLLECTION_NAME_LENGTH = 48; - private static final String DEFAULT_GRAPHSPACE = "DEFAULT"; - private static final String DEFAULT_GRAPH = "hugegraph"; - private static final String GREMLIN_TYPE = "GREMLIN"; - private static final String FILE_MAPPING_TABLE = "file_mapping"; - private static final String FILE_MAPPING_PATH_COLUMN = "path"; - private static final String EXECUTE_HISTORY_TABLE = "execute_history"; - private static final String FAILURE_REASON_COLUMN = "failure_reason"; - private static final String LOAD_TASK_TABLE = "load_task"; - private static final String GREMLIN_COLLECTION_TABLE = - "gremlin_collection"; - private static final String GREMLIN_COLLECTION_SCOPE_INDEX = - "gremlin_collection_scope_name"; - /* - * Columns added to `execute_history` after the legacy schema was - * released. Each entry is {column, type, backfill value or null}. - */ - private static final String[][] EXECUTE_HISTORY_COLUMNS = { - {"conn_id", "INT", null}, - {"graphspace", "VARCHAR(48)", null}, - {"graph", "VARCHAR(48)", null}, - {"async_id", "BIGINT", "0"}, - {"text", "TEXT", "''"}, - {"async_status", "TINYINT", "0"} - }; - /* Columns added to `gremlin_collection` after the legacy schema. */ - private static final String[][] GREMLIN_COLLECTION_COLUMNS = { - {"conn_id", "INT", null}, - {"graphspace", "VARCHAR(48)", null}, - {"graph", "VARCHAR(48)", null}, - {"type", "VARCHAR(48)", null} - }; - private static final String[] LOAD_OPTION_CREDENTIALS = { - "password", "token", "pdToken", "trustStoreToken" - }; - private static final String[][] REQUIRED_INDEXES = { - {"execute_history", "execute_history_graph_create_time", - "graphspace", "graph", "create_time"}, - {"file_mapping", "file_mapping_job_id", "job_id"}, - {"load_task", "load_task_job_id", "job_id"}, - {"job_manager", "job_manager_graph_create_time", - "graphspace", "graph", "create_time"}, - {"async_task", "async_task_graph", "graphspace", "graph"} - }; - private static final ObjectMapper JSON = new ObjectMapper(); - - @Override - public int getOrder() { - // Boot's DataSourceInitializerPostProcessor runs at HIGHEST + 1. - return Ordered.HIGHEST_PRECEDENCE; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) - throws BeansException { - if (!(bean instanceof DataSource)) { - return bean; - } - try (Connection conn = ((DataSource) bean).getConnection()) { - this.migrate(conn); - } catch (SQLException e) { - throw new BeanInitializationException( - "Failed to migrate Hubble database schema", e); - } - return bean; - } - - public void migrate(Connection conn) throws SQLException { - int currentLength = this.columnSize(conn, FILE_MAPPING_TABLE, - FILE_MAPPING_PATH_COLUMN); - if (currentLength > 0 && currentLength < FILE_MAPPING_PATH_LENGTH) { - this.migrateFileMappingPath(conn, currentLength); - } - - this.migrateExecuteHistoryFailureReason(conn); - this.addMissingColumns(conn, EXECUTE_HISTORY_TABLE, - EXECUTE_HISTORY_COLUMNS); - this.addMissingColumns(conn, GREMLIN_COLLECTION_TABLE, - GREMLIN_COLLECTION_COLUMNS); - this.migrateLegacyQueryScopes(conn); - this.migrateGremlinCollectionUniqueConstraint(conn); - this.addMissingIndexes(conn); - this.removeLegacyLoadTaskCredentials(conn); - } - - private void migrateLegacyQueryScopes(Connection conn) - throws SQLException { - List scopes = this.legacyScopes(conn); - LegacyScope fallback = scopes.isEmpty() ? - new LegacyScope(null, DEFAULT_GRAPHSPACE, - DEFAULT_GRAPH) : scopes.get(0); - int updated = 0; - if (this.scopeColumnsExist(conn, EXECUTE_HISTORY_TABLE)) { - updated += this.backfillScopes(conn, EXECUTE_HISTORY_TABLE, scopes, - fallback); - } - if (this.scopeColumnsExist(conn, GREMLIN_COLLECTION_TABLE) && - this.columnExists(conn, GREMLIN_COLLECTION_TABLE, "type")) { - updated += this.backfillScopes(conn, GREMLIN_COLLECTION_TABLE, - scopes, fallback); - try (PreparedStatement statement = conn.prepareStatement( - "UPDATE `gremlin_collection` SET `type` = ? " + - "WHERE `type` IS NULL OR `type` = ''")) { - statement.setString(1, GREMLIN_TYPE); - updated += statement.executeUpdate(); - } - } - if (updated > 0) { - log.info("Recovered scope for {} legacy query records", updated); - } - } - - boolean scopeColumnsExist(Connection conn, String table) - throws SQLException { - return this.tableExists(conn, table) && - this.columnExists(conn, table, "conn_id") && - this.columnExists(conn, table, "graphspace") && - this.columnExists(conn, table, "graph"); - } - - private int backfillScopes(Connection conn, String table, - List scopes, - LegacyScope fallback) throws SQLException { - int updated = 0; - for (LegacyScope scope : scopes) { - updated += this.backfillConnectionScope(conn, table, scope); - } - try (PreparedStatement statement = conn.prepareStatement( - String.format("UPDATE `%s` SET `graphspace` = ? " + - "WHERE `graphspace` IS NULL OR " + - "`graphspace` = ''", table))) { - statement.setString(1, fallback.graphspace); - updated += statement.executeUpdate(); - } - try (PreparedStatement statement = conn.prepareStatement( - String.format("UPDATE `%s` SET `graph` = ? " + - "WHERE `graph` IS NULL OR `graph` = ''", - table))) { - statement.setString(1, fallback.graph); - updated += statement.executeUpdate(); - } - return updated; - } - - private int backfillConnectionScope(Connection conn, String table, - LegacyScope scope) throws SQLException { - int updated = 0; - try (PreparedStatement statement = conn.prepareStatement( - String.format("UPDATE `%s` SET `graphspace` = ? WHERE " + - "`conn_id` = ? AND (`graphspace` IS NULL OR " + - "`graphspace` = '')", table))) { - statement.setString(1, scope.graphspace); - statement.setInt(2, scope.connectionId); - updated += statement.executeUpdate(); - } - try (PreparedStatement statement = conn.prepareStatement( - String.format("UPDATE `%s` SET `graph` = ? WHERE " + - "`conn_id` = ? AND (`graph` IS NULL OR " + - "`graph` = '')", table))) { - statement.setString(1, scope.graph); - statement.setInt(2, scope.connectionId); - updated += statement.executeUpdate(); - } - return updated; - } - - private List legacyScopes(Connection conn) - throws SQLException { - List scopes = new ArrayList<>(); - if (!this.tableExists(conn, "graph_connection") || - !this.columnExists(conn, "graph_connection", "id") || - !this.columnExists(conn, "graph_connection", "graph")) { - return scopes; - } - - boolean hasGraphspace = this.columnExists(conn, "graph_connection", - "graphspace"); - String sql = hasGraphspace ? - "SELECT `id`, `graphspace`, `graph` FROM " + - "`graph_connection` ORDER BY `id`" : - "SELECT `id`, `graph` FROM `graph_connection` " + - "ORDER BY `id`"; - try (Statement statement = conn.createStatement(); - ResultSet rows = statement.executeQuery(sql)) { - while (rows.next()) { - Integer connectionId = rows.getInt(1); - String graphspace = hasGraphspace ? rows.getString(2) : null; - String graph = rows.getString(hasGraphspace ? 3 : 2); - scopes.add(new LegacyScope( - connectionId, - blank(graphspace) ? DEFAULT_GRAPHSPACE : graphspace, - blank(graph) ? DEFAULT_GRAPH : graph)); - } - } - return scopes; - } - - private void migrateGremlinCollectionUniqueConstraint(Connection conn) - throws SQLException { - if (!this.tableExists(conn, GREMLIN_COLLECTION_TABLE) || - !this.columnExists(conn, GREMLIN_COLLECTION_TABLE, - "graphspace") || - !this.columnExists(conn, GREMLIN_COLLECTION_TABLE, "graph") || - !this.columnExists(conn, GREMLIN_COLLECTION_TABLE, "name") || - !this.columnExists(conn, GREMLIN_COLLECTION_TABLE, "type")) { - return; - } - - String product = conn.getMetaData().getDatabaseProductName(); - String normalizedProduct = product.toLowerCase(Locale.ROOT); - if (!normalizedProduct.contains("h2") && - !normalizedProduct.contains("mysql") && - !normalizedProduct.contains("mariadb")) { - log.warn("Skip gremlin_collection unique-key migration for " + - "unsupported database product {}", product); - return; - } - this.renameLegacyCollectionCollisions(conn); - List obsolete = normalizedProduct.contains("h2") ? - this.h2ObsoleteUniqueConstraints(conn) : - this.mysqlObsoleteUniqueIndexes(conn, product); - for (String name : obsolete) { - try (Statement statement = conn.createStatement()) { - statement.execute(dropUniqueKeySql(product, name)); - } - log.info("Removed obsolete gremlin_collection unique key {}", - name); - } - - Map> uniqueIndexes = this.uniqueIndexes(conn, - GREMLIN_COLLECTION_TABLE); - if (this.hasScopeUniqueIndex(uniqueIndexes)) { - return; - } - try (Statement statement = conn.createStatement()) { - statement.execute("CREATE UNIQUE INDEX `" + - GREMLIN_COLLECTION_SCOPE_INDEX + "` ON `" + - GREMLIN_COLLECTION_TABLE + "`(" + - "`graphspace`, `graph`, `name`, `type`)"); - } - log.info("Added scoped gremlin_collection unique index {}", - GREMLIN_COLLECTION_SCOPE_INDEX); - } - - private void renameLegacyCollectionCollisions(Connection conn) - throws SQLException { - List collisions = new ArrayList<>(); - try (Statement statement = conn.createStatement(); - ResultSet rows = statement.executeQuery( - "SELECT `graphspace`, `graph`, `name`, `type` FROM " + - "`gremlin_collection` GROUP BY `graphspace`, `graph`, " + - "`name`, `type` HAVING COUNT(*) > 1")) { - while (rows.next()) { - collisions.add(new LegacyCollectionCollision( - rows.getString(1), rows.getString(2), - rows.getString(3), rows.getString(4))); - } - } - - for (LegacyCollectionCollision collision : collisions) { - List duplicateRows = - this.legacyCollectionRows(conn, collision); - for (int i = 1; i < duplicateRows.size(); i++) { - LegacyCollectionRow row = duplicateRows.get(i); - String name = this.uniqueLegacyCollectionName(conn, collision, - row); - try (PreparedStatement statement = conn.prepareStatement( - "UPDATE `gremlin_collection` SET `name` = ? " + - "WHERE `id` = ?")) { - statement.setString(1, name); - statement.setInt(2, row.id); - statement.executeUpdate(); - } - log.warn("Renamed colliding legacy saved query {} (id {}) " + - "to {} while recovering graph scope", collision.name, - row.id, name); - } - } - } - - private List legacyCollectionRows( - Connection conn, LegacyCollectionCollision collision) - throws SQLException { - List rows = new ArrayList<>(); - try (PreparedStatement statement = conn.prepareStatement( - "SELECT `id`, `conn_id` FROM `gremlin_collection` WHERE " + - "`graphspace` = ? AND `graph` = ? AND `name` = ? AND " + - "`type` = ? ORDER BY `id`")) { - statement.setString(1, collision.graphspace); - statement.setString(2, collision.graph); - statement.setString(3, collision.name); - statement.setString(4, collision.type); - try (ResultSet result = statement.executeQuery()) { - while (result.next()) { - int connectionId = result.getInt(2); - rows.add(new LegacyCollectionRow( - result.getInt(1), - result.wasNull() ? null : connectionId)); - } - } - } - return rows; - } - - private String uniqueLegacyCollectionName( - Connection conn, LegacyCollectionCollision collision, - LegacyCollectionRow row) throws SQLException { - for (int attempt = 0; ; attempt++) { - String suffix = "_" + row.id + - (attempt == 0 ? "" : "_" + attempt); - int baseLength = Math.min(collision.name.length(), - COLLECTION_NAME_LENGTH - suffix.length()); - String candidate = collision.name.substring(0, baseLength) + suffix; - if (!this.legacyCollectionNameExists(conn, collision, row, - candidate)) { - return candidate; - } - } - } - - private boolean legacyCollectionNameExists( - Connection conn, LegacyCollectionCollision collision, - LegacyCollectionRow row, String name) throws SQLException { - String connectionPredicate = row.connectionId == null ? - "`conn_id` IS NULL" : "`conn_id` = ?"; - try (PreparedStatement statement = conn.prepareStatement( - "SELECT 1 FROM `gremlin_collection` WHERE `name` = ? AND " + - "((`graphspace` = ? AND `graph` = ? AND `type` = ?) OR " + - connectionPredicate + ")")) { - statement.setString(1, name); - statement.setString(2, collision.graphspace); - statement.setString(3, collision.graph); - statement.setString(4, collision.type); - if (row.connectionId != null) { - statement.setInt(5, row.connectionId); - } - try (ResultSet result = statement.executeQuery()) { - return result.next(); - } - } - } - - private List h2ObsoleteUniqueConstraints(Connection conn) - throws SQLException { - List constraints = new ArrayList<>(); - try (PreparedStatement statement = conn.prepareStatement( - "SELECT `CONSTRAINT_NAME`, `COLUMN_LIST` FROM " + - "`INFORMATION_SCHEMA`.`CONSTRAINTS` WHERE " + - "UPPER(`TABLE_NAME`) = ? AND `CONSTRAINT_TYPE` = 'UNIQUE'")) { - statement.setString(1, - GREMLIN_COLLECTION_TABLE.toUpperCase(Locale.ROOT)); - try (ResultSet rows = statement.executeQuery()) { - while (rows.next()) { - if (obsoleteUniqueColumns(rows.getString(2))) { - constraints.add(rows.getString(1)); - } - } - } - } - return constraints; - } - - private List mysqlObsoleteUniqueIndexes(Connection conn, - String product) - throws SQLException { - String normalized = product.toLowerCase(Locale.ROOT); - if (!normalized.contains("mysql") && - !normalized.contains("mariadb")) { - log.warn("Skip gremlin_collection unique-key migration for " + - "unsupported database product {}", product); - return new ArrayList<>(); - } - List indexes = new ArrayList<>(); - for (Map.Entry> entry : - this.uniqueIndexes(conn, GREMLIN_COLLECTION_TABLE).entrySet()) { - if (obsoleteUniqueColumns(String.join(",", entry.getValue()))) { - indexes.add(entry.getKey()); - } - } - return indexes; - } - - private Map> uniqueIndexes(Connection conn, - String table) - throws SQLException { - Map> columns = new LinkedHashMap<>(); - DatabaseMetaData metaData = conn.getMetaData(); - String[] tables = {table, table.toUpperCase(Locale.ROOT)}; - for (String tableName : tables) { - try (ResultSet rows = metaData.getIndexInfo(conn.getCatalog(), null, - tableName, true, - false)) { - while (rows.next()) { - String index = rows.getString("INDEX_NAME"); - String column = rows.getString("COLUMN_NAME"); - if (index == null || column == null || - "PRIMARY".equalsIgnoreCase(index) || - index.toUpperCase(Locale.ROOT).startsWith("PRIMARY_KEY")) { - continue; - } - short position = rows.getShort("ORDINAL_POSITION"); - columns.computeIfAbsent(index, key -> new TreeMap<>()) - .put(position, column.toLowerCase(Locale.ROOT)); - } - } - if (!columns.isEmpty()) { - break; - } - } - Map> indexes = new LinkedHashMap<>(); - for (Map.Entry> entry : - columns.entrySet()) { - indexes.put(entry.getKey(), - new ArrayList<>(entry.getValue().values())); - } - return indexes; - } - - private boolean hasScopeUniqueIndex(Map> indexes) { - for (List columns : indexes.values()) { - if ("graphspace,graph,name,type".equals(String.join(",", columns))) { - return true; - } - } - return false; - } - - private static boolean obsoleteUniqueColumns(String columns) { - String normalized = columns.toLowerCase(Locale.ROOT) - .replace("`", "") - .replace(" ", ""); - return "name".equals(normalized) || - "conn_id,name".equals(normalized); - } - - static String dropUniqueKeySql(String productName, String name) { - String operation = productName.toLowerCase(Locale.ROOT).contains("h2") ? - "DROP CONSTRAINT" : "DROP INDEX"; - return "ALTER TABLE `gremlin_collection` " + operation + " `" + - name.replace("`", "``") + "`"; - } - - private static boolean blank(String value) { - return value == null || value.trim().isEmpty(); - } - - /** - * Add the columns a legacy Hubble database is missing. `CREATE TABLE IF - * NOT EXISTS` is a no-op against a pre-existing table, so an H2 file - * created by an older Hubble keeps its old column set and every query - * touching the newer columns fails. Each column is added nullable and - * then backfilled, which keeps the statement portable and idempotent: - * on a fresh database every column already exists and nothing is run. - */ - private void addMissingColumns(Connection conn, String table, - String[][] columns) throws SQLException { - if (!this.tableExists(conn, table)) { - return; - } - - String product = conn.getMetaData().getDatabaseProductName() - .toLowerCase(Locale.ROOT); - if (!product.contains("h2") && !product.contains("mysql") && - !product.contains("mariadb")) { - log.warn("Skip {} column migration for unsupported database " + - "product {}", table, - conn.getMetaData().getDatabaseProductName()); - return; - } - - for (String[] column : columns) { - String name = column[0]; - boolean added = false; - if (!this.columnExists(conn, table, name)) { - try (Statement statement = conn.createStatement()) { - statement.execute(String.format( - "ALTER TABLE `%s` ADD COLUMN `%s` %s DEFAULT NULL", - table, name, column[1])); - } - added = true; - } - // Backfill unconditionally. The ALTER and the UPDATE commit - // separately, so a crash between them would leave the column - // present but permanently NULL: the next startup would see the - // column already exists and skip the backfill for good. The - // statement only touches NULL rows, so repeating it is a no-op. - if (column[2] != null) { - try (Statement statement = conn.createStatement()) { - statement.executeUpdate(String.format( - "UPDATE `%s` SET `%s` = %s WHERE `%s` IS NULL", - table, name, column[2], name)); - } - } - if (added) { - log.info("Added {}.{} {}", table, name, column[1]); - } - } - } - - private void addMissingIndexes(Connection conn) throws SQLException { - for (String[] definition : REQUIRED_INDEXES) { - String table = definition[0]; - String index = definition[1]; - if (!this.tableExists(conn, table) || - this.indexExists(conn, table, index)) { - continue; - } - boolean columnsExist = true; - for (int i = 2; i < definition.length; i++) { - columnsExist &= this.columnExists(conn, table, definition[i]); - } - if (!columnsExist) { - continue; - } - - StringBuilder columns = new StringBuilder(); - for (int i = 2; i < definition.length; i++) { - if (columns.length() > 0) { - columns.append(", "); - } - columns.append('`').append(definition[i]).append('`'); - } - try (Statement statement = conn.createStatement()) { - statement.execute(String.format( - "CREATE INDEX `%s` ON `%s`(%s)", index, table, - columns)); - } catch (SQLException e) { - if (!this.indexExists(conn, table, index)) { - throw e; - } - log.info("Index {} on {} was added concurrently", index, - table); - continue; - } - log.info("Added index {} on {}", index, table); - } - } - - private boolean indexExists(Connection conn, String table, String index) - throws SQLException { - DatabaseMetaData metaData = conn.getMetaData(); - String[] tables = {table, table.toUpperCase(Locale.ROOT)}; - for (String tableName : tables) { - try (ResultSet rs = metaData.getIndexInfo(conn.getCatalog(), null, - tableName, false, - false)) { - while (rs.next()) { - if (index.equalsIgnoreCase(rs.getString("INDEX_NAME"))) { - return true; - } - } - } - } - return false; - } - - private void migrateFileMappingPath(Connection conn, int currentLength) - throws SQLException { - String sql = this.alterFileMappingPathSql( - conn.getMetaData().getDatabaseProductName()); - if (sql == null) { - log.warn("Skip file_mapping.path migration for unsupported " + - "database product {}", - conn.getMetaData().getDatabaseProductName()); - return; - } - - try (Statement statement = conn.createStatement()) { - statement.execute(sql); - } - log.info("Migrated file_mapping.path from {} to VARCHAR({})", - currentLength, FILE_MAPPING_PATH_LENGTH); - } - - private void migrateExecuteHistoryFailureReason(Connection conn) - throws SQLException { - if (!this.tableExists(conn, EXECUTE_HISTORY_TABLE) || - this.columnSize(conn, EXECUTE_HISTORY_TABLE, - FAILURE_REASON_COLUMN) > 0) { - return; - } - - String product = conn.getMetaData().getDatabaseProductName() - .toLowerCase(Locale.ROOT); - if (!product.contains("h2") && !product.contains("mysql") && - !product.contains("mariadb")) { - log.warn("Skip execute_history.failure_reason migration for " + - "unsupported database product {}", - conn.getMetaData().getDatabaseProductName()); - return; - } - - try (Statement statement = conn.createStatement()) { - statement.execute("ALTER TABLE `execute_history` ADD COLUMN " + - "`failure_reason` VARCHAR(64) DEFAULT NULL"); - } - log.info("Added execute_history.failure_reason VARCHAR(64)"); - } - - private void removeLegacyLoadTaskCredentials(Connection conn) - throws SQLException { - if (!this.tableExists(conn, LOAD_TASK_TABLE)) { - return; - } - - int updated = 0; - try (Statement select = conn.createStatement(); - ResultSet rows = select.executeQuery( - "SELECT `id`, `options` FROM `load_task`"); - PreparedStatement update = conn.prepareStatement( - "UPDATE `load_task` SET `options` = ? WHERE `id` = ?")) { - while (rows.next()) { - int id = rows.getInt(1); - String options = rows.getString(2); - String sanitized = this.removeCredentials(options, id); - if (sanitized == null) { - continue; - } - update.setString(1, sanitized); - update.setInt(2, id); - update.addBatch(); - updated++; - } - if (updated > 0) { - update.executeBatch(); - } - } - if (updated > 0) { - log.info("Removed credentials from {} legacy load tasks", updated); - } - } - - private String removeCredentials(String options, int id) - throws SQLException { - try { - JsonNode parsed = JSON.readTree(options); - if (!(parsed instanceof ObjectNode)) { - throw new SQLException("Invalid load task options for task " + - id); - } - ObjectNode object = (ObjectNode) parsed; - boolean changed = false; - for (String credential : LOAD_OPTION_CREDENTIALS) { - changed |= object.remove(credential) != null; - } - return changed ? JSON.writeValueAsString(object) : null; - } catch (JsonProcessingException e) { - throw new SQLException("Failed to sanitize load task options for " + - "task " + id, e); - } - } - - private boolean tableExists(Connection conn, String table) - throws SQLException { - DatabaseMetaData metaData = conn.getMetaData(); - String[] tables = {table, table.toUpperCase(Locale.ROOT)}; - for (String tableName : tables) { - try (ResultSet rs = metaData.getTables(null, null, tableName, - new String[]{"TABLE"})) { - if (rs.next()) { - return true; - } - } - } - return false; - } - - private boolean columnExists(Connection conn, String table, String column) - throws SQLException { - DatabaseMetaData metaData = conn.getMetaData(); - String[] tables = {table, table.toUpperCase(Locale.ROOT)}; - String[] columns = {column, column.toUpperCase(Locale.ROOT)}; - for (String tableName : tables) { - for (String columnName : columns) { - try (ResultSet rs = metaData.getColumns(null, null, tableName, - columnName)) { - if (rs.next()) { - return true; - } - } - } - } - return false; - } - - private int columnSize(Connection conn, String table, String column) - throws SQLException { - DatabaseMetaData metaData = conn.getMetaData(); - String[] tables = {table, table.toUpperCase(Locale.ROOT)}; - String[] columns = {column, column.toUpperCase(Locale.ROOT)}; - for (String tableName : tables) { - for (String columnName : columns) { - try (ResultSet rs = metaData.getColumns(null, null, tableName, - columnName)) { - if (rs.next()) { - return rs.getInt("COLUMN_SIZE"); - } - } - } - } - return 0; - } - - private String alterFileMappingPathSql(String productName) { - String product = productName.toLowerCase(Locale.ROOT); - if (product.contains("h2")) { - return "ALTER TABLE `file_mapping` ALTER COLUMN `path` " + - "VARCHAR(2048) NOT NULL"; - } - if (product.contains("mysql") || product.contains("mariadb")) { - return "ALTER TABLE `file_mapping` MODIFY COLUMN `path` " + - "VARCHAR(2048) NOT NULL"; - } - return null; - } - - private static final class LegacyScope { - - private final Integer connectionId; - private final String graphspace; - private final String graph; - - private LegacyScope(Integer connectionId, String graphspace, - String graph) { - this.connectionId = connectionId; - this.graphspace = graphspace; - this.graph = graph; - } - } - - private static final class LegacyCollectionCollision { - - private final String graphspace; - private final String graph; - private final String name; - private final String type; - - private LegacyCollectionCollision(String graphspace, String graph, - String name, String type) { - this.graphspace = graphspace; - this.graph = graph; - this.name = name; - this.type = type; - } - } - - private static final class LegacyCollectionRow { - - private final int id; - private final Integer connectionId; - - private LegacyCollectionRow(int id, Integer connectionId) { - this.id = id; - this.connectionId = connectionId; - } - } -} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/H2DataSourceConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/H2DataSourceConfig.java new file mode 100644 index 000000000..ea65b35a1 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/H2DataSourceConfig.java @@ -0,0 +1,45 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.config; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.zaxxer.hikari.HikariDataSource; + +@Configuration +@EnableConfigurationProperties(DataSourceProperties.class) +public class H2DataSourceConfig { + + @Bean + @ConfigurationProperties("spring.datasource.hikari") + public HikariDataSource dataSource(DataSourceProperties properties) { + String url = properties.determineUrl(); + if (!url.startsWith("jdbc:h2:") || + !"org.h2.Driver".equals(properties.determineDriverClassName())) { + throw new IllegalArgumentException("Hubble metadata requires H2; " + + "configure a new H2 database"); + } + return properties.initializeDataSourceBuilder() + .type(HikariDataSource.class).build(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java index 228c3588f..37b659e92 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/IngestionProxyServlet.java @@ -28,8 +28,8 @@ import org.apache.hugegraph.common.Constant; import org.mitre.dsmiley.httpproxy.ProxyServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.nio.charset.StandardCharsets; import java.io.IOException; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MybatisPlusConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MybatisPlusConfig.java index 70d270eba..6347a82cf 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MybatisPlusConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/MybatisPlusConfig.java @@ -22,14 +22,18 @@ import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; -import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; @Configuration @EnableTransactionManagement public class MybatisPlusConfig { @Bean - public PaginationInterceptor paginationInterceptor() { - return new PaginationInterceptor(); + public MybatisPlusInterceptor paginationInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); + return interceptor; } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java index f19e86695..e124ee633 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/TomcatServletConfig.java @@ -20,7 +20,6 @@ import org.apache.hugegraph.exception.ExternalException; import org.apache.hugegraph.options.HubbleOptions; -import org.apache.tomcat.util.http.LegacyCookieProcessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -29,9 +28,6 @@ import java.net.InetAddress; import java.net.UnknownHostException; -/** - * Reference http://www.zizhixiaoshe.com/article/invalidcookie.html - */ @Component public class TomcatServletConfig implements WebServerFactoryCustomizer { @@ -49,8 +45,5 @@ public void customize(TomcatServletWebServerFactory factory) { throw new ExternalException("service.unknown-host", e, host); } factory.setPort(this.config.get(HubbleOptions.SERVER_PORT)); - factory.addContextCustomizers(context -> { - context.setCookieProcessor(new LegacyCookieProcessor()); - }); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java index eec2c0e70..27e949ba4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java @@ -20,8 +20,8 @@ import java.util.List; import java.util.function.Function; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.driver.factory.PDHugeClientFactory; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java index 4f9a746cb..00bdf42db 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/SettingController.java @@ -28,9 +28,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.util.concurrent.TimeUnit; @RestController diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java index 60393f8e2..991527e0e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/op/LogController.java @@ -33,7 +33,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java index d209e8733..617905445 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java @@ -63,7 +63,7 @@ import org.apache.hugegraph.util.PageUtil; import com.baomidou.mybatisplus.core.metadata.IPage; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import lombok.Builder; @@ -128,7 +128,6 @@ public void schemaGroovyExport(@PathVariable("graphspace") String graphSpace, HugeClient client = this.authClient(graphSpace, graph); String schema = GroovySchemaCompatibility.export(client.schema()); - response.setCharacterEncoding("UTF-8"); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", contentDisposition(graphSpace, graph)); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/AsyncTaskStatus.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/AsyncTaskStatus.java index 7591f87bd..ba4e013df 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/AsyncTaskStatus.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/AsyncTaskStatus.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; public enum AsyncTaskStatus implements IEnum { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteStatus.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteStatus.java index 8c931ae2c..043c8cd71 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteStatus.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteStatus.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; public enum ExecuteStatus implements IEnum { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java index fc1c4363e..9d3dc03f9 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/ExecuteType.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; import org.apache.hugegraph.exception.InternalException; import java.util.HashSet; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/FileMappingStatus.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/FileMappingStatus.java index 7310bbed0..46db35645 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/FileMappingStatus.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/FileMappingStatus.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; public enum FileMappingStatus implements IEnum { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/JobStatus.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/JobStatus.java index b6e80b49b..cc7cb4e23 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/JobStatus.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/JobStatus.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; public enum JobStatus implements IEnum { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadAction.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadAction.java index 42583bf1e..3deb3e219 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadAction.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadAction.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; public enum LoadAction implements IEnum { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java index 6db79f45e..701c7ded1 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/enums/LoadStatus.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.entity.enums; -import com.baomidou.mybatisplus.core.enums.IEnum; +import com.baomidou.mybatisplus.annotation.IEnum; public enum LoadStatus implements IEnum { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java index ba7abc40c..4a938e0a7 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java @@ -22,9 +22,9 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; //import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence import org.apache.hugegraph.service.HugeClientPoolService; @@ -34,7 +34,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.HandlerInterceptor; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.config.HugeConfig; @@ -47,7 +47,7 @@ @Log4j2 @Component -public class CustomInterceptor extends HandlerInterceptorAdapter { +public class CustomInterceptor implements HandlerInterceptor { //@Autowired //private LicenseService licenseService;// TODO C Remove Licence diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java index 2a23c422c..f50ab82a9 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ExceptionAdvisor.java @@ -22,7 +22,7 @@ import java.util.Map; import java.util.regex.Pattern; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.exception.ServerException; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java index 74b62e38b..3b452fa37 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java @@ -25,12 +25,12 @@ import org.springframework.http.HttpStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.HandlerInterceptor; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; -public class LoginInterceptor extends HandlerInterceptorAdapter { +public class LoginInterceptor implements HandlerInterceptor { @Autowired private AuthModeService authMode; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java index 8e49bd560..6e404d6d8 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/MessageSourceHandler.java @@ -33,8 +33,8 @@ import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.util.WebUtils; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import java.util.Locale; @Log4j2 diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java index bdafc8c62..67f9f9061 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/ResponseAdvisor.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.handler; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java index b627b2d32..83a1a973b 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java @@ -28,8 +28,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java index 544e2314b..23055039e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/BelongService.java @@ -38,12 +38,15 @@ import org.apache.hugegraph.util.PageUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.context.annotation.Lazy; @Log4j2 @Service public class BelongService extends AuthService { + // Resolve user details only when listing memberships, after service initialization. @Autowired + @Lazy private UserService userService; public void add(HugeClient client, String roleId, String userId) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java index 059146209..0c7af73f0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java @@ -45,6 +45,7 @@ import org.apache.hugegraph.options.HubbleOptions; import org.apache.hugegraph.service.algorithm.AsyncTaskService; import org.apache.hugegraph.service.auth.UserService; +import org.apache.hugegraph.service.auth.AuthModeService; import org.apache.hugegraph.service.load.LoadTaskService; import org.apache.hugegraph.service.query.ExecuteHistoryService; import org.apache.hugegraph.service.query.QueryService; @@ -91,6 +92,8 @@ public class GraphsService { @Autowired UserService userService; @Autowired + private AuthModeService authModeService; + @Autowired private QueryService queryService; @Autowired private ExecuteHistoryService historyService; @@ -301,8 +304,9 @@ public Map create(HugeClient client, String nickname, "graph-connection.graph.unmatch-regex"); Map conf = new HashMap<>(); - conf.put("gremlin.graph", - "org.apache.hugegraph.auth.HugeFactoryAuthProxy"); + conf.put("gremlin.graph", this.authModeService.enabled() ? + "org.apache.hugegraph.auth.HugeFactoryAuthProxy" : + "org.apache.hugegraph.HugeFactory"); conf.put("store", graph); boolean pdEnabled = config.get(org.apache.hugegraph.options.HubbleOptions.PD_ENABLED); if (pdEnabled) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java index e2578aa27..deb05a8f7 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java @@ -63,7 +63,7 @@ public class JobManagerService { private FileMappingService fileMappingService; public int count() { - return this.mapper.selectCount(null); + return Math.toIntExact(this.mapper.selectCount(null)); } public JobManager get(int id) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java index 32aef50da..23b3656c4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java @@ -157,13 +157,13 @@ public List list(String graphSpace, String graph, int jobId, } public int count() { - return this.mapper.selectCount(null); + return Math.toIntExact(this.mapper.selectCount(null)); } public int taskCountByJob(int jobId) { QueryWrapper query = Wrappers.query(); query.eq("job_id", jobId); - return this.mapper.selectCount(query); + return Math.toIntExact(this.mapper.selectCount(query)); } public List taskListByJob(int jobId) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java index b2ccb7503..3c662c48f 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.service.op; -import javax.annotation.PreDestroy; +import jakarta.annotation.PreDestroy; import java.net.URI; import java.nio.charset.StandardCharsets; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java index a6b016679..502ad3d24 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java @@ -130,7 +130,7 @@ public GremlinCollection getByName(String graphSpace, String graph, } public int count() { - return this.mapper.selectCount(null); + return Math.toIntExact(this.mapper.selectCount(null)); } @Transactional(isolation = Isolation.READ_COMMITTED) diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java index e76c27aa0..a53dfa1c7 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/schema/SchemaService.java @@ -45,6 +45,7 @@ import org.apache.hugegraph.util.HubbleUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.context.annotation.Lazy; import org.springframework.util.CollectionUtils; import java.util.ArrayList; @@ -69,11 +70,15 @@ public class SchemaService { @Autowired private HugeConfig config; + // Subclasses inherit these collaborators, so resolve self-references lazily. @Autowired + @Lazy private PropertyKeyService pkService; @Autowired + @Lazy private VertexLabelService vlService; @Autowired + @Lazy private EdgeLabelService elService; public HugeConfig config() { diff --git a/hugegraph-hubble/hubble-be/src/main/resources/application.properties b/hugegraph-hubble/hubble-be/src/main/resources/application.properties index bdbdb2a4d..8bd855324 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/application.properties +++ b/hugegraph-hubble/hubble-be/src/main/resources/application.properties @@ -23,8 +23,8 @@ spring.profiles.active=dev # SECURITY: Production or public deployments must terminate HTTPS before Hubble. # Never expose Hubble's plain HTTP listener directly to an untrusted network. # Trust forwarding headers only from Tomcat's internal-proxy ranges. Configure -# server.tomcat.internal-proxies explicitly when the HTTPS proxy uses other ranges. -server.use-forward-headers=true +# server.tomcat.remoteip.internal-proxies explicitly when the HTTPS proxy uses other ranges. +server.forward-headers-strategy=native # Keep an active browser session alive; Servlet access refreshes the idle timer. server.servlet.session.timeout=48h @@ -34,21 +34,15 @@ auth.login.initial-backoff-seconds=5 auth.login.max-backoff-seconds=600 auth.login.max-tracked-principals=10000 -# web static file path, local h2 database or remote mysql for you to choose -# local h2 database -spring.resources.static-locations=classpath:/ui/ +# Static resources and H2 metadata (use a new database for Hubble 1.8) +spring.web.resources.static-locations=classpath:/ui/ spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:file:./db;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.url=jdbc:h2:file:./data/hubble-v2;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=sa spring.datasource.password= -spring.datasource.schema=classpath:database/schema.sql -spring.datasource.data=classpath:database/data.sql - -# remote mysql -#spring.datasource.url=jdbc:mysql://{ip}:{port}/{database}?serverTimezone=Asia/Shanghai -#spring.datasource.username={username} -#spring.datasource.password={password} -#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:database/schema.sql +spring.sql.init.data-locations=classpath:database/data.sql spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=15 @@ -75,13 +69,12 @@ spring.servlet.multipart.resolve-lazily=false mybatis-plus.global-config.banner=false mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml -mybatis-plus.type-enums-package=org.apache.hugegraph.entity.enums -mybatis.type-aliases-package=org.apache.hugegraph.entity -mybatis.configuration.cache-enabled=false -mybatis.configuration.map-underscore-to-camel-case=true -mybatis.configuration.use-generated-keys=true -mybatis.configuration.default-executor-type=reuse -mybatis.configuration.default-statement-timeout=600 +mybatis-plus.type-aliases-package=org.apache.hugegraph.entity +mybatis-plus.configuration.cache-enabled=false +mybatis-plus.configuration.map-underscore-to-camel-case=true +mybatis-plus.configuration.use-generated-keys=true +mybatis-plus.configuration.default-executor-type=reuse +mybatis-plus.configuration.default-statement-timeout=600 management.endpoints.web.exposure.include=* @@ -91,6 +84,11 @@ management.health.elasticsearch.enabled=false logging.level.org.springframework=WARN logging.level.org.apache.hugegraph.mapper=INFO logging.level.org.apache.hugegraph.service=INFO -logging.file=logs/hugegraph-hubble.log +logging.file.name=logs/hugegraph-hubble.log #logging.config=file:./conf/log4j2.xml -logging.file.max-size=10MB +logging.logback.rollingpolicy.max-file-size=10MB + +# ESService creates the configured ES 7 client lazily; do not create a default ES 8 client. +spring.autoconfigure.exclude=\ + org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientAutoConfiguration,\ + org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration diff --git a/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql b/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql index df804edd0..f49c8cd3e 100644 --- a/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql +++ b/hugegraph-hubble/hubble-be/src/main/resources/database/schema.sql @@ -4,8 +4,8 @@ * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF * licenses this file to You 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 + * "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 * @@ -16,41 +16,6 @@ * under the License. */ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - */ - CREATE TABLE IF NOT EXISTS `user_info` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(48) NOT NULL, @@ -76,14 +41,14 @@ CREATE TABLE IF NOT EXISTS `execute_history` ( `conn_id` INT, `graphspace` VARCHAR(48) NOT NULL, `graph` VARCHAR(48) NOT NULL, - `async_id` LONG NOT NULL, + `async_id` BIGINT NOT NULL, `execute_type` TINYINT NOT NULL, `content` TEXT NOT NULL, `text` TEXT NOT NULL, `execute_status` TINYINT NOT NULL, `failure_reason` VARCHAR(64) DEFAULT NULL, `async_status` TINYINT NOT NULL DEFAULT 0, - `duration` LONG NOT NULL, + `duration` BIGINT NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`) ); @@ -92,7 +57,7 @@ CREATE INDEX IF NOT EXISTS `execute_history_conn_id` ON `execute_history`(`conn_ CREATE INDEX IF NOT EXISTS `execute_history_graph_create_time` ON `execute_history`(`graphspace`, `graph`, `create_time`); -// DROP TABLE IF EXISTS `edit_history`; +-- DROP TABLE IF EXISTS `edit_history`; CREATE TABLE IF NOT EXISTS `edit_history` ( `id` int NOT NULL AUTO_INCREMENT, @@ -104,7 +69,7 @@ CREATE TABLE IF NOT EXISTS `edit_history` `option_type` varchar(255) DEFAULT NULL, `option_time` datetime DEFAULT NULL, `option_person` varchar(255) DEFAULT NULL, - `content` longtext, + `content` text, PRIMARY KEY (`id`) ); @@ -133,8 +98,8 @@ CREATE TABLE IF NOT EXISTS `file_mapping` ( `job_id` INT NOT NULL DEFAULT 0, `name` VARCHAR(128) NOT NULL, `path` VARCHAR(2048) NOT NULL, - `total_lines` LONG NOT NULL, - `total_size` LONG NOT NULL, + `total_lines` BIGINT NOT NULL, + `total_size` BIGINT NOT NULL, `file_status` TINYINT NOT NULL DEFAULT 0, `file_setting` VARCHAR(65535) NOT NULL, `vertex_mappings` VARCHAR(65535) NOT NULL, @@ -159,11 +124,11 @@ CREATE TABLE IF NOT EXISTS `load_task` ( `options` VARCHAR(65535) NOT NULL, `vertices` VARCHAR(512) NOT NULL, `edges` VARCHAR(512) NOT NULL, - `file_total_lines` LONG NOT NULL, + `file_total_lines` BIGINT NOT NULL, `load_status` TINYINT NOT NULL, - `file_read_lines` LONG NOT NULL, - `last_duration` LONG NOT NULL, - `curr_duration` LONG NOT NULL, + `file_read_lines` BIGINT NOT NULL, + `last_duration` BIGINT NOT NULL, + `curr_duration` BIGINT NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`) ); @@ -175,9 +140,9 @@ CREATE TABLE IF NOT EXISTS `job_manager` ( `graph` VARCHAR(48) NOT NULL, `job_name` VARCHAR(100) NOT NULL DEFAULT '', `job_remarks` VARCHAR(200) NOT NULL DEFAULT '', - `job_size` LONG NOT NULL DEFAULT 0, + `job_size` BIGINT NOT NULL DEFAULT 0, `job_status` TINYINT NOT NULL DEFAULT 0, - `job_duration` LONG NOT NULL DEFAULT 0, + `job_duration` BIGINT NOT NULL DEFAULT 0, `update_time` DATETIME(6) NOT NULL, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`), @@ -196,7 +161,7 @@ CREATE TABLE IF NOT EXISTS `async_task` ( `algorithm_name` VARCHAR(48) NOT NULL DEFAULT '', `task_content` VARCHAR(65535) NOT NULL DEFAULT '', `task_status` TINYINT NOT NULL DEFAULT 0, - `task_duration` LONG NOT NULL DEFAULT 0, + `task_duration` BIGINT NOT NULL DEFAULT 0, `create_time` DATETIME(6) NOT NULL, PRIMARY KEY (`id`) ); diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/LegacySchemaStartupTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/LegacySchemaStartupTest.java deleted file mode 100644 index 194286ff4..000000000 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/LegacySchemaStartupTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You 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. - */ - -package org.apache.hugegraph.config; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - -import javax.sql.DataSource; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mybatis.spring.annotation.MapperScan; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.core.io.FileSystemResource; -import org.springframework.jdbc.datasource.init.ScriptUtils; -import org.springframework.test.context.junit4.SpringRunner; - -import org.apache.hugegraph.driver.HugeClient; -import org.apache.hugegraph.entity.enums.ExecuteType; -import org.apache.hugegraph.entity.query.ExecuteHistory; -import org.apache.hugegraph.entity.query.GremlinCollection; -import org.apache.hugegraph.options.HubbleOptions; -import org.apache.hugegraph.service.query.ExecuteHistoryService; -import org.apache.hugegraph.service.query.GremlinCollectionService; -import org.apache.hugegraph.testutil.Assert; -import com.baomidou.mybatisplus.core.metadata.IPage; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = LegacySchemaStartupTest.TestConfiguration.class, - webEnvironment = SpringBootTest.WebEnvironment.NONE, - properties = { - "spring.datasource.url=" + LegacySchemaStartupTest.JDBC_URL, - "spring.datasource.driver-class-name=org.h2.Driver", - "spring.datasource.username=sa", - "spring.datasource.password=", - "spring.datasource.schema=" + - "file:src/main/resources/database/schema.sql", - "spring.datasource.initialization-mode=always", - "spring.datasource.hikari.maximum-pool-size=2", - "spring.autoconfigure.exclude=" + - "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration", - "mybatis.configuration.map-underscore-to-camel-case=true", - "mybatis.configuration.use-generated-keys=true", - "mybatis-plus.type-enums-package=" + - "org.apache.hugegraph.entity.enums" - }) -public class LegacySchemaStartupTest { - - static final String JDBC_URL = - "jdbc:h2:./target/legacy-schema-startup;DB_CLOSE_DELAY=-1"; - - static { - prepareLegacyDatabase(); - } - - @Autowired - private DataSource dataSource; - @Autowired - private DatabaseSchemaMigrator migrator; - @Autowired - private ExecuteHistoryService historyService; - @Autowired - private GremlinCollectionService collectionService; - @MockBean - private HugeConfig config; - - private HugeClient client; - - @Before - public void setup() throws Exception { - Mockito.when(this.config.get(HubbleOptions.EXECUTE_HISTORY_SHOW_LIMIT)) - .thenReturn(500); - this.client = Mockito.mock(HugeClient.class); - Mockito.when(this.client.getGraphSpaceName()).thenReturn("DEFAULT"); - Mockito.when(this.client.getGraphName()).thenReturn("legacygraph"); - try (Connection conn = this.dataSource.getConnection()) { - this.migrator.migrate(conn); - } - } - - @Test - public void testLegacyDatabaseStartsAndRowsAreVisibleThroughServices() - throws Exception { - IPage histories = this.historyService.list( - this.client, ExecuteType.GREMLIN.getValue(), 1L, 10L, false); - Assert.assertEquals(1L, histories.getTotal()); - Assert.assertEquals("g.V()", histories.getRecords().get(0).getContent()); - - IPage collections = this.collectionService.list( - this.client, null, "GREMLIN", null, false, 1L, 10L); - Assert.assertEquals(1L, collections.getTotal()); - Assert.assertEquals("saved", collections.getRecords().get(0).getName()); - - try (Connection conn = this.dataSource.getConnection()) { - this.assertScopedRows(conn); - } - } - - @Test - public void testSavedQueryConstraintMatchesApplicationScope() - throws Exception { - try (Connection conn = this.dataSource.getConnection(); - Statement statement = conn.createStatement()) { - statement.execute("DELETE FROM `gremlin_collection` " + - "WHERE `graph` = 'othergraph'"); - statement.execute("INSERT INTO `gremlin_collection` " + - "(`conn_id`, `graphspace`, `graph`, `name`, " + - "`type`, `content`, `create_time`) VALUES " + - "(1, 'DEFAULT', 'othergraph', 'saved', " + - "'GREMLIN', 'g.E()', CURRENT_TIMESTAMP)"); - try (ResultSet rows = statement.executeQuery( - "SELECT COUNT(*) FROM `gremlin_collection` WHERE " + - "`graphspace` = 'DEFAULT' AND `graph` = 'othergraph' " + - "AND `name` = 'saved' AND `type` = 'GREMLIN'")) { - Assert.assertTrue(rows.next()); - Assert.assertEquals(1L, rows.getLong(1)); - } - Assert.assertThrows(SQLException.class, () -> - statement.execute("INSERT INTO `gremlin_collection` " + - "(`conn_id`, `graphspace`, `graph`, " + - "`name`, `type`, `content`, " + - "`create_time`) VALUES " + - "(1, 'DEFAULT', 'legacygraph', " + - "'saved', 'GREMLIN', 'g.E()', " + - "CURRENT_TIMESTAMP)")); - } - } - - @Test - public void testLegacyScopeBackfillRequiresAllTargetColumns() - throws Exception { - try (Connection conn = this.dataSource.getConnection(); - Statement statement = conn.createStatement()) { - statement.execute("DROP TABLE IF EXISTS `scope_guard`"); - statement.execute("CREATE TABLE `scope_guard` (`conn_id` INT)"); - Assert.assertFalse(this.migrator.scopeColumnsExist(conn, - "scope_guard")); - statement.execute("ALTER TABLE `scope_guard` ADD COLUMN " + - "`graphspace` VARCHAR(48)"); - statement.execute("ALTER TABLE `scope_guard` ADD COLUMN " + - "`graph` VARCHAR(48)"); - Assert.assertTrue(this.migrator.scopeColumnsExist(conn, - "scope_guard")); - statement.execute("DROP TABLE `scope_guard`"); - } - } - - @Test - public void testMysqlUsesDropIndexForLegacyUniqueKeys() { - Assert.assertEquals("ALTER TABLE `gremlin_collection` DROP INDEX " + - "`legacy_name_key`", - DatabaseSchemaMigrator.dropUniqueKeySql( - "MySQL", "legacy_name_key")); - } - - private void assertScopedRows(Connection conn) throws Exception { - try (Statement statement = conn.createStatement(); - ResultSet rows = statement.executeQuery( - "SELECT `conn_id`, `graphspace`, `graph` FROM " + - "`execute_history`")) { - Assert.assertTrue(rows.next()); - Assert.assertNull(rows.getObject(1)); - Assert.assertEquals("DEFAULT", rows.getString(2)); - Assert.assertEquals("legacygraph", rows.getString(3)); - } - } - - private static void prepareLegacyDatabase() { - Path database = Paths.get("target/legacy-schema-startup.mv.db"); - try { - Files.createDirectories(database.getParent()); - Files.deleteIfExists(database); - try (Connection conn = DriverManager.getConnection(JDBC_URL, - "sa", ""); - Statement statement = conn.createStatement()) { - ScriptUtils.executeSqlScript(conn, new FileSystemResource( - Paths.get("src/test/resources/database/schema.sql"))); - statement.execute("INSERT INTO `graph_connection` " + - "(`name`, `graph`, `host`, `port`, " + - "`create_time`) VALUES ('legacy', " + - "'legacygraph', 'localhost', 8080, " + - "CURRENT_TIMESTAMP)"); - statement.execute("INSERT INTO `execute_history` " + - "(`execute_type`, `content`, " + - "`execute_status`, `duration`, " + - "`create_time`) VALUES " + - "(0, 'g.V()', 1, 10, CURRENT_TIMESTAMP)"); - statement.execute("INSERT INTO `gremlin_collection` " + - "(`name`, `content`, `create_time`) VALUES " + - "('saved', 'g.E()', CURRENT_TIMESTAMP)"); - } - } catch (Exception e) { - throw new ExceptionInInitializerError(e); - } - } - - @SpringBootConfiguration - @EnableAutoConfiguration - @MapperScan("org.apache.hugegraph.mapper.query") - @Import({DatabaseSchemaMigrator.class, MybatisPlusConfig.class, - ExecuteHistoryService.class, GremlinCollectionService.class}) - public static class TestConfiguration { - } -} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java index d5274639a..5b4c1cc44 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java @@ -518,7 +518,7 @@ public void testOrdinaryUserCannotAssignGraphSpaceMembership() { org.junit.Assert.assertTrue(failure.getMessage() .contains("authorization objects")); - Mockito.verifyNoMoreInteractions(memberService); + Mockito.verifyNoInteractions(memberService); } @Test @@ -540,7 +540,7 @@ public void testCurrentSpaceAdminCannotUseLegacyRoleMembershipRoute() { assertForbidden(() -> controller.create("SPACE", member)); assertForbidden(() -> controller.createOrUpdate("SPACE", "bob", member)); - Mockito.verifyNoMoreInteractions(memberService); + Mockito.verifyNoInteractions(memberService); } @Test @@ -566,7 +566,7 @@ public void testOrdinaryUserCannotChangeAnyGraphSpaceMembership() { assertForbidden(() -> controller.removeGraphSpaceAdmin("SPACE", "bob")); - Mockito.verifyNoMoreInteractions(memberService); + Mockito.verifyNoInteractions(memberService); } private void setBaseUserService(BaseController controller, diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java index 4460e39d8..2af7568ef 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java @@ -534,7 +534,7 @@ public void testGraphSpaceUserRoleUpdatePreflightsAllRoles() { assertForbidden(() -> service.createOrUpdate( this.client, "SPACE_A", user)); - Mockito.verifyNoMoreInteractions(belongs); + Mockito.verifyNoInteractions(belongs); Mockito.verify(this.auth, Mockito.never()) .addSpaceMember(Mockito.anyString(), Mockito.anyString()); } @@ -651,7 +651,7 @@ public void testSpaceAdminCannotChangeGlobalAdminPreset() "\"permission_preset\":\"GS_READ_WRITE\"}")) .andExpect(status().isForbidden()); - Mockito.verifyNoMoreInteractions(members); + Mockito.verifyNoInteractions(members); } private static Target target(String id, String graphSpace) { diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java index c96ecd8c0..68e2bb94e 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java @@ -288,7 +288,7 @@ public void testOnlySuperadminCanMutateGraphSpaces() throws Exception { assertForbidden(() -> controller.delete("foreign")); assertForbidden(() -> controller.initBuiltIn(new BuiltInEntity())); - Mockito.verifyNoMoreInteractions(graphSpaceService); + Mockito.verifyNoInteractions(graphSpaceService); Mockito.when(userService.isSuperAdmin(client)).thenReturn(true); Assert.assertSame(client, controller.requireGlobalManager()); } @@ -310,7 +310,7 @@ public void testAnonymousModeCannotMutateGraphSpaces() throws Exception { assertForbidden(() -> controller.add(new GraphSpaceEntity())); - Mockito.verifyNoMoreInteractions(userService, graphSpaceService); + Mockito.verifyNoInteractions(userService, graphSpaceService); } @Test @@ -335,7 +335,7 @@ public void testForbiddenGraphSpaceMutationUsesHttpAndBody403() .andExpect(status().isForbidden()) .andExpect(jsonPath("$.status").value(403)); - Mockito.verifyNoMoreInteractions(graphSpaceService); + Mockito.verifyNoInteractions(graphSpaceService); } private static TestGraphSpaceController controller( diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java index 2bd80eb4d..4d343ca94 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java @@ -135,7 +135,7 @@ public void testAnonymousModeKeepsTemplateMutationAvailable() { Mockito.verify(controller.schemaTemplateService) .delete(controller.client, "template"); - Mockito.verifyNoMoreInteractions(controller.users); + Mockito.verifyNoInteractions(controller.users); } private static TestController controller(boolean authEnabled) { diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java index 8da943447..9b9db6b0c 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java @@ -296,7 +296,7 @@ public void testAnonymousModeGetsReadOnlyOperationsCapabilities() { Assert.assertEquals(Set.of("read_health", "read_topology", "read_metrics"), actions(context, "operations")); - Mockito.verifyNoMoreInteractions(fixture.users); + Mockito.verifyNoInteractions(fixture.users); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java index 2030e0173..4e4c6732f 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java @@ -129,7 +129,7 @@ public void testGraphSpacePresetRequiresAtLeastOneGraphSpace() { "auth.permission-preset.graphspace-required", error.getMessage()); } - Mockito.verifyNoMoreInteractions(this.graphSpace); + Mockito.verifyNoInteractions(this.graphSpace); } @Test @@ -145,7 +145,7 @@ public void testUnknownAccountPresetIsRejectedBeforeAccountCreation() { Assert.assertNotNull(error); Assert.assertEquals("auth.permission-preset.invalid", error.getMessage()); - Mockito.verifyNoMoreInteractions(this.graphSpace); + Mockito.verifyNoInteractions(this.graphSpace); } @Test @@ -217,7 +217,7 @@ public void testLegacyAccessUsesMembershipWithoutDefaultRoleApi() { this.client, "team", "alice")); Assert.assertFalse(this.service.hasGraphSpaceAccess( this.client, "team", "bob")); - Mockito.verifyNoMoreInteractions(this.graphSpace); + Mockito.verifyNoInteractions(this.graphSpace); Mockito.verify(this.auth, Mockito.times(2)).listSpaceMember("team"); } @@ -249,8 +249,8 @@ public void testModernRolePayloadRequiresPresetApi() { Assert.assertNotNull(error); Assert.assertEquals("auth.permission-preset.required", error.getMessage()); - Mockito.verifyNoMoreInteractions(this.belongService); - Mockito.verifyNoMoreInteractions(this.auth); + Mockito.verifyNoInteractions(this.belongService); + Mockito.verifyNoInteractions(this.auth); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/load/IngestTransactionIntegrationTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/load/IngestTransactionIntegrationTest.java index 3d90b6f01..38f997593 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/load/IngestTransactionIntegrationTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/load/IngestTransactionIntegrationTest.java @@ -73,15 +73,19 @@ "spring.datasource.driver-class-name=org.h2.Driver", "spring.datasource.username=sa", "spring.datasource.password=", - "spring.datasource.schema=" + + "spring.sql.init.schema-locations=" + "classpath:database/ingest-transaction-schema.sql", - "spring.datasource.initialization-mode=always", + "spring.sql.init.mode=always", "spring.datasource.hikari.maximum-pool-size=2", "spring.autoconfigure.exclude=" + - "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration", - "mybatis.configuration.map-underscore-to-camel-case=true", - "mybatis.configuration.use-generated-keys=true", - "mybatis-plus.type-enums-package=org.apache.hugegraph.entity.enums" + "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration," + + "org.springframework.boot.autoconfigure.elasticsearch." + + "ElasticsearchClientAutoConfiguration," + + "org.springframework.boot.autoconfigure.elasticsearch." + + "ElasticsearchRestClientAutoConfiguration", + "mybatis-plus.configuration.map-underscore-to-camel-case=true", + "mybatis-plus.configuration.use-generated-keys=true", + "mybatis-plus.global-config.banner=false" }) public class IngestTransactionIntegrationTest { diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/query/ExecuteHistoryRetentionTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/query/ExecuteHistoryRetentionTest.java index 2cfb7bff2..f0fd0af87 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/query/ExecuteHistoryRetentionTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/query/ExecuteHistoryRetentionTest.java @@ -51,13 +51,17 @@ "spring.datasource.driver-class-name=org.h2.Driver", "spring.datasource.username=sa", "spring.datasource.password=", - "spring.datasource.initialization-mode=never", + "spring.sql.init.mode=never", "spring.datasource.hikari.maximum-pool-size=2", "spring.autoconfigure.exclude=" + - "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration", - "mybatis.configuration.map-underscore-to-camel-case=true", - "mybatis.configuration.use-generated-keys=true", - "mybatis-plus.type-enums-package=org.apache.hugegraph.entity.enums" + "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration," + + "org.springframework.boot.autoconfigure.elasticsearch." + + "ElasticsearchClientAutoConfiguration," + + "org.springframework.boot.autoconfigure.elasticsearch." + + "ElasticsearchRestClientAutoConfiguration", + "mybatis-plus.configuration.map-underscore-to-camel-case=true", + "mybatis-plus.configuration.use-generated-keys=true", + "mybatis-plus.global-config.banner=false" }) public class ExecuteHistoryRetentionTest { diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java index d286602b1..ec96dabf5 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java @@ -261,7 +261,7 @@ public void testAnonymousDetailRejectsProtectedSpaceBeforeStatistics() { } catch (ExternalException e) { Assert.assertEquals(404, e.status()); } - Mockito.verifyNoMoreInteractions(this.graphsService); + Mockito.verifyNoInteractions(this.graphsService); } @Test @@ -289,7 +289,7 @@ public void testAuthenticatedDetailRejectsUnassignedProtectedSpace() { Assert.assertEquals(404, e.status()); } Mockito.verify(auth, Mockito.times(2)).isSpaceMember("protected"); - Mockito.verifyNoMoreInteractions(this.graphsService); + Mockito.verifyNoInteractions(this.graphsService); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java index 8b8a05f6a..be673067d 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java @@ -514,7 +514,7 @@ public void testCustomInterceptorKeepsGraphSpaceCollectionActionsUnscoped() request, new MockHttpServletResponse(), null)); Assert.assertNull(interceptor.graphSpace); } - Mockito.verifyNoMoreInteractions(spaces); + Mockito.verifyNoInteractions(spaces); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AvaticaRuntimeTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AvaticaRuntimeTest.java new file mode 100644 index 000000000..c187c6b92 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AvaticaRuntimeTest.java @@ -0,0 +1,75 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.unit; + +import java.net.URL; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import java.sql.Types; +import java.util.Properties; + +import org.apache.calcite.avatica.ConnectionConfigImpl; +import org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl; +import org.junit.Assert; +import org.junit.Test; + +public class AvaticaRuntimeTest { + + @Test + public void testRejectInvalidHttpClientBeforeConstruction() throws Exception { + InvalidHttpClient.constructed = false; + Properties properties = new Properties(); + properties.setProperty("httpclient_impl", InvalidHttpClient.class.getName()); + URL endpoint = new URL("http://localhost:1"); + AvaticaHttpClientFactoryImpl factory = new AvaticaHttpClientFactoryImpl(); + try { + factory.getClient(endpoint, new ConnectionConfigImpl(properties), null); + Assert.fail("Invalid HTTP client implementation was accepted"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getCause() instanceof ClassCastException); + } + Assert.assertFalse(InvalidHttpClient.constructed); + } + + @Test + public void testExistingCalciteCallerCanReadQueryAndMetadata() throws Exception { + Class.forName("org.apache.calcite.jdbc.Driver"); + try (Connection connection = DriverManager.getConnection("jdbc:calcite:"); + Statement statement = connection.createStatement(); + ResultSet result = statement.executeQuery( + "select cast(42 as integer) as ID, 'Avatica' as NAME")) { + Assert.assertTrue(result.next()); + Assert.assertEquals(42, result.getInt(1)); + Assert.assertEquals("Avatica", result.getString(2)); + Assert.assertEquals(Types.INTEGER, result.getMetaData().getColumnType(1)); + Assert.assertFalse(result.next()); + } + } + + public static class InvalidHttpClient { + + private static boolean constructed; + + public InvalidHttpClient(URL endpoint) { + constructed = true; + } + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java index 40a922d6b..f69dcf03e 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingSchemaTest.java @@ -25,16 +25,12 @@ import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.sql.SQLException; import java.sql.Statement; -import java.util.HashSet; -import java.util.Set; import org.junit.Test; import org.springframework.core.io.FileSystemResource; import org.springframework.jdbc.datasource.init.ScriptUtils; -import org.apache.hugegraph.config.DatabaseSchemaMigrator; import org.apache.hugegraph.testutil.Assert; public class FileMappingSchemaTest { @@ -60,346 +56,56 @@ public void testFileMappingStoresDeepUploadPath() throws Exception { } @Test - public void testSchemaMigratorWidensExistingFileMappingPath() - throws Exception { - String url = "jdbc:h2:mem:file_mapping_legacy_path;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url)) { - this.createLegacyFileMappingTable(conn); - - new DatabaseSchemaMigrator().migrate(conn); - - this.insertDeepPath(conn, this.deepUploadPath()); - } - } - - @Test - public void testSchemaMigratorAddsExecuteHistoryFailureReasonIdempotently() - throws Exception { - String url = "jdbc:h2:mem:execute_history_failure_reason;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url)) { - try (Statement statement = conn.createStatement()) { - statement.execute("CREATE TABLE `execute_history` (" + - "`id` INT NOT NULL AUTO_INCREMENT, " + - "PRIMARY KEY (`id`))"); - } - - DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); - migrator.migrate(conn); - migrator.migrate(conn); - - try (ResultSet columns = conn.getMetaData().getColumns( - null, null, "EXECUTE_HISTORY", "FAILURE_REASON")) { - Assert.assertTrue(columns.next()); - Assert.assertEquals(64, columns.getInt("COLUMN_SIZE")); + public void testNewDatabasePersistsAcrossRestart() throws Exception { + Path directory = Files.createTempDirectory("hubble-h2-"); + String url = "jdbc:h2:file:" + directory.resolve("metadata"); + try { + try (Connection conn = DriverManager.getConnection(url)) { + ScriptUtils.executeSqlScript(conn, new FileSystemResource( + this.mainSchemaPath())); + this.insertDeepPath(conn, this.deepUploadPath()); } - } - } - - @Test - public void testSchemaMigratorRemovesLegacyLoadTaskCredentials() - throws Exception { - String url = "jdbc:h2:mem:load_task_credentials;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url); - Statement statement = conn.createStatement()) { - statement.execute("CREATE TABLE `load_task` (" + - "`id` INT NOT NULL AUTO_INCREMENT, " + - "`options` VARCHAR(65535) NOT NULL, " + - "PRIMARY KEY (`id`))"); - statement.execute("INSERT INTO `load_task` (`options`) VALUES (" + - "'{\"graph\":\"hugegraph\"," + - "\"password\":\"canary-password\"," + - "\"token\":\"canary-token\"," + - "\"pdToken\":\"canary-pd-token\"," + - "\"trustStoreToken\":" + - "\"canary-truststore-token\"," + - "\"futureOption\":\"preserved\"}')"); - - DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); - migrator.migrate(conn); - migrator.migrate(conn); - - try (ResultSet rows = statement.executeQuery( - "SELECT `options` FROM `load_task`")) { - Assert.assertTrue(rows.next()); - String options = rows.getString(1); - Assert.assertFalse(options.contains("canary-")); - Assert.assertContains("futureOption", options); - Assert.assertContains("preserved", options); - } - } - } - - @Test - public void testSchemaMigratorAddsMissingLegacyColumns() throws Exception { - String url = "jdbc:h2:mem:legacy_schema_columns;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url)) { - ScriptUtils.executeSqlScript(conn, new FileSystemResource( - this.legacySchemaPath())); - try (Statement statement = conn.createStatement()) { - statement.execute("INSERT INTO `graph_connection` " + - "(`name`, `graph`, `host`, `port`, " + - "`create_time`) VALUES " + - "('legacy', 'legacygraph', 'localhost', " + - "8080, CURRENT_TIMESTAMP)"); - statement.execute("INSERT INTO `execute_history` " + - "(`execute_type`, `content`, " + - "`execute_status`, `duration`, " + - "`create_time`) VALUES " + - "(1, 'g.V()', 1, 10, CURRENT_TIMESTAMP)"); - statement.execute("INSERT INTO `gremlin_collection` " + - "(`name`, `content`, `create_time`) " + - "VALUES ('saved', 'g.E()', " + - "CURRENT_TIMESTAMP)"); - } - - DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); - // Running twice must be a no-op the second time - migrator.migrate(conn); - migrator.migrate(conn); - - for (String column : new String[]{"CONN_ID", "GRAPHSPACE", - "GRAPH", "ASYNC_ID", "TEXT", - "ASYNC_STATUS", - "FAILURE_REASON"}) { - this.assertColumnExists(conn, "EXECUTE_HISTORY", column); - } - for (String column : new String[]{"CONN_ID", "GRAPHSPACE", - "GRAPH", "TYPE"}) { - this.assertColumnExists(conn, "GREMLIN_COLLECTION", column); - } - - // The legacy rows survive and the new columns are backfilled - try (Statement statement = conn.createStatement(); - ResultSet rs = statement.executeQuery( - "SELECT `content`, `conn_id`, `graphspace`, `graph`, " + - "`async_id`, `text`, `async_status` " + - "FROM `execute_history`")) { - Assert.assertTrue(rs.next()); - Assert.assertEquals("g.V()", rs.getString(1)); - Assert.assertNull(rs.getObject(2)); - Assert.assertEquals("DEFAULT", rs.getString(3)); - Assert.assertEquals("legacygraph", rs.getString(4)); - Assert.assertEquals(0L, rs.getLong(5)); - Assert.assertEquals("", rs.getString(6)); - Assert.assertEquals(0, rs.getInt(7)); - } - try (Statement statement = conn.createStatement(); - ResultSet rs = statement.executeQuery( - "SELECT `content`, `conn_id`, `graphspace`, `graph`, " + - "`type` " + - "FROM `gremlin_collection`")) { - Assert.assertTrue(rs.next()); - Assert.assertEquals("g.E()", rs.getString(1)); - Assert.assertNull(rs.getObject(2)); - Assert.assertEquals("DEFAULT", rs.getString(3)); - Assert.assertEquals("legacygraph", rs.getString(4)); - Assert.assertEquals("GREMLIN", rs.getString(5)); - } - - // Names are unique only inside the application-visible scope. - try (Statement statement = conn.createStatement()) { - statement.execute("INSERT INTO `gremlin_collection` " + - "(`conn_id`, `graphspace`, `graph`, `name`, " + - "`type`, `content`, `create_time`) VALUES " + - "(1, 'DEFAULT', 'othergraph', 'saved', " + - "'GREMLIN', 'g.V()', CURRENT_TIMESTAMP)"); - Assert.assertThrows(SQLException.class, () -> - statement.execute("INSERT INTO `gremlin_collection` " + - "(`conn_id`, `graphspace`, `graph`, " + - "`name`, `type`, `content`, " + - "`create_time`) VALUES (1, 'DEFAULT', " + - "'legacygraph', 'saved', 'GREMLIN', " + - "'g.V()', CURRENT_TIMESTAMP)")); + try (Connection conn = DriverManager.getConnection(url)) { + // Startup runs the same schema again; it must retain data. + ScriptUtils.executeSqlScript(conn, new FileSystemResource( + this.mainSchemaPath())); + try (Statement statement = conn.createStatement(); + ResultSet result = statement.executeQuery( + "SELECT path FROM file_mapping")) { + Assert.assertTrue(result.next()); + Assert.assertEquals(this.deepUploadPath(), + result.getString(1)); + Assert.assertFalse(result.next()); + } } + } finally { + Files.deleteIfExists(directory.resolve("metadata.mv.db")); + Files.deleteIfExists(directory.resolve("metadata.trace.db")); + Files.delete(directory); } } @Test - public void testSchemaMigratorIsNoopOnFreshSchema() throws Exception { - String url = "jdbc:h2:mem:fresh_schema_columns;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url)) { + public void testCollectionsAreScopedByGraphAndType() throws Exception { + try (Connection conn = DriverManager.getConnection( + "jdbc:h2:mem:collection_scope")) { ScriptUtils.executeSqlScript(conn, new FileSystemResource( this.mainSchemaPath())); - - DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); - migrator.migrate(conn); - migrator.migrate(conn); - - this.assertColumnExists(conn, "EXECUTE_HISTORY", "GRAPHSPACE"); - this.assertColumnExists(conn, "GREMLIN_COLLECTION", "TYPE"); - this.assertIndexExists(conn, "EXECUTE_HISTORY", - "EXECUTE_HISTORY_GRAPH_CREATE_TIME"); - this.assertIndexExists(conn, "FILE_MAPPING", - "FILE_MAPPING_JOB_ID"); - this.assertIndexExists(conn, "LOAD_TASK", "LOAD_TASK_JOB_ID"); - this.assertIndexExists(conn, "JOB_MANAGER", - "JOB_MANAGER_GRAPH_CREATE_TIME"); - this.assertIndexExists(conn, "ASYNC_TASK", "ASYNC_TASK_GRAPH"); - // The fresh schema keeps its own column types untouched - try (ResultSet columns = conn.getMetaData().getColumns( - null, null, "EXECUTE_HISTORY", "GRAPHSPACE")) { - Assert.assertTrue(columns.next()); - Assert.assertEquals(48, columns.getInt("COLUMN_SIZE")); - } - } - } - - @Test - public void testSchemaMigratorRecoversScopeFromExistingConnectionId() - throws Exception { - String url = "jdbc:h2:mem:legacy_schema_conn_id;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url); - Statement statement = conn.createStatement()) { - ScriptUtils.executeSqlScript(conn, new FileSystemResource( - this.legacySchemaPath())); - statement.execute("INSERT INTO `graph_connection` " + - "(`name`, `graph`, `host`, `port`, " + - "`create_time`) VALUES " + - "('first', 'graph_a', 'host_a', 8080, " + - "CURRENT_TIMESTAMP), " + - "('second', 'graph_b', 'host_b', 8080, " + - "CURRENT_TIMESTAMP)"); - statement.execute("ALTER TABLE `execute_history` ADD COLUMN " + - "`conn_id` INT"); - statement.execute("ALTER TABLE `execute_history` ADD COLUMN " + - "`graphspace` VARCHAR(48)"); - statement.execute("ALTER TABLE `execute_history` ADD COLUMN " + - "`graph` VARCHAR(48)"); - statement.execute("INSERT INTO `execute_history` " + - "(`conn_id`, `graphspace`, `graph`, " + - "`execute_type`, `content`, `execute_status`, " + - "`duration`, `create_time`) VALUES " + - "(2, '', '', 0, 'g.V()', 1, 1, " + - "CURRENT_TIMESTAMP)"); - - new DatabaseSchemaMigrator().migrate(conn); - - try (ResultSet rows = statement.executeQuery( - "SELECT `graphspace`, `graph` FROM `execute_history`")) { - Assert.assertTrue(rows.next()); - Assert.assertEquals("DEFAULT", rows.getString(1)); - Assert.assertEquals("graph_b", rows.getString(2)); - } - } - } - - @Test - public void testSchemaMigratorPreservesCollidingLegacyCollections() - throws Exception { - String url = "jdbc:h2:mem:legacy_collection_collision;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url); - Statement statement = conn.createStatement()) { - statement.execute("CREATE TABLE `graph_connection` (" + - "`id` INT NOT NULL AUTO_INCREMENT, " + - "`graphspace` VARCHAR(48), " + - "`graph` VARCHAR(48), PRIMARY KEY (`id`))"); - statement.execute("CREATE TABLE `gremlin_collection` (" + - "`id` INT NOT NULL AUTO_INCREMENT, " + - "`conn_id` INT, `graphspace` VARCHAR(48), " + - "`graph` VARCHAR(48), `name` VARCHAR(48), " + - "`type` VARCHAR(48), `content` TEXT, " + - "PRIMARY KEY (`id`), UNIQUE (`conn_id`, `name`))"); - statement.execute("INSERT INTO `graph_connection` " + - "(`graphspace`, `graph`) VALUES " + - "('DEFAULT', 'shared'), ('DEFAULT', 'shared')"); - statement.execute("INSERT INTO `gremlin_collection` " + - "(`conn_id`, `name`, `content`) VALUES " + - "(NULL, 'saved', 'first'), " + - "(NULL, 'saved', 'second'), " + - "(NULL, 'saved_2', 'reserved')"); - - DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); - migrator.migrate(conn); - migrator.migrate(conn); - - Set names = new HashSet<>(); - Set contents = new HashSet<>(); - try (ResultSet rows = statement.executeQuery( - "SELECT `conn_id`, `name`, `content` " + - "FROM `gremlin_collection`")) { - while (rows.next()) { - Assert.assertNull(rows.getObject(1)); - names.add(rows.getString(2)); - contents.add(rows.getString(3)); - } - } - Assert.assertEquals(3, names.size()); - Assert.assertTrue(names.contains("saved")); - Assert.assertTrue(names.contains("saved_2")); - Assert.assertTrue(names.stream().anyMatch( - name -> name.startsWith("saved_2_") && - !name.equals("saved_2"))); - Assert.assertEquals(3, contents.size()); - Assert.assertTrue(contents.contains("first")); - Assert.assertTrue(contents.contains("second")); - Assert.assertTrue(contents.contains("reserved")); - } - } - - @Test - public void testSchemaMigratorAddsMissingIndexesIdempotently() - throws Exception { - String url = "jdbc:h2:mem:legacy_schema_indexes;DB_CLOSE_DELAY=-1"; - try (Connection conn = DriverManager.getConnection(url); - Statement statement = conn.createStatement()) { - statement.execute("CREATE TABLE `execute_history` (" + - "`id` INT, `graphspace` VARCHAR(48), " + - "`graph` VARCHAR(48), `create_time` DATETIME)"); - statement.execute("CREATE TABLE `file_mapping` (" + - "`id` INT, `job_id` INT)"); - statement.execute("CREATE TABLE `load_task` (" + - "`id` INT, `job_id` INT, `options` TEXT)"); - statement.execute("CREATE TABLE `job_manager` (" + - "`id` INT, `graphspace` VARCHAR(48), " + - "`graph` VARCHAR(48), `create_time` DATETIME)"); - statement.execute("CREATE TABLE `async_task` (" + - "`id` INT, `graphspace` VARCHAR(48), " + - "`graph` VARCHAR(48))"); - - DatabaseSchemaMigrator migrator = new DatabaseSchemaMigrator(); - migrator.migrate(conn); - migrator.migrate(conn); - - this.assertIndexExists(conn, "EXECUTE_HISTORY", - "EXECUTE_HISTORY_GRAPH_CREATE_TIME"); - this.assertIndexExists(conn, "FILE_MAPPING", - "FILE_MAPPING_JOB_ID"); - this.assertIndexExists(conn, "LOAD_TASK", "LOAD_TASK_JOB_ID"); - this.assertIndexExists(conn, "JOB_MANAGER", - "JOB_MANAGER_GRAPH_CREATE_TIME"); - this.assertIndexExists(conn, "ASYNC_TASK", "ASYNC_TASK_GRAPH"); - } - } - - private void assertColumnExists(Connection conn, String table, - String column) throws Exception { - try (ResultSet columns = conn.getMetaData().getColumns( - null, null, table, column)) { - Assert.assertTrue(table + "." + column + " should exist", - columns.next()); - } - } - - private void assertIndexExists(Connection conn, String table, - String index) throws Exception { - try (ResultSet indexes = conn.getMetaData().getIndexInfo( - conn.getCatalog(), null, table, false, false)) { - while (indexes.next()) { - if (index.equalsIgnoreCase(indexes.getString("INDEX_NAME"))) { - return; + try (PreparedStatement insert = conn.prepareStatement( + "INSERT INTO gremlin_collection " + + "(graphspace, graph, name, type, content, create_time) " + + "VALUES ('DEFAULT', ?, 'query', ?, 'g.V()', CURRENT_TIMESTAMP)")) { + for (String graph : new String[]{"one", "two"}) { + for (String type : new String[]{"GREMLIN", "CYPHER"}) { + insert.setString(1, graph); + insert.setString(2, type); + Assert.assertEquals(1, insert.executeUpdate()); + } } + Assert.assertThrows(java.sql.SQLException.class, + insert::executeUpdate); } } - Assert.fail(table + "." + index + " should exist"); - } - - private Path legacySchemaPath() { - Path modulePath = Paths.get("src/test/resources/database/schema.sql"); - if (Files.exists(modulePath)) { - return modulePath; - } - return Paths.get("hugegraph-hubble/hubble-be/src/test/resources/" + - "database/schema.sql"); } private void insertDeepPath(Connection conn, String deepPath) @@ -428,28 +134,6 @@ private void insertDeepPath(Connection conn, String deepPath) } } - private void createLegacyFileMappingTable(Connection conn) throws Exception { - try (Statement statement = conn.createStatement()) { - statement.execute("CREATE TABLE `file_mapping` (" + - "`id` INT NOT NULL AUTO_INCREMENT, " + - "`graphspace` VARCHAR(48) NOT NULL, " + - "`graph` VARCHAR(48) NOT NULL, " + - "`job_id` INT NOT NULL DEFAULT 0, " + - "`name` VARCHAR(128) NOT NULL, " + - "`path` VARCHAR(256) NOT NULL, " + - "`total_lines` LONG NOT NULL, " + - "`total_size` LONG NOT NULL, " + - "`file_status` TINYINT NOT NULL DEFAULT 0, " + - "`file_setting` VARCHAR(65535) NOT NULL, " + - "`vertex_mappings` VARCHAR(65535) NOT NULL, " + - "`edge_mappings` VARCHAR(65535) NOT NULL, " + - "`load_parameter` VARCHAR(65535) NOT NULL, " + - "`create_time` DATETIME(6) NOT NULL, " + - "`update_time` DATETIME(6) NOT NULL, " + - "PRIMARY KEY (`id`))"); - } - } - private Path mainSchemaPath() { Path modulePath = Paths.get("src/main/resources/database/schema.sql"); if (Files.exists(modulePath)) { diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java index 05f0f8ff4..c13130e9b 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java @@ -28,7 +28,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.util.NestedServletException; +import jakarta.servlet.ServletException; import org.apache.hugegraph.controller.graphs.GraphsController; import org.apache.hugegraph.config.HugeConfig; @@ -125,7 +125,7 @@ public void testCanonicalClearGraphPropagatesDefaultGraphRejection() post("/api/v1.3/graphspaces/DEFAULT/graphs/graph_a/clear") .with(this.withClient)); org.junit.Assert.fail("Expected default graph rejection"); - } catch (NestedServletException e) { + } catch (ServletException e) { Assert.assertInstanceOf(ExternalException.class, e.getCause()); } @@ -140,7 +140,7 @@ public void testLegacyGetClearGraphRouteIsNotSuccessful() .param("clear_schema", "true")) .andExpect(status().is4xxClientError()); - Mockito.verifyNoMoreInteractions(this.graphsService); + Mockito.verifyNoInteractions(this.graphsService); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java index c56010fef..720871d13 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java @@ -41,6 +41,7 @@ import org.apache.hugegraph.exception.ServerException; import org.apache.hugegraph.options.HubbleOptions; import org.apache.hugegraph.service.graphs.GraphsService; +import org.apache.hugegraph.service.auth.AuthModeService; import org.apache.hugegraph.service.query.QueryService; import org.apache.hugegraph.structure.graph.Edge; import org.apache.hugegraph.structure.graph.Vertex; @@ -51,6 +52,7 @@ public class GraphsServiceDefaultTest { private HugeClient client; private GraphsManager graphs; private GraphsService service; + private AuthModeService authMode; @Before public void setup() { @@ -58,6 +60,9 @@ public void setup() { this.graphs = Mockito.mock(GraphsManager.class); Mockito.when(this.client.graphs()).thenReturn(this.graphs); this.service = new GraphsService(); + this.authMode = Mockito.mock(AuthModeService.class); + Mockito.when(this.authMode.enabled()).thenReturn(true); + ReflectionTestUtils.setField(this.service, "authModeService", this.authMode); } @Test @@ -175,6 +180,25 @@ public void testCreateStandaloneGraphIncludesRequiredFactory() { "\"rocksdb.wal_path\":\"rocksdb-data/wal_demo\"")); } + @Test + public void testAnonymousGraphFactoryDoesNotDependOnPdMode() { + Mockito.when(this.authMode.enabled()).thenReturn(false); + for (boolean pdEnabled : new boolean[]{false, true}) { + Mockito.reset(this.graphs); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(pdEnabled); + ReflectionTestUtils.setField(this.service, "config", config); + + this.service.create(this.client, "Demo", "demo", null); + + ArgumentCaptor configJson = ArgumentCaptor.forClass(String.class); + Mockito.verify(this.graphs).createGraph(Mockito.eq("demo"), configJson.capture()); + Assert.assertTrue(configJson.getValue().contains( + "\"gremlin.graph\":\"org.apache.hugegraph.HugeFactory\"")); + Assert.assertFalse(configJson.getValue().contains("HugeFactoryAuthProxy")); + } + } + @Test public void testCreateStandaloneGraphRejectsUnsafeName() { HugeConfig config = Mockito.mock(HugeConfig.class); @@ -316,7 +340,7 @@ public void testElementCountFallsBackToLiveSmallGraph() { Assert.assertEquals(7L, result.get("vertex")); Assert.assertEquals(6L, result.get("edge")); Assert.assertNotNull(result.get("date")); - Mockito.verifyNoMoreInteractions(query); + Mockito.verifyNoInteractions(query); } @Test @@ -346,7 +370,7 @@ public void testElementCountSkipsUnavailableSnapshotsInPdMode() { Assert.assertEquals(1L, result.get("edge")); Assert.assertNotNull(result.get("date")); Mockito.verify(graph, Mockito.never()).getEVCount(Mockito.anyString()); - Mockito.verifyNoMoreInteractions(query); + Mockito.verifyNoInteractions(query); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/H2StartupTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/H2StartupTest.java new file mode 100644 index 000000000..71140beeb --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/H2StartupTest.java @@ -0,0 +1,122 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.unit; + +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.sql.DataSource; + +import org.apache.hugegraph.config.H2DataSourceConfig; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSessionFactory; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.support.ResourcePropertySource; + +public class H2StartupTest { + + private ApplicationContextRunner runner(String url) { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + DataSourceAutoConfiguration.class, + SqlInitializationAutoConfiguration.class, + MybatisPlusAutoConfiguration.class)) + .withUserConfiguration(H2DataSourceConfig.class) + .withInitializer(context -> { + try { + context.getEnvironment().getPropertySources().addLast( + new ResourcePropertySource(new FileSystemResource( + "src/main/resources/application.properties"))); + } catch (java.io.IOException e) { + throw new IllegalStateException(e); + } + }) + .withPropertyValues("spring.datasource.url=" + url, + "spring.datasource.username=sa", + "spring.datasource.password=", + "spring.sql.init.mode=always", + "spring.sql.init.schema-locations=" + + "classpath:database/schema.sql"); + } + + @Test + public void testStartupAndRestartPreserveMetadata() throws Exception { + Path directory = Files.createTempDirectory("hubble-startup-"); + ApplicationContextRunner runner = this.runner( + "jdbc:h2:file:" + directory.resolve("metadata")); + try { + runner.run(context -> { + Assert.assertNull(context.getStartupFailure()); + JdbcTemplate jdbc = new JdbcTemplate(context.getBean(DataSource.class)); + jdbc.update("INSERT INTO user_info(username, locale) VALUES (?, ?)", + "test-user", "en"); + }); + runner.run(context -> { + Assert.assertNull(context.getStartupFailure()); + JdbcTemplate jdbc = new JdbcTemplate(context.getBean(DataSource.class)); + Assert.assertEquals("en", jdbc.queryForObject( + "SELECT locale FROM user_info WHERE username = ?", + String.class, "test-user")); + jdbc.update("UPDATE user_info SET locale = ? WHERE username = ?", + "zh", "test-user"); + Assert.assertEquals("zh", jdbc.queryForObject( + "SELECT locale FROM user_info WHERE username = ?", + String.class, "test-user")); + }); + } finally { + Files.deleteIfExists(directory.resolve("metadata.mv.db")); + Files.deleteIfExists(directory.resolve("metadata.trace.db")); + Files.delete(directory); + } + } + + @Test + public void testProductionMybatisSettingsAreApplied() { + this.runner("jdbc:h2:mem:mybatis-settings").run(context -> { + Assert.assertNull(context.getStartupFailure()); + org.apache.ibatis.session.Configuration configuration = + context.getBean(SqlSessionFactory.class).getConfiguration(); + Assert.assertFalse(configuration.isCacheEnabled()); + Assert.assertTrue(configuration.isMapUnderscoreToCamelCase()); + Assert.assertTrue(configuration.isUseGeneratedKeys()); + Assert.assertEquals(ExecutorType.REUSE, configuration.getDefaultExecutorType()); + Assert.assertEquals(Integer.valueOf(600), configuration.getDefaultStatementTimeout()); + }); + } + + @Test + public void testNonH2MetadataIsRejected() { + this.runner("jdbc:mysql://localhost/hubble").run(context -> { + Throwable failure = context.getStartupFailure(); + Assert.assertNotNull(failure); + while (failure.getCause() != null) { + failure = failure.getCause(); + } + Assert.assertTrue(failure.getMessage().contains("Hubble metadata requires H2")); + }); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JaxbRuntimeTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JaxbRuntimeTest.java new file mode 100644 index 000000000..fcec1e524 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JaxbRuntimeTest.java @@ -0,0 +1,59 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +package org.apache.hugegraph.unit; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Collections; + +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.annotation.XmlRootElement; + +import org.junit.Assert; +import org.junit.Test; + +public class JaxbRuntimeTest { + + @Test + public void testOneProviderCanRoundTripXml() throws Exception { + JAXBContext context = JAXBContext.newInstance(Record.class); + ClassLoader loader = context.getClass().getClassLoader(); + String providerClass = context.getClass().getName().replace('.', '/') + ".class"; + Assert.assertEquals(1, Collections.list(loader.getResources(providerClass)).size()); + Assert.assertEquals(1, Collections.list(loader.getResources( + "org/glassfish/jaxb/core/api/impl/NameConverter.class")).size()); + + Record source = new Record(); + source.name = "测试 & JAXB"; + source.value = 9007199254740993L; + StringWriter xml = new StringWriter(); + context.createMarshaller().marshal(source, xml); + Record restored = (Record) context.createUnmarshaller() + .unmarshal(new StringReader(xml.toString())); + Assert.assertEquals(source.name, restored.name); + Assert.assertEquals(source.value, restored.value); + } + + @XmlRootElement(name = "record") + public static class Record { + + public String name; + public long value; + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java index 6d06e4812..c2bb8d8a3 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/JobManagerServiceTest.java @@ -120,7 +120,7 @@ public void testRefreshStatusKeepsLoadingWhenAnyLoadTaskRuns() service.refreshStatus(job); Assert.assertEquals(JobStatus.LOADING, job.getJobStatus()); - Mockito.verify(mapper, Mockito.never()).updateById(Mockito.any()); + Mockito.verify(mapper, Mockito.never()).updateById(Mockito.any(JobManager.class)); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java index 79e190e6f..9c3901d08 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java @@ -48,7 +48,7 @@ public void testJobCreateRejectsMissingNameAsParameterError() { } catch (ExternalException expected) { // Expected: the controller must not leak a NullPointerException. } - Mockito.verifyNoMoreInteractions(service); + Mockito.verifyNoInteractions(service); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java index c17ef4524..09ee66756 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/MessageSourceHandlerTest.java @@ -23,7 +23,7 @@ import java.util.Locale; import java.util.Properties; -import javax.servlet.http.Cookie; +import jakarta.servlet.http.Cookie; import org.junit.After; import org.junit.Assert; diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java index 350607319..b23590c7b 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OltpAlgoControllerTest.java @@ -57,7 +57,7 @@ public void testShortestPathUsesGremlinCapableClient() throws Exception { Assert.assertEquals("DEFAULT", controller.graphSpace); Assert.assertEquals("hugegraph", controller.graph); Mockito.verify(service).shortestPath(gremlinClient, body); - Mockito.verifyNoMoreInteractions(tokenClient); + Mockito.verifyNoInteractions(tokenClient); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index 14c8b8214..07800995c 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -62,6 +62,9 @@ GraphServiceImportTest.class, GraphMetricsControllerTest.class, HugeClientPoolServiceTest.class, + H2StartupTest.class, + AvaticaRuntimeTest.class, + JaxbRuntimeTest.class, GraphSpaceControllerTest.class, GraphSpaceAuthMutationAuthorizationTest.class, GraphSpaceAuthOwnershipTest.class, diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java index 7a5bf392a..da5f763bd 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserApiContractTest.java @@ -28,7 +28,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.util.NestedServletException; +import jakarta.servlet.ServletException; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.controller.auth.UserController; @@ -109,7 +109,7 @@ public void testRetiredUserRoutesAreUnavailable() throws Exception { .andExpect(status().isMethodNotAllowed()); this.mvc.perform(delete("/api/v1.3/auth/users/super/alice")) .andExpect(status().isNotFound()); - Mockito.verifyNoMoreInteractions(this.userService); + Mockito.verifyNoInteractions(this.userService); } private void assertRequestCause(Class expected, @@ -118,7 +118,7 @@ private void assertRequestCause(Class expected, request.perform(); org.junit.Assert.fail("Expected request to throw " + expected.getName()); - } catch (NestedServletException e) { + } catch (ServletException e) { org.apache.hugegraph.testutil.Assert.assertInstanceOf(expected, e.getCause()); } diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java index 9d7faa33f..b9d21b561 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java @@ -105,7 +105,7 @@ public void testPdUserCreationKeepsNickname() { ArgumentCaptor request = ArgumentCaptor.forClass(User.class); Mockito.verify(this.auth).createUser(request.capture()); Assert.assertEquals("display-name", request.getValue().nickname()); - Mockito.verifyNoMoreInteractions(this.standalonePermissions); + Mockito.verifyNoInteractions(this.standalonePermissions); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/resources/application.properties b/hugegraph-hubble/hubble-be/src/test/resources/application.properties index 627585c3a..b23192347 100644 --- a/hugegraph-hubble/hubble-be/src/test/resources/application.properties +++ b/hugegraph-hubble/hubble-be/src/test/resources/application.properties @@ -23,8 +23,9 @@ spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:db spring.datasource.username=sa spring.datasource.password= -spring.datasource.schema=classpath:database/schema.sql -spring.datasource.data=classpath:database/data.sql +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:database/schema.sql +spring.sql.init.data-locations=classpath:database/data.sql spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=15 @@ -39,16 +40,20 @@ spring.messages.encoding=UTF-8 spring.messages.basename=i18n/messages spring.messages.fallback-to-system-locale=false -mybatis.type-aliases-package=org.apache.hugegraph.entity -mybatis-plus.type-enums-package=org.apache.hugegraph.entity.enums -mybatis.configuration.cache-enabled=false -mybatis.configuration.map-underscore-to-camel-case=true -mybatis.configuration.use-generated-keys=true -mybatis.configuration.default-executor-type=reuse -mybatis.configuration.default-statement-timeout=600 +mybatis-plus.type-aliases-package=org.apache.hugegraph.entity +mybatis-plus.configuration.cache-enabled=false +mybatis-plus.configuration.map-underscore-to-camel-case=true +mybatis-plus.configuration.use-generated-keys=true +mybatis-plus.configuration.default-executor-type=reuse +mybatis-plus.configuration.default-statement-timeout=600 logging.level.org.springframework=WARN logging.level.org.apache.hugegraph.mapper=DEBUG logging.level.org.apache.hugegraph.service=INFO -logging.file=logs/hugegraph-hubble.log -logging.file.max-size=10MB +logging.file.name=logs/hugegraph-hubble.log +logging.logback.rollingpolicy.max-file-size=10MB + +# ESService creates the configured ES 7 client lazily; do not create a default ES 8 client. +spring.autoconfigure.exclude=\ + org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchClientAutoConfiguration,\ + org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions b/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions index b071dca5c..d798d6d8b 100644 --- a/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions +++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions @@ -23,6 +23,13 @@ function java_env_check() { echo "cannot find java in environment." exit 1 fi + local java_version + java_version=$(java -version 2>&1 | sed -n 's/.*version "\([^"]*\)".*/\1/p' | head -1) + local java_major=${java_version%%.*} + if [[ ! "${java_major}" =~ ^[0-9]+$ ]] || [[ ${java_major} -lt 17 ]]; then + echo "Hubble requires Java 17 or newer; found ${java_version:-unknown}." >&2 + exit 1 + fi } # read a property from .properties file diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh index 6845fb213..d2c8f4648 100644 --- a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh +++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh @@ -51,7 +51,8 @@ for jar in "${LIB_PATH}"/*.jar; do class_path=${class_path}:${jar} done -JAVA_OPTS="-Xms512m -Dfile.encoding=UTF-8" +# Hive ORC interns URI strings through reflection on Java 17. +JAVA_OPTS="-Xms512m -Dfile.encoding=UTF-8 --add-opens=java.base/java.net=ALL-UNNAMED" JAVA_DEBUG_OPTS="" FOREGROUND="false" diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/archive-hubble.sh b/hugegraph-hubble/hubble-dist/assembly/travis/archive-hubble.sh new file mode 100644 index 000000000..ec3170424 --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/archive-hubble.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +set -euo pipefail + +# BSD tar otherwise synthesizes AppleDouble files from macOS extended attributes. +export COPYFILE_DISABLE=1 + +if [[ $# -ne 2 || ! -d "$1" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +release_dir=$1 +archive=$2 +release_name=$(basename "${release_dir}") +release_parent=$(cd "$(dirname "${release_dir}")" && pwd) +mkdir -p "$(dirname "${archive}")" +archive="$(cd "$(dirname "${archive}")" && pwd)/$(basename "${archive}")" + +# A release directory may have been run locally. Preserve its databases and +# other runtime files, but never include them in a distributable archive. +tar -czf "${archive}" \ + --exclude='._*' --exclude='*/._*' \ + --exclude="${release_name}/data" \ + --exclude="${release_name}/logs" \ + --exclude="${release_name}/upload-files" \ + --exclude='*.db' --exclude='*.lock' --exclude='*.log' \ + --exclude='*.pid' --exclude='*/pid' \ + -C "${release_parent}" "${release_name}" diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/test_archive_hubble.py b/hugegraph-hubble/hubble-dist/assembly/travis/test_archive_hubble.py new file mode 100644 index 000000000..70720fa3e --- /dev/null +++ b/hugegraph-hubble/hubble-dist/assembly/travis/test_archive_hubble.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +"""Exercise the actual archive script without building Hubble or deleting data.""" + +import pathlib +import subprocess +import sys +import tarfile +import tempfile +import unittest + + +ARCHIVER = pathlib.Path(__file__).with_name("archive-hubble.sh") + + +class HubbleArchiveTest(unittest.TestCase): + def test_runtime_data_is_preserved_and_excluded_from_image_input(self): + with tempfile.TemporaryDirectory(prefix="hubble-archive-") as directory: + root = pathlib.Path(directory) + release = root / "apache-hugegraph-hubble-1.8.0" + runtime_files = ( + "data/hubble-v2.mv.db", "data/hubble-v2.trace.db", + "db.mv.db", "db.trace.db", "logs/application.log", + "upload-files/private.csv", "hubble.pid", "bin/pid", + "metadata.lock", + ) + for relative in runtime_files: + path = release / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(("preserve:" + relative).encode()) + sentinels = {relative: (release / relative).read_bytes() + for relative in runtime_files} + (release / "lib").mkdir() + (release / "lib/hubble.jar").write_bytes(b"application") + sidecar = release / "lib/._hubble.jar" + sidecar.write_bytes(b"existing AppleDouble metadata") + (release / "ui").mkdir() + (release / "ui/index.html").write_text("Hubble") + archive = root / "target/hubble.tar.gz" + + for attempt in range(2): + subprocess.run(["bash", str(ARCHIVER), str(release), str(archive)], + check=True) + with tarfile.open(archive) as package: + names = package.getnames() + for relative in runtime_files: + self.assertNotIn(f"{release.name}/{relative}", names) + for runtime_dir in ("data", "logs", "upload-files"): + self.assertNotIn(f"{release.name}/{runtime_dir}", names) + self.assertIn(f"{release.name}/lib/hubble.jar", names) + self.assertIn(f"{release.name}/ui/index.html", names) + self.assertFalse(any(pathlib.PurePosixPath(name).name.startswith("._") + for name in names)) + + # Docker uses the archive in a fresh build-stage directory. + image_input = root / f"fresh-image-{attempt}" + image_input.mkdir() + subprocess.run(["tar", "-xzf", str(archive), + "--strip-components=1", "-C", str(image_input)], + check=True) + self.assertEqual(b"application", + (image_input / "lib/hubble.jar").read_bytes()) + for relative, original in sentinels.items(): + self.assertFalse((image_input / relative).exists()) + self.assertEqual(original, (release / relative).read_bytes()) + self.assertEqual(b"existing AppleDouble metadata", sidecar.read_bytes()) + + @unittest.skipUnless(sys.platform == "darwin", "requires macOS extended attributes") + def test_extended_attributes_do_not_generate_appledouble_members(self): + with tempfile.TemporaryDirectory(prefix="hubble-archive-xattr-") as directory: + root = pathlib.Path(directory) + release = root / "hubble" + release.mkdir() + payload = release / "application.jar" + payload.write_bytes(b"application") + attribute = "com.apple.metadata:hugegraph-archive-test" + subprocess.run(["xattr", "-w", attribute, "preserve attribute", str(payload)], + check=True) + archive = root / "hubble.tar.gz" + subprocess.run(["bash", str(ARCHIVER), str(release), str(archive)], check=True) + with tarfile.open(archive) as package: + self.assertEqual(["hubble", "hubble/application.jar"], package.getnames()) + self.assertEqual(b"application", + package.extractfile("hubble/application.jar").read()) + self.assertEqual(b"preserve attribute", subprocess.check_output( + ["xattr", "-p", attribute, str(payload)]).strip()) + + def test_missing_release_fails_without_creating_archive(self): + with tempfile.TemporaryDirectory(prefix="hubble-archive-") as directory: + root = pathlib.Path(directory) + archive = root / "hubble.tar.gz" + result = subprocess.run(["bash", str(ARCHIVER), str(root / "missing"), + str(archive)], capture_output=True) + self.assertNotEqual(0, result.returncode) + self.assertFalse(archive.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/hugegraph-hubble/hubble-dist/pom.xml b/hugegraph-hubble/hubble-dist/pom.xml index bdbfdace0..bc57a1c8f 100644 --- a/hugegraph-hubble/hubble-dist/pom.xml +++ b/hugegraph-hubble/hubble-dist/pom.xml @@ -149,19 +149,12 @@ cd ${top.level.dir} && pwd rm -rf ${final.name}/ui cp -r ${hubble-fe.dir}/build ${final.name}/ui - rm -rf ${final.name}/logs - rm -rf ${final.name}/upload-files - rm -f ${final.name}/db.mv.db - rm -f ${final.name}/db.trace.db - rm -f ${final.name}/*.pid - rm -f ${final.name}/bin/pid find ${final.name}/ui -name "*.map" -type f -delete - tar -zcvf ${top.level.dir}/target/${final.name}.tar.gz ${final.name} || exit 1 + bash ${assembly.dir}/travis/archive-hubble.sh \ + ${final.name} ${top.level.dir}/target/${final.name}.tar.gz || exit 1 bash ${assembly.dir}/travis/check-hubble-dist.sh \ ${top.level.dir}/target/${final.name}.tar.gz || exit 1 - rm -rf ./hubble-dist/${final.name} - cp -r ${final.name} ./hubble-dist echo -n "${final.name} tar.gz available at: " echo "${top.level.dir}/target/${final.name}.tar.gz" rm -f ${top.level.dir}/dist.sh diff --git a/hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/query-context.integration.test.js b/hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/query-context.integration.test.js new file mode 100644 index 000000000..22fa68747 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/GraphAnalysis/query-context.integration.test.js @@ -0,0 +1,166 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You 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. + */ + +import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {useLayoutEffect} from 'react'; +import {MemoryRouter, Route, Routes, useLocation} from 'react-router-dom'; +import GraphAnalysisHome from './index'; +import GraphContextSwitcher from '../../components/GraphContextSwitcher'; +import * as api from '../../api'; + +jest.mock('../../api', () => ({ + analysis: { + getGraphSpaceList: jest.fn(), + getGraphList: jest.fn(), + getOlapMode: jest.fn(), + getExecutionLogs: jest.fn(), + fetchFavoriteQueries: jest.fn(), + getGraphData: jest.fn(), + getExecutionQuery: jest.fn(), + }, + manage: { + getGraphList: jest.fn(), + getMetaEdgeList: jest.fn(), + getMetaVertexList: jest.fn(), + getMetaPropertyList: jest.fn(), + }, + auth: {getVermeer: jest.fn()}, +})); +jest.mock('../../utils/config', () => ({ + isPdEnabled: () => false, + isCypherEnabled: () => false, +})); +jest.mock('react-i18next', () => ({ + initReactI18next: {type: '3rdParty', init: jest.fn()}, + useTranslation: () => ({t: key => key}), +})); +jest.mock('../../components/CodeEditor', () => ({value, onExecutionShortcut}) => ( +
 {
+            if (event.ctrlKey && event.key === 'Enter') {
+                onExecutionShortcut?.();
+            }
+        }}
+    >{value}
+    
+)); +jest.mock('../analysis/QueryResult/Home', () => () =>
); +jest.mock('../analysis/LogsDetail/Home', () => () =>
); +jest.mock('../algorithm/Home', () => () =>
); +jest.mock('../asyncTasks/Home', () => () =>
); + +const Location = () => {useLocation().pathname}; +// Exercise the first committed route before TopBar's passive effects hydrate it. +const ExecuteOnNavigation = ({action}) => { + const {pathname} = useLocation(); + useLayoutEffect(() => { + if (!pathname.endsWith('/java17_legacy17_ui')) { + return; + } + if (action === 'button') { + screen.getByRole('button', {name: 'analysis.query.execute_query'}).click(); + } + else { + screen.getByTestId('query-shortcut').dispatchEvent(new KeyboardEvent('keydown', { + key: 'Enter', ctrlKey: true, bubbles: true, + })); + } + }, [action, pathname]); + return null; +}; +const graphs = [{name: 'hugegraph'}, {name: 'java17_legacy17_ui'}]; +const graphResponse = {status: 200, data: {graphs}}; +const records = {status: 200, data: {records: [], total: 0}}; + +it.each(['button', 'shortcut'])( + 'waits for dropdown route hydration before executing via %s', async action => { + jest.clearAllMocks(); + window.matchMedia = window.matchMedia || (() => ({ + matches: false, addListener: jest.fn(), removeListener: jest.fn(), + })); + localStorage.clear(); + api.auth.getVermeer.mockResolvedValue({status: 200, data: {enable: false}}); + api.analysis.getGraphSpaceList.mockResolvedValue({ + status: 200, data: {graphspaces: ['DEFAULT']}, + }); + let resolveGraphs; + api.analysis.getGraphList + .mockImplementationOnce(() => new Promise(resolve => { + resolveGraphs = resolve; + })) + .mockResolvedValue(graphResponse); + api.manage.getGraphList.mockResolvedValue({status: 200, data: {records: graphs}}); + api.analysis.getOlapMode.mockResolvedValue({status: 200, data: {status: '1'}}); + api.analysis.getExecutionLogs.mockResolvedValue(records); + api.analysis.fetchFavoriteQueries.mockResolvedValue(records); + api.analysis.getGraphData.mockResolvedValue({ + status: 200, data: {vertexcount: 8, edgecount: 6}, + }); + api.analysis.getExecutionQuery.mockResolvedValue({status: 200, data: {}}); + api.manage.getMetaEdgeList.mockResolvedValue(records); + api.manage.getMetaVertexList.mockResolvedValue(records); + api.manage.getMetaPropertyList.mockResolvedValue(records); + + render( + + + + + } + /> + + + + ); + await waitFor(() => expect(api.analysis.getGraphList).toHaveBeenCalled()); + const run = () => screen.getByRole('button', {name: 'analysis.query.execute_query'}); + expect(run()).toBeDisabled(); + fireEvent.click(run()); + expect(api.analysis.getExecutionQuery).not.toHaveBeenCalled(); + expect(api.analysis.getGraphData).not.toHaveBeenCalled(); + + await act(async () => { + resolveGraphs(graphResponse); + }); + await waitFor(() => expect(run()).toBeEnabled()); + fireEvent.mouseDown(screen.getByRole('combobox', {name: 'workbench.context.graph'})); + fireEvent.click(await screen.findByText('java17_legacy17_ui', { + selector: '.ant-select-item-option-content', + })); + await screen.findByText('/gremlin/DEFAULT/java17_legacy17_ui'); + expect(api.analysis.getExecutionQuery).not.toHaveBeenCalled(); + await waitFor(() => expect(api.analysis.getGraphData) + .toHaveBeenCalledWith('DEFAULT', 'java17_legacy17_ui')); + await waitFor(() => expect(run()).toBeEnabled()); + await act(async () => { + fireEvent.click(run()); + }); + expect(api.analysis.getExecutionQuery).toHaveBeenCalledWith( + 'DEFAULT', 'java17_legacy17_ui', 'g.V().limit(10)' + ); + for (const [space, graph] of api.analysis.getGraphData.mock.calls) { + expect(space).toBe('DEFAULT'); + expect(graphs.some(item => item.name === graph)).toBe(true); + } + }); diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js index 32b60df0b..49bd5f6ee 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js @@ -22,6 +22,7 @@ import React, {useState, useCallback, useEffect, useContext, useRef} from 'react'; import {useTranslation} from 'react-i18next'; +import {useParams} from 'react-router-dom'; import GraphAnalysisContext from '../../Context'; import QueryBar from '../QueryBar/Home'; import QueryResult from '../QueryResult/Home'; @@ -76,6 +77,10 @@ export const extractQueryErrorMessage = (error, fallback) => { const AnalysisHome = () => { const {t} = useTranslation(); const {graphSpace, graph} = useContext(GraphAnalysisContext); + const {graphSpace: routeGraphSpace, graph: routeGraph} = useParams(); + const isGraphReady = Boolean(graphSpace && graph) + && (!routeGraphSpace || routeGraphSpace === graphSpace) + && (!routeGraph || routeGraph === graph); const cypherEnabled = isCypherEnabled(); const [queryStatus, setQueryStatus] = useState(STANDBY); const [queryMessage, setQueryMessage] = useState(); @@ -455,7 +460,7 @@ const AnalysisHome = () => { const onExecute = useCallback( tabKey => { - if (tabKey !== GREMLIN && tabKey !== CYPHER) { + if (!isGraphReady || (tabKey !== GREMLIN && tabKey !== CYPHER)) { return; } if (executionInFlight.current) { @@ -473,7 +478,7 @@ const AnalysisHome = () => { executionInFlight.current = false; }); }, - [executeMode, onExecuteQuery, onExecuteTask] + [executeMode, isGraphReady, onExecuteQuery, onExecuteTask] ); useEffect( @@ -569,6 +574,7 @@ const AnalysisHome = () => { onExecute={onExecute} onRefresh={onFavoriteRefresh} isExecuting={queryStatus === LOADING} + isGraphReady={isGraphReady} cypherEnabled={cypherEnabled} /> {analysisMode !== TEXT2GQL && { expect(await screen.findByText('graph counts 0 0')).toBeInTheDocument(); }; +it.each([null, undefined])('waits for a graph before executing a query (%s)', async graph => { + api.analysis.getExecutionQuery.mockResolvedValue({status: 200, data: {}}); + const {rerender} = render( + + + + ); + + await act(async () => { + fireEvent.click(screen.getByRole('button', {name: 'Run current'})); + }); + expect(api.analysis.getExecutionQuery).not.toHaveBeenCalled(); + expect(api.analysis.getGraphData).not.toHaveBeenCalled(); + + rerender( + + + + ); + await waitForInitialData(); + await act(async () => { + fireEvent.click(screen.getByRole('button', {name: 'Run current'})); + }); + expect(api.analysis.getExecutionQuery).toHaveBeenCalledWith( + 'DEFAULT', 'ready_graph', 'g.V().limit(10)' + ); +}); + it('starts with a limited default only when no saved query exists', async () => { render( diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js index 0d29d28b5..d3aca2f47 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js @@ -175,6 +175,7 @@ const PrimaryActions = props => { onExecute, isEmptyQuery, isExecuting, + isGraphReady = true, } = props; const isQueryMode = executeMode === QUERY; const emptyDesc = t('analysis.query.empty_query'); @@ -186,11 +187,11 @@ const PrimaryActions = props => { const onExecution = useCallback( () => { - if (!isEmptyQuery && !isExecuting) { + if (isGraphReady && !isEmptyQuery && !isExecuting) { onExecute(activeTab); } }, - [activeTab, isEmptyQuery, isExecuting, onExecute] + [activeTab, isEmptyQuery, isExecuting, isGraphReady, onExecute] ); return ( @@ -226,7 +227,7 @@ const PrimaryActions = props => {