package au.net.causal.projo.prefs.transform;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.AbstractList;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.supercsv.io.CsvListReader;
import org.supercsv.io.CsvListWriter;
import org.supercsv.prefs.CsvPreference;
import au.net.causal.projo.bind.MetadataUtils;
import au.net.causal.projo.prefs.DataTypeSupport;
import au.net.causal.projo.prefs.PreferenceKeyMetadata;
import au.net.causal.projo.prefs.PreferencesException;
import au.net.causal.projo.prefs.TransformDataTypeSupportChain;
import au.net.causal.projo.prefs.TransformGetChain;
import au.net.causal.projo.prefs.TransformPutChain;
import au.net.causal.projo.prefs.TransformRemoveChain;
import au.net.causal.projo.prefs.TransformResult;
import au.net.causal.projo.prefs.UnsupportedDataTypeException;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.reflect.TypeToken;
/**
* Converts lists of values to a comma-separated string if the store does not support storing lists natively.
* <p>
*
* Requires that the list component's data type can be converted to string form, or else this transformer will not declare support for the specific
* list data type.
*
* @author prunge
*/
public class ListTransformer implements PreferenceTransformer
{
private static final Logger log = LoggerFactory.getLogger(ListTransformer.class);
private final CsvPreference csvFormat = new CsvPreference.Builder(
(char)CsvPreference.STANDARD_PREFERENCE.getQuoteChar(),
CsvPreference.STANDARD_PREFERENCE.getDelimiterChar(),
"" /* end of line symbols */).build();
@Override
public <T> TransformResult<T> applyGet(String key, PreferenceKeyMetadata<T> keyMetadata, TransformGetChain chain) throws PreferencesException
{
if (!isSupported(keyMetadata, chain))
return(null);
TypeToken<?> elementType = collectionElementType(keyMetadata.getDataType());
PreferenceKeyMetadata<?> elementKeyMetadata = keyMetadata.withDataType(elementType);
//element type is appropriate, so it safe
ElementReadingEndpoint endpoint = new ElementReadingEndpoint(chain);
Collection<Object> col = createAppropriateCollectionType(keyMetadata);
Object elementValue;
int index = 0;
do
{
endpoint.setIndex(index);
elementValue = chain.getValueWithRestartedChain(key, (PreferenceKeyMetadata)elementKeyMetadata, endpoint);
if (elementValue != null)
col.add(elementValue);
index++;
}
while (elementValue != null);
return(new TransformResult<>((T)col));
}
private Collection<Object> createAppropriateCollectionType(PreferenceKeyMetadata<?> keyMetadata)
throws PreferencesException
{
Class<?> listType = keyMetadata.getDataType().getRawType();
//Default to array list
if (List.class.equals(listType) || AbstractList.class.equals(listType) || Collection.class.equals(listType))
return(new ArrayList<>());
//Default to linked hash set for sets
if (Set.class.equals(listType) || AbstractSet.class.equals(listType))
return(new LinkedHashSet<>());
//Sorted sets we'll use tree set
if (SortedSet.class.equals(listType) || NavigableSet.class.equals(listType))
return(new TreeSet<>());
else if (!listType.isInterface() && !Modifier.isAbstract(listType.getModifiers()))
{
//Must be instantiable with zero-arg constructor
try
{
//It's our list we are creating so this is safe
List<Object> list = (List<Object>)listType.getConstructor().newInstance();
return(list);
}
catch (NoSuchMethodException | IllegalAccessException | InstantiationException e)
{
throw new PreferencesException("Cannot instantiate list of type " + listType + ".", e);
}
catch (InvocationTargetException e)
{
if (e.getCause() instanceof RuntimeException)
throw ((RuntimeException)e.getCause());
else if (e.getCause() instanceof Error)
throw ((Error)e.getCause());
else if (e.getCause() instanceof PreferencesException)
throw ((PreferencesException)e.getCause());
else
throw new PreferencesException("Cannot instantiate list of type " + listType + ".", e.getCause());
}
}
else
throw new PreferencesException("Cannot instantiate list of type " + listType + " since it cannot be instantiated.");
}
@Override
public <T> boolean applyPut(String key, T value, PreferenceKeyMetadata<T> keyMetadata, TransformPutChain chain) throws PreferencesException
{
if (!isSupported(keyMetadata, chain))
return(false);
TypeToken<?> elementType = collectionElementType(keyMetadata.getDataType());
PreferenceKeyMetadata<?> elementKeyMetadata = keyMetadata.withDataType(elementType);
//For each element in the list, create new endpoint, run chain with element to endpoint
Iterable<?> valueList = (Iterable<?>)value;
Table<Integer, String, String> valueTable = HashBasedTable.create();
int index = 0;
for (Object element : valueList)
{
StringStorageEndpoint elementEndpoint = new StringStorageEndpoint(valueTable.row(index));
//element type is appropriate, so it safe
chain.putValueWithRestartedChain(key, element, (PreferenceKeyMetadata)elementKeyMetadata, elementEndpoint);
index++;
}
//log.info("Results: " + valueTable);
//Convert to our format using CSV writer?
for (String colKey : valueTable.columnKeySet())
{
List<String> colValueList = new ArrayList<>();
for (int i = 0; i < index; i++)
{
String curVal = valueTable.get(i, colKey);
if (curVal == null)
curVal = "";
//curVal = StringEscapeUtils.escapeCsv(curVal);
colValueList.add(curVal);
}
StringWriter buf = new StringWriter();
try (CsvListWriter csvWriter = new CsvListWriter(buf, csvFormat))
{
csvWriter.write(colValueList);
}
catch (IOException e)
{
//Should not happen since string writer doesn't throw I/O errors
throw new PreferencesException(e);
}
String completeValue = buf.toString();
chain.putValue(colKey, completeValue, keyMetadata.withDataType(String.class));
log.debug("Wrote value " + completeValue + " for key " + key);
}
return(true);
}
@Override
public <T> boolean applyRemove(String key, PreferenceKeyMetadata<T> keyMetadata, TransformRemoveChain chain) throws PreferencesException
{
if (!isSupported(keyMetadata, chain))
return(false);
chain.removeValue(key, keyMetadata.withDataType(String.class));
return(true);
}
private boolean isSupported(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain chain)
throws PreferencesException
{
//Must not be supported natively
if (chain.isDataTypeSupportedNatively(keyMetadata))
return(false);
//Must be a collection
if (!TypeToken.of(Collection.class).isAssignableFrom(keyMetadata.getDataType()))
return(false);
//The target is a string, so must support writing strings
if (!chain.isDataTypeSupported(keyMetadata.withDataType(String.class)))
return(false);
//Must also support converting element type into strings
if (!chain.isDataTypeSupportedWithRestartedChain(keyMetadata.withDataType(collectionElementType(keyMetadata.getDataType())), new StringDataTypeEndpoint()))
return(false);
return(true);
}
@Override
public DataTypeSupport applyDataTypeSupport(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain chain) throws PreferencesException
{
//Must be a collection
if (!TypeToken.of(Collection.class).isAssignableFrom(keyMetadata.getDataType()))
return(null);
//The target is a string, so must support writing strings
if (!chain.isDataTypeSupported(keyMetadata.withDataType(String.class)))
return(null);
//Must also support converting element type into strings
if (!chain.isDataTypeSupportedWithRestartedChain(keyMetadata.withDataType(collectionElementType(keyMetadata.getDataType())), new StringDataTypeEndpoint()))
return(null);
return(DataTypeSupport.ADD_SUPPORT);
}
private TypeToken<?> collectionElementType(TypeToken<?> collectionType)
{
return(TypeToken.of(MetadataUtils.getCollectionOrArrayComponentGenericType(collectionType.getType())));
}
private class ElementReadingEndpoint extends StringDataTypeEndpoint implements TransformGetChain
{
private int index;
private final TransformGetChain originalChain;
private final Map<String, List<String>> cache = new HashMap<>();
public ElementReadingEndpoint(TransformGetChain originalChain)
{
this.originalChain = originalChain;
}
@Override
public <T> T getValue(String key, PreferenceKeyMetadata<T> keyMetadata) throws UnsupportedDataTypeException, PreferencesException
{
List<String> values = cache.get(key);
if (values == null)
{
String sValue = originalChain.getValue(key, keyMetadata.withDataType(String.class));
try (CsvListReader reader = new CsvListReader(new StringReader(sValue), csvFormat))
{
values = reader.read();
if (values == null)
values = Collections.emptyList();
cache.put(key, values);
}
catch (IOException e)
{
throw new PreferencesException("Error parsing CSV value: " + sValue, e);
}
}
if (index < values.size())
return((T)values.get(index));
else
return(null);
}
@Override
public <T> T getValueWithRestartedChain(String key, PreferenceKeyMetadata<T> keyMetadata, TransformGetChain newEndpoint)
throws UnsupportedDataTypeException, PreferencesException
{
//It's an endpoint
return(getValue(key, keyMetadata));
}
public void setIndex(int index)
{
this.index = index;
}
}
private static class StringStorageEndpoint extends StringDataTypeEndpoint implements TransformPutChain
{
private final Map<? super String, ? super String> valueMap;
public StringStorageEndpoint(Map<? super String, ? super String> valueMap)
{
this.valueMap = valueMap;
}
@Override
public <T> void putValue(String key, T value, PreferenceKeyMetadata<T> keyMetadata)
throws UnsupportedDataTypeException, PreferencesException
{
if (value != null)
valueMap.put(key, value.toString());
}
@Override
public <T> void putValueWithRestartedChain(String key, T value, PreferenceKeyMetadata<T> keyMetadata, TransformPutChain newEndpoint)
throws UnsupportedDataTypeException, PreferencesException
{
//It's an endpoint
putValue(key, value, keyMetadata);
}
}
private static class StringDataTypeEndpoint implements TransformDataTypeSupportChain
{
@Override
public boolean isDataTypeSupported(PreferenceKeyMetadata<?> keyMetadata) throws PreferencesException
{
return(String.class.equals(keyMetadata.getDataType().getRawType()));
}
@Override
public boolean isDataTypeSupportedNatively(PreferenceKeyMetadata<?> keyMetadata) throws PreferencesException
{
return(isDataTypeSupported(keyMetadata));
}
@Override
public boolean isDataTypeSupportedNativelyWithRestartedChain(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain endpoint)
throws PreferencesException
{
//It's an endpoint, not a chain
return(isDataTypeSupported(keyMetadata));
}
@Override
public boolean isDataTypeSupportedWithRestartedChain(PreferenceKeyMetadata<?> keyMetadata, TransformDataTypeSupportChain endpoint)
throws PreferencesException
{
//It's an endpoint, not a chain
return(isDataTypeSupported(keyMetadata));
}
}
}