001
002 /*
003 * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: PersistentObjectSchemaUpdater.java 306 2012-03-07 03:27:21Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.pobj;
009
010 import java.io.ByteArrayOutputStream;
011 import java.io.File;
012 import java.io.IOException;
013 import java.util.ArrayList;
014 import java.util.HashSet;
015 import java.util.Set;
016
017 import javax.validation.ConstraintViolation;
018 import javax.xml.namespace.QName;
019 import javax.xml.stream.XMLEventReader;
020 import javax.xml.stream.XMLEventWriter;
021 import javax.xml.stream.XMLInputFactory;
022 import javax.xml.stream.XMLOutputFactory;
023 import javax.xml.stream.XMLStreamException;
024 import javax.xml.transform.Result;
025 import javax.xml.transform.Source;
026 import javax.xml.transform.stax.StAXResult;
027 import javax.xml.transform.stax.StAXSource;
028 import javax.xml.transform.stream.StreamResult;
029
030 import org.dellroad.stuff.schema.AbstractSchemaUpdater;
031
032 /**
033 * Support superclass for {@link PersistentObject} schema updaters.
034 *
035 * <p>
036 * This class holds a nested {@link PersistentObject} and ensures that it's up to date when started.
037 * Use {@link #getPersistentObject} to access it.
038 *
039 * <p>
040 * Updates are tracked by "secretly" inserting <code>{@link #UPDATES_ELEMENT_NAME <pobj:updates>}</code>
041 * elements into the serialized XML document; these updates are transparently removed when the document is read back.
042 * In this way the document and its set of applied updates always travel together.
043 *
044 * <p>
045 * Subclasses will typically override {@link #getInitialValue} for when there is no persistent file yet.
046 *
047 * @param <T> type of the root persistent object
048 */
049 public class PersistentObjectSchemaUpdater<T> extends AbstractSchemaUpdater<File, PersistentFileTransaction> {
050
051 /**
052 * XML namespace URI used for nested update elements.
053 */
054 public static final String NAMESPACE_URI = "http://dellroad-stuff.googlecode.com/ns/persistentObject";
055
056 /**
057 * Preferred XML namespace prefix for {@link #NAMESPACE_URI} elements.
058 */
059 public static final String XML_PREFIX = "pobj";
060
061 /**
062 * XML element name for the updates list.
063 */
064 public static final QName UPDATES_ELEMENT_NAME = new QName(NAMESPACE_URI, "updates", XML_PREFIX);
065
066 /**
067 * XML element name for a single update.
068 */
069 public static final QName UPDATE_ELEMENT_NAME = new QName(NAMESPACE_URI, "update", XML_PREFIX);
070
071 /**
072 * XML namespace URI used for namespace declarations.
073 */
074 public static final QName XMLNS_ATTRIBUTE_NAME = new QName("http://www.w3.org/2000/xmlns/", XML_PREFIX, "xmlns");
075
076 /**
077 * Default check interval for "out-of-band" updates to the persistent file ({@value}ms).
078 */
079 public static final long DEFAULT_CHECK_INTERVAL = 1000;
080
081 protected File file;
082 protected long writeDelay;
083 protected long checkInterval = DEFAULT_CHECK_INTERVAL;
084 protected int numBackups;
085 protected boolean allowEmptyStart;
086 protected PersistentObjectDelegate<T> delegate;
087
088 private ArrayList<String> updateNames;
089 private PersistentObject<T> persistentObject;
090
091 /**
092 * Configure the file used to store this object persistently. Required property.
093 */
094 public void setFile(File file) {
095 this.file = file;
096 }
097
098 /**
099 * Configure the maximum delay after an update operation before a write-back to the persistent file
100 * must be initiated. Default is zero.
101 */
102 public void setWriteDelay(long writeDelay) {
103 this.writeDelay = writeDelay;
104 }
105
106 /**
107 * Configure the check interval for "out-of-band" updates to the persistent file.
108 * Default is {@link #DEFAULT_CHECK_INTERVAL}.
109 */
110 public void setCheckInterval(long checkInterval) {
111 this.checkInterval = checkInterval;
112 }
113
114 /**
115 * Configure the {@link PersistentObjectDelegate}. Required property.
116 */
117 public void setDelegate(PersistentObjectDelegate<T> delegate) {
118 this.delegate = delegate;
119 }
120
121 /**
122 * Configure the number of backups to make of the persistent file.
123 *
124 * @see PersistentObject#getNumBackups
125 */
126 public void setNumBackups(int numBackups) {
127 this.numBackups = numBackups;
128 }
129
130 /**
131 * Configure whether to all "empty starts". Default is false.
132 *
133 * @see PersistentObject
134 */
135 public void setAllowEmptyStart(boolean allowEmptyStart) {
136 this.allowEmptyStart = allowEmptyStart;
137 }
138
139 /**
140 * Start this instance. Does nothing if already started.
141 *
142 * @throws IllegalArgumentException if an invalid file, write delay, or delegate is configured
143 * @throws PersistentObjectException if an error occurs
144 */
145 public synchronized void start() {
146
147 // Already started?
148 if (this.persistentObject != null)
149 return;
150
151 // Sanity check
152 if (this.file == null)
153 throw new IllegalArgumentException("no file configured");
154 if (this.writeDelay < 0)
155 throw new IllegalArgumentException("negative writeDelay configured");
156 if (this.delegate == null)
157 throw new IllegalArgumentException("no delegate configured");
158 if (this.numBackups < 0)
159 throw new IllegalArgumentException("negative numBackups configured");
160
161 // Create persistent object
162 this.persistentObject = new PersistentObject<T>(new UpdaterDelegate(), this.file, this.writeDelay, this.checkInterval);
163 this.persistentObject.setNumBackups(this.numBackups);
164 this.persistentObject.setAllowEmptyStart(this.allowEmptyStart);
165
166 // Do schema updates
167 boolean success = false;
168 try {
169 this.initializeAndUpdateDatabase(this.file);
170 success = true;
171 } catch (RuntimeException e) {
172 throw e;
173 } catch (Exception e) {
174 throw new PersistentObjectException(e);
175 } finally {
176 if (!success)
177 this.persistentObject = null;
178 }
179
180 // Start persistent object
181 this.persistentObject.start();
182 }
183
184 /**
185 * Stop this instance. Does nothing if already stopped.
186 *
187 * @throws PersistentObjectException if a delayed write back is pending and error occurs during writing
188 */
189 public synchronized void stop() {
190
191 // Already stopped?
192 if (this.persistentObject == null)
193 return;
194
195 // Stop
196 this.persistentObject.stop();
197 this.persistentObject = null;
198 }
199
200 /**
201 * Get the {@link PersistentObject}.
202 *
203 * @throws IllegalStateException if this instance is not started
204 */
205 public synchronized PersistentObject<T> getPersistentObject() {
206 if (this.persistentObject == null)
207 throw new IllegalStateException("not started");
208 return this.persistentObject;
209 }
210
211 /**
212 * Get the initial value for the persistent object when no persistent file is found.
213 *
214 * <p>
215 * The implementation in {@link PersistentObjectSchemaUpdater} just returns null, which leaves the
216 * initial root object unset. Subclasses should override as desired to provide an initial value.
217 *
218 * <p>
219 * The returned value must properly validate.
220 */
221 protected T getInitialValue() {
222 return null;
223 }
224
225 @Override
226 protected boolean databaseNeedsInitialization(PersistentFileTransaction transaction) throws Exception {
227 return transaction.getData() == null;
228 }
229
230 @Override
231 @SuppressWarnings("unchecked")
232 protected void initializeDatabase(PersistentFileTransaction transaction) throws Exception {
233
234 // Get initial value
235 T initialValue = this.getInitialValue();
236 if (initialValue == null)
237 return;
238
239 // Validate it
240 Set<ConstraintViolation<T>> violations = this.delegate.validate(initialValue);
241 if (!violations.isEmpty())
242 throw new PersistentObjectValidationException((Set<ConstraintViolation<?>>)(Object)violations);
243
244 // Serialize it
245 ByteArrayOutputStream buffer = new ByteArrayOutputStream(PersistentFileTransaction.FILE_BUFFER_SIZE);
246 StreamResult result = new StreamResult(buffer);
247 this.delegate.serialize(initialValue, result);
248
249 // Set it in the transaction
250 transaction.setData(buffer.toByteArray());
251 }
252
253 @Override
254 protected PersistentFileTransaction openTransaction(File file) throws Exception {
255 return new PersistentFileTransaction(file);
256 }
257
258 @Override
259 protected void commitTransaction(PersistentFileTransaction transaction) throws Exception {
260 this.updateNames = new ArrayList<String>(transaction.getUpdates());
261 transaction.commit();
262 }
263
264 @Override
265 protected void rollbackTransaction(PersistentFileTransaction transaction) throws Exception {
266 transaction.rollback();
267 }
268
269 @Override
270 protected Set<String> getAppliedUpdateNames(PersistentFileTransaction transaction) throws Exception {
271 return new HashSet<String>(transaction.getUpdates());
272 }
273
274 @Override
275 protected void recordUpdateApplied(PersistentFileTransaction transaction, String name) throws Exception {
276 transaction.addUpdate(name);
277 }
278
279 // Our PersistentObjectDelegate that hides the updates when (de)serializing
280 private class UpdaterDelegate extends FilterDelegate<T> {
281
282 private final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
283 private final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();
284
285 UpdaterDelegate() {
286 super(PersistentObjectSchemaUpdater.this.delegate);
287 }
288
289 /**
290 * Serialize object to XML, adding update list.
291 */
292 @Override
293 public void serialize(T obj, Result result) throws IOException {
294 try {
295 XMLEventWriter eventWriter = this.xmlOutputFactory.createXMLEventWriter(result);
296 UpdatesXMLEventWriter updatesWriter = new UpdatesXMLEventWriter(eventWriter,
297 PersistentObjectSchemaUpdater.this.updateNames);
298 super.serialize(obj, new StAXResult(updatesWriter));
299 updatesWriter.close();
300 } catch (IOException e) {
301 throw e;
302 } catch (XMLStreamException e) {
303 throw new PersistentObjectException(e);
304 }
305 }
306
307 /**
308 * Deserialize object from XML, removing update list.
309 */
310 @Override
311 public T deserialize(Source source) throws IOException {
312 try {
313 XMLEventReader eventReader = this.xmlInputFactory.createXMLEventReader(source);
314 UpdatesXMLEventReader updatesReader = new UpdatesXMLEventReader(eventReader);
315 return super.deserialize(new StAXSource(updatesReader));
316 } catch (IOException e) {
317 throw e;
318 } catch (XMLStreamException e) {
319 throw new PersistentObjectException(e);
320 }
321 }
322 }
323 }
324