/**
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.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Level;
import com.delcyon.capo.CapoApplication;
import com.delcyon.capo.annotations.XmlMappedArrays;
import com.delcyon.capo.datastream.NullOutputStream;
import com.delcyon.capo.datastream.OutputStreamAttributeFilterProvider;
import com.delcyon.capo.datastream.StreamUtil;
import com.delcyon.capo.datastream.stream_attribute_filter.InputStreamAttributeFilterProvider;
import com.delcyon.capo.datastream.stream_attribute_filter.MD5FilterInputStream;
import com.delcyon.capo.datastream.stream_attribute_filter.SizeFilterInputStream;
import com.delcyon.capo.datastream.stream_attribute_filter.StreamAttributeFilter;
import com.delcyon.capo.resourcemanager.ContentFormatType;
import com.delcyon.capo.resourcemanager.ResourceParameter;
import com.delcyon.capo.resourcemanager.ResourceURI;
import com.delcyon.capo.util.CloneControl;
import com.delcyon.capo.util.CloneControl.Clone;
import com.delcyon.capo.util.ControlledClone;
import com.delcyon.capo.util.ReflectionUtility;
import com.delcyon.capo.util.ToStringControl;
import com.delcyon.capo.util.ToStringControl.Control;
/**
* @author jeremiah
*
*/
@SuppressWarnings("unchecked")
@ToStringControl(control=Control.exclude,modifiers=Modifier.STATIC)
@XmlMappedArrays(name="MetaDataAttributes",keys="attributeKeys", values="attributeValues")
public abstract class AbstractContentMetaData implements ContentMetaData, ControlledClone
{
//this is to lower memory usage on common attribute values, slows things a bit, but can drastically reduce memory usage by using constants
private static final String[] CommonStrings = new String[]{"0","false","true"};
private static LinkedList<Class> inputStreamAttributeFilterLinkedList = null;
static
{
inputStreamAttributeFilterLinkedList = new LinkedList<Class>();
Set<String> inputStreamAttributeFilterProviderSet = CapoApplication.getAnnotationMap().get(InputStreamAttributeFilterProvider.class.getCanonicalName());
if (inputStreamAttributeFilterProviderSet != null) //this is only null during testing
{
for (String className : inputStreamAttributeFilterProviderSet)
{
try
{
Class inputStreamAttributeFilterProviderClass = Class.forName(className);
InputStreamAttributeFilterProvider inputStreamAttributeFilterProvider = (InputStreamAttributeFilterProvider) inputStreamAttributeFilterProviderClass.getAnnotation(InputStreamAttributeFilterProvider.class);
if (FilterInputStream.class.isAssignableFrom(inputStreamAttributeFilterProviderClass) == false)
{
CapoApplication.logger.log(Level.WARNING, inputStreamAttributeFilterProviderClass.getCanonicalName()+" is not a filterInputStream. SKIPPING");
}
else if (StreamAttributeFilter.class.isAssignableFrom(StreamAttributeFilter.class) == false)
{
CapoApplication.logger.log(Level.WARNING, inputStreamAttributeFilterProviderClass.getCanonicalName()+" is not a StreamAttributeFilter. SKIPPING");
}
else
{
inputStreamAttributeFilterLinkedList.add(inputStreamAttributeFilterProviderClass);
CapoApplication.logger.log(Level.CONFIG, "Loaded InputStreamAttributeFilterProvider "+inputStreamAttributeFilterProvider.name()+" from "+className);
}
} catch (ClassNotFoundException classNotFoundException)
{
CapoApplication.logger.log(Level.WARNING, "Error getting StreamAttributeFilter providers",classNotFoundException);
}
}
}
}
private static Vector<Class> outputStreamAttributeFilterVector = null;
static
{
outputStreamAttributeFilterVector = new Vector<Class>();
Set<String> outputStreamAttributeFilterProviderSet = CapoApplication.getAnnotationMap().get(OutputStreamAttributeFilterProvider.class.getCanonicalName());
if (outputStreamAttributeFilterProviderSet != null) //this is only null during testing, since we have built-in providers
{
for (String className : outputStreamAttributeFilterProviderSet)
{
try
{
Class outputStreamAttributeFilterProviderClass = Class.forName(className);
OutputStreamAttributeFilterProvider outputStreamAttributeFilterProvider = (OutputStreamAttributeFilterProvider) outputStreamAttributeFilterProviderClass.getAnnotation(OutputStreamAttributeFilterProvider.class);
if (FilterOutputStream.class.isAssignableFrom(outputStreamAttributeFilterProviderClass) == false)
{
CapoApplication.logger.log(Level.WARNING, outputStreamAttributeFilterProviderClass.getCanonicalName()+" is not a FilterOutputStream. SKIPPING");
}
else if (StreamAttributeFilter.class.isAssignableFrom(outputStreamAttributeFilterProviderClass) == false)
{
CapoApplication.logger.log(Level.WARNING, outputStreamAttributeFilterProviderClass.getCanonicalName()+" is not a StreamAttributeFilter. SKIPPING");
}
else
{
outputStreamAttributeFilterVector.add(outputStreamAttributeFilterProviderClass);
CapoApplication.logger.log(Level.CONFIG, "Loaded OutputStreamAttributeFilterProvider "+outputStreamAttributeFilterProvider.name()+" from "+className);
}
} catch (ClassNotFoundException classNotFoundException)
{
CapoApplication.logger.log(Level.WARNING, "Error getting OutputStreamAttributeFilter providers",classNotFoundException);
}
}
}
}
private boolean includeStreamAttributes = false;
private boolean isInitialized = false;
private ResourceURI resourceURI = null;
@CloneControl(filter=Clone.exclude)
private transient LinkedList<StreamAttributeFilter> streamAttributeFilterLinkedList = new LinkedList<StreamAttributeFilter>();
protected LinkedList<ContentMetaData> childContentMetaDataLinkedList = new LinkedList<ContentMetaData>();
private String[] attributeKeys = null;
private String[] attributeValues = null;
private boolean attributesLoaded = false;
@Override
public void preClone(Object parentClonedObject, Object clonedObject) throws Exception {};
@Override
public void postClone(Object parentClonedObject, Object clonedObject) throws Exception
{
internAttributes();
}
//minimize memory by making sure attribute names, and common values are interned
private void internAttributes()
{
initializeAttributeStorage();
for (int index = 0; index < attributeKeys.length; index++)
{
attributeKeys[index] = attributeKeys[index].intern();
}
for (String commonString : CommonStrings)
{
for (int index = 0; index < attributeKeys.length; index++)
{
if(commonString.equals(attributeValues[index]))
{
attributeValues[index] = commonString;
break;
}
}
}
}
private void initializeAttributeStorage()
{
if(attributeKeys == null)
{
//make sure we include initial room for the stream attributes
boolean _includeStreamAttributes = includeStreamAttributes;
includeStreamAttributes = true;
List<String> attributes = getSupportedAttributes();
includeStreamAttributes = _includeStreamAttributes;
attributeKeys = new String[attributes.size()];
attributeValues = new String[attributeKeys.length];
for (int index = 0; index < attributeKeys.length; index++)
{
attributeKeys[index] = attributes.get(index).intern();
}
}
}
/**
* this will try to initialize md5 and length fields by reading the input stream
* If you want to use a byte[] wrap it in a ByteArrayInputStream
* This WILL close the stream
* @param inputStream
* @throws Exception
*/
protected byte[] readInputStream(InputStream inputStream, boolean returnContent) throws Exception
{
this.includeStreamAttributes = true;
boolean useHugeBuffer = false;
OutputStream outputStream = null;
if(returnContent == true)
{
outputStream = new ByteArrayOutputStream();
}
else
{
useHugeBuffer = true;
outputStream = new NullOutputStream();
}
inputStream = wrapInputStream(inputStream);
if(useHugeBuffer == false)
{
StreamUtil.readInputStreamIntoOutputStream(inputStream, outputStream);
}
else
{
StreamUtil.readInputStreamIntoOutputStream(inputStream, outputStream,1024000);
}
inputStream.close();
loadAttributes(); //once we've read the file, load up he attribute map with any stream attributes.
this.isInitialized = true;
if (returnContent == true)
{
return ((ByteArrayOutputStream) outputStream).toByteArray();
}
else
{
return null;
}
}
/**
* this will try to initialize md5 and length fields by reading the input stream
* If you want to use a byte[] wrap it in a ByteArrayInputStream
* This does NOT close the stream
* @param inputStream
* @throws Exception
*/
protected InputStream wrapInputStream(InputStream inputStream) throws Exception
{
this.includeStreamAttributes = true;
streamAttributeFilterLinkedList.clear();
for (Class inputStreamAttributeFilterProviderClass : inputStreamAttributeFilterLinkedList)
{
inputStream = (FilterInputStream) inputStreamAttributeFilterProviderClass.getConstructor(InputStream.class).newInstance(inputStream);
streamAttributeFilterLinkedList.add((StreamAttributeFilter) inputStream);
}
return inputStream;
}
protected OutputStream wrapOutputStream(OutputStream outputStream) throws Exception
{
this.includeStreamAttributes = true;
streamAttributeFilterLinkedList.clear();
setValue(MD5FilterInputStream.ATTRIBUTE_NAME,null);
setValue(SizeFilterInputStream.ATTRIBUTE_NAME,null);
setValue(ContentFormatType.ATTRIBUTE_NAME,null);
for (Class outputStreamAttributeFilterProviderClass : outputStreamAttributeFilterVector)
{
outputStream = (FilterOutputStream) outputStreamAttributeFilterProviderClass.getConstructor(OutputStream.class).newInstance(outputStream);
streamAttributeFilterLinkedList.add((StreamAttributeFilter) outputStream);
}
return outputStream;
}
@Override
public boolean isDynamic()
{
if(includeStreamAttributes == true)
{
return false;
}
else
{
return true;
}
}
@Override
public boolean isInitialized()
{
return this.isInitialized;
}
@Override
public void setInitialized(boolean isInitialized)
{
this.isInitialized = isInitialized;
}
@Override
public void clearAttributes()
{
if(attributesLoaded != false)
{
attributeValues = new String[attributeKeys.length];
attributesLoaded = false;
}
}
private int getAttributeIndex(String attributeName)
{
initializeAttributeStorage();
attributeName = attributeName.intern();
for (int index = 0; index < attributeKeys.length; index++)
{
if(attributeKeys[index] == attributeName)
{
return index;
}
}
//somewhere, somehow it's possible for keys to be un-interned, so if we didn't find the attribute key, we use the slower code, and if we then find it, something has been messing with us and we intern everything
for (int index = 0; index < attributeKeys.length; index++)
{
if(attributeKeys[index].equals(attributeName))
{
internAttributes();
return index;
}
}
return -1;
}
@Override
public boolean hasAttribute(String attributeName)
{
if(getAttributeIndex(attributeName) >= 0)
{
return true;
}
else
{
return false;
}
}
@Override
public boolean areDynamicAttributeLoaded()
{
return attributesLoaded;
}
private void loadAttributes()
{
if (attributesLoaded == false)
{
attributesLoaded = true;
for (StreamAttributeFilter streamAttributeFilter : streamAttributeFilterLinkedList)
{
int attributeIndex = getAttributeIndex(streamAttributeFilter.getName());
if (attributeIndex < 0 || (attributeValues[attributeIndex] == null && streamAttributeFilter.getValue() != null))
{
setValue(streamAttributeFilter.getName(), streamAttributeFilter.getValue());
}
}
//once we've gotten the values, release the memory used by the list
streamAttributeFilterLinkedList.clear();
setValue(Attributes.path.toString(), getResourceURI().getPath());
setValue(Attributes.uri.toString(), getResourceURI().getBaseURI());
}
}
public ContentFormatType getContentFormatType()
{
if(isInitialized() == false)
{
init();
}
loadAttributes();
String contentFormatTypeString = getValue(ContentFormatType.ATTRIBUTE_NAME);
if (contentFormatTypeString != null)
{
return ContentFormatType.valueOf(contentFormatTypeString);
}
else
{
return null;
}
}
public void setContentFormatType(ContentFormatType contentFormatType)
{
setValue(ContentFormatType.ATTRIBUTE_NAME, contentFormatType.toString());
}
public String getMD5()
{
if(isInitialized() == false)
{
init();
}
loadAttributes();
return getValue(MD5FilterInputStream.ATTRIBUTE_NAME);
}
public void setMD5(String md5)
{
setValue(MD5FilterInputStream.ATTRIBUTE_NAME, md5);
}
public Long getLength()
{
if(isInitialized() == false)
{
init();
}
loadAttributes();
String length = getValue(SizeFilterInputStream.ATTRIBUTE_NAME);
if (length.matches("\\d+"))
{
return Long.parseLong(length);
}
return null;
}
public void setLength(Long length)
{
setValue(SizeFilterInputStream.ATTRIBUTE_NAME, length+"");
}
@Override
public String getValue(String name) throws RuntimeException
{
if (isInitialized == false)
{
init();
}
initializeAttributeStorage();
loadAttributes();
int attributeIndex = getAttributeIndex(name);
if(attributeIndex >= 0)
{
return attributeValues[attributeIndex];
}
return null;
}
abstract public void init() throws RuntimeException;
@Override
public String getValue(Enum name) throws RuntimeException
{
return getValue(name.toString());
}
@Override
public void setValue(String name, String value)
{
name = name.intern();
for (String commonString : CommonStrings)
{
if(commonString.equals(value))
{
value = commonString;
break;
}
}
initializeAttributeStorage();
boolean foundAttribute = false;
for (int index = 0; index < attributeKeys.length; index++)
{
if(attributeKeys[index] == name)
{
attributeValues[index] = value;
foundAttribute = true;
break;
}
}
if (foundAttribute == false)
{
//expand attribute storage to handle new value
String[] newAttributeKeys = new String[attributeKeys.length+1];
for (int index = 0; index < attributeKeys.length; index++)
{
//check to see if we need to intern the attributes
if(attributeKeys[index].intern() == name)
{
//looks like we do, so intern them, then set our value, since we should have picked it up the first time, and just finish up.
internAttributes();
attributeValues[index] = value;
newAttributeKeys = null;
return;
}
newAttributeKeys[index] = attributeKeys[index];
}
newAttributeKeys[attributeKeys.length] = name;
String[] newAttributeValues = new String[attributeValues.length+1];
for (int index = 0; index < attributeValues.length; index++)
{
newAttributeValues[index] = attributeValues[index];
}
newAttributeValues[attributeValues.length] = value;
//set new arrays into place
attributeValues = newAttributeValues;
attributeKeys = newAttributeKeys;
}
}
/**
* Convince Method
* @param enumclass
* @param object
*/
public void setValue(Enum enumclass, Object object)
{
if (object != null)
{
setValue(enumclass.toString(), object.toString());
}
else
{
setValue(enumclass.toString(), null);
}
}
public void setValue(String name, Object object)
{
if (object != null)
{
setValue(name, object.toString());
}
else
{
setValue(name, null);
}
}
@Override
public String toString()
{
return ReflectionUtility.processToString(this);
}
@Override
public boolean isSupported(String attributeName)
{
List<String> supportedAttributeList = getSupportedAttributes();
if(includeStreamAttributes == true)
{
for (Class inputStreamAttributeFilterProviderClass : inputStreamAttributeFilterLinkedList)
{
supportedAttributeList.add(((InputStreamAttributeFilterProvider) inputStreamAttributeFilterProviderClass.getAnnotation(InputStreamAttributeFilterProvider.class)).name());
}
}
return supportedAttributeList.contains(attributeName);
}
@Override
public List<String> getSupportedAttributes()
{
LinkedList<String> supportedAttributeLinkedList = new LinkedList<String>();
Enum[] supportedAttributeList = getAdditionalSupportedAttributes();
for (Enum attributeEnum : supportedAttributeList)
{
supportedAttributeLinkedList.add(attributeEnum.toString());
}
if(includeStreamAttributes == true)
{
for (Class inputStreamAttributeFilterProviderClass : inputStreamAttributeFilterLinkedList)
{
String streamAttributeName = ((InputStreamAttributeFilterProvider) inputStreamAttributeFilterProviderClass.getAnnotation(InputStreamAttributeFilterProvider.class)).name();
if(supportedAttributeLinkedList.contains(streamAttributeName) == false)
{
supportedAttributeLinkedList.add(streamAttributeName);
}
}
}
supportedAttributeLinkedList.add(Attributes.path.toString());
return supportedAttributeLinkedList;
}
@Override
public List<ContentMetaData> getContainedResources()
{
if(isInitialized == false)
{
init();
}
return childContentMetaDataLinkedList;
}
@Override
public void addContainedResource(ContentMetaData contentMetaData)
{
childContentMetaDataLinkedList.add(contentMetaData);
}
public abstract Enum[] getAdditionalSupportedAttributes();
public int getIntValue(Enum name, int defaultValue, ResourceParameter... resourceParameters)
{
return ContentMetaData.getIntValue(name, defaultValue, resourceParameters);
}
public boolean getBoolean(Enum name, boolean defaultValue, ResourceParameter... resourceParameters)
{
for (ResourceParameter resourceParameter : resourceParameters)
{
if (resourceParameter.getName().equals(name.toString()))
{
if(resourceParameter.getValue().equalsIgnoreCase("true"))
{
return true;
}
}
}
return defaultValue;
}
public String getString(Enum name, String defaultValue, ResourceParameter... resourceParameters)
{
for (ResourceParameter resourceParameter : resourceParameters)
{
if (resourceParameter.getName().equals(name.toString()))
{
return resourceParameter.getValue();
}
}
return defaultValue;
}
public void setResourceURI(ResourceURI resourceURI)
{
this.resourceURI = resourceURI;
}
@Override
public ResourceURI getResourceURI()
{
return resourceURI;
}
}