Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821632586e
|
||
|
|
1da0afa587
|
||
|
|
b50515a8e8
|
||
|
|
401e7ca471
|
||
|
|
2fd63e734b
|
||
|
|
afa9166f9c
|
||
|
|
9c42b3d913
|
||
|
|
2cb2646fe1 | ||
|
|
006ae730d7
|
||
|
|
55edcf4070
|
||
|
|
5802fc4adc
|
||
|
|
6337bfe838 | ||
|
|
b5787bbe6b | ||
|
|
6ac6a0366c
|
||
|
|
ea506f6de6
|
||
|
|
5574af58ed
|
||
|
|
f0dbd484d6
|
||
|
|
d0052ea879 | ||
|
|
79f04c373d | ||
|
|
174de92ca5 | ||
|
|
d1243e4526 | ||
|
|
845c92bfe2 | ||
|
|
b253e9990e | ||
|
|
4f27fd1db3 | ||
|
|
20e38e05d3 | ||
|
|
68e85e6ed3
|
||
|
|
8a6f345cb5 | ||
|
|
4cf841dfea
|
||
|
|
811b079b8a
|
||
|
|
5f47b112ab
|
||
|
|
5bda16c6f2 | ||
|
|
a74dd21fa0
|
||
|
|
2d2eef54c4
|
||
|
|
ebb950f560 | ||
|
|
f7889fdf74 | ||
|
|
d9f902e956 | ||
|
|
242a9fd4a3 |
No files matched your search
@@ -0,0 +1 @@
|
||||
Central Java verification, tests and secret scan are supplied by the organization-scoped workflow.
|
||||
+10
-53
@@ -1,57 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.5.0
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
|
||||
test_only:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|main|release/.*|hotfix/.*|integration.*)/
|
||||
tags:
|
||||
only: []
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /release\/.*/
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
only: [ main ]
|
||||
tags:
|
||||
only: [ /v.*/ ]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
branch_type: hotfix
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /hotfix\/.*/
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
@@ -0,0 +1,7 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -0,0 +1 @@
|
||||
* @KofTwentyTwo
|
||||
@@ -0,0 +1,23 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -1,5 +1,6 @@
|
||||
# Maven
|
||||
target/
|
||||
.flattened-pom.xml
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: main
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: non-release
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
asciidoc: false
|
||||
release_completion: false
|
||||
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2026-11-06
|
||||
|
||||
Published as `com.kingsrook.qbits:qbit-geo-data:0.2.0` on the QRun-IO Gitea Maven
|
||||
registry. Requires Java 21 and QQQ 4.1 (built against `qqq-bom-pom` 4.1.0-RC.21).
|
||||
This is the first release that works on QQQ 4.1; 0.1.0 was built against QQQ 0.35.0.
|
||||
|
||||
### Changed
|
||||
- Build against the published QQQ 4.1 core (`qqq-bom-pom` 4.1.0-RC.21) instead of 0.35.0.
|
||||
- `GeoDataQBitProducer.VERSION` is `0.2.0`.
|
||||
- Import the project's `junit-bom` 5.10.0 ahead of the QQQ BOM so every JUnit artifact
|
||||
converges on 5.10.0 and Surefire discovers the tests.
|
||||
- CI runs on Gitea through the shared `KofTwentyTwo/ci-workflows` Maven workflow
|
||||
(`main` verifies; a `vX.Y.Z` tag publishes to the Gitea registry).
|
||||
|
||||
### Fixed
|
||||
- `Country`, `StateProvince` and `City` ask for their tables (`produceTableMetaData`)
|
||||
and expose the setters `QRecordEntity` needs to discover fields, so the producer
|
||||
creates `<prefix>_country`, `<prefix>_stateProvince` and `<prefix>_city` on core 4.1
|
||||
instead of zero tables. The producer tolerates a table without fields.
|
||||
- `GeoDataSyncStep` resolves the bundled `countryAlpha2` / `stateCode` source codes to
|
||||
the persisted `countryId` / `stateProvinceId` before matching, so states and cities
|
||||
are persisted (0.1.0 reported success after writing 250 countries and zero states or
|
||||
cities because those source columns do not exist in the schema).
|
||||
- Cities match on `stateProvinceId`, `name`, `latitude` and `longitude` at the schema's
|
||||
seven-decimal precision; city names are not unique within a subdivision.
|
||||
- Numeric comparisons use `BigDecimal` value equality, so a second run reports zero
|
||||
inserts, updates or deactivations.
|
||||
- An inactive existing row whose source entry returns is reactivated.
|
||||
- Insert and update results are checked; a row rejected by the backend fails the sync
|
||||
with a `QException` instead of reporting success.
|
||||
|
||||
## [0.1.0] - 2025-12-28
|
||||
|
||||
### Added
|
||||
- Multi-module structure (qbit-geo-data-core + tools)
|
||||
- Country, StateProvince, City entities with sync process
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# qbit-geo-data
|
||||
|
||||
## Knowledge base
|
||||
|
||||
Reviewed knowledge for this repo and the wider QQQ platform lives in the second-brain vault:
|
||||
|
||||
- Platform hub: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
|
||||
- This repo's dossier: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qbit-geo-data.md`
|
||||
(reviewed commit `f7889fdf7495` on `main`, 2026-07-04)
|
||||
- QBit mechanics refresher: `R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/architecture/metadata-model.md`
|
||||
|
||||
Key cautions from the review: entities produce PossibleValueSources only (`produceTableMetaData` defaults false — no tables are registered); the sync step's natural keys (`countryAlpha2`/`stateCode`) don't exist in the schema; the `geoDataSync` process name collides on multi-instance registration; pom/README still declare AGPL-3.0 while LICENSE/NOTICE are Apache-2.0; main and develop have diverged.
|
||||
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright
|
||||
owner or by an individual or Legal Entity authorized to submit on
|
||||
behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written
|
||||
communication sent to the Licensor or its representatives, including
|
||||
but not limited to communication on electronic mailing lists, source
|
||||
code control systems, and issue tracking systems that are managed by,
|
||||
or on behalf of, the Licensor for the purpose of discussing and
|
||||
improving the Work, but excluding communication that is conspicuously
|
||||
marked or otherwise designated in writing by the copyright owner as
|
||||
"Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022-2025 QRunIO, LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,15 @@
|
||||
qBit: Geo Data
|
||||
Copyright 2022-2025 QRunIO, LLC
|
||||
|
||||
This product includes software developed at QRunIO, LLC (https://qrun.io/).
|
||||
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
================================================================================
|
||||
|
||||
Trademarks:
|
||||
|
||||
QQQ, qBits, qRun, qStudio, and the QRunIO logo are trademarks of QRunIO, LLC.
|
||||
Use of these trademarks must comply with QRunIO's Trademark Guidelines.
|
||||
|
||||
================================================================================
|
||||
@@ -7,16 +7,33 @@ Geographic reference data QBit providing countries, states/provinces, and cities
|
||||
|
||||
## Quick Start
|
||||
|
||||
The current release is **0.2.0**. It requires Java 21 and QQQ 4.1 (built against
|
||||
`qqq-bom-pom` 4.1.0-RC.21). See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
Add the dependency to your `pom.xml`:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-geo-data-core</artifactId>
|
||||
<version>${qbit-geo-data.version}</version>
|
||||
<artifactId>qbit-geo-data</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Releases are published to the QRun-IO Gitea Maven registry, which requires
|
||||
authentication for reads as well as writes. Add the repository and a matching
|
||||
`<server>` entry (with your own Gitea username and token) in your private Maven
|
||||
`settings.xml`:
|
||||
|
||||
```xml
|
||||
<repository>
|
||||
<id>qrun-io</id>
|
||||
<url>https://git.kof22.com/api/packages/QRun-IO/maven</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>false</enabled></snapshots>
|
||||
</repository>
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
||||
This QBit provides three entities with pre-populated reference data:
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<revision>0.2.0</revision>
|
||||
<qqq.version>4.1.0-RC.21</qqq.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -58,11 +59,10 @@
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20231013</version>
|
||||
</dependency>
|
||||
<!-- One JUnit line. The QQQ 4.1 BOM inherits junit-jupiter-engine/params at 6.0.2 from qqq-parent-project, while this pom's JUnit BOM
|
||||
manages junit-jupiter, the API and the platform at 5.10.0; the mixed engine fails discovery ("TestEngine with ID 'junit-jupiter'
|
||||
failed to discover tests") and no test runs. Import this pom's own JUnit version first (first import wins) so every JUnit
|
||||
artifact converges on 5.10.0. -->
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
@@ -70,6 +70,19 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Published QQQ 4.1 core (minimum QQQ 4.1), imported ahead of the parent's BOM. -->
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>${qqq.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20231013</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
@@ -79,6 +92,35 @@
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Resolve the CI-friendly ${revision} in the installed and deployed POMs so -->
|
||||
<!-- consumers of the published artifacts can resolve the parent chain. -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>1.7.3</version>
|
||||
<configuration>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
<flattenMode>resolveCiFriendliesOnly</flattenMode>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten-clean</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
||||
+5
-1
@@ -28,7 +28,7 @@ public class GeoDataQBitProducer implements QBitProducer
|
||||
{
|
||||
public static final String GROUP_ID = "com.kingsrook.qbits";
|
||||
public static final String ARTIFACT_ID = "qbit-geo-data";
|
||||
public static final String VERSION = "0.1.0";
|
||||
public static final String VERSION = "0.2.0";
|
||||
|
||||
private GeoDataQBitConfig config;
|
||||
|
||||
@@ -126,6 +126,10 @@ public class GeoDataQBitProducer implements QBitProducer
|
||||
*******************************************************************************/
|
||||
private void updateForeignKeyReferences(QTableMetaData table)
|
||||
{
|
||||
if(table.getFields() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
table.getFields().values().forEach(field ->
|
||||
{
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
|
||||
@@ -14,7 +14,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
|
||||
|
||||
@QMetaDataProducingEntity(producePossibleValueSource = true)
|
||||
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
|
||||
public class City extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "city";
|
||||
@@ -204,4 +204,103 @@ public class City extends QRecordEntity
|
||||
{
|
||||
return modifyDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for stateProvinceId (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setStateProvinceId(Integer stateProvinceId)
|
||||
{
|
||||
this.stateProvinceId = stateProvinceId;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for asciiName (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setAsciiName(String asciiName)
|
||||
{
|
||||
this.asciiName = asciiName;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for population (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setPopulation(Integer population)
|
||||
{
|
||||
this.population = population;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for latitude (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setLatitude(BigDecimal latitude)
|
||||
{
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for longitude (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setLongitude(BigDecimal longitude)
|
||||
{
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for timezone (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setTimezone(String timezone)
|
||||
{
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
|
||||
|
||||
@QMetaDataProducingEntity(producePossibleValueSource = true)
|
||||
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
|
||||
public class Country extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "country";
|
||||
@@ -171,4 +171,85 @@ public class Country extends QRecordEntity
|
||||
{
|
||||
return modifyDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for alpha2Code (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setAlpha2Code(String alpha2Code)
|
||||
{
|
||||
this.alpha2Code = alpha2Code;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for alpha3Code (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setAlpha3Code(String alpha3Code)
|
||||
{
|
||||
this.alpha3Code = alpha3Code;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for numericCode (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setNumericCode(Integer numericCode)
|
||||
{
|
||||
this.numericCode = numericCode;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for officialName (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setOfficialName(String officialName)
|
||||
{
|
||||
this.officialName = officialName;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
}
|
||||
+73
-1
@@ -13,7 +13,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
|
||||
|
||||
@QMetaDataProducingEntity(producePossibleValueSource = true)
|
||||
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
|
||||
public class StateProvince extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "stateProvince";
|
||||
@@ -155,4 +155,76 @@ public class StateProvince extends QRecordEntity
|
||||
{
|
||||
return modifyDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setId(Integer id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for countryId (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setCountryId(Integer countryId)
|
||||
{
|
||||
this.countryId = countryId;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for code (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for subdivisionType (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setSubdivisionType(String subdivisionType)
|
||||
{
|
||||
this.subdivisionType = subdivisionType;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isActive (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setIsActive(Boolean isActive)
|
||||
{
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for createDate (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setCreateDate(Instant createDate)
|
||||
{
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for modifyDate (QRecordEntity discovers a field only from a getter and setter pair).
|
||||
*******************************************************************************/
|
||||
public void setModifyDate(Instant modifyDate)
|
||||
{
|
||||
this.modifyDate = modifyDate;
|
||||
}
|
||||
}
|
||||
+60
-13
@@ -4,13 +4,15 @@
|
||||
** Syncs all three entity types (Country, StateProvince, City) from bundled
|
||||
** JSON files into database tables. Performs natural key upsert:
|
||||
** - Country: by alpha2Code
|
||||
** - StateProvince: by countryAlpha2 + code
|
||||
** - City: by countryAlpha2 + stateCode + name
|
||||
** - StateProvince: by countryId + code (countryAlpha2 resolved to the persisted country)
|
||||
** - City: by stateProvinceId + name + latitude + longitude (codes resolved to the persisted state)
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.geodata.sync;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -62,9 +64,39 @@ public class GeoDataSyncStep implements BackendStep
|
||||
// Sync in order: countries first (no dependencies), then states, then //
|
||||
// cities (which may reference states) //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
int countriesInserted = syncTable(prefix + "_country", "/data/countries.json", List.of("alpha2Code"));
|
||||
int statesInserted = syncTable(prefix + "_stateProvince", "/data/states.json", List.of("countryAlpha2", "code"));
|
||||
int citiesInserted = syncTable(prefix + "_city", "/data/cities.json", List.of("countryAlpha2", "stateCode", "name"));
|
||||
int countriesInserted = syncTable(prefix + "_country", loadJsonData("/data/countries.json"), List.of("alpha2Code"));
|
||||
Map<String, QRecord> countries = queryExisting(prefix + "_country", List.of("alpha2Code"));
|
||||
List<QRecord> states = loadJsonData("/data/states.json");
|
||||
for(QRecord state : states)
|
||||
{
|
||||
QRecord country = countries.get(state.getValueString("countryAlpha2"));
|
||||
if(country == null)
|
||||
{
|
||||
throw new QException("Geo Data state references an unavailable country");
|
||||
}
|
||||
state.setValue("countryId", country.getValue("id"));
|
||||
state.getValues().remove("countryAlpha2");
|
||||
}
|
||||
int statesInserted = syncTable(prefix + "_stateProvince", states, List.of("countryId", "code"));
|
||||
Map<String, QRecord> persistedStates = queryExisting(prefix + "_stateProvince", List.of("countryId", "code"));
|
||||
List<QRecord> cities = loadJsonData("/data/cities.json");
|
||||
for(QRecord city : cities)
|
||||
{
|
||||
QRecord country = countries.get(city.getValueString("countryAlpha2"));
|
||||
QRecord state = country == null ? null : persistedStates.get(country.getValue("id") + "|" + city.getValueString("stateCode"));
|
||||
if(state == null)
|
||||
{
|
||||
throw new QException("Geo Data city references an unavailable state");
|
||||
}
|
||||
city.setValue("stateProvinceId", state.getValue("id"));
|
||||
for(String coordinate : List.of("latitude", "longitude"))
|
||||
{
|
||||
city.setValue(coordinate, new BigDecimal(city.getValue(coordinate).toString()).setScale(7, RoundingMode.HALF_UP));
|
||||
}
|
||||
city.getValues().remove("countryAlpha2");
|
||||
city.getValues().remove("stateCode");
|
||||
}
|
||||
int citiesInserted = syncTable(prefix + "_city", cities, List.of("stateProvinceId", "name", "latitude", "longitude"));
|
||||
|
||||
LOG.info("Geo data sync complete",
|
||||
logPair("prefix", prefix),
|
||||
@@ -83,17 +115,15 @@ public class GeoDataSyncStep implements BackendStep
|
||||
** Sync a single table from JSON resource.
|
||||
** Returns count of records processed.
|
||||
*******************************************************************************/
|
||||
private int syncTable(String tableName, String resourcePath, List<String> naturalKeyFields) throws QException
|
||||
private int syncTable(String tableName, List<QRecord> sourceRecords, List<String> naturalKeyFields) throws QException
|
||||
{
|
||||
LOG.info("Syncing table",
|
||||
logPair("table", tableName),
|
||||
logPair("resource", resourcePath),
|
||||
logPair("naturalKey", naturalKeyFields));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// 1. Load source data from classpath JSON //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> sourceRecords = loadJsonData(resourcePath);
|
||||
LOG.info("Loaded source data", logPair("count", sourceRecords.size()));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
@@ -119,7 +149,7 @@ public class GeoDataSyncStep implements BackendStep
|
||||
source.setValue("isActive", true);
|
||||
toInsert.add(source);
|
||||
}
|
||||
else if(hasChanges(source, existing))
|
||||
else if(hasChanges(source, existing) || !Boolean.TRUE.equals(existing.getValueBoolean("isActive")))
|
||||
{
|
||||
source.setValue("id", existing.getValue("id"));
|
||||
source.setValue("isActive", true);
|
||||
@@ -182,7 +212,7 @@ public class GeoDataSyncStep implements BackendStep
|
||||
key.append("|");
|
||||
}
|
||||
Object value = record.getValue(field);
|
||||
key.append(value != null ? value.toString() : "");
|
||||
key.append(value instanceof Number ? new BigDecimal(value.toString()).stripTrailingZeros().toPlainString() : value != null ? value.toString() : "");
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
@@ -265,7 +295,14 @@ public class GeoDataSyncStep implements BackendStep
|
||||
}
|
||||
Object sourceValue = source.getValue(fieldName);
|
||||
Object existingValue = existing.getValue(fieldName);
|
||||
if(!Objects.equals(sourceValue, existingValue))
|
||||
if(sourceValue instanceof Number && existingValue instanceof Number)
|
||||
{
|
||||
if(new BigDecimal(sourceValue.toString()).compareTo(new BigDecimal(existingValue.toString())) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if(!Objects.equals(sourceValue, existingValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -283,7 +320,7 @@ public class GeoDataSyncStep implements BackendStep
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(tableName);
|
||||
insertInput.setRecords(records);
|
||||
new InsertAction().execute(insertInput);
|
||||
requireSuccess(tableName, records.size(), new InsertAction().execute(insertInput).getRecords());
|
||||
}
|
||||
|
||||
|
||||
@@ -296,6 +333,16 @@ public class GeoDataSyncStep implements BackendStep
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(tableName);
|
||||
updateInput.setRecords(records);
|
||||
new UpdateAction().execute(updateInput);
|
||||
requireSuccess(tableName, records.size(), new UpdateAction().execute(updateInput).getRecords());
|
||||
}
|
||||
/*******************************************************************************
|
||||
** Do not report a successful sync when a native action rejected rows.
|
||||
*******************************************************************************/
|
||||
private void requireSuccess(String tableName, int expected, List<QRecord> records) throws QException
|
||||
{
|
||||
if(records == null || records.size() != expected || records.stream().anyMatch(record -> !record.getErrors().isEmpty()))
|
||||
{
|
||||
throw new QException("Geo Data persistence failed for table " + tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-1
@@ -78,6 +78,38 @@ class GeoDataQBitProducerTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The enabled entities produce their tables, on the configured backend, with the
|
||||
** prefix; the possible value sources point at tables that exist. (On core 4.1 an
|
||||
** entity annotation only produces a table when it asks for it.)
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testProduce_createsTablesAndPossibleValueSourcesPointAtThem() throws QException
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.addBackend(new QBackendMetaData().withName("rdbms"));
|
||||
|
||||
new GeoDataQBitProducer()
|
||||
.withConfig(new GeoDataQBitConfig().withBackendName("rdbms").withTableNamePrefix("geo")
|
||||
.withEnableCountries(true).withEnableStateProvinces(true).withEnableCities(true))
|
||||
.produce(qInstance, "geo");
|
||||
|
||||
assertThat(qInstance.getTables().keySet()).contains("geo_country", "geo_stateProvince", "geo_city");
|
||||
assertThat(qInstance.getTable("geo_city").getBackendName()).isEqualTo("rdbms");
|
||||
assertThat(qInstance.getTable("geo_country").getPrimaryKeyField()).isEqualTo("id");
|
||||
assertThat(qInstance.getTable("geo_country").getFields().keySet()).contains("alpha2Code", "name", "isActive");
|
||||
assertThat(qInstance.getTable("geo_city").getFields().keySet()).contains("name");
|
||||
for(var pvs : qInstance.getPossibleValueSources().values())
|
||||
{
|
||||
if(pvs.getTableName() != null)
|
||||
{
|
||||
assertThat(qInstance.getTable(pvs.getTableName())).as(pvs.getName()).isNotNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test that producer stores config in QBit metadata.
|
||||
*******************************************************************************/
|
||||
@@ -133,6 +165,6 @@ class GeoDataQBitProducerTest
|
||||
{
|
||||
assertThat(GeoDataQBitProducer.GROUP_ID).isEqualTo("com.kingsrook.qbits");
|
||||
assertThat(GeoDataQBitProducer.ARTIFACT_ID).isEqualTo("qbit-geo-data");
|
||||
assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.1.0");
|
||||
assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.2.0");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user