001package jmri.managers;
002
003import java.util.*;
004import java.util.regex.Matcher;
005import java.util.regex.Pattern;
006import javax.annotation.Nonnull;
007import jmri.Conditional;
008import jmri.ConditionalManager;
009import jmri.InstanceManager;
010import jmri.Logix;
011import jmri.implementation.DefaultConditional;
012import jmri.implementation.SensorGroupConditional;
013import jmri.jmrit.sensorgroup.SensorGroupFrame;
014import jmri.jmrix.internal.InternalSystemConnectionMemo;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Basic Implementation of a ConditionalManager.
020 * <p>
021 * Note that Conditionals always have an associated parent Logix.
022 * <p>
023 * Logix system names must begin with IX, and be followed by a string, usually,
024 * but not always, a number. The system names of Conditionals always begin with
025 * the parent Logix's system name, then there is a capital C and a number.
026 * <p>
027 * Conditional system names are set automatically when the Conditional is
028 * created. All alphabetic characters in a Conditional system name must be upper
029 * case. This is enforced when a new Conditional is created via
030 * {@link jmri.jmrit.beantable.LogixTableAction}
031 * <p>
032 * Conditional user names have specific requirements that are
033 * addressed in the {@link jmri.Conditional} class.
034 *
035 * @author Dave Duchamp Copyright (C) 2007
036 * @author Pete Cresman Copyright (C) 2009
037 */
038public class DefaultConditionalManager extends AbstractManager<Conditional>
039        implements ConditionalManager {
040
041    public DefaultConditionalManager(InternalSystemConnectionMemo memo) {
042        super(memo);
043    }
044
045    @Override
046    public int getXMLOrder() {
047        return jmri.Manager.CONDITIONALS;
048    }
049
050    @Override
051    public char typeLetter() {
052        return 'X';
053    }
054
055    /**
056     * Method to create a new Conditional if the Conditional does not exist If
057     * the parent Logix cannot be found, the userName cannot be checked, but the
058     * Conditional is still created. The scenario can happen when a Logix is
059     * loaded from a file after its Conditionals.
060     *
061     * @param systemName properly formatted system name for the new Conditional
062     * @param userName must not be null, use "" instead
063     * @return null if a Conditional with the same systemName or userName
064     *         already exists, or if there is trouble creating a new Conditional
065     */
066    @Override
067    public Conditional createNewConditional(String systemName, String userName) {
068        Conditional c = null;
069
070        // Check system name
071        if (systemName == null || systemName.length() < 1) {
072            log.error("createNewConditional: systemName is null or empty");
073            return null;
074        }
075        c = getBySystemName(systemName);
076        if (c != null) {
077            return null;        // Conditional already exists
078        }
079
080        // Get the potential parent Logix
081        Logix lgx = getParentLogix(systemName);
082        if (lgx == null) {
083            log.error("Unable to find the parent logix for condtional '{}'", systemName);
084            return null;
085        }
086
087        // Check the user name
088        if (userName != null && userName.length() > 0) {
089            c = getByUserName(lgx, userName);
090            if (c != null) {
091                return null;        // Duplicate user name within the parent Logix
092            }
093        }
094
095        // Conditional does not exist, create a new Conditional
096        if (systemName.startsWith(SensorGroupFrame.logixSysName)) {
097            c = new SensorGroupConditional(systemName, userName);
098        } else {
099            c = new DefaultConditional(systemName, userName);
100        }
101        // save in the maps
102//@        register(c);
103
104        boolean addCompleted = lgx.addConditional(systemName, c);
105        if (!addCompleted) {
106            return null;
107        }
108
109        return c;
110    }
111
112    /**
113     * Do not insist that Conditional user names are unique,
114     * unlike the usual NamedBean support
115     */
116    @Override
117    protected void handleUserNameUniqueness(jmri.Conditional s) {
118        // eventually needs error checking and reporting
119    }
120
121    /**
122     * Regex patterns to derive the logix system name from the conditional system name
123     * The 3 route patterns deal with Route Logix names that end with a number,
124     * such as Logix RTX123 with Conditional RTX1231T.
125     */
126    private static final String[] PATTERNS = {
127        "(.*?)(C\\d+$)",               // Standard IX
128        "(.*?)([1-9]{1}[ALT]$)",       // LRoute/Route, 1-9
129        "(.*?)([0-9]{2}[ALT]$)",       // LRoute/Route, 10-99
130        "(.*?)([0-9]{3}[ALT]$)"        // LRoute/Route, 100-999
131    };
132
133    /**
134     * Parses the Conditional system name to get the parent Logix system name,
135     * then gets the parent Logix, and returns it.  For sensor groups, the parent
136     * Logix name is 'SYS'.  LRoutes and exported Routes (RTX prefix) require
137     * special logic
138     *
139     * @param name  system name of Conditionals
140     * @return the parent Logix or null
141     */
142    @Override
143    public Logix getParentLogix(String name) {
144        if (name == null || name.length() < 4) {
145            return null;
146        }
147
148        // Check for standard names
149        for (String pattern : PATTERNS) {
150            Pattern r = Pattern.compile(pattern);
151            Matcher m = r.matcher(name);
152            if (m.find()) {
153                Logix lgx = InstanceManager.getDefault(jmri.LogixManager.class).getBySystemName(m.group(1));
154                if (lgx != null) {
155                    return lgx;
156                }
157
158            }
159        }
160
161        // Now try non-standard names using a brute force scan
162        jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class);
163        for (Logix lgx : logixManager.getNamedBeanSet()) {
164            for (int i = 0; i < lgx.getNumConditionals(); i++) {
165                String cdlName = lgx.getConditionalByNumberOrder(i);
166                if (cdlName.equals(name)) {
167                    return lgx;
168                }
169            }
170        }
171        return null;
172    }
173
174    /**
175     * Remove an existing Conditional. Parent Logix must have been deactivated
176     * before invoking this.
177     */
178    @Override
179    public void deleteConditional(Conditional c) {
180//@        deregister(c);
181    }
182
183    /**
184     * Method to get an existing Conditional. First looks up assuming that name
185     * is a User Name. Note: the parent Logix must be passed in x for user name
186     * lookup. If this fails, or if x == null, looks up assuming that name is a
187     * System Name. If both fail, returns null.
188     *
189     * @param x     parent Logix (may be null)
190     * @param name  name to look up
191     * @return null if no match found
192     */
193    @Override
194    public Conditional getConditional(Logix x, String name) {
195        Conditional c = null;
196        if (x != null) {
197            c = getByUserName(x, name);
198            if (c != null) {
199                return c;
200            }
201        }
202        return getBySystemName(name);
203    }
204
205    @Override
206    public Conditional getConditional(String name) {
207        Conditional c = getBySystemName(name);
208        if (c == null) {
209            c = getByUserName(name);
210        }
211        return c;
212    }
213
214    /*
215     * Conditional user names are NOT unique.
216     * @param key The user name
217     * @return the conditional or null when not found or a duplicate
218     */
219    @Override
220    public Conditional getByUserName(String key) {
221        if (key == null) {
222            return null;
223        }
224
225        Conditional c = null;
226        for (Conditional chkC : getNamedBeanSet()) {
227            if (key.equals(chkC.getUserName())) {
228                if (c == null) {
229                    // Save first match
230                    c = chkC;
231                    continue;
232                }
233                // Found a second match, give up
234                log.warn("Duplicate conditional user names found, key = {}", key);
235                return null;
236            }
237        }
238        return c;
239    }
240
241    @Override
242    public Conditional getByUserName(Logix x, String key) {
243        if (x == null) {
244            return null;
245        }
246        for (int i = 0; i < x.getNumConditionals(); i++) {
247            Conditional c = getBySystemName(x.getConditionalByNumberOrder(i));
248            if (c != null) {
249                String uName = c.getUserName();
250                if (key.equals(uName)) {
251                    return c;
252                }
253            }
254        }
255        return null;
256    }
257
258    @Override
259    public Conditional getBySystemName(String name) {
260        if (name == null) {
261            return null;
262        }
263        Logix lgx = getParentLogix(name);
264        if (lgx == null) {
265            return null;
266        }
267        return lgx.getConditional(name);
268//@        return (Conditional) _tsys.get(name);
269    }
270
271    /**
272     * Get a list of all Conditional system names with the specified Logix
273     * parent
274     */
275    @Override
276    public List<String> getSystemNameListForLogix(Logix x) {
277        if (x == null) {
278            return null;
279        }
280        List<String> nameList = new ArrayList<>();
281
282        for (int i = 0; i < x.getNumConditionals(); i++) {
283            nameList.add(x.getConditionalByNumberOrder(i));
284        }
285        return nameList;
286    }
287
288    /**
289     * Create a named bean set for conditionals.  This requires special logic since conditional
290     * beans are not registered.
291     * @since 4.17.5
292     * @return a sorted named bean set of conditionals.
293     */
294    @Override
295    @Nonnull
296    public SortedSet<Conditional> getNamedBeanSet() {
297        TreeSet<Conditional> conditionals = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
298
299        jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class);
300        for (Logix lgx : logixManager.getNamedBeanSet()) {
301            for (int i = 0; i < lgx.getNumConditionals(); i++) {
302                Conditional cdl = getBySystemName(lgx.getConditionalByNumberOrder(i));
303                if (cdl != null) {
304                    conditionals.add(cdl);
305                }
306            }
307        }
308        return Collections.unmodifiableSortedSet(conditionals);
309    }
310
311    @Override
312    @Nonnull
313    public String getBeanTypeHandled(boolean plural) {
314        return Bundle.getMessage(plural ? "BeanNameConditionals" : "BeanNameConditional");
315    }
316
317    /**
318     * {@inheritDoc}
319     */
320    @Override
321    public Class<Conditional> getNamedBeanClass() {
322        return Conditional.class;
323    }
324
325    // --- Conditional Where Used processes ---
326
327    /**
328     * Maintain a list of conditionals that refer to a particular conditional.
329     * @since 4.7.4
330     */
331    private final HashMap<String, ArrayList<String>> conditionalWhereUsed = new HashMap<>();
332
333    /**
334     * Return a copy of the entire map.  Used by
335     * {@link jmri.jmrit.beantable.LogixTableAction#buildWhereUsedListing}
336     * @since 4.7.4
337     * @return a copy of the map
338     */
339    @Override
340    public HashMap<String, ArrayList<String>> getWhereUsedMap() {
341        return new HashMap<>(conditionalWhereUsed);
342    }
343
344    /**
345     * Add a conditional reference to the array indicated by the target system name.
346     * @since 4.7.4
347     * @param target The system name for the target conditional
348     * @param reference The system name of the conditional that contains the conditional reference
349     */
350    @Override
351    public void addWhereUsed(String target, String reference) {
352        if (target == null || target.equals("")) {
353            log.error("Invalid target name for addWhereUsed");
354            return;
355        }
356        if (reference == null || reference.equals("")) {
357            log.error("Invalid reference name for addWhereUsed");
358            return;
359        }
360
361        if (conditionalWhereUsed.containsKey(target)) {
362            ArrayList<String> refList = conditionalWhereUsed.get(target);
363            if (!refList.contains(reference)) {
364                refList.add(reference);
365                conditionalWhereUsed.replace(target, refList);
366            }
367        } else {
368            ArrayList<String> refList = new ArrayList<>();
369            refList.add(reference);
370            conditionalWhereUsed.put(target, refList);
371        }
372    }
373
374    /**
375     * Get a list of conditional references for the indicated conditional
376     * @since 4.7.4
377     * @param target The target conditional for a conditional reference
378     * @return an ArrayList or null if none
379     */
380    @Override
381    public ArrayList<String> getWhereUsed(String target) {
382        if (target == null || target.equals("")) {
383            log.error("Invalid target name for getWhereUsed");
384            return null;
385        }
386        return conditionalWhereUsed.get(target);
387    }
388
389    /**
390     * Remove a conditional reference from the array indicated by the target system name.
391     * @since 4.7.4
392     * @param target The system name for the target conditional
393     * @param reference The system name of the conditional that contains the conditional reference
394     */
395    @Override
396    public void removeWhereUsed(String target, String reference) {
397        if (target == null || target.equals("")) {
398            log.error("Invalid target name for removeWhereUsed");
399            return;
400        }
401        if (reference == null || reference.equals("")) {
402            log.error("Invalid reference name for removeWhereUsed");
403            return;
404        }
405
406        if (conditionalWhereUsed.containsKey(target)) {
407            ArrayList<?> refList = conditionalWhereUsed.get(target);
408            refList.remove(reference);
409            if (refList.size() == 0) {
410                conditionalWhereUsed.remove(target);
411            }
412        }
413    }
414
415    /**
416     * Display the complete structure, used for debugging purposes.
417     * @since 4.7.4
418     */
419    @Override
420    public void displayWhereUsed() {
421        log.info("- Display Conditional Where Used     ");
422        SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet());
423        for (String key : keys) {
424        log.info("    Target: {}                  ", key);
425            ArrayList<String> refList = conditionalWhereUsed.get(key);
426            for (String ref : refList) {
427            log.info("      Reference: {}             ", ref);
428            }
429        }
430    }
431
432    /**
433     * Get the target system names used by this conditional
434     * @since 4.7.4
435     * @param reference The system name of the conditional the refers to other conditionals.
436     * @return a list of the target conditionals
437     */
438    @Override
439    public ArrayList<String> getTargetList(String reference) {
440        ArrayList<String> targetList = new ArrayList<>();
441        SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet());
442        for (String key : keys) {
443            ArrayList<String> refList = conditionalWhereUsed.get(key);
444            for (String ref : refList) {
445                if (ref.equals(reference)) {
446                    targetList.add(key);
447                }
448            }
449        }
450        return targetList;
451    }
452
453    private final static Logger log = LoggerFactory.getLogger(DefaultConditionalManager.class);
454}