/**
Copyright (C) 2012 Delcyon, Inc.
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.delcyon.capo.resourcemanager.types;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.logging.Level;
import com.delcyon.capo.CapoApplication;
import com.delcyon.capo.Configuration.PREFERENCE;
import com.delcyon.capo.datastream.NullOutputStream;
import com.delcyon.capo.datastream.StreamEventFilterOutputStream;
import com.delcyon.capo.datastream.StreamEventListener;
import com.delcyon.capo.datastream.StreamUtil;
import com.delcyon.capo.resourcemanager.ResourceParameter;
import com.delcyon.capo.resourcemanager.ResourceType;
import com.delcyon.capo.resourcemanager.ResourceURI;
import com.delcyon.capo.resourcemanager.types.ContentMetaData.Attributes;
import com.delcyon.capo.resourcemanager.types.FileResourceType.Parameters;
import com.delcyon.capo.util.XMLSerializer;
import com.delcyon.capo.xml.cdom.CElement;
import com.delcyon.capo.xml.cdom.VariableContainer;
import com.delcyon.capo.xml.dom.ResourceDeclarationElement;
/**
* @author jeremiah
*/
public class FileResourceDescriptor extends AbstractResourceDescriptor implements StreamEventListener
{
private FileResourceContentMetaData contentMetaData = null;
private FileResourceContentMetaData actionMetaData = null;
private FileResourceContentMetaData outputMetaData = null;
private InputStream contentInputStream = null;
private CElement containerElement = null;
private File tempFile;
private State outputState = State.NONE;
OutputStream outputStream = null;
@Override
protected FileResourceContentMetaData buildResourceMetaData(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception
{
FileResourceContentMetaData contentMetaData = new FileResourceContentMetaData(getResourceURI().getBaseURI(),resourceParameters);
return contentMetaData;
}
@Override
public void setup(ResourceType resourceType, String resourceURI) throws Exception
{
super.setup(resourceType, resourceURI);
}
@Override
public void init(ResourceDeclarationElement declaringResourceElement,VariableContainer variableContainer, LifeCycle lifeCycle,boolean iterate, ResourceParameter... resourceParameters) throws Exception
{
super.init(declaringResourceElement,variableContainer, lifeCycle, iterate, resourceParameters);
//see if we have requested a parent directory
File parentDir = null;
String parentDirParameter = getVarValue(variableContainer, Parameters.PARENT_PROVIDED_DIRECTORY);
String rootDirParameter = getVarValue(variableContainer, Parameters.ROOT_DIR);
if (parentDirParameter != null && parentDirParameter.isEmpty() == false)
{
//this must be converted to a URI before processing or file treats it as a string and append things wierdly.
parentDir = new File(new URI(CapoApplication.getDataManager().getResourceDirectory(parentDirParameter).getResourceURI().getBaseURI()));
}
else if(rootDirParameter != null && rootDirParameter.isEmpty() == false)
{
parentDir = new File(rootDirParameter.replaceFirst("file:(//){0,1}", ""));
}
//Set default dir to capo dir on file requests, if one wasn't specified, and we have everything running
else if(CapoApplication.getDataManager() != null && CapoApplication.getDataManager().getResourceDirectory(PREFERENCE.CAPO_DIR.toString()) != null)
{
parentDir = new File(new URI(CapoApplication.getDataManager().getResourceDirectory(PREFERENCE.CAPO_DIR.toString()).getResourceURI().getBaseURI()));
}
//this lets us use custom relative URIs for files.
// file:filename and file:/filename file://filename
if (getResourceURI().isOpaque() || getResourceURI().getAuthority() != null || getResourceURI().getScheme() == null)
{
String uri = null;
if (parentDir == null)
{
uri = new File(getResourceURI().getBaseURI().replaceFirst("file:(//){0,1}", "")).toURI().toString();
}
else
{
//if the parent dir is already included in the URI, don't use it.
//and we're not specifically adding it with the parentDir parameter.
//The ROOT_DIR parameter will cascade down to child resource, so it has to be stripped out.
uri = new File(parentDir,getResourceURI().getBaseURI().replaceFirst("file:(//){0,1}", "")).toURI().toString();
if (uri.toString().startsWith(parentDir.toURI().toString()) && rootDirParameter != null && rootDirParameter.trim().isEmpty() == false)
{
uri = new File(getResourceURI().getBaseURI().replaceFirst("file:(//){0,1}", "")).toURI().toString();
}
}
if (uri.endsWith(File.separator))
{
uri = uri.substring(0, uri.length()-File.separator.length());
}
setResourceURI(new ResourceURI(uri));
}
else
{
CapoApplication.logger.log(Level.FINE, "Not rewriting URI: "+getResourceURI().getBaseURI());
}
}
@Override
public ContentMetaData getContentMetaData(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception
{
if (getResourceState() != State.OPEN && getResourceState() != State.STEPPING)
{
open(variableContainer,resourceParameters);
}
return contentMetaData;
}
@Override
protected void clearContent() throws Exception
{
contentMetaData = null;
if(contentInputStream != null)
{
contentInputStream.close();
}
containerElement = null;
}
@Override
public boolean next(VariableContainer variableContainer, ResourceParameter... resourceParameters) throws Exception
{
advanceState(State.OPEN, variableContainer, resourceParameters);
if(getResourceState() == State.OPEN)
{
setResourceState(State.STEPPING);
if(getResourceMetaData(variableContainer, resourceParameters).isContainer())
{
contentMetaData = new FileResourceContentMetaData(getResourceURI().getBaseURI());
containerElement = (CElement) XMLSerializer.export(CapoApplication.getDocumentBuilder().newDocument(), getResourceMetaData(variableContainer, resourceParameters));
}
else
{
contentMetaData = new FileResourceContentMetaData(getResourceURI().getBaseURI());
if(contentMetaData.exists())
{
contentInputStream = trackInputStream(contentMetaData.wrapInputStream(new FileInputStream(new File(new URI(getResourceURI().getBaseURI())))));
StreamUtil.readInputStreamIntoOutputStream(contentInputStream, new NullOutputStream());
contentInputStream = trackInputStream(contentMetaData.wrapInputStream(new FileInputStream(new File(new URI(getResourceURI().getBaseURI())))));
}
}
return true;
}
else if (getResourceState() == State.STEPPING)
{
setResourceState(State.OPEN);
return false;
}
return false;
}
//This is over written only to handle the special case of a directory, everything else actually goes through getInputStream()
@Override
public CElement readXML(VariableContainer variableContainer, ResourceParameter... resourceParameters) throws Exception
{
if(getResourceMetaData(variableContainer, resourceParameters).isContainer())
{
advanceState(State.STEPPING, variableContainer, resourceParameters);
return containerElement;
}
else
{
return super.readXML(variableContainer, resourceParameters);
}
}
@Override
public InputStream getInputStream(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception
{
advanceState(State.STEPPING, variableContainer, resourceParameters);
return contentInputStream;
}
@Override
public OutputStream getOutputStream(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception
{
advanceState(State.OPEN, variableContainer, resourceParameters);
addResourceParameters(variableContainer, resourceParameters);
String useTempString = getVarValue(variableContainer, Parameters.USE_TEMP);
boolean useTemp = false;
if(useTempString != null && useTempString.equalsIgnoreCase("true"))
{
useTemp = true;
}
File outputFile = new File(new URI(getResourceURI().getBaseURI()));
if (outputFile.exists() == false)
{
new File(outputFile.getParent()).mkdirs();
outputFile.createNewFile();
}
if(useTemp == true)
{
tempFile = File.createTempFile(outputFile.getName(), "part");
CapoApplication.logger.log(Level.INFO, "using temp file: "+tempFile+" for "+outputFile);
outputMetaData = new FileResourceContentMetaData(tempFile.toURI().toString());
outputMetaData.clearAttributes();
outputStream = trackOutputStream(outputMetaData.wrapOutputStream(new FileOutputStream(tempFile)));
}
else
{
outputMetaData = new FileResourceContentMetaData(getResourceURI().getBaseURI());
outputMetaData.clearAttributes();
outputStream = trackOutputStream(outputMetaData.wrapOutputStream(new FileOutputStream(outputFile)));
}
StreamEventFilterOutputStream streamEventFilterOutputStream = new StreamEventFilterOutputStream(outputStream);
streamEventFilterOutputStream.addStreamEventListener(this);
outputStream = streamEventFilterOutputStream;
outputState = State.OPEN;
return outputStream;
}
@Override
public ContentMetaData getOutputMetaData(VariableContainer variableContainer, ResourceParameter... resourceParameters) throws Exception
{
while(outputState == State.OPEN)
{
CapoApplication.logger.log(Level.FINE, "waiting for output to close...");
Thread.sleep(500);
}
//call this so that the attribute vectors get processed
outputMetaData.getValue(ContentMetaData.Attributes.container);
return outputMetaData;
}
@Override
public void processStreamEvent(StreamEvent streamEvent) throws IOException
{
if(streamEvent == StreamEvent.CLOSED)
{
outputStream = null;
outputState = State.CLOSED;
}
}
@Override
public void close(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception
{
super.close(variableContainer,resourceParameters);
if (contentMetaData != null)
{
contentMetaData.refresh();
}
if(contentInputStream != null)
{
try
{
contentInputStream.close();
contentInputStream = null;
}
catch (IOException ioException)
{
CapoApplication.logger.log(Level.WARNING, "Error closing stream", ioException);
}
}
if(outputStream != null)
{
try
{
File localTempFile = null;
if(tempFile != null)
{
localTempFile = tempFile;
tempFile = null;
}
outputStream.close();
outputStream = null;
if(localTempFile != null)
{
CapoApplication.logger.log(Level.WARNING, "ResourceDescriptor closed before outputStream closed, deleting temp file:"+localTempFile);
localTempFile.delete();
}
}
catch (IOException ioException)
{
CapoApplication.logger.log(Level.WARNING, "Error closing stream", ioException);
}
}
}
public void create() throws Exception
{
File file = new File(new URI(getResourceURI().getBaseURI()));
if (file.exists() == false)
{
String path = file.getCanonicalPath();
path = path.substring(0, path.lastIndexOf(File.separator+file.getName()));
File dirs = new File(path);
if (dirs.exists() == false)
{
CapoApplication.logger.log(Level.INFO, "Creating Directory: "+dirs.getCanonicalPath());
if (dirs.mkdirs() == false)
{
throw new Exception("Couldn't create: "+dirs.getCanonicalPath());
}
}
file.createNewFile();
}
}
@Override
public StreamFormat[] getSupportedStreamFormats(StreamType streamType)
{
if (streamType == StreamType.INPUT)
{
return new StreamFormat[]{StreamFormat.STREAM};
}
else if(streamType == StreamType.OUTPUT)
{
return new StreamFormat[]{StreamFormat.STREAM};
}
else
{
return null;
}
}
@Override
public StreamType[] getSupportedStreamTypes()
{
return new StreamType[]{StreamType.INPUT,StreamType.OUTPUT};
}
@Override
public Action[] getSupportedActions()
{
return new Action[]{Action.CREATE,Action.DELETE};
}
@Override
public boolean performAction(VariableContainer variableContainer,Action action,ResourceParameter... resourceParameters) throws Exception
{
super.addResourceParameters(variableContainer, resourceParameters);
URI uri = new URI(getResourceURI().getBaseURI());
if (uri.isAbsolute() == false)
{
CapoApplication.logger.log(Level.WARNING, "URI isn't absolute! "+uri);
}
File file = new File(uri);
boolean success = false;
if (action == Action.CREATE)
{
if (file.exists() == false)
{
String containerFlag = getVarValue(variableContainer, DefaultParameters.CONTAINER);
if (containerFlag != null && containerFlag.equalsIgnoreCase("true"))
{
CapoApplication.logger.log(Level.INFO, "Creating Directory: "+file.getCanonicalPath());
success = file.mkdirs();
}
else
{
String path = file.getCanonicalPath();
path = path.substring(0, path.lastIndexOf(File.separator+file.getName()));
File dirs = new File(path);
if (dirs.exists() == false)
{
CapoApplication.logger.log(Level.INFO, "Creating Directory: "+dirs.getCanonicalPath());
dirs.mkdirs();
}
success = file.createNewFile();
}
}
}
else if (action == Action.DELETE)
{
if (file.exists())
{
success = delete(file);
}
else
{
success = true;
}
}
else if (action == Action.SET_ATTRIBUTE && resourceParameters.length > 0)
{
Attributes attribute = Attributes.valueOf(resourceParameters[0].getName());
if (file.exists())
{
switch (attribute)
{
case lastModified:
success = file.setLastModified(Long.parseLong(resourceParameters[0].getValue()));
break;
case executable:
success = file.setExecutable(Boolean.parseBoolean(resourceParameters[0].getValue()));
break;
case readable:
success = file.setReadable(Boolean.parseBoolean(resourceParameters[0].getValue()));
break;
case writeable:
success = file.setWritable(Boolean.parseBoolean(resourceParameters[0].getValue()));
break;
default:
success = false;
break;
}
}
else
{
success = false;
}
}
else if (action == Action.COMMIT)
{
try
{
if(tempFile != null && tempFile.exists())
{
if(CapoApplication.logger.isLoggable(Level.FINER))
{
Thread.dumpStack();
}
File outputFile = new File(new URI(getResourceURI().getBaseURI()));
if (outputFile.exists() == false)
{
new File(outputFile.getParent()).mkdirs();
outputFile.createNewFile();
}
CapoApplication.logger.log(Level.INFO, "COMMITING Rename of "+tempFile+" to: "+outputFile+".");
if(tempFile.renameTo(outputFile))
{
CapoApplication.logger.log(Level.INFO, "Rename of temp file to: "+outputFile+" was successful.");
}
else
{
CapoApplication.logger.log(Level.WARNING, "Rename of temp file to: "+outputFile+" failed.");
}
tempFile = null;
}
} catch (Exception exception)
{
exception.printStackTrace();
return false;
}
return true;
}
else if (action == Action.ROLLBACK)
{
try
{
if(tempFile != null && tempFile.exists())
{
CapoApplication.logger.log(Level.WARNING, "ABORTING Rename of "+tempFile+".");
tempFile.delete();
tempFile = null;
if(outputStream != null)
{
outputStream.close();
}
}
} catch (Exception exception)
{
exception.printStackTrace();
return false;
}
return true;
}
if (success == true)
{
refreshResourceMetaData(variableContainer,resourceParameters);
}
return success;
}
private boolean delete(File file) throws Exception
{
if (file.exists())
{
if(file.isDirectory())
{
//see if this is a symlink, if so, it's not actual a directory, so just delete it.
if(file.getAbsolutePath().equals(file.getCanonicalPath()) == false)
{
return file.delete();
}
else
{
File[] children = file.listFiles();
for (File child : children)
{
delete(child);
}
}
}
return file.delete();
}
else
{
//gotta love symlinks, where they can exist = false, but still be deleted.
//delete will not throw an exception if the file doesn't exist, so this is pretty safe.
return file.delete();
}
}
}