| 1 | |
package org.webslinger.commons.vfs.handlers.cow; |
| 2 | |
|
| 3 | |
import java.io.InputStream; |
| 4 | |
import java.io.IOException; |
| 5 | |
import java.io.OutputStream; |
| 6 | |
import java.util.ArrayList; |
| 7 | |
import java.util.Arrays; |
| 8 | |
import java.util.Collection; |
| 9 | |
import java.util.HashMap; |
| 10 | |
import java.util.HashSet; |
| 11 | |
import java.util.LinkedHashMap; |
| 12 | |
import java.util.Map; |
| 13 | |
import java.util.Queue; |
| 14 | |
import java.util.concurrent.ConcurrentHashMap; |
| 15 | |
import java.util.concurrent.atomic.AtomicReference; |
| 16 | |
import javax.xml.parsers.DocumentBuilder; |
| 17 | |
import javax.xml.parsers.DocumentBuilderFactory; |
| 18 | |
import javax.xml.parsers.ParserConfigurationException; |
| 19 | |
import javax.xml.transform.OutputKeys; |
| 20 | |
import javax.xml.transform.Transformer; |
| 21 | |
import javax.xml.transform.TransformerFactory; |
| 22 | |
import javax.xml.transform.dom.DOMSource; |
| 23 | |
import javax.xml.transform.stream.StreamResult; |
| 24 | |
|
| 25 | |
import org.w3c.dom.Document; |
| 26 | |
import org.w3c.dom.Element; |
| 27 | |
import org.w3c.dom.Node; |
| 28 | |
import org.xml.sax.SAXException; |
| 29 | |
import org.xml.sax.SAXParseException; |
| 30 | |
|
| 31 | |
import org.apache.commons.vfs.FileName; |
| 32 | |
import org.apache.commons.vfs.FileObject; |
| 33 | |
import org.apache.commons.vfs.FileSystem; |
| 34 | |
import org.apache.commons.vfs.FileSystemException; |
| 35 | |
|
| 36 | |
import org.webslinger.commons.vfs.VFSUtil; |
| 37 | |
import org.webslinger.lang.ConcurrentCache; |
| 38 | |
import org.webslinger.util.GeneratedResult; |
| 39 | |
import org.webslinger.xml.XmlUtil; |
| 40 | |
|
| 41 | |
public class COWStateXml { |
| 42 | |
private static final Queue<DocumentBuilder> xmlBuilders; |
| 43 | |
|
| 44 | |
static { |
| 45 | 1 | DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); |
| 46 | 1 | builderFactory.setCoalescing(true); |
| 47 | 1 | builderFactory.setExpandEntityReferences(true); |
| 48 | 1 | builderFactory.setIgnoringComments(true); |
| 49 | 1 | builderFactory.setIgnoringElementContentWhitespace(true); |
| 50 | 1 | builderFactory.setNamespaceAware(false); |
| 51 | 1 | builderFactory.setValidating(false); |
| 52 | |
|
| 53 | |
try { |
| 54 | 1 | xmlBuilders = XmlUtil.createPool(builderFactory, 20); |
| 55 | 0 | } catch (RuntimeException e) { |
| 56 | 0 | throw e; |
| 57 | 0 | } catch (Exception e) { |
| 58 | 0 | throw (InternalError) new InternalError("loading xmlBuilders").initCause(e); |
| 59 | 1 | } |
| 60 | 1 | } |
| 61 | |
|
| 62 | |
private final FileSystem fileSystem; |
| 63 | |
private final FileName fileName; |
| 64 | |
private Document doc; |
| 65 | 72 | private final AtomicReference<State> state = new AtomicReference<State>(); |
| 66 | |
|
| 67 | 72 | public COWStateXml(FileObject file) throws IOException { |
| 68 | 72 | this.fileName = file.getName(); |
| 69 | 72 | this.fileSystem = file.getFileSystem(); |
| 70 | 72 | state.set(load(file)); |
| 71 | 72 | } |
| 72 | |
|
| 73 | |
protected static HashMap<String, Entry> parse(Document doc) { |
| 74 | 249 | HashMap<String, Entry> entries = new HashMap<String, Entry>(); |
| 75 | 249 | Node node = doc.getDocumentElement().getFirstChild(); |
| 76 | 640 | while (node != null) { |
| 77 | 391 | if (node.getNodeType() == Node.ELEMENT_NODE && "item".equals(node.getNodeName())) { |
| 78 | 197 | Element entryElement = (Element) node; |
| 79 | 197 | boolean isDeleted = "true".equals(entryElement.getAttribute("deleted")); |
| 80 | 197 | LinkedHashMap<String, Element> bases = new LinkedHashMap<String, Element>(); |
| 81 | 197 | Node baseNode = node.getFirstChild(); |
| 82 | 541 | while (baseNode != null) { |
| 83 | 344 | if (baseNode.getNodeType() == Node.ELEMENT_NODE && "base".equals(baseNode.getNodeName())) { |
| 84 | 168 | Element baseElement = (Element) baseNode; |
| 85 | 168 | String base = baseElement.getAttribute("target"); |
| 86 | 168 | Element oldElement = bases.put(base, baseElement); |
| 87 | 168 | if (oldElement != null) entryElement.removeChild(oldElement); |
| 88 | |
} |
| 89 | 344 | baseNode = baseNode.getNextSibling(); |
| 90 | |
} |
| 91 | 197 | Entry newEntry = new Entry(entryElement, bases, isDeleted); |
| 92 | 197 | Entry oldEntry = entries.put(newEntry.element.getAttribute("name"), newEntry); |
| 93 | 197 | if (oldEntry != null) node.getParentNode().removeChild(oldEntry.element); |
| 94 | |
} |
| 95 | 391 | node = node.getNextSibling(); |
| 96 | |
} |
| 97 | 249 | return entries; |
| 98 | |
} |
| 99 | |
|
| 100 | |
protected static State load(FileObject file) throws FileSystemException { |
| 101 | |
long lastModified; |
| 102 | |
HashMap<String, Entry> entries; |
| 103 | |
Document doc; |
| 104 | 72 | DocumentBuilder documentBuilder = xmlBuilders.poll(); |
| 105 | |
try { |
| 106 | 72 | if (!file.exists()) { |
| 107 | 17 | doc = documentBuilder.newDocument(); |
| 108 | 17 | doc.appendChild(doc.createElement("state")); |
| 109 | 17 | lastModified = -1; |
| 110 | 17 | entries = new HashMap<String, Entry>(); |
| 111 | |
} else { |
| 112 | 55 | InputStream in = file.getContent().getInputStream(); |
| 113 | 55 | lastModified = file.getContent().getLastModifiedTime(); |
| 114 | 55 | doc = documentBuilder.parse(in); |
| 115 | 55 | entries = parse(doc); |
| 116 | 55 | in.close(); |
| 117 | |
} |
| 118 | 0 | } catch (Exception e) { |
| 119 | 0 | throw VFSUtil.makeFileSystemException(e); |
| 120 | |
} finally { |
| 121 | 72 | xmlBuilders.offer(documentBuilder); |
| 122 | 72 | } |
| 123 | 72 | return new State(doc, entries, lastModified); |
| 124 | |
} |
| 125 | |
|
| 126 | |
protected static void save(FileObject file, State state) throws FileSystemException { |
| 127 | 194 | DocumentBuilder documentBuilder = xmlBuilders.element(); |
| 128 | |
try { |
| 129 | 194 | if (!state.entries.isEmpty()) { |
| 130 | 136 | OutputStream out = file.getContent().getOutputStream(); |
| 131 | 136 | DOMSource source = new DOMSource(state.doc); |
| 132 | 136 | StreamResult result = new StreamResult(out); |
| 133 | 136 | TransformerFactory factory = TransformerFactory.newInstance(); |
| 134 | 136 | Transformer transformer = factory.newTransformer(); |
| 135 | 136 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); |
| 136 | 136 | transformer.transform(source, result); |
| 137 | 136 | out.close(); |
| 138 | 136 | } else { |
| 139 | 58 | file.delete(); |
| 140 | |
} |
| 141 | 0 | } catch (Exception e) { |
| 142 | 0 | throw VFSUtil.makeFileSystemException(e); |
| 143 | |
} finally { |
| 144 | 194 | xmlBuilders.offer(documentBuilder); |
| 145 | 194 | } |
| 146 | 194 | } |
| 147 | |
|
| 148 | |
protected FileObject getFile() throws FileSystemException { |
| 149 | 194 | return fileSystem.resolveFile(fileName); |
| 150 | |
} |
| 151 | |
|
| 152 | |
public void removeDeletedChildren(Collection<String> names) throws FileSystemException { |
| 153 | 62 | state.get().removeDeletedChildren(names); |
| 154 | 62 | } |
| 155 | |
|
| 156 | |
public Collection<String> loadData(String name, boolean[] isDeleted) throws FileSystemException { |
| 157 | 682 | return state.get().loadData(name, isDeleted); |
| 158 | |
} |
| 159 | |
|
| 160 | |
public boolean submitRequests(String name, COWStorageHandler.Request... requests) throws FileSystemException { |
| 161 | 194 | boolean[] saved = new boolean[1]; |
| 162 | |
State oldState, newState; |
| 163 | |
do { |
| 164 | 194 | oldState = state.get(); |
| 165 | 194 | newState = oldState.submitRequests(name, requests); |
| 166 | 194 | if (oldState == newState) return false; |
| 167 | 194 | if (newState == null) { |
| 168 | 58 | Document doc = (Document) oldState.doc.cloneNode(false); |
| 169 | 58 | doc.appendChild(doc.createElement("state")); |
| 170 | 58 | newState = new State(doc, new HashMap<String, Entry>(), -1); |
| 171 | |
} |
| 172 | 194 | } while (!state.compareAndSet(oldState, newState)); |
| 173 | 194 | save(getFile(), newState); |
| 174 | 194 | return true; |
| 175 | |
} |
| 176 | |
|
| 177 | |
public String toString() { |
| 178 | 0 | return "COWStateXml(" + fileName.toString() + ")"; |
| 179 | |
} |
| 180 | |
|
| 181 | |
protected final static class State { |
| 182 | |
protected final Document doc; |
| 183 | |
protected final HashMap<String, Entry> entries; |
| 184 | |
protected final long lastModified; |
| 185 | |
|
| 186 | 324 | protected State(Document doc, HashMap<String, Entry> entries, long lastModified) { |
| 187 | 324 | this.doc = doc; |
| 188 | 324 | this.entries = entries; |
| 189 | 324 | this.lastModified = lastModified; |
| 190 | 324 | } |
| 191 | |
|
| 192 | |
private State copy(boolean doCopy) { |
| 193 | 379 | if (!doCopy) return this; |
| 194 | 194 | Document newDoc = (Document) doc.cloneNode(true); |
| 195 | 194 | State newState = new State(newDoc, parse(newDoc), lastModified); |
| 196 | 194 | return newState; |
| 197 | |
} |
| 198 | |
|
| 199 | |
private State checkEmpty(String name) { |
| 200 | 67 | Entry entry = entries.get(name); |
| 201 | 67 | if (entry != null && !entry.isDeleted && entry.bases.isEmpty()) { |
| 202 | 14 | entries.remove(name); |
| 203 | 14 | entry.element.getParentNode().removeChild(entry.element); |
| 204 | |
} |
| 205 | 67 | return entries.isEmpty() ? null : this; |
| 206 | |
} |
| 207 | |
|
| 208 | |
private Entry newEntry(String name, LinkedHashMap<String, Element> bases, boolean isDeleted) { |
| 209 | 138 | if (".cowstate.xml".equals(name)) new Exception().printStackTrace(); |
| 210 | 138 | Element element = doc.createElement("item"); |
| 211 | 138 | doc.getDocumentElement().appendChild(element); |
| 212 | 138 | Entry entry = new Entry(element, bases, isDeleted); |
| 213 | 138 | element.setAttribute("name", name); |
| 214 | 138 | Entry oldEntry = entries.put(name, entry); |
| 215 | 138 | if (oldEntry != null) oldEntry.element.getParentNode().removeChild(oldEntry.element); |
| 216 | 138 | return entry; |
| 217 | |
} |
| 218 | |
|
| 219 | |
protected State delete(String name, boolean doCopy) { |
| 220 | 50 | if (!entries.containsKey(name)) return this; |
| 221 | 41 | State newState = copy(doCopy); |
| 222 | 41 | Entry entry = newState.entries.remove(name); |
| 223 | 41 | entry.element.getParentNode().removeChild(entry.element); |
| 224 | 41 | return newState.checkEmpty(name); |
| 225 | |
} |
| 226 | |
|
| 227 | |
protected State setDeleted(String name, boolean isDeleted, boolean doCopy) { |
| 228 | 20 | Entry entry = entries.get(name); |
| 229 | 20 | if (entry != null) { |
| 230 | 11 | if (entry.isDeleted == isDeleted) return this; |
| 231 | 9 | } else if (!isDeleted) { |
| 232 | 0 | return this; |
| 233 | |
} |
| 234 | 20 | State newState = copy(doCopy); |
| 235 | 20 | entry = newState.newEntry(name, entry == null ? new LinkedHashMap<String, Element>() : newState.entries.get(name).bases, isDeleted); |
| 236 | 20 | if (isDeleted) { |
| 237 | 12 | entry.element.setAttribute("deleted", "true"); |
| 238 | |
} else { |
| 239 | 8 | entry.element.removeAttribute("deleted"); |
| 240 | |
} |
| 241 | 20 | return newState.checkEmpty(name); |
| 242 | |
} |
| 243 | |
|
| 244 | |
protected State addBase(String name, String relative, boolean doCopy) { |
| 245 | 118 | Entry entry = entries.get(name); |
| 246 | 118 | if (entry != null && entry.bases.containsKey(relative)) return this; |
| 247 | 118 | State newState = copy(doCopy); |
| 248 | 118 | LinkedHashMap<String, Element> bases = entry != null ? new LinkedHashMap<String, Element>(entry.bases) : new LinkedHashMap<String, Element>(); |
| 249 | 118 | Element base = newState.doc.createElement("base"); |
| 250 | 118 | base.setAttribute("target", relative); |
| 251 | 118 | bases.put(relative, base); |
| 252 | 118 | newState.newEntry(name, bases, false).element.appendChild(base); |
| 253 | 118 | return newState; |
| 254 | |
} |
| 255 | |
|
| 256 | |
protected State removeBase(String name, String relative, boolean doCopy) { |
| 257 | 6 | Entry entry = entries.get(name); |
| 258 | 6 | if (entry == null || !entry.bases.containsKey(relative)) return this; |
| 259 | 6 | State newState = copy(doCopy); |
| 260 | 6 | entry = newState.entries.get(name); |
| 261 | 6 | entry.element.removeChild(entry.bases.remove(relative)); |
| 262 | 6 | return newState.checkEmpty(name); |
| 263 | |
} |
| 264 | |
|
| 265 | |
protected void removeDeletedChildren(Collection<String> names) throws FileSystemException { |
| 266 | 62 | for (Entry entry: entries.values()) { |
| 267 | 69 | if (entry.isDeleted) names.remove(entry.element.getAttribute("name")); |
| 268 | |
} |
| 269 | 62 | } |
| 270 | |
|
| 271 | |
protected Collection<String> loadData(String name, boolean[] isDeleted) { |
| 272 | 682 | Entry entry = entries.get(name); |
| 273 | 682 | if (entry == null) return null; |
| 274 | 72 | isDeleted[0] = entry.isDeleted; |
| 275 | 72 | return entry.bases.keySet(); |
| 276 | |
} |
| 277 | |
|
| 278 | |
protected State submitRequests(String name, COWStorageHandler.Request... requests) { |
| 279 | 194 | State newState = copy(true); |
| 280 | 194 | int i = 0, j = 0; |
| 281 | 388 | for (COWStorageHandler.Request request: requests) { |
| 282 | 194 | if (request instanceof COWStorageHandler.ClearRequest) { |
| 283 | 50 | newState.delete(name, false); |
| 284 | 144 | } else if (request instanceof COWStorageHandler.SetDeletedRequest) { |
| 285 | 20 | newState.setDeleted(name, ((COWStorageHandler.SetDeletedRequest) request).deleted, false); |
| 286 | 124 | } else if (request instanceof COWStorageHandler.AddBaseRequest) { |
| 287 | 118 | newState.addBase(name, ((COWStorageHandler.AddBaseRequest) request).relative, false); |
| 288 | 6 | } else if (request instanceof COWStorageHandler.RemoveBaseRequest) { |
| 289 | 6 | newState.removeBase(name, ((COWStorageHandler.RemoveBaseRequest) request).relative, false); |
| 290 | |
} |
| 291 | |
} |
| 292 | 194 | return newState.entries.isEmpty() ? null : newState; |
| 293 | |
} |
| 294 | |
} |
| 295 | |
|
| 296 | |
private final static class Entry { |
| 297 | |
protected final Element element; |
| 298 | |
protected final LinkedHashMap<String, Element> bases; |
| 299 | |
protected final boolean isDeleted; |
| 300 | |
|
| 301 | 335 | protected Entry(Element element, LinkedHashMap<String, Element> bases, boolean isDeleted) { |
| 302 | 335 | this.element = element; |
| 303 | 335 | this.bases = bases; |
| 304 | 335 | this.isDeleted = isDeleted; |
| 305 | 335 | } |
| 306 | |
} |
| 307 | |
} |