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}