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     * Get a list of all Conditional system names
290     * Overrides the bean method
291     * @since 4.7.4
292     * @deprecated 4.11.5 - use direct access via
293     *                  {@link #getNamedBeanSet}
294     * @return a list of conditional system names regardless of parent Logix
295     */
296    @Deprecated // 4.11.5
297    @Override
298    @Nonnull
299    public List<String> getSystemNameList() {
300        jmri.util.LoggingUtil.deprecationWarning(log, "getSystemNameList");
301        List<String> nameList = new ArrayList<>();
302
303        jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class);
304        for (Logix lgx : logixManager.getNamedBeanSet()) {
305            for (int i = 0; i < lgx.getNumConditionals(); i++) {
306                nameList.add(lgx.getConditionalByNumberOrder(i));
307            }
308        }
309        Collections.sort(nameList);
310        return nameList;
311    }
312
313    /**
314     * Create a named bean set for conditionals.  This requires special logic since conditional
315     * beans are not registered.
316     * @since 4.17.5
317     * @return a sorted named bean set of conditionals.
318     */
319    @Override
320    @Nonnull
321    public SortedSet<Conditional> getNamedBeanSet() {
322        TreeSet<Conditional> conditionals = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
323
324        jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class);
325        for (Logix lgx : logixManager.getNamedBeanSet()) {
326            for (int i = 0; i < lgx.getNumConditionals(); i++) {
327                Conditional cdl = getBySystemName(lgx.getConditionalByNumberOrder(i));
328                if (cdl != null) {
329                    conditionals.add(cdl);
330                }
331            }
332        }
333        return Collections.unmodifiableSortedSet(conditionals);
334    }
335
336    /**
337     *
338     * @return the default instance of the DefaultConditionalManager
339     * @deprecated since 4.17.3; use {@link jmri.InstanceManager#getDefault(java.lang.Class)} instead
340     */
341    @Deprecated
342    static public DefaultConditionalManager instance() {
343        return InstanceManager.getDefault(DefaultConditionalManager.class);
344    }
345
346    @Override
347    @Nonnull
348    public String getBeanTypeHandled(boolean plural) {
349        return Bundle.getMessage(plural ? "BeanNameConditionals" : "BeanNameConditional");
350    }
351
352    /**
353     * {@inheritDoc}
354     */
355    @Override
356    public Class<Conditional> getNamedBeanClass() {
357        return Conditional.class;
358    }
359
360    // --- Conditional Where Used processes ---
361
362    /**
363     * Maintain a list of conditionals that refer to a particular conditional.
364     * @since 4.7.4
365     */
366    private final HashMap<String, ArrayList<String>> conditionalWhereUsed = new HashMap<>();
367
368    /**
369     * Return a copy of the entire map.  Used by
370     * {@link jmri.jmrit.beantable.LogixTableAction#buildWhereUsedListing}
371     * @since 4.7.4
372     * @return a copy of the map
373     */
374    @Override
375    public HashMap<String, ArrayList<String>> getWhereUsedMap() {
376        return new HashMap<>(conditionalWhereUsed);
377    }
378
379    /**
380     * Add a conditional reference to the array indicated by the target system name.
381     * @since 4.7.4
382     * @param target The system name for the target conditional
383     * @param reference The system name of the conditional that contains the conditional reference
384     */
385    @Override
386    public void addWhereUsed(String target, String reference) {
387        if (target == null || target.equals("")) {
388            log.error("Invalid target name for addWhereUsed");
389            return;
390        }
391        if (reference == null || reference.equals("")) {
392            log.error("Invalid reference name for addWhereUsed");
393            return;
394        }
395
396        if (conditionalWhereUsed.containsKey(target)) {
397            ArrayList<String> refList = conditionalWhereUsed.get(target);
398            if (!refList.contains(reference)) {
399                refList.add(reference);
400                conditionalWhereUsed.replace(target, refList);
401            }
402        } else {
403            ArrayList<String> refList = new ArrayList<>();
404            refList.add(reference);
405            conditionalWhereUsed.put(target, refList);
406        }
407    }
408
409    /**
410     * Get a list of conditional references for the indicated conditional
411     * @since 4.7.4
412     * @param target The target conditional for a conditional reference
413     * @return an ArrayList or null if none
414     */
415    @Override
416    public ArrayList<String> getWhereUsed(String target) {
417        if (target == null || target.equals("")) {
418            log.error("Invalid target name for getWhereUsed");
419            return null;
420        }
421        return conditionalWhereUsed.get(target);
422    }
423
424    /**
425     * Remove a conditional reference from the array indicated by the target system name.
426     * @since 4.7.4
427     * @param target The system name for the target conditional
428     * @param reference The system name of the conditional that contains the conditional reference
429     */
430    @Override
431    public void removeWhereUsed(String target, String reference) {
432        if (target == null || target.equals("")) {
433            log.error("Invalid target name for removeWhereUsed");
434            return;
435        }
436        if (reference == null || reference.equals("")) {
437            log.error("Invalid reference name for removeWhereUsed");
438            return;
439        }
440
441        if (conditionalWhereUsed.containsKey(target)) {
442            ArrayList<?> refList = conditionalWhereUsed.get(target);
443            refList.remove(reference);
444            if (refList.size() == 0) {
445                conditionalWhereUsed.remove(target);
446            }
447        }
448    }
449
450    /**
451     * Display the complete structure, used for debugging purposes.
452     * @since 4.7.4
453     */
454    @Override
455    public void displayWhereUsed() {
456        log.info("- Display Conditional Where Used     ");
457        SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet());
458        for (String key : keys) {
459        log.info("    Target: {}                  ", key);
460            ArrayList<String> refList = conditionalWhereUsed.get(key);
461            for (String ref : refList) {
462            log.info("      Reference: {}             ", ref);
463            }
464        }
465    }
466
467    /**
468     * Get the target system names used by this conditional
469     * @since 4.7.4
470     * @param reference The system name of the conditional the refers to other conditionals.
471     * @return a list of the target conditionals
472     */
473    @Override
474    public ArrayList<String> getTargetList(String reference) {
475        ArrayList<String> targetList = new ArrayList<>();
476        SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet());
477        for (String key : keys) {
478            ArrayList<String> refList = conditionalWhereUsed.get(key);
479            for (String ref : refList) {
480                if (ref.equals(reference)) {
481                    targetList.add(key);
482                }
483            }
484        }
485        return targetList;
486    }
487
488    private final static Logger log = LoggerFactory.getLogger(DefaultConditionalManager.class);
489}