diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..122cfe7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [ master, 'feature/**' ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Build and compile + run: mvn clean compile -q + + - name: Run tests (skip MongoDB-dependent tests) + run: mvn test -Dtest="GenericComparisonServiceListTest,GenericComparisonServiceLargeScaleTest" -DfailIfNoTests=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff37778 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +*.xlsx diff --git a/.idea/.gitignore b/.idea/.gitignore index 26d3352..eaf91e2 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,3 @@ -# Default ignored files -/shelf/ -/workspace.xml +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml index f731083..46e4659 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 63e9001..4140949 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 712ab9d..a468a99 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -1,20 +1,20 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 74ec385..c43067d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,12 +1,12 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..9661ac7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/MongoDiffUI.java b/MongoDiffUI.java new file mode 100644 index 0000000..d0e8bdf --- /dev/null +++ b/MongoDiffUI.java @@ -0,0 +1,20 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//JAVA 17+ +//DEPS org.springframework.boot:spring-boot-starter-web:3.2.5 +//DEPS org.springframework.boot:spring-boot-starter-data-mongodb:3.2.5 + +//SOURCES src/main/java/com/example/comparison/ComparisonApplication.java +//SOURCES src/main/java/com/example/comparison/model/Account.java +//SOURCES src/main/java/com/example/comparison/model/ComparisonBreak.java +//SOURCES src/main/java/com/example/comparison/service/GenericComparisonService.java +//SOURCES src/main/java/com/example/comparison/controller/SampleDataController.java + +//FILES static/index.html=src/main/resources/static/index.html + +import com.example.comparison.ComparisonApplication; + +public class MongoDiffUI { + public static void main(String[] args) { + ComparisonApplication.main(args); + } +} diff --git a/license.txt b/license.txt index ce8c174..f1a838d 100644 --- a/license.txt +++ b/license.txt @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 David Olivares - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +MIT License + +Copyright (c) 2024 David Olivares + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pom.xml b/pom.xml index d133ea2..d54c003 100644 --- a/pom.xml +++ b/pom.xml @@ -1,71 +1,77 @@ - - 4.0.0 - - com.example - comparison-app - 1.0.0 - jar - - - org.springframework.boot - spring-boot-starter-parent - 2.7.0 - - - 11 - 11 - 11 - - - - - org.springframework.boot - spring-boot-starter - - - org.apache.poi - poi-ooxml - 5.2.3 - - - - - org.springframework.boot - spring-boot-starter-data-mongodb - - - - - org.springframework.boot - spring-boot-starter-test - test - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo - test - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-compiler-plugin - - 10 - 10 - - - - - + + 4.0.0 + + com.example + comparison-app + 1.0.0 + jar + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + 17 + 17 + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.poi + poi-ooxml + 5.2.3 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + + org.springframework.boot + spring-boot-starter-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring3x + 4.12.2 + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + diff --git a/src/main/java/com/example/comparison/ComparisonApplication.java b/src/main/java/com/example/comparison/ComparisonApplication.java index fbec517..42e1e68 100644 --- a/src/main/java/com/example/comparison/ComparisonApplication.java +++ b/src/main/java/com/example/comparison/ComparisonApplication.java @@ -1,11 +1,11 @@ -package com.example.comparison; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ComparisonApplication { - public static void main(String[] args) { - SpringApplication.run(ComparisonApplication.class, args); - } -} +package com.example.comparison; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ComparisonApplication { + public static void main(String[] args) { + SpringApplication.run(ComparisonApplication.class, args); + } +} diff --git a/src/main/java/com/example/comparison/controller/SampleDataController.java b/src/main/java/com/example/comparison/controller/SampleDataController.java new file mode 100644 index 0000000..cfdf55a --- /dev/null +++ b/src/main/java/com/example/comparison/controller/SampleDataController.java @@ -0,0 +1,263 @@ +package com.example.comparison.controller; + +import com.example.comparison.model.Account; +import com.example.comparison.model.ComparisonBreak; +import com.example.comparison.service.GenericComparisonService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/sample") +@CrossOrigin +public class SampleDataController { + + private static final Logger log = LoggerFactory.getLogger(SampleDataController.class); + + private static final String BASELINE = "accountBaseline"; + private static final String RC = "accountRC"; + private static final String BREAKS = "sampleComparisonBreaks"; + private static final List ATTRIBUTES = Arrays.asList( + "accountName", "accountType", "broker", "creationDate", + "balance", "currency", "riskLevel", "lastTradeDate", + "totalTrades", "availableMargin" + ); + + @Autowired(required = false) + private MongoTemplate mongoTemplate; + + @Autowired + private GenericComparisonService comparisonService; + + private List memBreaks; + private String lastMode; // "db" or "mem" + + @PostMapping("/load") + public Map loadSample() { + log.info("POST /api/sample/load - loading sample data from MongoDB"); + if (mongoTemplate == null) { + throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "MongoDB is not available. Use 'Load Mongo Mem' instead."); + } + long start = System.currentTimeMillis(); + + mongoTemplate.dropCollection(BASELINE); + mongoTemplate.dropCollection(RC); + mongoTemplate.dropCollection(BREAKS); + + Date baseDate = new Date(1630000000000L); + List baselineAccounts = new ArrayList<>(); + List rcAccounts = new ArrayList<>(); + + // 200 matching accounts in both collections + for (int i = 1; i <= 200; i++) { + String accountId = String.format("acct%04d", i); + Account a = generateAccount(accountId, i, baseDate); + Account b = generateAccount(accountId, i, baseDate); + // Plant balance difference in first 30 + if (i <= 30) { + b.setBalance(b.getBalance() + 10.0); + } + baselineAccounts.add(a); + rcAccounts.add(b); + } + + // 5 extra only in baseline (acct0201-acct0205) + for (int i = 201; i <= 205; i++) { + String accountId = String.format("acct%04d", i); + baselineAccounts.add(generateAccount(accountId, i, baseDate)); + } + + // 10 extra only in RC (acct0301-acct0310) + for (int i = 301; i <= 310; i++) { + String accountId = String.format("acct%04d", i); + rcAccounts.add(generateAccount(accountId, i, baseDate)); + } + + mongoTemplate.insert(baselineAccounts, BASELINE); + mongoTemplate.insert(rcAccounts, RC); + + comparisonService.compareCollections( + Account.class, BASELINE, RC, + "accountId", ATTRIBUTES, BREAKS + ); + + long elapsed = System.currentTimeMillis() - start; + lastMode = "db"; + memBreaks = null; + return buildResponseFromDb(elapsed); + } + + @PostMapping("/load-mem") + public Map loadSampleInMemory() { + log.info("POST /api/sample/load-mem - loading sample data in-memory"); + long start = System.currentTimeMillis(); + + Date baseDate = new Date(1630000000000L); + List baselineAccounts = new ArrayList<>(); + List rcAccounts = new ArrayList<>(); + + for (int i = 1; i <= 200; i++) { + String accountId = String.format("acct%04d", i); + Account a = generateAccount(accountId, i, baseDate); + Account b = generateAccount(accountId, i, baseDate); + if (i <= 30) { + b.setBalance(b.getBalance() + 10.0); + } + baselineAccounts.add(a); + rcAccounts.add(b); + } + + for (int i = 201; i <= 205; i++) { + String accountId = String.format("acct%04d", i); + baselineAccounts.add(generateAccount(accountId, i, baseDate)); + } + + for (int i = 301; i <= 310; i++) { + String accountId = String.format("acct%04d", i); + rcAccounts.add(generateAccount(accountId, i, baseDate)); + } + + GenericComparisonService.ListComparisonResult result = + comparisonService.compareLists(baselineAccounts, rcAccounts, "accountId", ATTRIBUTES); + + long elapsed = System.currentTimeMillis() - start; + memBreaks = result.breaks; + lastMode = "mem"; + return buildResponseFromBreaks(memBreaks, elapsed); + } + + @GetMapping("/status") + public Map status() { + log.info("GET /api/sample/status - lastMode={}", lastMode); + if ("mem".equals(lastMode) && memBreaks != null) { + return buildResponseFromBreaks(memBreaks, null); + } + if (mongoTemplate == null || !mongoTemplate.collectionExists(BREAKS)) { + Map empty = new LinkedHashMap<>(); + empty.put("loaded", false); + return empty; + } + return buildResponseFromDb(null); + } + + @GetMapping("/breaks/{comparisonKey}") + public List breaks(@PathVariable String comparisonKey) { + log.info("GET /api/sample/breaks/{} - lastMode={}", comparisonKey, lastMode); + if ("mem".equals(lastMode) && memBreaks != null) { + return memBreaks.stream() + .filter(b -> comparisonKey.equals(b.getComparisonKey())) + .collect(Collectors.toList()); + } + if (mongoTemplate == null) { + return Collections.emptyList(); + } + Query query = Query.query(Criteria.where("comparisonKey").is(comparisonKey)); + return mongoTemplate.find(query, ComparisonBreak.class, BREAKS); + } + + private Map buildResponseFromDb(Long durationMs) { + List all = mongoTemplate.findAll(ComparisonBreak.class, BREAKS); + return buildResponseFromBreaks(all, durationMs); + } + + private Map buildResponseFromBreaks(List all, Long durationMs) { + Map> grouped = all.stream() + .collect(Collectors.groupingBy(ComparisonBreak::getComparisonKey)); + + int totalAttrs = ATTRIBUTES.size(); + String durationStr = durationMs != null ? String.format("%.1fs", durationMs / 1000.0) : null; + + List> scope = new ArrayList<>(); + for (Map.Entry> entry : grouped.entrySet()) { + String key = entry.getKey(); + List keyBreaks = entry.getValue(); + + boolean isMatch = keyBreaks.stream().anyMatch(b -> "match".equals(b.getBreakType())); + boolean isOnlyOnA = keyBreaks.stream().anyMatch(b -> "onlyOnA".equals(b.getBreakType())); + boolean isOnlyOnB = keyBreaks.stream().anyMatch(b -> "onlyOnB".equals(b.getBreakType())); + long diffCount = keyBreaks.stream().filter(b -> "difference".equals(b.getBreakType())).count(); + + int totalFields, matchedFields, breakFields; + if (isOnlyOnA || isOnlyOnB) { + totalFields = 1; + matchedFields = 0; + breakFields = 1; + } else if (isMatch) { + totalFields = totalAttrs; + matchedFields = totalAttrs; + breakFields = 0; + } else { + totalFields = totalAttrs; + breakFields = (int) diffCount; + matchedFields = totalAttrs - breakFields; + } + + int matchPct = totalFields > 0 ? Math.round(matchedFields * 100f / totalFields) : 0; + int breakPct = 100 - matchPct; + + Map item = new LinkedHashMap<>(); + item.put("id", key); + item.put("name", "accountBaseline vs accountRC"); + item.put("category", "Sample Comparison"); + item.put("phase", "completed"); + item.put("matchPct", matchPct); + item.put("breakPct", breakPct); + item.put("totalFields", totalFields); + item.put("matchedFields", matchedFields); + item.put("breakFields", breakFields); + item.put("duration", durationStr); + scope.add(item); + } + + scope.sort(Comparator.comparing(m -> (String) m.get("id"))); + + Map session = new LinkedHashMap<>(); + session.put("name", "Sample Comparison"); + session.put("startedAt", new Date().toInstant().toString()); + session.put("totalIds", scope.size()); + + Map response = new LinkedHashMap<>(); + response.put("loaded", true); + response.put("session", session); + response.put("scope", scope); + return response; + } + + private Account generateAccount(String accountId, int index, Date baseDate) { + Account account = new Account(); + account.setAccountId(accountId); + account.setAccountName("Account " + accountId); + account.setAccountType(index % 2 == 0 ? "Margin" : "Cash"); + account.setBroker("BrokerX"); + long oneDayMillis = 24L * 60 * 60 * 1000; + Date creationDate = new Date(baseDate.getTime() - (index * oneDayMillis)); + account.setCreationDate(creationDate); + account.setBalance(index * 1000.0); + account.setCurrency("USD"); + account.setRiskLevel(index % 3 == 0 ? "High" : "Medium"); + Date lastTradeDate = new Date(creationDate.getTime() + oneDayMillis); + account.setLastTradeDate(lastTradeDate); + account.setTotalTrades(index % 100); + account.setAvailableMargin(account.getBalance() * 0.1); + account.setEmail(accountId + "@example.com"); + account.setPhoneNumber("555-010" + index); + account.setAddress("123 Main St"); + account.setCountry("USA"); + account.setState("CA"); + account.setCity("Los Angeles"); + account.setZipCode("90001"); + account.setInvestmentStyle("Growth"); + account.setAccountStatus("Active"); + return account; + } +} diff --git a/src/main/java/com/example/comparison/model/Account.java b/src/main/java/com/example/comparison/model/Account.java index a6efa11..e3ed7a0 100644 --- a/src/main/java/com/example/comparison/model/Account.java +++ b/src/main/java/com/example/comparison/model/Account.java @@ -1,154 +1,154 @@ -package com.example.comparison.model; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.util.Date; - -@Document(collection = "accountCollection") -public class Account { - @Id - private String accountId; - private String accountName; - private String accountType; - private String broker; - private Date creationDate; - private double balance; - private String currency; - private String riskLevel; - private Date lastTradeDate; - private int totalTrades; - private double availableMargin; - private String email; - private String phoneNumber; - private String address; - private String country; - private String state; - private String city; - private String zipCode; - private String investmentStyle; - private String accountStatus; - - // Getters and Setters - - public String getAccountId() { - return accountId; - } - public void setAccountId(String accountId) { - this.accountId = accountId; - } - public String getAccountName() { - return accountName; - } - public void setAccountName(String accountName) { - this.accountName = accountName; - } - public String getAccountType() { - return accountType; - } - public void setAccountType(String accountType) { - this.accountType = accountType; - } - public String getBroker() { - return broker; - } - public void setBroker(String broker) { - this.broker = broker; - } - public Date getCreationDate() { - return creationDate; - } - public void setCreationDate(Date creationDate) { - this.creationDate = creationDate; - } - public double getBalance() { - return balance; - } - public void setBalance(double balance) { - this.balance = balance; - } - public String getCurrency() { - return currency; - } - public void setCurrency(String currency) { - this.currency = currency; - } - public String getRiskLevel() { - return riskLevel; - } - public void setRiskLevel(String riskLevel) { - this.riskLevel = riskLevel; - } - public Date getLastTradeDate() { - return lastTradeDate; - } - public void setLastTradeDate(Date lastTradeDate) { - this.lastTradeDate = lastTradeDate; - } - public int getTotalTrades() { - return totalTrades; - } - public void setTotalTrades(int totalTrades) { - this.totalTrades = totalTrades; - } - public double getAvailableMargin() { - return availableMargin; - } - public void setAvailableMargin(double availableMargin) { - this.availableMargin = availableMargin; - } - public String getEmail() { - return email; - } - public void setEmail(String email) { - this.email = email; - } - public String getPhoneNumber() { - return phoneNumber; - } - public void setPhoneNumber(String phoneNumber) { - this.phoneNumber = phoneNumber; - } - public String getAddress() { - return address; - } - public void setAddress(String address) { - this.address = address; - } - public String getCountry() { - return country; - } - public void setCountry(String country) { - this.country = country; - } - public String getState() { - return state; - } - public void setState(String state) { - this.state = state; - } - public String getCity() { - return city; - } - public void setCity(String city) { - this.city = city; - } - public String getZipCode() { - return zipCode; - } - public void setZipCode(String zipCode) { - this.zipCode = zipCode; - } - public String getInvestmentStyle() { - return investmentStyle; - } - public void setInvestmentStyle(String investmentStyle) { - this.investmentStyle = investmentStyle; - } - public String getAccountStatus() { - return accountStatus; - } - public void setAccountStatus(String accountStatus) { - this.accountStatus = accountStatus; - } -} +package com.example.comparison.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +@Document(collection = "accountCollection") +public class Account { + @Id + private String accountId; + private String accountName; + private String accountType; + private String broker; + private Date creationDate; + private double balance; + private String currency; + private String riskLevel; + private Date lastTradeDate; + private int totalTrades; + private double availableMargin; + private String email; + private String phoneNumber; + private String address; + private String country; + private String state; + private String city; + private String zipCode; + private String investmentStyle; + private String accountStatus; + + // Getters and Setters + + public String getAccountId() { + return accountId; + } + public void setAccountId(String accountId) { + this.accountId = accountId; + } + public String getAccountName() { + return accountName; + } + public void setAccountName(String accountName) { + this.accountName = accountName; + } + public String getAccountType() { + return accountType; + } + public void setAccountType(String accountType) { + this.accountType = accountType; + } + public String getBroker() { + return broker; + } + public void setBroker(String broker) { + this.broker = broker; + } + public Date getCreationDate() { + return creationDate; + } + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + public double getBalance() { + return balance; + } + public void setBalance(double balance) { + this.balance = balance; + } + public String getCurrency() { + return currency; + } + public void setCurrency(String currency) { + this.currency = currency; + } + public String getRiskLevel() { + return riskLevel; + } + public void setRiskLevel(String riskLevel) { + this.riskLevel = riskLevel; + } + public Date getLastTradeDate() { + return lastTradeDate; + } + public void setLastTradeDate(Date lastTradeDate) { + this.lastTradeDate = lastTradeDate; + } + public int getTotalTrades() { + return totalTrades; + } + public void setTotalTrades(int totalTrades) { + this.totalTrades = totalTrades; + } + public double getAvailableMargin() { + return availableMargin; + } + public void setAvailableMargin(double availableMargin) { + this.availableMargin = availableMargin; + } + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + public String getPhoneNumber() { + return phoneNumber; + } + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + public String getAddress() { + return address; + } + public void setAddress(String address) { + this.address = address; + } + public String getCountry() { + return country; + } + public void setCountry(String country) { + this.country = country; + } + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + public String getCity() { + return city; + } + public void setCity(String city) { + this.city = city; + } + public String getZipCode() { + return zipCode; + } + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + public String getInvestmentStyle() { + return investmentStyle; + } + public void setInvestmentStyle(String investmentStyle) { + this.investmentStyle = investmentStyle; + } + public String getAccountStatus() { + return accountStatus; + } + public void setAccountStatus(String accountStatus) { + this.accountStatus = accountStatus; + } +} diff --git a/src/main/java/com/example/comparison/model/Car.java b/src/main/java/com/example/comparison/model/Car.java index 155fd44..d243b88 100644 --- a/src/main/java/com/example/comparison/model/Car.java +++ b/src/main/java/com/example/comparison/model/Car.java @@ -1,119 +1,119 @@ -package com.example.comparison.model; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.util.Date; - -@Document(collection = "carCollection") -public class Car { - @Id - private String vin; - private String make; - private String model; - private int year; - private String color; - private long mileage; - private String engineType; - private String transmission; - private double price; - private String fuelType; - private String owner; - private Date registrationDate; - private String registrationState; - private Date lastServiceDate; - private String warrantyStatus; - - // Getters and setters - - public String getVin() { - return vin; - } - public void setVin(String vin) { - this.vin = vin; - } - public String getMake() { - return make; - } - public void setMake(String make) { - this.make = make; - } - public String getModel() { - return model; - } - public void setModel(String model) { - this.model = model; - } - public int getYear() { - return year; - } - public void setYear(int year) { - this.year = year; - } - public String getColor() { - return color; - } - public void setColor(String color) { - this.color = color; - } - public long getMileage() { - return mileage; - } - public void setMileage(long mileage) { - this.mileage = mileage; - } - public String getEngineType() { - return engineType; - } - public void setEngineType(String engineType) { - this.engineType = engineType; - } - public String getTransmission() { - return transmission; - } - public void setTransmission(String transmission) { - this.transmission = transmission; - } - public double getPrice() { - return price; - } - public void setPrice(double price) { - this.price = price; - } - public String getFuelType() { - return fuelType; - } - public void setFuelType(String fuelType) { - this.fuelType = fuelType; - } - public String getOwner() { - return owner; - } - public void setOwner(String owner) { - this.owner = owner; - } - public Date getRegistrationDate() { - return registrationDate; - } - public void setRegistrationDate(Date registrationDate) { - this.registrationDate = registrationDate; - } - public String getRegistrationState() { - return registrationState; - } - public void setRegistrationState(String registrationState) { - this.registrationState = registrationState; - } - public Date getLastServiceDate() { - return lastServiceDate; - } - public void setLastServiceDate(Date lastServiceDate) { - this.lastServiceDate = lastServiceDate; - } - public String getWarrantyStatus() { - return warrantyStatus; - } - public void setWarrantyStatus(String warrantyStatus) { - this.warrantyStatus = warrantyStatus; - } -} +package com.example.comparison.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +@Document(collection = "carCollection") +public class Car { + @Id + private String vin; + private String make; + private String model; + private int year; + private String color; + private long mileage; + private String engineType; + private String transmission; + private double price; + private String fuelType; + private String owner; + private Date registrationDate; + private String registrationState; + private Date lastServiceDate; + private String warrantyStatus; + + // Getters and setters + + public String getVin() { + return vin; + } + public void setVin(String vin) { + this.vin = vin; + } + public String getMake() { + return make; + } + public void setMake(String make) { + this.make = make; + } + public String getModel() { + return model; + } + public void setModel(String model) { + this.model = model; + } + public int getYear() { + return year; + } + public void setYear(int year) { + this.year = year; + } + public String getColor() { + return color; + } + public void setColor(String color) { + this.color = color; + } + public long getMileage() { + return mileage; + } + public void setMileage(long mileage) { + this.mileage = mileage; + } + public String getEngineType() { + return engineType; + } + public void setEngineType(String engineType) { + this.engineType = engineType; + } + public String getTransmission() { + return transmission; + } + public void setTransmission(String transmission) { + this.transmission = transmission; + } + public double getPrice() { + return price; + } + public void setPrice(double price) { + this.price = price; + } + public String getFuelType() { + return fuelType; + } + public void setFuelType(String fuelType) { + this.fuelType = fuelType; + } + public String getOwner() { + return owner; + } + public void setOwner(String owner) { + this.owner = owner; + } + public Date getRegistrationDate() { + return registrationDate; + } + public void setRegistrationDate(Date registrationDate) { + this.registrationDate = registrationDate; + } + public String getRegistrationState() { + return registrationState; + } + public void setRegistrationState(String registrationState) { + this.registrationState = registrationState; + } + public Date getLastServiceDate() { + return lastServiceDate; + } + public void setLastServiceDate(Date lastServiceDate) { + this.lastServiceDate = lastServiceDate; + } + public String getWarrantyStatus() { + return warrantyStatus; + } + public void setWarrantyStatus(String warrantyStatus) { + this.warrantyStatus = warrantyStatus; + } +} diff --git a/src/main/java/com/example/comparison/model/ComparisonBreak.java b/src/main/java/com/example/comparison/model/ComparisonBreak.java index 8c77ef8..fb4fc12 100644 --- a/src/main/java/com/example/comparison/model/ComparisonBreak.java +++ b/src/main/java/com/example/comparison/model/ComparisonBreak.java @@ -1,44 +1,44 @@ -package com.example.comparison.model; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -@Document(collection = "accountComparisonBreaks") -public class ComparisonBreak { - @Id - private String id; - private String comparisonKey; - private String differenceField; - private String valueInCollectionA; - private String valueInCollectionB; - private String breakType; // "difference", "onlyOnA", "onlyOnB" - - public ComparisonBreak() {} - - public ComparisonBreak(String comparisonKey, String differenceField, String valueInCollectionA, String valueInCollectionB, String breakType) { - this.comparisonKey = comparisonKey; - this.differenceField = differenceField; - this.valueInCollectionA = valueInCollectionA; - this.valueInCollectionB = valueInCollectionB; - this.breakType = breakType; - } - - // Getters and Setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - - public String getComparisonKey() { return comparisonKey; } - public void setComparisonKey(String comparisonKey) { this.comparisonKey = comparisonKey; } - - public String getDifferenceField() { return differenceField; } - public void setDifferenceField(String differenceField) { this.differenceField = differenceField; } - - public String getValueInCollectionA() { return valueInCollectionA; } - public void setValueInCollectionA(String valueInCollectionA) { this.valueInCollectionA = valueInCollectionA; } - - public String getValueInCollectionB() { return valueInCollectionB; } - public void setValueInCollectionB(String valueInCollectionB) { this.valueInCollectionB = valueInCollectionB; } - - public String getBreakType() { return breakType; } - public void setBreakType(String breakType) { this.breakType = breakType; } -} +package com.example.comparison.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "accountComparisonBreaks") +public class ComparisonBreak { + @Id + private String id; + private String comparisonKey; + private String differenceField; + private String valueInCollectionA; + private String valueInCollectionB; + private String breakType; // "difference", "onlyOnA", "onlyOnB" + + public ComparisonBreak() {} + + public ComparisonBreak(String comparisonKey, String differenceField, String valueInCollectionA, String valueInCollectionB, String breakType) { + this.comparisonKey = comparisonKey; + this.differenceField = differenceField; + this.valueInCollectionA = valueInCollectionA; + this.valueInCollectionB = valueInCollectionB; + this.breakType = breakType; + } + + // Getters and Setters + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getComparisonKey() { return comparisonKey; } + public void setComparisonKey(String comparisonKey) { this.comparisonKey = comparisonKey; } + + public String getDifferenceField() { return differenceField; } + public void setDifferenceField(String differenceField) { this.differenceField = differenceField; } + + public String getValueInCollectionA() { return valueInCollectionA; } + public void setValueInCollectionA(String valueInCollectionA) { this.valueInCollectionA = valueInCollectionA; } + + public String getValueInCollectionB() { return valueInCollectionB; } + public void setValueInCollectionB(String valueInCollectionB) { this.valueInCollectionB = valueInCollectionB; } + + public String getBreakType() { return breakType; } + public void setBreakType(String breakType) { this.breakType = breakType; } +} diff --git a/src/main/java/com/example/comparison/model/MyEntity.java b/src/main/java/com/example/comparison/model/MyEntity.java index 80aa3df..42cae3c 100644 --- a/src/main/java/com/example/comparison/model/MyEntity.java +++ b/src/main/java/com/example/comparison/model/MyEntity.java @@ -1,32 +1,32 @@ -package com.example.comparison.model; - -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -@Document -public class MyEntity { - @Id - private String id; - private String comparisonKey; // Key used for merging/sorting - // Only a few of the many fields we want to compare: - private String field1; - private String field2; - private String field3; - // ... imagine up to 80 fields, with only a subset used for comparison - - // Getters and setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - - public String getComparisonKey() { return comparisonKey; } - public void setComparisonKey(String comparisonKey) { this.comparisonKey = comparisonKey; } - - public String getField1() { return field1; } - public void setField1(String field1) { this.field1 = field1; } - - public String getField2() { return field2; } - public void setField2(String field2) { this.field2 = field2; } - - public String getField3() { return field3; } - public void setField3(String field3) { this.field3 = field3; } -} +package com.example.comparison.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +public class MyEntity { + @Id + private String id; + private String comparisonKey; // Key used for merging/sorting + // Only a few of the many fields we want to compare: + private String field1; + private String field2; + private String field3; + // ... imagine up to 80 fields, with only a subset used for comparison + + // Getters and setters + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getComparisonKey() { return comparisonKey; } + public void setComparisonKey(String comparisonKey) { this.comparisonKey = comparisonKey; } + + public String getField1() { return field1; } + public void setField1(String field1) { this.field1 = field1; } + + public String getField2() { return field2; } + public void setField2(String field2) { this.field2 = field2; } + + public String getField3() { return field3; } + public void setField3(String field3) { this.field3 = field3; } +} diff --git a/src/main/java/com/example/comparison/service/ExcelReportService.java b/src/main/java/com/example/comparison/service/ExcelReportService.java index c177044..74c987b 100644 --- a/src/main/java/com/example/comparison/service/ExcelReportService.java +++ b/src/main/java/com/example/comparison/service/ExcelReportService.java @@ -1,245 +1,245 @@ -package com.example.comparison.service; - -import com.example.comparison.model.ComparisonBreak; -import org.apache.poi.ss.usermodel.*; -import org.apache.poi.ss.util.CellRangeAddress; -import org.apache.poi.xssf.usermodel.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.stereotype.Service; - -import java.lang.reflect.Field; -import java.util.*; -import java.util.stream.Collectors; - -@Service -public class ExcelReportService { - - @Autowired - private MongoTemplate mongoTemplate; - - /** - * Generates an Excel workbook containing: - * - A "Summary" sheet with counts per break type. - * - "onlyOnA" and "onlyOnB" detail sheets. - * - A "difference" sheet that, for each field: - * • Uses two columns if differences exist (with different background colors), - * or one merged column if values are the same. - * - * @param clazz the class of the compared object (e.g. Account.class, Car.class) - * @param keyAttribute the name of the key attribute (must be unique) - * @param collectionA name of the first collection (side A) - * @param collectionB name of the second collection (side B) - * @param breakCollection name of the collection where ComparisonBreak documents are stored - * @return an XSSFWorkbook containing the generated report - */ - public XSSFWorkbook generateExcelReport(Class clazz, - String keyAttribute, - String collectionA, - String collectionB, - String breakCollection) { - // Retrieve all break records. - List allBreaks = mongoTemplate.findAll(ComparisonBreak.class, breakCollection); - - // Group breaks by type for summary. - Map breaksByType = allBreaks.stream() - .collect(Collectors.groupingBy(ComparisonBreak::getBreakType, Collectors.counting())); - long totalBreaks = allBreaks.size(); - - // Group breaks by key. - Map> breaksByKey = allBreaks.stream() - .collect(Collectors.groupingBy(ComparisonBreak::getComparisonKey)); - - XSSFWorkbook workbook = new XSSFWorkbook(); - - // Create sheets. - createSummarySheet(workbook, breaksByType, totalBreaks); - createSimpleDetailSheet(workbook, "onlyOnA", breaksByKey, clazz, keyAttribute, collectionA, null); - createSimpleDetailSheet(workbook, "onlyOnB", breaksByKey, clazz, keyAttribute, null, collectionB); - createDifferenceSheet(workbook, breaksByKey, clazz, keyAttribute, collectionA, collectionB); - - return workbook; - } - - private void createSummarySheet(XSSFWorkbook workbook, Map breaksByType, long totalBreaks) { - XSSFSheet sheet = workbook.createSheet("Summary"); - int rowIndex = 0; - Row header = sheet.createRow(rowIndex++); - header.createCell(0).setCellValue("Break Type"); - header.createCell(1).setCellValue("Count"); - - String[] types = {"onlyOnA", "onlyOnB", "difference"}; - for (String type : types) { - Row row = sheet.createRow(rowIndex++); - row.createCell(0).setCellValue(type); - row.createCell(1).setCellValue(breaksByType.getOrDefault(type, 0L)); - } - Row totalRow = sheet.createRow(rowIndex); - totalRow.createCell(0).setCellValue("Total"); - totalRow.createCell(1).setCellValue(totalBreaks); - } - - private void createSimpleDetailSheet(XSSFWorkbook workbook, - String breakType, - Map> breaksByKey, - Class clazz, - String keyAttribute, - String collectionA, - String collectionB) { - XSSFSheet sheet = workbook.createSheet(breakType); - int rowIndex = 0; - Row header = sheet.createRow(rowIndex++); - int colIndex = 0; - header.createCell(colIndex++).setCellValue("Key"); - - Field[] fields = clazz.getDeclaredFields(); - List fieldNames = new ArrayList<>(); - for (Field f : fields) { - fieldNames.add(f.getName()); - } - if ("onlyOnA".equals(breakType)) { - for (String field : fieldNames) { - header.createCell(colIndex++).setCellValue(field + " (A)"); - } - } else if ("onlyOnB".equals(breakType)) { - for (String field : fieldNames) { - header.createCell(colIndex++).setCellValue(field + " (B)"); - } - } - - for (Map.Entry> entry : breaksByKey.entrySet()) { - String key = entry.getKey(); - List list = entry.getValue(); - boolean hasType = list.stream().anyMatch(b -> b.getBreakType().equals(breakType)); - if (!hasType) continue; - Object objA = (collectionA != null) ? mongoTemplate.findById(key, clazz, collectionA) : null; - Object objB = (collectionB != null) ? mongoTemplate.findById(key, clazz, collectionB) : null; - Row row = sheet.createRow(rowIndex++); - colIndex = 0; - row.createCell(colIndex++).setCellValue(key); - for (String field : fieldNames) { - if ("onlyOnA".equals(breakType)) { - String value = (objA != null) ? getFieldValue(objA, field) : ""; - row.createCell(colIndex++).setCellValue(value); - } else if ("onlyOnB".equals(breakType)) { - String value = (objB != null) ? getFieldValue(objB, field) : ""; - row.createCell(colIndex++).setCellValue(value); - } - } - } - } - - /** - * Creates the "difference" sheet. - * For each field: if any record has a difference, two header columns will be used. - * Then for each record: - * - If the field is different for that record, two cells are shown (with different cell styles). - * - If not, the two cells are merged and the common value is displayed. - */ - private void createDifferenceSheet(XSSFWorkbook workbook, - Map> breaksByKey, - Class clazz, - String keyAttribute, - String collectionA, - String collectionB) { - XSSFSheet sheet = workbook.createSheet("difference"); - int rowIndex = 0; - Row header = sheet.createRow(rowIndex++); - int colIndex = 0; - header.createCell(colIndex++).setCellValue("Key"); - - // Get declared fields. - Field[] fields = clazz.getDeclaredFields(); - List fieldNames = new ArrayList<>(); - for (Field f : fields) { - fieldNames.add(f.getName()); - } - // Determine for each field whether any record has a difference. - Map fieldDiffMap = new HashMap<>(); - for (String field : fieldNames) { - fieldDiffMap.put(field, false); - } - for (List list : breaksByKey.values()) { - for (ComparisonBreak br : list) { - if ("difference".equals(br.getBreakType())) { - fieldDiffMap.put(br.getDifferenceField(), true); - } - } - } - // Build header row. - for (String field : fieldNames) { - if (fieldDiffMap.get(field)) { - // Field may be different in some records: reserve two columns. - header.createCell(colIndex++).setCellValue(field + " (A)"); - header.createCell(colIndex++).setCellValue(field + " (B)"); - } else { - header.createCell(colIndex++).setCellValue(field); - } - } - - // Create cell styles for differences. - CellStyle styleA = workbook.createCellStyle(); - styleA.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex()); - styleA.setFillPattern(FillPatternType.SOLID_FOREGROUND); - - CellStyle styleB = workbook.createCellStyle(); - styleB.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex()); - styleB.setFillPattern(FillPatternType.SOLID_FOREGROUND); - - // Process each record (key) that has a "difference" break. - for (Map.Entry> entry : breaksByKey.entrySet()) { - String key = entry.getKey(); - List breaksForKey = entry.getValue(); - boolean hasDiffForRecord = breaksForKey.stream().anyMatch(b -> "difference".equals(b.getBreakType())); - if (!hasDiffForRecord) continue; - Object objA = (collectionA != null) ? mongoTemplate.findById(key, clazz, collectionA) : null; - Object objB = (collectionB != null) ? mongoTemplate.findById(key, clazz, collectionB) : null; - Row row = sheet.createRow(rowIndex++); - colIndex = 0; - row.createCell(colIndex++).setCellValue(key); - // For each field, check if the field is marked as diff-type. - for (String field : fieldNames) { - boolean isFieldDiffType = fieldDiffMap.get(field); - // For this record, does a difference exist for the field? - boolean isDiffForKey = breaksForKey.stream() - .anyMatch(b -> "difference".equals(b.getBreakType()) && b.getDifferenceField().equals(field)); - if (isFieldDiffType) { - if (isDiffForKey) { - // Field is different: show two cells with different styles. - Cell cellA = row.createCell(colIndex++); - String valueA = (objA != null) ? getFieldValue(objA, field) : ""; - cellA.setCellValue(valueA); - cellA.setCellStyle(styleA); - Cell cellB = row.createCell(colIndex++); - String valueB = (objB != null) ? getFieldValue(objB, field) : ""; - cellB.setCellValue(valueB); - cellB.setCellStyle(styleB); - } else { - // No difference for this record: merge two cells. - Cell cell = row.createCell(colIndex); - String value = (objA != null) ? getFieldValue(objA, field) : ""; - cell.setCellValue(value); - sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), colIndex, colIndex + 1)); - colIndex += 2; - } - } else { - // Field is never different: only one cell. - Cell cell = row.createCell(colIndex++); - String value = (objA != null) ? getFieldValue(objA, field) : ""; - cell.setCellValue(value); - } - } - } - } - - private String getFieldValue(Object obj, String fieldName) { - try { - Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - Object value = field.get(obj); - return (value != null) ? value.toString() : ""; - } catch (Exception e) { - return ""; - } - } -} +package com.example.comparison.service; + +import com.example.comparison.model.ComparisonBreak; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class ExcelReportService { + + @Autowired + private MongoTemplate mongoTemplate; + + /** + * Generates an Excel workbook containing: + * - A "Summary" sheet with counts per break type. + * - "onlyOnA" and "onlyOnB" detail sheets. + * - A "difference" sheet that, for each field: + * • Uses two columns if differences exist (with different background colors), + * or one merged column if values are the same. + * + * @param clazz the class of the compared object (e.g. Account.class, Car.class) + * @param keyAttribute the name of the key attribute (must be unique) + * @param collectionA name of the first collection (side A) + * @param collectionB name of the second collection (side B) + * @param breakCollection name of the collection where ComparisonBreak documents are stored + * @return an XSSFWorkbook containing the generated report + */ + public XSSFWorkbook generateExcelReport(Class clazz, + String keyAttribute, + String collectionA, + String collectionB, + String breakCollection) { + // Retrieve all break records. + List allBreaks = mongoTemplate.findAll(ComparisonBreak.class, breakCollection); + + // Group breaks by type for summary. + Map breaksByType = allBreaks.stream() + .collect(Collectors.groupingBy(ComparisonBreak::getBreakType, Collectors.counting())); + long totalBreaks = allBreaks.size(); + + // Group breaks by key. + Map> breaksByKey = allBreaks.stream() + .collect(Collectors.groupingBy(ComparisonBreak::getComparisonKey)); + + XSSFWorkbook workbook = new XSSFWorkbook(); + + // Create sheets. + createSummarySheet(workbook, breaksByType, totalBreaks); + createSimpleDetailSheet(workbook, "onlyOnA", breaksByKey, clazz, keyAttribute, collectionA, null); + createSimpleDetailSheet(workbook, "onlyOnB", breaksByKey, clazz, keyAttribute, null, collectionB); + createDifferenceSheet(workbook, breaksByKey, clazz, keyAttribute, collectionA, collectionB); + + return workbook; + } + + private void createSummarySheet(XSSFWorkbook workbook, Map breaksByType, long totalBreaks) { + XSSFSheet sheet = workbook.createSheet("Summary"); + int rowIndex = 0; + Row header = sheet.createRow(rowIndex++); + header.createCell(0).setCellValue("Break Type"); + header.createCell(1).setCellValue("Count"); + + String[] types = {"onlyOnA", "onlyOnB", "difference"}; + for (String type : types) { + Row row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(type); + row.createCell(1).setCellValue(breaksByType.getOrDefault(type, 0L)); + } + Row totalRow = sheet.createRow(rowIndex); + totalRow.createCell(0).setCellValue("Total"); + totalRow.createCell(1).setCellValue(totalBreaks); + } + + private void createSimpleDetailSheet(XSSFWorkbook workbook, + String breakType, + Map> breaksByKey, + Class clazz, + String keyAttribute, + String collectionA, + String collectionB) { + XSSFSheet sheet = workbook.createSheet(breakType); + int rowIndex = 0; + Row header = sheet.createRow(rowIndex++); + int colIndex = 0; + header.createCell(colIndex++).setCellValue("Key"); + + Field[] fields = clazz.getDeclaredFields(); + List fieldNames = new ArrayList<>(); + for (Field f : fields) { + fieldNames.add(f.getName()); + } + if ("onlyOnA".equals(breakType)) { + for (String field : fieldNames) { + header.createCell(colIndex++).setCellValue(field + " (A)"); + } + } else if ("onlyOnB".equals(breakType)) { + for (String field : fieldNames) { + header.createCell(colIndex++).setCellValue(field + " (B)"); + } + } + + for (Map.Entry> entry : breaksByKey.entrySet()) { + String key = entry.getKey(); + List list = entry.getValue(); + boolean hasType = list.stream().anyMatch(b -> b.getBreakType().equals(breakType)); + if (!hasType) continue; + Object objA = (collectionA != null) ? mongoTemplate.findById(key, clazz, collectionA) : null; + Object objB = (collectionB != null) ? mongoTemplate.findById(key, clazz, collectionB) : null; + Row row = sheet.createRow(rowIndex++); + colIndex = 0; + row.createCell(colIndex++).setCellValue(key); + for (String field : fieldNames) { + if ("onlyOnA".equals(breakType)) { + String value = (objA != null) ? getFieldValue(objA, field) : ""; + row.createCell(colIndex++).setCellValue(value); + } else if ("onlyOnB".equals(breakType)) { + String value = (objB != null) ? getFieldValue(objB, field) : ""; + row.createCell(colIndex++).setCellValue(value); + } + } + } + } + + /** + * Creates the "difference" sheet. + * For each field: if any record has a difference, two header columns will be used. + * Then for each record: + * - If the field is different for that record, two cells are shown (with different cell styles). + * - If not, the two cells are merged and the common value is displayed. + */ + private void createDifferenceSheet(XSSFWorkbook workbook, + Map> breaksByKey, + Class clazz, + String keyAttribute, + String collectionA, + String collectionB) { + XSSFSheet sheet = workbook.createSheet("difference"); + int rowIndex = 0; + Row header = sheet.createRow(rowIndex++); + int colIndex = 0; + header.createCell(colIndex++).setCellValue("Key"); + + // Get declared fields. + Field[] fields = clazz.getDeclaredFields(); + List fieldNames = new ArrayList<>(); + for (Field f : fields) { + fieldNames.add(f.getName()); + } + // Determine for each field whether any record has a difference. + Map fieldDiffMap = new HashMap<>(); + for (String field : fieldNames) { + fieldDiffMap.put(field, false); + } + for (List list : breaksByKey.values()) { + for (ComparisonBreak br : list) { + if ("difference".equals(br.getBreakType())) { + fieldDiffMap.put(br.getDifferenceField(), true); + } + } + } + // Build header row. + for (String field : fieldNames) { + if (fieldDiffMap.get(field)) { + // Field may be different in some records: reserve two columns. + header.createCell(colIndex++).setCellValue(field + " (A)"); + header.createCell(colIndex++).setCellValue(field + " (B)"); + } else { + header.createCell(colIndex++).setCellValue(field); + } + } + + // Create cell styles for differences. + CellStyle styleA = workbook.createCellStyle(); + styleA.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex()); + styleA.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + CellStyle styleB = workbook.createCellStyle(); + styleB.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex()); + styleB.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + // Process each record (key) that has a "difference" break. + for (Map.Entry> entry : breaksByKey.entrySet()) { + String key = entry.getKey(); + List breaksForKey = entry.getValue(); + boolean hasDiffForRecord = breaksForKey.stream().anyMatch(b -> "difference".equals(b.getBreakType())); + if (!hasDiffForRecord) continue; + Object objA = (collectionA != null) ? mongoTemplate.findById(key, clazz, collectionA) : null; + Object objB = (collectionB != null) ? mongoTemplate.findById(key, clazz, collectionB) : null; + Row row = sheet.createRow(rowIndex++); + colIndex = 0; + row.createCell(colIndex++).setCellValue(key); + // For each field, check if the field is marked as diff-type. + for (String field : fieldNames) { + boolean isFieldDiffType = fieldDiffMap.get(field); + // For this record, does a difference exist for the field? + boolean isDiffForKey = breaksForKey.stream() + .anyMatch(b -> "difference".equals(b.getBreakType()) && b.getDifferenceField().equals(field)); + if (isFieldDiffType) { + if (isDiffForKey) { + // Field is different: show two cells with different styles. + Cell cellA = row.createCell(colIndex++); + String valueA = (objA != null) ? getFieldValue(objA, field) : ""; + cellA.setCellValue(valueA); + cellA.setCellStyle(styleA); + Cell cellB = row.createCell(colIndex++); + String valueB = (objB != null) ? getFieldValue(objB, field) : ""; + cellB.setCellValue(valueB); + cellB.setCellStyle(styleB); + } else { + // No difference for this record: merge two cells. + Cell cell = row.createCell(colIndex); + String value = (objA != null) ? getFieldValue(objA, field) : ""; + cell.setCellValue(value); + sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), colIndex, colIndex + 1)); + colIndex += 2; + } + } else { + // Field is never different: only one cell. + Cell cell = row.createCell(colIndex++); + String value = (objA != null) ? getFieldValue(objA, field) : ""; + cell.setCellValue(value); + } + } + } + } + + private String getFieldValue(Object obj, String fieldName) { + try { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(obj); + return (value != null) ? value.toString() : ""; + } catch (Exception e) { + return ""; + } + } +} diff --git a/src/main/java/com/example/comparison/service/GenericComparisonService.java b/src/main/java/com/example/comparison/service/GenericComparisonService.java index ba24ee5..51ec086 100644 --- a/src/main/java/com/example/comparison/service/GenericComparisonService.java +++ b/src/main/java/com/example/comparison/service/GenericComparisonService.java @@ -1,432 +1,432 @@ -package com.example.comparison.service; - -import com.example.comparison.model.ComparisonBreak; // Ensure this points to your updated model -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeanWrapper; -import org.springframework.beans.BeanWrapperImpl; -import org.springframework.beans.NotReadablePropertyException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.util.CloseableIterator; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; - -@Service -public class GenericComparisonService { - - private static final Logger logger = LoggerFactory.getLogger(GenericComparisonService.class); - - @Autowired(required = false) // Make MongoTemplate optional for non-Spring unit tests - private MongoTemplate mongoTemplate; - - // Setter for MongoTemplate to allow injection in tests if needed, or manual setup - public void setMongoTemplate(MongoTemplate mongoTemplate) { - this.mongoTemplate = mongoTemplate; - } - - - // Helper class to store results from list comparison - public static class ListComparisonResult { - public final List breaks; - public final long itemsProcessedA; - public final long itemsProcessedB; - public final long keysOnlyInA; - public final long keysOnlyInB; - public final long keysWithAttributeMismatch; - public final long fullyMatchedKeys; - public final long totalAttributeDifferences; - - public ListComparisonResult(List breaks, long itemsProcessedA, long itemsProcessedB, - long keysOnlyInA, long keysOnlyInB, long keysWithAttributeMismatch, - long fullyMatchedKeys, long totalAttributeDifferences) { - this.breaks = breaks; - this.itemsProcessedA = itemsProcessedA; - this.itemsProcessedB = itemsProcessedB; - this.keysOnlyInA = keysOnlyInA; - this.keysOnlyInB = keysOnlyInB; - this.keysWithAttributeMismatch = keysWithAttributeMismatch; - this.fullyMatchedKeys = fullyMatchedKeys; - this.totalAttributeDifferences = totalAttributeDifferences; - } - - @Override - public String toString() { - long commonKeys = fullyMatchedKeys + keysWithAttributeMismatch; - return String.format( - "List Comparison Summary:\n" + - " Items Processed from List A: %d\n" + - " Items Processed from List B: %d\n" + - " Keys Only in List A: %d\n" + - " Keys Only in List B: %d\n" + - " Common Keys Found: %d\n" + - " - Fully Matched Keys: %d\n" + - " - Keys with Attribute Mismatches: %d\n" + - " Total Individual Attribute Differences: %d\n" + - " Total ComparisonBreak Records Generated: %d", - itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, - commonKeys, - fullyMatchedKeys, keysWithAttributeMismatch, - totalAttributeDifferences, - breaks.size() - ); - } - } - - - public void compareCollections(Class clazz, - String collectionA, - String collectionB, - String keyAttribute, - List attributesToCompare, - String outputCollectionName) { - if (mongoTemplate == null) { - throw new IllegalStateException("MongoTemplate has not been initialized. Call setMongoTemplate or ensure Spring context is loaded."); - } - - long itemsProcessedA = 0; - long itemsProcessedB = 0; - long keysOnlyInA = 0; - long keysOnlyInB = 0; - long keysWithAttributeMismatch = 0; - long fullyMatchedKeys = 0; - long totalAttributeDifferences = 0; - - Query query = new Query().with(Sort.by(Sort.Direction.ASC, keyAttribute)); - List allBreaksAndMatches = new ArrayList<>(); - - try (CloseableIterator streamA = mongoTemplate.stream(query, clazz, collectionA); - CloseableIterator streamB = mongoTemplate.stream(query, clazz, collectionB)) { - - Iterator iteratorA = streamA; - Iterator iteratorB = streamB; - - T currentA = null; - if (iteratorA.hasNext()) { - currentA = iteratorA.next(); - itemsProcessedA++; - } - T currentB = null; - if (iteratorB.hasNext()) { - currentB = iteratorB.next(); - itemsProcessedB++; - } - - while (currentA != null || currentB != null) { - if (currentA != null && currentB != null) { - Comparable keyA = getKeyValue(currentA, keyAttribute, collectionA); - Comparable keyB = getKeyValue(currentB, keyAttribute, collectionB); - - int cmp = compareKeys(keyA, keyB, keyAttribute); - - String keyAStr = (keyA == null) ? "null" : keyA.toString(); - String keyBStr = (keyB == null) ? "null" : keyB.toString(); - - if (cmp == 0) { - int individualDiffsForKey = recordAttributeDifferences(currentA, currentB, keyAStr, attributesToCompare, allBreaksAndMatches); - if (individualDiffsForKey == 0) { - fullyMatchedKeys++; - // For a "match", differenceField, valueA, valueB are null. - allBreaksAndMatches.add(new ComparisonBreak(keyAStr, null, null, null, "match")); - } else { - keysWithAttributeMismatch++; - totalAttributeDifferences += individualDiffsForKey; - } - currentA = iteratorA.hasNext() ? iteratorA.next() : null; - if (currentA != null) itemsProcessedA++; - currentB = iteratorB.hasNext() ? iteratorB.next() : null; - if (currentB != null) itemsProcessedB++; - } else if (cmp < 0) { - keysOnlyInA++; - // differenceField="RecordMissing", valueA="exists", valueB="missing" - allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); - currentA = iteratorA.hasNext() ? iteratorA.next() : null; - if (currentA != null) itemsProcessedA++; - } else { // cmp > 0 - keysOnlyInB++; - // differenceField="RecordMissing", valueA="missing", valueB="exists" - allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); - currentB = iteratorB.hasNext() ? iteratorB.next() : null; - if (currentB != null) itemsProcessedB++; - } - } else if (currentA != null) { - keysOnlyInA++; - Comparable keyA = getKeyValue(currentA, keyAttribute, collectionA); - String keyAStr = (keyA == null) ? "null" : keyA.toString(); - allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); - currentA = iteratorA.hasNext() ? iteratorA.next() : null; - if (currentA != null) itemsProcessedA++; - } else { // currentB must be non-null - keysOnlyInB++; - Comparable keyB = getKeyValue(currentB, keyAttribute, collectionB); - String keyBStr = (keyB == null) ? "null" : keyB.toString(); - allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); - currentB = iteratorB.hasNext() ? iteratorB.next() : null; - if (currentB != null) itemsProcessedB++; - } - } - - if (!allBreaksAndMatches.isEmpty()) { - mongoTemplate.insert(allBreaksAndMatches, outputCollectionName); - logger.info("Comparison results for collections '{}' and '{}' (key: '{}') stored in '{}'.", - collectionA, collectionB, keyAttribute, outputCollectionName); - } else { - logger.info("Comparison for collections '{}' and '{}' (key: '{}'): No differences, unique items, or matches found to report to collection '{}'.", - collectionA, collectionB, keyAttribute, outputCollectionName); - } - - } catch (Exception e) { - logger.error("Error during MongoDB collection comparison between {} and {}: {}", collectionA, collectionB, e.getMessage(), e); - throw new RuntimeException("Failed to compare MongoDB collections " + collectionA + " and " + collectionB, e); - } - - logSummary("MongoDB Collection Comparison", collectionA, collectionB, keyAttribute, - itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, - keysWithAttributeMismatch, fullyMatchedKeys, totalAttributeDifferences, - allBreaksAndMatches.size(), outputCollectionName); - } - - public ListComparisonResult compareLists(List listA, List listB, - String keyAttribute, - List attributesToCompare) { - long itemsProcessedA = 0; - long itemsProcessedB = 0; - long keysOnlyInA = 0; - long keysOnlyInB = 0; - long keysWithAttributeMismatch = 0; - long fullyMatchedKeys = 0; - long totalAttributeDifferences = 0; - List allBreaksAndMatches = new ArrayList<>(); - - Comparator keyComparator = (o1, o2) -> { - if (o1 == null && o2 == null) return 0; - if (o1 == null) return -1; - if (o2 == null) return 1; - - Comparable key1 = getKeyValue(o1, keyAttribute, "listA_internal_sort"); - Comparable key2 = getKeyValue(o2, keyAttribute, "listB_internal_sort"); - return compareKeys(key1, key2, keyAttribute); - }; - - List sortedA = new ArrayList<>(listA); - List sortedB = new ArrayList<>(listB); - try { - sortedA.sort(keyComparator); - sortedB.sort(keyComparator); - } catch (IllegalArgumentException e) { - logger.error("Error during list pre-sort for key attribute '{}': {}. Ensure key attribute is Comparable.", keyAttribute, e.getMessage(), e); - throw new RuntimeException("Failed to sort lists for comparison due to non-Comparable key: " + keyAttribute, e); - } - - - Iterator iteratorA = sortedA.iterator(); - Iterator iteratorB = sortedB.iterator(); - - T currentA = null; - if (iteratorA.hasNext()) { - currentA = iteratorA.next(); - itemsProcessedA++; - } - T currentB = null; - if (iteratorB.hasNext()) { - currentB = iteratorB.next(); - itemsProcessedB++; - } - - try { - while (currentA != null || currentB != null) { - if (currentA != null && currentB != null) { - Comparable keyA = getKeyValue(currentA, keyAttribute, "listA"); - Comparable keyB = getKeyValue(currentB, keyAttribute, "listB"); - - int cmp = compareKeys(keyA, keyB, keyAttribute); - - String keyAStr = (keyA == null) ? "null" : keyA.toString(); - String keyBStr = (keyB == null) ? "null" : keyB.toString(); - - if (cmp == 0) { - int individualDiffsForKey = recordAttributeDifferences(currentA, currentB, keyAStr, attributesToCompare, allBreaksAndMatches); - if (individualDiffsForKey == 0) { - fullyMatchedKeys++; - allBreaksAndMatches.add(new ComparisonBreak(keyAStr, null, null, null, "match")); - } else { - keysWithAttributeMismatch++; - totalAttributeDifferences += individualDiffsForKey; - } - currentA = iteratorA.hasNext() ? iteratorA.next() : null; - if (currentA != null) itemsProcessedA++; - currentB = iteratorB.hasNext() ? iteratorB.next() : null; - if (currentB != null) itemsProcessedB++; - } else if (cmp < 0) { - keysOnlyInA++; - allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); - currentA = iteratorA.hasNext() ? iteratorA.next() : null; - if (currentA != null) itemsProcessedA++; - } else { // cmp > 0 - keysOnlyInB++; - allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); - currentB = iteratorB.hasNext() ? iteratorB.next() : null; - if (currentB != null) itemsProcessedB++; - } - } else if (currentA != null) { - keysOnlyInA++; - Comparable keyA = getKeyValue(currentA, keyAttribute, "listA"); - String keyAStr = (keyA == null) ? "null" : keyA.toString(); - allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); - currentA = iteratorA.hasNext() ? iteratorA.next() : null; - if (currentA != null) itemsProcessedA++; - } else { // currentB must be non-null - keysOnlyInB++; - Comparable keyB = getKeyValue(currentB, keyAttribute, "listB"); - String keyBStr = (keyB == null) ? "null" : keyB.toString(); - allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); - currentB = iteratorB.hasNext() ? iteratorB.next() : null; - if (currentB != null) itemsProcessedB++; - } - } - } catch (Exception e) { - logger.error("Error during Java list comparison (key: {}): {}", keyAttribute, e.getMessage(), e); - throw new RuntimeException("Failed to compare lists with key attribute " + keyAttribute, e); - } - - ListComparisonResult result = new ListComparisonResult<>( - allBreaksAndMatches, itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, - keysWithAttributeMismatch, fullyMatchedKeys, totalAttributeDifferences - ); - - logSummary("Java List Comparison", "List A", "List B", keyAttribute, - itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, - keysWithAttributeMismatch, fullyMatchedKeys, totalAttributeDifferences, - allBreaksAndMatches.size(), null); - - return result; - } - - private int compareKeys(Comparable keyA, Comparable keyB, String keyAttribute) { - if (keyA == null && keyB == null) { - return 0; - } else if (keyA == null) { - return -1; - } else if (keyB == null) { - return 1; - } else { - try { - //noinspection unchecked,rawtypes - return ((Comparable)keyA).compareTo(keyB); - } catch (ClassCastException e) { - logger.warn("ClassCastException during key comparison for key attribute '{}'. " + - "Key A: '{}' (type {}), Key B: '{}' (type {}). " + - "Falling back to String comparison.", - keyAttribute, keyA, keyA.getClass().getName(), keyB, keyB.getClass().getName(), e); - return String.valueOf(keyA).compareTo(String.valueOf(keyB)); - } - } - } - - private Comparable getKeyValue(T object, String keyAttribute, String sourceHint) { - if (object == null) { - logger.warn("Encountered a null object from source '{}' while trying to get key attribute '{}'. Treating key as null.", sourceHint, keyAttribute); - return null; - } - BeanWrapper wrapper = new BeanWrapperImpl(object); - Object keyValue; - try { - keyValue = wrapper.getPropertyValue(keyAttribute); - } catch (NotReadablePropertyException e) { - logger.trace("Key attribute '{}' not found on an object from source '{}'. Treating key as null. Object: {}", keyAttribute, sourceHint, object, e); - return null; - } - - if (keyValue == null) { - return null; - } - - if (keyValue instanceof Comparable) { - return (Comparable) keyValue; - } - - String errorMessage = String.format( - "Key attribute '%s' from source '%s' yielded a non-null value of type '%s' which is not Comparable. Value: '%s'. Object: %s", - keyAttribute, sourceHint, keyValue.getClass().getName(), keyValue.toString(), object.toString() - ); - logger.error(errorMessage); - throw new IllegalArgumentException(errorMessage); - } - - private int recordAttributeDifferences(T a, - T b, - String comparisonKey, // Renamed from 'key' to match model conceptually - List attributesToCompare, - List differencesOutputList) { - BeanWrapper wrapperA = new BeanWrapperImpl(a); - BeanWrapper wrapperB = new BeanWrapperImpl(b); - int currentKeyDifferences = 0; - - for (String attr : attributesToCompare) { - Object valueAObj = null; - boolean attrAMissing = false; - try { - valueAObj = wrapperA.getPropertyValue(attr); - } catch (NotReadablePropertyException e) { - attrAMissing = true; - logger.trace("Attribute '{}' not readable from object in source A for key '{}'. Assuming null for comparison.", attr, comparisonKey); - } - - Object valueBObj = null; - boolean attrBMissing = false; - try { - valueBObj = wrapperB.getPropertyValue(attr); - } catch (NotReadablePropertyException e) { - attrBMissing = true; - logger.trace("Attribute '{}' not readable from object in source B for key '{}'. Assuming null for comparison.", attr, comparisonKey); - } - - if (!Objects.equals(valueAObj, valueBObj)) { - String valueInCollectionA = attrAMissing ? "[[missing]]" : (valueAObj == null ? "null" : valueAObj.toString()); - String valueInCollectionB = attrBMissing ? "[[missing]]" : (valueBObj == null ? "null" : valueBObj.toString()); - String differenceField = attr; // The attribute name that differs - - differencesOutputList.add(new ComparisonBreak( - comparisonKey, - differenceField, - valueInCollectionA, - valueInCollectionB, - "difference" // breakType - )); - currentKeyDifferences++; - } - } - return currentKeyDifferences; - } - - private void logSummary(String comparisonTitle, String sourceAName, String sourceBName, String keyAttribute, - long itemsProcessedA, long itemsProcessedB, long keysOnlyInA, long keysOnlyInB, - long keysWithAttributeMismatch, long fullyMatchedKeys, long totalAttributeDifferences, - long totalBreaksWritten, String outputTargetName) { - - long commonKeys = fullyMatchedKeys + keysWithAttributeMismatch; - StringBuilder summary = new StringBuilder(); - summary.append(String.format("%s Summary (Key: '%s'):\n", comparisonTitle, keyAttribute)); - summary.append(String.format(" Source A ('%s') Items Processed: %d\n", sourceAName, itemsProcessedA)); - summary.append(String.format(" Source B ('%s') Items Processed: %d\n", sourceBName, itemsProcessedB)); - summary.append(String.format(" Keys Only in A: %d\n", keysOnlyInA)); - summary.append(String.format(" Keys Only in B: %d\n", keysOnlyInB)); - summary.append(String.format(" Common Keys Found: %d\n", commonKeys)); - summary.append(String.format(" - Fully Matched Keys: %d\n", fullyMatchedKeys)); - summary.append(String.format(" - Keys with Attribute Mismatches: %d\n", keysWithAttributeMismatch)); - summary.append(String.format(" Total Individual Attribute Differences: %d\n", totalAttributeDifferences)); - if (outputTargetName != null) { - summary.append(String.format(" Total Records Written to '%s': %d", outputTargetName, totalBreaksWritten)); - } else { - summary.append(String.format(" Total ComparisonBreak Records Generated: %d", totalBreaksWritten)); - } - logger.info(summary.toString()); - } +package com.example.comparison.service; + +import com.example.comparison.model.ComparisonBreak; // Ensure this points to your updated model +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +@Service +public class GenericComparisonService { + + private static final Logger logger = LoggerFactory.getLogger(GenericComparisonService.class); + + @Autowired(required = false) // Make MongoTemplate optional for non-Spring unit tests + private MongoTemplate mongoTemplate; + + // Setter for MongoTemplate to allow injection in tests if needed, or manual setup + public void setMongoTemplate(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + + // Helper class to store results from list comparison + public static class ListComparisonResult { + public final List breaks; + public final long itemsProcessedA; + public final long itemsProcessedB; + public final long keysOnlyInA; + public final long keysOnlyInB; + public final long keysWithAttributeMismatch; + public final long fullyMatchedKeys; + public final long totalAttributeDifferences; + + public ListComparisonResult(List breaks, long itemsProcessedA, long itemsProcessedB, + long keysOnlyInA, long keysOnlyInB, long keysWithAttributeMismatch, + long fullyMatchedKeys, long totalAttributeDifferences) { + this.breaks = breaks; + this.itemsProcessedA = itemsProcessedA; + this.itemsProcessedB = itemsProcessedB; + this.keysOnlyInA = keysOnlyInA; + this.keysOnlyInB = keysOnlyInB; + this.keysWithAttributeMismatch = keysWithAttributeMismatch; + this.fullyMatchedKeys = fullyMatchedKeys; + this.totalAttributeDifferences = totalAttributeDifferences; + } + + @Override + public String toString() { + long commonKeys = fullyMatchedKeys + keysWithAttributeMismatch; + return String.format( + "List Comparison Summary:\n" + + " Items Processed from List A: %d\n" + + " Items Processed from List B: %d\n" + + " Keys Only in List A: %d\n" + + " Keys Only in List B: %d\n" + + " Common Keys Found: %d\n" + + " - Fully Matched Keys: %d\n" + + " - Keys with Attribute Mismatches: %d\n" + + " Total Individual Attribute Differences: %d\n" + + " Total ComparisonBreak Records Generated: %d", + itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, + commonKeys, + fullyMatchedKeys, keysWithAttributeMismatch, + totalAttributeDifferences, + breaks.size() + ); + } + } + + + public void compareCollections(Class clazz, + String collectionA, + String collectionB, + String keyAttribute, + List attributesToCompare, + String outputCollectionName) { + if (mongoTemplate == null) { + throw new IllegalStateException("MongoTemplate has not been initialized. Call setMongoTemplate or ensure Spring context is loaded."); + } + + long itemsProcessedA = 0; + long itemsProcessedB = 0; + long keysOnlyInA = 0; + long keysOnlyInB = 0; + long keysWithAttributeMismatch = 0; + long fullyMatchedKeys = 0; + long totalAttributeDifferences = 0; + + Query query = new Query().with(Sort.by(Sort.Direction.ASC, keyAttribute)); + List allBreaksAndMatches = new ArrayList<>(); + + try (Stream streamA = mongoTemplate.stream(query, clazz, collectionA); + Stream streamB = mongoTemplate.stream(query, clazz, collectionB)) { + + Iterator iteratorA = streamA.iterator(); + Iterator iteratorB = streamB.iterator(); + + T currentA = null; + if (iteratorA.hasNext()) { + currentA = iteratorA.next(); + itemsProcessedA++; + } + T currentB = null; + if (iteratorB.hasNext()) { + currentB = iteratorB.next(); + itemsProcessedB++; + } + + while (currentA != null || currentB != null) { + if (currentA != null && currentB != null) { + Comparable keyA = getKeyValue(currentA, keyAttribute, collectionA); + Comparable keyB = getKeyValue(currentB, keyAttribute, collectionB); + + int cmp = compareKeys(keyA, keyB, keyAttribute); + + String keyAStr = (keyA == null) ? "null" : keyA.toString(); + String keyBStr = (keyB == null) ? "null" : keyB.toString(); + + if (cmp == 0) { + int individualDiffsForKey = recordAttributeDifferences(currentA, currentB, keyAStr, attributesToCompare, allBreaksAndMatches); + if (individualDiffsForKey == 0) { + fullyMatchedKeys++; + // For a "match", differenceField, valueA, valueB are null. + allBreaksAndMatches.add(new ComparisonBreak(keyAStr, null, null, null, "match")); + } else { + keysWithAttributeMismatch++; + totalAttributeDifferences += individualDiffsForKey; + } + currentA = iteratorA.hasNext() ? iteratorA.next() : null; + if (currentA != null) itemsProcessedA++; + currentB = iteratorB.hasNext() ? iteratorB.next() : null; + if (currentB != null) itemsProcessedB++; + } else if (cmp < 0) { + keysOnlyInA++; + // differenceField="RecordMissing", valueA="exists", valueB="missing" + allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); + currentA = iteratorA.hasNext() ? iteratorA.next() : null; + if (currentA != null) itemsProcessedA++; + } else { // cmp > 0 + keysOnlyInB++; + // differenceField="RecordMissing", valueA="missing", valueB="exists" + allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); + currentB = iteratorB.hasNext() ? iteratorB.next() : null; + if (currentB != null) itemsProcessedB++; + } + } else if (currentA != null) { + keysOnlyInA++; + Comparable keyA = getKeyValue(currentA, keyAttribute, collectionA); + String keyAStr = (keyA == null) ? "null" : keyA.toString(); + allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); + currentA = iteratorA.hasNext() ? iteratorA.next() : null; + if (currentA != null) itemsProcessedA++; + } else { // currentB must be non-null + keysOnlyInB++; + Comparable keyB = getKeyValue(currentB, keyAttribute, collectionB); + String keyBStr = (keyB == null) ? "null" : keyB.toString(); + allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); + currentB = iteratorB.hasNext() ? iteratorB.next() : null; + if (currentB != null) itemsProcessedB++; + } + } + + if (!allBreaksAndMatches.isEmpty()) { + mongoTemplate.insert(allBreaksAndMatches, outputCollectionName); + logger.info("Comparison results for collections '{}' and '{}' (key: '{}') stored in '{}'.", + collectionA, collectionB, keyAttribute, outputCollectionName); + } else { + logger.info("Comparison for collections '{}' and '{}' (key: '{}'): No differences, unique items, or matches found to report to collection '{}'.", + collectionA, collectionB, keyAttribute, outputCollectionName); + } + + } catch (Exception e) { + logger.error("Error during MongoDB collection comparison between {} and {}: {}", collectionA, collectionB, e.getMessage(), e); + throw new RuntimeException("Failed to compare MongoDB collections " + collectionA + " and " + collectionB, e); + } + + logSummary("MongoDB Collection Comparison", collectionA, collectionB, keyAttribute, + itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, + keysWithAttributeMismatch, fullyMatchedKeys, totalAttributeDifferences, + allBreaksAndMatches.size(), outputCollectionName); + } + + public ListComparisonResult compareLists(List listA, List listB, + String keyAttribute, + List attributesToCompare) { + long itemsProcessedA = 0; + long itemsProcessedB = 0; + long keysOnlyInA = 0; + long keysOnlyInB = 0; + long keysWithAttributeMismatch = 0; + long fullyMatchedKeys = 0; + long totalAttributeDifferences = 0; + List allBreaksAndMatches = new ArrayList<>(); + + Comparator keyComparator = (o1, o2) -> { + if (o1 == null && o2 == null) return 0; + if (o1 == null) return -1; + if (o2 == null) return 1; + + Comparable key1 = getKeyValue(o1, keyAttribute, "listA_internal_sort"); + Comparable key2 = getKeyValue(o2, keyAttribute, "listB_internal_sort"); + return compareKeys(key1, key2, keyAttribute); + }; + + List sortedA = new ArrayList<>(listA); + List sortedB = new ArrayList<>(listB); + try { + sortedA.sort(keyComparator); + sortedB.sort(keyComparator); + } catch (IllegalArgumentException e) { + logger.error("Error during list pre-sort for key attribute '{}': {}. Ensure key attribute is Comparable.", keyAttribute, e.getMessage(), e); + throw new RuntimeException("Failed to sort lists for comparison due to non-Comparable key: " + keyAttribute, e); + } + + + Iterator iteratorA = sortedA.iterator(); + Iterator iteratorB = sortedB.iterator(); + + T currentA = null; + if (iteratorA.hasNext()) { + currentA = iteratorA.next(); + itemsProcessedA++; + } + T currentB = null; + if (iteratorB.hasNext()) { + currentB = iteratorB.next(); + itemsProcessedB++; + } + + try { + while (currentA != null || currentB != null) { + if (currentA != null && currentB != null) { + Comparable keyA = getKeyValue(currentA, keyAttribute, "listA"); + Comparable keyB = getKeyValue(currentB, keyAttribute, "listB"); + + int cmp = compareKeys(keyA, keyB, keyAttribute); + + String keyAStr = (keyA == null) ? "null" : keyA.toString(); + String keyBStr = (keyB == null) ? "null" : keyB.toString(); + + if (cmp == 0) { + int individualDiffsForKey = recordAttributeDifferences(currentA, currentB, keyAStr, attributesToCompare, allBreaksAndMatches); + if (individualDiffsForKey == 0) { + fullyMatchedKeys++; + allBreaksAndMatches.add(new ComparisonBreak(keyAStr, null, null, null, "match")); + } else { + keysWithAttributeMismatch++; + totalAttributeDifferences += individualDiffsForKey; + } + currentA = iteratorA.hasNext() ? iteratorA.next() : null; + if (currentA != null) itemsProcessedA++; + currentB = iteratorB.hasNext() ? iteratorB.next() : null; + if (currentB != null) itemsProcessedB++; + } else if (cmp < 0) { + keysOnlyInA++; + allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); + currentA = iteratorA.hasNext() ? iteratorA.next() : null; + if (currentA != null) itemsProcessedA++; + } else { // cmp > 0 + keysOnlyInB++; + allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); + currentB = iteratorB.hasNext() ? iteratorB.next() : null; + if (currentB != null) itemsProcessedB++; + } + } else if (currentA != null) { + keysOnlyInA++; + Comparable keyA = getKeyValue(currentA, keyAttribute, "listA"); + String keyAStr = (keyA == null) ? "null" : keyA.toString(); + allBreaksAndMatches.add(new ComparisonBreak(keyAStr, "RecordMissing", "exists", "missing", "onlyOnA")); + currentA = iteratorA.hasNext() ? iteratorA.next() : null; + if (currentA != null) itemsProcessedA++; + } else { // currentB must be non-null + keysOnlyInB++; + Comparable keyB = getKeyValue(currentB, keyAttribute, "listB"); + String keyBStr = (keyB == null) ? "null" : keyB.toString(); + allBreaksAndMatches.add(new ComparisonBreak(keyBStr, "RecordMissing", "missing", "exists", "onlyOnB")); + currentB = iteratorB.hasNext() ? iteratorB.next() : null; + if (currentB != null) itemsProcessedB++; + } + } + } catch (Exception e) { + logger.error("Error during Java list comparison (key: {}): {}", keyAttribute, e.getMessage(), e); + throw new RuntimeException("Failed to compare lists with key attribute " + keyAttribute, e); + } + + ListComparisonResult result = new ListComparisonResult<>( + allBreaksAndMatches, itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, + keysWithAttributeMismatch, fullyMatchedKeys, totalAttributeDifferences + ); + + logSummary("Java List Comparison", "List A", "List B", keyAttribute, + itemsProcessedA, itemsProcessedB, keysOnlyInA, keysOnlyInB, + keysWithAttributeMismatch, fullyMatchedKeys, totalAttributeDifferences, + allBreaksAndMatches.size(), null); + + return result; + } + + private int compareKeys(Comparable keyA, Comparable keyB, String keyAttribute) { + if (keyA == null && keyB == null) { + return 0; + } else if (keyA == null) { + return -1; + } else if (keyB == null) { + return 1; + } else { + try { + //noinspection unchecked,rawtypes + return ((Comparable)keyA).compareTo(keyB); + } catch (ClassCastException e) { + logger.warn("ClassCastException during key comparison for key attribute '{}'. " + + "Key A: '{}' (type {}), Key B: '{}' (type {}). " + + "Falling back to String comparison.", + keyAttribute, keyA, keyA.getClass().getName(), keyB, keyB.getClass().getName(), e); + return String.valueOf(keyA).compareTo(String.valueOf(keyB)); + } + } + } + + private Comparable getKeyValue(T object, String keyAttribute, String sourceHint) { + if (object == null) { + logger.warn("Encountered a null object from source '{}' while trying to get key attribute '{}'. Treating key as null.", sourceHint, keyAttribute); + return null; + } + BeanWrapper wrapper = new BeanWrapperImpl(object); + Object keyValue; + try { + keyValue = wrapper.getPropertyValue(keyAttribute); + } catch (NotReadablePropertyException e) { + logger.trace("Key attribute '{}' not found on an object from source '{}'. Treating key as null. Object: {}", keyAttribute, sourceHint, object, e); + return null; + } + + if (keyValue == null) { + return null; + } + + if (keyValue instanceof Comparable) { + return (Comparable) keyValue; + } + + String errorMessage = String.format( + "Key attribute '%s' from source '%s' yielded a non-null value of type '%s' which is not Comparable. Value: '%s'. Object: %s", + keyAttribute, sourceHint, keyValue.getClass().getName(), keyValue.toString(), object.toString() + ); + logger.error(errorMessage); + throw new IllegalArgumentException(errorMessage); + } + + private int recordAttributeDifferences(T a, + T b, + String comparisonKey, // Renamed from 'key' to match model conceptually + List attributesToCompare, + List differencesOutputList) { + BeanWrapper wrapperA = new BeanWrapperImpl(a); + BeanWrapper wrapperB = new BeanWrapperImpl(b); + int currentKeyDifferences = 0; + + for (String attr : attributesToCompare) { + Object valueAObj = null; + boolean attrAMissing = false; + try { + valueAObj = wrapperA.getPropertyValue(attr); + } catch (NotReadablePropertyException e) { + attrAMissing = true; + logger.trace("Attribute '{}' not readable from object in source A for key '{}'. Assuming null for comparison.", attr, comparisonKey); + } + + Object valueBObj = null; + boolean attrBMissing = false; + try { + valueBObj = wrapperB.getPropertyValue(attr); + } catch (NotReadablePropertyException e) { + attrBMissing = true; + logger.trace("Attribute '{}' not readable from object in source B for key '{}'. Assuming null for comparison.", attr, comparisonKey); + } + + if (!Objects.equals(valueAObj, valueBObj)) { + String valueInCollectionA = attrAMissing ? "[[missing]]" : (valueAObj == null ? "null" : valueAObj.toString()); + String valueInCollectionB = attrBMissing ? "[[missing]]" : (valueBObj == null ? "null" : valueBObj.toString()); + String differenceField = attr; // The attribute name that differs + + differencesOutputList.add(new ComparisonBreak( + comparisonKey, + differenceField, + valueInCollectionA, + valueInCollectionB, + "difference" // breakType + )); + currentKeyDifferences++; + } + } + return currentKeyDifferences; + } + + private void logSummary(String comparisonTitle, String sourceAName, String sourceBName, String keyAttribute, + long itemsProcessedA, long itemsProcessedB, long keysOnlyInA, long keysOnlyInB, + long keysWithAttributeMismatch, long fullyMatchedKeys, long totalAttributeDifferences, + long totalBreaksWritten, String outputTargetName) { + + long commonKeys = fullyMatchedKeys + keysWithAttributeMismatch; + StringBuilder summary = new StringBuilder(); + summary.append(String.format("%s Summary (Key: '%s'):\n", comparisonTitle, keyAttribute)); + summary.append(String.format(" Source A ('%s') Items Processed: %d\n", sourceAName, itemsProcessedA)); + summary.append(String.format(" Source B ('%s') Items Processed: %d\n", sourceBName, itemsProcessedB)); + summary.append(String.format(" Keys Only in A: %d\n", keysOnlyInA)); + summary.append(String.format(" Keys Only in B: %d\n", keysOnlyInB)); + summary.append(String.format(" Common Keys Found: %d\n", commonKeys)); + summary.append(String.format(" - Fully Matched Keys: %d\n", fullyMatchedKeys)); + summary.append(String.format(" - Keys with Attribute Mismatches: %d\n", keysWithAttributeMismatch)); + summary.append(String.format(" Total Individual Attribute Differences: %d\n", totalAttributeDifferences)); + if (outputTargetName != null) { + summary.append(String.format(" Total Records Written to '%s': %d", outputTargetName, totalBreaksWritten)); + } else { + summary.append(String.format(" Total ComparisonBreak Records Generated: %d", totalBreaksWritten)); + } + logger.info(summary.toString()); + } } \ No newline at end of file diff --git a/src/main/java/infra/MongoInspectorService.java b/src/main/java/infra/MongoInspectorService.java index 5dbc5d0..b83f6d1 100644 --- a/src/main/java/infra/MongoInspectorService.java +++ b/src/main/java/infra/MongoInspectorService.java @@ -1,99 +1,99 @@ -package infra; - -import com.mongodb.ConnectionString; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.model.Sorts; -import org.bson.Document; -import org.bson.conversions.Bson; -import org.bson.types.ObjectId; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -import java.util.Arrays; -import java.util.Date; -import java.util.List; - -@Component -public class MongoInspectorService implements CommandLineRunner { - - // List of MongoDB connection strings. Each URI is assumed to include the database name. - private final List mongoUris = Arrays.asList( - "mongodb://localhost:27017/db1", - "mongodb://localhost:27017/db2" - ); - - @Override - public void run(String... args) { - for (String uri : mongoUris) { - // Parse the connection string to extract the default database name. - ConnectionString connectionString = new ConnectionString(uri); - String dbName = connectionString.getDatabase(); - System.out.println("Inspecting database: " + dbName); - - try (MongoClient mongoClient = MongoClients.create(uri)) { - MongoDatabase database = mongoClient.getDatabase(dbName); - - // Loop through each collection in the database. - for (String collectionName : database.listCollectionNames()) { - System.out.println("Collection: " + collectionName); - - // Get collection stats using the "collStats" command. - Document stats = database.runCommand(new Document("collStats", collectionName)); - long sizeInBytes = stats.getLong("size"); - String formattedSize = formatSize(sizeInBytes); - - // Get the MongoCollection instance. - MongoCollection collection = database.getCollection(collectionName); - - // Find the most recent document (assumes _id is an ObjectId). - Date lastRecordTime = getRecordTimestamp(collection, Sorts.descending("_id")); - // Find the first document to approximate the collection creation time. - Date creationTime = getRecordTimestamp(collection, Sorts.ascending("_id")); - - System.out.println(" Size: " + formattedSize); - System.out.println(" Last record added: " + - (lastRecordTime != null ? lastRecordTime : "No records found")); - System.out.println(" Collection creation time (approx.): " + - (creationTime != null ? creationTime : "No records found")); - } - } catch (Exception e) { - System.out.println("Error inspecting database " + dbName + ": " + e.getMessage()); - } - } - } - - /** - * Formats the size from bytes to a human-readable string (bytes, MB, or GB). - */ - private String formatSize(long bytes) { - if (bytes < 1024 * 1024) { - return bytes + " bytes"; - } else if (bytes < 1024L * 1024 * 1024) { - return String.format("%.2f MB", bytes / (1024.0 * 1024)); - } else { - return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); - } - } - - /** - * Retrieves the timestamp of a record by sorting the collection using the provided sort order. - * Assumes that the _id field is an ObjectId so that its embedded timestamp can be extracted. - * - * @param collection The MongoDB collection. - * @param sortOrder The sort order (e.g., Sorts.descending("_id") or Sorts.ascending("_id")). - * @return The Date corresponding to the record's _id timestamp, or null if no document is found. - */ - private Date getRecordTimestamp(MongoCollection collection, Bson sortOrder) { - Document doc = collection.find().sort(sortOrder).limit(1).first(); - if (doc != null) { - Object id = doc.get("_id"); - if (id instanceof ObjectId) { - return ((ObjectId) id).getDate(); - } - } - return null; - } -} +package infra; + +import com.mongodb.ConnectionString; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Sorts; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.ObjectId; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@Component +public class MongoInspectorService implements CommandLineRunner { + + // List of MongoDB connection strings. Each URI is assumed to include the database name. + private final List mongoUris = Arrays.asList( + "mongodb://localhost:27017/db1", + "mongodb://localhost:27017/db2" + ); + + @Override + public void run(String... args) { + for (String uri : mongoUris) { + // Parse the connection string to extract the default database name. + ConnectionString connectionString = new ConnectionString(uri); + String dbName = connectionString.getDatabase(); + System.out.println("Inspecting database: " + dbName); + + try (MongoClient mongoClient = MongoClients.create(uri)) { + MongoDatabase database = mongoClient.getDatabase(dbName); + + // Loop through each collection in the database. + for (String collectionName : database.listCollectionNames()) { + System.out.println("Collection: " + collectionName); + + // Get collection stats using the "collStats" command. + Document stats = database.runCommand(new Document("collStats", collectionName)); + long sizeInBytes = stats.getLong("size"); + String formattedSize = formatSize(sizeInBytes); + + // Get the MongoCollection instance. + MongoCollection collection = database.getCollection(collectionName); + + // Find the most recent document (assumes _id is an ObjectId). + Date lastRecordTime = getRecordTimestamp(collection, Sorts.descending("_id")); + // Find the first document to approximate the collection creation time. + Date creationTime = getRecordTimestamp(collection, Sorts.ascending("_id")); + + System.out.println(" Size: " + formattedSize); + System.out.println(" Last record added: " + + (lastRecordTime != null ? lastRecordTime : "No records found")); + System.out.println(" Collection creation time (approx.): " + + (creationTime != null ? creationTime : "No records found")); + } + } catch (Exception e) { + System.out.println("Error inspecting database " + dbName + ": " + e.getMessage()); + } + } + } + + /** + * Formats the size from bytes to a human-readable string (bytes, MB, or GB). + */ + private String formatSize(long bytes) { + if (bytes < 1024 * 1024) { + return bytes + " bytes"; + } else if (bytes < 1024L * 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); + } + } + + /** + * Retrieves the timestamp of a record by sorting the collection using the provided sort order. + * Assumes that the _id field is an ObjectId so that its embedded timestamp can be extracted. + * + * @param collection The MongoDB collection. + * @param sortOrder The sort order (e.g., Sorts.descending("_id") or Sorts.ascending("_id")). + * @return The Date corresponding to the record's _id timestamp, or null if no document is found. + */ + private Date getRecordTimestamp(MongoCollection collection, Bson sortOrder) { + Document doc = collection.find().sort(sortOrder).limit(1).first(); + if (doc != null) { + Object id = doc.get("_id"); + if (id instanceof ObjectId) { + return ((ObjectId) id).getDate(); + } + } + return null; + } +} diff --git a/src/main/java/infra/dmn/DecisionTable_LoanDecisionTable.csv b/src/main/java/infra/dmn/DecisionTable_LoanDecisionTable.csv new file mode 100644 index 0000000..f0de603 --- /dev/null +++ b/src/main/java/infra/dmn/DecisionTable_LoanDecisionTable.csv @@ -0,0 +1,3 @@ +Rule,CONDITION–Age,CONDITION–Score,ACTION–Decision +1,Applicant Age >= 18,Credit Score >= 700,"""Approve""" +2,true,true,"""Reject""" diff --git a/src/main/java/infra/dmn/DmnDecisionTableConverter.java b/src/main/java/infra/dmn/DmnDecisionTableConverter.java index f96f9f4..7141387 100644 --- a/src/main/java/infra/dmn/DmnDecisionTableConverter.java +++ b/src/main/java/infra/dmn/DmnDecisionTableConverter.java @@ -1,152 +1,152 @@ -package infra.dmn; -import java.io.File; -import java.io.FileWriter; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import org.w3c.dom.*; - -public class DmnDecisionTableConverter { - - // Change this if your DMN uses a different namespace/version - private static final String DMN_NS = "https://www.omg.org/spec/DMN/20191111/MODEL/"; - - public static void main(String[] args) throws Exception { - args = new String[]{"./src/main/java/infra/dmn/example.xml","./src/main/java/infra/dmn"}; - if (args.length < 2) { - System.err.println("Usage: java DmnDecisionTableConverter "); - System.exit(1); - } - - File dmnFile = new File(args[0]); - File outDir = new File(args[1]); - if (!outDir.exists() && !outDir.mkdirs()) { - throw new RuntimeException("Cannot create output directory: " + outDir); - } - - // 1. Parse DMN XML - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setNamespaceAware(false); - dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - DocumentBuilder db = dbf.newDocumentBuilder(); - Document doc = db.parse(dmnFile); - - // 2. Find all elements - NodeList decisions = doc.getElementsByTagNameNS(DMN_NS, "decision"); - for (int i = 0; i < decisions.getLength(); i++) { - Element decision = (Element) decisions.item(i); - - // 3. Locate the first inside this decision - NodeList dtList = decision.getElementsByTagNameNS(DMN_NS, "decisionTable"); - if (dtList.getLength() == 0) continue; - Element dt = (Element) dtList.item(0); - - String tableId = dt.getAttribute("id"); - if (tableId == null || tableId.isEmpty()) { - tableId = decision.getAttribute("id"); - } - - // 4. Extract input column names - List inputNames = new ArrayList<>(); - NodeList inputs = dt.getElementsByTagNameNS(DMN_NS, "input"); - for (int j = 0; j < inputs.getLength(); j++) { - Element inp = (Element) inputs.item(j); - - // Try