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