/**
* Copyright (c) 2011-2014, OpenIoT
*
* This file is part of OpenIoT.
*
* OpenIoT is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* OpenIoT 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with OpenIoT. If not, see <http://www.gnu.org/licenses/>.
*
* Contact: OpenIoT mailto: info@openiot.eu
* @author rhietala
* @author Timotee Maret
* @author Sofiane Sarni
* @author Ali Salehi
* @author Mehdi Riahi
* @author Julien Eberle
*/
package org.openiot.gsn.beans;
import org.openiot.gsn.http.rest.StreamElement4Rest;
import org.openiot.gsn.utils.CaseInsensitiveComparator;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.TreeMap;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
public final class StreamElement implements Serializable {
private static final long serialVersionUID = 2000261462783698617L;
private static final transient Logger logger = Logger.getLogger( StreamElement.class );
private transient TreeMap < String , Integer > indexedFieldNames = null;
private long timeStamp = -1;
private String [ ] fieldNames;
private Serializable [ ] fieldValues;
private transient Byte [ ] fieldTypes;
private transient long internalPrimayKey = -1;
private static final String NULL_ENCODING = "NULL"; // null encoding for transmission over xml-rpc
public StreamElement (StreamElement other) {
this.fieldNames=new String[other.fieldNames.length];
this.fieldValues=new Serializable[other.fieldValues.length];
this.fieldTypes=new Byte[other.fieldTypes.length];
for (int i=0;i<other.fieldNames.length;i++) {
fieldNames[i]=other.fieldNames[i];
fieldValues[i]=other.fieldValues[i];
fieldTypes[i]=other.fieldTypes[i];
}
this.timeStamp=other.timeStamp;
this.internalPrimayKey = other.internalPrimayKey;
}
public StreamElement ( DataField [ ] outputStructure , final Serializable [ ] data ) {
this(outputStructure,data,System.currentTimeMillis());
}
public StreamElement ( DataField [ ] outputStructure , final Serializable [ ] data , final long timeStamp ) {
this.fieldNames = new String [ outputStructure.length ];
this.fieldTypes = new Byte [ outputStructure.length ];
this.timeStamp = timeStamp;
for ( int i = 0 ; i < this.fieldNames.length ; i++ ) {
this.fieldNames[ i ] = outputStructure[ i ].getName( ).toLowerCase( );
this.fieldTypes[ i ] = outputStructure[ i ].getDataTypeID( );
}
if ( this.fieldNames.length != data.length ) throw new IllegalArgumentException( "The length of dataFileNames and the actual data provided in the constructor of StreamElement doesn't match." );
this.verifyTypesCompatibility( this.fieldTypes , data );
this.fieldValues = data;
}
public StreamElement ( final String [ ] dataFieldNames , final Byte [ ] dataFieldTypes , final Serializable [ ] data ) {
this( dataFieldNames , dataFieldTypes , data , System.currentTimeMillis( ) );
}
public StreamElement ( final String [ ] dataFieldNames , final Byte [ ] dataFieldTypes , final Serializable [ ] data , final long timeStamp ) {
if ( dataFieldNames.length != dataFieldTypes.length )
throw new IllegalArgumentException( "The length of dataFileNames and dataFileTypes provided in the constructor of StreamElement doesn't match." );
if ( dataFieldNames.length != data.length ) throw new IllegalArgumentException( "The length of dataFileNames and the actual data provided in the constructor of StreamElement doesn't match." );
this.timeStamp = timeStamp;
this.fieldTypes = dataFieldTypes;
this.fieldNames = dataFieldNames;
this.fieldValues = data;
this.verifyTypesCompatibility( dataFieldTypes , data );
}
public StreamElement(TreeMap<String, Serializable> output,DataField[] fields) {
int nbFields = output.keySet().size();
if(output.containsKey("timed"))
nbFields--;
String fieldNames[] = new String[nbFields];
Byte fieldTypes[] = new Byte[nbFields];
Serializable fieldValues[] = new Serializable[nbFields];
TreeMap < String , Integer > indexedFieldNames = new TreeMap<String, Integer>(new CaseInsensitiveComparator());
int idx = 0;
long timestamp =System.currentTimeMillis();
for (String key:output.keySet()) {
Serializable value = output.get(key);
if(key.equalsIgnoreCase("timed"))
timestamp = (Long) value;
else {
fieldNames[idx] = key;
fieldValues[idx] = value;
for (int i=0;i<fields.length;i++) {
if (fields[i].getName().equalsIgnoreCase(key))
fieldTypes[idx] = fields[i].getDataTypeID();
}
indexedFieldNames.put(key, idx);
idx++;
}
}
this.fieldNames=fieldNames;
this.fieldTypes=fieldTypes;
this.fieldValues=fieldValues;
this.indexedFieldNames=indexedFieldNames;
this.timeStamp=timestamp;
}
/**
* Verify if the data corresponds to the fieldType
* @param fieldType
* @param data
* @throws IllegalArgumentException
*/
private void verifyTypeCompatibility ( Byte fieldType , Serializable data) throws IllegalArgumentException {
if ( data == null ) return;
switch ( fieldType ) {
case DataTypes.TINYINT :
if ( !( data instanceof Byte ) )
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
case DataTypes.SMALLINT :
if ( !( data instanceof Short ) )
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
case DataTypes.BIGINT :
if ( !( data instanceof Long ) )
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
case DataTypes.CHAR :
case DataTypes.VARCHAR :
if ( !( data instanceof String ) )
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
case DataTypes.INTEGER :
if ( !( data instanceof Integer))
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
case DataTypes.DOUBLE :
if ( !( data instanceof Double || data instanceof Float ) )
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
case DataTypes.BINARY :
// if ( data[ i ] instanceof String ) data[ i ] = ( ( String )
// data[ i ] ).getBytes( );
if ( !( data instanceof byte [ ] || data instanceof String ) )
throw new IllegalArgumentException( "The field is defined as " + DataTypes.TYPE_NAMES[ fieldType ]
+ " while the actual data in the field is of type : *" + data.getClass( ).getCanonicalName( ) + "*" );
break;
}
}
/**
* Checks the type compatibility of all fields of the StreamElement
* @param fieldTypes the array of all fields' type
* @param data the array of data to check
* @throws IllegalArgumentException if a data field doesn't match the given type
*/
private void verifyTypesCompatibility ( final Byte [ ] fieldTypes , final Serializable [ ] data ) throws IllegalArgumentException {
for ( int i = 0 ; i < data.length ; i++ ) {
try{
verifyTypeCompatibility(fieldTypes[i], data[i]);
}catch(IllegalArgumentException e){
throw new IllegalArgumentException("The newly constructed Stream Element is not consistent for the " + ( i + 1 ) + "th field.", e);
}
}
}
public String toString ( ) {
final StringBuffer output = new StringBuffer( "timed = " );
output.append( this.getTimeStamp( ) ).append( "\t" );
for ( int i = 0 ; i < this.fieldNames.length ; i++ )
output.append( "," ).append( this.fieldNames[ i ] ).append( "/" ).append( this.fieldTypes[ i ] ).append( " = " ).append( this.fieldValues[ i ] );
return output.toString( );
}
public final String [ ] getFieldNames ( ) {
return this.fieldNames;
}
/*
* Returns the field types in GSN format. Checkout org.openiot.gsn.beans.DataTypes
*/
public final Byte [ ] getFieldTypes ( ) {
return this.fieldTypes;
}
public final Serializable [ ] getData ( ) {
return this.fieldValues;
}
public void setData (int index,Serializable data ) {
this.fieldValues[index]=data;
}
public long getTimeStamp ( ) {
return this.timeStamp;
}
public StringBuilder getFieldTypesInString ( ) {
final StringBuilder stringBuilder = new StringBuilder( );
for ( final byte i : this.getFieldTypes( ) )
stringBuilder.append( DataTypes.TYPE_NAMES[ i ] ).append( " , " );
return stringBuilder;
}
/**
* Returns true if the timestamp is valid. A timestamp is valid if it is
* above zero.
*
* @return Whether the timestamp is valid or not.
*/
public boolean isTimestampSet ( ) {
return this.timeStamp > 0;
}
/**
* Sets the time stamp of this stream element.
*
* @param timeStamp The time stamp value. If the timestamp is zero or
* negative, it is considered non valid and zero will be placed.
*/
public void setTimeStamp ( long timeStamp ) {
if ( this.timeStamp <= 0 )
timeStamp = 0;
else
this.timeStamp = timeStamp;
}
/**
* This method gets the attribute name as the input and returns the value
* corresponding to that tuple.
*
* @param fieldName The name of the tuple.
* @return The value corresponding to the named tuple.
*/
public final Serializable getData ( final String fieldName ) {
generateIndex();
Integer index = indexedFieldNames.get( fieldName );
if (index == null) {
logger.info("There is a request for field "+fieldName+" for StreamElement: "+this.toString()+". As the requested field doesn't exist, GSN returns Null to the callee.");
return null;
}
return this.fieldValues[ index ];
}
/**
* This method gets the attribute name as the input and returns the type of the value
* corresponding to that tuple.
*
* @param fieldName The name of the tuple.
* @return The type of the value corresponding to the named tuple.
*/
public final Byte getType ( final String fieldName ) {
generateIndex();
Integer index = indexedFieldNames.get( fieldName );
if (index == null) {
logger.warn("There is a request for type of field "+fieldName+" for StreamElement: "+this.toString()+". As the requested field doesn't exist, GSN returns Null to the callee.");
return null;
}
return this.fieldTypes[ index ];
}
public long getInternalPrimayKey ( ) {
return internalPrimayKey;
}
public void setInternalPrimayKey ( long internalPrimayKey ) {
this.internalPrimayKey = internalPrimayKey;
}
/**
* @return
*/
public Object [ ] getDataInRPCFriendly ( ) {
Object [ ] toReturn = new Object [ fieldValues.length ];
for ( int i = 0 ; i < toReturn.length ; i++ ) {
//process null values
if (fieldValues[i]==null) {
toReturn[i] = NULL_ENCODING;
continue;
}
switch ( fieldTypes[ i ] ) {
case DataTypes.DOUBLE :
toReturn[ i ] = fieldValues[ i ];
break;
case DataTypes.BIGINT :
toReturn[ i ] = Long.toString( ( Long ) fieldValues[ i ] );
break;
// case DataTypes.TIME :
// toReturn[ i ] = Long.toString( ( Long ) fieldValues[ i ] );
// break;
case DataTypes.TINYINT :
case DataTypes.SMALLINT :
case DataTypes.INTEGER :
toReturn[ i ] = new Integer( ( Integer ) fieldValues[ i ] );
break;
case DataTypes.CHAR :
case DataTypes.VARCHAR :
case DataTypes.BINARY :
toReturn[ i ] = fieldValues[ i ];
break;
default :
logger.error( "Type can't be converted : TypeID : " + fieldTypes[ i ] );
}
}
return toReturn;
}
/**
* Returns the type of the field in the output format or -1 if the field doesn't exit.
* @param outputFormat
* @param fieldName
* @return
*/
private static byte findIndexInDataField(DataField[] outputFormat, String fieldName) {
for (int i=0;i<outputFormat.length;i++)
if (outputFormat[i].getName( ).equalsIgnoreCase( fieldName ))
return outputFormat[i].getDataTypeID( );
return -1;
}
/***
* Used with the new JRuby/Mongrel/Rest interface
*/
public static StreamElement fromREST ( DataField [ ] outputFormat , String [ ] fieldNames , String [ ] fieldValues , String timestamp ) {
Serializable [ ] values = new Serializable [ outputFormat.length ];
for ( int i = 0 ; i < fieldNames.length ; i++ ) {
switch ( findIndexInDataField( outputFormat , (String)fieldNames[i] ) ) {
case DataTypes.DOUBLE :
values[ i ] = Double.parseDouble(fieldValues[ i ]);
break;
case DataTypes.BIGINT :
// case DataTypes.TIME :
values[ i ] = Long.parseLong( ( String ) fieldValues[ i ] );
break;
case DataTypes.TINYINT :
values[ i ] = Byte.parseByte( ( String ) fieldValues[ i ] );
break;
case DataTypes.SMALLINT :
case DataTypes.INTEGER :
values[ i ] = Integer.parseInt( fieldValues[ i ] );
break;
case DataTypes.CHAR :
case DataTypes.VARCHAR :
values[ i ] = new String( Base64.decodeBase64( fieldValues[ i ].getBytes()));
break;
case DataTypes.BINARY :
values[ i ] = (byte[]) Base64.decodeBase64( fieldValues[ i ].getBytes());
break;
case -1:
default :
logger.error( "The field name doesn't exit in the output structure : FieldName : "+(String)fieldNames[i] );
}
}
return new StreamElement( outputFormat , values , Long.parseLong(timestamp ));
}
public static StreamElement createElementFromREST( DataField [ ] outputFormat , String [ ] fieldNames , Object[ ] fieldValues ) {
ArrayList<Serializable> values = new ArrayList<Serializable>();
// ArrayList<String> fields = new ArrayList<String>();
long timestamp = -1;
for ( int i = 0 ; i < fieldNames.length ; i++ ) {
if (fieldNames[i].equalsIgnoreCase("TIMED")) {
timestamp = Long.parseLong((String) fieldValues[i]);
continue;
}
boolean found = false;
for (DataField f:outputFormat) {
if(f.getName().equalsIgnoreCase(fieldNames[i])) {
// fields.add(fieldNames[i]);
found=true;
break;
}
}
if (found==false)
continue;
switch ( findIndexInDataField( outputFormat ,fieldNames[i] ) ) {
case DataTypes.DOUBLE :
values.add(Double.parseDouble( (String)fieldValues[ i ]));
break;
case DataTypes.BIGINT :
values.add( Long.parseLong( (String) fieldValues[ i ] ));
break;
case DataTypes.TINYINT :
values.add(Byte.parseByte( (String)fieldValues[ i ]));
break;
case DataTypes.SMALLINT :
values.add( Short.parseShort( (String)fieldValues[ i ] ));
break;
case DataTypes.INTEGER :
values.add( Integer.parseInt( (String)fieldValues[ i ] ));
break;
case DataTypes.CHAR :
case DataTypes.VARCHAR :
values.add(new String((byte[]) fieldValues[ i ]));
break;
case DataTypes.BINARY :
try{
// StreamElementTest.md5Digest(fieldValues[ i ]);
}catch (Exception e) {
e.printStackTrace();
}
values.add((byte[]) fieldValues[ i ]);
break;
case -1:
default :
logger.error( "The field name doesn't exit in the output structure : FieldName : "+(String)fieldNames[i] );
}
}
if (timestamp==-1)
timestamp=System.currentTimeMillis();
return new StreamElement( outputFormat , values.toArray(new Serializable[] {}) , timestamp );
}
public StreamElement4Rest toRest() {
StreamElement4Rest toReturn = new StreamElement4Rest(this);
return toReturn;
}
/**
* Build the index for mapping field name to their positions in the array if it is not yet built
* This assumes that StreamElements cannot change their structure
*/
private void generateIndex(){
if ( indexedFieldNames == null ) {
indexedFieldNames = new TreeMap < String , Integer >( new CaseInsensitiveComparator( ) );
for ( int i = 0 ; i < this.fieldNames.length ; i++ )
this.indexedFieldNames.put( fieldNames[ i ] , i );
}
}
/**
* set the data in the coresponding field, throws an exception if the data type doesn't match
* @param fieldName
* @param data
* @throws IllegalArgumentException
*/
protected void setData(String fieldName, Serializable data) throws IllegalArgumentException {
generateIndex();
Integer index = indexedFieldNames.get( fieldName );
if (index == null) {
logger.warn("There is a request for setting field "+fieldName+" for StreamElement: "+this.toString()+". But the requested field doesn't exist.");
}
verifyTypeCompatibility(fieldTypes[index], data);
setData(index,data);
}
}