/*
* (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.file;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.Date;
import org.netbeans.lib.cvsclient.command.GlobalOptions;
import org.netbeans.lib.cvsclient.request.Request;
import org.netbeans.lib.cvsclient.util.BugLog;
import org.netbeans.lib.cvsclient.util.LoggedDataInputStream;
import org.netbeans.lib.cvsclient.util.LoggedDataOutputStream;
/**
* Provides a basic implementation of FileHandler, and does much of the handling of reading and writing files and performing CRLF
* conversions.
*
* @author Robert Greig
*/
public class DefaultFileHandler implements FileHandler {
/**
* Whether to emit debug information.
*/
private static final boolean DEBUG = false;
/**
* The size of chunks read from disk.
*/
private static final int CHUNK_SIZE = 32768;
/**
* The date the next file written should be marked as being modified on.
*/
private Date modifiedDate;
private TransmitTextFilePreprocessor transmitTextFilePreprocessor;
private WriteTextFilePreprocessor writeTextFilePreprocessor;
private WriteTextFilePreprocessor writeRcsDiffFilePreprocessor;
private GlobalOptions globalOptions;
/**
* Creates a DefaultFileHandler.
*/
public DefaultFileHandler() {
setTransmitTextFilePreprocessor(new DefaultTransmitTextFilePreprocessor());
setWriteTextFilePreprocessor(new DefaultWriteTextFilePreprocessor());
setWriteRcsDiffFilePreprocessor(new WriteRcsDiffFilePreprocessor());
}
/**
* Returns the preprocessor for transmitting text files.
*/
public TransmitTextFilePreprocessor getTransmitTextFilePreprocessor() {
return transmitTextFilePreprocessor;
}
/**
* Sets the preprocessor for transmitting text files. The default one changes all line endings to Unix-lineendings (cvs default).
*/
public void setTransmitTextFilePreprocessor(TransmitTextFilePreprocessor transmitTextFilePreprocessor) {
this.transmitTextFilePreprocessor = transmitTextFilePreprocessor;
}
/**
* Gets the preprocessor for writing text files after getting (and un-gzipping) from server.
*/
public WriteTextFilePreprocessor getWriteTextFilePreprocessor() {
return writeTextFilePreprocessor;
}
/**
* Sets the preprocessor for writing text files after getting (and un-gzipping) from server.
*/
public void setWriteTextFilePreprocessor(WriteTextFilePreprocessor writeTextFilePreprocessor) {
this.writeTextFilePreprocessor = writeTextFilePreprocessor;
}
/**
* Gets the preprocessor for merging text files after getting (and un-gzipping) the diff received from server.
*/
public WriteTextFilePreprocessor getWriteRcsDiffFilePreprocessor() {
return writeRcsDiffFilePreprocessor;
}
/**
* Sets the preprocessor for merging text files after getting (and un-gzipping) the diff received from server.
*/
public void setWriteRcsDiffFilePreprocessor(WriteTextFilePreprocessor writeRcsDiffFilePreprocessor) {
this.writeRcsDiffFilePreprocessor = writeRcsDiffFilePreprocessor;
}
/**
* Get the string to transmit containing the file transmission length.
*
* @return a String to transmit to the server (including carriage return)
* @param length
* the amount of data that will be sent
*/
protected String getLengthString(long length) {
return String.valueOf(length) + "\n"; // NOI18N
}
protected Reader getProcessedReader(File f) throws IOException {
return new FileReader(f);
}
protected InputStream getProcessedInputStream(File file) throws IOException {
return new FileInputStream(file);
}
/**
* Get any requests that must be sent before commands are sent, to init this file handler.
*
* @return an array of Requests that must be sent
*/
@Override
public Request[] getInitialisationRequests() {
return null;
}
/**
* Transmit a text file to the server, using the standard CVS protocol conventions. CR/LFs are converted to the Unix format.
*
* @param file
* the file to transmit
* @param dos
* the data outputstream on which to transmit the file
*/
@Override
public void transmitTextFile(File file, LoggedDataOutputStream dos) throws IOException {
if (file == null || !file.exists()) {
throw new IllegalArgumentException("File is either null or " + "does not exist. Cannot transmit.");
}
File fileToSend = file;
final TransmitTextFilePreprocessor transmitTextFilePreprocessor = getTransmitTextFilePreprocessor();
if (transmitTextFilePreprocessor != null) {
fileToSend = transmitTextFilePreprocessor.getPreprocessedTextFile(file);
}
BufferedInputStream bis = null;
try {
// first write the length of the file
long length = fileToSend.length();
dos.writeBytes(getLengthString(length), "US-ASCII");
bis = new BufferedInputStream(new FileInputStream(fileToSend));
// now transmit the file itself
byte[] chunk = new byte[CHUNK_SIZE];
while (length > 0) {
int bytesToRead = length >= CHUNK_SIZE ? CHUNK_SIZE : (int) length;
int count = bis.read(chunk, 0, bytesToRead);
if (count == -1) {
throw new IOException("Unexpected end of stream from " + fileToSend + ".");
}
length -= count;
dos.write(chunk, 0, count);
}
dos.flush();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException ex) {
// ignore
}
}
if (transmitTextFilePreprocessor != null) {
transmitTextFilePreprocessor.cleanup(fileToSend);
}
}
}
/**
* Transmit a binary file to the server, using the standard CVS protocol conventions.
*
* @param file
* the file to transmit
* @param dos
* the data outputstream on which to transmit the file
*/
@Override
public void transmitBinaryFile(File file, LoggedDataOutputStream dos) throws IOException {
if (file == null || !file.exists()) {
throw new IllegalArgumentException("File is either null or " + "does not exist. Cannot transmit.");
}
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(file));
// first write the length of the file
long length = file.length();
dos.writeBytes(getLengthString(length), "US-ASCII");
// now transmit the file itself
byte[] chunk = new byte[CHUNK_SIZE];
while (length > 0) {
int bytesToRead = length >= CHUNK_SIZE ? CHUNK_SIZE : (int) length;
int count = bis.read(chunk, 0, bytesToRead);
if (count == -1) {
throw new IOException("Unexpected end of stream from " + file + ".");
}
length -= count;
dos.write(chunk, 0, count);
}
dos.flush();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
/**
* Write (either create or replace) a file on the local machine with one read from the server.
*
* @param path
* the absolute path of the file, (including the file name).
* @param mode
* the mode of the file
* @param dis
* the stream to read the file from, as bytes
* @param length
* the number of bytes to read
*/
@Override
public void writeTextFile(String path, String mode, LoggedDataInputStream dis, int length) throws IOException {
writeAndPostProcessTextFile(path, mode, dis, length, getWriteTextFilePreprocessor());
}
/**
* Merge a text file on the local machine with the diff from the server. (it uses the RcsDiff response format - see cvsclient.ps for
* details)
*
* @param path
* the absolute path of the file, (including the file name).
* @param mode
* the mode of the file
* @param dis
* the stream to read the file from, as bytes
* @param length
* the number of bytes to read
*/
@Override
public void writeRcsDiffFile(String path, String mode, LoggedDataInputStream dis, int length) throws IOException {
writeAndPostProcessTextFile(path, mode, dis, length, getWriteRcsDiffFilePreprocessor());
}
/**
* Common code for writeTextFile() and writeRcsDiffFile() methods. Differs only in the passed file processor.
*/
private void writeAndPostProcessTextFile(String path, String mode, LoggedDataInputStream dis, int length,
WriteTextFilePreprocessor processor) throws IOException {
if (DEBUG) {
System.err.println("[writeTextFile] writing: " + path); // NOI18N
System.err.println("[writeTextFile] length: " + length); // NOI18N
System.err.println("Reader object is: " + dis.hashCode()); // NOI18N
}
File file = new File(path);
boolean readOnly = resetReadOnly(file);
createNewFile(file);
// For CRLF conversion, we have to read the file
// into a temp file, then do the conversion. This is because we cannot
// perform a sequence of readLines() until we've read the file from
// the server - the file transmission is not followed by a newline.
// Bah.
File tempFile = File.createTempFile("cvsCRLF", "tmp"); // NOI18N
try {
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(tempFile));
byte[] chunk = new byte[CHUNK_SIZE];
while (length > 0) {
int count = length >= CHUNK_SIZE ? CHUNK_SIZE : length;
count = dis.read(chunk, 0, count);
if (count == -1) {
throw new IOException("Unexpected end of stream: " + path + "\nMissing " + length
+ " bytes. Probably network communication failure.\nPlease try again."); // NOI18N
}
length -= count;
if (DEBUG) {
System.err.println("Still got: " + length + " to read"); // NOI18N
}
os.write(chunk, 0, count);
}
} finally {
if (os != null) {
try {
os.close();
} catch (IOException ex) {
// ignore
}
}
}
// Here we read the temp file in again, doing any processing required
// (for example, unzipping). We must not convert bytes to characters
// because it would break characters that are not in the current encoding
InputStream tempInput = getProcessedInputStream(tempFile);
try {
// BUGLOG - assert the processor is not null..
processor.copyTextFileToLocation(tempInput, file, new StreamProvider(file));
} finally {
tempInput.close();
}
if (modifiedDate != null) {
file.setLastModified(modifiedDate.getTime());
modifiedDate = null;
}
} finally {
tempFile.delete();
}
if (readOnly) {
FileUtils.setFileReadOnly(file, true);
}
}
/**
* Write (either create or replace) a binary file on the local machine with one read from the server.
*
* @param path
* the absolute path of the file, (including the file name).
* @param mode
* the mode of the file
* @param dis
* the stream to read the file from, as bytes
* @param length
* the number of bytes to read
*/
@Override
public void writeBinaryFile(String path, String mode, LoggedDataInputStream dis, int length) throws IOException {
if (DEBUG) {
System.err.println("[writeBinaryFile] writing: " + path); // NOI18N
System.err.println("[writeBinaryFile] length: " + length); // NOI18N
System.err.println("Reader object is: " + dis.hashCode()); // NOI18N
}
File file = new File(path);
boolean readOnly = resetReadOnly(file);
createNewFile(file);
// FUTURE: optimisation possible - no need to use a temp file if there
// is no post processing required (e.g. unzipping). So perhaps enhance
// the interface to allow this stage to be optional
File cvsDir = new File(file.getParentFile(), "CVS");
cvsDir.mkdir();
File tempFile = File.createTempFile("cvsPostConversion", "tmp", cvsDir); // NOI18N
try {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile));
byte[] chunk = new byte[CHUNK_SIZE];
try {
while (length > 0) {
int bytesToRead = length >= CHUNK_SIZE ? CHUNK_SIZE : (int) length;
int count = dis.read(chunk, 0, bytesToRead);
if (count == -1) {
throw new IOException("Unexpected end of stream: " + path + "\nMissing " + length
+ " bytes. Probably network communication failure.\nPlease try again."); // NOI18N
}
if (count < 0) {
break;
}
length -= count;
if (DEBUG) {
System.err.println("Still got: " + length + " to read"); // NOI18N
}
bos.write(chunk, 0, count);
}
} finally {
bos.close();
}
// Here we read the temp file in, taking the opportunity to process
// the file, e.g. unzip the data
BufferedInputStream tempIS = new BufferedInputStream(getProcessedInputStream(tempFile));
bos = new BufferedOutputStream(createOutputStream(file));
try {
for (int count = tempIS.read(chunk, 0, CHUNK_SIZE); count > 0; count = tempIS.read(chunk, 0, CHUNK_SIZE)) {
bos.write(chunk, 0, count);
}
} finally {
bos.close();
tempIS.close();
}
// now we need to modifiy the timestamp on the file, if specified
if (modifiedDate != null) {
file.setLastModified(modifiedDate.getTime());
modifiedDate = null;
}
} finally {
tempFile.delete();
}
if (readOnly) {
FileUtils.setFileReadOnly(file, true);
}
}
/** Extension point allowing subclasses to change file creation logic. */
protected boolean createNewFile(File file) throws IOException {
file.getParentFile().mkdirs();
return file.createNewFile();
}
/**
* Extension point allowing subclasses to change file write logic. The stream is close()d after usage.
*/
protected OutputStream createOutputStream(File file) throws IOException {
return new FileOutputStream(file);
}
private class StreamProvider implements OutputStreamProvider {
private final File file;
public StreamProvider(File file) {
this.file = file;
}
@Override
public OutputStream createOutputStream() throws IOException {
return DefaultFileHandler.this.createOutputStream(file);
}
}
private boolean resetReadOnly(File file) throws java.io.IOException {
boolean readOnly = globalOptions != null && globalOptions.isCheckedOutFilesReadOnly();
if (file.exists() && readOnly) {
readOnly = !file.canWrite();
if (readOnly) {
FileUtils.setFileReadOnly(file, false);
}
}
return readOnly;
}
/**
* Remove the specified file from the local disk.
*
* @param pathname
* the full path to the file to remove
* @throws IOException
* if an IO error occurs while removing the file
*/
@Override
public void removeLocalFile(String pathname) throws IOException {
File fileToDelete = new File(pathname);
if (fileToDelete.exists() && !fileToDelete.delete()) {
System.err.println("Could not delete file " + fileToDelete.getAbsolutePath());
}
}
/**
* Rename the local file. If the destination file exists, the operation does nothing.
*
* @param pathname
* the full path to the file to rename
* @param newName
* the new name of the file (not the full path)
* @throws IOException
* if an IO error occurs while renaming the file
*/
@Override
public void renameLocalFile(String pathname, String newName) throws IOException {
File sourceFile = new File(pathname);
File destinationFile = new File(sourceFile.getParentFile(), newName);
if (destinationFile.exists()) {
destinationFile.delete();
}
sourceFile.renameTo(destinationFile);
}
/**
* Set the modified date of the next file to be written. The next call to writeFile will use this date.
*
* @param modifiedDate
* the date the file should be marked as modified
*/
@Override
public void setNextFileDate(Date modifiedDate) {
this.modifiedDate = modifiedDate;
}
/**
* Sets the global options. This can be useful to detect, whether local files should be made read-only.
*/
@Override
public void setGlobalOptions(GlobalOptions globalOptions) {
BugLog.getInstance().assertNotNull(globalOptions);
this.globalOptions = globalOptions;
transmitTextFilePreprocessor.setTempDir(globalOptions.getTempDir());
}
}