Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61618f3345 | ||
|
|
835c5add68
|
||
|
|
a9fa2bc932
|
||
|
|
d4b2b626ca
|
||
|
|
4f96071157
|
||
|
|
ad0c5da85a | ||
|
|
631c86b244
|
||
|
|
52c34a54e7
|
||
|
|
df8b615c09 | ||
|
|
6cb8e34c5d
|
||
|
|
4be0bfe7ba
|
||
|
|
0d4f6df850
|
||
|
|
8e7fba0d65 | ||
|
|
01b9705428
|
||
|
|
63f026dfc2
|
||
|
|
78ae9e0722
|
||
|
|
e7774e4dd0
|
||
|
|
1f7c3e63ba
|
||
|
|
514c42b3f8 |
No files matched your search
@@ -3,5 +3,5 @@ on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@main
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -1,6 +1,6 @@
|
||||
# QQQ Mobile Scanners
|
||||
|
||||
This is an application QBit and a separate local demo, using Java 21 and QQQ 4.1.0-RC.21. Optional `examples/echo` (scan-to-print) uses released Printing 1.0.0; its Generation, Record Files and Scripting owner inputs are not yet public. Contract versions: UI API 1.10, `mobile-scanners` 1.5, `printing` 2.3, `mobile-record-files` 1.0.
|
||||
This is an application QBit and a separate local demo, using Java 21 and QQQ 4.1.0-RC.21. Optional `examples/echo` (scan-to-print) uses released Printing 1.0.0; its Generation, Record Files and Scripting owner inputs are not yet public. Contract versions: UI API 1.10, `mobile-scanners` 1.6 (1.5 on the maintained local server), `printing` 2.3, `mobile-record-files` 1.0.
|
||||
The Android host is maintained in [QRun-IO/qqq-android on Gitea](https://git.kof22.com/QRun-IO/qqq-android) (reviews pulls/8 and pulls/10 are merged into `main`). Present status is in `docs/SESSION-STATE.md`; older logs are in `docs/history/`.
|
||||
|
||||
## Build and verify
|
||||
|
||||
@@ -52,7 +52,8 @@ Android host --(/qqq/v1 + mobile-scanners extension)--> QQQ server + this qbit -
|
||||
|
||||
## Project layout
|
||||
|
||||
- `src/main`, `src/test`: the reusable qbit (enrollment routes, approval/revocation, scan process, table metadata) and its H2 behavior and permission tests.
|
||||
- `pom.xml`: the `qbit-mobile-scanners-reactor` aggregator (packaging pom, single `<revision>` source) of the two published modules, `base` and `integrations/record-files`; `demo` and `examples/echo` are not part of it. One root `mvn deploy` publishes the reactor POM, `qbit-mobile-scanners` and `qbit-mobile-scanners-record-files` at the same version.
|
||||
- `base/src/main`, `base/src/test`: the reusable qbit (enrollment routes, approval/revocation, scan process, table metadata) and its H2 behavior and permission tests.
|
||||
- `demo`: basic local QQQ server with persistent H2 storage, native administration UI and password login. QQQ `4.0.0` lacks the password session exchange it needs.
|
||||
- `e2e`: browser acceptance tests against the packaged frontend and an isolated real demo server.
|
||||
- `examples/echo` and `examples/echo/demo`: the optional Echo library and its runnable composed server.
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-mobile-scanners-reactor</artifactId>
|
||||
<version>${revision}</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-mobile-scanners</artifactId>
|
||||
<version>${revision}</version>
|
||||
<name>QQQ Mobile Scanners</name>
|
||||
<description>Approved mobile devices and native QQQ barcode capture processes.</description>
|
||||
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners</url>
|
||||
<scm>
|
||||
<connection>scm:git:https://git.kof22.com/QRun-IO/qbit-mobile-scanners.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@git.kof22.com:22/QRun-IO/qbit-mobile-scanners.git</developerConnection>
|
||||
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners</url>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
<issueManagement>
|
||||
<system>Gitea</system>
|
||||
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners/issues</url>
|
||||
</issueManagement>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache License, Version 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>${qqq.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.2.224</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-postgres</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>1.21.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.14</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.11.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>14.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<configLocation>${maven.multiModuleProjectDirectory}/checkstyle/checkstyle.xml</configLocation>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
<failOnViolation>true</failOnViolation>
|
||||
<violationSeverity>warning</violationSeverity>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5
-3
@@ -69,11 +69,13 @@ final class MobileDeviceActivity
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Preserve last known coordinates with explicit availability and timestamps.
|
||||
** Preserve last known coordinates with explicit availability and timestamps,
|
||||
** and refresh reported software versions in the same presence write.
|
||||
***************************************************************************/
|
||||
static void heartbeat(String id, Map<String, Object> input, boolean foreground) throws QException
|
||||
static void heartbeat(QRecord device, Map<String, Object> input, boolean foreground) throws QException
|
||||
{
|
||||
QRecord changes = new QRecord().withValue("id", id).withValue("lastSeenAt", Instant.now());
|
||||
QRecord changes = new QRecord().withValue("id", device.getValueString("id")).withValue("lastSeenAt", Instant.now());
|
||||
MobileService.refreshDeviceInfo(device, input, changes);
|
||||
String status = MobileService.optional(input, "locationStatus", 30);
|
||||
if(status != null && !List.of("AVAILABLE", "PERMISSION_DENIED", "DISABLED", "UNAVAILABLE").contains(status))
|
||||
{
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -46,7 +46,7 @@ public final class MobileScannersUiApi
|
||||
***************************************************************************/
|
||||
public static MiddlewareSpecExtension extension()
|
||||
{
|
||||
return new MiddlewareSpecExtension().withName("mobile-scanners").withSemanticVersion("1.5.0")
|
||||
return new MiddlewareSpecExtension().withName("mobile-scanners").withSemanticVersion("1.6.0")
|
||||
.withDescription("Native scanner devices, customer branding and SCANNER process widget contract.")
|
||||
.withSpec(MobileBrandingSpec.class).withSpec(MobileStatusSpec.class)
|
||||
.withSpec(MobileEnrollSpec.class).withSpec(MobileCurrentDeviceSpec.class)
|
||||
File renamed without changes.
+22
-1
@@ -44,6 +44,7 @@ public final class MobileService
|
||||
{
|
||||
static final ThreadLocal<QBackendTransaction> TRANSACTION = new ThreadLocal<>();
|
||||
|
||||
static final List<String> DEVICE_INFO_FIELDS = List.of("manufacturer", "model", "osVersion", "appVersion");
|
||||
static final int AIM_IDENTIFIER_LENGTH = 3;
|
||||
static final int RAW_DATA_MAXIMUM_BYTES = 8192;
|
||||
static final int RAW_DATA_MAXIMUM_CHARACTERS = 10924;
|
||||
@@ -78,7 +79,8 @@ public final class MobileService
|
||||
QRecord credential = new QRecord().withValue("id", id).withValue("tokenHash", hash(token));
|
||||
QRecord device = new QRecord().withValue("id", id).withValue("installationId", id).withValue("status", "PENDING")
|
||||
.withValue("enrolledBy", userId()).withValue("enrolledAt", Instant.now());
|
||||
for(String name : new String[] { "displayName", "manufacturer", "model", "osVersion", "appVersion" })
|
||||
device.setValue("displayName", optional(input, "displayName", 200));
|
||||
for(String name : DEVICE_INFO_FIELDS)
|
||||
{
|
||||
device.setValue(name, optional(input, name, 200));
|
||||
}
|
||||
@@ -567,6 +569,25 @@ public final class MobileService
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** An updated app or OS reports new versions after enrollment; copy only
|
||||
** changed, bounded values so scans record the software that produced them.
|
||||
** Absent, null or blank values never erase what enrollment recorded.
|
||||
***************************************************************************/
|
||||
static void refreshDeviceInfo(QRecord device, Map<String, ?> input, QRecord changes) throws QException
|
||||
{
|
||||
for(String name : DEVICE_INFO_FIELDS)
|
||||
{
|
||||
String value = optional(input, name, 200);
|
||||
if(value != null && !value.isBlank() && !value.equals(device.getValueString(name)))
|
||||
{
|
||||
changes.setValue(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Enforce canonical UUIDs before using them as identifiers.
|
||||
***************************************************************************/
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -24,7 +24,7 @@ public class MobileSessionHeartbeatSpec extends MobileDeviceJsonSpec
|
||||
{
|
||||
return new BasicOperation().withHttpMethod(HttpMethod.POST).withPath(MobileScannersUiApi.PREFIX + "/sessions/heartbeat")
|
||||
.withOperationId("mobileScannersSessionHeartbeat").withShortSummary("Report current handheld session activity")
|
||||
.withLongDescription("Binds the current QQQ sign-in to the verified device. Nullable display metadata never grants permissions. Ended sign-ins return 401 and cannot start another mobile session. screenAvailable=false immediately removes the retained frame. Lease duration is expiresAt minus serverTime, not the device clock.");
|
||||
.withLongDescription("Binds the current QQQ sign-in to the verified device. Nullable display metadata never grants permissions. Ended sign-ins return 401 and cannot start another mobile session. screenAvailable=false immediately removes the retained frame. Lease duration is expiresAt minus serverTime, not the device clock. Optional manufacturer, model, osVersion and appVersion refresh the enrolled device record when they differ (1.6); scans recorded afterwards carry the refreshed values.");
|
||||
}
|
||||
|
||||
|
||||
File renamed without changes.
+5
-1
@@ -52,10 +52,14 @@ final class MobileSessionSchemas
|
||||
activity.put("traceId", nullableText(100));
|
||||
activity.put("locationStatus", text(30).withEnumValues(List.of("AVAILABLE", "PERMISSION_DENIED", "DISABLED", "UNAVAILABLE")));
|
||||
activity.put("location", ref("MobileScannersLocationV1").withNullable(true));
|
||||
for(String field : MobileService.DEVICE_INFO_FIELDS)
|
||||
{
|
||||
activity.put(field, text(200));
|
||||
}
|
||||
schemas.put("MobileScannersLocationV1", object("Optional foreground observation with runtime permission. Coordinates are last known, not a guarantee of current position; the server separately records receipt time.", Map.of(
|
||||
"latitude", new BoundedIntegerSchema(-90, 90).withType(Type.NUMBER), "longitude", new BoundedIntegerSchema(-180, 180).withType(Type.NUMBER),
|
||||
"accuracyMeters", new BoundedIntegerSchema(0, 10000000).withType(Type.NUMBER), "capturedAt", timestamp()), "latitude", "longitude", "accuracyMeters", "capturedAt"));
|
||||
schemas.put("MobileScannersHeartbeatRequestV1", object("Display activity only. Never include form values, barcodes, passwords or raw error messages. JSON maximum 4096 bytes. Omitted captureMode means APP_CONTENT, requiring foreground eligible app pixels. DEVICE_SCREEN requires explicit per-session Android operator consent and may remain available in background. False screenAvailable clears pixels and lease; changing mode invalidates old demand.", activity, "foreground", "screenAvailable"));
|
||||
schemas.put("MobileScannersHeartbeatRequestV1", object("Display activity only. Never include form values, barcodes, passwords or raw error messages. JSON maximum 4096 bytes. Omitted captureMode means APP_CONTENT, requiring foreground eligible app pixels. DEVICE_SCREEN requires explicit per-session Android operator consent and may remain available in background. False screenAvailable clears pixels and lease; changing mode invalidates old demand. Optional manufacturer, model, osVersion and appVersion (1.6) refresh the enrolled device record when they differ, so later scans carry the installed software; omitted or blank values leave stored values unchanged.", activity, "foreground", "screenAvailable"));
|
||||
schemas.put("MobileScannersScreenLeaseV1", object("Server-authorized capture demand. Compute remaining duration from expiresAt minus heartbeat serverTime. Sequence is scoped to this lease and resumes at nextSequence after client restart.", Map.of(
|
||||
"leaseId", uuid(), "expiresAt", timestamp(), "frameIntervalMillis", integer(1000, 1000), "maxWidth", integer(640, 640),
|
||||
"maxHeight", integer(1280, 1280), "maxBytes", integer(262144, 262144), "nextSequence", integer(0, 9007199254740990L), "captureMode", captureMode()),
|
||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -153,7 +153,7 @@ final class MobileSessionService
|
||||
String deviceId = currentDevice();
|
||||
synchronized(current())
|
||||
{
|
||||
MobileService.requireApproved(deviceId);
|
||||
QRecord device = MobileService.requireApproved(deviceId);
|
||||
QRecord session = observe(deviceId);
|
||||
boolean foreground = bool(input, "foreground");
|
||||
MobileCaptureMode mode = MobileCaptureMode.parse(input.get("captureMode"));
|
||||
@@ -173,7 +173,7 @@ final class MobileSessionService
|
||||
changes.setValue(name, MobileService.optional(input, name, 80));
|
||||
}
|
||||
changes.setValue("traceId", MobileService.optional(input, "traceId", 100));
|
||||
MobileDeviceActivity.heartbeat(deviceId, input, foreground);
|
||||
MobileDeviceActivity.heartbeat(device, input, foreground);
|
||||
update(changes);
|
||||
String id = session.getValueString("id");
|
||||
if(!available)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+20
-3
@@ -170,12 +170,18 @@ components:
|
||||
\ APP_CONTENT, requiring foreground eligible app pixels. DEVICE_SCREEN requires\
|
||||
\ explicit per-session Android operator consent and may remain available in\
|
||||
\ background. False screenAvailable clears pixels and lease; changing mode\
|
||||
\ invalidates old demand."
|
||||
\ invalidates old demand. Optional manufacturer, model, osVersion and appVersion\
|
||||
\ (1.6) refresh the enrolled device record when they differ, so later scans\
|
||||
\ carry the installed software; omitted or blank values leave stored values\
|
||||
\ unchanged."
|
||||
properties:
|
||||
appName:
|
||||
maxLength: 200
|
||||
nullable: true
|
||||
type: "string"
|
||||
appVersion:
|
||||
maxLength: 200
|
||||
type: "string"
|
||||
captureBlockedReason:
|
||||
maxLength: 80
|
||||
nullable: true
|
||||
@@ -207,6 +213,15 @@ components:
|
||||
- "UNAVAILABLE"
|
||||
maxLength: 30
|
||||
type: "string"
|
||||
manufacturer:
|
||||
maxLength: 200
|
||||
type: "string"
|
||||
model:
|
||||
maxLength: 200
|
||||
type: "string"
|
||||
osVersion:
|
||||
maxLength: 200
|
||||
type: "string"
|
||||
processInstanceId:
|
||||
maxLength: 200
|
||||
nullable: true
|
||||
@@ -686,7 +701,7 @@ info:
|
||||
description: "Native scanner devices, customer branding and SCANNER process widget\
|
||||
\ contract."
|
||||
title: "QQQ Middleware API extension: mobile-scanners"
|
||||
version: "1.5.0"
|
||||
version: "1.6.0"
|
||||
openapi: "3.0.3"
|
||||
paths:
|
||||
/qqq/v1/qbit/mobile-scanners/branding:
|
||||
@@ -967,7 +982,9 @@ paths:
|
||||
\ display metadata never grants permissions. Ended sign-ins return 401 and\
|
||||
\ cannot start another mobile session. screenAvailable=false immediately removes\
|
||||
\ the retained frame. Lease duration is expiresAt minus serverTime, not the\
|
||||
\ device clock."
|
||||
\ device clock. Optional manufacturer, model, osVersion and appVersion refresh\
|
||||
\ the enrolled device record when they differ (1.6); scans recorded afterwards\
|
||||
\ carry the refreshed values."
|
||||
operationId: "mobileScannersSessionHeartbeat"
|
||||
parameters:
|
||||
- description: "Enrolled installation UUID"
|
||||
File renamed without changes.
+13
-5
@@ -64,11 +64,19 @@ class MobileScannersApiContractTest
|
||||
assertTrue(model.getComponents().getSchemas().containsKey(schema), schema);
|
||||
}
|
||||
assertEquals(7, extension.getSpecs().size());
|
||||
assertEquals("1.5.0", extension.getSemanticVersion());
|
||||
assertEquals(java.util.List.of(), UiApiContract.check(generated, Files.readString(contract), Files.readString(Path.of("src/test/resources/openapi/mobile-scanners-1.4.0.yaml")), 1,
|
||||
"Regenerate the current extension contract."));
|
||||
assertEquals(java.util.List.of(), UiApiContract.check(generated, Files.readString(contract), Files.readString(Path.of("src/test/resources/openapi/mobile-scanners-1.5.0.yaml")), 1,
|
||||
"Regenerate the current extension contract."));
|
||||
assertEquals("1.6.0", extension.getSemanticVersion());
|
||||
for(String previous : java.util.List.of("1.4.0", "1.5.0", "1.6.0"))
|
||||
{
|
||||
assertEquals(java.util.List.of(), UiApiContract.check(generated, Files.readString(contract), Files.readString(Path.of("src/test/resources/openapi/mobile-scanners-" + previous + ".yaml")), 1,
|
||||
"Regenerate the current extension contract."));
|
||||
}
|
||||
var heartbeatRequest = model.getComponents().getSchemas().get("MobileScannersHeartbeatRequestV1");
|
||||
for(String optional : MobileService.DEVICE_INFO_FIELDS)
|
||||
{
|
||||
assertTrue(heartbeatRequest.getProperties().containsKey(optional), optional);
|
||||
assertEquals(200, heartbeatRequest.getProperties().get(optional).getMaxLength(), optional);
|
||||
assertFalse(generated.contains("- \"" + optional + "\""), optional + " must stay optional, never a required key.");
|
||||
}
|
||||
var scanEvent = model.getComponents().getSchemas().get("MobileScannersScanEventV1");
|
||||
var scanContext = model.getComponents().getSchemas().get("MobileScannersScanContextV1");
|
||||
for(String optional : java.util.List.of("aimIdentifier", "rawDataBase64"))
|
||||
+101
-1
@@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
@@ -942,6 +943,93 @@ class MobileScannersTest
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Enrollment records the installed software; heartbeats without versions,
|
||||
** or with blank ones, leave it unchanged and scans copy it.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void enrollmentStoresVersionsAndHeartbeatWithoutThemLeavesThemUnchanged() throws Exception
|
||||
{
|
||||
String device = approvedDevice(Map.of("osVersion", "14", "appVersion", "1.0.0"));
|
||||
QRecord enrolled = MobileService.get(MobileScannersQBitProducer.DEVICE, device);
|
||||
assertEquals("Zebra", enrolled.getValueString("manufacturer"));
|
||||
assertEquals("TC22", enrolled.getValueString("model"));
|
||||
assertEquals("14", enrolled.getValueString("osVersion"));
|
||||
assertEquals("1.0.0", enrolled.getValueString("appVersion"));
|
||||
bind(device);
|
||||
MobileSessionService.heartbeat(Map.of("foreground", true, "screenAvailable", false));
|
||||
MobileSessionService.heartbeat(new HashMap<>(Map.of("foreground", true, "screenAvailable", false, "appVersion", "", "osVersion", " ")));
|
||||
QRecord unchanged = MobileService.get(MobileScannersQBitProducer.DEVICE, device);
|
||||
for(String field : MobileService.DEVICE_INFO_FIELDS)
|
||||
{
|
||||
assertEquals(enrolled.getValueString(field), unchanged.getValueString(field), field);
|
||||
}
|
||||
QRecord scan = MobileService.capture(scan());
|
||||
assertEquals("1.0.0", scan.getValueString("appVersion"));
|
||||
assertEquals("14", scan.getValueString("osVersion"));
|
||||
assertEquals("Zebra", scan.getValueString("manufacturer"));
|
||||
assertEquals("TC22", scan.getValueString("model"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** An in-place app update reports its new version on the next heartbeat and
|
||||
** every later scan carries it; earlier scans keep the version that made them.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void heartbeatRefreshesDeviceVersionsForLaterScans() throws Exception
|
||||
{
|
||||
String device = approvedDevice(Map.of("osVersion", "14", "appVersion", "1.0.0"));
|
||||
bind(device);
|
||||
QRecord before = MobileService.capture(scan());
|
||||
assertEquals("1.0.0", before.getValueString("appVersion"));
|
||||
MobileSessionService.heartbeat(Map.of("foreground", true, "screenAvailable", false, "appVersion", "1.0.1", "osVersion", "15"));
|
||||
QRecord updated = MobileService.get(MobileScannersQBitProducer.DEVICE, device);
|
||||
assertEquals("1.0.1", updated.getValueString("appVersion"));
|
||||
assertEquals("15", updated.getValueString("osVersion"));
|
||||
assertEquals("Zebra", updated.getValueString("manufacturer"));
|
||||
assertEquals("TC22", updated.getValueString("model"));
|
||||
assertEquals("APPROVED", updated.getValueString("status"));
|
||||
QRecord after = MobileService.capture(scan());
|
||||
assertEquals("1.0.1", after.getValueString("appVersion"));
|
||||
assertEquals("15", after.getValueString("osVersion"));
|
||||
assertEquals("1.0.0", MobileService.get(MobileScannersQBitProducer.SCAN, before.getValueString("id")).getValueString("appVersion"));
|
||||
MobileSessionService.heartbeat(Map.of("foreground", true, "screenAvailable", false, "manufacturer", "Zebra Technologies", "model", "MC9400"));
|
||||
QRecord hardware = MobileService.get(MobileScannersQBitProducer.DEVICE, device);
|
||||
assertEquals("Zebra Technologies", hardware.getValueString("manufacturer"));
|
||||
assertEquals("MC9400", hardware.getValueString("model"));
|
||||
assertEquals("1.0.1", hardware.getValueString("appVersion"));
|
||||
assertEquals("MC9400", MobileService.capture(scan()).getValueString("model"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Heartbeat versions obey the enrollment limits: strings of at most 200
|
||||
** characters; anything else is a 400 and the device record is untouched.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void heartbeatRejectsInvalidDeviceVersionsLikeEnrollment() throws Exception
|
||||
{
|
||||
String tooLong = "v".repeat(201);
|
||||
String longest = "v".repeat(200);
|
||||
assertThrows(QBadRequestException.class, () -> MobileService.enroll(Map.of("installationId", UUID.randomUUID().toString(), "appVersion", tooLong)));
|
||||
String device = approvedDevice(Map.of("osVersion", "14", "appVersion", "1.0.0"));
|
||||
bind(device);
|
||||
assertThrows(QBadRequestException.class, () -> MobileSessionService.heartbeat(Map.of("foreground", true, "screenAvailable", false, "appVersion", tooLong)));
|
||||
assertThrows(QBadRequestException.class, () -> MobileSessionService.heartbeat(Map.of("foreground", true, "screenAvailable", false, "osVersion", 15)));
|
||||
QRecord unchanged = MobileService.get(MobileScannersQBitProducer.DEVICE, device);
|
||||
assertEquals("1.0.0", unchanged.getValueString("appVersion"));
|
||||
assertEquals("14", unchanged.getValueString("osVersion"));
|
||||
assertEquals("1.0.0", MobileService.capture(scan()).getValueString("appVersion"));
|
||||
MobileSessionService.heartbeat(Map.of("foreground", true, "screenAvailable", false, "appVersion", longest));
|
||||
assertEquals(longest, MobileService.get(MobileScannersQBitProducer.DEVICE, device).getValueString("appVersion"));
|
||||
assertEquals(longest, MobileService.capture(scan()).getValueString("appVersion"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A realistic installation, with no hardware identifier requirement.
|
||||
***************************************************************************/
|
||||
@@ -1163,7 +1251,19 @@ class MobileScannersTest
|
||||
***************************************************************************/
|
||||
private String approvedDevice() throws QException
|
||||
{
|
||||
String id = (String) enroll().get("deviceId");
|
||||
return approvedDevice(Map.of());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Approve an enrollment that also reported its software versions.
|
||||
***************************************************************************/
|
||||
private String approvedDevice(Map<String, Object> versions) throws QException
|
||||
{
|
||||
Map<String, Object> enrollment = new HashMap<>(Map.of("installationId", UUID.randomUUID().toString(), "displayName", "Test TC22", "manufacturer", "Zebra", "model", "TC22"));
|
||||
enrollment.putAll(versions);
|
||||
String id = (String) MobileService.enroll(enrollment).get("deviceId");
|
||||
QContext.init(instance, admin);
|
||||
MobileService.setApproval(id, true);
|
||||
QContext.init(instance, scanner);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File diff suppressed because it is too large.
Load diff
+3
-3
@@ -7,7 +7,7 @@
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<qqq.version>4.1.0-RC.21</qqq.version>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -34,7 +34,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.35</version>
|
||||
<version>1.0.0-RC.38</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
@@ -78,7 +78,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
<version>3.5.6</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled>
|
||||
|
||||
+1
-1
@@ -773,7 +773,7 @@ class MobileHttpAcceptanceTest
|
||||
}
|
||||
}
|
||||
assertNotNull(mobile);
|
||||
assertEquals("1.5.0", mobile.getString("version"));
|
||||
assertEquals("1.6.0", mobile.getString("version"));
|
||||
assertEquals("/qbit/mobile-scanners", mobile.getString("pathPrefix"));
|
||||
var openApiResponse = request("GET", "/qqq/v1/openapi.json", null, null, device);
|
||||
assertEquals(200, openApiResponse.statusCode());
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ Scans, sessions, bindings and Echo scans are **kept indefinitely** by default. T
|
||||
|
||||
## Database migration beyond H2
|
||||
|
||||
The supplied, tested scripts are H2: `src/main/resources/db/mobile-scanners-h2.sql` and the Echo `echo-h2.sql`. Other backends are unsupported and untested here; a host that uses one **owns the equivalent migration**; do not assume `ADD COLUMN IF NOT EXISTS` or the column types. Additive columns that older databases need before running current servers: on `mobileScan`, nullable `aimIdentifier` (`VARCHAR(3)`) and `rawDataBase64`; on Echo, nullable `echo_scan.aimIdentifier`, `rendererValue` and `humanReadable`, and **`echo_run.copies`, which is `INT NOT NULL DEFAULT 1`** (existing runs keep one copy), so it is not nullable and the equivalent migration must supply the default. Echo also has nullable template columns (`templateId`, `templateRevisionId`) in `echo-h2.sql`. Test the migration on a copy, and snapshot first.
|
||||
The supplied, tested scripts are H2: `base/src/main/resources/db/mobile-scanners-h2.sql` and the Echo `echo-h2.sql`. Other backends are unsupported and untested here; a host that uses one **owns the equivalent migration**; do not assume `ADD COLUMN IF NOT EXISTS` or the column types. Additive columns that older databases need before running current servers: on `mobileScan`, nullable `aimIdentifier` (`VARCHAR(3)`) and `rawDataBase64`; on Echo, nullable `echo_scan.aimIdentifier`, `rendererValue` and `humanReadable`, and **`echo_run.copies`, which is `INT NOT NULL DEFAULT 1`** (existing runs keep one copy), so it is not nullable and the equivalent migration must supply the default. Echo also has nullable template columns (`templateId`, `templateRevisionId`) in `echo-h2.sql`. Test the migration on a copy, and snapshot first.
|
||||
|
||||
## Demo-only file adapters
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ The [local demo](LOCAL-DEMO.md) source defaults remain QQQ `4.1.0-RC.21` and Pri
|
||||
- Isolated MC9400 native acceptance passed search/filter/paging/Back, grouped/related records, camera/gallery upload, parent-scoped saved-photo list, approved preview, rotation, pending-upload navigation protection and forced-logout cleanup. All requests matched the semantic API. Process-photo regression also passed.
|
||||
- The final date-only correction received unit/lint/build checks and actual TC22 verification. Actual-device screenshots confirm Actions → Add photo, dates/timezones, currency, related tabs, photo empty state and return to page two. Synthetic camera/gallery inputs in automated acceptance are not a new operator camera report.
|
||||
- Earlier 135/188/198/377-test cohorts and 15 browser E2E cases are historical evidence, not a fresh run against future public dependencies. The snapshot source comparison applies to its original revision only.
|
||||
- 2026-11-06, branch `fix/refresh-device-versions`: the MC9400 in-place 1.0.0 to 1.0.1 update left the device record and every later scan reporting `appVersion` 1.0.0, because only enrollment ever wrote `manufacturer`/`model`/`osVersion`/`appVersion` and no later client request carried them. Extension 1.6 adds the same optional fields to `MobileScannersHeartbeatRequestV1`; the heartbeat refreshes the device record when a value differs and scans copy the refreshed record (see [UI-API](UI-API.md)). The Android host change (send `osVersion`/`appVersion` on heartbeat) is on the same-named branch in the Android repository; devices need that client build before their versions refresh. Root module: 56 tests, zero Checkstyle violations, new baseline `mobile-scanners-1.6.0.yaml`.
|
||||
|
||||
## Handhelds and remaining limits
|
||||
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
# Mobile Scanners semantic UI API
|
||||
|
||||
Mobile Scanners is an independently versioned QQQ UI API extension. `MobileScannersQBitProducer` registers the `mobile-scanners` contract, currently **1.5.0** (core UI API 1.10 on the maintained local server), through `MiddlewareSpecExtensionsMetaData`. The framework generates its routes, discovery advertisement and merged OpenAPI from the same endpoint specs. Optional operator-consented device-screen viewing is described in [DEVICE-SCREEN-VIEWING.md](DEVICE-SCREEN-VIEWING.md); app-content remains the default. There is no separate `/mobile/v1` API or legacy alias.
|
||||
Mobile Scanners is an independently versioned QQQ UI API extension. `MobileScannersQBitProducer` registers the `mobile-scanners` contract, currently **1.6.0** (core UI API 1.10 on the maintained local server), through `MiddlewareSpecExtensionsMetaData`. The framework generates its routes, discovery advertisement and merged OpenAPI from the same endpoint specs. Optional operator-consented device-screen viewing is described in [DEVICE-SCREEN-VIEWING.md](DEVICE-SCREEN-VIEWING.md); app-content remains the default. There is no separate `/mobile/v1` API or legacy alias.
|
||||
|
||||
The host must also install `MobileScannersRouteProvider` on the same server. This provider binds verified device headers to the current request; it does not register additional business routes. Keep the versioned QQQ routes enabled. The qbit registers the `android` client channel while preserving the host's existing channel policies. The demo disables legacy unversioned middleware routes.
|
||||
|
||||
@@ -36,6 +36,8 @@ Public discovery, OpenAPI, branding and connectivity ignore stale device headers
|
||||
|
||||
Enrollment takes `MobileScannersEnrollRequestV1`: required canonical UUID `installationId`, optional `displayName`, `manufacturer`, `model`, `osVersion` and `appVersion` strings, each at most 200 characters. The JSON request body is limited to 4096 characters. A duplicate installation returns HTTP 409 and never rotates or discloses its existing credential. A new installation starts `PENDING` and receives its private `deviceToken` once.
|
||||
|
||||
Since extension 1.6, `MobileScannersHeartbeatRequestV1` also accepts the same optional `manufacturer`, `model`, `osVersion` and `appVersion` strings (at most 200 characters, 400 otherwise). An approved device's record is updated only when a sent value differs; omitted, null or blank values leave the enrolled values unchanged. Scan events copy the device record at save time, so an in-place app or OS update is reflected on the next heartbeat and every later scan, while earlier scans keep the versions that produced them. The Android host sends `osVersion` and `appVersion` on every heartbeat.
|
||||
|
||||
Send `X-QQQ-Device-Id` and `X-QQQ-Device-Token` on authenticated device/core metadata, widget and process requests to this same origin. The device ID is a UUID; the token is private and at most 128 characters. These headers supplement the authenticated user session. They never replace user permission checks. Never put a token in metadata, URLs, process values, screenshots, logs or fixtures; never forward credentials to a branding asset origin. Store the issued credential privately. Only its digest is stored server-side.
|
||||
|
||||
`devices/current` returns `deviceId`, `displayName` and `status`, never a token. `PENDING` and `REVOKED` devices may read their status. Scanner access still requires `APPROVED`, checked when launching and saving, including asynchronous/resumed execution. Device approval/revocation uses the ordinary QQQ processes, including single-record and bulk selections.
|
||||
@@ -81,7 +83,7 @@ Schema migration (H2, idempotent, nullable): `ALTER TABLE mobileScan ADD COLUMN
|
||||
|
||||
## Contract checks and compatibility
|
||||
|
||||
The published fragment is [mobile-scanners.yaml](../src/main/resources/openapi/qqq-v1/mobile-scanners.yaml); the initial release baseline is [mobile-scanners-1.0.0.yaml](../src/test/resources/openapi/mobile-scanners-1.0.0.yaml), with additional frozen baselines such as [mobile-scanners-1.4.0.yaml](../src/test/resources/openapi/mobile-scanners-1.4.0.yaml) (the contract immediately before the 1.5.0 capture metadata) and [mobile-scanners-1.5.0.yaml](../src/test/resources/openapi/mobile-scanners-1.5.0.yaml), the frozen 1.5.0 release snapshot that later changes are checked against. `MobileScannersApiContractTest` rejects generated-document drift, unresolved contract structure, incompatible baseline changes and missing version bumps using QQQ's shared contract checker. Object required fields and scanner numeric bounds are standard OpenAPI keywords, added by small Schema subclasses because RC20's base model does not expose those setters.
|
||||
The published fragment is [mobile-scanners.yaml](../base/src/main/resources/openapi/qqq-v1/mobile-scanners.yaml); the initial release baseline is [mobile-scanners-1.0.0.yaml](../base/src/test/resources/openapi/mobile-scanners-1.0.0.yaml), with additional frozen baselines such as [mobile-scanners-1.4.0.yaml](../base/src/test/resources/openapi/mobile-scanners-1.4.0.yaml) (the contract immediately before the 1.5.0 capture metadata) and [mobile-scanners-1.5.0.yaml](../base/src/test/resources/openapi/mobile-scanners-1.5.0.yaml), the frozen 1.5.0 release snapshot that later changes are checked against. `MobileScannersApiContractTest` rejects generated-document drift, unresolved contract structure, incompatible baseline changes and missing version bumps using QQQ's shared contract checker. Object required fields and scanner numeric bounds are standard OpenAPI keywords, added by small Schema subclasses because RC20's base model does not expose those setters.
|
||||
|
||||
For an intentional contract edit, update specs/schemas and the extension semantic version, then regenerate only the current fragment:
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<qqq.version>4.1.0-RC.21</qqq.version>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version>
|
||||
<mobile.version>0.1.0-SNAPSHOT</mobile.version>
|
||||
<echo.version>0.1.0-SNAPSHOT</echo.version>
|
||||
<generation.version>0.1.0-SNAPSHOT</generation.version>
|
||||
<generation.version>1.1.0-RC.1</generation.version>
|
||||
<renderer.version>${generation.version}</renderer.version>
|
||||
<printing.version>1.0.0</printing.version>
|
||||
<recordfiles.version>0.1.0-SNAPSHOT</recordfiles.version>
|
||||
<printing.version>1.2.0-RC.1</printing.version>
|
||||
<recordfiles.version>0.4.0-RC.1</recordfiles.version>
|
||||
<mobilerecordfiles.version>0.1.0-SNAPSHOT</mobilerecordfiles.version>
|
||||
<collaboration.version>0.1.0-record-files-SNAPSHOT</collaboration.version>
|
||||
<collaboration.version>1.2.0-RC.1</collaboration.version>
|
||||
</properties>
|
||||
<dependencyManagement><dependencies>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
|
||||
@@ -40,14 +40,14 @@
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners-record-files</artifactId><version>${mobilerecordfiles.version}</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-collaboration</artifactId><version>${collaboration.version}</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.35</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.38</version></dependency>
|
||||
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.2.224</version></dependency>
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>6.0.2</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>6.0.2</version><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build><plugins>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version><configuration><showWarnings>true</showWarnings><failOnWarning>true</failOnWarning><proc>none</proc><compilerArgs><arg>-Xlint:deprecation</arg><arg>-Xlint:unchecked</arg><arg>-Werror</arg></compilerArgs></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.8.1</version></plugin>
|
||||
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version><configuration><mainClass>com.kingsrook.examples.echo.demo.EchoDemo</mainClass></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version><dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies><configuration><configLocation>${project.basedir}/../../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration><executions><execution><phase>validate</phase><goals><goal>check</goal></goals></execution></executions></plugin>
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<qqq.version>4.1.0-RC.21</qqq.version>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version>
|
||||
<mobile.version>0.1.0-SNAPSHOT</mobile.version>
|
||||
<!-- Echo's copies per scan needs a Generation build with the print-copies change (appended copies on
|
||||
DocumentGenerationService.Request and GenerationPorts.PrintRequest, supportsCopies on the printing bridge,
|
||||
migration 20261102-print-copies-h2.sql). Printing 1.0.0 is released and qualified; only the Generation
|
||||
cohort still needs a coherent published input. Until that is published at this coordinate, override
|
||||
generation.version with a coherent build that has it; no private build is named here. -->
|
||||
<generation.version>0.1.0-SNAPSHOT</generation.version>
|
||||
<printing.version>1.0.0</printing.version>
|
||||
<generation.version>1.1.0-RC.1</generation.version>
|
||||
<printing.version>1.2.0-RC.1</printing.version>
|
||||
</properties>
|
||||
<dependencyManagement><dependencies>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
|
||||
@@ -37,14 +37,15 @@
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-document-generation-record-files</artifactId><version>${generation.version}</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId></dependency>
|
||||
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.2.224</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><version>42.7.8</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>6.0.2</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>6.0.2</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.35</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId><version>1.0.0-RC.38</version><scope>test</scope></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners-demo</artifactId><version>${mobile.version}</version><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build><resources><resource><directory>src/main/resources</directory><filtering>false</filtering></resource><resource><directory>${project.basedir}/../..</directory><filtering>false</filtering><includes><include>LICENSE</include><include>NOTICE</include></includes><targetPath>META-INF/qqq-echo</targetPath></resource></resources><plugins>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version><configuration><showWarnings>true</showWarnings><failOnWarning>true</failOnWarning><proc>none</proc><compilerArgs><arg>-Xlint:deprecation</arg><arg>-Xlint:unchecked</arg><arg>-Werror</arg></compilerArgs></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version><dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies><configuration><configLocation>${project.basedir}/../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration><executions><execution><phase>validate</phase><goals><goal>check</goal></goals></execution></executions></plugin>
|
||||
</plugins></build>
|
||||
</project>
|
||||
@@ -10,10 +10,14 @@ import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecke
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -35,7 +39,13 @@ public final class EchoPermissions implements CustomPermissionChecker
|
||||
{
|
||||
throw new QPermissionDeniedException("Echo access is required; direct state writes are not allowed.");
|
||||
}
|
||||
EchoConfig.current().identities.current();
|
||||
// Browsing the application performs no scan or print operation.
|
||||
// Process execution, widget and record access still require approved device authority.
|
||||
boolean processDiscovery = metadata instanceof QProcessMetaData && (action instanceof MetaDataInput || action instanceof ProcessMetaDataInput);
|
||||
if(!(metadata instanceof QAppMetaData) && !processDiscovery)
|
||||
{
|
||||
EchoConfig.current().identities.current();
|
||||
}
|
||||
}
|
||||
catch(Exception failure)
|
||||
{
|
||||
@@ -43,3 +53,4 @@ public final class EchoPermissions implements CustomPermissionChecker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -118,7 +118,7 @@ public final class EchoPrintingDestinations implements EchoDestinations
|
||||
String refusal = refusal(current.mode(), scan);
|
||||
if(refusal != null)
|
||||
{
|
||||
throw new QException(refusal);
|
||||
throw new EchoScanRejected(refusal);
|
||||
}
|
||||
return current.plan();
|
||||
}
|
||||
@@ -274,7 +274,8 @@ public final class EchoPrintingDestinations implements EchoDestinations
|
||||
for(Object item : scene.getJSONArray("elements"))
|
||||
{
|
||||
JSONObject text = (JSONObject) item;
|
||||
if("text".equals(text.optString("type")) && "/record/humanReadable".equals(text.optString("valuePath"))
|
||||
if(("text".equals(text.optString("type")) || "rasterText".equals(text.optString("type")))
|
||||
&& "/record/humanReadable".equals(text.optString("valuePath"))
|
||||
&& text.optInt("y", -1) > barcode.optInt("y", -1))
|
||||
{
|
||||
return true;
|
||||
@@ -293,15 +294,15 @@ public final class EchoPrintingDestinations implements EchoDestinations
|
||||
{
|
||||
if((mode == SceneMode.GS1) != "gs1-128".equals(scan.rendererSymbology()))
|
||||
{
|
||||
return "GS1-128 scans require the GS1-128 document; that document accepts only GS1-128. Leave Echo and choose the matching document; no print was requested.";
|
||||
return "GS1-128 scans require the GS1-128 document; that document accepts only GS1-128. No print was requested. Scan a barcode matching the selected document.";
|
||||
}
|
||||
if(mode == SceneMode.PLAIN && EchoTemplates.SCANNER_SYMBOLOGIES.contains(scan.rendererSymbology()))
|
||||
{
|
||||
return "EAN and UPC need the scanner-fidelity document. Leave Echo and choose it; no print was requested.";
|
||||
return "EAN and UPC need the scanner-fidelity document. No print was requested. Scan a barcode matching the selected document.";
|
||||
}
|
||||
if(mode == SceneMode.SCANNER && scan.aimIdentifier() == null)
|
||||
{
|
||||
return "The scanner-fidelity document needs the scanner's AIM identifier and this scan has none. Leave Echo and choose the plain document; no print was requested.";
|
||||
return "The scanner-fidelity document needs the scanner's AIM identifier and this scan has none. No print was requested. Scan a barcode with its AIM identifier.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -349,8 +350,7 @@ public final class EchoPrintingDestinations implements EchoDestinations
|
||||
for(Object item : new JSONArray(mediaProfilesJson == null ? "[]" : mediaProfilesJson))
|
||||
{
|
||||
PrintingMediaProfile profile = PrintingMediaProfile.parse((JSONObject) item);
|
||||
choices.add(new MediaChoice(profile.id(), profile.name() + " (" + String.format(Locale.ROOT, "%.1f x %.1f mm, %d dpi",
|
||||
profile.widthMicrons() / 1000.0, profile.heightMicrons() / 1000.0, profile.dpiX()) + ")"));
|
||||
choices.add(new MediaChoice(profile.id(), mediaLabel(profile)));
|
||||
}
|
||||
}
|
||||
catch(RuntimeException failure)
|
||||
@@ -362,6 +362,26 @@ public final class EchoPrintingDestinations implements EchoDestinations
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Prefer exact quarter-inch stock sizes; other stock retains metric units.
|
||||
** Keep meaningful printer names without repeating a dimensions-only name.
|
||||
***************************************************************************/
|
||||
private static String mediaLabel(PrintingMediaProfile profile)
|
||||
{
|
||||
boolean inches = profile.widthMicrons() % 6350 == 0 && profile.heightMicrons() % 6350 == 0;
|
||||
int divisor = inches ? 25400 : 1000;
|
||||
String width = java.math.BigDecimal.valueOf(profile.widthMicrons()).divide(java.math.BigDecimal.valueOf(divisor)).stripTrailingZeros().toPlainString();
|
||||
String height = java.math.BigDecimal.valueOf(profile.heightMicrons()).divide(java.math.BigDecimal.valueOf(divisor)).stripTrailingZeros().toPlainString();
|
||||
String resolution = profile.dpiX() == profile.dpiY() ? String.valueOf(profile.dpiX()) : profile.dpiX() + " × " + profile.dpiY();
|
||||
String name = profile.name().strip();
|
||||
String remainder = name.toLowerCase(Locale.ROOT).replaceAll("\\b(inches|inch|in|mm|label|paper|dpi)\\b", "")
|
||||
.replaceAll("[\\s0-9.,x×()·-]+", "");
|
||||
boolean generic = remainder.isBlank() || List.of("custom", "default", "media", "userdefined", "windowsqueuedefault").contains(remainder);
|
||||
return (generic ? "" : name + " · ") + width + " × " + height + (inches ? " in label · " : " mm label · ") + resolution + " dpi";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Select one reported media profile by identity; never guess dimensions.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueRangeBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
@@ -87,7 +88,7 @@ public final class EchoQBitProducer
|
||||
instance.addTable(scan);
|
||||
instance.addWidget(new QWidgetMetaData().withName(WIDGET).withType("composite").withLabel("Echo barcode scanner")
|
||||
.withCodeReference(new QCodeReference(EchoWidget.class)).withPermissionRules(rules()));
|
||||
instance.addProcess(new QProcessMetaData().withName(PROCESS).withLabel("Echo — scan to print").withPermissionRules(rules())
|
||||
instance.addProcess(new QProcessMetaData().withName(PROCESS).withLabel("Echo Printing").withIcon(new QIcon().withName("account_tree")).withPermissionRules(rules())
|
||||
.withCancelStep(new QBackendStepMetaData().withName("cancel").withCode(new QCodeReference(EchoSteps.Cancel.class)))
|
||||
.withStep(new QBackendStepMetaData().withName("begin").withCode(new QCodeReference(EchoSteps.Begin.class)))
|
||||
.withStep(new QFrontendStepMetaData().withName("setup").withLabel("Choose printer, paper or label, and document")
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Kingsrook, LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.kingsrook.examples.echo;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Only deterministic pre-print barcode/document incompatibility uses this type.
|
||||
*******************************************************************************/
|
||||
final class EchoScanRejected extends QException
|
||||
{
|
||||
/***************************************************************************
|
||||
** The message contains guidance, never a scanned payload.
|
||||
***************************************************************************/
|
||||
EchoScanRejected(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -175,12 +175,21 @@ public final class EchoService
|
||||
{
|
||||
requireText(runId, 36);
|
||||
MobileDeviceAuthorization.Identity identity = identity();
|
||||
Scan scan = Scan.from(context);
|
||||
if(!identity.actorId().equals(context.get("userId")) || !identity.deviceId().equals(context.get("deviceId"))
|
||||
if(context == null || !identity.actorId().equals(context.get("userId")) || !identity.deviceId().equals(context.get("deviceId"))
|
||||
|| !EchoQBitProducer.APP.equals(context.get("appId")) || !EchoQBitProducer.PROCESS.equals(context.get("processName")))
|
||||
{
|
||||
throw new QException("Scan context does not match this approved scanner sign-in.");
|
||||
}
|
||||
Scan scan;
|
||||
try
|
||||
{
|
||||
scan = Scan.from(context);
|
||||
}
|
||||
catch(EchoScanRejected rejection)
|
||||
{
|
||||
store.requireUnadmitted(runId, identity, requireText(context.get("scanId"), 64), claimedCopies);
|
||||
throw rejection;
|
||||
}
|
||||
EchoStore.Admission admission = store.claim(runId, identity, scan, claimedCopies);
|
||||
if(admission.acceptedJob() != null)
|
||||
{
|
||||
@@ -215,7 +224,16 @@ public final class EchoService
|
||||
{
|
||||
throw new QException("This Echo run has no chosen document. Leave Echo and choose again.");
|
||||
}
|
||||
EchoDestinations.Plan plan = destinations.plan(identity, destination, admission.document(), scan);
|
||||
EchoDestinations.Plan plan;
|
||||
try
|
||||
{
|
||||
plan = destinations.plan(identity, destination, admission.document(), scan);
|
||||
}
|
||||
catch(EchoScanRejected rejection)
|
||||
{
|
||||
store.reject(runId, admission.scanId(), scan.eventId());
|
||||
throw rejection;
|
||||
}
|
||||
if(plan == null || plan.templateId() == null || plan.templateId() < 1 || plan.templateRevisionId() == null || plan.templateRevisionId() < 1
|
||||
|| plan.format() == null || !stockMatches(plan.stock(), destination))
|
||||
{
|
||||
@@ -375,7 +393,7 @@ public final class EchoService
|
||||
case "EAN8", "LABEL-TYPE-EAN8" -> "ean8";
|
||||
case "UPCA", "LABEL-TYPE-UPCA" -> "upca";
|
||||
case "UPCE0", "LABEL-TYPE-UPCE0" -> "upce";
|
||||
default -> throw new QException("This barcode type cannot yet be reproduced faithfully by Echo. No print was requested.");
|
||||
default -> throw new EchoScanRejected("This barcode type cannot yet be reproduced faithfully by Echo. No print was requested.");
|
||||
};
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// the qualified first-physical profile is printable ASCII 0x20..0x7E for all types; //
|
||||
@@ -385,11 +403,18 @@ public final class EchoService
|
||||
if(gs1)
|
||||
{
|
||||
renderer = "gs1-128";
|
||||
EchoGs1.parse(value);
|
||||
try
|
||||
{
|
||||
EchoGs1.parse(value);
|
||||
}
|
||||
catch(QException invalidGs1)
|
||||
{
|
||||
throw new EchoScanRejected(invalidGs1.getMessage());
|
||||
}
|
||||
}
|
||||
else if("gs1-128".equals(renderer) || value.chars().anyMatch(character -> character < 0x20 || character > 0x7E))
|
||||
{
|
||||
throw new QException("This scan needs control/binary/GS1 or non-ASCII payload handling that Echo has not qualified; only printable ASCII is supported. No print was requested.");
|
||||
throw new EchoScanRejected("This scan needs control/binary/GS1 or non-ASCII payload handling that Echo has not qualified; only printable ASCII is supported. No print was requested.");
|
||||
}
|
||||
String aim = requireFidelityMetadata(context, renderer, value);
|
||||
return new Scan(event, value, type, renderer, source, captured, mobileScan, received, aim);
|
||||
@@ -409,23 +434,23 @@ public final class EchoService
|
||||
boolean gs1 = "gs1-128".equals(renderer);
|
||||
if(aim != null && !(gs1 ? "]C1" : PLAIN_AIM.get(renderer)).equals(aim))
|
||||
{
|
||||
throw new QException("This scan reports a barcode mode (GS1, ECI, structured or full-ASCII) that Echo cannot reproduce faithfully. No print was requested.");
|
||||
throw new EchoScanRejected("This scan reports a barcode mode (GS1, ECI, structured or full-ASCII) that Echo cannot reproduce faithfully. No print was requested.");
|
||||
}
|
||||
if(EchoTemplates.SCANNER_SYMBOLOGIES.contains(renderer))
|
||||
{
|
||||
if(aim == null)
|
||||
{
|
||||
throw new QException("EAN and UPC need the scanner's AIM identifier to be reproduced faithfully, and this scan has none. No print was requested.");
|
||||
throw new EchoScanRejected("EAN and UPC need the scanner's AIM identifier to be reproduced faithfully, and this scan has none. No print was requested.");
|
||||
}
|
||||
if(!validRetailValue(renderer, value))
|
||||
{
|
||||
throw new QException("This EAN or UPC value has the wrong length, number system or check digit. No print was requested.");
|
||||
throw new EchoScanRejected("This EAN or UPC value has the wrong length, number system or check digit. No print was requested.");
|
||||
}
|
||||
}
|
||||
Object raw = context.get("rawDataBase64");
|
||||
if(gs1 && raw == null)
|
||||
{
|
||||
throw new QException("GS1-128 duplication needs the scanner's original raw bytes and AIM identifier. No print was requested.");
|
||||
throw new EchoScanRejected("GS1-128 duplication needs the scanner's original raw bytes and AIM identifier. No print was requested.");
|
||||
}
|
||||
if(raw != null)
|
||||
{
|
||||
|
||||
@@ -93,12 +93,16 @@ public final class EchoSteps
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
String run = null;
|
||||
java.util.Map<String, java.io.Serializable> context = null;
|
||||
try
|
||||
{
|
||||
run = boundRun(input, output);
|
||||
Integer claimed = copies(input.getValue("copies"));
|
||||
var context = MobileScannerActions.capture(input, EchoQBitProducer.APP);
|
||||
context = MobileScannerActions.capture(input, EchoQBitProducer.APP);
|
||||
EchoService.Acceptance accepted = EchoConfig.current().service.print(run, context, claimed);
|
||||
output.addValue("scanRejected", false);
|
||||
output.addValue("rejectedEventId", null);
|
||||
output.addValue("rejectionMessage", null);
|
||||
output.addValue("scanContext", new HashMap<>(context));
|
||||
output.addValue("scanId", context.get("scanId"));
|
||||
output.addValue("echoScanId", accepted.scanId());
|
||||
@@ -110,6 +114,19 @@ public final class EchoSteps
|
||||
output.addValue(EchoQBitProducer.WIDGET, new EchoWidget.Data(accepted.copies()));
|
||||
output.getProcessState().setNextStepName("scan");
|
||||
}
|
||||
catch(EchoScanRejected rejection)
|
||||
{
|
||||
output.addValue("scanRejected", true);
|
||||
output.addValue("rejectedEventId", context.get("eventId"));
|
||||
output.addValue("rejectionMessage", rejection.getMessage());
|
||||
output.addValue("scanSaved", false);
|
||||
for(String name : java.util.List.of("acknowledgedEventId", "scanId", "echoScanId", "printJobId", "printStatus", "scanContext"))
|
||||
{
|
||||
output.addValue(name, null);
|
||||
}
|
||||
restorePinnedCopies(run, output);
|
||||
output.getProcessState().setNextStepName("scan");
|
||||
}
|
||||
catch(QException failure)
|
||||
{
|
||||
restorePinnedCopies(run, output);
|
||||
|
||||
@@ -76,9 +76,56 @@ final class EchoStore
|
||||
throw new QException("Echo setup is already complete or closed. Leave Echo to choose again.");
|
||||
}
|
||||
write(connection, "UPDATE echo_run SET state='OPEN',printerRecordId=?,mediaId=?,mediaRevision=?,widthMicrons=?,heightMicrons=?,dpiX=?,dpiY=?,templateId=?,templateRevisionId=?,copies=? WHERE id=?",
|
||||
destination.printerRecordId(), destination.mediaId(), destination.mediaRevision(), Integer.toString(destination.widthMicrons()),
|
||||
Integer.toString(destination.heightMicrons()), Integer.toString(destination.dpiX()), Integer.toString(destination.dpiY()),
|
||||
document.templateId().toString(), document.templateRevisionId().toString(), Integer.toString(copies), id);
|
||||
destination.printerRecordId(), destination.mediaId(), destination.mediaRevision(), destination.widthMicrons(),
|
||||
destination.heightMicrons(), destination.dpiX(), destination.dpiY(),
|
||||
document.templateId(), document.templateRevisionId(), copies, id);
|
||||
connection.commit();
|
||||
}
|
||||
catch(Exception failure)
|
||||
{
|
||||
rollback(connection, failure);
|
||||
if(failure instanceof QException expected)
|
||||
{
|
||||
throw expected;
|
||||
}
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
catch(SQLException failure)
|
||||
{
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Canonical capture already retains invalid barcode evidence. A pre-admission
|
||||
** refusal is safe only for an owned open run with no uncertain or prior work.
|
||||
***************************************************************************/
|
||||
void requireUnadmitted(String runId, MobileDeviceAuthorization.Identity identity, String scanId, Integer copies) throws QException
|
||||
{
|
||||
try(Connection connection = dataSource.getConnection())
|
||||
{
|
||||
connection.setAutoCommit(false);
|
||||
try
|
||||
{
|
||||
Run run = run(connection, runId, identity);
|
||||
if(!"OPEN".equals(run.state()) || run.pending() != null || copies != null && copies != run.copies())
|
||||
{
|
||||
throw new QException("This run cannot acknowledge a new rejected scan; resolve its current state first.");
|
||||
}
|
||||
try(PreparedStatement query = connection.prepareStatement("SELECT id FROM echo_scan WHERE id=?"))
|
||||
{
|
||||
query.setString(1, scanId);
|
||||
try(ResultSet rows = query.executeQuery())
|
||||
{
|
||||
if(rows.next())
|
||||
{
|
||||
throw new QException("This scan has prior work and requires reconciliation.");
|
||||
}
|
||||
}
|
||||
}
|
||||
connection.commit();
|
||||
}
|
||||
catch(Exception failure)
|
||||
@@ -167,6 +214,10 @@ final class EchoStore
|
||||
connection.commit();
|
||||
return new Admission(id, destination, acceptedJob, false, pinned, run.document(), run.copies());
|
||||
}
|
||||
if("REJECTED".equals(state) && pinned == null)
|
||||
{
|
||||
throw new EchoScanRejected("This barcode was rejected before printing. Scan a barcode matching the selected document.");
|
||||
}
|
||||
if(run.pending() != null && !run.pending().equals(scan.eventId()))
|
||||
{
|
||||
throw new QException("The previous print is unconfirmed. Retry that scan or leave Echo before starting another.");
|
||||
@@ -220,11 +271,11 @@ final class EchoStore
|
||||
try(Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE echo_scan SET state='IN_FLIGHT',templateId=?,templateRevisionId=?,format=?,topMicrons=?,rightMicrons=?,bottomMicrons=?,leftMicrons=? WHERE id=? AND state IN ('IN_FLIGHT','ATTENTION') AND templateId IS NULL"))
|
||||
{
|
||||
String[] values = {plan.templateId().toString(), plan.templateRevisionId().toString(), plan.format().name(), plan.stock().topMicrons().toString(),
|
||||
plan.stock().rightMicrons().toString(), plan.stock().bottomMicrons().toString(), plan.stock().leftMicrons().toString(), scanId};
|
||||
Object[] values = {plan.templateId(), plan.templateRevisionId(), plan.format().name(), plan.stock().topMicrons(),
|
||||
plan.stock().rightMicrons(), plan.stock().bottomMicrons(), plan.stock().leftMicrons(), scanId};
|
||||
for(int index = 0; index < values.length; index++)
|
||||
{
|
||||
statement.setString(index + 1, values[index]);
|
||||
statement.setObject(index + 1, values[index]);
|
||||
}
|
||||
if(statement.executeUpdate() != 1)
|
||||
{
|
||||
@@ -268,6 +319,43 @@ final class EchoStore
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The pin and rejection compare-and-set compete; only one can win. Preserve
|
||||
** the rejected capture, and release only its exact pending run binding.
|
||||
***************************************************************************/
|
||||
void reject(String runId, String scanId, String eventId) throws QException
|
||||
{
|
||||
try(Connection connection = dataSource.getConnection())
|
||||
{
|
||||
connection.setAutoCommit(false);
|
||||
try(PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE echo_scan SET state='REJECTED' WHERE id=? AND runId=? AND eventId=? AND state IN ('IN_FLIGHT','ATTENTION') AND templateId IS NULL AND templateRevisionId IS NULL AND printJobId IS NULL"))
|
||||
{
|
||||
statement.setString(1, scanId);
|
||||
statement.setString(2, runId);
|
||||
statement.setString(3, eventId);
|
||||
if(statement.executeUpdate() != 1)
|
||||
{
|
||||
connection.rollback();
|
||||
throw new QException("This scan requires reconciliation; it cannot be acknowledged as rejected.");
|
||||
}
|
||||
write(connection, "UPDATE echo_run SET pendingEventId=NULL WHERE id=? AND pendingEventId=?", runId, eventId);
|
||||
connection.commit();
|
||||
}
|
||||
catch(SQLException failure)
|
||||
{
|
||||
rollback(connection, failure);
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
catch(SQLException failure)
|
||||
{
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** A failed or lost response keeps its event identity and pending run binding.
|
||||
** No automatic retry, replacement print request or success acknowledgment.
|
||||
@@ -378,13 +466,13 @@ final class EchoStore
|
||||
/***************************************************************************
|
||||
** All SQL identifiers are fixed source; external values are bound parameters.
|
||||
***************************************************************************/
|
||||
private static void write(Connection connection, String sql, String... values) throws SQLException
|
||||
private static void write(Connection connection, String sql, Object... values) throws SQLException
|
||||
{
|
||||
try(PreparedStatement statement = connection.prepareStatement(sql))
|
||||
{
|
||||
for(int index = 0; index < values.length; index++)
|
||||
{
|
||||
statement.setString(index + 1, values[index]);
|
||||
statement.setObject(index + 1, values[index]);
|
||||
}
|
||||
statement.executeUpdate();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Copyright (c) 2026 Kingsrook, LLC. SPDX-License-Identifier: Apache-2.0
|
||||
-- Host migration input, not automatically executed by metadata registration.
|
||||
CREATE TABLE IF NOT EXISTS echo_run (
|
||||
id VARCHAR(36) PRIMARY KEY, actorId VARCHAR(200) NOT NULL, deviceId VARCHAR(36) NOT NULL,
|
||||
publicSessionId VARCHAR(36) NOT NULL, printerRecordId VARCHAR(200) NOT NULL, mediaId VARCHAR(200) NOT NULL,
|
||||
mediaRevision VARCHAR(64) NOT NULL DEFAULT '', widthMicrons INT NOT NULL DEFAULT 0, heightMicrons INT NOT NULL DEFAULT 0,
|
||||
dpiX INT NOT NULL DEFAULT 0, dpiY INT NOT NULL DEFAULT 0,
|
||||
state VARCHAR(30) NOT NULL, pendingEventId VARCHAR(36)
|
||||
);
|
||||
-- Preserving migration: the document chosen once at setup. Existing rows keep NULL and are never cleared.
|
||||
ALTER TABLE echo_run ADD COLUMN IF NOT EXISTS templateId INT;
|
||||
ALTER TABLE echo_run ADD COLUMN IF NOT EXISTS templateRevisionId INT;
|
||||
CREATE TABLE IF NOT EXISTS echo_scan (
|
||||
id VARCHAR(64) PRIMARY KEY, runId VARCHAR(36) NOT NULL REFERENCES echo_run(id),
|
||||
eventId VARCHAR(36) NOT NULL, actorId VARCHAR(200) NOT NULL, deviceId VARCHAR(36) NOT NULL, mobileScanId VARCHAR(64) NOT NULL,
|
||||
barcode VARCHAR(4096) NOT NULL, symbology VARCHAR(200) NOT NULL, rendererSymbology VARCHAR(100) NOT NULL,
|
||||
scanSource VARCHAR(200) NOT NULL, capturedAt VARCHAR(40) NOT NULL, receivedAt VARCHAR(40) NOT NULL, state VARCHAR(30) NOT NULL,
|
||||
printJobId VARCHAR(200), UNIQUE(runId, eventId)
|
||||
,templateId INT, templateRevisionId INT, format VARCHAR(10), topMicrons INT, rightMicrons INT, bottomMicrons INT, leftMicrons INT
|
||||
);
|
||||
-- Preserving migration: the scanner's AIM identifier (for example ]E0), when the capture carried one. Existing rows keep NULL
|
||||
-- (no AIM was recorded) and are never rewritten. Other databases need the equivalent additive nullable VARCHAR(3) column.
|
||||
ALTER TABLE echo_scan ADD COLUMN IF NOT EXISTS aimIdentifier VARCHAR(3);
|
||||
-- Preserving migration: copies printed per scan, chosen once at setup. Existing runs keep exactly one copy (their prior behaviour).
|
||||
-- Other databases need the equivalent additive INT NOT NULL DEFAULT 1 column.
|
||||
ALTER TABLE echo_run ADD COLUMN IF NOT EXISTS copies INT NOT NULL DEFAULT 1;
|
||||
-- Derived canonical GS1 input and HRI. Original barcode is never rewritten. Other hosts need equivalent nullable columns.
|
||||
ALTER TABLE echo_scan ADD COLUMN IF NOT EXISTS rendererValue VARCHAR(4096);
|
||||
ALTER TABLE echo_scan ADD COLUMN IF NOT EXISTS humanReadable VARCHAR(4096);
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Kingsrook, LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.kingsrook.examples.echo;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
|
||||
/** Navigation does not perform scanning; execution retains approved-device checks. */
|
||||
class EchoAppVisibilityTest
|
||||
{
|
||||
/** Remove the synthetic session after each test. */
|
||||
@AfterEach
|
||||
void clear()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
/** Authorized navigation must not require hardware enrollment. */
|
||||
@Test
|
||||
void authorizedBrowserCanSeeAppWithoutADevice()
|
||||
{
|
||||
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")).withPermissions("echo.use"));
|
||||
assertDoesNotThrow(() -> new EchoPermissions().checkPermissionsThrowing(new AbstractActionInput(), new QAppMetaData().withName("echo")));
|
||||
}
|
||||
|
||||
/** Navigation remains permission protected. */
|
||||
@Test
|
||||
void browserWithoutEchoPermissionCannotSeeApp()
|
||||
{
|
||||
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")));
|
||||
assertThrows(QPermissionDeniedException.class,
|
||||
() -> new EchoPermissions().checkPermissionsThrowing(new AbstractActionInput(), new QAppMetaData().withName("echo")));
|
||||
}
|
||||
|
||||
/** Seeing the app never authorizes scanning without an approved device. */
|
||||
@Test
|
||||
void browserWithoutApprovedDeviceCannotExecuteScanProcess()
|
||||
{
|
||||
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")).withPermissions("echo.use"));
|
||||
assertThrows(QPermissionDeniedException.class,
|
||||
() -> new EchoPermissions().checkPermissionsThrowing(new RunProcessInput(), new QProcessMetaData().withName("echoPrint")));
|
||||
}
|
||||
|
||||
/** Catalog discovery exposes the process without performing its device-bound setup. */
|
||||
@Test
|
||||
void authorizedBrowserCanDiscoverProcessWithoutADevice()
|
||||
{
|
||||
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")).withPermissions("echo.use"));
|
||||
assertDoesNotThrow(() -> new EchoPermissions().checkPermissionsThrowing(new MetaDataInput(), new QProcessMetaData().withName("echoPrint")));
|
||||
assertDoesNotThrow(
|
||||
() -> new EchoPermissions().checkPermissionsThrowing(new ProcessMetaDataInput(), new QProcessMetaData().withName("echoPrint")));
|
||||
}
|
||||
|
||||
/** Catalog discovery still requires Echo permission. */
|
||||
@Test
|
||||
void browserWithoutPermissionCannotDiscoverProcess()
|
||||
{
|
||||
QContext.init(null, new QSession().withUser(new QUser().withIdReference("fake-operator")));
|
||||
assertThrows(QPermissionDeniedException.class,
|
||||
() -> new EchoPermissions().checkPermissionsThrowing(new MetaDataInput(), new QProcessMetaData().withName("echoPrint")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Kingsrook, LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.kingsrook.examples.echo;
|
||||
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Published centered GS1 scenes retain the same data and placement contract.
|
||||
*******************************************************************************/
|
||||
class EchoCenteredLabelTest
|
||||
{
|
||||
static final String SCENE = """
|
||||
{"schema":"qqq-zpl-scene/1","elements":[
|
||||
{"type":"barcode","symbology":"gs1-128","valuePath":"/record/rendererValue","quiet":10,"x":36,"y":36,
|
||||
"fit":{"widthMicrons":95504,"heightMicrons":16000},"align":"center"},
|
||||
{"type":"rasterText","valuePath":"/record/humanReadable","x":36,"y":206,"fontAsset":"forge-echo-abel-regular",
|
||||
"fontSize":44,"width":1128,"height":58,"align":"center"}]}
|
||||
""";
|
||||
|
||||
|
||||
|
||||
/** Both renderer-supported human-readable element forms are admitted. */
|
||||
@Test
|
||||
void centeredRasterAndLegacyTextAreBothGs1Documents()
|
||||
{
|
||||
assertEquals(EchoPrintingDestinations.SceneMode.GS1, EchoPrintingDestinations.sceneMode(SCENE));
|
||||
assertEquals(EchoPrintingDestinations.SceneMode.GS1, EchoPrintingDestinations.sceneMode(SCENE.replace("rasterText", "text")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Text remains derived from GS1 data and below the barcode origin. */
|
||||
@Test
|
||||
void wrongHumanReadableBindingOrPlacementStillRefuses()
|
||||
{
|
||||
for(String type : java.util.List.of("text", "rasterText"))
|
||||
{
|
||||
JSONObject wrong = new JSONObject(SCENE.replace("rasterText", type));
|
||||
wrong.getJSONArray("elements").getJSONObject(1).put("valuePath", "/record/barcode");
|
||||
assertNull(EchoPrintingDestinations.sceneMode(wrong.toString()));
|
||||
for(int y : java.util.List.of(35, 36))
|
||||
{
|
||||
JSONObject above = new JSONObject(SCENE.replace("rasterText", type));
|
||||
above.getJSONArray("elements").getJSONObject(1).put("y", y);
|
||||
assertNull(EchoPrintingDestinations.sceneMode(above.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Raster text never relaxes the barcode binding or bounded fit requirement. */
|
||||
@Test
|
||||
void barcodeBindingAndFitRemainMandatory()
|
||||
{
|
||||
assertNull(EchoPrintingDestinations.sceneMode(SCENE.replace("/record/rendererValue", "/record/barcode")));
|
||||
JSONObject scene = new JSONObject(SCENE);
|
||||
scene.getJSONArray("elements").getJSONObject(0).remove("fit");
|
||||
assertNull(EchoPrintingDestinations.sceneMode(scene.toString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Human-readable stock dimensions preserve native profile identities. */
|
||||
@Test
|
||||
void readableStockNamesPreserveIdentityAndBothResolutions() throws Exception
|
||||
{
|
||||
assertEquals("4 × 1 in label · 300 dpi", label("4 x 1", 101600, 25400, 300, 300));
|
||||
assertEquals("4 × 1 in label · 300 dpi", label("Windows queue default (101.6 × 25.4 mm, 300 × 300 DPI)", 101600, 25400, 300, 300));
|
||||
assertEquals("100 × 50 mm label · 203 dpi", label("100 x 50 mm", 100000, 50000, 203, 203));
|
||||
assertEquals("Shipping · 4 × 1 in label · 300 × 600 dpi", label("Shipping", 101600, 25400, 300, 600));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Build one synthetic printer profile using the existing owner wire schema. */
|
||||
private static String label(String name, int width, int height, int dpiX, int dpiY) throws Exception
|
||||
{
|
||||
JSONObject profile = new JSONObject().put("id", "synthetic-media").put("revision", "a".repeat(64)).put("name", name)
|
||||
.put("widthMicrons", width).put("heightMicrons", height).put("dpiX", dpiX).put("dpiY", dpiY)
|
||||
.put("source", "WINDOWS_QUEUE_DEFAULT").put("applyMode", "DRIVER_DEFAULT");
|
||||
var choice = EchoPrintingDestinations.choices(new org.json.JSONArray().put(profile).toString()).getFirst();
|
||||
assertEquals("synthetic-media", choice.id());
|
||||
return choice.label();
|
||||
}
|
||||
}
|
||||
@@ -191,9 +191,9 @@ class EchoOwnerAdaptersTest
|
||||
List<EchoDestinations.MediaChoice> choices = EchoPrintingDestinations.choices(PROFILES);
|
||||
assertEquals(1, choices.size());
|
||||
assertEquals("4x6", choices.getFirst().id());
|
||||
assertTrue(choices.getFirst().label().contains("101.6 x 152.4 mm, 300 dpi"));
|
||||
assertEquals(1, EchoMediaChoices.select(choices, null, "4 x", 0, 50).size());
|
||||
assertEquals(0, EchoMediaChoices.select(choices, null, "label", 0, 50).size());
|
||||
assertEquals("4 × 6 in label · 300 dpi", choices.getFirst().label());
|
||||
assertEquals(1, EchoMediaChoices.select(choices, null, "4 ×", 0, 50).size());
|
||||
assertEquals(1, EchoMediaChoices.select(choices, null, "label", 0, 50).size());
|
||||
assertEquals(0, EchoMediaChoices.select(choices, List.of("2x1"), null, 0, 50).size());
|
||||
assertEquals("4x6", EchoMediaChoices.select(choices, List.of("4x6"), null, 0, 50).getFirst().getId());
|
||||
assertEquals(0, EchoMediaChoices.select(choices, null, null, 1, 50).size());
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Kingsrook, LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.kingsrook.examples.echo;
|
||||
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qbits.documentgeneration.GenerationPorts;
|
||||
import com.kingsrook.qbits.mobilescanners.EchoMobileFixture;
|
||||
import com.kingsrook.qbits.mobilescanners.MobileDeviceAuthorization;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.postgresql.ds.PGSimpleDataSource;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Isolated PostgreSQL 17 contract: numeric bindings, preserving DDL and pin CAS.
|
||||
*******************************************************************************/
|
||||
class EchoPostgresTest
|
||||
{
|
||||
/***************************************************************************
|
||||
** No production database credentials are used; the caller supplies a fixture.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "echo.postgres.url", matches = ".+")
|
||||
void setupAndPinUseNativeIntegerBindings() throws Exception
|
||||
{
|
||||
PGSimpleDataSource dataSource = new PGSimpleDataSource();
|
||||
dataSource.setURL(System.getProperty("echo.postgres.url"));
|
||||
dataSource.setUser("postgres");
|
||||
String schema = "echo_fixture_" + UUID.randomUUID().toString().replace("-", "");
|
||||
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
|
||||
{
|
||||
assertEquals(17, connection.getMetaData().getDatabaseMajorVersion());
|
||||
statement.execute("CREATE SCHEMA " + schema);
|
||||
}
|
||||
dataSource.setCurrentSchema(schema);
|
||||
try
|
||||
{
|
||||
String sql;
|
||||
try(var resource = getClass().getResourceAsStream("/db/echo-postgres.sql"))
|
||||
{
|
||||
sql = new String(resource.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
|
||||
{
|
||||
statement.execute(sql);
|
||||
}
|
||||
new EchoMobileFixture(new QInstance(), "jdbc:h2:mem:" + schema + ";DATABASE_TO_UPPER=FALSE;DB_CLOSE_DELAY=-1");
|
||||
var identity = MobileDeviceAuthorization.captureCurrent().orElseThrow();
|
||||
EchoStore store = new EchoStore(dataSource);
|
||||
store.begin("synthetic-run", identity);
|
||||
var destination = new EchoDestinations.Destination("synthetic-printer", "4x6", "synthetic-revision", 101600, 152400, 300, 300);
|
||||
store.select("synthetic-run", identity, destination, new EchoDestinations.Document(7, 11), 3);
|
||||
assertEquals(3, store.copies("synthetic-run", identity));
|
||||
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
|
||||
{
|
||||
statement.execute("INSERT INTO echo_scan(id,runId,eventId,actorId,deviceId,mobileScanId,barcode,symbology,rendererSymbology,scanSource,capturedAt,receivedAt,state)"
|
||||
+ " VALUES ('synthetic-scan','synthetic-run','synthetic-event','synthetic-actor','synthetic-device','synthetic-mobile-scan','001234','CODE128','code128','fixture','fixture','fixture','IN_FLIGHT')");
|
||||
}
|
||||
var plan = new EchoDestinations.Plan(7, 11, new GenerationPorts.Stock(101600, 152400, 3000, 3000, 3000, 3000, 300), GenerationPorts.Format.ZPL);
|
||||
store.requireUnadmitted("synthetic-run", identity, "new-scan", 3);
|
||||
assertThrows(QException.class, () -> store.requireUnadmitted("synthetic-run", identity, "synthetic-scan", 3));
|
||||
store.pin("synthetic-scan", plan);
|
||||
assertThrows(EchoStore.PinNotWon.class, () -> store.pin("synthetic-scan", plan));
|
||||
assertThrows(QException.class, () -> store.reject("synthetic-run", "synthetic-scan", "synthetic-event"));
|
||||
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
|
||||
{
|
||||
statement.execute("INSERT INTO echo_scan(id,runId,eventId,actorId,deviceId,mobileScanId,barcode,symbology,rendererSymbology,scanSource,capturedAt,receivedAt,state)"
|
||||
+ " VALUES ('rejected-scan','synthetic-run','rejected-event','synthetic-actor','synthetic-device','rejected-mobile','synthetic','QRCODE','qr','fixture','fixture','fixture','IN_FLIGHT')");
|
||||
statement.execute("UPDATE echo_run SET pendingEventId='rejected-event' WHERE id='synthetic-run'");
|
||||
}
|
||||
assertThrows(QException.class, () -> store.requireUnadmitted("synthetic-run", identity, "new-scan", 3));
|
||||
store.reject("synthetic-run", "rejected-scan", "rejected-event");
|
||||
assertThrows(EchoStore.PinNotWon.class, () -> store.pin("rejected-scan", plan));
|
||||
store.requireUnadmitted("synthetic-run", identity, "new-scan", 3);
|
||||
|
||||
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
|
||||
{
|
||||
statement.execute(sql);
|
||||
try(var rows = statement.executeQuery("SELECT templateId,templateRevisionId,topMicrons,barcode,rendererValue,humanReadable FROM echo_scan WHERE id='synthetic-scan'"))
|
||||
{
|
||||
assertTrue(rows.next());
|
||||
assertEquals(7, rows.getInt(1));
|
||||
assertEquals(11, rows.getInt(2));
|
||||
assertEquals(3000, rows.getInt(3));
|
||||
assertEquals("001234", rows.getString(4));
|
||||
assertEquals(null, rows.getString(5));
|
||||
assertEquals(null, rows.getString(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EchoMobileFixture.clear();
|
||||
try(var connection = dataSource.getConnection(); var statement = connection.createStatement())
|
||||
{
|
||||
statement.execute("DROP SCHEMA " + schema + " CASCADE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ class EchoTest
|
||||
private Runnable afterAccept;
|
||||
private Runnable afterLookup;
|
||||
private QInstance metadata;
|
||||
private boolean rejectQr;
|
||||
private boolean documentWithdrawn;
|
||||
private int documentRevisionShift;
|
||||
private final List<Integer> plannedDocuments = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
@@ -156,6 +157,10 @@ class EchoTest
|
||||
@Override
|
||||
public Plan plan(MobileDeviceAuthorization.Identity identity, Destination destination, Document document, EchoService.Scan scan) throws QException
|
||||
{
|
||||
if(rejectQr && "QRCODE".equals(scan.symbology()))
|
||||
{
|
||||
throw new EchoScanRejected("Wrong barcode type; no print was requested.");
|
||||
}
|
||||
plannedDocuments.add(document.templateId());
|
||||
if(!authorizeDocument(identity, destination, document.templateId()).equals(document))
|
||||
{
|
||||
@@ -273,6 +278,21 @@ class EchoTest
|
||||
** Setup is a real frontend, then both scans return to the same scan step.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void echoPrintingUsesProcessIdentityAndIcon()
|
||||
{
|
||||
var process = QContext.getQInstance().getProcess(EchoQBitProducer.PROCESS);
|
||||
assertEquals("echoPrint", process.getName());
|
||||
assertEquals("Echo Printing", process.getLabel());
|
||||
assertEquals("account_tree", process.getIcon().getName());
|
||||
assertEquals("Echo Scans", QContext.getQInstance().getTable(EchoQBitProducer.SCAN_TABLE).getLabel());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Existing runtime behavior remains unchanged by presentation metadata.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void processChoosesOnceAndLoopsWithExactSymbology() throws Exception
|
||||
{
|
||||
RunProcessOutput setup = new RunProcessAction().execute(new RunProcessInput().withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS));
|
||||
@@ -310,6 +330,93 @@ class EchoTest
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Rejection releases scanning without losing setup or retaining receipts.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void rejectedBarcodeKeepsSetupAndClearsReceipts() throws Exception
|
||||
{
|
||||
rejectQr = true;
|
||||
RunProcessOutput setup = new RunProcessAction().execute(new RunProcessInput().withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS));
|
||||
RunProcessOutput ready = new RunProcessAction().execute(resume(setup, "setup").withValue("printerId", "printer").withValue("mediaId", "4x6").withValue("templateId", 7).withValue("copies", 3));
|
||||
for(int index = 0; index < 2; index++)
|
||||
{
|
||||
RunProcessInput wrong = resume(ready, "scan");
|
||||
wrong.getValues().putAll(event("QRCODE", "synthetic-wrong-type"));
|
||||
String rejectedEvent = wrong.getValueString("eventId");
|
||||
RunProcessOutput rejected = new RunProcessAction().execute(wrong);
|
||||
assertEquals(true, rejected.getValue("scanRejected"));
|
||||
assertEquals(rejectedEvent, rejected.getValue("rejectedEventId"));
|
||||
assertEquals(false, rejected.getValue("scanSaved"));
|
||||
assertNull(rejected.getValue("acknowledgedEventId"));
|
||||
assertNull(rejected.getValue("scanId"));
|
||||
assertNull(rejected.getValue("printJobId"));
|
||||
assertEquals(index, requests.size());
|
||||
for(String field : List.of("echoRunId", "printerId", "mediaId", "templateId", "copies"))
|
||||
{
|
||||
assertEquals(ready.getValue(field), rejected.getValue(field));
|
||||
}
|
||||
ready = scan(rejected, "CODE128", "synthetic-valid");
|
||||
assertEquals(true, ready.getValue("scanSaved"));
|
||||
assertEquals(false, ready.getValue("scanRejected"));
|
||||
assertNull(ready.getValue("rejectedEventId"));
|
||||
assertNull(ready.getValue("rejectionMessage"));
|
||||
assertEquals(index + 1, requests.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Rejected replays never print; a won pin cannot be relabeled as rejection.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void rejectionCannotReleasePinnedWorkAndReplayRemainsRejected() throws Exception
|
||||
{
|
||||
rejectQr = true;
|
||||
String run = config.service.start("printer", "4x6", 7, 3);
|
||||
var wrong = captured("QRCODE", "synthetic-wrong-type");
|
||||
assertThrows(EchoScanRejected.class, () -> config.service.print(run, wrong));
|
||||
assertThrows(EchoScanRejected.class, () -> config.service.print(run, wrong));
|
||||
assertTrue(requests.isEmpty());
|
||||
var valid = captured("CODE128", "synthetic-valid");
|
||||
var receipt = config.service.print(run, valid);
|
||||
var store = new EchoStore(dataSource);
|
||||
assertThrows(QException.class, () -> store.reject(run, receipt.scanId(), receipt.eventId()));
|
||||
assertEquals(receipt, config.service.print(run, valid));
|
||||
assertEquals(1, requests.size());
|
||||
assertThrows(QException.class, () -> store.pin(wrong.get("scanId").toString(), acceptedPlans.get("echo:" + receipt.scanId())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Unsupported types and invalid GS1 return ready without losing setup.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void unsupportedAndInvalidGs1RecoverButUncertainWorkDoesNot() throws Exception
|
||||
{
|
||||
RunProcessOutput setup = new RunProcessAction().execute(new RunProcessInput().withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS));
|
||||
RunProcessOutput ready = new RunProcessAction().execute(resume(setup, "setup").withValue("printerId", "printer").withValue("mediaId", "4x6").withValue("templateId", 7));
|
||||
RunProcessOutput unsupported = scan(ready, "MAXICODE", "synthetic");
|
||||
assertEquals(true, unsupported.getValue("scanRejected"));
|
||||
RunProcessInput invalid = resume(unsupported, "scan");
|
||||
invalid.getValues().putAll(event("CODE128", "invalid-gs1"));
|
||||
invalid.addValue("aimIdentifier", "]C1");
|
||||
invalid.addValue("rawDataBase64", java.util.Base64.getEncoder().encodeToString("invalid-gs1".getBytes(StandardCharsets.US_ASCII)));
|
||||
RunProcessOutput rejected = new RunProcessAction().execute(invalid);
|
||||
assertEquals(true, rejected.getValue("scanRejected"));
|
||||
assertTrue(requests.isEmpty());
|
||||
assertEquals(true, scan(rejected, "CODE128", "synthetic-valid").getValue("scanSaved"));
|
||||
String run = ready.getValueString("echoRunId");
|
||||
uncertain = true;
|
||||
assertThrows(QException.class, () -> config.service.print(run, captured("CODE128", "uncertain")));
|
||||
QException blocked = assertThrows(QException.class, () -> config.service.print(run, captured("MAXICODE", "unsupported")));
|
||||
assertFalse(blocked instanceof EchoScanRejected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Runs without a chosen count print exactly one copy, and the run stores it.
|
||||
***************************************************************************/
|
||||
@@ -1481,6 +1588,13 @@ class EchoTest
|
||||
config.service.close(run);
|
||||
desktop("operator", true);
|
||||
assertEquals("000001", source(acceptedScan.scanId()).getValueString("barcode"));
|
||||
var metadataInput = new com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput();
|
||||
metadataInput.setProcessName(EchoQBitProducer.PROCESS);
|
||||
var processView = new com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction().execute(metadataInput);
|
||||
assertTrue(processView.getProcess().getHasPermission());
|
||||
assertThrows(QException.class, () -> new RunProcessAction().execute(new RunProcessInput()
|
||||
.withInputSource(com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource.USER).withProcessName(EchoQBitProducer.PROCESS)));
|
||||
assertThrows(QException.class, () -> config.service.begin(UUID.randomUUID().toString()));
|
||||
assertThrows(QException.class, () -> config.service.print(run, context));
|
||||
assertThrows(QException.class, () -> new EchoPermissions().checkPermissionsThrowing(
|
||||
new RunProcessInput().withProcessName(EchoQBitProducer.PROCESS), QContext.getQInstance().getProcess(EchoQBitProducer.PROCESS)));
|
||||
@@ -1617,3 +1731,5 @@ class EchoTest
|
||||
"eventId", UUID.randomUUID().toString(), "capturedAt", "2026-11-01T17:00:00Z"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-1
@@ -66,7 +66,7 @@ class EchoHttpFixtureTest
|
||||
JSONObject item = (JSONObject) extension;
|
||||
if("mobile-scanners".equals(item.getString("name")))
|
||||
{
|
||||
mobile15 = "1.5.0".equals(item.getString("version"));
|
||||
mobile15 = item.getString("version").matches("1\\.([5-9]|[1-9][0-9]+)\\.[0-9]+");
|
||||
}
|
||||
}
|
||||
assertTrue(mobile15);
|
||||
@@ -111,7 +111,23 @@ class EchoHttpFixtureTest
|
||||
assertTrue(firstValues.getString("printJobId").startsWith("fixture-job-"));
|
||||
JSONObject retry = complete(form("/qqq/v1/processes/echoPrint/" + process + "/step/scan", scanner, firstEvent, true), EchoQBitProducer.PROCESS, scanner, true);
|
||||
assertEquals(firstValues.getString("printJobId"), retry.getJSONObject("values").getString("printJobId"));
|
||||
JSONObject unsupported = event("MAXICODE");
|
||||
JSONObject rejected = complete(form("/qqq/v1/processes/echoPrint/" + process + "/step/scan", scanner, unsupported, true), EchoQBitProducer.PROCESS, scanner, true);
|
||||
assertEquals("scan", rejected.getString("nextStep"));
|
||||
JSONObject rejection = rejected.getJSONObject("values");
|
||||
assertTrue(rejection.getBoolean("scanRejected"));
|
||||
assertFalse(rejection.getBoolean("scanSaved"));
|
||||
assertEquals(unsupported.getString("eventId"), rejection.getString("rejectedEventId"));
|
||||
assertFalse(rejection.getString("rejectionMessage").isBlank());
|
||||
assertEquals(run, rejection.getString("echoRunId"));
|
||||
for(String cleared : java.util.List.of("acknowledgedEventId", "scanId", "printJobId"))
|
||||
{
|
||||
assertTrue(rejection.isNull(cleared), "No prior positive receipt survives rejection.");
|
||||
}
|
||||
JSONObject second = complete(form("/qqq/v1/processes/echoPrint/" + process + "/step/scan", scanner, event("QRCODE"), true), EchoQBitProducer.PROCESS, scanner, true);
|
||||
assertFalse(second.getJSONObject("values").getBoolean("scanRejected"));
|
||||
assertTrue(second.getJSONObject("values").isNull("rejectedEventId"));
|
||||
assertTrue(second.getJSONObject("values").isNull("rejectionMessage"));
|
||||
assertEquals(run, second.getJSONObject("values").getString("echoRunId"));
|
||||
assertEquals("scan", second.getString("nextStep"));
|
||||
JSONArray scans = query(EchoQBitProducer.SCAN_TABLE, scanner);
|
||||
|
||||
@@ -58,8 +58,13 @@ function Initialize-IsolatedScannerSource
|
||||
Remove-IsolatedBuildDirectory -ModuleRoot $ModuleRoot -LeafName 'scanner-source'
|
||||
$destination = Join-Path $ModuleRoot 'target/scanner-source'
|
||||
New-Item -ItemType Directory -Path $destination -Force | Out-Null
|
||||
foreach($name in @('pom.xml', 'mvnw.cmd', 'mvnw', '.mvn', 'src', 'checkstyle'))
|
||||
foreach($name in @('mvnw.cmd', 'mvnw', '.mvn', 'checkstyle'))
|
||||
{
|
||||
Copy-Item -LiteralPath (Join-Path $ScannerRoot $name) -Destination $destination -Recurse -Force
|
||||
}
|
||||
# The scanner base module (pom.xml, src) lives in base/ since the reactor layout; the scratch tree keeps the single-module shape.
|
||||
foreach($name in @('pom.xml', 'src'))
|
||||
{
|
||||
Copy-Item -LiteralPath (Join-Path (Join-Path $ScannerRoot 'base') $name) -Destination $destination -Recurse -Force
|
||||
}
|
||||
}
|
||||
@@ -70,16 +70,23 @@ OpenAPI identifiers are `getQbitRecordFilesCapabilities`,
|
||||
`postQbitRecordFilesUploadsByUploadIdCancel`. Native invoker/credential-scope IDs
|
||||
instead use `METHOD /qbit/record-files/...`; capability gating must distinguish them.
|
||||
|
||||
## Publication preparation
|
||||
## Publication
|
||||
|
||||
This module is a standard QBit module (parent `qbit-build-parent` 2.1.0, Apache-2.0
|
||||
metadata, canonical Gitea URLs) and produces a separate JAR from the root
|
||||
scanner; the root has no module or dependency on it. Preparing the POM is not
|
||||
publication. Publishing still requires a full immutable (non-SNAPSHOT) dependency set
|
||||
and the normal hosted qualification and publication path. No public version of this
|
||||
module exists, and no release test is claimed here.
|
||||
This module is a module of the repository's reactor (root `pom.xml`, artifact
|
||||
`qbit-mobile-scanners-reactor`), next to `base` (`qbit-mobile-scanners`). It inherits
|
||||
the root's single `<revision>` and the same `qbit-build-parent` 2.1.0, Apache-2.0
|
||||
metadata and canonical Gitea URLs. One root `mvn deploy` (the shared Gitea Actions
|
||||
`maven` workflow) tests and publishes the reactor POM, `qbit-mobile-scanners` and
|
||||
`qbit-mobile-scanners-record-files` at one version to the Gitea Maven registry.
|
||||
Its dependencies are immutable releases from that registry: `qbit-record-files`
|
||||
0.1.0-RC.1 and `qbit-collaboration` 1.1.0-RC.1, with QQQ 4.1.0-RC.22 from the root.
|
||||
Build only this module and the base with `./mvnw -pl integrations/record-files -am verify`.
|
||||
|
||||
## Reproducible isolated verification
|
||||
## Reproducible isolated verification (legacy local harness)
|
||||
|
||||
The isolated scripts below date from the pre-publication frozen-candidate adoption on a
|
||||
Windows workstation and are not part of the release path; they predate the reactor
|
||||
layout and are kept as evidence tooling only.
|
||||
|
||||
From this directory, run `./Verify-Isolated.ps1`. It verifies every checksum in the
|
||||
retained bundle, seeds a private repository from the local Maven cache, copies and
|
||||
|
||||
@@ -8,12 +8,12 @@ $ErrorActionPreference = 'Stop'
|
||||
$test = Join-Path $PSScriptRoot ('target/path-regression-' + [Guid]::NewGuid().ToString('N'))
|
||||
$inputRoot = Join-Path $test 'input'
|
||||
$moduleRoot = Join-Path $test 'module-root'
|
||||
New-Item -ItemType Directory -Force -Path $inputRoot, $moduleRoot, (Join-Path $inputRoot '.mvn'), (Join-Path $inputRoot 'src'), (Join-Path $inputRoot 'checkstyle'), (Join-Path $moduleRoot 'target/isolated-m2') | Out-Null
|
||||
foreach($name in @('pom.xml', 'mvnw.cmd', 'mvnw')) { Set-Content -LiteralPath (Join-Path $inputRoot $name) -Value 'fixture' }
|
||||
$removedSource = Join-Path $inputRoot 'src/Removed.java'
|
||||
New-Item -ItemType Directory -Force -Path $inputRoot, $moduleRoot, (Join-Path $inputRoot '.mvn'), (Join-Path $inputRoot 'base/src'), (Join-Path $inputRoot 'checkstyle'), (Join-Path $moduleRoot 'target/isolated-m2') | Out-Null
|
||||
foreach($name in @('mvnw.cmd', 'mvnw', 'base/pom.xml')) { Set-Content -LiteralPath (Join-Path $inputRoot $name) -Value 'fixture' }
|
||||
$removedSource = Join-Path $inputRoot 'base/src/Removed.java'
|
||||
Set-Content -LiteralPath $removedSource -Value 'old-source'
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $inputRoot 'src/main/resources') | Out-Null
|
||||
$removedResource = Join-Path $inputRoot 'src/main/resources/removed.txt'
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $inputRoot 'base/src/main/resources') | Out-Null
|
||||
$removedResource = Join-Path $inputRoot 'base/src/main/resources/removed.txt'
|
||||
Set-Content -LiteralPath $removedResource -Value 'old-resource'
|
||||
$sentinel = Join-Path $moduleRoot 'target/isolated-m2/keep.txt'
|
||||
Set-Content -LiteralPath $sentinel -Value 'repository-must-survive'
|
||||
@@ -23,7 +23,7 @@ New-Item -ItemType Directory -Force -Path (Join-Path $scratch 'target/classes')
|
||||
Set-Content -LiteralPath (Join-Path $scratch 'target/classes/Removed.class') -Value 'stale-class'
|
||||
Remove-Item -LiteralPath $removedSource
|
||||
Remove-Item -LiteralPath $removedResource
|
||||
Set-Content -LiteralPath (Join-Path $inputRoot 'src/Current.java') -Value 'current-source'
|
||||
Set-Content -LiteralPath (Join-Path $inputRoot 'base/src/Current.java') -Value 'current-source'
|
||||
Initialize-IsolatedScannerSource -ScannerRoot $inputRoot -ModuleRoot $moduleRoot
|
||||
if((Test-Path -LiteralPath (Join-Path $scratch 'src/Removed.java')) -or (Test-Path -LiteralPath (Join-Path $scratch 'target/classes/Removed.class'))) { throw 'Removed source/class survived the fresh snapshot.' }
|
||||
if(Test-Path -LiteralPath (Join-Path $scratch 'src/main/resources/removed.txt')) { throw 'Removed resource survived the fresh snapshot.' }
|
||||
|
||||
@@ -110,7 +110,7 @@ try
|
||||
Remove-IsolatedBuildDirectory -ModuleRoot $module -LeafName 'module'
|
||||
$runtimeClasspath = Join-Path $target 'runtime-classpath.txt'
|
||||
if(Test-Path -LiteralPath $runtimeClasspath) { Remove-Item -LiteralPath $runtimeClasspath }
|
||||
Invoke-Maven @('-B', '-ntp', 'clean', 'verify', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath', '-Dmdep.includeScope=test', '-Dmdep.outputFile=target/runtime-classpath.txt')
|
||||
Invoke-Maven @('-B', '-ntp', '-Pisolated-module', 'clean', 'verify', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath', '-Dmdep.includeScope=test', '-Dmdep.outputFile=target/runtime-classpath.txt')
|
||||
$runtime = (Get-Content -LiteralPath (Join-Path $target 'runtime-classpath.txt') -Raw).Trim().Split(';')
|
||||
$manifest = foreach($path in $runtime) { @{path=$path; sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() } }
|
||||
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $target 'runtime-sha256.json') -Encoding utf8
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<relativePath />
|
||||
</parent>
|
||||
<parent><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-mobile-scanners-reactor</artifactId><version>${revision}</version><relativePath>../../pom.xml</relativePath></parent>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-mobile-scanners-record-files</artifactId>
|
||||
<version>${revision}</version>
|
||||
@@ -33,23 +28,13 @@
|
||||
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners/issues</url>
|
||||
</issueManagement>
|
||||
<properties>
|
||||
<!-- effective version 0.1.0-SNAPSHOT; the parent's own revision (2.1.0) is overridden here -->
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- the same property names the root POM and the Echo demo use, with their defaults -->
|
||||
<qqq.version>4.1.0-RC.21</qqq.version>
|
||||
<mobile.version>0.1.0-SNAPSHOT</mobile.version>
|
||||
<recordfiles.version>0.1.0-SNAPSHOT</recordfiles.version>
|
||||
<collaboration.version>0.1.0-record-files-SNAPSHOT</collaboration.version>
|
||||
<!-- developer-script override for core and rdbms only; by default it is the single qqq line, never a second one -->
|
||||
<qqq.frozen.version>${qqq.version}</qqq.frozen.version>
|
||||
<!-- the base module is built in the same reactor; the others are published releases from the Gitea registry -->
|
||||
<mobile.version>${project.version}</mobile.version>
|
||||
<recordfiles.version>0.4.0-RC.1</recordfiles.version>
|
||||
<collaboration.version>1.2.0-RC.1</collaboration.version>
|
||||
</properties>
|
||||
<dependencyManagement><dependencies>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-bom-pom</artifactId><version>${qqq.version}</version><type>pom</type><scope>import</scope></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-core</artifactId><version>${qqq.frozen.version}</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-backend-module-rdbms</artifactId><version>${qqq.frozen.version}</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-middleware-javalin</artifactId><version>${qqq.version}</version></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-collaboration</artifactId><version>${collaboration.version}</version></dependency>
|
||||
</dependencies></dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -62,13 +47,20 @@
|
||||
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-params</artifactId><scope>test</scope></dependency>
|
||||
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.27.7</version><scope>test</scope></dependency>
|
||||
</dependencies>
|
||||
<build><directory>${project.basedir}/target/module</directory><plugins>
|
||||
<build><plugins>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version><configuration><systemPropertyVariables><qqq.logger.logSessionId.disabled>true</qqq.logger.logSessionId.disabled></systemPropertyVariables></configuration></plugin>
|
||||
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-checkstyle-plugin</artifactId><version>3.6.0</version>
|
||||
<dependencies><dependency><groupId>com.puppycrawl.tools</groupId><artifactId>checkstyle</artifactId><version>14.3.0</version></dependency></dependencies>
|
||||
<configuration><configLocation>../../checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration>
|
||||
<configuration><configLocation>${maven.multiModuleProjectDirectory}/checkstyle/checkstyle.xml</configLocation><includeTestSourceDirectory>true</includeTestSourceDirectory><failOnViolation>true</failOnViolation><violationSeverity>warning</violationSeverity></configuration>
|
||||
<executions><execution><id>style</id><phase>validate</phase><goals><goal>check</goal></goals></execution></executions>
|
||||
</plugin>
|
||||
</plugins></build>
|
||||
<profiles>
|
||||
<!-- Isolated verification (Verify-Isolated.ps1) keeps its module output out of target/ itself; a normal reactor build and the standard capture use target/. -->
|
||||
<profile>
|
||||
<id>isolated-module</id>
|
||||
<build><directory>${project.basedir}/target/module</directory></build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -1,17 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<!-- Reactor root AND parent of every module. The release pipeline rewrites only the version property below in THIS file, so it is the single version
|
||||
source of every published module. The root keeps the modules' former parent (qbit-build-parent 2.1.0), so inherited build configuration is unchanged. -->
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.2.1</version>
|
||||
<relativePath />
|
||||
</parent>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-mobile-scanners</artifactId>
|
||||
<artifactId>qbit-mobile-scanners-reactor</artifactId>
|
||||
<version>${revision}</version>
|
||||
<name>QQQ Mobile Scanners</name>
|
||||
<description>Approved mobile devices and native QQQ barcode capture processes.</description>
|
||||
<packaging>pom</packaging>
|
||||
<name>QQQ Mobile Scanners (reactor)</name>
|
||||
<description>Aggregator for the published Mobile Scanners modules: the base qbit and its optional Record Files integration.</description>
|
||||
<url>https://git.kof22.com/QRun-IO/qbit-mobile-scanners</url>
|
||||
<scm>
|
||||
<connection>scm:git:https://git.kof22.com/QRun-IO/qbit-mobile-scanners.git</connection>
|
||||
@@ -31,91 +34,41 @@
|
||||
</license>
|
||||
</licenses>
|
||||
<properties>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<revision>0.6.0-SNAPSHOT</revision>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<qqq.version>4.1.0-RC.21</qqq.version>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<version>${qqq.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.2.224</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-postgres</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>1.21.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.14</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.11.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<!-- Every published module, so one root deploy builds every GAV. demo/ and examples/ are runnable demonstrations with their own coordinates and are intentionally
|
||||
not part of this reactor. Build a subset with -pl <module> -am. -->
|
||||
<modules>
|
||||
<module>base</module>
|
||||
<module>integrations/record-files</module>
|
||||
</modules>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>14.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<!-- Publish consumer POMs with the version resolved (the parent reference and the reactor root itself would otherwise carry the unresolved property). -->
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>1.7.3</version>
|
||||
<configuration>
|
||||
<configLocation>checkstyle/checkstyle.xml</configLocation>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
<failOnViolation>true</failOnViolation>
|
||||
<violationSeverity>warning</violationSeverity>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
<flattenMode>ossrh</flattenMode>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle</id>
|
||||
<phase>validate</phase>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten.clean</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
|
||||
Reference in new issue
Block a user