/**
* This file is licensed under the University of Illinois/NCSA Open Source License. See LICENSE.TXT for details.
*/
package edu.illinois.codingtracker.recording;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import edu.illinois.codingspectator.data.CodingSpectatorDataPlugin;
import edu.illinois.codingtracker.helpers.CollectionHelper;
import edu.illinois.codingtracker.helpers.Debugger;
import edu.illinois.codingtracker.helpers.Messages;
import edu.illinois.codingtracker.helpers.ResourceHelper;
import edu.illinois.codingtracker.helpers.StringHelper;
/**
*
* @author Stas Negara
*
*/
public class KnownFilesRecorder {
enum FileProperties {
ENCODING, TIMESTAMP
}
private static final String PROPERTIES_DELIMETER= ",";
private static KnownFilesRecorder recorderInstance= null;
private final Properties knownFiles; //Is thread-safe since SE 6
private Map<String, String> currentWorkspaceOptions;
private static final long REFRESH_INTERVAL= 7 * 24 * 60 * 60 * 1000; //Refresh known files every 7 days
private static final IPath CODINGTRACKER_PATH= Platform.getStateLocation(Platform.getBundle(Activator.PLUGIN_ID));
private static final IPath KNOWNFILES_PATH= CODINGTRACKER_PATH.append(CodingSpectatorDataPlugin.getCodingSpectatorVersion().toString());
private final File knownFilesFile= KNOWNFILES_PATH.append("knownFiles.txt").toFile();
private final File workspaceOptionsFile= KNOWNFILES_PATH.append("workspaceOptions.txt").toFile();
/**
* Very dangerous! Should be used ONLY for testing!
*/
public void reset() {
knownFiles.clear();
currentWorkspaceOptions.clear();
}
public static KnownFilesRecorder getInstance() {
if (recorderInstance == null) {
recorderInstance= new KnownFilesRecorder();
}
return recorderInstance;
}
private KnownFilesRecorder() {
knownFiles= readPropertiesFromFile(knownFilesFile);
refreshKnownFiles();
currentWorkspaceOptions= CollectionHelper.getMap(readPropertiesFromFile(workspaceOptionsFile));
}
private void refreshKnownFiles() {
long currentTime= System.currentTimeMillis();
Iterator<Object> keysIterator= knownFiles.keySet().iterator();
boolean hasChanged= false;
while (keysIterator.hasNext()) {
String key= keysIterator.next().toString();
if (!isCVSEntriesPath(key)) {
String timestamp= getSpecificProperty(knownFiles.getProperty(key), FileProperties.TIMESTAMP);
if (currentTime - Long.valueOf(timestamp) > REFRESH_INTERVAL) {
keysIterator.remove();
hasChanged= true;
}
}
}
if (hasChanged) {
recordKnownFiles();
}
}
private boolean isCVSEntriesPath(String filePath) {
return filePath.endsWith("/CVS/Entries");
}
private String getSpecificProperty(String propertiesString, FileProperties property) {
String[] properties= propertiesString.split(PROPERTIES_DELIMETER);
switch (property) {
case ENCODING:
return properties[0];
case TIMESTAMP:
return properties[1];
}
return ""; //should not reach here
}
public void recordKnownFiles() {
Debugger.debug("recordKnownFiles");
writePropertiesToFile(knownFiles, knownFilesFile);
}
//TODO: See if reading and writing to Properties in this class, and to a file in FileHelper have sufficient similarities
//to be factored out in common methods.
private synchronized Properties readPropertiesFromFile(File file) {
Properties properties= new Properties();
InputStreamReader inputStreamReader= null;
try {
if (file.exists()) {
inputStreamReader= new InputStreamReader(new FileInputStream(file), ResourceHelper.UNIVERSAL_CHARSET);
properties.load(inputStreamReader);
}
} catch (IOException e) {
Debugger.logExceptionToErrorLog(e, Messages.Recorder_ReadPropertiesFromFileException + file.getName());
} finally {
if (inputStreamReader != null) {
try {
inputStreamReader.close();
} catch (IOException e) {
//do nothing
}
}
}
return properties;
}
private synchronized void writePropertiesToFile(Properties properties, File file) {
BufferedWriter bufferedWriter= null;
try {
ResourceHelper.ensureFileExists(file);
bufferedWriter= new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), ResourceHelper.UNIVERSAL_CHARSET));
properties.store(bufferedWriter, null);
} catch (IOException e) {
Debugger.logExceptionToErrorLog(e, Messages.Recorder_WritePropertiesToFileException + file.getName());
} finally {
if (bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
//do nothing
}
}
}
}
public boolean isFileKnown(IFile file, boolean shouldMatchEncoding) {
return isFileKnown(file, ResourceHelper.getCharsetNameForFile(file), shouldMatchEncoding);
}
public boolean isFileKnown(IFile file, String charsetName, boolean shouldMatchEncoding) {
String propertiesString= knownFiles.getProperty(getKeyForResource(file));
if (propertiesString != null) {
if (shouldMatchEncoding) {
//Compare Charsets rather than string representations to account for aliases.
Charset fileCharset= ResourceHelper.getCharsetForNameOrDefault(charsetName);
Charset storedCharset= ResourceHelper.getCharsetForNameOrDefault(getSpecificProperty(propertiesString, FileProperties.ENCODING));
return fileCharset.equals(storedCharset);
} else {
return true;
}
}
return false;
}
void addKnownFile(IFile file, String charsetName) {
String propertiesString= charsetName + PROPERTIES_DELIMETER + String.valueOf(System.currentTimeMillis());
knownFiles.setProperty(getKeyForResource(file), propertiesString);
}
public Object removeKnownFile(IFile file) {
return knownFiles.remove(getKeyForResource(file));
}
public void removeKnownFiles(Set<IFile> files) {
boolean hasChanged= false;
for (IFile file : files) {
Object removed= removeKnownFile(file);
if (removed != null) {
hasChanged= true;
}
}
if (hasChanged) {
recordKnownFiles();
}
}
public synchronized void addCVSEntriesFile(IFile cvsEntriesSourceFile) {
addKnownFile(cvsEntriesSourceFile, ResourceHelper.getCharsetNameForFile(cvsEntriesSourceFile));
File cvsEntriesDestinationFile= getTrackedCVSEntriesFile(cvsEntriesSourceFile);
try {
ResourceHelper.ensureFileExists(cvsEntriesDestinationFile);
ResourceHelper.writeFileContent(cvsEntriesDestinationFile, ResourceHelper.readFileContent(cvsEntriesSourceFile), false);
} catch (IOException e) {
Debugger.logExceptionToErrorLog(e, Messages.Recorder_CVSEntriesCopyFailure);
}
}
public void moveKnownFiles(IResource movedResource, IPath destination, boolean success) {
reorganizeKnownFiles(movedResource, destination, success, true);
}
public void copyKnownFiles(IResource copiedResource, IPath destination, boolean success) {
reorganizeKnownFiles(copiedResource, destination, success, false);
}
public void removeKnownFilesForResource(IResource resource) {
reorganizeKnownFiles(resource, resource.getFullPath(), false, true);
}
private void reorganizeKnownFiles(IResource reorganizedResource, IPath destination, boolean shouldCreateNewEntry, boolean shouldRemoveOldEntry) {
String oldKey= getKeyForResource(reorganizedResource);
if (reorganizedResource instanceof IFile) {
if (knownFiles.containsKey(oldKey)) {
reorganizeKnownFile(oldKey, getKeyForPath(destination), shouldCreateNewEntry, shouldRemoveOldEntry);
recordKnownFiles();
}
} else { //IContainer
String oldPrefix= oldKey + IPath.SEPARATOR;
Set<Entry<Object, Object>> reorganizedEntries= getKnownFileEntriesPrefixedBy(oldPrefix);
if (!reorganizedEntries.isEmpty()) {
String newPrefix= getKeyForPath(destination) + IPath.SEPARATOR;
for (Entry<Object, Object> entry : reorganizedEntries) {
String oldEntryKey= (String)entry.getKey();
String newEntryKey= StringHelper.replacePrefix(oldEntryKey, oldPrefix, newPrefix);
reorganizeKnownFile(oldEntryKey, newEntryKey, shouldCreateNewEntry, shouldRemoveOldEntry);
}
recordKnownFiles();
}
}
}
private void reorganizeKnownFile(String oldKey, String newKey, boolean shouldCreateNewEntry, boolean shouldRemoveOldEntry) {
if (shouldCreateNewEntry) {
knownFiles.setProperty(newKey, knownFiles.getProperty(oldKey));
}
if (shouldRemoveOldEntry) {
knownFiles.remove(oldKey); //remove the old entry after the new one is added (to reuse its value above)
}
}
private Set<Entry<Object, Object>> getKnownFileEntriesPrefixedBy(String prefix) {
Set<Entry<Object, Object>> result= new HashSet<Entry<Object, Object>>();
for (Entry<Object, Object> entry : knownFiles.entrySet()) {
if (((String)entry.getKey()).startsWith(prefix)) {
result.add(entry);
}
}
return result;
}
private String getKeyForResource(IResource resource) {
return ResourceHelper.getPortableResourcePath(resource);
}
private String getKeyForPath(IPath path) {
return path.toPortableString();
}
public static String getKnownFilesPath() {
return KNOWNFILES_PATH.toOSString();
}
public File getTrackedCVSEntriesFile(IFile cvsEntriesSourceFile) {
return KNOWNFILES_PATH.append(cvsEntriesSourceFile.getFullPath()).toFile();
}
boolean areWorkspaceOptionsCurrent(Map<String, String> workspaceOptions) {
return currentWorkspaceOptions.equals(workspaceOptions);
}
void recordWorkspaceOptions(Map<String, String> workspaceOptions) {
Debugger.debug("recordWorkspaceOptions");
currentWorkspaceOptions= workspaceOptions;
writePropertiesToFile(CollectionHelper.getProperties(currentWorkspaceOptions), workspaceOptionsFile);
}
boolean areProjectOptionsCurrent(String projectName, Map<String, String> projectOptions) {
Properties trackedProjectOptions= readPropertiesFromFile(getProjectOptionsFile(projectName));
return CollectionHelper.getMap(trackedProjectOptions).equals(projectOptions);
}
void recordProjectOptions(String projectName, Map<String, String> projectOptions) {
Debugger.debug("recordProjectOptions: " + projectName);
writePropertiesToFile(CollectionHelper.getProperties(projectOptions), getProjectOptionsFile(projectName));
}
private File getProjectOptionsFile(String projectName) {
return getProjectFile(projectName, "projectOptions.txt");
}
boolean areReferencingProjectsCurrent(String projectName, Set<String> referencingProjectNames) {
Properties trackedReferencingProjects= readPropertiesFromFile(getReferencingProjectsFile(projectName));
return trackedReferencingProjects.keySet().equals(referencingProjectNames);
}
private File getReferencingProjectsFile(String projectName) {
return getProjectFile(projectName, "referencingProjects.txt");
}
void recordReferencingProjects(String projectName, Set<String> referencingProjectNames) {
Debugger.debug("recordReferencingProjectsForProject: " + projectName);
writePropertiesToFile(CollectionHelper.getProperties(referencingProjectNames), getReferencingProjectsFile(projectName));
}
private File getProjectFile(String projectName, String fileName) {
return KNOWNFILES_PATH.append(projectName).append(fileName).toFile();
}
}