/*
* 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");
}
}
}
}