001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.database.dao;
019
020import java.io.IOException;
021import java.io.PrintWriter;
022import java.io.Writer;
023import java.util.Collection;
024import java.util.Iterator;
025import java.util.Map;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028import java.util.regex.Pattern;
029
030import javax.xml.stream.XMLOutputFactory;
031import javax.xml.stream.XMLStreamException;
032import javax.xml.stream.XMLStreamWriter;
033
034import org.apache.commons.codec.binary.Base64;
035import org.syncany.chunk.Chunk;
036import org.syncany.chunk.MultiChunk;
037import org.syncany.database.ChunkEntry;
038import org.syncany.database.ChunkEntry.ChunkChecksum;
039import org.syncany.database.DatabaseVersion;
040import org.syncany.database.DatabaseVersionHeader;
041import org.syncany.database.FileContent;
042import org.syncany.database.FileVersion;
043import org.syncany.database.FileVersion.FileType;
044import org.syncany.database.MultiChunkEntry;
045import org.syncany.database.PartialFileHistory;
046import org.syncany.database.VectorClock;
047import org.syncany.util.StringUtil;
048
049/**
050 * This class uses an {@link XMLStreamWriter} to output the given {@link DatabaseVersion}s
051 * to a {@link PrintWriter} (or file). Database versions are written sequentially, i.e. 
052 * according to their position in the given iterator. 
053 * 
054 * <p>A written file includes a representation of the entire database version, including
055 * {@link DatabaseVersionHeader}, {@link PartialFileHistory}, {@link FileVersion}, 
056 * {@link FileContent}, {@link Chunk} and {@link MultiChunk}.
057 * 
058 * @see DatabaseXmlSerializer
059 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
060 */
061public class DatabaseXmlWriter {
062        private static final Logger logger = Logger.getLogger(DatabaseXmlWriter.class.getSimpleName());
063        
064        private static final Pattern XML_RESTRICTED_CHARS_PATTERN = Pattern.compile("[\u0001-\u0008]|[\u000B-\u000C]|[\u000E-\u001F]|[\u007F-\u0084]|[\u0086-\u009F]");
065        private static final int XML_FORMAT_VERSION = 1;
066        
067        private Iterator<DatabaseVersion> databaseVersions;
068        private PrintWriter out;
069        
070        public DatabaseXmlWriter(Iterator<DatabaseVersion> databaseVersions, PrintWriter out) {
071                this.databaseVersions = databaseVersions;
072                this.out = out;
073        }
074        
075        public void write() throws XMLStreamException, IOException {
076                IndentXmlStreamWriter xmlOut = new IndentXmlStreamWriter(out);
077                
078                xmlOut.writeStartDocument();
079                
080                xmlOut.writeStartElement("database");
081                xmlOut.writeAttribute("version", XML_FORMAT_VERSION);
082                 
083                xmlOut.writeStartElement("databaseVersions");
084                                        
085                while (databaseVersions.hasNext()) {
086                        DatabaseVersion databaseVersion = databaseVersions.next();
087                        
088                        // Database version
089                        xmlOut.writeStartElement("databaseVersion");
090                        
091                        // Header, chunks, multichunks, file contents, and file histories
092                        writeDatabaseVersionHeader(xmlOut, databaseVersion);
093                        writeChunks(xmlOut, databaseVersion.getChunks());
094                        writeMultiChunks(xmlOut, databaseVersion.getMultiChunks());
095                        writeFileContents(xmlOut, databaseVersion.getFileContents());
096                        writeFileHistories(xmlOut, databaseVersion.getFileHistories()); 
097                        
098                        xmlOut.writeEndElement(); // </databaserVersion>
099                }
100                
101                xmlOut.writeEndElement(); // </databaseVersions>
102                xmlOut.writeEndElement(); // </database>
103                 
104                xmlOut.writeEndDocument();
105                
106                xmlOut.flush();
107                xmlOut.close();
108                
109                out.flush();
110                out.close();            
111        }
112
113        private void writeDatabaseVersionHeader(IndentXmlStreamWriter xmlOut, DatabaseVersion databaseVersion) throws IOException, XMLStreamException {
114                if (databaseVersion.getTimestamp() == null || databaseVersion.getClient() == null
115                                || databaseVersion.getVectorClock() == null || databaseVersion.getVectorClock().isEmpty()) {
116
117                        logger.log(Level.SEVERE, "Cannot write database version. Header fields must be filled: "+databaseVersion.getHeader());
118                        throw new IOException("Cannot write database version. Header fields must be filled: "+databaseVersion.getHeader());
119                }
120                
121                xmlOut.writeStartElement("header");
122                
123                xmlOut.writeEmptyElement("time");
124                xmlOut.writeAttribute("value", databaseVersion.getTimestamp().getTime());
125                
126                xmlOut.writeEmptyElement("client");
127                xmlOut.writeAttribute("name", databaseVersion.getClient());
128                
129                xmlOut.writeStartElement("vectorClock");
130
131                VectorClock vectorClock = databaseVersion.getVectorClock();                     
132                for (Map.Entry<String, Long> vectorClockEntry : vectorClock.entrySet()) {
133                        xmlOut.writeEmptyElement("client");
134                        xmlOut.writeAttribute("name", vectorClockEntry.getKey());
135                        xmlOut.writeAttribute("value", vectorClockEntry.getValue());
136                }
137                
138                xmlOut.writeEndElement(); // </vectorClock>
139                xmlOut.writeEndElement(); // </header>  
140        }
141        
142        private void writeChunks(IndentXmlStreamWriter xmlOut, Collection<ChunkEntry> chunks) throws XMLStreamException {
143                if (chunks.size() > 0) {
144                        xmlOut.writeStartElement("chunks");
145                                                                
146                        for (ChunkEntry chunk : chunks) {
147                                xmlOut.writeEmptyElement("chunk");
148                                xmlOut.writeAttribute("checksum", chunk.getChecksum().toString());
149                                xmlOut.writeAttribute("size", chunk.getSize());
150                        }
151                        
152                        xmlOut.writeEndElement(); // </chunks>
153                }               
154        }
155        
156        private void writeMultiChunks(IndentXmlStreamWriter xmlOut, Collection<MultiChunkEntry> multiChunks) throws XMLStreamException {
157                if (multiChunks.size() > 0) {
158                        xmlOut.writeStartElement("multiChunks");
159                        
160                        for (MultiChunkEntry multiChunk : multiChunks) {
161                                xmlOut.writeStartElement("multiChunk");
162                                xmlOut.writeAttribute("id", multiChunk.getId().toString());
163                                xmlOut.writeAttribute("size", multiChunk.getSize());
164                        
165                                xmlOut.writeStartElement("chunkRefs");
166                                
167                                Collection<ChunkChecksum> multiChunkChunks = multiChunk.getChunks();
168                                for (ChunkChecksum chunkChecksum : multiChunkChunks) {
169                                        xmlOut.writeEmptyElement("chunkRef");
170                                        xmlOut.writeAttribute("ref", chunkChecksum.toString());
171                                }                       
172                                
173                                xmlOut.writeEndElement(); // </chunkRefs>
174                                xmlOut.writeEndElement(); // </multiChunk>                      
175                        }                       
176                        
177                        xmlOut.writeEndElement(); // </multiChunks>
178                }
179        }
180        
181        private void writeFileContents(IndentXmlStreamWriter xmlOut, Collection<FileContent> fileContents) throws XMLStreamException {
182                if (fileContents.size() > 0) {
183                        xmlOut.writeStartElement("fileContents");
184                        
185                        for (FileContent fileContent : fileContents) {
186                                xmlOut.writeStartElement("fileContent");
187                                xmlOut.writeAttribute("checksum", fileContent.getChecksum().toString());
188                                xmlOut.writeAttribute("size", fileContent.getSize());
189                                
190                                xmlOut.writeStartElement("chunkRefs");
191                                
192                                Collection<ChunkChecksum> fileContentChunkChunks = fileContent.getChunks();
193                                for (ChunkChecksum chunkChecksum : fileContentChunkChunks) {
194                                        xmlOut.writeEmptyElement("chunkRef");
195                                        xmlOut.writeAttribute("ref", chunkChecksum.toString());
196                                }                       
197
198                                xmlOut.writeEndElement(); // </chunkRefs>
199                                xmlOut.writeEndElement(); // </fileContent>                     
200                        }       
201                        
202                        xmlOut.writeEndElement(); // </fileContents>                                    
203                }               
204        }
205        
206        private void writeFileHistories(IndentXmlStreamWriter xmlOut, Collection<PartialFileHistory> fileHistories) throws XMLStreamException, IOException {
207                xmlOut.writeStartElement("fileHistories");
208                
209                for (PartialFileHistory fileHistory : fileHistories) {
210                        xmlOut.writeStartElement("fileHistory");
211                        xmlOut.writeAttribute("id", fileHistory.getFileHistoryId().toString());
212                        
213                        xmlOut.writeStartElement("fileVersions");
214                        
215                        Collection<FileVersion> fileVersions = fileHistory.getFileVersions().values();
216                        for (FileVersion fileVersion : fileVersions) {
217                                if (fileVersion.getVersion() == null || fileVersion.getType() == null || fileVersion.getPath() == null 
218                                                || fileVersion.getStatus() == null || fileVersion.getSize() == null || fileVersion.getLastModified() == null) {
219                                        
220                                        throw new IOException("Unable to write file version, because one or many mandatory fields are null (version, type, path, name, status, size, last modified): "+fileVersion);
221                                }
222                                
223                                if (fileVersion.getType() == FileType.SYMLINK && fileVersion.getLinkTarget() == null) {
224                                        throw new IOException("Unable to write file version: All symlinks must have a target.");
225                                }
226                                
227                                xmlOut.writeEmptyElement("fileVersion");
228                                xmlOut.writeAttribute("version", fileVersion.getVersion());
229                                xmlOut.writeAttribute("type", fileVersion.getType().toString());
230                                xmlOut.writeAttribute("status", fileVersion.getStatus().toString());
231                                
232                                if (containsXmlRestrictedChars(fileVersion.getPath())) {
233                                        xmlOut.writeAttribute("pathEncoded", encodeXmlRestrictedChars(fileVersion.getPath()));
234                                }
235                                else {
236                                        xmlOut.writeAttribute("path", fileVersion.getPath());   
237                                }
238                                
239                                xmlOut.writeAttribute("size", fileVersion.getSize());
240                                xmlOut.writeAttribute("lastModified", fileVersion.getLastModified().getTime());                                         
241                                
242                                if (fileVersion.getLinkTarget() != null) {
243                                        xmlOut.writeAttribute("linkTarget", fileVersion.getLinkTarget());
244                                }
245
246                                if (fileVersion.getUpdated() != null) {
247                                        xmlOut.writeAttribute("updated", fileVersion.getUpdated().getTime());
248                                }
249                                
250                                if (fileVersion.getChecksum() != null) {
251                                        xmlOut.writeAttribute("checksum", fileVersion.getChecksum().toString());
252                                }
253                                
254                                if (fileVersion.getDosAttributes() != null) {
255                                        xmlOut.writeAttribute("dosattrs", fileVersion.getDosAttributes());
256                                }
257                                
258                                if (fileVersion.getPosixPermissions() != null) {
259                                        xmlOut.writeAttribute("posixperms", fileVersion.getPosixPermissions());
260                                }
261                        }
262                        
263                        xmlOut.writeEndElement(); // </fileVersions>
264                        xmlOut.writeEndElement(); // </fileHistory>     
265                }                                               
266                
267                xmlOut.writeEndElement(); // </fileHistories>           
268        }
269        
270        private String encodeXmlRestrictedChars(String str) {
271                return Base64.encodeBase64String(StringUtil.toBytesUTF8(str));
272        }
273
274        /**
275         * Detects disallowed characters as per the XML 1.1 definition
276         * at http://www.w3.org/TR/xml11/#charsets
277         */
278        private boolean containsXmlRestrictedChars(String str) {
279                return XML_RESTRICTED_CHARS_PATTERN.matcher(str).find();
280        }
281
282        /**
283         * Wraps an {@link XMLStreamWriter} class to write XML data to
284         * the given {@link Writer}. 
285         * 
286         * <p>The class extends the regular xml stream writer to add 
287         * tab-based indents to the structure. The output is well-formatted
288         * human-readable XML. 
289         */
290        public static class IndentXmlStreamWriter {
291                private int indent;
292                private XMLStreamWriter out;
293                
294                public IndentXmlStreamWriter(Writer out) throws XMLStreamException {
295                        XMLOutputFactory factory = XMLOutputFactory.newInstance();
296                        
297                        this.indent = 0;
298                        this.out = factory.createXMLStreamWriter(out);
299                }
300                
301                private void writeStartDocument() throws XMLStreamException {
302                        out.writeStartDocument();
303                }
304
305                private void writeStartElement(String s) throws XMLStreamException {
306                        writeNewLineAndIndent(indent++);
307                        out.writeStartElement(s);       
308                }
309                
310                private void writeEmptyElement(String s) throws XMLStreamException {
311                        writeNewLineAndIndent(indent);                  
312                        out.writeEmptyElement(s);       
313                }
314                
315                private void writeAttribute(String name, String value) throws XMLStreamException {
316                        out.writeAttribute(name, value);
317                }
318                
319                private void writeAttribute(String name, int value) throws XMLStreamException {
320                        out.writeAttribute(name, Integer.toString(value));
321                }
322                
323                private void writeAttribute(String name, long value) throws XMLStreamException {
324                        out.writeAttribute(name, Long.toString(value));
325                }
326                
327                private void writeEndElement() throws XMLStreamException {                      
328                        writeNewLineAndIndent(--indent);
329                        out.writeEndElement();                  
330                }
331                
332                private void writeEndDocument() throws XMLStreamException {
333                        out.writeEndDocument();
334                }
335                
336                private void close() throws XMLStreamException {
337                        out.close();
338                }
339                
340                private void flush() throws XMLStreamException {
341                        out.flush();
342                }       
343                
344                private void writeNewLineAndIndent(int indent) throws XMLStreamException {
345                        out.writeCharacters("\n");                      
346                        
347                        for (int i=0; i<indent; i++) {
348                                out.writeCharacters("\t");
349                        }
350                }
351        }       
352}