Added feature: bulk clearing (set to null) of a given product attribute for all products in a given product tree - actually applies the modified products via PUT API

This commit is contained in:
Max Martens 2026-02-05 17:42:17 +01:00
parent 2d742ca3f1
commit 39e5042350
8 changed files with 428 additions and 149 deletions

View File

@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,7 +1,9 @@
# ABTProducts PUT request body generator
Simple tool to quickly edit HTM products via ABTProducts REST API.
- Requires JRE 21
- Requires JRE 17
## Generating a PUT output (that you need to supply to PUT API yourself):
- Run via: `java -jar ABTProductsPUTGenerator.jar`
- Specify custom input/output path via: `java -jar ABTProductsPUTGenerator.jar <inputPath> <outputPath>`
- Takes a ABTProducts GET response body in JSON format (product details)
@ -10,3 +12,14 @@ Simple tool to quickly edit HTM products via ABTProducts REST API.
- `curl -X PUT -H 'Content-Type: application/json' {baseUrl}/abt/abtproducts/1.0/38 --data @output.json`
- Default input path: /input.json
- Default output path: /output.json (output is overwritten if it exists)
## Bulk clearing (set to null) of a certain product attribute for all productIds in a product-tree (SE product reponse)
- Run via: `java -jar ABTProductsPUTGenerator.jar clearAttribute <inputPath> <attributeToClear> <environment> <wso2BearerToken>`
- Takes a SE GET product tree response body or ABTProducts GET response body in JSON format
- Also needs the attribute to clear and the WSO2 environment and valid WSO2 bearer token for that environment
- Performs the following operations:
- Finds all productId's in the given product(tree) and for each productId found:
- GETs productdetails via ABTProducts API
- Converts it to PUT request body using the functionality in the previous section
- Replaces <attributeToClear> with `null`
- Actually sends the modified PUT request body to ABTProducts PUT API

View File

@ -58,8 +58,8 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>

View File

