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 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    @CheckForNull
144    public Logix getParentLogix(String name) {
145        if (name == null || name.length() < 4) {
146            return null;
147        }
148
149        // Check for standard names
150        for (String pattern : PATTERNS) {
151            Pattern r = Pattern.compile(pattern);
152            Matcher m = r.matcher(name);
153            if (m.find()) {
154                Logix lgx = InstanceManager.getDefault(jmri.LogixManager.class).getBySystemName(m.group(1));
155                if (lgx != null) {
156                    return lgx;
157                }
158
159            }
160        }
161
162        // Now try non-standard names using a brute force scan
163        jmri.LogixManager logixManager = InstanceManager.getDefault(jmri.LogixManager.class);
164        for (Logix lgx : logixManager.getNamedBeanSet()) {
165            for (int i = 0; i < lgx.getNumConditionals(); i++) {
166                String cdlName = lgx.getConditionalByNumberOrder(i);
167                if (cdlName.equals(name)) {
168                    return lgx;
169                }
170            }
171        }
172        return null;
173    }
174
175    /**
176     * Remove an existing Conditional. Parent Logix must have been deactivated
177     * before invoking this.
178     */
179    @Override
180    public void deleteConditional(Conditional c) {
181//@        deregister(c);
182    }
183
184    /**
185     * Method to get an existing Conditional. First looks up assuming that name
186     * is a User Name. Note: the parent Logix must be passed in x for user name
187     * lookup. If this fails, or if x == null, looks up assuming that name is a
188     * System Name. If both fail, returns null.
189     *
190     * @param x     parent Logix (may be null)
191     * @param name  name to look up
192     * @return null if no match found
193     */
194    @Override
195    @CheckForNull
196    public Conditional getConditional(@CheckForNull Logix x, String name) {
197        if (x != null) {
198            Conditional c = getByUserName(x, name);
199            if (c != null) {
200                return c;
201            }
202        }
203        return getBySystemName(name);
204    }
205
206    @Override
207    @CheckForNull
208    public Conditional getConditional(String name) {
209        Conditional c = getBySystemName(name);
210        if (c == null) {
211            c = getByUserName(name);
212        }
213        return c;
214    }
215
216    /*
217     * Conditional user names are NOT unique.
218     * @param key The user name
219     * @return the conditional or null when not found or a duplicate
220     */
221    @Override
222    @CheckForNull
223    public Conditional getByUserName(String key) {
224        if (key == null) {
225            return null;
226        }
227
228        Conditional c = null;
229        for (Conditional chkC : getNamedBeanSet()) {
230            if (key.equals(chkC.getUserName())) {
231                if (c == null) {
232                    // Save first match
233                    c = chkC;
234                    continue;
235                }
236                // Found a second match, give up
237                log.warn("Duplicate conditional user names found, key = {}", key);
238                return null;
239            }
240        }
241        return c;
242    }
243
244    @Override
245    @CheckForNull
246    public Conditional getByUserName(@CheckForNull Logix x, @Nonnull String key) {
247        if (x == null) {
248            return null;
249        }
250        for (int i = 0; i < x.getNumConditionals(); i++) {
251            Conditional c = getBySystemName(x.getConditionalByNumberOrder(i));
252            if (c != null) {
253                String uName = c.getUserName();
254                if (key.equals(uName)) {
255                    return c;
256                }
257            }
258        }
259        return null;
260    }
261
262    @Override
263    @CheckForNull
264    public Conditional getBySystemName(String name) {
265        if (name == null) {
266            return null;
267        }
268        Logix lgx = getParentLogix(name);
269        if (lgx == null) {
270            return null;
271        }
272        return lgx.getConditional(name);
273    }
274
275    /**
276     * Get a list of all Conditional system names with the specified Logix
277     * parent
278     */
279    @Override
280    public List<String> getSystemNameListForLogix(Logix x) {
281        if (x == null) {
282            return null;
283        }
284        List<String> nameList = new ArrayList<>();
285
286        for (int i = 0; i < x.getNumConditionals(); i++) {
287            nameList.add(x.getConditionalByNumberOrder(i));
288        }
289        return nameList;
290    }
291
292    /**
293     * Create a named bean set for conditionals.  This requires special logic since conditional
294     * beans are not registered.
295     * @since 4.17.5
296     * @return a sorted named bean set of conditionals.
297     */
298    @Override
299    @Nonnull
300    public SortedSet<Conditional> getNamedBeanSet() {
301        TreeSet<Conditional> conditionals = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
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                Conditional cdl = getBySystemName(lgx.getConditionalByNumberOrder(i));
307                if (cdl != null) {
308                    conditionals.add(cdl);
309                }
310            }
311        }
312        return Collections.unmodifiableSortedSet(conditionals);
313    }
314
315    @Override
316    @Nonnull
317    public String getBeanTypeHandled(boolean plural) {
318        return Bundle.getMessage(plural ? "BeanNameConditionals" : "BeanNameConditional");
319    }
320
321    /**
322     * {@inheritDoc}
323     */
324    @Override
325    public Class<Conditional> getNamedBeanClass() {
326        return Conditional.class;
327    }
328
329    // --- Conditional Where Used processes ---
330
331    /**
332     * Maintain a list of conditionals that refer to a particular conditional.
333     * @since 4.7.4
334     */
335    private final HashMap<String, ArrayList<String>> conditionalWhereUsed = new HashMap<>();
336
337    /**
338     * Return a copy of the entire map.  Used by
339     * {@link jmri.jmrit.beantable.LogixTableAction#buildWhereUsedListing}
340     * @since 4.7.4
341     * @return a copy of the map
342     */
343    @Override
344    public HashMap<String, ArrayList<String>> getWhereUsedMap() {
345        return new HashMap<>(conditionalWhereUsed);
346    }
347
348    /**
349     * Add a conditional reference to the array indicated by the target system name.
350     * @since 4.7.4
351     * @param target The system name for the target conditional
352     * @param reference The system name of the conditional that contains the conditional reference
353     */
354    @Override
355    public void addWhereUsed(String target, String reference) {
356        if (target == null || target.isEmpty()) {
357            log.error("Invalid target name for addWhereUsed");
358            return;
359        }
360        if (reference == null || reference.isEmpty()) {
361            log.error("Invalid reference name for addWhereUsed");
362            return;
363        }
364
365        if (conditionalWhereUsed.containsKey(target)) {
366            ArrayList<String> refList = conditionalWhereUsed.get(target);
367            if (!refList.contains(reference)) {
368                refList.add(reference);
369                conditionalWhereUsed.replace(target, refList);
370            }
371        } else {
372            ArrayList<String> refList = new ArrayList<>();
373            refList.add(reference);
374            conditionalWhereUsed.put(target, refList);
375        }
376    }
377
378    /**
379     * Get a list of conditional references for the indicated conditional
380     * @since 4.7.4
381     * @param target The target conditional for a conditional reference
382     * @return an ArrayList or null if none
383     */
384    @Override
385    public ArrayList<String> getWhereUsed(String target) {
386        if (target == null || target.isEmpty()) {
387            log.error("Invalid target name for getWhereUsed");
388            return null;
389        }
390        return conditionalWhereUsed.get(target);
391    }
392
393    /**
394     * Remove a conditional reference from the array indicated by the target system name.
395     * @since 4.7.4
396     * @param target The system name for the target conditional
397     * @param reference The system name of the conditional that contains the conditional reference
398     */
399    @Override
400    public void removeWhereUsed(String target, String reference) {
401        if (target == null || target.isEmpty()) {
402            log.error("Invalid target name for removeWhereUsed");
403            return;
404        }
405        if (reference == null || reference.isEmpty()) {
406            log.error("Invalid reference name for removeWhereUsed");
407            return;
408        }
409
410        if (conditionalWhereUsed.containsKey(target)) {
411            ArrayList<?> refList = conditionalWhereUsed.get(target);
412            refList.remove(reference);
413            if (refList.isEmpty()) {
414                conditionalWhereUsed.remove(target);
415            }
416        }
417    }
418
419    /**
420     * Display the complete structure, used for debugging purposes.
421     * @since 4.7.4
422     */
423    @Override
424    public void displayWhereUsed() {
425        log.info("- Display Conditional Where Used     ");
426        SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet());
427        for (String key : keys) {
428        log.info("    Target: {}                  ", key);
429            ArrayList<String> refList = conditionalWhereUsed.get(key);
430            for (String ref : refList) {
431            log.info("      Reference: {}             ", ref);
432            }
433        }
434    }
435
436    /**
437     * Get the target system names used by this conditional
438     * @since 4.7.4
439     * @param reference The system name of the conditional the refers to other conditionals.
440     * @return a list of the target conditionals
441     */
442    @Override
443    public ArrayList<String> getTargetList(String reference) {
444        ArrayList<String> targetList = new ArrayList<>();
445        SortedSet<String> keys = new TreeSet<>(conditionalWhereUsed.keySet());
446        for (String key : keys) {
447            ArrayList<String> refList = conditionalWhereUsed.get(key);
448            for (String ref : refList) {
449                if (ref.equals(reference)) {
450                    targetList.add(key);
451                }
452            }
453        }
454        return targetList;
455    }
456
457    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultConditionalManager.class);
458}