# crawler-lib-java
AI Summary
Purpose:
- Durable note for the Maven/Java package crawler at
~/labrador/crawler/crawler-lib-java. - Tracks the Python migration effort on branch
python-migration-maven.
Key points:
- Original implementation is Java (Spring/MyBatis). The Python migration replicates the exact column order and JSON encoding of every DB insert row so the two implementations are byte-compatible.
- Python source root:
src/ai/labradorlabs/inside the repo. Tests:tests/. venv:.venv/. Activate:. .venv/bin/activate. LANGUAGE='java',REPOSITORY='Maven'.- Key dependency files already in place (as of 2026-06-09):
sw/common.py(GetData, getNowDate),input/crawlerInfo.py(CrawlerInfo, LIBRARY_INFO),util/constants.py,db/base.py,db/cdbvdb.py. conftest.py addssrc/to sys.path for tests. - Row VO contract:
src/ai/labradorlabs/db/labradorDBVo.py—LabradorDBVoclass.
- getVersionVo(extracted) → 24-column VERSION_JAVA row (TB_COMP_LIB_VERSION_JAVA). JSON via jsonUtil.to_json (Jackson-exact: compact, ensure_ascii=False). ALWAYS-set columns (empty→"[]" / full-null dict): LICENSE, SCM, ISSUE_MANAGEMENT, ORGANIZATION, PARENT, ORIGINAL_INFO, DEPENDENCIES, DEPENDENCY_MANAGEMENT. CONDITIONAL (None when absent): STANDARD_LICENSE, LICENSE_IDS, LICENSE_AI. - getProductVo(version_row) → 9-column PRODUCT row (TB_COMP_LIB_PRODUCT). LICENSE = version_row's LICENSE (raw POM license JSON), matching Java product.setLicense(last.getLicense()) — NOT STANDARD_LICENSE (corrected P2b Task 1). - getMavenArchiveVo(record) → 9-column MAVEN_ARCHIVE row. ORIGINAL_INFO always json.dumps (never None). - getDependencyVo(version_row) → generator of DEPENDENCY rows. ID = sha256(PRODUCT_KEY + VERSION + origin_key + version_range). Yields nothing when DEPENDENCIES is None. - getCrawlerStatusVo(name, json_raw) → STATUS row (TYPE, RAW_DATA, TIMESTAMP).
MavenUrlUtil(P2a Task 4):src/ai/labradorlabs/util/mavenUrlUtil.py.groupIdToPath,getPomUrl,getArtifactSha1Url,getFallbackRepos. Java source-of-truth corrections: MavenCentral POM baseUrl =LINK_FILE(http://search.maven.org/remotecontent?filepath=); MAVEN_ATLASSIAN_E name="AtlassianExternal"baseUrl=https://packages.atlassian.com/mvn/maven-external/; MAVEN_CLOJARS name="Clojars". Fallback order: MavenCentral→Jenkins→Clojars→Cloudera→Eclipse→AtlassianExternal→MavenGoogle.MAVEN_REPOSlist added tocrawlerInfo.LIBRARY_INFO.- Test suite: 162 tests, all green as of 2026-06-10 (P4 Task 4).
BaseCrawler(Phase1 + P2d Task 1):src/ai/labradorlabs/crawler/baseCralwer.py. Routes inserts to correct maven tables viaCrawlerInfo.LIBRARY_INFOkeys. INSERT INTO … ON DUPLICATE KEY UPDATE viasetInsertIntoDB; INSERT IGNORE viasetInsertIgnoreDB.getLimitSelectsDBbuildsWHERE … ORDER BY PRODUCT_KEY LIMIT :start, 1000. Class-leveldb = LabradorDB()can be overridden per-instance for testing. No component/gatheringDBVo/versionSort — those are Phase2.
- P2d Task 1 work-list methods (all LANGUAGE='java', REPOSITORY='Maven'): - selectNotProcessedProducts(limit) → labradordb.TB_COMP_LIB_PRODUCT … ORDER BY PRODUCT_KEY LIMIT 0, :limit - selectNotProcessedArchives(repository_name, limit) → gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE … REPOSITORY_NAME=:repo AND PROCESSED IS NULL LIMIT 0, :limit - selectPriorityVersions(limit) / selectNotProcessedVersions(limit) → TB_COMP_LIB_VERSION_JAVA - selectVersionsOfProduct(product_key) → TB_COMP_LIB_VERSION_JAVA … ORDER BY FIXED_SORT_ORDER ASC, SORT_ORDER ASC, VERSION ASC - updateMavenArchiveProcessed(record) → UPDATE gatheringdb.TB_COMP_LIB_MAVEN_ARCHIVE SET PROCESSED = NOW() WHERE … - updateVersionSortOrder(row) → UPDATE TB_COMP_LIB_VERSION_JAVA SET SORT_ORDER = :so WHERE … - Test suite after P2d Task 1: 115/115 passed. Commit 44fb45a.
LicenseAnalysis(P2c Tasks 2-4):src/ai/labradorlabs/crawler/licenseAnalysis.py.convertLicenseNameToSpdxCode(name)→ regex + Levenshtein min-distance SPDX match (None if no regex match).convertToStandardLicense(licenses)→ dedup list with key order fixed at{name,url,id}; special cases: CDDL+GPL, APL+apache/2.0, CPE+GPL (no 3); " or " split; unmatched keeps original{name,url}; empty/None → None. LICENSE_IDS path (Task 4):normalizeLicenseName(lowercases, unifies license/licence, appliesapache-/mit-/bsd-/gpl-/lgpl-/agpl-licensepatterns,version X.Y→vX.Y; cleanup[^a-z0-9\s-]— strips.sov2.0→v20, faithful to Java LicenseAnalysisComponent:175 (the earlier.-preserving draft was reverted ina7c9d78));findSimilarLicenseId(Levenshtein ≤ 3 against all DB licenses, normalized both sides);extractLicenseIdFromUrl(apache/aws/spdx via MavenCrawlingUtil stubs, opensource.org last-path segment direct lookup);convertLicenseIds(3-step: normalize→DB lookup → URL → similarity);convertLicenseIdsWithName(direct DB lookup by raw name).MavenCrawlingUtilinjectable vialicenseAnalysis.mavenCrawlingUtil.LicenseDaoinjectable vialicenseAnalysis.licenseDao = FakeLicenseDao().MavenCrawlingUtil(P2c Task 4):src/ai/labradorlabs/util/mavenCrawlingUtil.py.convertApacheSiteByUrl,convertAwsSiteByUrl,convertSpdxSiteByUrl,getMavenCrawlingInfo— all return None (network stubs, integration-env only).LicenseeUtil(P2c Task 5):src/ai/labradorlabs/util/licenseeUtil.py.getAiLibraryLicense(license_datas, url=None)— mirrors dotnet/golanggetAiLibraryLicense. POSThttp://211.115.125.171/api/v1/licenses, body{"texts":[{"text":…}]}, 30s timeout. Normalises string/dict/None inputs. Response key lookup case-insensitive (AI_LICENSEthenai_license). Empty list → None. Non-200 → None. Output viato_json(compact). Stub methods (executeGitClone,getLicenseFromTag,getLicenseFileTextsFromClone,getTagFromVersion) deferred to Phase 2d Docker integration.AI_LICENSE_URLadded tocrawlerInfo.LIBRARY_INFO.LicenseResolver(P2c Task 6):src/ai/labradorlabs/crawler/licenseResolver.py. Wires the full license pipeline (mirrorsMavenPomCrawlerService:295-354). Output:{standardLicense, licenseIds, licenseAi}. Flow: (1) AI call over POM license NAMES first (empty names → skip → None); (2)convertToStandardLicense→ None → return early (ids also None); (3) ids from standard entries withidkey; fallback to per-namegetLicenseIdFromName; final fallbackconvertLicenseIds; [] → None.licenseAiuses AI API per user decision (Java used mvnrepository scrape). git-clone path is Phase 2d. Class-levelanalysis = LicenseAnalysis()andlicensee = LicenseeUtil()— both injectable as instance attrs for tests. Test suite: 108/108 passed.MavenCrawler(P3 Task 4):src/ai/labradorlabs/crawler/mavenCrawler.py. Ports JavaMavenCrawlerService.doCreateAllGAV+startCrawl.ingestRepo(repo, clean):clean=True→deleteMavenArchivesInRepofirst; streams viaIndexHelper.streamRecords; skipsclassifier != null; buildsgetMavenArchiveVorows; flushesinsertMavenArchiveevery BATCH=10000; writesinsertCrawlerStatusRowwith_msToDate(indexTimestamp); callsinsertAllProductsFromArchivefor MavenCentral only.startCrawl(): MavenCentral →clean=True; others →clean=False(incremental_lastUpdatedMillisviaselectCrawlerStatus). Per-repo exceptions isolated.indexHelperinjectable. Test suite: 144/144 passed (8 new tests). Commitc93f61c.app/main_index.py(P3 Task 5):src/ai/labradorlabs/app/main_index.py.main()instantiatesMavenCrawlerand callsstartCrawl(). Under__main__: runsmain()once then loopsschedule.every(1).days.do(main)/time.sleep(1). Mirrorsmain.py/main_resync.py. Test suite: 145/145 passed (1 new test). Commit051951d.GoogleMavenCrawler(P4 Task 4):src/ai/labradorlabs/crawler/googleMavenCrawler.py. Archive INTAKE only (POM crawl done byEtcMavenPomCrawlervia MavenGoogle archives).perform(): bootstrap (no STATUS) → fetch master-index + write STATUS RAW_DATA={groupId:null,…} TIMESTAMP=master Last-Modified ms. Non-bootstrap →updatedMasterIndex+updatedGroupIndex.updatedMasterIndex(): if master Last-Modified changed, merges new groupIds into existing RAW_DATA (preserves old timestamps) and upserts STATUS.updatedGroupIndex(): per groupId: known==null → collect all versions; known==ms → skip; else → diff new versions vsselectArchiveVersionsOfProduct. STATUS row built directly as{'TYPE':…,'RAW_DATA':to_json(group_map),'TIMESTAMP':str(status['timestamp'])}(bypassesgetCrawlerStatusVoto avoid TIMESTAMP=now override). REPO_NAME='MavenGoogle'. Test suite: 162/162 passed (6 new tests). Commitde6cd04.
Relevant when:
- Continuing the Python migration (Task 10+).
- Debugging row format mismatch between Python and Java outputs.
- Adding new VO methods.
Do not read full document unless:
- Exact column lists or method signatures are needed.
Linked documents:
ai/worklog/2026/2026-W24.mdai/workspace/repos.md
Open Questions
- Phase 2a COMPLETE (47 tests green incl. live Java-oracle sort check): comparableVersion.py (faithful Maven ComparableVersion port, differential-verified vs maven-artifact-3.6.2), versionSort.py (pure-Python sortVersionUsingMaven, no JPype runtime dep), mavenUrlUtil.py, getXmlRequests/getSha1Requests. Next: Phase 2b (component POM parsing).
- requirements.txt still pins JPype1 — now unused for sort (kept for Phase 3 index-helper jar). Remove if Phase 3 drops JPype.
- Phase 2b COMPLETE (78 tests green):
util/jsonUtil.py(Jackson-exact compact JSON:separators=(',',':'),ensure_ascii=False, empty list→[], always-full dicts) + Phase 1 VO byte-exactness correction (incl. PRODUCT.LICENSE = version LICENSE, not STANDARD_LICENSE);crawler/component.pyComponentGetter(POM lxml ns-strip,${}substitution port of TagNodeUtil, scm/issue/org/parent/name/desc/url/license extractors, properties+parent resolution, dependencies/DM/exclusions with exact per-method key order,extractPomFieldsassembler). Real commons-lang3 POM parses. Code-review fix: unresolved dependency${}version → null (Java parity; getDependencyManagement keeps literal). - Deferred POM edge cases (commented in code, low risk):
_replaceOnceunclosed-${restore vs Java drop;getNameparent-element${}fallback simplified. Revisit only if a real POM diverges. - Phase 2c COMPLETE (108 tests green, code-reviewed): LicenseDao (2 tables), LicenseAnalysis (SPDX match/standard/ids), LicenseeUtil (AI API client + Docker stubs), MavenCrawlingUtil (network stubs), LicenseResolver (wiring → standardLicense/licenseIds/licenseAi). Decisions: license map key order fixed
{name,url,id}(semantic eq); LICENSE_AI = AI API result (changed from Java mvnrepository scrape). - Phase 2c deferred to Docker/2d (documented stubs): git clone + licensee (
executeGitClone/getLicenseFromTag/getLicenseFileTextsFromClone/getTagFromVersion), mvnrepository scrape + apache/aws/spdx URL→name conversion. - Phase 2d COMPLETE (128 tests green, code-reviewed Approved):
crawler/mavenPomCrawler.pyMavenPomCrawler(BaseCrawler)—fetchPom(central+fallback, transient propagates),crawlVersion(POM→extractPomFields→resolveLicense→sha1→getVersionVo row; not-found→minimal row),createVersionFromArchive(SNAPSHOT skip, empty-file sha1→None, releaseDate=lastModified),rebuildProductAndSort(sort + product from highest version, LICENSE=version LICENSE),run()4-loop (products→central archives→priority versions→remaining),_drainno-progress break.app/main.py(schedule 5min) +app/main_resync.py. DECISION: unified on parent-resolution extraction (Phase 2b). Integration test parses real commons-lang3 POM → byte-exact row. - Phase 2d intentional deviations (improve omission-safety, reviewed sound): transient → keep retryable (Java marked processed=1 on any error → omission); no-progress break replaces Java's mark-on-error loop termination (stuck items stay processed=0, retried next run — never dropped).
- Phase 3 COMPLETE (incl. jar):
index-helper/standalone Maven module →maven-index-helper.jar(12.8MB shaded, built with mvnw 3.5.4 on JDK 25; fixes: ComponentsXmlResourceTransformer for plexus components.xml merge, runtime--add-opens java.base/java.lang[.reflect]=ALL-UNNAMEDfor indexer-core 6.0.0 CGLIB). Jar committed atsrc/ai/labradorlabs/util/jars/. Smoke-verified: Plexus init + Wagon + Maven Central index download (full first-run processing ~15-20min — live NDJSON record output NOT verified end-to-end; verify on server). Known: Clojars index fails on Lucene 32KB term limit (indexer-core 6.0.0 limitation, per-repo exception isolated). Python side: IndexHelper subprocess NDJSON, MavenCrawler ingestRepo/startCrawl, main_index. - Phase 4 COMPLETE (164 tests at gate):
getLastModified(HEAD Last-Modified→millis),selectVersionExists/selectArchiveVersionsOfProduct,EtcMavenPomCrawler(single-repo POM fetch viacrawlVersion(record, repo=)param, new-versions-only, central excluded),GoogleMavenCrawler(master/group-index.xml intake, STATUS rawData {gid:lastModified} map, Last-Modified diff; status persisted only when archives inserted — Java parity),app/main_etc.py+main_google.py. - Phase 5 COMPLETE (191 tests final, code-reviewed Approved): real
LicenseeUtil(git clone https no-token — Java's hardcoded ghp_ token NOT carried over; tag match incl. fixing Java'ssplit(".")regex bug; licensee CLI as argv list+cwd — de-shelled after review; license-file texts walk), git-clone license fallback wired intocrawlVersion(POM licenses empty + scm/url → clone → getLicenseFromTag → recompute; clone-no-license → STANDARD_LICENSE[]; null-POM path never clones), realMavenCrawlingUtil(mvnrepository License cell scrape + apache/aws/spdx URL converters, CSS selectors match Java jsoup), resync mode (run(mode='resync')ordered offset pagination over ALL archives, idempotent upsert; ORDER BY added vs Java's unordered resync select),Dockerfile.python(ubuntu22.04 + openjdk-11 + rbenv ruby/licensee /tools/licensee + python; PYTHONPATH=/workspace/src; mkdir /workspace/output; docker build untested — no docker in env) +.dockerignore. - Remaining for production (server-side verification): docker build + live run (index jar full NDJSON output, live DB writes, licensee/git/mvnrepository/dl.google.com/AI-API live calls), license SPDX accuracy vs license DB, abnormal mode (Java had a separate abnormal profile — not ported; decide if needed). First live run should target a staging DB (full index ingest DELETEs+reinserts the repo's archive rows).
Phase 2 Decisions (2026-06-09, user-confirmed)
- Version sort = pure Python reproducing Java
VersionSortUtil.sortVersionUsingMaven
(Apache ComparableVersion + preprocessing _→-, +→-, pr→rc; insertion-sort ascending, incl. the equal-version drop quirk). No JPype / sort-gen jar at runtime. The existing jar/Java is used only as a test oracle (differential check, skipped when no JVM).
- License AI API =
http://211.115.125.171/api/v1/licenses(POST), body
{"texts":[{"text": <str>}, ...]}, 30s timeout, no auth. Response {"NAMES":[{NAME,CONFIDENCE}], "AI_LICENSE":[ids]}; extract AI_LICENSE/ai_license (case-insensitive) → json.dumps(list) into LICENSE_AI (None if empty). Input priority: cloned license-file text → GitHub API license → POM <licenses> name. Independent of the TB_LICENSE SPDX match that fills STANDARD_LICENSE/LICENSE_IDS. Same client as crawler-lib-dotnet/golang (licenseeUtil.getAiLibraryLicense).
- DEPENDENCY table NOT used. Dependencies live only in the VERSION row
DEPENDENCIES
(and DEPENDENCY_MANAGEMENT) JSON columns. baseCralwer.insertDependency / VERSION_DEPENDENCY_TABLE stay dormant (not called).
Details
Branch
python-migration-maven (working branch for all Python migration work).
Run tests
cd ~/labrador/crawler/crawler-lib-java
. .venv/bin/activate
python -m pytest tests/ -vColumn order: VERSION_JAVA (TB_COMP_LIB_VERSION_JAVA)
LANGUAGE, REPOSITORY, PRODUCT_KEY, VERSION, NAME, DESCRIPTION, LICENSE, LICENSE_AI, STANDARD_LICENSE, LICENSE_IDS, URL, SCM, ISSUE_MANAGEMENT, ORGANIZATION, PARENT, ORIGINAL_INFO, SHA1_VALUE, CREATED, RELEASE_DATE, PROCESSED, SORT_ORDER, FIXED_SORT_ORDER, DEPENDENCIES, DEPENDENCY_MANAGEMENT
Column order: PRODUCT (TB_COMP_LIB_PRODUCT)
LANGUAGE, REPOSITORY, PRODUCT_KEY, LATEST_VERSION, NAME, DESCRIPTION, LICENSE, CREATED, PROCESSED
Column order: MAVEN_ARCHIVE (TB_COMP_LIB_MAVEN_ARCHIVE)
LANGUAGE, REPOSITORY_NAME, PRODUCT_KEY, VERSION, NAME, DESCRIPTION, SHA1_VALUE, ORIGINAL_INFO, LAST_MODIFIED
Column order: DEPENDENCY (TB_COMP_LIB_VERSION_JAVA_DEPENDENCY)
ID, LANGUAGE, REPOSITORY, PRODUCT_KEY, VERSION, ORIGIN_PRODUCT_KEY, ORIGIN_VERSION_RANGE, RECORD_CREATED
Column order: STATUS (TB_CRAWLER_STATUS)
TYPE, RAW_DATA, TIMESTAMP