package com.limegroup.gnutella.library;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.settings.LibrarySettings;
import org.limewire.core.settings.SharingSettings;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.setting.AbstractSettingsGroup;
import org.limewire.setting.SettingsGroupManager;
import org.limewire.util.CommonUtils;
import org.limewire.util.FileUtils;
import org.limewire.util.GenericsUtils;
import org.limewire.util.GenericsUtils.ScanMode;
// Provided like a singleton by LimeWireLibraryModule.lfd()
class LibraryFileData extends AbstractSettingsGroup {
private static final Log LOG = LogFactory.getLog(LibraryFileData.class);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final CategoryManager categoryManager;
private static enum Version {
// for prior versions [before 5.0], see OldLibraryData & LibraryConverter
ONE, // the first ever version [active 5.0 -> 5.1]
TWO, // [active 5.2]
THREE; // the current version [active 5.3]
}
private static final String CURRENT_VERSION_KEY = "CURRENT_VERSION";
// private static final String USER_EXTENSIONS_KEY = "USER_EXTENSIONS";
// private static final String USER_REMOVED_KEY = "USER_REMOVED";
// private static final String MANAGED_DIRECTORIES_KEY = "MANAGED_DIRECTORIES";
// private static final String DO_NOT_MANAGE_KEY = "DO_NOT_MANAGE";
// private static final String EXCLUDE_FILES_KEY = "EXCLUDE_FILES";
private static final String SHARE_DATA_KEY = "SHARE_DATA";
private static final String FILE_DATA_KEY = "FILE_DATA";
private static final String COLLECTION_NAME_KEY = "COLLECTION_NAMES";
private static final String COLLECTION_SHARE_DATA_KEY = "COLLECTION_SHARE_DATA";
private static final String SAFE_URNS = "SAFE_URNS";
static final Integer DEFAULT_SHARED_COLLECTION_ID = 0;
private static final Integer MIN_COLLECTION_ID = 1;
private final Version CURRENT_VERSION = Version.THREE;
private final Map<String, List<Integer>> fileData = new HashMap<String, List<Integer>>();
private final SortedMap<Integer, String> collectionNames = new TreeMap<Integer, String>();
private final Map<Integer, List<String>> collectionShareData = new HashMap<Integer, List<String>>();
private final Set<String> safeUrns = new HashSet<String>();
private volatile boolean dirty = false;
private final File saveFile = new File(CommonUtils.getUserSettingsDir(), "library5.dat");
private final File backupFile = new File(CommonUtils.getUserSettingsDir(), "library5.bak");
private volatile boolean loaded = false;
LibraryFileData(CategoryManager categoryManager) {
this.categoryManager = categoryManager;
SettingsGroupManager.instance().addSettingsGroup(this);
}
private int originalNumPublicSharedFiles = -1;
public boolean isLoaded() {
return loaded;
}
@Override
public void reload() {
load();
}
@Override
public boolean revertToDefault() {
clear();
return true;
}
private void clear() {
lock.writeLock().lock();
try {
dirty = true;
fileData.clear();
} finally {
lock.writeLock().unlock();
}
}
public boolean save() {
if(!loaded || !dirty) {
return false;
}
Map<String, Object> save = new HashMap<String, Object>();
lock.readLock().lock();
try {
save.put(CURRENT_VERSION_KEY, CURRENT_VERSION);
save.put(FILE_DATA_KEY, fileData);
save.put(COLLECTION_NAME_KEY, collectionNames);
save.put(COLLECTION_SHARE_DATA_KEY, collectionShareData);
save.put(SAFE_URNS, safeUrns);
if(FileUtils.writeWithBackupFile(save, backupFile, saveFile, LOG)) {
dirty = false;
}
} finally {
lock.readLock().unlock();
}
return true;
}
void load() {
boolean failed = false;
if(!loadFromFile(saveFile)) {
failed = !loadFromFile(backupFile);
}
dirty = failed;
// Save initial public share list size for inspection stats
if (fileData.size() > 0) {
originalNumPublicSharedFiles = peekPublicSharedListCount();
} else {
originalNumPublicSharedFiles = 0;
}
loaded = true;
}
private boolean loadFromFile(File file) {
Map<String, Object> readMap = null;
try {
ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
Object read = in.readObject();
readMap = GenericsUtils.scanForMap(read, String.class, Object.class, ScanMode.REMOVE);
if (readMap != null) {
Object currentVersion = readMap.get("CURRENT_VERSION");
if(currentVersion == null) {
currentVersion = Version.ONE;
}
if(currentVersion instanceof Version) {
initializeFromVersion(((Version)currentVersion), readMap);
return true;
} else {
return false;
}
}
} catch(Throwable throwable) {
LOG.error("Error loading library", throwable);
}
return false;
}
/**
* Initializes the read map assuming it's a particular version.
*/
private void initializeFromVersion(Version version, Map<String, Object> readMap) {
Map<String, List<Integer>> fileData;
Map<Integer, String> collectionNames;
Map<Integer, List<String>> collectionShareData;
Set<String> safeUrns;
switch(version) {
case ONE:
Map<File, FileProperties> oldShareData = GenericsUtils.scanForMap(readMap.get(SHARE_DATA_KEY), File.class, FileProperties.class, ScanMode.REMOVE);
fileData = new HashMap<String, List<Integer>>();
collectionNames = new HashMap<Integer, String>();
collectionShareData = new HashMap<Integer, List<String>>();
convertShareData(oldShareData, fileData, collectionNames, collectionShareData);
final Map<String, List<Integer>> fileDataFinal = fileData;
LibraryConverterHelper helper = new LibraryConverterHelper(new LibraryConverterHelper.FileAdder() {
@Override
public void addFile(File file) {
if(!fileDataFinal.containsKey(createKey(file))) {
fileDataFinal.put(createKey(file), Collections.<Integer>emptyList());
}
}
}, categoryManager);
//add save directories to library
Set<File> convertedDirectories = new HashSet<File>();
List<File> emptyList = Collections.emptyList();
helper.convertSaveDirectories(emptyList, emptyList, convertedDirectories);
safeUrns = new HashSet<String>();
break;
case TWO:
fileData = new HashMap<String, List<Integer>>();
Map<File, List<Integer>> oldFileData = GenericsUtils.scanForMapOfList(readMap.get(FILE_DATA_KEY), File.class, List.class, Integer.class, ScanMode.REMOVE);
convertShareData(oldFileData, fileData);
collectionNames = GenericsUtils.scanForMap(readMap.get(COLLECTION_NAME_KEY), Integer.class, String.class, ScanMode.REMOVE);
collectionShareData = GenericsUtils.scanForMapOfList(readMap.get(COLLECTION_SHARE_DATA_KEY), Integer.class, List.class, String.class, ScanMode.REMOVE);
safeUrns = GenericsUtils.scanForSet(readMap.get(SAFE_URNS), String.class, ScanMode.REMOVE);
break;
case THREE:
fileData = GenericsUtils.scanForMapOfList(readMap.get(FILE_DATA_KEY), String.class, List.class, Integer.class, ScanMode.REMOVE);
collectionNames = GenericsUtils.scanForMap(readMap.get(COLLECTION_NAME_KEY), Integer.class, String.class, ScanMode.REMOVE);
collectionShareData = GenericsUtils.scanForMapOfList(readMap.get(COLLECTION_SHARE_DATA_KEY), Integer.class, List.class, String.class, ScanMode.REMOVE);
safeUrns = GenericsUtils.scanForSet(readMap.get(SAFE_URNS), String.class, ScanMode.REMOVE);
break;
default:
throw new IllegalStateException("Invalid version: " + version);
}
fileData = internKeys(fileData);
safeUrns = internSafeUrns(safeUrns);
validateCollectionData(fileData, collectionNames, collectionShareData);
lock.writeLock().lock();
try {
clear();
this.fileData.putAll(fileData);
this.collectionNames.putAll(collectionNames);
this.collectionShareData.putAll(collectionShareData);
this.safeUrns.addAll(safeUrns);
} finally {
lock.writeLock().unlock();
}
}
private static Map<String,List<Integer>> internKeys(Map<String, List<Integer>> oldFileData) {
Map<String,List<Integer>> newFileData = new HashMap<String, List<Integer>>();
for ( Map.Entry<String, List<Integer>> entry : oldFileData.entrySet() ) {
newFileData.put(entry.getKey().intern(), entry.getValue());
}
return newFileData;
}
private static Set<String> internSafeUrns(Set<String> oldSafeUrns) {
Set<String> newSafeUrns = new HashSet<String>();
for ( String entry : oldSafeUrns ) {
newSafeUrns.add(entry.intern());
}
return newSafeUrns;
}
private void validateCollectionData(Map<String, List<Integer>> fileData, Map<Integer, String> collectionNames, Map<Integer, List<String>> collectionShareData) {
// TODO: Do some validation
}
/** Converts 5.0 & 5.1 style share data into 5.2-style collections. */
private void convertShareData(Map<File, FileProperties> oldShareData, Map<String, List<Integer>> fileData, Map<Integer, String> collectionNames, Map<Integer, List<String>> collectionShareData) {
int currentId = MIN_COLLECTION_ID;
Map<String, Integer> friendToCollectionMap = new HashMap<String, Integer>();
for(Map.Entry<File, FileProperties> data : oldShareData.entrySet()) {
File file = data.getKey();
FileProperties shareData = data.getValue();
if (shareData == null
|| ((shareData.friends == null || shareData.friends.isEmpty()) && !shareData.gnutella)) {
fileData.put(createKey(file), Collections.<Integer> emptyList());
} else {
if (shareData.friends != null) {
for (String friend : shareData.friends) {
Integer collectionId = friendToCollectionMap.get(friend);
if (collectionId == null) {
collectionId = currentId;
friendToCollectionMap.put(friend, collectionId);
collectionNames.put(collectionId, friend);
List<String> shareList = new ArrayList<String>(1);
shareList.add(friend);
collectionShareData.put(collectionId, shareList);
currentId++;
}
List<Integer> collections = fileData.get(createKey(file));
if (collections == null || collections == Collections.<Integer> emptyList()) {
collections = new ArrayList<Integer>(1);
fileData.put(createKey(file), collections);
}
collections.add(collectionId);
}
}
if (shareData.gnutella) {
List<Integer> collections = fileData.get(createKey(file));
if (collections == null || collections == Collections.<Integer> emptyList()) {
collections = new ArrayList<Integer>(1);
fileData.put(createKey(file), collections);
}
collections.add(DEFAULT_SHARED_COLLECTION_ID);
}
}
}
}
/** Converts 5.0 & 5.1 style share data into 5.2-style collections. */
private void convertShareData(Map<File, List<Integer>> oldFileData, Map<String, List<Integer>> fileData) {
for(Map.Entry<File, List<Integer>> data : oldFileData.entrySet()) {
fileData.put(createKey(data.getKey()), data.getValue());
}
}
private static String createKey(File file) {
return file.getPath().intern();
}
/** Returns true if this URN was marked as safe. */
boolean isFileSafe(String urn) {
lock.readLock().lock();
try {
return safeUrns.contains(urn);
} finally {
lock.readLock().unlock();
}
}
/** Caches the URN as being safe or not. */
void setFileSafe(String urn, boolean safe) {
lock.writeLock().lock();
try {
if(!safe) {
if(safeUrns.remove(urn)) {
dirty = true;
}
} else {
if(safeUrns.add(urn)) {
dirty = true;
}
}
} finally {
lock.writeLock().unlock();
}
}
/** Clears all file data. */
void clearFileData() {
lock.writeLock().lock();
try {
if(!fileData.isEmpty()) {
fileData.clear();
dirty = true;
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Adds a managed file.
*/
void addManagedFile(File file) {
lock.writeLock().lock();
try {
boolean changed = false;
String key = createKey(file);
if(!fileData.containsKey(key)) {
fileData.put(key, Collections.<Integer>emptyList());
changed = true;
}
dirty |= changed;
} finally {
lock.writeLock().unlock();
}
}
/**
* Adds a managed file.
*/
void addOrRenameManagedFile(File file, File originalFile) {
if (originalFile == null) {
addManagedFile(file);
} else {
lock.writeLock().lock();
try {
boolean changed = false;
String key = createKey(file);
if(!fileData.containsKey(key)) {
String originalKey = createKey(originalFile);
if (fileData.containsKey(originalKey)) {
fileData.put(key, fileData.get(originalKey));
fileData.remove(originalKey);
} else {
fileData.put(key, Collections.<Integer>emptyList());
}
changed = true;
}
dirty |= changed;
} finally {
lock.writeLock().unlock();
}
}
}
/**
* Removes a file from being managed.
*/
void removeManagedFile(File file) {
lock.writeLock().lock();
try {
boolean changed = fileData.remove(createKey(file)) != null;
dirty |= changed;
} finally {
lock.writeLock().unlock();
}
}
/** Returns a list of all files that should be managed. */
Iterable<File> getManagedFiles() {
List<File> indivFiles = new ArrayList<File>();
lock.readLock().lock();
try {
for ( String key : fileData.keySet() ) {
indivFiles.add(new File(key));
}
} finally {
lock.readLock().unlock();
}
return indivFiles;
}
/** Retuns true if the given folder is the incomplete folder. */
boolean isIncompleteDirectory(File folder) {
return FileUtils.canonicalize(SharingSettings.INCOMPLETE_DIRECTORY.get()).equals(folder);
}
/** Returns the IDs of all collections. */
Collection<Integer> getStoredCollectionIds() {
lock.readLock().lock();
try {
return new ArrayList<Integer>(collectionNames.keySet());
} finally {
lock.readLock().unlock();
}
}
/** Marks the given file as either in the collection or not in the collection. */
void setFileInCollection(File file, int collectionId, boolean contained) {
lock.writeLock().lock();
try {
if(contained) {
dirty |= addFileToCollection(file, collectionId);
} else {
dirty |= removeFileFromCollection(file, collectionId);
}
} finally {
lock.writeLock().unlock();
}
}
/** Sets whether or not all the given files should be in the collection. */
void setFilesInCollection(Iterable<FileDesc> fileDescs, int collectionId, boolean contained) {
lock.writeLock().lock();
try {
if(contained) {
for(FileDesc fd : fileDescs) {
dirty |= addFileToCollection(fd.getFile(), collectionId);
}
} else {
for(FileDesc fd : fileDescs) {
dirty |= removeFileFromCollection(fd.getFile(), collectionId);
}
}
} finally {
lock.writeLock().unlock();
}
}
/** Returns true if the file was removed from the collection, false if it wasn't in the collection. */
private boolean removeFileFromCollection(File file, int collectionId) {
List<Integer> collections = fileData.get(createKey(file));
if(collections == null || collections.isEmpty()) {
return false;
}
// cast to ensure we use remove(Object) and not remove(int)
return collections.remove((Integer)collectionId);
}
/** Returns true if file was added to the collection, false if it already was in the collection. */
private boolean addFileToCollection(File file, int collectionId) {
boolean changed = false;
List<Integer> collections = fileData.get(createKey(file));
if(collections == null || collections == Collections.<Integer>emptyList()) {
collections = new ArrayList<Integer>(1);
fileData.put(createKey(file), collections);
}
if(!collections.contains(collectionId)) {
collections.add(collectionId);
changed = true;
}
return changed;
}
/** Returns true if the file is in the given collection. */
boolean isFileInCollection(File file, int collectionId) {
lock.readLock().lock();
try {
List<Integer> collections = fileData.get(createKey(file));
if(collections != null) {
return collections.contains(collectionId);
} else {
return false;
}
} finally {
lock.readLock().unlock();
}
}
/** Returns the name of the given collection's id. */
String getNameForCollection(int collectionId) {
lock.readLock().lock();
try {
return collectionNames.get(collectionId);
} finally {
lock.readLock().unlock();
}
}
/** Sets a new name for the collection of the given id. */
boolean setNameForCollection(int collectionId, String name) {
lock.writeLock().lock();
try {
String oldName = collectionNames.put(collectionId, name);
boolean changed = oldName == null || !oldName.equals(name);
dirty |= changed;
return changed;
} finally {
lock.writeLock().unlock();
}
}
/** Returns an ID that will be used for a new collection with the given name. */
int createNewCollection(String name) {
lock.writeLock().lock();
try {
int nextId = MIN_COLLECTION_ID;
if(!collectionNames.isEmpty()) {
nextId = collectionNames.lastKey() + 1;
}
collectionNames.put(nextId, name);
dirty = true;
return nextId;
} finally {
lock.writeLock().unlock();
}
}
/** Removes the collection's share data & name. This assumes all files have already been dereferenced. */
void removeCollection(int collectionId) {
lock.writeLock().lock();
try {
dirty |= collectionNames.remove(collectionId) != null;
dirty |= collectionShareData.remove(collectionId) != null;
} finally {
lock.writeLock().unlock();
}
}
/** Adds a new shareId to the given collection's Id. */
boolean addFriendToCollection(int collectionId, String friendId) {
lock.writeLock().lock();
try {
List<String> ids = collectionShareData.get(collectionId);
if(ids == null) {
ids = Collections.emptyList();
}
if(!ids.contains(friendId)) {
ids = new ArrayList<String>(ids);
ids.add(friendId);
collectionShareData.put(collectionId, Collections.unmodifiableList(ids));
dirty = true;
return true;
} else {
return false;
}
} finally {
lock.writeLock().unlock();
}
}
/** Removes a particular shareId from the given collection's Id. */
boolean removeFriendFromCollection(int collectionId, String friendId) {
lock.writeLock().lock();
try {
List<String> ids = collectionShareData.get(collectionId);
if(ids != null && ids.contains(friendId)) {
ids = new ArrayList<String>(ids);
ids.remove(friendId);
collectionShareData.put(collectionId, Collections.unmodifiableList(ids));
dirty = true;
return true;
} else {
return false;
}
} finally {
lock.writeLock().unlock();
}
}
/** Returns all shareIds for the given collection id. */
List<String> getFriendsForCollection(int collectionId) {
lock.readLock().lock();
try {
List<String> ids = collectionShareData.get(collectionId);
if(ids != null) {
return Collections.unmodifiableList(new ArrayList<String>(ids));
} else {
return Collections.emptyList();
}
} finally {
lock.readLock().unlock();
}
}
/**
* Sets a new share id list for the given collection id. Returns null if no
* change was performed because the lists were the same, otherwise returns
* the list this replaced.
*/
List<String> setFriendsForCollection(int collectionId, List<String> newIds) {
lock.writeLock().lock();
try {
List<String> oldIds = collectionShareData.get(collectionId);
if(oldIds == null) {
oldIds = Collections.emptyList();
}
// See if old & new are the same -- if so, don't bother.
// (use a HashSet so that equality isn't order based)
if(new HashSet<String>(oldIds).equals(newIds)) {
return null;
} else {
if(newIds.isEmpty()) {
collectionShareData.remove(collectionId);
} else {
newIds = Collections.unmodifiableList(new ArrayList<String>(newIds));
collectionShareData.put(collectionId, newIds);
}
dirty = true;
return oldIds;
}
} finally {
lock.writeLock().unlock();
}
}
boolean isProgramManagingAllowed() {
return LibrarySettings.ALLOW_PROGRAMS.getValue();
}
boolean isGnutellaDocumentSharingAllowed() {
return LibrarySettings.ALLOW_DOCUMENT_GNUTELLA_SHARING.getValue();
}
public int peekPublicSharedListCount() {
lock.readLock().lock();
try {
int count = 0;
for ( List<Integer> listForFile : fileData.values() ) {
if (listForFile.contains(DEFAULT_SHARED_COLLECTION_ID)) {
count++;
}
}
return count;
}
finally {
lock.readLock().unlock();
}
}
/**
* Helper method for inspections since this class can not contain inspections due to crazy guice stuff.
*/
int getChangeInNumPublicFiles() {
if (originalNumPublicSharedFiles > -1) {
return peekPublicSharedListCount() - originalNumPublicSharedFiles;
} else {
return -1;
}
}
private static class FileProperties implements Serializable {
private static final long serialVersionUID = 767248414812908206L;
private boolean gnutella;
private Set<String> friends;
@Override
public String toString() {
return "FileProperties: gnutella: " + gnutella + ", friends: " + friends;
}
}
}