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.UnsupportedEncodingException; 021import java.util.Date; 022import java.util.logging.Level; 023import java.util.logging.Logger; 024 025import org.apache.commons.codec.binary.Base64; 026import org.syncany.database.ChunkEntry; 027import org.syncany.database.ChunkEntry.ChunkChecksum; 028import org.syncany.database.DatabaseVersion; 029import org.syncany.database.FileContent; 030import org.syncany.database.FileContent.FileChecksum; 031import org.syncany.database.FileVersion; 032import org.syncany.database.FileVersion.FileStatus; 033import org.syncany.database.FileVersion.FileType; 034import org.syncany.database.MemoryDatabase; 035import org.syncany.database.MultiChunkEntry; 036import org.syncany.database.MultiChunkEntry.MultiChunkId; 037import org.syncany.database.PartialFileHistory; 038import org.syncany.database.PartialFileHistory.FileHistoryId; 039import org.syncany.database.VectorClock; 040import org.syncany.database.VectorClock.VectorClockComparison; 041import org.syncany.database.dao.DatabaseXmlSerializer.DatabaseReadType; 042import org.xml.sax.Attributes; 043import org.xml.sax.SAXException; 044import org.xml.sax.helpers.DefaultHandler; 045 046/** 047 * This class is used by the {@link DatabaseXmlSerializer} to read an XML-based 048 * database file from disk. It extends a {@link DefaultHandler} used by a 049 * SAX parser. 050 * 051 * <p>The class can read either an entire file into memory, or only parts of it -- 052 * excluding contents (headers only) or only selecting certain database version 053 * types (DEFAULT or PURGE). 054 * 055 * @see DatabaseXmlSerializer 056 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 057 */ 058public class DatabaseXmlParseHandler extends DefaultHandler { 059 private static final Logger logger = Logger.getLogger(DatabaseXmlParseHandler.class.getSimpleName()); 060 061 private MemoryDatabase database; 062 private VectorClock versionFrom; 063 private VectorClock versionTo; 064 private DatabaseReadType readType; 065 066 private String elementPath; 067 private DatabaseVersion databaseVersion; 068 private VectorClock vectorClock; 069 private boolean vectorClockInLoadRange; 070 private FileContent fileContent; 071 private MultiChunkEntry multiChunk; 072 private PartialFileHistory fileHistory; 073 074 public DatabaseXmlParseHandler(MemoryDatabase database, VectorClock fromVersion, VectorClock toVersion, DatabaseReadType readType) { 075 this.elementPath = ""; 076 this.database = database; 077 this.versionFrom = fromVersion; 078 this.versionTo = toVersion; 079 this.readType = readType; 080 } 081 082 @Override 083 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { 084 elementPath += "/" + qName; 085 086 if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion")) { 087 databaseVersion = new DatabaseVersion(); 088 } 089 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/header/time")) { 090 Date timeValue = new Date(Long.parseLong(attributes.getValue("value"))); 091 databaseVersion.setTimestamp(timeValue); 092 } 093 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/header/client")) { 094 String clientName = attributes.getValue("name"); 095 databaseVersion.setClient(clientName); 096 } 097 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/header/vectorClock")) { 098 vectorClock = new VectorClock(); 099 } 100 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/header/vectorClock/client")) { 101 String clientName = attributes.getValue("name"); 102 Long clientValue = Long.parseLong(attributes.getValue("value")); 103 104 vectorClock.setClock(clientName, clientValue); 105 } 106 else if (readType == DatabaseReadType.FULL) { 107 if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/chunks/chunk")) { 108 String chunkChecksumStr = attributes.getValue("checksum"); 109 ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(chunkChecksumStr); 110 int chunkSize = Integer.parseInt(attributes.getValue("size")); 111 112 ChunkEntry chunkEntry = new ChunkEntry(chunkChecksum, chunkSize); 113 databaseVersion.addChunk(chunkEntry); 114 } 115 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/fileContents/fileContent")) { 116 String checksumStr = attributes.getValue("checksum"); 117 long size = Long.parseLong(attributes.getValue("size")); 118 119 fileContent = new FileContent(); 120 fileContent.setChecksum(FileChecksum.parseFileChecksum(checksumStr)); 121 fileContent.setSize(size); 122 } 123 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/fileContents/fileContent/chunkRefs/chunkRef")) { 124 String chunkChecksumStr = attributes.getValue("ref"); 125 126 fileContent.addChunk(ChunkChecksum.parseChunkChecksum(chunkChecksumStr)); 127 } 128 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/multiChunks/multiChunk")) { 129 String multChunkIdStr = attributes.getValue("id"); 130 MultiChunkId multiChunkId = MultiChunkId.parseMultiChunkId(multChunkIdStr); 131 long size = Long.parseLong(attributes.getValue("size")); 132 133 if (multiChunkId == null) { 134 throw new SAXException("Cannot read ID from multichunk " + multChunkIdStr); 135 } 136 137 multiChunk = new MultiChunkEntry(multiChunkId, size); 138 } 139 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/multiChunks/multiChunk/chunkRefs/chunkRef")) { 140 String chunkChecksumStr = attributes.getValue("ref"); 141 142 multiChunk.addChunk(ChunkChecksum.parseChunkChecksum(chunkChecksumStr)); 143 } 144 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/fileHistories/fileHistory")) { 145 String fileHistoryIdStr = attributes.getValue("id"); 146 FileHistoryId fileId = FileHistoryId.parseFileId(fileHistoryIdStr); 147 fileHistory = new PartialFileHistory(fileId); 148 } 149 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/fileHistories/fileHistory/fileVersions/fileVersion")) { 150 String fileVersionStr = attributes.getValue("version"); 151 String path = attributes.getValue("path"); 152 String pathEncoded = attributes.getValue("pathEncoded"); 153 String sizeStr = attributes.getValue("size"); 154 String typeStr = attributes.getValue("type"); 155 String statusStr = attributes.getValue("status"); 156 String lastModifiedStr = attributes.getValue("lastModified"); 157 String updatedStr = attributes.getValue("updated"); 158 String checksumStr = attributes.getValue("checksum"); 159 String linkTarget = attributes.getValue("linkTarget"); 160 String dosAttributes = attributes.getValue("dosattrs"); 161 String posixPermissions = attributes.getValue("posixperms"); 162 163 if (fileVersionStr == null || (path == null && pathEncoded == null) || typeStr == null || statusStr == null || sizeStr == null 164 || lastModifiedStr == null) { 165 throw new SAXException( 166 "FileVersion: Attributes missing: version, path/pathEncoded, type, status, size and last modified are mandatory"); 167 } 168 169 // Filter it if it was purged somewhere in the future, see #58 170 Long fileVersionNum = Long.parseLong(fileVersionStr); 171 172 // Go add it! 173 FileVersion fileVersion = new FileVersion(); 174 175 fileVersion.setVersion(fileVersionNum); 176 177 if (path != null) { 178 fileVersion.setPath(path); 179 } 180 else { 181 try { 182 fileVersion.setPath(new String(Base64.decodeBase64(pathEncoded), "UTF-8")); 183 } 184 catch (UnsupportedEncodingException e) { 185 throw new RuntimeException("Invalid Base64 encoding for filename: " + pathEncoded); 186 } 187 } 188 189 fileVersion.setType(FileType.valueOf(typeStr)); 190 fileVersion.setStatus(FileStatus.valueOf(statusStr)); 191 fileVersion.setSize(Long.parseLong(sizeStr)); 192 fileVersion.setLastModified(new Date(Long.parseLong(lastModifiedStr))); 193 194 if (updatedStr != null) { 195 fileVersion.setUpdated(new Date(Long.parseLong(updatedStr))); 196 } 197 198 if (checksumStr != null) { 199 fileVersion.setChecksum(FileChecksum.parseFileChecksum(checksumStr)); 200 } 201 202 if (linkTarget != null) { 203 fileVersion.setLinkTarget(linkTarget); 204 } 205 206 if (dosAttributes != null) { 207 fileVersion.setDosAttributes(dosAttributes); 208 } 209 210 if (posixPermissions != null) { 211 fileVersion.setPosixPermissions(posixPermissions); 212 } 213 214 fileHistory.addFileVersion(fileVersion); 215 216 } 217 } 218 } 219 220 @Override 221 public void endElement(String uri, String localName, String qName) throws SAXException { 222 if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion")) { 223 if (vectorClockInLoadRange) { 224 database.addDatabaseVersion(databaseVersion); 225 logger.log(Level.INFO, " + Added database version " + databaseVersion.getHeader()); 226 } 227 else { 228 //logger.log(Level.FINEST, " + IGNORING database version " + databaseVersion.getHeader() + " (not in load range " + versionFrom + " - " 229 // + versionTo + " OR type filter mismatch: " + filterType + " =?= " + databaseVersion.getHeader().getType()); 230 } 231 232 databaseVersion = null; 233 vectorClockInLoadRange = true; 234 } 235 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/header/vectorClock")) { 236 vectorClockInLoadRange = vectorClockInRange(vectorClock, versionFrom, versionTo); 237 238 databaseVersion.setVectorClock(vectorClock); 239 vectorClock = null; 240 } 241 else if (readType == DatabaseReadType.FULL) { 242 if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/fileContents/fileContent")) { 243 databaseVersion.addFileContent(fileContent); 244 fileContent = null; 245 } 246 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/multiChunks/multiChunk")) { 247 databaseVersion.addMultiChunk(multiChunk); 248 multiChunk = null; 249 } 250 else if (elementPath.equalsIgnoreCase("/database/databaseVersions/databaseVersion/fileHistories/fileHistory")) { 251 // File history might be empty if file versions are ignored! 252 if (fileHistory.getFileVersions().size() > 0) { 253 databaseVersion.addFileHistory(fileHistory); 254 } 255 256 fileHistory = null; 257 } 258 else { 259 // System.out.println("NO MATCH"); 260 } 261 } 262 263 elementPath = elementPath.substring(0, elementPath.lastIndexOf("/")); 264 } 265 266 @Override 267 public void characters(char[] ch, int start, int length) throws SAXException { 268 // Nothing 269 } 270 271 private boolean vectorClockInRange(VectorClock vectorClock, VectorClock vectorClockRangeFrom, VectorClock vectorClockRangeTo) { 272 // Determine if: versionFrom < databaseVersion 273 boolean greaterOrEqualToVersionFrom = false; 274 275 if (vectorClockRangeFrom == null) { 276 greaterOrEqualToVersionFrom = true; 277 } 278 else { 279 VectorClockComparison comparison = VectorClock.compare(vectorClockRangeFrom, vectorClock); 280 281 if (comparison == VectorClockComparison.EQUAL || comparison == VectorClockComparison.SMALLER) { 282 greaterOrEqualToVersionFrom = true; 283 } 284 } 285 286 // Determine if: databaseVersion < versionTo 287 boolean lowerOrEqualToVersionTo = false; 288 289 if (vectorClockRangeTo == null) { 290 lowerOrEqualToVersionTo = true; 291 } 292 else { 293 VectorClockComparison comparison = VectorClock.compare(vectorClock, vectorClockRangeTo); 294 295 if (comparison == VectorClockComparison.EQUAL || comparison == VectorClockComparison.SMALLER) { 296 lowerOrEqualToVersionTo = true; 297 } 298 } 299 300 return greaterOrEqualToVersionFrom && lowerOrEqualToVersionTo; 301 } 302}