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}