Subversion Repositories SmartDukaan

Rev

Rev 37337 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37337 amit 1
package com.spice.profitmandi.web.offercircular;
2
 
3
import java.util.ArrayList;
4
import java.util.LinkedHashMap;
5
import java.util.List;
6
import java.util.Map;
7
import java.util.regex.Pattern;
8
 
9
import com.spice.profitmandi.dao.repository.offers.CircularIngestRepository;
10
 
11
/**
12
 * Which divisions and products are in scope. Read from offers.oem_division and
13
 * offers.scope_rule - this is editable configuration, never constants in code, so
14
 * changing scope is a data change.
15
 *
16
 * A label with no division row is reported as UNKNOWN rather than silently skipped,
17
 * so a new OEM division in next month's circular is visible immediately.
18
 */
19
public final class ScopeConfig {
20
 
21
    private static final String INCLUDE_ONLY = "INCLUDE_ONLY";
22
    private static final String EXCLUDE = "EXCLUDE";
23
 
24
    private static final class Division {
25
        final boolean inScope;
26
        final int categoryId;
27
        Division(boolean inScope, int categoryId) {
28
            this.inScope = inScope;
29
            this.categoryId = categoryId;
30
        }
31
    }
32
 
33
    private static final class Rule {
34
        final String type;
35
        final Pattern pattern;
36
        Rule(String type, Pattern pattern) { this.type = type; this.pattern = pattern; }
37
    }
38
 
39
    /** Outcome for one row, carrying the brand/category to match products against. */
40
    public static final class Decision {
41
        private final boolean keep;
42
        private final String reason;
43
        private final String brand;
44
        private final int categoryId;
45
        private final List<Pattern> includeOnly;
37526 amit 46
        private final String canonicalLabel;
37337 amit 47
 
48
        private Decision(boolean keep, String reason, String brand, int categoryId,
37526 amit 49
                         List<Pattern> includeOnly, String canonicalLabel) {
37337 amit 50
            this.keep = keep;
51
            this.reason = reason;
52
            this.brand = brand;
53
            this.categoryId = categoryId;
54
            this.includeOnly = includeOnly;
37526 amit 55
            this.canonicalLabel = canonicalLabel;
37337 amit 56
        }
57
        public boolean isKeep() { return keep; }
58
        public String getReason() { return reason; }
59
        public String getBrand() { return brand; }
60
        public int getCategoryId() { return categoryId; }
61
 
37526 amit 62
        /**
63
         * The division's own label, with any alias already resolved. Everything that
64
         * looks a division up by name must use THIS and not the label the PDF printed -
65
         * see CircularIngestRepository.selectDivisionAliases for what goes wrong
66
         * otherwise.
67
         */
68
        public String getCanonicalLabel() { return canonicalLabel; }
69
 
37337 amit 70
        /** Drops entities that no INCLUDE_ONLY rule accepts (Apple -> iPhone only). */
71
        public List<String> filterEntities(List<String> entities) {
72
            if (includeOnly == null || includeOnly.isEmpty()) {
73
                return entities;
74
            }
75
            List<String> kept = new ArrayList<>();
76
            for (String entity : entities) {
77
                for (Pattern p : includeOnly) {
78
                    if (p.matcher(entity).find()) {
79
                        kept.add(entity);
80
                        break;
81
                    }
82
                }
83
            }
84
            return kept;
85
        }
86
    }
87
 
88
    private final Map<String, Division> divisions = new LinkedHashMap<>();
89
    private final Map<String, List<Rule>> rules = new LinkedHashMap<>();
90
    private final Map<String, String> brandByLabel = new LinkedHashMap<>();
37526 amit 91
    /** alias label -> the division's own label. Never the other way round. */
92
    private final Map<String, String> aliases = new LinkedHashMap<>();
37337 amit 93
 
94
    private ScopeConfig() { }
95
 
96
    public static ScopeConfig load(CircularIngestRepository repository) {
97
        ScopeConfig config = new ScopeConfig();
98
        for (Object[] row : repository.selectDivisions()) {
99
            String label = (String) row[0];
100
            boolean inScope = isTrue(row[1]);
101
            int categoryId = ((Number) row[2]).intValue();
102
            config.divisions.put(label, new Division(inScope, categoryId));
103
            String brand = row.length > 3 && row[3] != null ? (String) row[3] : "";
104
            if (!brand.isEmpty()) {
105
                config.brandByLabel.put(label, brand);
106
            }
107
        }
37526 amit 108
        for (Object[] row : repository.selectDivisionAliases()) {
109
            String alias = (String) row[0];
110
            String target = (String) row[1];
111
            // A label that is a division in its own right must never be shadowed by an
112
            // alias - it would resolve two ways and which won would depend on load order.
113
            // The controller refuses to save such an alias; this is the second guard, for
114
            // rows that predate it or were inserted by hand.
115
            if (alias == null || target == null || config.divisions.containsKey(alias)) {
116
                continue;
117
            }
118
            config.aliases.put(alias, target);
119
        }
37337 amit 120
        for (Object[] row : repository.selectScopeRules()) {
121
            String label = row[0] == null ? "" : (String) row[0];
122
            config.rules.computeIfAbsent(label, k -> new ArrayList<>())
123
                    .add(new Rule((String) row[1],
124
                            Pattern.compile((String) row[2], Pattern.CASE_INSENSITIVE)));
125
        }
126
        return config;
127
    }
128
 
129
    /**
130
     * in_scope is TINYINT(1), which MySQL Connector/J maps to Boolean while Hibernate
131
     * may hand back a Number. Accept either rather than depend on driver settings.
132
     */
133
    private static boolean isTrue(Object value) {
134
        if (value instanceof Boolean) {
135
            return (Boolean) value;
136
        }
137
        if (value instanceof Number) {
138
            return ((Number) value).intValue() == 1;
139
        }
140
        return value != null && "1".equals(value.toString());
141
    }
142
 
37526 amit 143
    /**
144
     * The label this one really means - itself, unless it is a known alias.
145
     *
146
     * Callers must run every label through this BEFORE writing anything, and must keep
147
     * using the result for the division lookup, the offer insert and the product-alias
148
     * lookup. The one place the raw label survives is offer_raw_row, which records what
149
     * the PDF actually said.
150
     */
151
    public String canonicalLabel(String label) {
152
        String target = aliases.get(label);
153
        return target == null ? label : target;
154
    }
155
 
156
    public Decision evaluate(String rawLabel, String products) {
157
        String label = canonicalLabel(rawLabel);
37337 amit 158
        Division division = divisions.get(label);
159
        if (division == null) {
37526 amit 160
            return new Decision(false, "UNKNOWN division label: " + rawLabel, null, 0, null, label);
37337 amit 161
        }
162
        if (!division.inScope) {
37526 amit 163
            return new Decision(false, "out of scope by config: " + label, null, 0, null, label);
37337 amit 164
        }
165
        String haystack = products == null ? "" : products;
166
        List<Pattern> includeOnly = new ArrayList<>();
167
        // division-specific rules first, then global ones (label "")
168
        for (String scope : new String[]{label, ""}) {
169
            List<Rule> scoped = rules.get(scope);
170
            if (scoped == null) {
171
                continue;
172
            }
173
            for (Rule rule : scoped) {
174
                if (EXCLUDE.equals(rule.type) && rule.pattern.matcher(haystack).find()) {
175
                    return new Decision(false,
176
                            "EXCLUDE rule /" + rule.pattern.pattern() + "/ on "
37526 amit 177
                                    + (scope.isEmpty() ? "all" : scope), null, 0, null, label);
37337 amit 178
                }
179
            }
180
            List<Pattern> includes = new ArrayList<>();
181
            for (Rule rule : scoped) {
182
                if (INCLUDE_ONLY.equals(rule.type)) {
183
                    includes.add(rule.pattern);
184
                }
185
            }
186
            if (!includes.isEmpty()) {
187
                boolean any = false;
188
                for (Pattern p : includes) {
189
                    if (p.matcher(haystack).find()) {
190
                        any = true;
191
                        break;
192
                    }
193
                }
194
                if (!any) {
195
                    return new Decision(false, "no INCLUDE_ONLY rule matched on " + label,
37526 amit 196
                            null, 0, null, label);
37337 amit 197
                }
198
                includeOnly.addAll(includes);
199
            }
200
        }
201
        String brand = brandByLabel.get(label);
202
        return new Decision(true, null, brand == null ? label.split(" ")[0] : brand,
37526 amit 203
                division.categoryId, includeOnly, label);
37337 amit 204
    }
205
}