/******************************************************************************
* Copyright (c) 2013, Linagora
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Linagora - initial API and implementation
*******************************************************************************/
package com.ebmwebsourcing.petals.studio.dev.properties;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import asia.redact.bracket.properties.Properties;
import com.ebmwebsourcing.petals.studio.dev.properties.internal.Utils;
/**
* The handler for configuration properties.
* @author Vincent Zurczak - Linagora
*/
public class AbstractModel {
/**
* A keyword to indicate the empty string.
*/
public static final String VALUE_EMPTY = "EMPTY STRING";
/**
* Use {@link AbstractModel#getModelVersion()}.
*/
public static final String PROPERTY_VERSION = "version";
private static final String TYPE_PREFIX = "Type: ";
private static final String TYPE_NULLABLE = "nullable";
private static final String TYPE_REQUIRED = "required";
private static final String TYPE_PATTERN_RANGE_1 = "(\\[|\\])\\s*(\\d+)\\s*,\\s*(\\d*)\\s*(\\[|\\])"; // [ min, optional-max ]
private static final String TYPE_PATTERN_RANGE_2 = "(\\[|\\])\\s*(\\d*)\\s*,\\s*(\\d+)\\s*(\\[|\\])"; // [ optional-min, max ]
private static final String TYPE_PATTERN_LIST = "\\{([^}]+)\\}";
private static final String TYPE_PATTERN =
AbstractModel.TYPE_PREFIX
+ "((" + SupportedTypes.BOOLEAN + ")|"
+ "(" + SupportedTypes.DOUBLE + ")|"
+ "(" + SupportedTypes.FLOAT + ")|"
+ "(" + SupportedTypes.STRING + ")|"
+ "(" + SupportedTypes.LIST + ")|"
+ "(" + SupportedTypes.INTEGER + "\\s*(" + TYPE_PATTERN_RANGE_1 + ")*)|"
+ "(" + SupportedTypes.INTEGER + "\\s*(" + TYPE_PATTERN_RANGE_2 + ")*)|"
+ "(" + SupportedTypes.ENUMERATION + "\\s*(" + TYPE_PATTERN_LIST + ")*)|"
+ "(" + SupportedTypes.LONG + "))"
+ "(\\s*,\\s*" + TYPE_REQUIRED + ""
+ "(\\s*,\\s*" + TYPE_NULLABLE + ")?)?";
private final Properties properties;
/**
* Empty constructor (for tests).
*/
protected AbstractModel() {
this.properties = Properties.Factory.getInstance();
}
/**
* Constructor which reads properties from a file.
* @param propertiesFile a properties file
* @throws IOException if something went wrong
*/
public AbstractModel( File propertiesFile ) throws IOException {
Reader reader = new FileReader( propertiesFile );
this.properties = Properties.Factory.getInstance( reader );
}
/**
* Constructor which reads properties from an input stream.
* <p>
* The input stream is not closed by this method.
* </p>
*
* @param input an input stream for a properties file
* @throws IOException if something went wrong
*/
public AbstractModel( InputStream input ) throws IOException {
this.properties = Properties.Factory.getInstance( input );
}
/**
* Insert properties from a map.
* @param map a map with the properties
* @throws IOException if something went wrong
*/
public final void insert( Map<String,Object> map ) throws IOException {
for( Map.Entry<String,Object> entry : map.entrySet()) {
String value = entry.getValue() == null ? null : String.valueOf( entry.getValue());
this.properties.put( entry.getKey(), value );
}
}
/**
* @return the model version (only mandatory property)
*/
public String getModelVersion() {
return this.properties.get( PROPERTY_VERSION ).trim();
}
/**
* Returns a trimmed property.
* @param property a property
* @return a trimmed property value, or null if the value was null
*/
public String getTrimmedProperty( String property ) {
String s = this.properties.get( property );
return s == null ? null : s.trim();
}
/**
* Gets an integer range associated with a property.
* @param property a property
* @return null if the property is not an integer or did not have a range, a range otherwise
*/
public IntegerRange getRange( String property ) {
IntegerRange result = null;
if( getType( property ) == SupportedTypes.INTEGER ) {
String typeDef = findTypeDeclaration( property );
if( typeDef != null ) {
boolean found = false;
Matcher m = Pattern.compile( TYPE_PATTERN_RANGE_1 ).matcher( typeDef );
if( ! ( found = m.find())) {
m = Pattern.compile( TYPE_PATTERN_RANGE_2 ).matcher( typeDef );
found = m.find();
}
if( found ) {
boolean includeMin = m.group( 1 ).charAt( 0 ) == '[';
boolean includeMax = m.group( 4 ).charAt( 0 ) == ']';
String startAS = m.group( 2 );
int min = Utils.isEmpty( startAS ) ? -1 : Integer.valueOf( startAS );
String endAS = m.group( 3 );
int max = Utils.isEmpty( endAS ) ? -1 : Integer.valueOf( endAS );
result = new IntegerRange( min, max, includeMin, includeMax );
}
}
}
return result;
}
/**
* Gets the enumeration items.
* @param property a property
* @return null if the property is not an enumeration, a list of items otherwise (possibly empty)
*/
public Collection<String> getEnumeration( String property ) {
Collection<String> result = null;
if( getType( property ) == SupportedTypes.ENUMERATION ) {
result = new HashSet<String> ();
String typeDef = findTypeDeclaration( property );
Matcher m = Pattern.compile( TYPE_PATTERN_LIST ).matcher( typeDef );
if( m.find()) {
for( String s : m.group( 1 ).split( ";" ))
result.add( s.trim());
}
}
return result;
}
/**
* Gets the type of a property.
* @param property a property
* @return the associated type (String by default)
*/
public SupportedTypes getType( String property ) {
SupportedTypes result = SupportedTypes.STRING;
String typeDef = findTypeDeclaration( property );
if( typeDef != null ) {
typeDef = typeDef.substring( TYPE_PREFIX.length()).toLowerCase();
for( SupportedTypes type : SupportedTypes.values()) {
if( typeDef.startsWith( type.toString())) {
result = type;
break;
}
}
}
return result;
}
/**
* Determines whether a property is required.
* @param property a property
* @return true if it is required, false otherwise (false by default)
*/
public boolean isRequired( String property ) {
boolean required = false;
String typeDef = findTypeDeclaration( property );
if( typeDef != null ) {
for( String decl : typeDef.split( "," )) {
if( decl.trim().endsWith( TYPE_REQUIRED )
&& ! decl.contains( "}" )
&& ! decl.contains( "{" )
&& ! decl.contains( "[" )
&& ! decl.contains( "]" )) {
required = true;
break;
}
}
}
return required;
}
/**
* Determines whether a property is nullable.
* @param property a property
* @return true if it is nullable, false otherwise (false by default)
*/
public boolean isNullable( String property ) {
boolean nullable = false;
String typeDef = findTypeDeclaration( property );
if( typeDef != null )
nullable = typeDef.endsWith( TYPE_NULLABLE );
return nullable;
}
/**
* Checks the type definition of a property is valid.
* @return true if the syntax is correct, false otherwise
*/
public boolean isTypeDefinitionValid( String property ) {
boolean valid = true;
String typeDef = findTypeDeclaration( property );
if( typeDef != null )
valid = checkTypePattern( typeDef );
return valid;
}
/**
* Gets the documentation associated with a property
* @param property a property
* @return a non-null string
*/
public String getDocumentation( String property ) {
StringBuilder sb = new StringBuilder();
for( String comment : this.properties.getComments( property )) {
comment = comment.replaceAll( "#", "" ).trim();
sb.append( comment + "\n" );
}
return sb.toString().trim();
}
/**
* Gets the value of a "list" property as a list of strings.
* @param property a property
* @return null if the property is not of type LIST, a list otherwise
*/
public List<String> getValueAsList( String property ) {
List<String> result = null;
if( getType( property ) == SupportedTypes.LIST ) {
result = new ArrayList<String> ();
for( String s : this.properties.get( property ).split( "\\|" ))
result.add( s.trim());
}
return result;
}
/**
* @param property a property
* @return true if the property was defined in the model, false otherwise
*/
public boolean isValidProperty( String property ) {
return this.properties.containsKey( property );
}
/**
* Validates the hold properties.
* @return null if no error was found, an error message otherwise
*/
public String validatePropertyValue( String property ) {
String result = null;
String value = this.properties.get( property );
if( value == null ) {
if( isRequired( property )
&& ! isNullable( property ))
result = "Property " + property + " must be set.";
} else {
value = value.trim();
switch( getType( property )) {
case BOOLEAN:
if( ! value.equalsIgnoreCase( "true" )
&& ! value.equalsIgnoreCase( "false" ))
result = "Property " + property + " must be a boolean.";
break;
case DOUBLE:
try {
Double.valueOf( value );
} catch( NumberFormatException e ) {
result = "Property " + property + " must be a double.";
}
break;
case FLOAT:
try {
Float.valueOf( value );
} catch( NumberFormatException e ) {
result = "Property " + property + " must be a float.";
}
break;
case INTEGER:
try {
int t = Integer.valueOf( value );
IntegerRange range = getRange( property );
if( range != null ) {
// Both min and max have not be set?
if( range.getMin() == -1 && range.getMax() == -1 ) {
result = "Property " + property + " defines an invalid range.";
}
// Min must be less than Max
else if( range.getMin() >= range.getMax() && range.getMax() != -1 ) {
result = "Property " + property + " defines an invalid range. 'Min' must be less than 'max'.";
}
// Check the min?
else if( range.getMin() != -1 ) {
if( range.isMinIncluded() && t < range.getMin())
result = "Property " + property + " must be greater or equal than " + range.getMin() + ".";
else if( ! range.isMinIncluded() && t <= range.getMin())
result = "Property " + property + " must be strictly greater than " + range.getMin() + ".";
}
// Check the max?
else if( range.getMax() != -1 ) {
if( range.isMaxIncluded() && t > range.getMax())
result = "Property " + property + " must be less or equal than " + range.getMax() + ".";
else if( ! range.isMaxIncluded() && t >= range.getMax())
result = "Property " + property + " must be strictly less than " + range.getMax() + ".";
}
}
} catch( NumberFormatException e ) {
result = "Property " + property + " must be an integer.";
}
break;
case LONG:
try {
Long.valueOf( value );
} catch( NumberFormatException e ) {
result = "Property " + property + " must be a long.";
}
break;
case STRING:
case LIST:
// nothing
break;
case ENUMERATION:
Collection<String> items = getEnumeration( property );
if( items != null && ! items.contains( value ))
result = "Property " + property + " has an invalid value (not in the enumeration).";
break;
}
}
return result;
}
/**
* Validates the model (properties, types definition and check the specified values, if any).
* @return a map (key: property names, or "" for the whole file ; value : a list of error messages)
*/
public Map<String,List<String>> validateAbstractModel() {
Map<String,List<String>> result = new HashMap<String,List<String>> ();
if( ! this.properties.containsKey( PROPERTY_VERSION )) {
recordEntry( result, "", "The property " + PROPERTY_VERSION + " was not found." );
} else {
for( String property : this.properties.getPropertyMap().keySet()) {
String s;
if( ! property.toLowerCase().equals( property ))
recordEntry( result, property, "The property " + PROPERTY_VERSION + " must be in lower case." );
else if( ! isTypeDefinitionValid( property ))
recordEntry( result, property, "The type definition of the property " + PROPERTY_VERSION + " is invalid." );
else if( ! Utils.isEmpty( this.properties.get( property ))
&& ( s = validatePropertyValue( property )) != null )
recordEntry( result, property, s );
}
}
return result;
}
/**
* @return the properties
*/
public Properties getProperties() {
return this.properties;
}
/**
* Finds the type declaration for a property.
* @param property a property
* @return the type declaration, or null if the property was not found or the type not declared
* <p>
* If not null, the result is trimmed and set in lower case.
* </p>
*/
public String findTypeDeclaration( String property ) {
String result = null;
List<String> comments = this.properties.getComments( property );
for( String comment : comments ) {
comment = comment.replaceAll( "^#{1,}\\s*", "" );
if( ! comment.startsWith( TYPE_PREFIX ))
continue;
result = comment.trim().toLowerCase();
break;
}
return result;
}
/**
* Checks that a type definition is valid.
* @param s the string to check
* @return true if it is valid, false otherwise
*/
public static boolean checkTypePattern( String s ) {
return Pattern.compile( TYPE_PATTERN, Pattern.CASE_INSENSITIVE ).matcher( s ).matches();
}
/**
* Creates an instance without throwing any exception.
* @param propertiesFile a properties file
* @return a model instance, or null if it failed
*/
public static AbstractModel create( File propertiesFile ) {
AbstractModel result = null;
try {
result = new AbstractModel( propertiesFile );
} catch( IOException e ) {
// nothing
}
return result;
}
/**
* Creates an instance without throwing any exception.
* @param inputStream an input stream (not closed by this method)
* @return a model instance, or null if it failed
*/
public static AbstractModel create( InputStream inputStream ) {
AbstractModel result = null;
try {
result = new AbstractModel( inputStream );
} catch( IOException e ) {
// nothing
}
return result;
}
/**
* Records a validation entry in a map.
* @param map a map associating properties and error messages
* @param key a property name, or the empty string to indicate the whole model
* @param msg an error message
*/
private void recordEntry( Map<String,List<String>> map, String key, String msg ) {
List<String> messages = map.get( key );
if( messages == null )
messages = new ArrayList<String> ();
messages.add( msg );
map.put( key, messages );
}
}