Blame | Last modification | View Log | RSS feed
package com.smartdukaan.cron.offercircular;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.regex.Pattern;import com.spice.profitmandi.dao.repository.offers.CircularIngestRepository;/*** Which divisions and products are in scope. Read from offers.oem_division and* offers.scope_rule - this is editable configuration, never constants in code, so* changing scope is a data change.** A label with no division row is reported as UNKNOWN rather than silently skipped,* so a new OEM division in next month's circular is visible immediately.*/public final class ScopeConfig {private static final String INCLUDE_ONLY = "INCLUDE_ONLY";private static final String EXCLUDE = "EXCLUDE";private static final class Division {final boolean inScope;final int categoryId;Division(boolean inScope, int categoryId) {this.inScope = inScope;this.categoryId = categoryId;}}private static final class Rule {final String type;final Pattern pattern;Rule(String type, Pattern pattern) { this.type = type; this.pattern = pattern; }}/** Outcome for one row, carrying the brand/category to match products against. */public static final class Decision {private final boolean keep;private final String reason;private final String brand;private final int categoryId;private final List<Pattern> includeOnly;private Decision(boolean keep, String reason, String brand, int categoryId,List<Pattern> includeOnly) {this.keep = keep;this.reason = reason;this.brand = brand;this.categoryId = categoryId;this.includeOnly = includeOnly;}public boolean isKeep() { return keep; }public String getReason() { return reason; }public String getBrand() { return brand; }public int getCategoryId() { return categoryId; }/** Drops entities that no INCLUDE_ONLY rule accepts (Apple -> iPhone only). */public List<String> filterEntities(List<String> entities) {if (includeOnly == null || includeOnly.isEmpty()) {return entities;}List<String> kept = new ArrayList<>();for (String entity : entities) {for (Pattern p : includeOnly) {if (p.matcher(entity).find()) {kept.add(entity);break;}}}return kept;}}private final Map<String, Division> divisions = new LinkedHashMap<>();private final Map<String, List<Rule>> rules = new LinkedHashMap<>();private final Map<String, String> brandByLabel = new LinkedHashMap<>();private ScopeConfig() { }public static ScopeConfig load(CircularIngestRepository repository) {ScopeConfig config = new ScopeConfig();for (Object[] row : repository.selectDivisions()) {String label = (String) row[0];boolean inScope = isTrue(row[1]);int categoryId = ((Number) row[2]).intValue();config.divisions.put(label, new Division(inScope, categoryId));String brand = row.length > 3 && row[3] != null ? (String) row[3] : "";if (!brand.isEmpty()) {config.brandByLabel.put(label, brand);}}for (Object[] row : repository.selectScopeRules()) {String label = row[0] == null ? "" : (String) row[0];config.rules.computeIfAbsent(label, k -> new ArrayList<>()).add(new Rule((String) row[1],Pattern.compile((String) row[2], Pattern.CASE_INSENSITIVE)));}return config;}/*** in_scope is TINYINT(1), which MySQL Connector/J maps to Boolean while Hibernate* may hand back a Number. Accept either rather than depend on driver settings.*/private static boolean isTrue(Object value) {if (value instanceof Boolean) {return (Boolean) value;}if (value instanceof Number) {return ((Number) value).intValue() == 1;}return value != null && "1".equals(value.toString());}public Decision evaluate(String label, String products) {Division division = divisions.get(label);if (division == null) {return new Decision(false, "UNKNOWN division label: " + label, null, 0, null);}if (!division.inScope) {return new Decision(false, "out of scope by config: " + label, null, 0, null);}String haystack = products == null ? "" : products;List<Pattern> includeOnly = new ArrayList<>();// division-specific rules first, then global ones (label "")for (String scope : new String[]{label, ""}) {List<Rule> scoped = rules.get(scope);if (scoped == null) {continue;}for (Rule rule : scoped) {if (EXCLUDE.equals(rule.type) && rule.pattern.matcher(haystack).find()) {return new Decision(false,"EXCLUDE rule /" + rule.pattern.pattern() + "/ on "+ (scope.isEmpty() ? "all" : scope), null, 0, null);}}List<Pattern> includes = new ArrayList<>();for (Rule rule : scoped) {if (INCLUDE_ONLY.equals(rule.type)) {includes.add(rule.pattern);}}if (!includes.isEmpty()) {boolean any = false;for (Pattern p : includes) {if (p.matcher(haystack).find()) {any = true;break;}}if (!any) {return new Decision(false, "no INCLUDE_ONLY rule matched on " + label,null, 0, null);}includeOnly.addAll(includes);}}String brand = brandByLabel.get(label);return new Decision(true, null, brand == null ? label.split(" ")[0] : brand,division.categoryId, includeOnly);}}