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}