Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37330 amit 1
package com.smartdukaan.cron.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;
46
 
47
        private Decision(boolean keep, String reason, String brand, int categoryId,
48
                         List<Pattern> includeOnly) {
49
            this.keep = keep;
50
            this.reason = reason;
51
            this.brand = brand;
52
            this.categoryId = categoryId;
53
            this.includeOnly = includeOnly;
54
        }
55
        public boolean isKeep() { return keep; }
56
        public String getReason() { return reason; }
57
        public String getBrand() { return brand; }
58
        public int getCategoryId() { return categoryId; }
59
 
60
        /** Drops entities that no INCLUDE_ONLY rule accepts (Apple -> iPhone only). */
61
        public List<String> filterEntities(List<String> entities) {
62
            if (includeOnly == null || includeOnly.isEmpty()) {
63
                return entities;
64
            }
65
            List<String> kept = new ArrayList<>();
66
            for (String entity : entities) {
67
                for (Pattern p : includeOnly) {
68
                    if (p.matcher(entity).find()) {
69
                        kept.add(entity);
70
                        break;
71
                    }
72
                }
73
            }
74
            return kept;
75
        }
76
    }
77
 
78
    private final Map<String, Division> divisions = new LinkedHashMap<>();
79
    private final Map<String, List<Rule>> rules = new LinkedHashMap<>();
80
    private final Map<String, String> brandByLabel = new LinkedHashMap<>();
81
 
82
    private ScopeConfig() { }
83
 
84
    public static ScopeConfig load(CircularIngestRepository repository) {
85
        ScopeConfig config = new ScopeConfig();
86
        for (Object[] row : repository.selectDivisions()) {
87
            String label = (String) row[0];
88
            boolean inScope = isTrue(row[1]);
89
            int categoryId = ((Number) row[2]).intValue();
90
            config.divisions.put(label, new Division(inScope, categoryId));
91
            String brand = row.length > 3 && row[3] != null ? (String) row[3] : "";
92
            if (!brand.isEmpty()) {
93
                config.brandByLabel.put(label, brand);
94
            }
95
        }
96
        for (Object[] row : repository.selectScopeRules()) {
97
            String label = row[0] == null ? "" : (String) row[0];
98
            config.rules.computeIfAbsent(label, k -> new ArrayList<>())
99
                    .add(new Rule((String) row[1],
100
                            Pattern.compile((String) row[2], Pattern.CASE_INSENSITIVE)));
101
        }
102
        return config;
103
    }
104
 
105
    /**
106
     * in_scope is TINYINT(1), which MySQL Connector/J maps to Boolean while Hibernate
107
     * may hand back a Number. Accept either rather than depend on driver settings.
108
     */
109
    private static boolean isTrue(Object value) {
110
        if (value instanceof Boolean) {
111
            return (Boolean) value;
112
        }
113
        if (value instanceof Number) {
114
            return ((Number) value).intValue() == 1;
115
        }
116
        return value != null && "1".equals(value.toString());
117
    }
118
 
119
    public Decision evaluate(String label, String products) {
120
        Division division = divisions.get(label);
121
        if (division == null) {
122
            return new Decision(false, "UNKNOWN division label: " + label, null, 0, null);
123
        }
124
        if (!division.inScope) {
125
            return new Decision(false, "out of scope by config: " + label, null, 0, null);
126
        }
127
        String haystack = products == null ? "" : products;
128
        List<Pattern> includeOnly = new ArrayList<>();
129
        // division-specific rules first, then global ones (label "")
130
        for (String scope : new String[]{label, ""}) {
131
            List<Rule> scoped = rules.get(scope);
132
            if (scoped == null) {
133
                continue;
134
            }
135
            for (Rule rule : scoped) {
136
                if (EXCLUDE.equals(rule.type) && rule.pattern.matcher(haystack).find()) {
137
                    return new Decision(false,
138
                            "EXCLUDE rule /" + rule.pattern.pattern() + "/ on "
139
                                    + (scope.isEmpty() ? "all" : scope), null, 0, null);
140
                }
141
            }
142
            List<Pattern> includes = new ArrayList<>();
143
            for (Rule rule : scoped) {
144
                if (INCLUDE_ONLY.equals(rule.type)) {
145
                    includes.add(rule.pattern);
146
                }
147
            }
148
            if (!includes.isEmpty()) {
149
                boolean any = false;
150
                for (Pattern p : includes) {
151
                    if (p.matcher(haystack).find()) {
152
                        any = true;
153
                        break;
154
                    }
155
                }
156
                if (!any) {
157
                    return new Decision(false, "no INCLUDE_ONLY rule matched on " + label,
158
                            null, 0, null);
159
                }
160
                includeOnly.addAll(includes);
161
            }
162
        }
163
        String brand = brandByLabel.get(label);
164
        return new Decision(true, null, brand == null ? label.split(" ")[0] : brand,
165
                division.categoryId, includeOnly);
166
    }
167
}