/*
* (c) Copyright 2010-2011 AgileBirds
*
* This file is part of OpenFlexo.
*
* OpenFlexo 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, either version 3 of the License, or
* (at your option) any later version.
*
* OpenFlexo 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 OpenFlexo. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.netbeans.lib.cvsclient.admin;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.netbeans.lib.cvsclient.command.GlobalOptions;
import org.netbeans.lib.cvsclient.file.FileUtils;
/**
* A handler for administrative information that maintains full compatibility with the one employed by the original C implementation of a
* CVS client.
* <p>
* This implementation strives to provide complete compatibility with the standard CVS client, so that operations on locally checked-out
* files can be carried out by either this library or the standard client without causing the other to fail. Any such failure should be
* considered a bug in this library.
*
* @author Robert Greig
*/
public class StandardAdminHandler implements AdminHandler {
private static final Object ksEntries = new Object();
private static Runnable t9yBeforeRename;
/**
* Create or update the administration files for a particular file. This will create the CVS directory if necessary, and the Root and
* Repository files if necessary. It will also update the Entries file with the new entry
*
* @param localDirectory
* the local directory where the file in question lives (the absolute path). Must not end with a slash.
* @param entry
* the entry object for that file. If null, there is no entry to add, and the Entries file will not have any entries added to
* it (it will be created if it does not exist, however).
*/
@Override
public void updateAdminData(String localDirectory, String repositoryPath, Entry entry, GlobalOptions globalOptions) throws IOException {
// add this directory to the list of those to check for emptiness if
// the prune option is specified
final File CVSdir = new File(localDirectory, "CVS"); // NOI18N
CVSdir.mkdirs();
// now ensure that the Root and Repository files exist
File rootFile = new File(CVSdir, "Root"); // NOI18N
if (!rootFile.exists()) {
final Writer w = new BufferedWriter(new FileWriter(rootFile));
try {
w.write(globalOptions.getCVSRoot());
} finally {
w.close();
}
}
File repositoryFile = new File(CVSdir, "Repository"); // NOI18N
if (!repositoryFile.exists()) {
final Writer w = new BufferedWriter(new FileWriter(repositoryFile));
try {
if (entry != null && !entry.isDirectory()) {
// If there is a file entry, the repository path is for a file!
int length = entry.getName().length();
repositoryPath = repositoryPath.substring(0, repositoryPath.length() - length);
}
if (repositoryPath.endsWith("/")) { // NOI18N
repositoryPath = repositoryPath.substring(0, repositoryPath.length() - 1);
}
if (repositoryPath.length() == 0) {
repositoryPath = "."; // NOI18N
}
// we write out the relative path to the repository file
w.write(repositoryPath);
} finally {
w.close();
}
}
final File entriesFile = new File(CVSdir, "Entries"); // NOI18N
// We assume that if we do not have an Entries file, we need to add
// the file to the parent CVS directory as well as create the
// Entries file for this directory
if (entriesFile.createNewFile()) {
// need to know if we had to create any directories so that we can
// update the CVS/Entries file in the *parent* director
addDirectoryToParentEntriesFile(CVSdir);
// We have created a new Entries file, so put a D in it to
// indicate that we understand directories.
// TODO: investigate what the point of this is. The command-line
// CVS client does it
final Writer w = new BufferedWriter(new FileWriter(entriesFile));
try {
w.write("D"); // NOI18N
} finally {
w.close();
}
}
// Update the Entries file
if (entry != null) {
updateEntriesFile(entriesFile, entry);
}
}
/**
* Simply delegates to File.exists(), does not provide any virtual files.
*
* @param file
* file to test for existence
* @return true if the file exists on disk, false otherwise
*/
@Override
public boolean exists(File file) {
return file.exists();
}
/**
* Add a directory entry to the parent directory's Entries file, if it exists. Any line containing only a D is deleted from the parent
* Entries file.
*
* @param CVSdir
* the full path to the CVS directory for the directory that has just been added
*/
private void addDirectoryToParentEntriesFile(File CVSdir) throws IOException {
synchronized (ksEntries) {
File parentCVSEntries = seekEntries(CVSdir.getParentFile().getParentFile());
// only update if the file exists. The file will not exist in the
// case where this is the top level of the module
if (parentCVSEntries != null) {
final File directory = parentCVSEntries.getParentFile();
final File tempFile = new File(directory, "Entries.Backup"); // NOI18N
tempFile.createNewFile();
BufferedReader reader = null;
BufferedWriter writer = null;
try {
reader = new BufferedReader(new FileReader(parentCVSEntries));
writer = new BufferedWriter(new FileWriter(tempFile));
String line;
// As in the updateEntriesFile method the new Entry
// only may be written, if it does not exist
boolean written = false;
Entry directoryEntry = new Entry();
directoryEntry.setName(CVSdir.getParentFile().getName());
directoryEntry.setDirectory(true);
while ((line = reader.readLine()) != null) {
if (line.trim().equals("D")) { // NOI18N
// do not write out this line
continue;
}
final Entry currentEntry = new Entry(line);
if (currentEntry.getName() != null && currentEntry.getName().equals(directoryEntry.getName())) {
writer.write(directoryEntry.toString());
written = true;
} else {
writer.write(line);
}
writer.newLine();
}
if (!written) {
writer.write(directoryEntry.toString());
writer.newLine();
}
} finally {
try {
if (writer != null) {
writer.close();
}
} finally {
if (reader != null) {
reader.close();
}
}
}
if (t9yBeforeRename != null) {
t9yBeforeRename.run();
}
FileUtils.renameFile(tempFile, parentCVSEntries);
}
}
}
/**
* Update the specified Entries file with a given entry This method currently does the following, in order:
* <OL>
* <LI>Create a new temporary file, Entries.temp</LI>
* <LI>Iterate through each line of the original file, checking if it matches the entry of the entry to be updated. If not, simply copy
* the line otherwise write the new entry and discard the old one</LI>
* <LI>Once all lines have been written, close both files</LI>
* <LI>Move the original file to Entries.temp2</LI>
* <LI>Move the file Entries.temp to Entries</LI>
* <LI>Delete Entries.temp2</LI></UL> Note that in the case where the Entries file does not exist, it is simply created and the entry
* appended immediately. This all ensures that if a failure occurs at any stage, the original Entries file can be retrieved
*
* @param originalFile
* the original Entries file, which need not exist yet
* @param entry
* the specific entry to update
* @throws IOException
* if an error occurs writing the files
*/
private void updateEntriesFile(File originalFile, Entry entry) throws IOException {
synchronized (ksEntries) {
final File directory = originalFile.getParentFile();
final File tempFile = new File(directory, "Entries.Backup"); // NOI18N
tempFile.createNewFile();
BufferedReader reader = null;
BufferedWriter writer = null;
try {
reader = new BufferedReader(new FileReader(originalFile));
writer = new BufferedWriter(new FileWriter(tempFile));
String line;
// indicates whether we have written the entry that was passed in
// if we finish copying the file without writing it, then it is
// a new entry and must simply be append at the end
boolean written = false;
while ((line = reader.readLine()) != null) {
final Entry currentEntry = new Entry(line);
if (currentEntry.getName() != null && currentEntry.getName().equals(entry.getName())) {
writer.write(entry.toString());
written = true;
} else {
writer.write(line);
}
writer.newLine();
}
if (!written) {
writer.write(entry.toString());
writer.newLine();
}
} finally {
try {
if (writer != null) {
writer.close();
}
} finally {
if (reader != null) {
reader.close();
}
}
}
if (t9yBeforeRename != null) {
t9yBeforeRename.run();
}
FileUtils.renameFile(tempFile, originalFile);
}
}
/**
* Get the Entry for the specified file, if one exists
*
* @param f
* the file
* @throws IOException
* if the Entries file cannot be read
*/
@Override
public Entry getEntry(File file) throws IOException {
final File entriesFile = seekEntries(file.getParentFile()); // NOI18N
// if there is no Entries file we cannot very well get any Entry
// from it
if (entriesFile == null) {
return null;
}
processEntriesDotLog(new File(file.getParent(), "CVS")); // NOI18N
BufferedReader reader = null;
Entry entry = null;
boolean found = false;
try {
reader = new BufferedReader(new FileReader(entriesFile));
String line;
while (!found && (line = reader.readLine()) != null) {
entry = new Entry(line);
// can have a name of null in the case of the single
// D entry line spec, indicating no subdirectories
if (entry.getName() != null) {
// file equality and string equality are not the same thing
File entryFile = new File(file.getParentFile(), entry.getName());
found = entryFile.equals(file);
}
}
} finally {
if (reader != null) {
reader.close();
}
}
if (!found) {
return null;
}
return entry;
}
/**
* Get the entries for a specified directory.
*
* @param directory
* the directory for which to get the entries
* @return an array of Entry objects
*/
public Entry[] getEntriesAsArray(File directory) throws IOException {
List entries = new LinkedList();
final File entriesFile = seekEntries(directory);
// if there is no Entries file we just return the empty iterator
if (entriesFile == null) {
return new Entry[0];
}
processEntriesDotLog(new File(directory, "CVS")); // NOI18N
BufferedReader reader = null;
Entry entry = null;
try {
reader = new BufferedReader(new FileReader(entriesFile));
String line;
while ((line = reader.readLine()) != null) {
entry = new Entry(line);
// can have a name of null in the case of the single
// D entry line spec, indicating no subdirectories
if (entry.getName() != null) {
entries.add(entry);
}
}
} finally {
if (reader != null) {
reader.close();
}
}
Entry[] toReturnArray = new Entry[entries.size()];
toReturnArray = (Entry[]) entries.toArray(toReturnArray);
return toReturnArray;
}
/**
* Get the entries for a specified directory.
*
* @param directory
* the directory for which to get the entries (CVS/Entries is appended)
* @return an iterator of Entry objects
*/
@Override
public Iterator getEntries(File directory) throws IOException {
List entries = new LinkedList();
final File entriesFile = seekEntries(directory);
// if there is no Entries file we just return the empty iterator
if (entriesFile == null) {
return entries.iterator();
}
processEntriesDotLog(new File(directory, "CVS")); // NOI18N
BufferedReader reader = null;
Entry entry = null;
try {
reader = new BufferedReader(new FileReader(entriesFile));
String line;
while ((line = reader.readLine()) != null) {
entry = new Entry(line);
// can have a name of null in the case of the single
// D entry line spec, indicating no subdirectories
if (entry.getName() != null) {
entries.add(entry);
}
}
} finally {
if (reader != null) {
reader.close();
}
}
return entries.iterator();
}
/**
* Set the Entry for the specified file
*
* @param f
* the file whose entry is being updated
* @throws IOException
* if an error occurs writing the details
*/
@Override
public void setEntry(File file, Entry entry) throws IOException {
String parent = file.getParent();
File entriesFile = seekEntries(parent);
if (entriesFile == null) {
entriesFile = new File(parent, "CVS/Entries"); // NOI18N
}
processEntriesDotLog(new File(parent, "CVS")); // NOI18N
updateEntriesFile(entriesFile, entry);
}
/**
* Remove the Entry for the specified file
*
* @param f
* the file whose entry is to be removed
* @throws IOException
* if an error occurs writing the Entries file
*/
@Override
public void removeEntry(File file) throws IOException {
synchronized (ksEntries) {
final File entriesFile = seekEntries(file.getParent());
// if there is no Entries file we cannot very well remove an Entry
// from it
if (entriesFile == null) {
return;
}
processEntriesDotLog(new File(file.getParent(), "CVS")); // NOI18N
final File directory = file.getParentFile();
final File tempFile = new File(directory, "Entries.Backup"); // NOI18N
tempFile.createNewFile();
BufferedReader reader = null;
BufferedWriter writer = null;
try {
reader = new BufferedReader(new FileReader(entriesFile));
writer = new BufferedWriter(new FileWriter(tempFile));
String line;
// allows us to determine whether we need to put in a "D" line. We do
// that if we remove the last directory from the Entries file
boolean directoriesExist = false;
while ((line = reader.readLine()) != null) {
final Entry currentEntry = new Entry(line);
if (currentEntry.getName() != null &&
!currentEntry.getName().equals(file.getName())) {
writer.write(currentEntry.toString());
writer.newLine();
directoriesExist = directoriesExist || currentEntry.isDirectory();
}
}
if (!directoriesExist) {
writer.write("D"); // NOI18N
writer.newLine();
}
} finally {
try {
if (writer != null) {
writer.close();
}
} finally {
if (reader != null) {
reader.close();
}
}
}
if (t9yBeforeRename != null) {
t9yBeforeRename.run();
}
FileUtils.renameFile(tempFile, entriesFile);
}
}
/**
* Get the repository path for a given directory, for example in the directory /home/project/foo/bar, the repository directory might be
* /usr/cvs/foo/bar. The repository directory is commonly stored in the file
*
* <pre>
* Repository
* </pre>
*
* in the CVS directory on the client. (This is the case in the standard CVS command-line tool). However, the path stored in that file
* is relative to the repository path
*
* @param directory
* the directory
* @param the
* repository path on the server, e.g. /home/bob/cvs. Must not end with a slash.
*/
@Override
public String getRepositoryForDirectory(String directory, String repository) throws IOException {
// if there is no "CVS/Repository" file, try to search up the file-
// hierarchy
File repositoryFile = null;
String repositoryDirs = ""; // NOI18N
File dirFile = new File(directory);
while (true) {
// if there is no Repository file we cannot very well get any
// repository from it
if (dirFile == null || dirFile.getName().length() == 0 || !dirFile.exists()) {
throw new FileNotFoundException("Repository file not found " + // NOI18N
"for directory " + directory); // NOI18N
}
repositoryFile = new File(dirFile, "CVS/Repository"); // NOI18N
if (repositoryFile.exists()) {
break;
}
repositoryDirs = '/' + dirFile.getName() + repositoryDirs;
dirFile = dirFile.getParentFile();
}
BufferedReader reader = null;
// fileRepository is the value of the repository read from the
// Repository file
String fileRepository = null;
try {
reader = new BufferedReader(new FileReader(repositoryFile));
fileRepository = reader.readLine();
} finally {
if (reader != null) {
reader.close();
}
}
if (fileRepository == null) {
fileRepository = ""; // NOI18N
}
fileRepository += repositoryDirs;
// absolute repository path ?
if (fileRepository.startsWith("/")) { // NOI18N
return fileRepository;
}
// #69795 'normalize' repository path
if (fileRepository.startsWith("./")) { // NOI18N
fileRepository = fileRepository.substring(2);
}
// otherwise the cvs is using relative repository path
// must be a forward slash, regardless of the local filing system
return repository + '/' + fileRepository;
}
/**
* Update the Entries file using information in the Entries.Log file (if present). If Entries.Log is not present, this method does
* nothing.
*
* @param directory
* the directory that contains the Entries file
* @throws IOException
* if an error occurs reading or writing the files
*/
private void processEntriesDotLog(File directory) throws IOException {
synchronized (ksEntries) {
final File entriesDotLogFile = new File(directory, "Entries.Log"); // NOI18N
if (!entriesDotLogFile.exists()) {
return;
}
BufferedReader reader = new BufferedReader(new FileReader(entriesDotLogFile));
// make up a list of changes to be made based on what is in
// the .log file. Then apply them all later
List additionsList = new LinkedList();
HashSet removalSet = new HashSet();
String line;
try {
while ((line = reader.readLine()) != null) {
if (line.startsWith("A ")) { // NOI18N
final Entry entry = new Entry(line.substring(2));
additionsList.add(entry);
} else if (line.startsWith("R ")) { // NOI18N
final Entry entry = new Entry(line.substring(2));
removalSet.add(entry.getName());
}
// otherwise ignore the line since we don't understand it
}
} finally {
reader.close();
}
if (additionsList.size() > 0 || removalSet.size() > 0) {
final File backup = new File(directory, "Entries.Backup"); // NOI18N
final BufferedWriter writer = new BufferedWriter(new FileWriter(backup));
final File entriesFile = new File(directory, "Entries"); // NOI18N
reader = new BufferedReader(new FileReader(entriesFile));
try {
// maintain a count of the number of directories so that
// we know whether to write the "D" line
int directoryCount = 0;
while ((line = reader.readLine()) != null) {
// we will write out the directory "understanding" line
// later, if necessary
if (line.trim().equals("D")) { // NOI18N
continue;
}
final Entry entry = new Entry(line);
if (entry.isDirectory()) {
directoryCount++;
}
if (!removalSet.contains(entry.getName())) {
writer.write(entry.toString());
writer.newLine();
if (entry.isDirectory()) {
directoryCount--;
}
}
}
Iterator it = additionsList.iterator();
while (it.hasNext()) {
final Entry entry = (Entry) it.next();
if (entry.isDirectory()) {
directoryCount++;
}
writer.write(entry.toString());
writer.newLine();
}
if (directoryCount == 0) {
writer.write("D"); // NOI18N
writer.newLine();
}
} finally {
try {
reader.close();
} finally {
writer.close();
}
}
if (t9yBeforeRename != null) {
t9yBeforeRename.run();
}
FileUtils.renameFile(backup, entriesFile);
}
entriesDotLogFile.delete();
}
}
/**
* Get all the files contained within a given directory that are <b>known to CVS</b>.
*
* @param directory
* the directory to look in
* @return a set of all files.
*/
@Override
public Set getAllFiles(File directory) throws IOException {
TreeSet fileSet = new TreeSet();
BufferedReader reader = null;
try {
final File entriesFile = seekEntries(directory); // NOI18N
// if for any reason we don't have an Entries file just return
// with the empty set.
if (entriesFile == null) {
return fileSet; // Premature return
}
reader = new BufferedReader(new FileReader(entriesFile));
String line;
while ((line = reader.readLine()) != null) {
System.out.println("line=" + line);
final Entry entry = new Entry(line);
if (entry.getName() != null) {
final File f = new File(directory, entry.getName());
if (f.isFile()) {
fileSet.add(f);
}
}
}
} finally {
if (reader != null) {
reader.close();
}
}
return fileSet;
}
// XXX where is pairing setStickyTagForDirectory?
/**
* Checks for presence of CVS/Tag file and returns it's value.
*
* @return the value of CVS/Tag file for the specified directory null if file doesn't exist
*/
@Override
public String getStickyTagForDirectory(File directory) {
BufferedReader reader = null;
File tagFile = new File(directory, "CVS/Tag"); // NOI18N
try {
reader = new BufferedReader(new FileReader(tagFile));
String tag = reader.readLine();
return tag;
} catch (IOException ex) {
// silently ignore??
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
// silently ignore
}
}
}
return null;
}
/**
* If Entries do not exist restore them from backup.
*
* @param folder
* path where to seek CVS/Entries
* @return CVS/Entries file or null
*/
private static File seekEntries(File folder) {
synchronized (ksEntries) {
File entries = new File(folder, "CVS/Entries"); // NOI18N
if (entries.exists()) {
return entries;
} else {
File backup = new File(folder, "CVS/Entries.Backup"); // NOI18N
if (backup.exists()) {
try {
if (t9yBeforeRename != null) {
t9yBeforeRename.run();
}
FileUtils.renameFile(backup, entries);
return entries;
} catch (IOException e) {
// ignore
}
}
}
return null;
}
}
private static File seekEntries(String folder) {
return seekEntries(new File(folder));
}
static void t9yBeforeRenameSync(Runnable run) {
t9yBeforeRename = run;
}
}