@ -3,8 +3,11 @@ package nl.htm.ovpay.abt;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -19,6 +22,15 @@ public class ABTProductsPUTGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(ABTProductsPUTGenerator.class);
public static void main(String[] args) throws Exception {
LOGGER.info("Starting ABTProductsPUTGenerator with arguments {}", Arrays.stream(args).toList());
if (args.length > 0 && args[0].equalsIgnoreCase("clearAttribute")) {
clearAttribute(args);
} else {
generateOutput(args);
}
}
private static void generateOutput(String[] args) throws Exception {
if (args.length != 2) {
LOGGER.info("To modify input/output path, use: java -jar ABTProductsPUTGenerator.jar <inputPath> <outputPath>");
}
@ -40,6 +52,49 @@ public class ABTProductsPUTGenerator {
}
}
private static void clearAttribute(String[] args) throws Exception {
if (args.length != 5) {
LOGGER.error("Incorrect input parameters!");
LOGGER.error("To clear attribute, use: java -jar ABTProductsPUTGenerator.jar clearAttribute <inputPath> <attributeToClear> <environment> <wso2BearerToken>");
return;
}
var inputFile = args[1];
var attributeToClear = args[2];
var environment = args[3];
var wso2BearerToken = args[4];
try (InputStream is = getInputStream(inputFile)) {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(is);
var productIds = jsonNode.findValues("productId").stream().filter(JsonNode::isValueNode).map(JsonNode::asText).toList();
LOGGER.info("Found productIds to process: {}", productIds);
LOGGER.warn("Are you SURE you want to set attribute \"{}\" for all these productIds on environment {} to NULL?", attributeToClear, environment);
LOGGER.warn("Type 'yes' to continue...");
var input = new Scanner(System.in).nextLine();
if (!input.equalsIgnoreCase("yes")) {
LOGGER.info("Aborting...");
return;
}
for (var productId : productIds) {
LOGGER.info("Getting product details for product {}...", productId);
var productJsonString = APIHelper.getProductDetails(environment, productId, wso2BearerToken);
var productJsonNode = mapper.readTree(productJsonString);
var putJsonNode = processJsonNode(productJsonNode);
LOGGER.info("Clearing attribute \"{}\" from product with productId {}...", attributeToClear, productId);
((ObjectNode)putJsonNode).putRawValue(attributeToClear, null);
LOGGER.info("PUT product details for product with productId {} with cleared attribute \"{}\"...", productId, attributeToClear);
LOGGER.info("PUT product details with JSON body: {}", putJsonNode.toPrettyString());
APIHelper.putProductDetails(environment, productId, putJsonNode.toString(), wso2BearerToken);
}
LOGGER.info("DONE clearing attribute \"{}\" for productIds {}!", attributeToClear, productIds);
}
}
private static InputStream getInputStream(String filePath) throws IOException {
var externalResource = new File(filePath);
if (externalResource.exists()) {

View File

@ -0,0 +1,89 @@
package nl.htm.ovpay.abt;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class APIHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(APIHelper.class);
private static final String PRODUCT_DETAILS_URI = "https://services.<ENV>.api.htm.nl/abt/abtproducts/1.0/products/<PRODUCTID>";
public static String envToUriPart(String environment) {
return switch (environment) {
case "DEV" -> "dev";
case "ACC" -> "acc";
case "PRD" -> "";
default -> throw new IllegalArgumentException("Invalid environment: " + environment);
};
}
public static String getProductDetails(String environment, String productId, String wso2BearerToken) throws Exception {
var envUriPart = envToUriPart(environment);
var getProductDetailsUri = PRODUCT_DETAILS_URI.replace("<ENV>", envUriPart).replace("<PRODUCTID>", productId);
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, DummyX509TrustManager.getDummyArray(), new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
URL url = new URL(getProductDetailsUri);
URLConnection con = url.openConnection();
HttpURLConnection http = (HttpURLConnection)con;
http.setRequestMethod("GET");
http.setDoOutput(false);
http.setRequestProperty("Authorization", "Bearer " + wso2BearerToken);
http.connect();
try(InputStream is = http.getInputStream()) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
}
public static void putProductDetails(String environment, String productId, String jsonBody, String wso2BearerToken) throws Exception {
var envUriPart = envToUriPart(environment);
var putProductDetailsUri = PRODUCT_DETAILS_URI.replace("<ENV>", envUriPart).replace("<PRODUCTID>", productId);
LOGGER.info("PUT product details to URI: {}", putProductDetailsUri);
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, DummyX509TrustManager.getDummyArray(), new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
URL url = new URL(putProductDetailsUri);
URLConnection con = url.openConnection();
HttpURLConnection http = (HttpURLConnection)con;
http.setRequestMethod("PUT");
http.setDoOutput(true);
http.setRequestProperty("Authorization", "Bearer " + wso2BearerToken);
http.setRequestProperty("Content-Type", "application/json");
byte[] out = jsonBody.getBytes(StandardCharsets.UTF_8);
int length = out.length;
http.setFixedLengthStreamingMode(length);
http.connect();
try(OutputStream os = http.getOutputStream()) {
os.write(out);
}
try(InputStream is = http.getInputStream()) {
LOGGER.info("Got response from PUT API: {}", new String(is.readAllBytes(), StandardCharsets.UTF_8));
}
}
}

View File

@ -0,0 +1,38 @@
package nl.htm.ovpay.abt;
import java.security.cert.X509Certificate;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public final class DummyX509TrustManager implements X509TrustManager {
private static DummyX509TrustManager INSTANCE;
private DummyX509TrustManager() {
// prevent instantiation
}
public static DummyX509TrustManager getInstance() {
if (INSTANCE == null) {
INSTANCE = new DummyX509TrustManager();
}
return INSTANCE;
}
public static TrustManager[] getDummyArray() {
if (INSTANCE == null) {
INSTANCE = new DummyX509TrustManager();
}
return new TrustManager[] { INSTANCE };
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}

View File

@ -1,23 +1,33 @@
{
"productId": 251,
"fikoArticleNumber": null,
"productId": 663,
"parentProductId": null,
"gboPackageTemplateId": "30901",
"layerInfo": {
"layerInfoId": 7,
"choiceKey": "isRenewable",
"choiceLabel": "Kies voor een doorlopend abonnement of een enkele termijn",
"isCustomChoice": false
},
"fikoArticleNumber": null,
"gboPackageTemplateId": "30001",
"tapConnectProductCode": null,
"productName": "MaxTestPOST-21-okt-test-1 edited PUT",
"productDescription": "21-okt-test-1 edited PUT - reis met 90% korting gedurende de eerste F&F pilot!",
"validityPeriod": null,
"productTranslations": null,
"productName": "Test OVPAY-2306",
"productDescription": "Test OVPAY-2306 (sellingPeriods in kindje verwijderen en later opnieuw weer kunnen toevoegen)",
"validityPeriod": {
"validityPeriodId": 782,
"fromInclusive": "2025-12-31T23:00:00.000Z",
"toInclusive": "2026-03-30T22:00:00.000Z"
},
"productTranslations": [],
"allowedGboAgeProfiles": [],
"productOwner": {
"productOwnerId": 1,
"name": "Corneel Verstoep",
"organization": "HTM"
"name": "Wie dit leest",
"organization": "... is een aap."
},
"marketSegments": null,
"customerSegments": null,
"allowedGboAgeProfiles": null,
"marketSegments": [],
"customerSegments": [],
"productCategory": {
"productCategoryId": 9,
"productCategoryId": 1,
"isTravelProduct": true,
"name": "Kortingsabonnement"
},
@ -25,32 +35,10 @@
"requiredCustomerLevelId": 1,
"name": "guest"
},
"requiredProducts": null,
"incompatibleProducts": null,
"mandatoryCustomerDataItems": [
{
"mandatoryCustomerDataItemId": 4,
"customerDataItem": "emailAddress"
},
{
"mandatoryCustomerDataItemId": 5,
"customerDataItem": "address"
}
],
"requiredGboPersonalAttributes": [
{
"requiredGboPersonalAttributeId": 1,
"name": "NAME"
},
{
"requiredGboPersonalAttributeId": 2,
"name": "BIRTHDATE"
},
{
"requiredGboPersonalAttributeId": 3,
"name": "PHOTO"
}
],
"requiredProducts": [],
"incompatibleProducts": [],
"mandatoryCustomerDataItems": [],
"requiredGboPersonalAttributes": [],
"tokenTypes": [
{
"tokenTypeId": 1,
@ -61,72 +49,36 @@
"paymentMomentId": 1,
"name": "prepaid"
},
"serviceOptions": null,
"validityDuration": "P7D",
"maxStartInFutureDuration": "P6W",
"isRenewable": false,
"serviceOptions": [
{
"serviceOptionId": 4,
"action": "cancel_notAllowed",
"description": "Stopzetting is niet toegestaan (doorgaans in combinatie met refund_notAllowed)"
},
{
"serviceOptionId": 10,
"action": "refund_notAllowed",
"description": "Terugbetaling niet toegestaan (doorgaans in combinatie met cancel_notAllowed)"
}
],
"validityDuration": "P1W",
"maxStartInFutureDuration": "P1W",
"isRenewable": null,
"sendInvoice": false,
"imageReference": "https://www.htm.nl/media/leif2leu/htm-logo-mobile.svg",
"productPageUrl": "https://www.htm.nl/nog-onbekende-product-pagina",
"termsUrl": "https://www.htm.nl/nog-onbekende-productvoorwaarden-pagina",
"imageReference": null,
"productPageUrl": null,
"termsUrl": null,
"isSellableAtHtm": true,
"needsSolvencyCheckConsumer": false,
"needsSolvencyCheckBusiness": false,
"sellingPeriods": [
{
"sellingPeriodId": 240,
"fromInclusive": "2024-09-06T00:00:00.000+00:00",
"toInclusive": "2024-12-29T23:59:59.000+00:00",
"sellingPeriodId": 1382,
"fromInclusive": "2025-12-31T23:00:00.000Z",
"toInclusive": "2026-03-30T22:00:00.000Z",
"salesTouchpoint": {
"salesTouchpointId": 6,
"name": "Service-engine",
"isActive": true,
"retailer": {
"retailerId": 1000,
"name": "HTM intern beheer",
"street": "Koningin Julianaplein",
"number": 10,
"numberAddition": null,
"postalCode": "2595 AA",
"city": "Den Haag",
"country": "Nederland",
"emailAddress": "info@htm.nl",
"phoneNumber": "070 374 9002",
"taxId": null,
"imageReference": "https://www.htm.nl/typo3conf/ext/htm_template/Resources/Public/img/logo.svg"
}
},
"forbiddenPaymentMethods": null,
"sellingPrices": [
{
"sellingPriceId": 318,
"taxCode": "V21",
"taxPercentage": 21.0000,
"amountExclTax": 94,
"amountInclTax": 100,
"fromInclusive": "2024-09-06T00:00:00.000+00:00",
"toInclusive": "2024-12-18T23:59:59.000+00:00",
"internalPrice": 92.0000
},
{
"sellingPriceId": 319,
"taxCode": "V21",
"taxPercentage": 21.0000,
"amountExclTax": 98,
"amountInclTax": 102,
"fromInclusive": "2024-12-19T00:00:00.000+00:00",
"toInclusive": "2024-12-29T23:59:59.000+00:00",
"internalPrice": 0.0000
}
]
},
{
"sellingPeriodId": 241,
"fromInclusive": "2024-09-06T00:00:00.000+00:00",
"toInclusive": "2024-12-29T23:59:59.000+00:00",
"salesTouchpoint": {
"salesTouchpointId": 5,
"name": "Servicewinkel (Team Incident Masters)",
"salesTouchpointId": 3,
"name": "Website",
"isActive": true,
"retailer": {
"retailerId": 1001,
@ -139,64 +91,196 @@
"country": "Nederland",
"emailAddress": "info@htm.nl",
"phoneNumber": "070 374 9002",
"taxId": null,
"imageReference": "https://www.htm.nl/typo3conf/ext/htm_template/Resources/Public/img/logo.svg"
"taxId": 572309345923,
"imageReference": "https://www.htm.nl/media/leif2leu/htm-logo-mobile.svg"
}
},
"forbiddenPaymentMethods": [
{
"forbiddenPaymentMethodId": 2,
"name": "creditcard",
"issuer": "Visa"
"forbiddenPaymentMethods": [],
"sellingPrices": []
}
],
"sellingPrices": [
"purchasePrices": [],
"productVariants": [
{
"sellingPriceId": 320,
"taxCode": "V21",
"taxPercentage": 21.0000,
"amountExclTax": 94,
"amountInclTax": 100,
"fromInclusive": "2024-09-06T00:00:00.000+00:00",
"toInclusive": "2024-12-18T23:59:59.000+00:00",
"internalPrice": 92.0000
"productId": 664,
"parentProductId": 663,
"layerInfo": {
"layerInfoId": null,
"choiceKey": null,
"choiceLabel": null,
"isCustomChoice": false
},
"fikoArticleNumber": null,
"gboPackageTemplateId": "30001",
"tapConnectProductCode": null,
"productName": "Losse week - Test OVPAY-2306",
"productDescription": "Test OVPAY-2306 (sellingPeriods in kindje verwijderen en later opnieuw weer kunnen toevoegen)",
"validityPeriod": {
"validityPeriodId": 783,
"fromInclusive": "2025-12-31T23:00:00.000Z",
"toInclusive": "2026-03-30T22:00:00.000Z"
},
"productTranslations": [],
"allowedGboAgeProfiles": [],
"productOwner": {
"productOwnerId": 1,
"name": "Wie dit leest",
"organization": "... is een aap."
},
"marketSegments": [],
"customerSegments": [],
"productCategory": {
"productCategoryId": 1,
"isTravelProduct": true,
"name": "Kortingsabonnement"
},
"requiredCustomerLevel": {
"requiredCustomerLevelId": 1,
"name": "guest"
},
"requiredProducts": [],
"incompatibleProducts": [],
"mandatoryCustomerDataItems": [],
"requiredGboPersonalAttributes": [],
"tokenTypes": [
{
"tokenTypeId": 1,
"name": "EMV"
}
],
"paymentMoment": {
"paymentMomentId": 1,
"name": "prepaid"
},
"serviceOptions": [
{
"serviceOptionId": 4,
"action": "cancel_notAllowed",
"description": "Stopzetting is niet toegestaan (doorgaans in combinatie met refund_notAllowed)"
},
{
"sellingPriceId": 321,
"taxCode": "V21",
"taxPercentage": 21.0000,
"amountExclTax": 98,
"amountInclTax": 102,
"fromInclusive": "2024-12-19T00:00:00.000+00:00",
"toInclusive": "2024-12-29T23:59:59.000+00:00",
"internalPrice": 0.0000
}
]
}
],
"purchasePrices": [
{
"purchasePriceId": 184,
"taxCode": "V21",
"taxPercentage": 21.0000,
"amountExclTax": 0,
"amountInclTax": 0,
"fromInclusive": "2024-09-01T00:00:00.000+00:00",
"toInclusive": "2024-12-31T23:59:59.000+00:00"
}
],
"auditTrail": [
{
"auditTrailId": 228,
"action": "update",
"user": "api",
"timestamp": "2024-10-21T09:00:30.410+00:00"
},
{
"auditTrailId": 227,
"action": "insert",
"user": "api",
"timestamp": "2024-10-21T08:58:39.237+00:00"
"serviceOptionId": 10,
"action": "refund_notAllowed",
"description": "Terugbetaling niet toegestaan (doorgaans in combinatie met cancel_notAllowed)"
}
],
"validityDuration": "P1W",
"maxStartInFutureDuration": "P1W",
"isRenewable": false,
"sendInvoice": false,
"imageReference": null,
"productPageUrl": null,
"termsUrl": null,
"isSellableAtHtm": true,
"needsSolvencyCheckConsumer": false,
"needsSolvencyCheckBusiness": false,
"sellingPeriods": [
{
"sellingPeriodId": 1384,
"fromInclusive": "2025-12-31T23:00:00.000Z",
"toInclusive": "2026-03-30T22:00:00.000Z",
"salesTouchpoint": {
"salesTouchpointId": 3,
"name": "Website",
"isActive": true,
"retailer": {
"retailerId": 1001,
"name": "HTM externe touchpoints",
"street": "Koningin Julianaplein",
"number": 10,
"numberAddition": null,
"postalCode": "2595 AA",
"city": "Den Haag",
"country": "Nederland",
"emailAddress": "info@htm.nl",
"phoneNumber": "070 374 9002",
"taxId": 572309345923,
"imageReference": "https://www.htm.nl/media/leif2leu/htm-logo-mobile.svg"
}
},
"forbiddenPaymentMethods": [],
"sellingPrices": []
}
],
"purchasePrices": [],
"productVariants": []
},
{
"productId": 665,
"parentProductId": 663,
"layerInfo": {
"layerInfoId": null,
"choiceKey": null,
"choiceLabel": null,
"isCustomChoice": false
},
"fikoArticleNumber": null,
"gboPackageTemplateId": "30001",
"tapConnectProductCode": null,
"productName": "Doorlopend - Test OVPAY-2306",
"productDescription": "Test OVPAY-2306 (sellingPeriods in kindje verwijderen en later opnieuw weer kunnen toevoegen)",
"validityPeriod": {
"validityPeriodId": 784,
"fromInclusive": "2025-12-31T23:00:00.000Z",
"toInclusive": "2026-03-30T22:00:00.000Z"
},
"productTranslations": [],
"allowedGboAgeProfiles": [],
"productOwner": {
"productOwnerId": 1,
"name": "Wie dit leest",
"organization": "... is een aap."
},
"marketSegments": [],
"customerSegments": [],
"productCategory": {
"productCategoryId": 1,
"isTravelProduct": true,
"name": "Kortingsabonnement"
},
"requiredCustomerLevel": {
"requiredCustomerLevelId": 1,
"name": "guest"
},
"requiredProducts": [],
"incompatibleProducts": [],
"mandatoryCustomerDataItems": [],
"requiredGboPersonalAttributes": [],
"tokenTypes": [
{
"tokenTypeId": 1,
"name": "EMV"
}
],
"paymentMoment": {
"paymentMomentId": 1,
"name": "prepaid"
},
"serviceOptions": [
{
"serviceOptionId": 4,
"action": "cancel_notAllowed",
"description": "Stopzetting is niet toegestaan (doorgaans in combinatie met refund_notAllowed)"
},
{
"serviceOptionId": 10,
"action": "refund_notAllowed",
"description": "Terugbetaling niet toegestaan (doorgaans in combinatie met cancel_notAllowed)"
}
],
"validityDuration": "P1W",
"maxStartInFutureDuration": "P1W",
"isRenewable": true,
"sendInvoice": false,
"imageReference": null,
"productPageUrl": null,
"termsUrl": null,
"isSellableAtHtm": true,
"needsSolvencyCheckConsumer": false,
"needsSolvencyCheckBusiness": false,
"sellingPeriods": [],
"purchasePrices": [],
"productVariants": []
}
]
}