001
002 /*
003 * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: PersistentFileTransaction.java 238 2012-01-18 21:26:56Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.pobj;
009
010 import java.io.BufferedInputStream;
011 import java.io.BufferedOutputStream;
012 import java.io.ByteArrayInputStream;
013 import java.io.ByteArrayOutputStream;
014 import java.io.File;
015 import java.io.FileInputStream;
016 import java.io.FileOutputStream;
017 import java.io.IOException;
018 import java.util.ArrayList;
019 import java.util.Collections;
020 import java.util.List;
021
022 import javax.xml.stream.XMLEventReader;
023 import javax.xml.stream.XMLEventWriter;
024 import javax.xml.stream.XMLInputFactory;
025 import javax.xml.stream.XMLOutputFactory;
026 import javax.xml.stream.XMLStreamException;
027 import javax.xml.transform.Transformer;
028 import javax.xml.transform.TransformerException;
029 import javax.xml.transform.stream.StreamResult;
030 import javax.xml.transform.stream.StreamSource;
031
032 /**
033 * Represents an open "transaction" on a {@link PersistentObject}'s persistent file.
034 *
035 * <p>
036 * This class is used by {@link PersistentObjectSchemaUpdater} and would normally not be used directly.
037 */
038 public class PersistentFileTransaction {
039
040 static final int FILE_BUFFER_SIZE = 32 * 1024 - 32;
041
042 private final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
043 private final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();
044 private final ArrayList<String> updates = new ArrayList<String>();
045 private final File file;
046
047 private byte[] current;
048 private boolean modified;
049
050 /**
051 * Constructor.
052 *
053 * @param file persistent file
054 * @throws PersistentObjectException if no updates are found
055 */
056 public PersistentFileTransaction(File file) throws IOException, XMLStreamException {
057
058 // Save file
059 if (file == null)
060 throw new IllegalStateException("null file");
061 this.file = file;
062
063 // Read in file if it exists
064 if (file.exists())
065 this.readFile();
066 }
067
068 /**
069 * Get the current XML data. Does not include the XML update list.
070 */
071 public byte[] getData() {
072 return this.current;
073 }
074
075 /**
076 * Set the current XML data. Data should not include the XML update list.
077 */
078 public void setData(byte[] data) {
079 this.current = data;
080 this.modified = true;
081 }
082
083 /**
084 * Commit this transaction. This results in the persistent file being atomically overwritten (including update list).
085 */
086 public void commit() throws IOException, XMLStreamException {
087
088 // Sanity check
089 if (this.current == null)
090 throw new PersistentObjectException("no data to save");
091
092 // Anything changed?
093 if (!this.modified)
094 return;
095
096 // Write data with updates to temporary file
097 File tempFile = null;
098 BufferedOutputStream output = null;
099 try {
100 XMLEventReader eventReader = this.xmlInputFactory.createXMLEventReader(new ByteArrayInputStream(this.current));
101 tempFile = File.createTempFile(this.file.getName(), null, this.file.getParentFile());
102 output = new BufferedOutputStream(new FileOutputStream(tempFile), FILE_BUFFER_SIZE);
103 XMLEventWriter eventWriter = this.xmlOutputFactory.createXMLEventWriter(output);
104 UpdatesXMLEventWriter updatesWriter = new UpdatesXMLEventWriter(eventWriter, this.updates);
105 updatesWriter.add(eventReader);
106 updatesWriter.close();
107 output.close();
108 output = null;
109 eventReader.close();
110
111 // Move temp file into place
112 if (!tempFile.renameTo(this.file))
113 throw new IOException("error renaming `" + tempFile.getName() + "' to `" + this.file.getName() + "'");
114 tempFile = null;
115 } finally {
116 if (output != null) {
117 try {
118 output.close();
119 } catch (IOException e) {
120 // ignore
121 }
122 }
123 if (tempFile != null)
124 tempFile.delete();
125 }
126
127 // Done
128 this.current = null;
129 this.updates.clear();
130 this.modified = false;
131 }
132
133 /**
134 * Cancel this transaction.
135 */
136 public void rollback() {
137 this.current = null;
138 this.updates.clear();
139 this.modified = false;
140 }
141
142 /**
143 * Get the updates list associated with this transaction.
144 *
145 * @return unmodifiable list of updates
146 */
147 public List<String> getUpdates() {
148 return Collections.unmodifiableList(this.updates);
149 }
150
151 /**
152 * Add an update to the list associated with this transaction.
153 */
154 public void addUpdate(String name) {
155 this.updates.add(name);
156 this.modified = true;
157 }
158
159 /**
160 * Apply an XSLT transform to the current XML object in this transaction.
161 *
162 * @throws IllegalStateException if the current root object is null
163 * @throws PersistentObjectException if an error occurs
164 * @throws TransformerException if the transformation fails
165 */
166 public void transform(Transformer transformer) throws TransformerException {
167
168 // Sanity check
169 if (this.current == null)
170 throw new PersistentObjectException("no data to transform");
171
172 // Debug
173 //System.out.println("************************** BEFORE TRANSFORM");
174 //System.out.println(new String(this.current));
175
176 // Set up source and result
177 StreamSource source = new StreamSource(new ByteArrayInputStream(this.current));
178 source.setSystemId(file.toURI().toString());
179 ByteArrayOutputStream buffer = new ByteArrayOutputStream(FILE_BUFFER_SIZE);
180 StreamResult result = new StreamResult(buffer);
181 result.setSystemId(file.toURI().toString());
182
183 // Apply transform
184 transformer.transform(source, result);
185
186 // Save result as the new current value
187 this.current = buffer.toByteArray();
188 this.modified = true;
189
190 // Debug
191 //System.out.println("************************** AFTER TRANSFORM");
192 //System.out.println(new String(this.current));
193 }
194
195 private void readFile() throws IOException, XMLStreamException {
196
197 // Read in file, extracting and removing the updates list in the process
198 ByteArrayOutputStream buffer = new ByteArrayOutputStream(FILE_BUFFER_SIZE);
199 XMLEventWriter eventWriter = this.xmlOutputFactory.createXMLEventWriter(buffer);
200 BufferedInputStream input = new BufferedInputStream(new FileInputStream(this.file), FILE_BUFFER_SIZE);
201 XMLEventReader eventReader = this.xmlInputFactory.createXMLEventReader(input);
202 UpdatesXMLEventReader updatesReader = new UpdatesXMLEventReader(eventReader);
203 eventWriter.add(updatesReader);
204 eventWriter.close();
205 eventReader.close();
206 input.close();
207
208 // Was the update list found?
209 List<String> fileUpdates = updatesReader.getUpdates();
210 if (fileUpdates == null)
211 throw new PersistentObjectException("file `" + this.file + "' does not contain an updates list");
212
213 // Save current content (without updates) and updates list
214 this.current = buffer.toByteArray();
215 this.updates.clear();
216 this.updates.addAll(fileUpdates);
217 }
218 }
219