001package jmri.jmrix;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.Iterator;
010import java.util.List;
011import java.util.Locale;
012import java.util.Properties;
013import java.util.ServiceLoader;
014import java.util.Set;
015import java.util.TreeSet;
016import javax.annotation.CheckForNull;
017import javax.annotation.Nonnull;
018import jmri.InstanceManager;
019import jmri.configurexml.ClassMigrationManager;
020import jmri.configurexml.ConfigXmlManager;
021import jmri.configurexml.ErrorHandler;
022import jmri.configurexml.ErrorMemo;
023import jmri.configurexml.XmlAdapter;
024import jmri.jmrix.internal.InternalConnectionTypeList;
025import jmri.profile.Profile;
026import jmri.profile.ProfileUtils;
027import jmri.spi.PreferencesManager;
028import jmri.util.jdom.JDOMUtil;
029import jmri.util.prefs.AbstractPreferencesManager;
030import jmri.util.prefs.HasConnectionButUnableToConnectException;
031import org.jdom2.Element;
032import org.jdom2.JDOMException;
033import org.openide.util.lookup.ServiceProvider;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037/**
038 * Manager for ConnectionConfig objects.
039 *
040 * @author Randall Wood (C) 2015
041 */
042@ServiceProvider(service = PreferencesManager.class)
043public class ConnectionConfigManager extends AbstractPreferencesManager implements Iterable<ConnectionConfig> {
044
045    private final ArrayList<ConnectionConfig> connections = new ArrayList<>();
046    private final static String NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/connections-2-9-6.xsd"; // NOI18N
047    public final static String CONNECTIONS = "connections"; // NOI18N
048    public final static String CONNECTION = "connection"; // NOI18N
049    public final static String CLASS = "class"; // NOI18N
050    public final static String USER_NAME = "userName"; // NOI18N
051    public final static String SYSTEM_NAME = "systemPrefix"; // NOI18N
052    public final static String MANUFACTURER = "manufacturer"; // NOI18N
053    private final static Logger log = LoggerFactory.getLogger(ConnectionConfigManager.class);
054
055    @Override
056    public void initialize(Profile profile) throws HasConnectionButUnableToConnectException {
057        if (!isInitialized(profile)) {
058            log.debug("Initializing...");
059            Element sharedConnections = null;
060            Element perNodeConnections = null;
061            this.setPortNamePattern();
062            try {
063                sharedConnections = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(profile).getConfigurationFragment(CONNECTIONS, NAMESPACE, true));
064            } catch (NullPointerException ex) {
065                // Normal if this is a new profile
066                log.info("No connections configured.");
067                log.debug("Null pointer thrown reading shared configuration.", ex);
068            }
069            if (sharedConnections != null) {
070                try {
071                    perNodeConnections = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(profile).getConfigurationFragment(CONNECTIONS, NAMESPACE, false));
072                } catch (NullPointerException ex) {
073                    // Normal if the profile has not been used on this computer
074                    log.info("No local configuration found.");
075                    log.debug("Null pointer thrown reading local configuration.", ex);
076                    // TODO: notify user
077                }
078                for (Element shared : sharedConnections.getChildren(CONNECTION)) {
079                    Element perNode = shared;
080                    String className = shared.getAttributeValue(CLASS);
081                    String userName = shared.getAttributeValue(USER_NAME, ""); // NOI18N
082                    String systemName = shared.getAttributeValue(SYSTEM_NAME, ""); // NOI18N
083                    String manufacturer = shared.getAttributeValue(MANUFACTURER, ""); // NOI18N
084                    log.debug("Read shared connection {}:{} ({}) class {}", userName, systemName, manufacturer, className);
085                    if (perNodeConnections != null) {
086                        for (Element e : perNodeConnections.getChildren(CONNECTION)) {
087                            if (systemName.equals(e.getAttributeValue(SYSTEM_NAME))) {
088                                perNode = e;
089                                className = perNode.getAttributeValue(CLASS);
090                                userName = perNode.getAttributeValue(USER_NAME, ""); // NOI18N
091                                manufacturer = perNode.getAttributeValue(MANUFACTURER, ""); // NOI18N
092                                log.debug("Read perNode connection {}:{} ({}) class {}", userName, systemName, manufacturer, className);
093                            }
094                        }
095                    }
096                    String newClassName = InstanceManager.getDefault(ClassMigrationManager.class).getClassName(className);
097                    if (!className.equals(newClassName)) {
098                        log.info("Class {} will be used for connection {} instead of {} if preferences are saved", newClassName, userName, className);
099                        className = newClassName;
100                    }
101                    try {
102                        log.debug("Creating connection {}:{} ({}) class {}", userName, systemName, manufacturer, className);
103                        XmlAdapter adapter = (XmlAdapter) Class.forName(className).getDeclaredConstructor().newInstance();
104                        ConnectionConfigManagerErrorHandler handler = new ConnectionConfigManagerErrorHandler();
105                        adapter.setExceptionHandler(handler);
106                        if (!adapter.load(shared, perNode)) {
107                            log.error("Unable to create {} for {}, load returned false", className, shared);
108                            String english = Bundle.getMessage(Locale.ENGLISH, "ErrorSingleConnection", userName, systemName); // NOI18N
109                            String localized = Bundle.getMessage("ErrorSingleConnection", userName, systemName); // NOI18N
110                            this.addInitializationException(profile, new HasConnectionButUnableToConnectException(english, localized));
111                        }
112                        handler.exceptions.forEach((exception) -> {
113                            this.addInitializationException(profile, exception);
114                        });
115                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex) {
116                        log.error("Unable to create {} for {}", className, shared, ex);
117                        String english = Bundle.getMessage(Locale.ENGLISH, "ErrorSingleConnection", userName, systemName); // NOI18N
118                        String localized = Bundle.getMessage("ErrorSingleConnection", userName, systemName); // NOI18N
119                        this.addInitializationException(profile, new HasConnectionButUnableToConnectException(english, localized, ex));
120                    } catch (RuntimeException | jmri.configurexml.JmriConfigureXmlException ex) {
121                        log.error("Unable to load {} into {}", shared, className, ex);
122                        String english = Bundle.getMessage(Locale.ENGLISH, "ErrorSingleConnection", userName, systemName); // NOI18N
123                        String localized = Bundle.getMessage("ErrorSingleConnection", userName, systemName); // NOI18N
124                        this.addInitializationException(profile, new HasConnectionButUnableToConnectException(english, localized, ex));
125                    }
126                }
127            }
128            setInitialized(profile, true);
129            List<Exception> exceptions = this.getInitializationExceptions(profile);
130            if (exceptions.size() == 1) {
131                if (exceptions.get(0) instanceof HasConnectionButUnableToConnectException) {
132                    throw (HasConnectionButUnableToConnectException) exceptions.get(0);
133                } else {
134                    throw new HasConnectionButUnableToConnectException(exceptions.get(0));
135                }
136            } else if (exceptions.size() > 1) {
137                String english = Bundle.getMessage(Locale.ENGLISH, "ErrorMultipleConnections"); // NOI18N
138                String localized = Bundle.getMessage("ErrorMultipleConnections"); // NOI18N
139                throw new HasConnectionButUnableToConnectException(english, localized);
140            }
141            log.debug("Initialized...");
142        }
143    }
144
145    @Override
146    @Nonnull
147    public Set<Class<? extends PreferencesManager>> getRequires() {
148        return new HashSet<>();
149    }
150
151    @Override
152    public void savePreferences(Profile profile) {
153        log.debug("Saving connections preferences...");
154        // store shared Connection preferences
155        savePreferences(profile, true);
156        // store private or perNode Connection preferences
157        savePreferences(profile, false);
158        log.debug("Saved connections preferences...");
159    }
160
161    private synchronized void savePreferences(Profile profile, boolean shared) {
162        Element element = new Element(CONNECTIONS, NAMESPACE);
163        connections.stream().forEach((o) -> {
164            log.debug("Saving connection {} ({})...", o.getConnectionName(), shared);
165            Element e = ConfigXmlManager.elementFromObject(o, shared);
166            if (e != null) {
167                element.addContent(e);
168            }
169        });
170        // save connections, or save an empty connections element if user removed all connections
171        try {
172            ProfileUtils.getAuxiliaryConfiguration(profile).putConfigurationFragment(JDOMUtil.toW3CElement(element), shared);
173        } catch (JDOMException ex) {
174            log.error("Unable to create create XML", ex);
175        }
176    }
177
178    /**
179     * Add a {@link jmri.jmrix.ConnectionConfig} following the rules specified
180     * in {@link java.util.Collection#add(java.lang.Object)}.
181     *
182     * @param c an existing ConnectionConfig
183     * @return true if c was added, false otherwise
184     * @throws NullPointerException if c is null
185     */
186    public boolean add(@Nonnull ConnectionConfig c) throws NullPointerException {
187        if (c == null) {
188            throw new NullPointerException();
189        }
190        if (!connections.contains(c)) {
191            boolean result = connections.add(c);
192            int i = connections.indexOf(c);
193            fireIndexedPropertyChange(CONNECTIONS, i, null, c);
194            return result;
195        }
196        return false;
197    }
198
199    /**
200     * Remove a {@link jmri.jmrix.ConnectionConfig} following the rules
201     * specified in {@link java.util.Collection#add(java.lang.Object)}.
202     *
203     * @param c an existing ConnectionConfig
204     * @return true if c was removed, false otherwise
205     */
206    public boolean remove(@Nonnull ConnectionConfig c) {
207        int i = connections.indexOf(c);
208        boolean result = connections.remove(c);
209        if (result) {
210            fireIndexedPropertyChange(CONNECTIONS, i, c, null);
211        }
212        return result;
213    }
214
215    /**
216     * Get an Array of {@link jmri.jmrix.ConnectionConfig} objects.
217     *
218     * @return an Array, possibly empty if there are no ConnectionConfig
219     *         objects.
220     */
221    @Nonnull
222    public ConnectionConfig[] getConnections() {
223        return connections.toArray(new ConnectionConfig[connections.size()]);
224    }
225
226    /**
227     * Get the {@link jmri.jmrix.ConnectionConfig} at index following the rules
228     * specified in {@link java.util.Collection#add(java.lang.Object)}.
229     *
230     * @param index index of the ConnectionConfig to return
231     * @return the ConnectionConfig at the specified location
232     */
233    public ConnectionConfig getConnections(int index) {
234        return connections.get(index);
235    }
236
237    @Override
238    public Iterator<ConnectionConfig> iterator() {
239        return connections.iterator();
240    }
241
242    /**
243     * Get the class names for classes supporting layout connections for the
244     * given manufacturer.
245     *
246     * @param manufacturer the name of the manufacturer
247     * @return An array of supporting class names; will return the list of
248     *         internal connection classes if manufacturer is not a known
249     *         manufacturer; the array may be empty if there are no supporting
250     *         classes for the given manufacturer.
251     */
252    @Nonnull
253    public String[] getConnectionTypes(@Nonnull String manufacturer) {
254        return this.getDefaultConnectionTypeManager().getConnectionTypes(manufacturer);
255    }
256
257    /**
258     * Get the list of known manufacturers.
259     *
260     * @return An array of known manufacturers.
261     */
262    @Nonnull
263    public String[] getConnectionManufacturers() {
264        return this.getDefaultConnectionTypeManager().getConnectionManufacturers();
265    }
266
267    /**
268     * Get the manufacturer that is supported by a connection type. If there are
269     * multiple manufacturers supported by connectionType, returns only the
270     * first manufacturer.
271     *
272     * @param connectionType the class name of a connection type.
273     * @return the supported manufacturer. Returns null if no manufacturer is
274     *         associated with the connectionType.
275     */
276    @CheckForNull
277    public String getConnectionManufacturer(@Nonnull String connectionType) {
278        for (String manufacturer : this.getConnectionManufacturers()) {
279            for (String manufacturerType : this.getConnectionTypes(manufacturer)) {
280                if (connectionType.equals(manufacturerType)) {
281                    return manufacturer;
282                }
283            }
284        }
285        return null;
286    }
287
288    /**
289     * Get the list of all known manufacturers that a single connection type
290     * supports.
291     *
292     * @param connectionType the class name of a connection type.
293     * @return an Array of supported manufacturers. Returns an empty Array if no
294     *         manufacturer is associated with the connectionType.
295     */
296    @Nonnull
297    public String[] getConnectionManufacturers(@Nonnull String connectionType) {
298        ArrayList<String> manufacturers = new ArrayList<>();
299        for (String manufacturer : this.getConnectionManufacturers()) {
300            for (String manufacturerType : this.getConnectionTypes(manufacturer)) {
301                if (connectionType.equals(manufacturerType)) {
302                    manufacturers.add(manufacturer);
303                }
304            }
305        }
306        return manufacturers.toArray(new String[manufacturers.size()]);
307    }
308
309    /**
310     * Get the default {@link ConnectionTypeManager}, creating it if needed.
311     *
312     * @return the default ConnectionTypeManager
313     */
314    private ConnectionTypeManager getDefaultConnectionTypeManager() {
315        if (InstanceManager.getNullableDefault(ConnectionTypeManager.class) == null) {
316            InstanceManager.setDefault(ConnectionTypeManager.class, new ConnectionTypeManager());
317        }
318        return InstanceManager.getDefault(ConnectionTypeManager.class);
319    }
320
321    private static class ConnectionTypeManager {
322
323        private final HashMap<String, ConnectionTypeList> connectionTypeLists = new HashMap<>();
324
325        public ConnectionTypeManager() {
326            for (ConnectionTypeList ctl : ServiceLoader.load(ConnectionTypeList.class)) {
327                for (String manufacturer : ctl.getManufacturers()) {
328                    if (!connectionTypeLists.containsKey(manufacturer)) {
329                        connectionTypeLists.put(manufacturer, ctl);
330                        log.debug("Added {} connectionTypeList", manufacturer);
331                    } else {
332                        log.debug("Need a proxy for {} from {} in {}", manufacturer, ctl.getClass().getName(), this);
333                        ProxyConnectionTypeList proxy;
334                        ConnectionTypeList existing = connectionTypeLists.get(manufacturer);
335                        if (existing instanceof ProxyConnectionTypeList) {
336                            proxy = (ProxyConnectionTypeList) existing;
337                        } else {
338                            proxy = new ProxyConnectionTypeList(existing);
339                        }
340                        proxy.add(ctl);
341                        connectionTypeLists.put(manufacturer, proxy);
342                    }
343                }
344            }
345        }
346
347        public String[] getConnectionTypes(String manufacturer) {
348            ConnectionTypeList ctl = this.connectionTypeLists.get(manufacturer);
349            if (ctl != null) {
350                return ctl.getAvailableProtocolClasses();
351            }
352            return this.connectionTypeLists.get(InternalConnectionTypeList.NONE).getAvailableProtocolClasses();
353        }
354
355        public String[] getConnectionManufacturers() {
356            ArrayList<String> a = new ArrayList<>(this.connectionTypeLists.keySet());
357            a.remove(InternalConnectionTypeList.NONE);
358            a.sort(null);
359            a.add(0, InternalConnectionTypeList.NONE);
360            return a.toArray(new String[a.size()]);
361        }
362
363    }
364
365    private static class ProxyConnectionTypeList implements ConnectionTypeList {
366
367        private final ArrayList<ConnectionTypeList> connectionTypeLists = new ArrayList<>();
368
369        public ProxyConnectionTypeList(@Nonnull ConnectionTypeList connectionTypeList) {
370            log.debug("Creating proxy for {}", connectionTypeList.getManufacturers()[0]);
371            this.add(connectionTypeList);
372        }
373
374        public final void add(@Nonnull ConnectionTypeList connectionTypeList) {
375            log.debug("Adding {} to proxy", connectionTypeList.getClass().getName());
376            this.connectionTypeLists.add(connectionTypeList);
377        }
378
379        @Override
380        @Nonnull
381        public String[] getAvailableProtocolClasses() {
382            TreeSet<String> classes = new TreeSet<>();
383            this.connectionTypeLists.stream().forEach((connectionTypeList) -> {
384                classes.addAll(Arrays.asList(connectionTypeList.getAvailableProtocolClasses()));
385            });
386            return classes.toArray(new String[classes.size()]);
387        }
388
389        @Override
390        @Nonnull
391        public String[] getManufacturers() {
392            TreeSet<String> manufacturers = new TreeSet<>();
393            this.connectionTypeLists.stream().forEach((connectionTypeList) -> {
394                manufacturers.addAll(Arrays.asList(connectionTypeList.getManufacturers()));
395            });
396            return manufacturers.toArray(new String[manufacturers.size()]);
397        }
398
399    }
400
401    /**
402     * Override the default port name patterns unless the
403     * purejavacomm.portnamepattern property was set on the command line.
404     */
405    private void setPortNamePattern() {
406        final String pattern = "purejavacomm.portnamepattern";
407        Properties properties = System.getProperties();
408        if (properties.getProperty(pattern) == null) {
409            try (InputStream in = ConnectionConfigManager.class.getResourceAsStream("PortNamePatterns.properties")) { // NOI18N
410                properties.load(in);
411            } catch (IOException ex) {
412                log.error("Unable to read PortNamePatterns.properties", ex);
413            }
414        }
415    }
416
417    private static class ConnectionConfigManagerErrorHandler extends ErrorHandler {
418
419        ArrayList<HasConnectionButUnableToConnectException> exceptions = new ArrayList<>();
420
421        public ConnectionConfigManagerErrorHandler() {
422            super();
423        }
424
425        /**
426         * Capture ErrorMemos as initialization exceptions. {@inheritDoc}
427         */
428        @Override
429        // The memo has a generic message (since the real cause never makes it this far anyway)
430        // If the memo reliably had an exception, we could make a decision about
431        // how to handle that, but since it doesn't all we can do is log it
432        public void handle(ErrorMemo memo) {
433            if (memo.exception != null) {
434                this.exceptions.add(new HasConnectionButUnableToConnectException(memo.description, Bundle.getMessage("ErrorSubException", memo.description), memo.exception));
435            } else {
436                this.exceptions.add(new HasConnectionButUnableToConnectException(memo.description, Bundle.getMessage("ErrorSubException", memo.description) + memo.description));
437            }
438        }
439    }
440
441}