/* * Copyright 2004 - 2008 Christian Sprajc, Dennis Waldherr. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder 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. * * PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id: FileInfo.java 5858 2008-11-24 02:30:33Z tot $ */ package de.dal33t.powerfolder.light; import java.io.Serializable; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Logger; import de.dal33t.powerfolder.util.ImmutableList; import de.dal33t.powerfolder.util.Reject; /** * Represents the history of a file. To prevent side effects, subclasses should * be immutable. * * @author Dennis Waldherr * @author Christian Sprajc */ public class FileHistory implements Serializable { private int hashCode; private static final Logger log = Logger.getLogger(FileHistory.class .getName()); private static final long serialVersionUID = 100L; private final ImmutableList<Record> history; public static class Conflict { private final FileInfo localFileInfo; private final FileInfo otherFileInfo; private final FileInfo ancestorFileInfo; public Conflict(FileInfo localFileInfo, FileInfo otherFileInfo, FileInfo ancestorFileInfo) { this.localFileInfo = localFileInfo; this.otherFileInfo = otherFileInfo; this.ancestorFileInfo = ancestorFileInfo; } /** * Returns the most recent FileInfo of this FileHistory. * * @return */ public FileInfo getLocalFileInfo() { return localFileInfo; } /** * Returns the most recent FileInfo of the other FileHistory. * * @return */ public FileInfo getOtherFileInfo() { return otherFileInfo; } /** * Returns the FileInfo ancestor common to both, the local and the * remote, FileInfos. * * @return a common ancestor FileInfo or null if there isn't one */ public FileInfo getAncestorFileInfo() { return ancestorFileInfo; } } public static class Record { private final FileInfo mergedFirst; private final FileInfo mergedSecond; private final FileInfo fileInfo; public Record(FileInfo fileInfoA, FileInfo fileInfoB, FileInfo fileInfo) { this.mergedFirst = fileInfoA; this.mergedSecond = fileInfoB; this.fileInfo = fileInfo; } /** * @return one of the files merged to create the resulting FileInfo or * null if no merging was done */ public FileInfo getMergedFirst() { return mergedFirst; } /** * @return one of the files merged to create the resulting FileInfo or * null if no merging was done */ public FileInfo getMergedSecond() { return mergedSecond; } public boolean wasMerged() { return mergedFirst != null; } public FileInfo getFileInfo() { return fileInfo; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((fileInfo == null) ? 0 : fileInfo.hashCode()); result = prime * result + ((mergedFirst == null) ? 0 : mergedFirst.hashCode()); result = prime * result + ((mergedSecond == null) ? 0 : mergedSecond.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Record other = (Record) obj; if (!fileInfo.equals(other.fileInfo)) { return false; } if (mergedFirst == null) { if (other.mergedFirst != null) { return false; } } else if (!mergedFirst.equals(other.mergedFirst)) { return false; } if (mergedSecond == null) { if (other.mergedSecond != null) { return false; } } else if (!mergedSecond.equals(other.mergedSecond)) { return false; } return true; } } private FileHistory(Record record) { super(); Reject.ifNull(record, "file is null!"); history = new ImmutableList<Record>(record); } private FileHistory(ImmutableList<Record> history) { this.history = history; } /** * @return the Record with the most recent version. */ public Record getRecord() { return history.getHead(); } /** * Tests if the given FileHistory has a version conflict with this one. A * conflict happens if both histories have a common VersionedFile which is * not the most recent version for any of the histories or if they do not * have a common ancestor. * * @param other * another history * @return a {@link Conflict} or null if none was detected */ public Conflict getConflictWith(FileHistory other) { Reject.notNull(other, "other"); if (getRecord().getFileInfo().isVersionDateAndSizeIdentical( other.getRecord().getFileInfo()) || !getRecord().getFileInfo().equals( other.getRecord().getFileInfo())) { return null; } FileInfo cv = getCommonVersion(other); if (cv == null || !cv.isVersionDateAndSizeIdentical(getRecord().getFileInfo()) && !cv.isVersionDateAndSizeIdentical(other.getRecord() .getFileInfo())) { return new Conflict(getRecord().getFileInfo(), other.getRecord() .getFileInfo(), cv); } return null; } /** * Adds a new version to the history and replaces the most recent file. * * @param newFileInfo * the file version to add * @return a new FileHistory with the given fileInfo as the most recent * version */ public FileHistory addVersion(Record newRecord) { Reject.ifNull(newRecord, "newRecord is null"); if (history.getTail() != null) { FileInfo newFileInfo = newRecord.fileInfo; FileInfo lastFileInfo = history.getTail().getHead().fileInfo; if (lastFileInfo.getVersion() >= newFileInfo.getVersion()) { // Only merged histories are allowed to have FileInfos with the // same version in it! throw new IllegalStateException( "Strange history add. Last file: " + lastFileInfo.toDetailString() + ", added: " + newFileInfo.toDetailString()); } } return new FileHistory(history.add(newRecord)); } /** * Returns the most recent file version that is shared by this history and * the given one. * * @param other * @return null, if the given history has no common ancestor with this * history */ public FileInfo getCommonVersion(FileHistory other) { Set<FileInfo> tmp = new HashSet<FileInfo>(); for (Record r : history) { tmp.add(r.fileInfo); } for (Record r : other.history) { if (tmp.contains(r.fileInfo)) { return r.fileInfo; } } return null; } /** * Returns true, if the given FileHistory is same as this one. * * @param other * @return */ @Override public boolean equals(Object o) { if (o != null && o.getClass() != getClass()) { return false; } return structuralEquals((FileHistory) o); } @Override public int hashCode() { if (hashCode == 0) { hashCode = 19; for (Record r : history) { hashCode = hashCode * 37 + r.hashCode(); } if (hashCode == 0) { hashCode = -1; } } return hashCode; } private boolean structuralEquals(FileHistory other) { if (this == other) { return true; } if (other == null) { return false; } Iterator<Record> i = history.iterator(), j = other.history.iterator(); for (; i.hasNext() && j.hasNext();) { if (!i.next().equals(j.next())) { return false; } } return i.hasNext() == j.hasNext(); } }