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 = "-4-19-2";
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) {
331        // add history to end of document
332        if (InstanceManager.getNullableDefault(FileHistory.class) != null) {
333            root.addContent(jmri.jmrit.revhistory.configurexml.FileHistoryXml.storeDirectly(InstanceManager.getDefault(FileHistory.class)));
334        }
335    }
336
337    protected boolean finalStore(Element root, File file) {
338        try {
339            // Document doc = newDocument(root, dtdLocation+"layout-config-"+dtdVersion+".dtd");
340            Document doc = newDocument(root);
341
342            // add XSLT processing instruction
343            // <?xml-stylesheet type="text/xsl" href="XSLT/panelfile"+schemaVersion+".xsl"?>
344            java.util.Map<String, String> m = new java.util.HashMap<>();
345            m.put("type", "text/xsl");
346            m.put("href", xsltLocation + "panelfile" + schemaVersion + ".xsl");
347            ProcessingInstruction p = new ProcessingInstruction("xml-stylesheet", m);
348            doc.addContent(0, p);
349
350            // add version at front
351            storeVersion(root);
352
353            writeXML(file, doc);
354        } catch (java.io.FileNotFoundException ex3) {
355            storingErrorEncountered(null, "storing to file " + file.getName(),
356                    "File not found " + file.getName(), null, null, ex3);
357            log.error("FileNotFound error writing file: {}", ex3.getLocalizedMessage());
358            return false;
359        } catch (java.io.IOException ex2) {
360            storingErrorEncountered(null, "storing to file " + file.getName(),
361                    "IO error writing file " + file.getName(), null, null, ex2);
362            log.error("IO error writing file: {}", ex2.getLocalizedMessage());
363            return false;
364        }
365        return true;
366    }
367
368    /** {@inheritDoc} */
369    @Override
370    public void storePrefs() {
371        storePrefs(prefsFile);
372    }
373
374    /** {@inheritDoc} */
375    @Override
376    public void storePrefs(File file) {
377        synchronized (this) {
378            Element root = initStore();
379            addPrefsStore(root);
380            finalStore(root, file);
381        }
382    }
383
384    /** {@inheritDoc} */
385    @Override
386    public void storeUserPrefs(File file) {
387        synchronized (this) {
388            Element root = initStore();
389            addUserPrefsStore(root);
390            finalStore(root, file);
391        }
392    }
393
394    /**
395     * Set location for preferences file.
396     * <p>
397     * File need not exist, but location must be writable when storePrefs()
398     * called.
399     *
400     * @param prefsFile new location for preferences file
401     */
402    public void setPrefsLocation(File prefsFile) {
403        this.prefsFile = prefsFile;
404    }
405    File prefsFile;
406
407    /** {@inheritDoc} */
408    @Override
409    public boolean storeConfig(File file) {
410        boolean result = true;
411        Element root = initStore();
412        if (!addConfigStore(root)) {
413            result = false;
414        }
415        includeHistory(root);
416        if (!finalStore(root, file)) {
417            result = false;
418        }
419        return result;
420    }
421
422    /** {@inheritDoc} */
423    @Override
424    public boolean storeUser(File file) {
425        boolean result = true;
426        Element root = initStore();
427        if (!addConfigStore(root)) {
428            result = false;
429        }
430        if (!addUserStore(root)) {
431            result = false;
432        }
433        includeHistory(root);
434        if (!finalStore(root, file)) {
435            result = false;
436        }
437        return result;
438    }
439
440    /** {@inheritDoc} */
441    @Override
442    public boolean makeBackup(File file) {
443        return makeBackupFile(defaultBackupDirectory, file);
444    }
445
446    String defaultBackupDirectory = FileUtil.getUserFilesPath() + "backupPanels";
447
448    /**
449     *
450     * @param o The object to get an XML representation of
451     * @return An XML element representing o
452     */
453    static public Element elementFromObject(Object o) {
454        return ConfigXmlManager.elementFromObject(o, true);
455    }
456
457    /**
458     *
459     * @param object The object to get an XML representation of
460     * @param shared true if the XML should be shared, false if the XML should
461     *               be per-node
462     * @return An XML element representing object
463     */
464    static public Element elementFromObject(Object object, boolean shared) {
465        String aName = adapterName(object);
466        log.debug("store using {}", aName);
467        XmlAdapter adapter = null;
468        try {
469            adapter = (XmlAdapter) Class.forName(adapterName(object)).getDeclaredConstructor().newInstance();
470        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException
471                    | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex) {
472            log.error("Cannot load configuration adapter for {}", object.getClass().getName(), ex);
473        }
474        if (adapter != null) {
475            return adapter.store(object, shared);
476        } else {
477            log.error("Cannot store configuration for {}", object.getClass().getName());
478            return null;
479        }
480    }
481
482    private void storeVersion(Element root) {
483        // add version at front
484        root.addContent(0,
485                new Element("jmriversion")
486                        .addContent(new Element("major").addContent("" + jmri.Version.major))
487                        .addContent(new Element("minor").addContent("" + jmri.Version.minor))
488                        .addContent(new Element("test").addContent("" + jmri.Version.test))
489                        .addContent(new Element("modifier").addContent(jmri.Version.getModifier()))
490        );
491    }
492
493    /**
494     * Load a file.
495     * <p>
496     * Handles problems locally to the extent that it can, by routing them to
497     * the creationErrorEncountered method.
498     *
499     * @param fi file to load
500     * @return true if no problems during the load
501     * @throws jmri.configurexml.JmriConfigureXmlException if unable to load
502     *                                                     file
503     */
504    @Override
505    public boolean load(File fi) throws JmriConfigureXmlException {
506        return load(fi, false);
507    }
508
509    /** {@inheritDoc} */
510    @Override
511    public boolean load(URL url) throws JmriConfigureXmlException {
512        return load(url, false);
513    }
514
515    /**
516     * Load a file.
517     * <p>
518     * Handles problems locally to the extent that it can, by routing them to
519     * the creationErrorEncountered method.
520     *
521     * @param fi               file to load
522     * @param registerDeferred true to register objects to defer
523     * @return true if no problems during the load
524     * @throws JmriConfigureXmlException if problem during load
525     * @see jmri.configurexml.XmlAdapter#loadDeferred()
526     * @since 2.11.2
527     */
528    @Override
529    public boolean load(File fi, boolean registerDeferred) throws JmriConfigureXmlException {
530        return this.load(FileUtil.fileToURL(fi), registerDeferred);
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     * <p>
539     * Always processes on Swing thread
540     *
541     * @param url              URL of file to load
542     * @param registerDeferred true to register objects to defer
543     * @return true if no problems during the load
544     * @throws JmriConfigureXmlException if problem during load
545     * @see jmri.configurexml.XmlAdapter#loadDeferred()
546     * @since 3.3.2
547     */
548    @Override
549    public boolean load(URL url, boolean registerDeferred) throws JmriConfigureXmlException {
550        log.trace("starting load({}, {})", url, registerDeferred);
551
552        // we do the actual load on the Swing thread in case it changes visible windows
553        Boolean retval = jmri.util.ThreadingUtil.runOnGUIwithReturn(() -> {
554            try {
555                Boolean ret = loadOnSwingThread(url, registerDeferred);
556                return ret;
557            } catch (Exception e) {
558                log.trace("  ending load() via JmriConfigureXmlException");
559                throw new RuntimeException(e);
560            }
561        });
562
563        log.trace("  ending load({}, {} with {})", url, registerDeferred, retval);
564        return retval;
565    }
566
567    private XmlFile.Validate validate = XmlFile.Validate.CheckDtdThenSchema;
568
569    /** {@inheritDoc} */
570    @Override
571    public void setValidate(XmlFile.Validate v) {
572        validate = v;
573    }
574
575    /** {@inheritDoc} */
576    @Override
577    public XmlFile.Validate getValidate() {
578        return validate;
579    }
580
581    // must run on GUI thread only; that's ensured at the using level.
582    private Boolean loadOnSwingThread(URL url, boolean registerDeferred) throws JmriConfigureXmlException {
583        boolean result = true;
584        Element root = null;
585        /* We will put all the elements into a load list, along with the load order
586         As XML files prior to 2.13.1 had no order to the store, beans would be stored/loaded
587         before beans that they were dependant upon had been stored/loaded
588         */
589        Map<Element, Integer> loadlist = Collections.synchronizedMap(new LinkedHashMap<>());
590
591        try {
592            setValidate(validate);
593            root = super.rootFromURL(url);
594            // get the objects to load
595            List<Element> items = root.getChildren();
596            for (Element item : items) {
597                //Put things into an ordered list
598                Attribute a = item.getAttribute("class");
599                if (a == null) {
600                    // this is an element that we're not meant to read
601                    log.debug("skipping {}", item);
602                    continue;
603                }
604                String adapterName = a.getValue();
605                log.debug("attempt to get adapter {} for {}", adapterName, item);
606                adapterName = currentClassName(adapterName);
607                XmlAdapter adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance();
608                int order = adapter.loadOrder();
609                log.debug("add {} to load list with order id of {}", item, order);
610                loadlist.put(item, order);
611            }
612
613            List<Map.Entry<Element, Integer>> l = new ArrayList<>(loadlist.entrySet());
614            Collections.sort(l, (Map.Entry<Element, Integer> o1, Map.Entry<Element, Integer> o2) -> o1.getValue().compareTo(o2.getValue()));
615
616            for (Map.Entry<Element, Integer> elementIntegerEntry : l) {
617                Element item = elementIntegerEntry.getKey();
618                String adapterName = item.getAttribute("class").getValue();
619                adapterName = currentClassName(adapterName);
620                log.debug("load {} via {}", item, adapterName);
621                XmlAdapter adapter = null;
622                try {
623                    adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance();
624
625                    // get version info
626                    // loadVersion(root, adapter);
627                    // and do it
628                    if (adapter.loadDeferred() && registerDeferred) {
629                        // register in the list for deferred load
630                        loadDeferredList.add(item);
631                        log.debug("deferred load registered for {} {}", item, adapterName);
632                    } else {
633                        boolean loadStatus = adapter.load(item, item);
634                        log.debug("load status for {} {} is {}", item, adapterName, loadStatus);
635
636                        // if any adaptor load fails, then the entire load has failed
637                        if (!loadStatus) {
638                            result = false;
639                        }
640                    }
641                } catch (Exception e) {
642                    creationErrorEncountered(adapter, "load(" + url.getFile() + ")", "Unexpected error (Exception)", null, null, e);
643
644                    result = false;  // keep going, but return false to signal problem
645                } catch (Throwable et) {
646                    creationErrorEncountered(adapter, "in load(" + url.getFile() + ")", "Unexpected error (Throwable)", null, null, et);
647
648                    result = false;  // keep going, but return false to signal problem
649                }
650            }
651
652        } catch (java.io.FileNotFoundException e1) {
653            // this returns false to indicate un-success, but not enough
654            // of an error to require a message
655            creationErrorEncountered(null, "opening file " + url.getFile(),
656                    "File not found", null, null, e1);
657            result = false;
658        } catch (org.jdom2.JDOMException e) {
659            creationErrorEncountered(null, "parsing file " + url.getFile(),
660                    "Parse error", null, null, e);
661            result = false;
662        } catch (java.io.IOException e) {
663            creationErrorEncountered(null, "loading from file " + url.getFile(),
664                    "IOException", null, null, e);
665            result = false;
666        } catch (ClassNotFoundException e) {
667            creationErrorEncountered(null, "loading from file " + url.getFile(),
668                    "ClassNotFoundException", null, null, e);
669            result = false;
670        } catch (InstantiationException e) {
671            creationErrorEncountered(null, "loading from file " + url.getFile(),
672                    "InstantiationException", null, null, e);
673            result = false;
674        } catch (IllegalAccessException e) {
675            creationErrorEncountered(null, "loading from file " + url.getFile(),
676                    "IllegalAccessException", null, null, e);
677            result = false;
678        } catch (NoSuchMethodException e) {
679            creationErrorEncountered(null, "loading from file " + url.getFile(),
680                    "NoSuchMethodException", null, null, e);
681            result = false;
682        } catch (java.lang.reflect.InvocationTargetException e) {
683            creationErrorEncountered(null, "loading from file " + url.getFile(),
684                    "InvocationTargetException", null, null, e);
685            result = false;
686        } finally {
687            // no matter what, close error reporting
688            handler.done();
689        }
690
691        // loading complete, as far as it got, make history entry
692        FileHistory r = InstanceManager.getNullableDefault(FileHistory.class);
693        if (r != null) {
694            FileHistory included = null;
695            if (root != null) {
696                Element filehistory = root.getChild("filehistory");
697                if (filehistory != null) {
698                    included = jmri.jmrit.revhistory.configurexml.FileHistoryXml.loadFileHistory(filehistory);
699                }
700            }
701            r.addOperation((result ? "Load OK" : "Load with errors"), url.getFile(), included);
702        } else {
703            log.info("Not recording file history");
704        }
705        return result;
706    }
707
708    /** {@inheritDoc} */
709    @Override
710    public boolean loadDeferred(File fi) {
711        return this.loadDeferred(FileUtil.fileToURL(fi));
712    }
713
714    /** {@inheritDoc} */
715    @Override
716    public boolean loadDeferred(URL url) {
717        boolean result = true;
718        // Now process the load-later list
719        log.debug("Start processing deferred load list (size): {}", loadDeferredList.size());
720        if (!loadDeferredList.isEmpty()) {
721            for (Element item : loadDeferredList) {
722                String adapterName = item.getAttribute("class").getValue();
723                log.debug("deferred load via {}", adapterName);
724                XmlAdapter adapter = null;
725                try {
726                    adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance();
727                    boolean loadStatus = adapter.load(item, item);
728                    log.debug("deferred load status for {} is {}", adapterName, loadStatus);
729
730                    // if any adaptor load fails, then the entire load has failed
731                    if (!loadStatus) {
732                        result = false;
733                    }
734                } catch (Exception e) {
735                    creationErrorEncountered(adapter, "deferred load(" + url.getFile() + ")",
736                            "Unexpected error (Exception)", null, null, e);
737                    result = false;  // keep going, but return false to signal problem
738                } catch (Throwable et) {
739                    creationErrorEncountered(adapter, "in deferred load(" + url.getFile() + ")",
740                            "Unexpected error (Throwable)", null, null, et);
741                    result = false;  // keep going, but return false to signal problem
742                }
743            }
744        }
745        log.debug("Done processing deferred load list with result: {}", result);
746        return result;
747    }
748
749    /**
750     * Find a file by looking
751     * <ul>
752     * <li> in xml/layout/ in the preferences directory, if that exists
753     * <li> in xml/layout/ in the application directory, if that exists
754     * <li> in xml/ in the preferences directory, if that exists
755     * <li> in xml/ in the application directory, if that exists
756     * <li> at top level in the application directory
757     * </ul>
758     *
759     * @param f Local filename, perhaps without path information
760     * @return Corresponding File object
761     */
762    @Override
763    public URL find(String f) {
764        URL u = FileUtil.findURL(f, "xml/layout", "xml"); // NOI18N
765        if (u == null) {
766            this.locateFileFailed(f);
767        }
768        return u;
769    }
770
771    /**
772     * Report a failure to find a file. This is a separate member to ease
773     * testing.
774     *
775     * @param f Name of file not located.
776     */
777    void locateFileFailed(String f) {
778        log.warn("Could not locate file {}", f);
779    }
780
781    /**
782     * Invoke common handling of errors that happen during the "load" process.
783     * <p>
784     * Exceptions passed into this are absorbed.
785     *
786     * @param adapter     Object that encountered the error (for reporting), may
787     *                    be null
788     * @param operation   description of the operation being attempted, may be
789     *                    null
790     * @param description description of error encountered
791     * @param systemName  System name of bean being handled, may be null
792     * @param userName    used name of the bean being handled, may be null
793     * @param exception   Any exception being handled in the processing, may be
794     *                    null
795     */
796    static public void creationErrorEncountered(
797            XmlAdapter adapter,
798            String operation,
799            String description,
800            String systemName,
801            String userName,
802            Throwable exception) {
803        // format and log a message (note reordered from arguments)
804//        System.out.format("creationErrorEncountered: %s%n", exception.getMessage());
805//        System.out.format("creationErrorEncountered: %s, %s, %s, %s, %s, %s%n", adapter, operation, description, systemName, userName, exception == null ? null : exception.getMessage());
806        ErrorMemo e = new ErrorMemo(
807                adapter, operation, description,
808                systemName, userName, exception, "loading");
809        if (adapter != null) {
810            ErrorHandler aeh = adapter.getExceptionHandler();
811            if (aeh != null) {
812                aeh.handle(e);
813            }
814        } else {
815            handler.handle(e);
816        }
817    }
818
819    /**
820     * Invoke common handling of errors that happen during the "store" process.
821     * <p>
822     * Exceptions passed into this are absorbed.
823     *
824     * @param adapter     Object that encountered the error (for reporting), may
825     *                    be null
826     * @param operation   description of the operation being attempted, may be
827     *                    null
828     * @param description description of error encountered
829     * @param systemName  System name of bean being handled, may be null
830     * @param userName    used name of the bean being handled, may be null
831     * @param exception   Any exception being handled in the processing, may be
832     *                    null
833     */
834    static public void storingErrorEncountered(
835            XmlAdapter adapter,
836            String operation,
837            String description,
838            String systemName,
839            String userName,
840            Throwable exception) {
841        // format and log a message (note reordered from arguments)
842        ErrorMemo e = new ErrorMemo(
843                adapter, operation, description,
844                systemName, userName, exception, "storing");
845        if (adapter != null) {
846            ErrorHandler aeh = adapter.getExceptionHandler();
847            if (aeh != null) {
848                aeh.handle(e);
849            }
850        } else {
851            handler.handle(e);
852        }
853    }
854
855    private static ErrorHandler handler = new ErrorHandler();
856
857    static public void setErrorHandler(ErrorHandler handler) {
858        ConfigXmlManager.handler = handler;
859    }
860
861    /**
862     * @return the loadDeferredList
863     */
864    protected List<Element> getLoadDeferredList() {
865        return loadDeferredList;
866    }
867
868    // initialize logging
869    private final static Logger log = LoggerFactory.getLogger(ConfigXmlManager.class);
870
871}