/* * Syncany, www.syncany.org * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.syncany.database.dao; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.apache.commons.codec.binary.Base64; import org.syncany.chunk.Chunk; import org.syncany.chunk.MultiChunk; import org.syncany.database.ChunkEntry; import org.syncany.database.ChunkEntry.ChunkChecksum; import org.syncany.database.DatabaseVersion; import org.syncany.database.DatabaseVersionHeader; import org.syncany.database.FileContent; import org.syncany.database.FileVersion; import org.syncany.database.FileVersion.FileType; import org.syncany.database.MultiChunkEntry; import org.syncany.database.PartialFileHistory; import org.syncany.database.VectorClock; import org.syncany.util.StringUtil; /** * This class uses an {@link XMLStreamWriter} to output the given {@link DatabaseVersion}s * to a {@link PrintWriter} (or file). Database versions are written sequentially, i.e. * according to their position in the given iterator. * * <p>A written file includes a representation of the entire database version, including * {@link DatabaseVersionHeader}, {@link PartialFileHistory}, {@link FileVersion}, * {@link FileContent}, {@link Chunk} and {@link MultiChunk}. * * @see DatabaseXmlSerializer * @author Philipp C. Heckel <philipp.heckel@gmail.com> */ public class DatabaseXmlWriter { private static final Logger logger = Logger.getLogger(DatabaseXmlWriter.class.getSimpleName()); private static final Pattern XML_RESTRICTED_CHARS_PATTERN = Pattern.compile("[\u0001-\u0008]|[\u000B-\u000C]|[\u000E-\u001F]|[\u007F-\u0084]|[\u0086-\u009F]"); private static final int XML_FORMAT_VERSION = 1; private Iterator<DatabaseVersion> databaseVersions; private PrintWriter out; public DatabaseXmlWriter(Iterator<DatabaseVersion> databaseVersions, PrintWriter out) { this.databaseVersions = databaseVersions; this.out = out; } public void write() throws XMLStreamException, IOException { IndentXmlStreamWriter xmlOut = new IndentXmlStreamWriter(out); xmlOut.writeStartDocument(); xmlOut.writeStartElement("database"); xmlOut.writeAttribute("version", XML_FORMAT_VERSION); xmlOut.writeStartElement("databaseVersions"); while (databaseVersions.hasNext()) { DatabaseVersion databaseVersion = databaseVersions.next(); // Database version xmlOut.writeStartElement("databaseVersion"); // Header, chunks, multichunks, file contents, and file histories writeDatabaseVersionHeader(xmlOut, databaseVersion); writeChunks(xmlOut, databaseVersion.getChunks()); writeMultiChunks(xmlOut, databaseVersion.getMultiChunks()); writeFileContents(xmlOut, databaseVersion.getFileContents()); writeFileHistories(xmlOut, databaseVersion.getFileHistories()); xmlOut.writeEndElement(); // </databaserVersion> } xmlOut.writeEndElement(); // </databaseVersions> xmlOut.writeEndElement(); // </database> xmlOut.writeEndDocument(); xmlOut.flush(); xmlOut.close(); out.flush(); out.close(); } private void writeDatabaseVersionHeader(IndentXmlStreamWriter xmlOut, DatabaseVersion databaseVersion) throws IOException, XMLStreamException { if (databaseVersion.getTimestamp() == null || databaseVersion.getClient() == null || databaseVersion.getVectorClock() == null || databaseVersion.getVectorClock().isEmpty()) { logger.log(Level.SEVERE, "Cannot write database version. Header fields must be filled: "+databaseVersion.getHeader()); throw new IOException("Cannot write database version. Header fields must be filled: "+databaseVersion.getHeader()); } xmlOut.writeStartElement("header"); xmlOut.writeEmptyElement("time"); xmlOut.writeAttribute("value", databaseVersion.getTimestamp().getTime()); xmlOut.writeEmptyElement("client"); xmlOut.writeAttribute("name", databaseVersion.getClient()); xmlOut.writeStartElement("vectorClock"); VectorClock vectorClock = databaseVersion.getVectorClock(); for (Map.Entry<String, Long> vectorClockEntry : vectorClock.entrySet()) { xmlOut.writeEmptyElement("client"); xmlOut.writeAttribute("name", vectorClockEntry.getKey()); xmlOut.writeAttribute("value", vectorClockEntry.getValue()); } xmlOut.writeEndElement(); // </vectorClock> xmlOut.writeEndElement(); // </header> } private void writeChunks(IndentXmlStreamWriter xmlOut, Collection<ChunkEntry> chunks) throws XMLStreamException { if (chunks.size() > 0) { xmlOut.writeStartElement("chunks"); for (ChunkEntry chunk : chunks) { xmlOut.writeEmptyElement("chunk"); xmlOut.writeAttribute("checksum", chunk.getChecksum().toString()); xmlOut.writeAttribute("size", chunk.getSize()); } xmlOut.writeEndElement(); // </chunks> } } private void writeMultiChunks(IndentXmlStreamWriter xmlOut, Collection<MultiChunkEntry> multiChunks) throws XMLStreamException { if (multiChunks.size() > 0) { xmlOut.writeStartElement("multiChunks"); for (MultiChunkEntry multiChunk : multiChunks) { xmlOut.writeStartElement("multiChunk"); xmlOut.writeAttribute("id", multiChunk.getId().toString()); xmlOut.writeAttribute("size", multiChunk.getSize()); xmlOut.writeStartElement("chunkRefs"); Collection<ChunkChecksum> multiChunkChunks = multiChunk.getChunks(); for (ChunkChecksum chunkChecksum : multiChunkChunks) { xmlOut.writeEmptyElement("chunkRef"); xmlOut.writeAttribute("ref", chunkChecksum.toString()); } xmlOut.writeEndElement(); // </chunkRefs> xmlOut.writeEndElement(); // </multiChunk> } xmlOut.writeEndElement(); // </multiChunks> } } private void writeFileContents(IndentXmlStreamWriter xmlOut, Collection<FileContent> fileContents) throws XMLStreamException { if (fileContents.size() > 0) { xmlOut.writeStartElement("fileContents"); for (FileContent fileContent : fileContents) { xmlOut.writeStartElement("fileContent"); xmlOut.writeAttribute("checksum", fileContent.getChecksum().toString()); xmlOut.writeAttribute("size", fileContent.getSize()); xmlOut.writeStartElement("chunkRefs"); Collection<ChunkChecksum> fileContentChunkChunks = fileContent.getChunks(); for (ChunkChecksum chunkChecksum : fileContentChunkChunks) { xmlOut.writeEmptyElement("chunkRef"); xmlOut.writeAttribute("ref", chunkChecksum.toString()); } xmlOut.writeEndElement(); // </chunkRefs> xmlOut.writeEndElement(); // </fileContent> } xmlOut.writeEndElement(); // </fileContents> } } private void writeFileHistories(IndentXmlStreamWriter xmlOut, Collection<PartialFileHistory> fileHistories) throws XMLStreamException, IOException { xmlOut.writeStartElement("fileHistories"); for (PartialFileHistory fileHistory : fileHistories) { xmlOut.writeStartElement("fileHistory"); xmlOut.writeAttribute("id", fileHistory.getFileHistoryId().toString()); xmlOut.writeStartElement("fileVersions"); Collection<FileVersion> fileVersions = fileHistory.getFileVersions().values(); for (FileVersion fileVersion : fileVersions) { if (fileVersion.getVersion() == null || fileVersion.getType() == null || fileVersion.getPath() == null || fileVersion.getStatus() == null || fileVersion.getSize() == null || fileVersion.getLastModified() == null) { 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); } if (fileVersion.getType() == FileType.SYMLINK && fileVersion.getLinkTarget() == null) { throw new IOException("Unable to write file version: All symlinks must have a target."); } xmlOut.writeEmptyElement("fileVersion"); xmlOut.writeAttribute("version", fileVersion.getVersion()); xmlOut.writeAttribute("type", fileVersion.getType().toString()); xmlOut.writeAttribute("status", fileVersion.getStatus().toString()); if (containsXmlRestrictedChars(fileVersion.getPath())) { xmlOut.writeAttribute("pathEncoded", encodeXmlRestrictedChars(fileVersion.getPath())); } else { xmlOut.writeAttribute("path", fileVersion.getPath()); } xmlOut.writeAttribute("size", fileVersion.getSize()); xmlOut.writeAttribute("lastModified", fileVersion.getLastModified().getTime()); if (fileVersion.getLinkTarget() != null) { xmlOut.writeAttribute("linkTarget", fileVersion.getLinkTarget()); } if (fileVersion.getUpdated() != null) { xmlOut.writeAttribute("updated", fileVersion.getUpdated().getTime()); } if (fileVersion.getChecksum() != null) { xmlOut.writeAttribute("checksum", fileVersion.getChecksum().toString()); } if (fileVersion.getDosAttributes() != null) { xmlOut.writeAttribute("dosattrs", fileVersion.getDosAttributes()); } if (fileVersion.getPosixPermissions() != null) { xmlOut.writeAttribute("posixperms", fileVersion.getPosixPermissions()); } } xmlOut.writeEndElement(); // </fileVersions> xmlOut.writeEndElement(); // </fileHistory> } xmlOut.writeEndElement(); // </fileHistories> } private String encodeXmlRestrictedChars(String str) { return Base64.encodeBase64String(StringUtil.toBytesUTF8(str)); } /** * Detects disallowed characters as per the XML 1.1 definition * at http://www.w3.org/TR/xml11/#charsets */ private boolean containsXmlRestrictedChars(String str) { return XML_RESTRICTED_CHARS_PATTERN.matcher(str).find(); } /** * Wraps an {@link XMLStreamWriter} class to write XML data to * the given {@link Writer}. * * <p>The class extends the regular xml stream writer to add * tab-based indents to the structure. The output is well-formatted * human-readable XML. */ public static class IndentXmlStreamWriter { private int indent; private XMLStreamWriter out; public IndentXmlStreamWriter(Writer out) throws XMLStreamException { XMLOutputFactory factory = XMLOutputFactory.newInstance(); this.indent = 0; this.out = factory.createXMLStreamWriter(out); } private void writeStartDocument() throws XMLStreamException { out.writeStartDocument(); } private void writeStartElement(String s) throws XMLStreamException { writeNewLineAndIndent(indent++); out.writeStartElement(s); } private void writeEmptyElement(String s) throws XMLStreamException { writeNewLineAndIndent(indent); out.writeEmptyElement(s); } private void writeAttribute(String name, String value) throws XMLStreamException { out.writeAttribute(name, value); } private void writeAttribute(String name, int value) throws XMLStreamException { out.writeAttribute(name, Integer.toString(value)); } private void writeAttribute(String name, long value) throws XMLStreamException { out.writeAttribute(name, Long.toString(value)); } private void writeEndElement() throws XMLStreamException { writeNewLineAndIndent(--indent); out.writeEndElement(); } private void writeEndDocument() throws XMLStreamException { out.writeEndDocument(); } private void close() throws XMLStreamException { out.close(); } private void flush() throws XMLStreamException { out.flush(); } private void writeNewLineAndIndent(int indent) throws XMLStreamException { out.writeCharacters("\n"); for (int i=0; i<indent; i++) { out.writeCharacters("\t"); } } } }