/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.ha.deploy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.res.StringManager;
/**
* This factory is used to read files and write files by splitting them up into
* smaller messages. So that entire files don't have to be read into memory.
* <BR>
* The factory can be used as a reader or writer but not both at the same time.
* When done reading or writing the factory will close the input or output
* streams and mark the factory as closed. It is not possible to use it after
* that. <BR>
* To force a cleanup, call cleanup() from the calling object. <BR>
* This class is not thread safe.
*
* @version 1.0
*/
public class FileMessageFactory {
/*--Static Variables----------------------------------------*/
private static final Log log = LogFactory.getLog(FileMessageFactory.class);
private static final StringManager sm = StringManager.getManager(FileMessageFactory.class);
/**
* The number of bytes that we read from file
*/
public static final int READ_SIZE = 1024 * 10; //10kb
/**
* The file that we are reading/writing
*/
protected final File file;
/**
* True means that we are writing with this factory. False means that we are
* reading with this factory
*/
protected final boolean openForWrite;
/**
* Once the factory is used, it cannot be reused.
*/
protected boolean closed = false;
/**
* When openForWrite=false, the input stream is held by this variable
*/
protected FileInputStream in;
/**
* When openForWrite=true, the output stream is held by this variable
*/
protected FileOutputStream out;
/**
* The number of messages we have written
*/
protected int nrOfMessagesProcessed = 0;
/**
* The total size of the file
*/
protected long size = 0;
/**
* The total number of packets that we split this file into
*/
protected long totalNrOfMessages = 0;
/**
* The number of the last message processed. Message IDs are 1 based.
*/
protected AtomicLong lastMessageProcessed = new AtomicLong(0);
/**
* Messages received out of order are held in the buffer until required. If
* everything is worked as expected, messages will spend very little time in
* the buffer.
*/
protected final Map<Long, FileMessage> msgBuffer = new ConcurrentHashMap<>();
/**
* The bytes that we hold the data in, not thread safe.
*/
protected byte[] data = new byte[READ_SIZE];
/**
* Flag that indicates if a thread is writing messages to disk. Access to
* this flag must be synchronised.
*/
protected boolean isWriting = false;
/**
* The time this instance was created. (in milliseconds)
*/
protected long creationTime = 0;
/**
* The maximum valid time(in seconds) from creationTime.
*/
protected int maxValidTime = -1;
/**
* Private constructor, either instantiates a factory to read or write. <BR>
* When openForWrite==true, then a the file, f, will be created and an
* output stream is opened to write to it. <BR>
* When openForWrite==false, an input stream is opened, the file has to
* exist.
*
* @param f
* File - the file to be read/written
* @param openForWrite
* boolean - true means we are writing to the file, false means
* we are reading from the file
* @throws FileNotFoundException -
* if the file to be read doesn't exist
* @throws IOException -
* if the system fails to open input/output streams to the file
* or if it fails to create the file to be written to.
*/
private FileMessageFactory(File f, boolean openForWrite)
throws FileNotFoundException, IOException {
this.file = f;
this.openForWrite = openForWrite;
if (log.isDebugEnabled())
log.debug("open file " + f + " write " + openForWrite);
if (openForWrite) {
if (!file.exists())
if (!file.createNewFile()) {
throw new IOException(sm.getString("fileNewFail", file));
}
out = new FileOutputStream(f);
} else {
size = file.length();
totalNrOfMessages = (size / READ_SIZE) + 1;
in = new FileInputStream(f);
}//end if
creationTime = System.currentTimeMillis();
}
/**
* Creates a factory to read or write from a file. When opening for read,
* the readMessage can be invoked, and when opening for write the
* writeMessage can be invoked.
*
* @param f
* File - the file to be read or written
* @param openForWrite
* boolean - true, means we are writing to the file, false means
* we are reading from it
* @throws FileNotFoundException -
* if the file to be read doesn't exist
* @throws IOException -
* if it fails to create the file that is to be written
* @return FileMessageFactory
*/
public static FileMessageFactory getInstance(File f, boolean openForWrite)
throws FileNotFoundException, IOException {
return new FileMessageFactory(f, openForWrite);
}
/**
* Reads file data into the file message and sets the size, totalLength,
* totalNrOfMsgs and the message number <BR>
* If EOF is reached, the factory returns null, and closes itself, otherwise
* the same message is returned as was passed in. This makes sure that not
* more memory is ever used. To remember, neither the file message or the
* factory are thread safe. dont hand off the message to one thread and read
* the same with another.
*
* @param f
* FileMessage - the message to be populated with file data
* @throws IllegalArgumentException -
* if the factory is for writing or is closed
* @throws IOException -
* if a file read exception occurs
* @return FileMessage - returns the same message passed in as a parameter,
* or null if EOF
*/
public FileMessage readMessage(FileMessage f)
throws IllegalArgumentException, IOException {
checkState(false);
int length = in.read(data);
if (length == -1) {
cleanup();
return null;
} else {
f.setData(data, length);
f.setTotalNrOfMsgs(totalNrOfMessages);
f.setMessageNumber(++nrOfMessagesProcessed);
return f;
}//end if
}
/**
* Writes a message to file. If (msg.getMessageNumber() ==
* msg.getTotalNrOfMsgs()) the output stream will be closed after writing.
*
* @param msg
* FileMessage - message containing data to be written
* @throws IllegalArgumentException -
* if the factory is opened for read or closed
* @throws IOException -
* if a file write error occurs
* @return returns true if the file is complete and outputstream is closed,
* false otherwise.
*/
public boolean writeMessage(FileMessage msg)
throws IllegalArgumentException, IOException {
if (!openForWrite)
throw new IllegalArgumentException(
"Can't write message, this factory is reading.");
if (log.isDebugEnabled())
log.debug("Message " + msg + " data " + HexUtils.toHexString(msg.getData())
+ " data length " + msg.getDataLength() + " out " + out);
if (msg.getMessageNumber() <= lastMessageProcessed.get()) {
// Duplicate of message already processed
log.warn("Receive Message again -- Sender ActTimeout too short [ name: "
+ msg.getContextName()
+ " war: "
+ msg.getFileName()
+ " data: "
+ HexUtils.toHexString(msg.getData())
+ " data length: " + msg.getDataLength() + " ]");
return false;
}
FileMessage previous =
msgBuffer.put(Long.valueOf(msg.getMessageNumber()), msg);
if (previous !=null) {
// Duplicate of message not yet processed
log.warn("Receive Message again -- Sender ActTimeout too short [ name: "
+ msg.getContextName()
+ " war: "
+ msg.getFileName()
+ " data: "
+ HexUtils.toHexString(msg.getData())
+ " data length: " + msg.getDataLength() + " ]");
return false;
}
FileMessage next = null;
synchronized (this) {
if (!isWriting) {
next = msgBuffer.get(Long.valueOf(lastMessageProcessed.get() + 1));
if (next != null) {
isWriting = true;
} else {
return false;
}
} else {
return false;
}
}
while (next != null) {
out.write(next.getData(), 0, next.getDataLength());
lastMessageProcessed.incrementAndGet();
out.flush();
if (next.getMessageNumber() == next.getTotalNrOfMsgs()) {
out.close();
cleanup();
return true;
}
synchronized(this) {
next =
msgBuffer.get(Long.valueOf(lastMessageProcessed.get() + 1));
if (next == null) {
isWriting = false;
}
}
}
return false;
}//writeMessage
/**
* Closes the factory, its streams and sets all its references to null
*/
public void cleanup() {
if (in != null)
try {
in.close();
} catch (Exception ignore) {
}
if (out != null)
try {
out.close();
} catch (Exception ignore) {
}
in = null;
out = null;
size = 0;
closed = true;
data = null;
nrOfMessagesProcessed = 0;
totalNrOfMessages = 0;
msgBuffer.clear();
lastMessageProcessed = null;
}
/**
* Check to make sure the factory is able to perform the function it is
* asked to do. Invoked by readMessage/writeMessage before those methods
* proceed.
*
* @param openForWrite The value to check
* @throws IllegalArgumentException if the state is not the expected one
*/
protected void checkState(boolean openForWrite)
throws IllegalArgumentException {
if (this.openForWrite != openForWrite) {
cleanup();
if (openForWrite)
throw new IllegalArgumentException(
"Can't write message, this factory is reading.");
else
throw new IllegalArgumentException(
"Can't read message, this factory is writing.");
}
if (this.closed) {
cleanup();
throw new IllegalArgumentException("Factory has been closed.");
}
}
/**
* Example usage.
*
* @param args
* String[], args[0] - read from filename, args[1] write to
* filename
* @throws Exception An error occurred
*/
public static void main(String[] args) throws Exception {
System.out
.println("Usage: FileMessageFactory fileToBeRead fileToBeWritten");
System.out
.println("Usage: This will make a copy of the file on the local file system");
FileMessageFactory read = getInstance(new File(args[0]), false);
FileMessageFactory write = getInstance(new File(args[1]), true);
FileMessage msg = new FileMessage(null, args[0], args[0]);
msg = read.readMessage(msg);
if (msg == null) {
System.out.println("Empty input file : " + args[0]);
return;
}
System.out.println("Expecting to write " + msg.getTotalNrOfMsgs()
+ " messages.");
int cnt = 0;
while (msg != null) {
write.writeMessage(msg);
cnt++;
msg = read.readMessage(msg);
}//while
System.out.println("Actually wrote " + cnt + " messages.");
}///main
public File getFile() {
return file;
}
public boolean isValid() {
if (maxValidTime > 0) {
long timeNow = System.currentTimeMillis();
int timeIdle = (int) ((timeNow - creationTime) / 1000L);
if (timeIdle > maxValidTime) {
cleanup();
if (file.exists()) file.delete();
return false;
}
}
return true;
}
public int getMaxValidTime() {
return maxValidTime;
}
public void setMaxValidTime(int maxValidTime) {
this.maxValidTime = maxValidTime;
}
}