blob: 054a3690df0811bcce0dbb43b2651114a482a278 [file] [log] [blame]
/*
* 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.qi4j.api.composite;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.qi4j.api.Qi4j;
import org.qi4j.api.property.GenericPropertyInfo;
import org.qi4j.api.property.Property;
import org.qi4j.api.util.Classes;
import org.qi4j.api.util.Dates;
import org.qi4j.api.value.ValueComposite;
/**
* Transfer java.util.Properties to Composite properties
*/
public final class PropertyMapper
{
private final static Map<Type, MappingStrategy> STRATEGY;
static
{
STRATEGY = new HashMap<>();
STRATEGY.put( Integer.class, new IntegerMapper() );
STRATEGY.put( Long.class, new LongMapper() );
STRATEGY.put( Short.class, new ShortMapper() );
STRATEGY.put( Byte.class, new ByteMapper() );
STRATEGY.put( String.class, new StringMapper() );
STRATEGY.put( Character.class, new CharMapper() );
STRATEGY.put( Float.class, new FloatMapper() );
STRATEGY.put( Double.class, new DoubleMapper() );
STRATEGY.put( Date.class, new DateMapper() );
STRATEGY.put( Boolean.class, new BooleanMapper() );
STRATEGY.put( BigDecimal.class, new BigDecimalMapper() );
STRATEGY.put( BigInteger.class, new BigIntegerMapper() );
STRATEGY.put( Enum.class, new EnumMapper() );
STRATEGY.put( Array.class, new ArrayMapper() );
STRATEGY.put( Map.class, new MapMapper() );
STRATEGY.put( List.class, new ListMapper() );
STRATEGY.put( Set.class, new SetMapper() );
STRATEGY.put( ValueComposite.class, new ValueCompositeMapper() );
}
/**
* Populate the Composite with properties from the given properties object.
*
* @param props properties object
* @param composite the composite instance
*
* @throws IllegalArgumentException if properties could not be transferred to composite
*/
public static void map( Properties props, Composite composite )
throws IllegalArgumentException
{
for( Map.Entry<Object, Object> objectObjectEntry : props.entrySet() )
{
try
{
String methodName = objectObjectEntry.getKey().toString();
Method propertyMethod = composite.getClass().getInterfaces()[ 0 ].getMethod( methodName );
propertyMethod.setAccessible( true );
Object value = objectObjectEntry.getValue();
Type propertyType = GenericPropertyInfo.propertyTypeOf( propertyMethod );
value = mapToType( composite, propertyType, value.toString() );
@SuppressWarnings( "unchecked" )
Property<Object> property = (Property<Object>) propertyMethod.invoke( composite );
property.set( value );
}
catch( NoSuchMethodException e )
{
throw new IllegalArgumentException( "Could not find any property named " + objectObjectEntry.getKey() );
}
catch( IllegalAccessException e )
{
//noinspection ThrowableInstanceNeverThrown
throw new IllegalArgumentException( "Could not populate property named " + objectObjectEntry.getKey(), e );
}
catch( InvocationTargetException e )
{
//noinspection ThrowableInstanceNeverThrown
String message = "Could not populate property named " + objectObjectEntry.getKey();
throw new IllegalArgumentException( message, e );
}
}
}
@SuppressWarnings( "raw" )
private static Object mapToType( Composite composite, Type propertyType, Object value )
{
final String stringValue = value.toString();
MappingStrategy strategy;
if( propertyType instanceof Class )
{
Class type = (Class) propertyType;
if( type.isArray() )
{
strategy = STRATEGY.get( Array.class );
}
else if( Enum.class.isAssignableFrom( Classes.RAW_CLASS.map( propertyType ) ) )
{
strategy = STRATEGY.get( Enum.class );
}
else
{
strategy = STRATEGY.get( type );
}
if( strategy == null ) // If null, try with the ValueComposite Mapper...
{
strategy = STRATEGY.get( ValueComposite.class );
}
}
else if( propertyType instanceof ParameterizedType )
{
ParameterizedType type = ( (ParameterizedType) propertyType );
if( type.getRawType() instanceof Class )
{
Class clazz = (Class) type.getRawType();
if( List.class.isAssignableFrom( clazz ) )
{
strategy = STRATEGY.get( List.class );
}
else if( Set.class.isAssignableFrom( clazz ) )
{
strategy = STRATEGY.get( Set.class );
}
else if( Map.class.isAssignableFrom( clazz ) )
{
strategy = STRATEGY.get( Map.class );
}
else
{
throw new IllegalArgumentException( propertyType + " is not supported." );
}
}
else
{
throw new IllegalArgumentException( propertyType + " is not supported." );
}
}
else
{
throw new IllegalArgumentException( propertyType + " is not supported." );
}
if( strategy == null )
{
throw new IllegalArgumentException( propertyType + " is not supported." );
}
return strategy.map( composite, propertyType, stringValue );
}
/**
* Load a Properties object from the given stream, close it, and then populate
* the Composite with the properties.
*
* @param propertyInputStream properties input stream
* @param composite the instance
*
* @throws IOException if the stream could not be read
*/
public static void map( InputStream propertyInputStream, Composite composite )
throws IOException
{
if( propertyInputStream != null )
{
Properties configProps = new Properties();
try
{
configProps.load( propertyInputStream );
}
finally
{
propertyInputStream.close();
}
map( configProps, composite );
}
}
/**
* Create Properties object which is backed by the given Composite.
*
* @param composite the instance
*
* @return properties instance
*/
public static Properties toJavaProperties( final Composite composite )
{
return new Properties()
{
private static final long serialVersionUID = 3550125427530538865L;
@Override
public Object get( Object o )
{
try
{
Method propertyMethod = composite.getClass().getMethod( o.toString() );
Property<?> property = (Property<?>) propertyMethod.invoke( composite );
return property.get();
}
catch( NoSuchMethodException | IllegalAccessException | InvocationTargetException e )
{
return null;
}
}
@Override
public Object put( Object o, Object o1 )
{
Object oldValue = get( o );
try
{
Method propertyMethod = composite.getClass().getMethod( o.toString(), Object.class );
propertyMethod.invoke( composite, o1 );
}
catch( NoSuchMethodException | IllegalAccessException | InvocationTargetException e )
{
e.printStackTrace();
}
return oldValue;
}
};
}
private static void tokenize( String valueString, boolean mapSyntax, TokenizerCallback callback )
{
char[] data = valueString.toCharArray();
int oldPos = 0;
for( int pos = 0; pos < data.length; pos++ )
{
char ch = data[ pos ];
if( ch == '\"' )
{
pos = resolveQuotes( valueString, callback, data, pos, '\"' );
oldPos = pos;
}
if( ch == '\'' )
{
pos = resolveQuotes( valueString, callback, data, pos, '\'' );
oldPos = pos;
}
if( ch == ',' || ( mapSyntax && ch == ':' ) )
{
String token = new String( data, oldPos, pos - oldPos );
callback.token( token );
oldPos = pos + 1;
}
}
String token = new String( data, oldPos, data.length - oldPos );
callback.token( token );
}
private static int resolveQuotes( String valueString,
TokenizerCallback callback,
char[] data,
int pos, char quote
)
{
boolean found = false;
for( int j = pos + 1; j < data.length; j++ )
{
if( !found )
{
if( data[ j ] == quote )
{
String token = new String( data, pos + 1, j - pos - 1 );
callback.token( token );
found = true;
}
}
else
{
if( data[ j ] == ',' )
{
return j + 1;
}
}
}
if( !found )
{
throw new IllegalArgumentException( "String is not quoted correctly: " + valueString );
}
return data.length;
}
private interface TokenizerCallback
{
void token( String token );
}
private interface MappingStrategy
{
Object map( Composite composite, Type type, String value );
}
private static class StringMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return value;
}
}
private static class IntegerMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new Integer( value.trim() );
}
}
private static class FloatMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new Float( value.trim() );
}
}
private static class DoubleMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new Double( value.trim() );
}
}
private static class LongMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new Long( value.trim() );
}
}
private static class ShortMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new Short( value.trim() );
}
}
private static class ByteMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new Byte( value.trim() );
}
}
private static class CharMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return value.trim().charAt( 0 );
}
}
private static class BigDecimalMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new BigDecimal( value.trim() );
}
}
private static class BigIntegerMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return new BigInteger( value.trim() );
}
}
private static class EnumMapper
implements MappingStrategy
{
@Override
@SuppressWarnings( "unchecked" )
public Object map( Composite composite, Type type, String value )
{
return Enum.valueOf( (Class<Enum>) type, value );
}
}
private static class DateMapper
implements MappingStrategy
{
@Override
public Object map( Composite composite, Type type, String value )
{
return Dates.fromString( value.trim() );
}
}
private static class ValueCompositeMapper
implements MappingStrategy
{
@Override
@SuppressWarnings( "unchecked" )
public Object map( Composite composite, Type type, String value )
{
return Qi4j.FUNCTION_COMPOSITE_INSTANCE_OF.map( composite ).module().newValueFromSerializedState( (Class<Object>) type, value );
}
}
private static class ArrayMapper
implements MappingStrategy
{
@Override
@SuppressWarnings( {"raw", "unchecked"} )
public Object map( final Composite composite, Type type, String value )
{
final Class arrayType = ( (Class) type ).getComponentType();
final ArrayList result = new ArrayList();
tokenize( value, false, new TokenizerCallback()
{
@Override
public void token( String token )
{
result.add( mapToType( composite, arrayType, token ) );
}
} );
return result.toArray( (Object[]) Array.newInstance( arrayType, result.size() ) );
}
}
private static class BooleanMapper
implements MappingStrategy
{
@Override
public Object map( final Composite composite, Type type, String value )
{
return Boolean.valueOf( value.trim() );
}
}
private static class ListMapper
implements MappingStrategy
{
@Override
@SuppressWarnings( {"raw", "unchecked"} )
public Object map( final Composite composite, Type type, String value )
{
final Type dataType = ( (ParameterizedType) type ).getActualTypeArguments()[ 0 ];
final Collection result = new ArrayList();
tokenize( value, false, new TokenizerCallback()
{
@Override
public void token( String token )
{
result.add( mapToType( composite, dataType, token ) );
}
} );
return result;
}
}
private static class SetMapper
implements MappingStrategy
{
@Override
@SuppressWarnings( {"raw", "unchecked"} )
public Object map( final Composite composite, Type type, String value )
{
final Type dataType = ( (ParameterizedType) type ).getActualTypeArguments()[ 0 ];
final Collection result = new HashSet();
tokenize( value, false, new TokenizerCallback()
{
@Override
public void token( String token )
{
result.add( mapToType( composite, dataType, token ) );
}
} );
return result;
}
}
private static class MapMapper
implements MappingStrategy
{
@Override
@SuppressWarnings( {"raw", "unchecked"} )
public Object map( final Composite composite, Type generictype, String value )
{
ParameterizedType type = (ParameterizedType) generictype;
final Type keyType = type.getActualTypeArguments()[ 0 ];
final Type valueType = type.getActualTypeArguments()[ 0 ];
final Map result = new HashMap();
tokenize( value, true, new TokenizerCallback()
{
boolean keyArrivingNext = true;
String key;
@Override
public void token( String token )
{
if( keyArrivingNext )
{
key = token;
keyArrivingNext = false;
}
else
{
result.put( mapToType( composite, keyType, key ), mapToType( composite, valueType, token ) );
keyArrivingNext = true;
}
}
} );
return result;
}
}
private PropertyMapper()
{
}
}