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 &lt;pobj:updates&gt;}</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