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