001package jmri.jmrix.powerline;
002
003import java.util.Locale;
004import jmri.Manager.NameValidity;
005import java.util.regex.Matcher;
006import java.util.regex.Pattern;
007import jmri.NamedBean;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011/**
012 * Utility Class supporting parsing and testing of addresses.
013 * <p>
014 * Two address formats are supported: For X10: Ptnxx where: t is the type code,
015 * 'S' for sensors, and 'L' for lights n is the house code of the input or
016 * output bit (A - P) xx is a bit number of the input or output bit (1-16)
017 * examples: PLA2 (House Code A, Unit 2), PSK1 (House Code K, Unit 1) For
018 * Insteon: Pthh.hh.hh where: t is the type code, 'S' for sensors, and 'L' for
019 * lights aa is two hexadecimal digits examples: PLA2.43.CB
020 *
021 * @author Dave Duchamp, Copyright (C) 2004
022 * @author Bob Jacobsen, Copyright (C) 2006, 2007, 2008, 2009
023 * @author Ken Cameron, Copyright (C) 2008, 2009, 2010
024 */
025public class SerialAddress {
026
027    private Matcher hCodes = null;
028    private Matcher aCodes = null;
029    private Matcher iCodes = null;
030    private Matcher dCodes = null;
031    private static final char MIN_HOUSE_CODE = 'A';
032    private static final char MAX_HOUSE_CODE = 'P';
033
034    public SerialAddress(SerialSystemConnectionMemo m) {
035        this.memo = m;
036        hCodes = Pattern.compile("^(" + memo.getSystemPrefix() + ")([LTS])([" + MIN_HOUSE_CODE + "-" + MAX_HOUSE_CODE + "])(\\d++)$").matcher("");
037        aCodes = Pattern.compile("^(" + memo.getSystemPrefix() + ")([LTS]).*$").matcher("");
038        iCodes = Pattern.compile("^(" + memo.getSystemPrefix() + ")([LTS])(\\p{XDigit}\\p{XDigit})[.](\\p{XDigit}\\p{XDigit})[.](\\p{XDigit}\\p{XDigit})$").matcher("");
039        dCodes = Pattern.compile("^(" + memo.getSystemPrefix() + ")([L])(\\p{Digit}{1,3}+)$").matcher("");
040    }
041
042    SerialSystemConnectionMemo memo = null;
043
044    /**
045     * Validate the format for a system name.
046     *
047     * @param name   the name to validate
048     * @param type   the type letter for the name
049     * @param locale the locale for messages to the user
050     * @return the name, unchanged
051     */
052    String validateSystemNameFormat(String name, char type, Locale locale) {
053        boolean aTest = aCodes.reset(name).matches();
054        boolean hTest = hCodes.reset(name).matches();
055        boolean iTest = iCodes.reset(name).matches();
056        boolean dTest = dCodes.reset(name).matches();
057        if (!aTest || aCodes.group(2).charAt(0) != type) {
058            throw new NamedBean.BadSystemNameException(
059                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidPrefix", memo.getSystemPrefix() + type),
060                    Bundle.getMessage(locale, "InvalidSystemNameInvalidPrefix", memo.getSystemPrefix() + type));
061        } else if (hTest && hCodes.groupCount() == 4) {
062            // This is a PLaxx address - validate the house code and unit address fields
063            if (hCodes.group(3).charAt(0) < MIN_HOUSE_CODE || hCodes.group(3).charAt(0) > MAX_HOUSE_CODE) {
064                throw new NamedBean.BadSystemNameException(
065                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidHouseCode", name),
066                        Bundle.getMessage(locale, "InvalidSystemNameInvalidHouseCode", name));
067            }
068            try {
069                int num;
070                num = Integer.parseInt(hCodes.group(4));
071                if ((num < 1) || (num > 16)) {
072                    throw new NamedBean.BadSystemNameException(
073                            Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidDevice", name),
074                            Bundle.getMessage(locale, "InvalidSystemNameInvalidDevice", name));
075                }
076            } catch (NumberFormatException e) {
077                throw new NamedBean.BadSystemNameException(
078                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidDevice", name),
079                        Bundle.getMessage(locale, "InvalidSystemNameInvalidDevice", name));
080            }
081        } else if (iTest) {
082            // This is a PLaa.bb.cc address - validate the Insteon address fields
083            if (iCodes.groupCount() != 5) {
084                throw new NamedBean.BadSystemNameException(
085                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidInsteon", name),
086                        Bundle.getMessage(locale, "InvalidSystemNameInvalidInsteon", name));
087            }
088        } else if (dTest) {
089            // This is a PLnnn address - validate the DMX address fields
090            if (dCodes.groupCount() != 3) {
091                throw new NamedBean.BadSystemNameException(
092                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidDmx", name),
093                        Bundle.getMessage(locale, "InvalidSystemNameInvalidDmx", name));
094            }
095            try {
096                int num;
097                num = Integer.parseInt(dCodes.group(3));
098                if ((num < 1) || (num > 512)) {
099                    throw new NamedBean.BadSystemNameException(
100                            Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidDevice", name),
101                            Bundle.getMessage(locale, "InvalidSystemNameInvalidDevice", name));
102                }
103            } catch (NumberFormatException e) {
104                throw new NamedBean.BadSystemNameException(
105                        Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidDevice", name),
106                        Bundle.getMessage(locale, "InvalidSystemNameInvalidDevice", name));
107            }
108        } else {
109            throw new NamedBean.BadSystemNameException(
110                    Bundle.getMessage(Locale.ENGLISH, "InvalidSystemNameInvalidFormat", name),
111                    Bundle.getMessage(locale, "InvalidSystemNameInvalidFormat", name));
112        }
113        return name;
114    }
115
116    /**
117     * Public static method to validate system name format.
118     *
119     * @param systemName name to test
120     * @param type Letter indicating device type expected
121     * @return VALID if system name has a valid format, else return INVALID
122     */
123    public NameValidity validSystemNameFormat(String systemName, char type) {
124        try {
125            validateSystemNameFormat(systemName, type, Locale.getDefault());
126        } catch (IllegalArgumentException ex) {
127            // TODO: match possible prefixes as VALID_AS_PREFIX
128            return NameValidity.INVALID;
129        }
130        return NameValidity.VALID;
131    }
132
133    /**
134     * Public static method to validate system name for configuration returns
135     * 'true' if system name has a valid meaning in current configuration, else
136     * returns 'false'.
137     *
138     * @param systemName name to test
139     * @param type       type to test
140     * @return  true for valid names
141     */
142    public boolean validSystemNameConfig(String systemName, char type) {
143        return validSystemNameFormat(systemName, type) == NameValidity.VALID;
144    }
145
146    /**
147     * Public static method determines whether a systemName names an Insteon
148     * device.
149     *
150     * @param systemName name to test
151     * @return true if system name corresponds to Insteon device
152     */
153    public boolean isInsteon(String systemName) {
154        // ensure that input system name has a valid format
155        if ((!aCodes.reset(systemName).matches()) || (validSystemNameFormat(systemName, aCodes.group(2).charAt(0)) != NameValidity.VALID)) {
156            // No point in normalizing if a valid system name format is not present
157            return false;
158        } else {
159            if (hCodes.reset(systemName).matches() && hCodes.groupCount() == 4) {
160                // This is a PLaxx address
161                try {
162                    return false; // is X10, or at least not Insteon
163                } catch (Exception e) {
164                    log.error("illegal character in house code field system name: {}", systemName);
165                    return false;  // can't be parsed, isn't Insteon
166                }
167            }
168        }
169        return true;
170    }
171
172    /**
173     * Public static method to normalize a system name.
174     * <p>
175     * This routine is used to ensure that each system name is uniquely linked
176     * to one bit, by removing extra zeros inserted by the user.
177     * <p>
178     * If the supplied system name does not have a valid format, an empty string
179     * is returned. Otherwise a normalized name is returned in the same format
180     * as the input name.
181     *
182     * @param systemName name to process
183     * @return If the supplied system name does not have a valid format, an empty string
184     * is returned. Otherwise a normalized name is returned in the same format
185     * as the input name.
186     */
187    public String normalizeSystemName(String systemName) {
188        // ensure that input system name has a valid format, test all formats
189        boolean aMatch = aCodes.reset(systemName).matches();
190        int aCount = aCodes.groupCount();
191        boolean hMatch = hCodes.reset(systemName).matches();
192        int hCount = hCodes.groupCount();
193        boolean iMatch = iCodes.reset(systemName).matches();
194        int iCount = iCodes.groupCount();
195        if (!aMatch || aCount != 2 || (validSystemNameFormat(systemName, aCodes.group(2).charAt(0)) != NameValidity.VALID)) {
196            // No point in normalizing if a valid system name format is not present
197            // DMX addresses are normalized already
198            return "";
199        }
200        String nName = "";
201        // check for the presence of a char to differentiate the two address formats
202        if (hMatch && hCount == 4) {
203            // This is a PLaxx address
204            nName = hCodes.group(1) + hCodes.group(2) + hCodes.group(3) + Integer.toString(Integer.parseInt(hCodes.group(4)));
205        }
206        if (nName.equals("")) {
207            // check for the presence of a char to differentiate the two address formats
208            if (iMatch && iCount == 5) {
209                // This is a PLaa.bb.cc Insteon address
210                nName = iCodes.group(1) + iCodes.group(2) + iCodes.group(3) + "." + iCodes.group(4) + "." + iCodes.group(5);
211            } else {
212                if (log.isDebugEnabled()) {
213                    log.debug("valid name doesn't normalize: {} hMatch: {} hCount: {}", systemName, hMatch, hCount);
214                }
215            }
216        }
217        return nName;
218    }
219
220    /**
221     * Extract housecode from system name, as a letter A-P.
222     * <p>
223     * If the supplied system name does not have a valid format, an empty string
224     * is returned.
225     *
226     * @param systemName system name
227     * @return house code letter
228     */
229    public String houseCodeFromSystemName(String systemName) {
230        String hCode = "";
231        // ensure that input system name has a valid format
232        if ((!aCodes.reset(systemName).matches()) || (validSystemNameFormat(systemName, aCodes.group(2).charAt(0)) != NameValidity.VALID)) {
233            // No point in normalizing if a valid system name format is not present
234        } else {
235            if (hCodes.reset(systemName).matches() && hCodes.groupCount() == 2) {
236                // This is a PLaxx address
237                try {
238                    hCode = hCodes.group(1);
239                } catch (Exception e) {
240                    log.error("illegal character in house code field system name: {}", systemName);
241                    return "";
242                }
243            }
244        }
245        return hCode;
246    }
247
248    /**
249     * Extract devicecode from system name, as a string 1-16.
250     * 
251     * @param systemName name
252     * @return If the supplied system name does not have a valid format, an empty string
253     * is returned. X10 type device code
254     */
255    public String deviceCodeFromSystemName(String systemName) {
256        String dCode = "";
257        // ensure that input system name has a valid format
258        if ((!aCodes.reset(systemName).matches()) || (validSystemNameFormat(systemName, aCodes.group(2).charAt(0)) != NameValidity.VALID)) {
259            // No point in normalizing if a valid system name format is not present
260        } else {
261            if (hCodes.reset(systemName).matches()) {
262                if (hCodes.groupCount() == 2) {
263                    // This is a PLaxx address
264                    try {
265                        dCode = hCodes.group(2);
266                    } catch (Exception e) {
267                        log.error("illegal character in number field system name: {}", systemName);
268                        return "";
269                    }
270                }
271            } else {
272                if (iCodes.reset(systemName).matches()) {
273                    dCode = iCodes.group(3) + iCodes.group(4) + iCodes.group(5);
274                } else {
275                    log.error("illegal insteon address: {}", systemName);
276                    return "";
277                }
278            }
279        }
280        return dCode;
281    }
282
283    /**
284     * Extract housecode from system name, as a value 1-16.
285     * <p>
286     * If the supplied system name does not have a valid format, an -1 is
287     * returned.
288     *
289     * @param systemName name
290     * @return valid 1-16, invalid, return -1
291     */
292    public int x10HouseCodeAsValueFromSystemName(String systemName) {
293        int hCode = -1;
294        // ensure that input system name has a valid format
295        if ((!aCodes.reset(systemName).matches()) || (validSystemNameFormat(systemName, aCodes.group(2).charAt(0)) != NameValidity.VALID)) {
296            // No point in normalizing if a valid system name format is not present
297        } else {
298            if (hCodes.reset(systemName).matches() && hCodes.groupCount() == 4) {
299                // This is a PLaxx address
300                try {
301                    hCode = hCodes.group(3).charAt(0) - 0x40;
302                } catch (Exception e) {
303                    log.error("illegal character in number field system name: {}", systemName);
304                    return -1;
305                }
306            }
307        }
308        return hCode;
309    }
310
311    /**
312     * Extract devicecode from system name, as a value 1-16.
313     * <p>
314     * If the supplied system name does not have a valid format, an -1 is
315     * returned.
316     *
317     * @param systemName name
318     * @return value of X10 device code, -1 if invalid
319     */
320    public int x10DeviceCodeAsValueFromSystemName(String systemName) {
321        int dCode = -1;
322        // ensure that input system name has a valid format
323        if ((!aCodes.reset(systemName).matches()) || (validSystemNameFormat(systemName, aCodes.group(2).charAt(0)) != NameValidity.VALID)) {
324            // No point in normalizing if a valid system name format is not present
325        } else {
326            if (hCodes.reset(systemName).matches() && hCodes.groupCount() == 4) {
327                // This is a PLaxx address
328                try {
329                    dCode = Integer.parseInt(hCodes.group(4));
330                } catch (NumberFormatException e) {
331                    log.error("illegal character in number field system name: {}", systemName);
332                    return -1;
333                }
334            }
335        }
336        return dCode;
337    }
338
339    /**
340     * Extract Insteon high device id from system name.
341     * <p>
342     * If the supplied system name does not have a valid format, an empty string
343     * is returned.
344     *
345     * @param systemName name
346     * @return Insteon high byte value
347     */
348    public int insteonIdHighCodeAsValueFromSystemName(String systemName) {
349        int dCode = -1;
350        // ensure that input system name has a valid format
351        if (!iCodes.reset(systemName).matches() || validSystemNameFormat(systemName, iCodes.group(2).charAt(0)) != NameValidity.VALID) {
352            // No point in normalizing if a valid system name format is not present
353        } else {
354            if (iCodes.groupCount() == 5) {
355                // This is a PLhh.mm.ll address
356                try {
357                    dCode = Integer.parseInt(iCodes.group(3), 16);
358                } catch (NumberFormatException e) {
359                    log.error("illegal character in high id system name: {}", systemName);
360                    return -1;
361                }
362            }
363        }
364        return dCode;
365    }
366
367    /**
368     * Extract Insteon middle device id from system name.
369     * <p>
370     * If the supplied system name does not have a valid format, an empty string
371     * is returned.
372     *
373     * @param systemName name
374     * @return Insteon middle id value, -1 if invalid
375     */
376    public int insteonIdMiddleCodeAsValueFromSystemName(String systemName) {
377        int dCode = -1;
378        // ensure that input system name has a valid format
379        if (!iCodes.reset(systemName).matches() || validSystemNameFormat(systemName, iCodes.group(2).charAt(0)) != NameValidity.VALID) {
380            // No point in normalizing if a valid system name format is not present
381        } else {
382            if (iCodes.groupCount() == 5) {
383                // This is a PLhh.mm.ll address
384                try {
385                    dCode = Integer.parseInt(iCodes.group(4), 16);
386                } catch (NumberFormatException e) {
387                    log.error("illegal character in high id system name: {}", systemName);
388                    return -1;
389                }
390            }
391        }
392        return dCode;
393    }
394
395    /**
396     * Extract Insteon low device id from system name.
397     * <p>
398     * If the supplied system name does not have a valid format, an empty string
399     * is returned.
400     *
401     * @param systemName name
402     * @return Insteon low value id, -1 if invalid
403     */
404    public int insteonIdLowCodeAsValueFromSystemName(String systemName) {
405        int dCode = -1;
406        // ensure that input system name has a valid format
407        if (!iCodes.reset(systemName).matches() || validSystemNameFormat(systemName, iCodes.group(2).charAt(0)) != NameValidity.VALID) {
408            // No point in normalizing if a valid system name format is not present
409        } else {
410            if (iCodes.groupCount() == 5) {
411                // This is a PLhh.mm.ll address
412                try {
413                    dCode = Integer.parseInt(iCodes.group(5), 16);
414                } catch (NumberFormatException e) {
415                    log.error("illegal character in high id system name: {}", systemName);
416                    return -1;
417                }
418            }
419        }
420        return dCode;
421    }
422
423    /**
424     * Extract DMX unit id from system name.
425     * <p>
426     * If the supplied system name does not have a valid format, an empty string
427     * is returned.
428     *
429     * @param systemName name
430     * @return dmx unit id, -1 if invalid
431     */
432
433    public int dmxUnitIdCodeAsValueFromSystemName(String systemName) {
434        int dCode = -1;
435        // ensure that input system name has a valid format
436        if (!dCodes.reset(systemName).matches() || validSystemNameFormat(systemName, dCodes.group(2).charAt(0)) != NameValidity.VALID) {
437            // No point in normalizing if a valid system name format is not present
438        } else {
439            if (dCodes.groupCount() == 3) {
440                // This is a PLddd address
441                try {
442                    dCode = Integer.parseInt(dCodes.group(3));
443                } catch (NumberFormatException e) {
444                    log.error("illegal character in dmx unit id system name: {}", systemName);
445                    return -1;
446                }
447            }
448        }
449        return dCode;
450    }
451    
452    private final static Logger log = LoggerFactory.getLogger(SerialAddress.class);
453
454}