001package jmri.configurexml;
002
003import java.io.File;
004import java.net.URL;
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.LinkedHashMap;
008import java.util.List;
009import java.util.Map;
010
011import javax.annotation.CheckReturnValue;
012
013import jmri.InstanceManager;
014import jmri.jmrit.XmlFile;
015import jmri.jmrit.revhistory.FileHistory;
016import jmri.util.FileUtil;
017
018import org.jdom2.Attribute;
019import org.jdom2.Document;
020import org.jdom2.Element;
021import org.jdom2.ProcessingInstruction;
022import org.slf4j.Logger;
023import org.slf4j.LoggerFactory;
024
025/**
026 * Provides the mechanisms for storing an entire layout configuration to XML.
027 * "Layout" refers to the hardware: Specific communication systems, etc.
028 *
029 * @see <a href="package-summary.html">Package summary for details of the
030 * overall structure</a>
031 * @author Bob Jacobsen Copyright (c) 2002, 2008
032 */
033public class ConfigXmlManager extends jmri.jmrit.XmlFile
034        implements jmri.ConfigureManager {
035
036    /**
037     * Define the current schema version string for the layout-config schema.
038     * See the <a href="package-summary.html#schema">Schema versioning
039     * discussion</a>. Also controls the stylesheet file version.
040     */
041    static final public String schemaVersion = "-5-5-5";
042
043    public ConfigXmlManager() {
044    }
045
046    /** {@inheritDoc} */
047    @Override
048    public void registerConfig(Object o) {
049        registerConfig(o, 50);
050    }
051
052    /** {@inheritDoc} */
053    @Override
054    public void registerPref(Object o) {
055        // skip if already present, leaving in original order
056        if (plist.contains(o)) {
057            return;
058        }
059        confirmAdapterAvailable(o);
060        // and add to list
061        plist.add(o);
062    }
063
064    /**
065     * Common check routine to confirm an adapter is available as part of
066     * registration process.
067     * <p>
068     * Note: Should only be called for debugging purposes, for example, when
069     * Log4J DEBUG level is selected, to load fewer classes at startup.
070     *
071     * @param o object to confirm XML adapter exists for
072     */
073    void confirmAdapterAvailable(Object o) {
074        if (log.isDebugEnabled()) {
075            String adapter = adapterName(o);
076            log.debug("register {} adapter {}", o, adapter);
077            if (adapter != null) {
078                try {
079                    Class.forName(adapter);
080                } catch (ClassNotFoundException | NoClassDefFoundError ex) {
081                    locateClassFailed(ex, adapter, o);
082                }
083            }
084        }
085    }
086
087    /**
088     * Handles ConfigureXml classes that have moved to a new package or been
089     * superseded.
090     *
091     * @param name name of the moved or superceded ConfigureXml class
092     * @return name of the ConfigureXml class in newer package or of superseding
093     *         class
094     */
095    static public String currentClassName(String name) {
096        return InstanceManager.getDefault(ClassMigrationManager.class).getClassName(name);
097    }
098
099    /** {@inheritDoc} */
100    @Override
101    public void removePrefItems() {
102        log.debug("removePrefItems dropped {}", plist.size());
103        plist.clear();
104    }
105
106    /** {@inheritDoc} */
107    @Override
108    public Object findInstance(Class<?> c, int index) {
109        List<Object> temp = new ArrayList<>(plist);
110        temp.addAll(clist.keySet());
111        temp.addAll(tlist);
112        temp.addAll(ulist);
113        temp.addAll(uplist);
114        for (Object o : temp) {
115            if (c.isInstance(o)) {
116                if (index-- == 0) {
117                    return o;
118                }
119            }
120        }
121        return null;
122    }
123
124    /** {@inheritDoc} */
125    @Override
126    public List<Object> getInstanceList(Class<?> c) {
127        List<Object> result = new ArrayList<>();
128
129        List<Object> temp = new ArrayList<>(plist);
130        temp.addAll(clist.keySet());
131        temp.addAll(tlist);
132        temp.addAll(ulist);
133        temp.addAll(uplist);
134        for (Object o : temp) {
135            if (c.isInstance(o)) {
136                result.add(o);
137            }
138        }
139        return result;
140    }
141
142    /** {@inheritDoc} */
143    @Override
144    public void registerConfig(Object o, int x) {
145        // skip if already present, leaving in original order
146        if (clist.containsKey(o)) {
147            return;
148        }
149        confirmAdapterAvailable(o);
150        // and add to list
151        clist.put(o, x);
152    }
153
154    /** {@inheritDoc} */
155    @Override
156    public void registerTool(Object o) {
157        // skip if already present, leaving in original order
158        if (tlist.contains(o)) {
159            return;
160        }
161        confirmAdapterAvailable(o);
162        // and add to list
163        tlist.add(o);
164    }
165
166    /**
167     * Register an object whose state is to be tracked. It is not an error if
168     * the original object was already registered.
169     *
170     * @param o The object, which must have an associated adapter class.
171     */
172    @Override
173    public void registerUser(Object o) {
174        // skip if already present, leaving in original order
175        if (ulist.contains(o)) {
176            return;
177        }
178        confirmAdapterAvailable(o);
179        // and add to list
180        ulist.add(o);
181    }
182
183    /** {@inheritDoc} */
184    @Override
185    public void registerUserPrefs(Object o) {
186        // skip if already present, leaving in original order
187        if (uplist.contains(o)) {
188            return;
189        }
190        confirmAdapterAvailable(o);
191        // and add to list
192        uplist.add(o);
193    }
194
195    /** {@inheritDoc} */
196    @Override
197    public void deregister(Object o) {
198        plist.remove(o);
199        if (o != null) {
200            clist.remove(o);
201        }
202        tlist.remove(o);
203        ulist.remove(o);
204        uplist.remove(o);
205    }
206
207    private List<Object> plist = new ArrayList<>();
208    Map<Object, Integer> clist = Collections.synchronizedMap(new LinkedHashMap<>());
209    private List<Object> tlist = new ArrayList<>();
210    private List<Object> ulist = new ArrayList<>();
211    private List<Object> uplist = new ArrayList<>();
212    private final List<Element> loadDeferredList = new ArrayList<>();
213
214    /**
215     * Find the name of the adapter class for an object.
216     *
217     * @param o object of a configurable type
218     * @return class name of adapter
219     */
220    public static String adapterName(Object o) {
221        String className = o.getClass().getName();
222        log.trace("handle object of class {}", className);
223        int lastDot = className.lastIndexOf(".");
224        if (lastDot > 0) {
225            // found package-class boundary OK
226            String result = className.substring(0, lastDot)
227                    + ".configurexml."
228                    + className.substring(lastDot + 1, className.length())
229                    + "Xml";
230            log.trace("adapter class name is {}", result);
231            return result;
232        } else {
233            // no last dot found!
234            log.error("No package name found, which is not yet handled!");
235            return null;
236        }
237    }
238
239    /**
240     * Handle failure to load adapter class. Although only a one-liner in this
241     * class, it is a separate member to facilitate testing.
242     *
243     * @param ex          the exception throw failing to load adapterName as o
244     * @param adapterName name of the adapter class
245     * @param o           adapter object
246     */
247    void locateClassFailed(Throwable ex, String adapterName, Object o) {
248        log.error("{} could not load adapter class {}", ex, adapterName);
249        log.debug("Stack trace is", ex);
250    }
251
252    protected Element initStore() {
253        Element root = new Element("layout-config");
254        root.setAttribute("noNamespaceSchemaLocation",
255                "http://jmri.org/xml/schema/layout" + schemaVersion + ".xsd",
256                org.jdom2.Namespace.getNamespace("xsi",
257                        "http://www.w3.org/2001/XMLSchema-instance"));
258        return root;
259    }
260
261    protected void addPrefsStore(Element root) {
262        for (int i = 0; i < plist.size(); i++) {
263            Object o = plist.get(i);
264            Element e = elementFromObject(o);
265            if (e != null) {
266                root.addContent(e);
267            }
268        }
269    }
270
271    @CheckReturnValue
272    protected boolean addConfigStore(Element root) {
273        boolean result = true;
274        List<Map.Entry<Object, Integer>> l = new ArrayList<>(clist.entrySet());
275        Collections.sort(l, (Map.Entry<Object, Integer> o1, Map.Entry<Object, Integer> o2) -> o1.getValue().compareTo(o2.getValue()));
276        for (int i = 0; i < l.size(); i++) {
277            try {
278                Object o = l.get(i).getKey();
279                Element e = elementFromObject(o);
280                if (e != null) {
281                    root.addContent(e);
282                }
283            } catch (Exception e) {
284                storingErrorEncountered(null, "storing to file in addConfigStore",
285                        "Exception thrown", null, null, e);
286                result = false;
287            }
288        }
289        return result;
290    }
291
292    @CheckReturnValue
293    protected boolean addToolsStore(Element root) {
294        boolean result = true;
295        for (Object o : tlist) {
296            try {
297                Element e = elementFromObject(o);
298                if (e != null) {
299                    root.addContent(e);
300                }
301            } catch (Exception e) {
302                result = false;
303                storingErrorEncountered(null, "storing to file in addToolsStore",
304                        "Exception thrown", null, null, e);
305            }
306        }
307        return result;
308    }
309
310    @CheckReturnValue
311    protected boolean addUserStore(Element root) {
312        boolean result = true;
313        for (Object o : ulist) {
314            try {
315                Element e = elementFromObject(o);
316                if (e != null) {
317                    root.addContent(e);
318                }
319            } catch (Exception e) {
320                result = false;
321                storingErrorEncountered(null, "storing to file in addUserStore",
322                        "Exception thrown", null, null, e);
323            }
324        }
325        return result;
326    }
327
328    protected void addUserPrefsStore(Element root) {
329        for (Object o : uplist) {
330            Element e = elementFromObject(o);
331            if (e != null) {
332                root.addContent(e);
333            }
334        }
335    }
336
337    protected void includeHistory(Element root, File file) {
338        // add history to end of document
339        if (InstanceManager.getNullableDefault(FileHistory.class) != null) {
340            var historyElement = jmri.jmrit.revhistory.configurexml.FileHistoryXml.storeDirectly(
341                    InstanceManager.getDefault(FileHistory.class), file.getPath());
342            if (historyElement != null) {
343                root.addContent(historyElement);
344            }
345        }
346    }
347
348    @CheckReturnValue
349    protected boolean finalStore(Element root, File file) {
350        try {
351            // Document doc = newDocument(root, dtdLocation+"layout-config-"+dtdVersion+".dtd");
352            Document doc = newDocument(root);
353
354            // add XSLT processing instruction
355            // <?xml-stylesheet type="text/xsl" href="XSLT/panelfile"+schemaVersion+".xsl"?>
356            java.util.Map<String, String> m = new java.util.HashMap<>();
357            m.put("type", "text/xsl");
358            m.put("href", xsltLocation + "panelfile" + schemaVersion + ".xsl");
359            ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
360            doc.addContent(0, p);
361
362            // add version at front
363            storeVersion(root);
364
365            writeXML(file, doc);
366        } catch (java.io.FileNotFoundException ex3) {
367            storingErrorEncountered(null, "storing to file " + file.getName(),
368                    "File not found " + file.getName(), null, null, ex3);
369            log.error("FileNotFound error writing file: {}", ex3.getLocalizedMessage());
370            return false;
371        } catch (java.io.IOException ex2) {
372            storingErrorEncountered(null, "storing to file " + file.getName(),
373                    "IO error writing file " + file.getName(), null, null, ex2);
374            log.error("IO error writing file: {}", ex2.getLocalizedMessage());
375            return false;
376        }
377        return true;
378    }
379
380    /** {@inheritDoc} */
381    @Override
382    public void storePrefs() {
383        storePrefs(prefsFile);
384    }
385
386    /** {@inheritDoc} */
387    @Override
388    public void storePrefs(File file) {
389        synchronized (this) {
390            Element root = initStore();
391            addPrefsStore(root);
392            boolean result = finalStore(root, file);
393            if (!result) {
394                log.error("storePrefs failed to store:{}",file);
395            }
396        }
397    }
398
399    /** {@inheritDoc} */
400    @Override
401    public void storeUserPrefs(File file) {
402        synchronized (this) {
403            Element root = initStore();
404            addUserPrefsStore(root);
405            boolean result = finalStore(root, file);
406            if (!result) {
407                log.error("storeUserPrefs failed to store:{}",file);
408            }
409        }
410    }
411
412    /**
413     * Set location for preferences file.
414     * <p>
415     * File need not exist, but location must be writable when storePrefs()
416     * called.
417     *
418     * @param prefsFile new location for preferences file
419     */
420    public void setPrefsLocation(File prefsFile) {
421        this.prefsFile = prefsFile;
422    }
423    File prefsFile;
424
425    /** {@inheritDoc} */
426    @Override
427    @CheckReturnValue
428    public boolean storeConfig(File file) {
429        boolean result = true;
430        Element root = initStore();
431        if (!addConfigStore(root)) {
432            result = false;
433        }
434        includeHistory(root, file);
435        if (!finalStore(root, file)) {
436            result = false;
437        }
438        return result;
439    }
440
441    /** {@inheritDoc} */
442    @Override
443    @CheckReturnValue
444    public boolean storeUser(File file) {
445        boolean result = true;
446        Element root = initStore();
447        if (!addConfigStore(root)) {
448            result = false;
449        }
450        if (!addUserStore(root)) {
451            result = false;
452        }
453        includeHistory(root, file);
454        if (!finalStore(root, file)) {
455            result = false;
456        }
457        return result;
458    }
459
460    /** {@inheritDoc} */
461    @Override
462    public boolean makeBackup(File file) {
463        return makeBackupFile(FileUtil.getUserFilesPath() + "backupPanels", file);
464    }
465
466    /**
467     *
468     * @param o The object to get an XML representation of
469     * @return An XML element representing o
470     */
471    static public Element elementFromObject(Object o) {
472        return ConfigXmlManager.elementFromObject(o, true);
473    }
474
475    /**
476     *
477     * @param object The object to get an XML representation of
478     * @param shared true if the XML should be shared, false if the XML should
479     *               be per-node
480     * @return An XML element representing object
481     */
482    static public Element elementFromObject(Object object, boolean shared) {
483        String aName = adapterName(object);
484        log.debug("store using {}", aName);
485        XmlAdapter adapter = null;
486        try {
487            adapter = (XmlAdapter) Class.forName(adapterName(object)).getDeclaredConstructor().newInstance();
488        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException
489                    | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex) {
490            log.error("Cannot load configuration adapter for {}", object.getClass().getName(), ex);
491        }
492        if (adapter != null) {
493            return adapter.store(object, shared);
494        } else {
495            log.error("Cannot store configuration for {}", object.getClass().getName());
496            return null;
497        }
498    }
499
500    private void storeVersion(Element root) {
501        // add version at front
502        root.addContent(0,
503                new Element("jmriversion")
504                        .addContent(new Element("major").addContent("" + jmri.Version.major))
505                        .addContent(new Element("minor").addContent("" + jmri.Version.minor))
506                        .addContent(new Element("test").addContent("" + jmri.Version.test))
507                        .addContent(new Element("modifier").addContent(jmri.Version.getModifier()))
508        );
509    }
510
511    /**
512     * Load a file.
513     * <p>
514     * Handles problems locally to the extent that it can, by routing them to
515     * the creationErrorEncountered method.
516     *
517     * @param fi file to load
518     * @return true if no problems during the load
519     * @throws jmri.configurexml.JmriConfigureXmlException if unable to load
520     *                                                     file
521     */
522    @Override
523    public boolean load(File fi) throws JmriConfigureXmlException {
524        return load(fi, false);
525    }
526
527    /** {@inheritDoc} */
528    @Override
529    public boolean load(URL url) throws JmriConfigureXmlException {
530        return load(url, false);
531    }
532
533    /**
534     * Load a file.
535     * <p>
536     * Handles problems locally to the extent that it can, by routing them to
537     * the creationErrorEncountered method.
538     *
539     * @param fi               file to load
540     * @param registerDeferred true to register objects to defer
541     * @return true if no problems during the load
542     * @throws JmriConfigureXmlException if problem during load
543     * @see jmri.configurexml.XmlAdapter#loadDeferred()
544     * @since 2.11.2
545     */
546    @Override
547    public boolean load(File fi, boolean registerDeferred) throws JmriConfigureXmlException {
548        return this.load(FileUtil.fileToURL(fi), registerDeferred);
549    }
550
551    /**
552     * Load a file.
553     * <p>
554     * Handles problems locally to the extent that it can, by routing them to
555     * the creationErrorEncountered method.
556     * <p>
557     * Always processes on Swing thread
558     *
559     * @param url              URL of file to load
560     * @param registerDeferred true to register objects to defer
561     * @return true if no problems during the load
562     * @throws JmriConfigureXmlException if problem during load
563     * @see jmri.configurexml.XmlAdapter#loadDeferred()
564     * @since 3.3.2
565     */
566    @Override
567    public boolean load(URL url, boolean registerDeferred) throws JmriConfigureXmlException {
568        log.trace("starting load({}, {})", url, registerDeferred);
569
570        // we do the actual load on the Swing thread in case it changes visible windows
571        Boolean retval = jmri.util.ThreadingUtil.runOnGUIwithReturn(() -> {
572            try {
573                Boolean ret = loadOnSwingThread(url, registerDeferred);
574                return ret;
575            } catch (Exception e) {
576                log.trace("  ending load() via JmriConfigureXmlException");
577                throw new RuntimeException(e);
578            }
579        });
580
581        log.trace("  ending load({}, {} with {})", url, registerDeferred, retval);
582        return retval;
583    }
584
585    private XmlFile.Validate validate = XmlFile.Validate.CheckDtdThenSchema;
586
587    /** {@inheritDoc} */
588    @Override
589    public void setValidate(XmlFile.Validate v) {
590        validate = v;
591    }
592
593    /** {@inheritDoc} */
594    @Override
595    public XmlFile.Validate getValidate() {
596        return validate;
597    }
598
599    // must run on GUI thread only; that's ensured at the using level.
600    private Boolean loadOnSwingThread(URL url, boolean registerDeferred) throws JmriConfigureXmlException {
601        boolean result = true;
602        Element root = null;
603        /* We will put all the elements into a load list, along with the load order
604         As XML files prior to 2.13.1 had no order to the store, beans would be stored/loaded
605         before beans that they were dependant upon had been stored/loaded
606         */
607        Map<Element, Integer> loadlist = Collections.synchronizedMap(new LinkedHashMap<>());
608
609        try {
610            setValidate(validate);
611            root = super.rootFromURL(url);
612            // get the objects to load
613            List<Element> items = root.getChildren();
614            for (Element item : items) {
615                //Put things into an ordered list
616                Attribute a = item.getAttribute("class");
617                if (a == null) {
618                    // this is an element that we're not meant to read
619                    log.debug("skipping {}", item);
620                    continue;
621                }
622                String adapterName = a.getValue();
623                log.debug("attempt to get adapter {} for {}", adapterName, item);
624                adapterName = currentClassName(adapterName);
625                XmlAdapter adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance();
626                int order = adapter.loadOrder();
627                log.debug("add {} to load list with order id of {}", item, order);
628                loadlist.put(item, order);
629            }
630
631            List<Map.Entry<Element, Integer>> l = new ArrayList<>(loadlist.entrySet());
632            Collections.sort(l, (Map.Entry<Element, Integer> o1, Map.Entry<Element, Integer> o2) -> o1.getValue().compareTo(o2.getValue()));
633
634            for (Map.Entry<Element, Integer> elementIntegerEntry : l) {
635                Element item = elementIntegerEntry.getKey();
636                String adapterName = item.getAttribute("class").getValue();
637                adapterName = currentClassName(adapterName);
638                log.debug("load {} via {}", item, adapterName);
639                XmlAdapter adapter = null;
640                try {
641                    adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance();
642
643                    // get version info
644                    // loadVersion(root, adapter);
645                    // and do it
646                    if (adapter.loadDeferred() && registerDeferred) {
647                        // register in the list for deferred load
648                        loadDeferredList.add(item);
649                        log.debug("deferred load registered for {} {}", item, adapterName);
650                    } else {
651                        boolean loadStatus = adapter.load(item, item);
652                        log.debug("load status for {} {} is {}", item, adapterName, loadStatus);
653
654                        // if any adaptor load fails, then the entire load has failed
655                        if (!loadStatus) {
656                            result = false;
657                        }
658                    }
659                } catch (Exception e) {
660                    creationErrorEncountered(adapter, "load(" + url.getFile() + ")", "Unexpected error (Exception)", null, null, e);
661
662                    result = false;  // keep going, but return false to signal problem
663                } catch (Throwable et) {
664                    creationErrorEncountered(adapter, "in load(" + url.getFile() + ")", "Unexpected error (Throwable)", null, null, et);
665
666                    result = false;  // keep going, but return false to signal problem
667                }
668            }
669
670        } catch (java.io.FileNotFoundException e1) {
671            // this returns false to indicate un-success, but not enough
672            // of an error to require a message
673            creationErrorEncountered(null, "opening file " + url.getFile(),
674                    "File not found", null, null, e1);
675            result = false;
676        } catch (org.jdom2.JDOMException e) {
677            creationErrorEncountered(null, "parsing file " + url.getFile(),
678                    "Parse error", null, null, e);
679            result = false;
680        } catch (java.io.IOException e) {
681            creationErrorEncountered(null, "loading from file " + url.getFile(),
682                    "IOException", null, null, e);
683            result = false;
684        } catch (ClassNotFoundException e) {
685            creationErrorEncountered(null, "loading from file " + url.getFile(),
686                    "ClassNotFoundException", null, null, e);
687            result = false;
688        } catch (InstantiationException e) {
689            creationErrorEncountered(null, "loading from file " + url.getFile(),
690                    "InstantiationException", null, null, e);
691            result = false;
692        } catch (IllegalAccessException e) {
693            creationErrorEncountered(null, "loading from file " + url.getFile(),
694                    "IllegalAccessException", null, null, e);
695            result = false;
696        } catch (NoSuchMethodException e) {
697            creationErrorEncountered(null, "loading from file " + url.getFile(),
698                    "NoSuchMethodException", null, null, e);
699            result = false;
700        } catch (java.lang.reflect.InvocationTargetException e) {
701            creationErrorEncountered(null, "loading from file " + url.getFile(),
702                    "InvocationTargetException", null, null, e);
703            result = false;
704        } finally {
705            // no matter what, close error reporting
706            handler.done();
707        }
708
709        // loading complete, as far as it got, make history entry
710        FileHistory r = InstanceManager.getNullableDefault(FileHistory.class);
711        if (r != null) {
712            FileHistory included = null;
713            if (root != null) {
714                Element filehistory = root.getChild("filehistory");
715                if (filehistory != null) {
716                    included = jmri.jmrit.revhistory.configurexml.FileHistoryXml.loadFileHistory(filehistory);
717                }
718            }
719            String friendlyName = url.getFile().replaceAll("%20", " ");
720            r.addOperation((result ? "Load OK" : "Load with errors"), friendlyName, included);
721        } else {
722            log.info("Not recording file history");
723        }
724        return result;
725    }
726
727    /** {@inheritDoc} */
728    @Override
729    public boolean loadDeferred(File fi) {
730        return this.loadDeferred(FileUtil.fileToURL(fi));
731    }
732
733    /** {@inheritDoc} */
734    @Override
735    @CheckReturnValue
736    public boolean loadDeferred(URL url) {
737        boolean result = true;
738        // Now process the load-later list
739        log.debug("Start processing deferred load list (size): {}", loadDeferredList.size());
740        if (!loadDeferredList.isEmpty()) {
741            for (Element item : loadDeferredList) {
742                String adapterName = item.getAttribute("class").getValue();
743                log.debug("deferred load via {}", adapterName);
744                XmlAdapter adapter = null;
745                try {
746                    adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance();
747                    boolean loadStatus = adapter.load(item, item);
748                    log.debug("deferred load status for {} is {}", adapterName, loadStatus);
749
750                    // if any adaptor load fails, then the entire load has failed
751                    if (!loadStatus) {
752                        result = false;
753                    }
754                } catch (Exception e) {
755                    creationErrorEncountered(adapter, "deferred load(" + url.getFile() + ")",
756                            "Unexpected error (Exception)", null, null, e);
757                    result = false;  // keep going, but return false to signal problem
758                } catch (Throwable et) {
759                    creationErrorEncountered(adapter, "in deferred load(" + url.getFile() + ")",
760                            "Unexpected error (Throwable)", null, null, et);
761                    result = false;  // keep going, but return false to signal problem
762                }
763            }
764        }
765        log.debug("Done processing deferred load list with result: {}", result);
766        return result;
767    }
768
769    /**
770     * Find a file by looking
771     * <ul>
772     * <li> in xml/layout/ in the preferences directory, if that exists
773     * <li> in xml/layout/ in the application directory, if that exists
774     * <li> in xml/ in the preferences directory, if that exists
775     * <li> in xml/ in the application directory, if that exists
776     * <li> at top level in the application directory
777     * </ul>
778     *
779     * @param f Local filename, perhaps without path information
780     * @return Corresponding File object
781     */
782    @Override
783    public URL find(String f) {
784        URL u = FileUtil.findURL(f, "xml/layout", "xml"); // NOI18N
785        if (u == null) {
786            this.locateFileFailed(f);
787        }
788        return u;
789    }
790
791    /**
792     * Report a failure to find a file. This is a separate member to ease
793     * testing.
794     *
795     * @param f Name of file not located.
796     */
797    void locateFileFailed(String f) {
798        log.warn("Could not locate file {}", f);
799    }
800
801    /**
802     * Invoke common handling of errors that happen during the "load" process.
803     * <p>
804     * Exceptions passed into this are absorbed.
805     *
806     * @param adapter     Object that encountered the error (for reporting), may
807     *                    be null
808     * @param operation   description of the operation being attempted, may be
809     *                    null
810     * @param description description of error encountered
811     * @param systemName  System name of bean being handled, may be null
812     * @param userName    used name of the bean being handled, may be null
813     * @param exception   Any exception being handled in the processing, may be
814     *                    null
815     */
816    static public void creationErrorEncountered(
817            XmlAdapter adapter,
818            String operation,
819            String description,
820            String systemName,
821            String userName,
822            Throwable exception) {
823        // format and log a message (note reordered from arguments)
824//        System.out.format("creationErrorEncountered: %s%n", exception.getMessage());
825//        System.out.format("creationErrorEncountered: %s, %s, %s, %s, %s, %s%n", adapter, operation, description, systemName, userName, exception == null ? null : exception.getMessage());
826        ErrorMemo e = new ErrorMemo(
827                adapter, operation, description,
828                systemName, userName, exception, "loading");
829        if (adapter != null) {
830            ErrorHandler aeh = adapter.getExceptionHandler();
831            if (aeh != null) {
832                aeh.handle(e);
833            }
834        } else {
835            handler.handle(e);
836        }
837    }
838
839    /**
840     * Invoke common handling of errors that happen during the "store" process.
841     * <p>
842     * Exceptions passed into this are absorbed.
843     *
844     * @param adapter     Object that encountered the error (for reporting), may
845     *                    be null
846     * @param operation   description of the operation being attempted, may be
847     *                    null
848     * @param description description of error encountered
849     * @param systemName  System name of bean being handled, may be null
850     * @param userName    used name of the bean being handled, may be null
851     * @param exception   Any exception being handled in the processing, may be
852     *                    null
853     */
854    static public void storingErrorEncountered(
855            XmlAdapter adapter,
856            String operation,
857            String description,
858            String systemName,
859            String userName,
860            Throwable exception) {
861        // format and log a message (note reordered from arguments)
862        ErrorMemo e = new ErrorMemo(
863                adapter, operation, description,
864                systemName, userName, exception, "storing");
865        if (adapter != null) {
866            ErrorHandler aeh = adapter.getExceptionHandler();
867            if (aeh != null) {
868                aeh.handle(e);
869            }
870        } else {
871            handler.handle(e);
872        }
873    }
874
875    private static ErrorHandler handler = new ErrorHandler();
876
877    static public void setErrorHandler(ErrorHandler handler) {
878        ConfigXmlManager.handler = handler;
879    }
880
881    /**
882     * @return the loadDeferredList
883     */
884    protected List<Element> getLoadDeferredList() {
885        return loadDeferredList;
886    }
887
888    // initialize logging
889    private final static Logger log = LoggerFactory.getLogger(ConfigXmlManager.class);
890
891}