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