001package jmri.util.prefs;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.FileOutputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.OutputStream;
009import java.nio.charset.StandardCharsets;
010
011import jmri.profile.AuxiliaryConfiguration;
012import jmri.util.FileUtil;
013import jmri.util.ThreadingUtil;
014import jmri.util.xml.XMLUtil;
015
016import org.w3c.dom.DOMException;
017import org.w3c.dom.Document;
018import org.w3c.dom.Element;
019import org.w3c.dom.Node;
020import org.w3c.dom.NodeList;
021import org.xml.sax.InputSource;
022import org.xml.sax.SAXException;
023
024/**
025 *
026 * @author Randall Wood
027 */
028public abstract class JmriConfiguration implements AuxiliaryConfiguration {
029
030    JmriConfiguration() {
031    }
032
033    protected abstract File getConfigurationFile(boolean shared);
034
035    protected abstract boolean isSharedBackedUp();
036
037    protected abstract void setSharedBackedUp(boolean backedUp);
038
039    protected abstract boolean isPrivateBackedUp();
040
041    protected abstract void setPrivateBackedUp(boolean backedUp);
042
043    File persistentFile;
044    Document persistentDocument; 
045    
046    Document getDocumentFromFile(File file) {
047        if (file != null && file.canRead()) {
048            try {
049                try (final InputStream is = new FileInputStream(file)) {
050                    InputSource input = new InputSource(is);
051                    input.setSystemId(file.toURI().toURL().toString());
052                    
053                    log.debug("Start read of user-interface.xml");
054                    
055                    var document = XMLUtil.parse(input, false, true, null, null);
056                    log.debug("End read of user-interface.xml");
057                    return document;
058                }
059            } catch (IOException | SAXException | IllegalArgumentException ex) {
060                log.warn("Cannot parse {}", file, ex);
061                return null;
062            }
063        }
064        return null;
065    }
066    
067    // migrate to new (2026) schema from older netbeans.org one upon rewriting profile.xml
068    void updateDocumentSchema(Element root) {
069
070        // remove previous namespace definition on the root element
071        root.getOwnerDocument().renameNode(root, null, root.getLocalName());
072
073        // manually adjust attributes
074        root.removeAttribute("xmlns:xsi");
075        root.removeAttributeNS(null, "xmlns");
076        root.removeAttribute("xmlns");
077        root.removeAttribute("xsi");
078        root.removeAttribute("xsi:noNamespaceSchemaLocation");
079        root.removeAttribute("noNamespaceSchemaLocation");
080        root.setAttribute("xmlns:xsi",
081                "http://www.w3.org/2001/XMLSchema-instance");
082        root.setAttribute("xsi:noNamespaceSchemaLocation",
083                "http://jmri.org/xml/schema/auxiliary-configuration.xsd");
084        root.setAttribute("xmlns",
085                "");
086    }
087    
088    @Override
089    public Element getConfigurationFragment(final String elementName, final String namespace, final boolean shared) {
090        return ThreadingUtil.runOnGUIwithReturn(() -> {
091            synchronized (this) {
092                File file = this.getConfigurationFile(shared);
093                if (persistentDocument == null || ! file.equals(persistentFile)) {
094                    log.debug("getting config for get from {}", file);
095                    persistentDocument = getDocumentFromFile(file);
096                    persistentFile = file;
097                }
098                if (persistentDocument == null) {
099                    return null;
100                }
101                Element root = persistentDocument.getDocumentElement();
102
103                updateDocumentSchema(root);
104
105                return XMLUtil.findElement(root, elementName, namespace);
106            }
107        });
108    }
109
110    @Override
111    public void putConfigurationFragment(final Element fragment, final boolean shared) throws IllegalArgumentException {
112        ThreadingUtil.runOnGUI(() -> {
113            synchronized (this) {
114                String elementName = fragment.getLocalName();
115                String namespace = fragment.getNamespaceURI();
116                if (namespace == null) {
117                    throw new IllegalArgumentException();
118                }
119                File file = this.getConfigurationFile(shared);
120                if (persistentDocument == null || ! file.equals(persistentFile) ) {
121                    log.debug("getting config for put from {}", file);
122                    persistentDocument = getDocumentFromFile(file);
123                    persistentFile = file;
124                }
125                if (persistentDocument == null) {
126                    persistentDocument = XMLUtil.createDocument("auxiliary-configuration", JmriConfigurationProvider.NAMESPACE, null, null); // NOI18N
127                }
128                Element root = persistentDocument.getDocumentElement();
129                Element oldFragment = XMLUtil.findElement(root, elementName, namespace);
130                if (oldFragment != null) {
131                    root.removeChild(oldFragment);
132                }
133                Node ref = null;
134                NodeList list = root.getChildNodes();
135                for (int i = 0; i < list.getLength(); i++) {
136                    Node node = list.item(i);
137                    if (node.getNodeType() != Node.ELEMENT_NODE) {
138                        continue;
139                    }
140                    int comparison = node.getNodeName().compareTo(elementName);
141                    if (comparison == 0) {
142                        comparison = node.getNamespaceURI().compareTo(namespace);
143                    }
144                    if (comparison > 0) {
145                        ref = node;
146                        break;
147                    }
148                }
149                root.insertBefore(root.getOwnerDocument().importNode(fragment, true), ref);
150                
151                updateDocumentSchema(root);
152                
153                try {
154                    this.backup(shared);
155                    try (final OutputStream os = new FileOutputStream(file)) {
156                        log.debug("Start write of user-interface.xml");
157                        try {
158                            XMLUtil.write(persistentDocument, os, StandardCharsets.UTF_8.name());
159                        } catch (IOException ex) {
160                            log.error("Cannot write {}", file, ex);
161                        }
162                        log.debug("End write of user-interface.xml");
163                        os.flush();
164                    }
165                } catch (IOException ex) {
166                    log.error("Cannot write {}", file, ex);
167                }
168            }
169        });
170    }
171
172    @Override
173    public boolean removeConfigurationFragment(final String elementName, final String namespace, final boolean shared) throws IllegalArgumentException {
174        return ThreadingUtil.runOnGUIwithReturn(() -> {
175            synchronized (this) {
176                File file = this.getConfigurationFile(shared);
177                if (file.canWrite()) {
178                    try {
179                        if (persistentDocument == null || ! file.equals(persistentFile)) {
180                            log.debug("getting config for remove from {}", file);
181                            persistentDocument = getDocumentFromFile(file);
182                            persistentFile = file;
183                        }
184                        Element root = persistentDocument.getDocumentElement();
185                        Element toRemove = XMLUtil.findElement(root, elementName, namespace);
186                        if (toRemove != null) {
187                            root.removeChild(toRemove);
188                            this.backup(shared);
189
190                            updateDocumentSchema(root);
191                
192                            if (root.getElementsByTagName("*").getLength() > 0) {
193                                // NOI18N
194                                try (final OutputStream os = new FileOutputStream(file)) {
195                                    log.debug("Start write of user-interface.xml");
196                                    XMLUtil.write(persistentDocument, os, StandardCharsets.UTF_8.name());
197                                    log.debug("End write of user-interface.xml");
198                                }
199                            } else if (!file.delete()) {
200                                log.warn("Unable to delete {}", file);
201                            }
202                            return true;
203                        }
204                    } catch (IOException | DOMException ex) {
205                        log.error("Cannot remove {} from {}", elementName, file, ex);
206                    }
207                }
208                return false;
209            }
210        });
211    }
212
213    private void backup(boolean shared) {
214        final File file = this.getConfigurationFile(shared);
215        if (!(shared ? this.isSharedBackedUp() : this.isPrivateBackedUp()) && file.exists()) {
216            log.debug("Backing up {}", file);
217            try {
218                FileUtil.backup(file);
219                if (shared) {
220                    this.setSharedBackedUp(true);
221                } else {
222                    this.setPrivateBackedUp(true);
223                }
224            } catch (IOException ex) {
225                log.error("Error backing up {}", file, ex);
226            }
227        }
228    }
229
230    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriConfiguration.class);
231}