/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * Licensed 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.pentaho.di.core.util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.ParserConfigurationException; import org.pentaho.di.core.Const; import org.pentaho.di.core.database.DatabaseMeta; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.xml.XMLHandler; import org.pentaho.di.core.xml.XMLParserFactoryProducer; import org.pentaho.di.repository.ObjectId; import org.pentaho.di.repository.Repository; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class SerializationHelper { private static final String INDENT_STRING = " "; /** * This method will perform the work that used to be done by hand in each kettle input meta for: readData(Node * stepnode). We handle all primitive types, complex user types, arrays, lists and any number of nested object levels, * via recursion of this method. * * @param object * The object to be persisted * @param node * The node to 'attach' our XML to */ public static void read( Object object, Node node ) { // get this classes fields, public, private, protected, package, everything Field[] fields = object.getClass().getFields(); for ( Field field : fields ) { // ignore fields which are final, static or transient if ( Modifier.isFinal( field.getModifiers() ) || Modifier.isStatic( field.getModifiers() ) || Modifier.isTransient( field.getModifiers() ) ) { continue; } // if the field is not accessible (private), we'll open it up so we can operate on it if ( !field.isAccessible() ) { field.setAccessible( true ); } // check if we're going to try to read an array if ( field.getType().isArray() ) { try { // get the node (if available) for the field Node fieldNode = XMLHandler.getSubNode( node, field.getName() ); if ( fieldNode == null ) { // doesn't exist (this is possible if fields were empty/null when persisted) continue; } // get the Java classname for the array elements String fieldClassName = XMLHandler.getTagAttribute( fieldNode, "class" ); Class<?> clazz = null; // primitive types require special handling if ( fieldClassName.equals( "boolean" ) ) { clazz = boolean.class; } else if ( fieldClassName.equals( "int" ) ) { clazz = int.class; } else if ( fieldClassName.equals( "float" ) ) { clazz = float.class; } else if ( fieldClassName.equals( "double" ) ) { clazz = double.class; } else if ( fieldClassName.equals( "long" ) ) { clazz = long.class; } else { // normal, non primitive array class clazz = Class.forName( fieldClassName ); } // get the child nodes for the field NodeList childrenNodes = fieldNode.getChildNodes(); // create a new, appropriately sized array int arrayLength = 0; for ( int i = 0; i < childrenNodes.getLength(); i++ ) { Node child = childrenNodes.item( i ); // ignore TEXT_NODE, they'll cause us to have a larger count than reality, even if they are empty if ( child.getNodeType() != Node.TEXT_NODE ) { arrayLength++; } } // create a new instance of our array Object array = Array.newInstance( clazz, arrayLength ); // set the new array on the field (on object, passed in) field.set( object, array ); int arrayIndex = 0; for ( int i = 0; i < childrenNodes.getLength(); i++ ) { Node child = childrenNodes.item( i ); if ( child.getNodeType() == Node.TEXT_NODE ) { continue; } // roll through all of our array elements setting them as encountered if ( String.class.isAssignableFrom( clazz ) || Number.class.isAssignableFrom( clazz ) ) { Constructor<?> constructor = clazz.getConstructor( String.class ); Object instance = constructor.newInstance( XMLHandler.getTagAttribute( child, "value" ) ); Array.set( array, arrayIndex++, instance ); } else if ( Boolean.class.isAssignableFrom( clazz ) || boolean.class.isAssignableFrom( clazz ) ) { Object value = Boolean.valueOf( XMLHandler.getTagAttribute( child, "value" ) ); Array.set( array, arrayIndex++, value ); } else if ( Integer.class.isAssignableFrom( clazz ) || int.class.isAssignableFrom( clazz ) ) { Object value = Integer.valueOf( XMLHandler.getTagAttribute( child, "value" ) ); Array.set( array, arrayIndex++, value ); } else if ( Float.class.isAssignableFrom( clazz ) || float.class.isAssignableFrom( clazz ) ) { Object value = Float.valueOf( XMLHandler.getTagAttribute( child, "value" ) ); Array.set( array, arrayIndex++, value ); } else if ( Double.class.isAssignableFrom( clazz ) || double.class.isAssignableFrom( clazz ) ) { Object value = Double.valueOf( XMLHandler.getTagAttribute( child, "value" ) ); Array.set( array, arrayIndex++, value ); } else if ( Long.class.isAssignableFrom( clazz ) || long.class.isAssignableFrom( clazz ) ) { Object value = Long.valueOf( XMLHandler.getTagAttribute( child, "value" ) ); Array.set( array, arrayIndex++, value ); } else { // create an instance of 'fieldClassName' Object instance = clazz.newInstance(); // add the instance to the array Array.set( array, arrayIndex++, instance ); // read child, the same way as the parent read( instance, child ); } } } catch ( Throwable t ) { t.printStackTrace(); // TODO: log this } } else if ( List.class.isAssignableFrom( field.getType() ) ) { // handle lists try { // get the node (if available) for the field Node fieldNode = XMLHandler.getSubNode( node, field.getName() ); if ( fieldNode == null ) { // doesn't exist (this is possible if fields were empty/null when persisted) continue; } // get the Java classname for the array elements String fieldClassName = XMLHandler.getTagAttribute( fieldNode, "class" ); Class<?> clazz = Class.forName( fieldClassName ); // create a new, appropriately sized array List<Object> list = new ArrayList<Object>(); field.set( object, list ); // iterate over all of the array elements and add them one by one as encountered NodeList childrenNodes = fieldNode.getChildNodes(); for ( int i = 0; i < childrenNodes.getLength(); i++ ) { Node child = childrenNodes.item( i ); if ( child.getNodeType() == Node.TEXT_NODE ) { continue; } // create an instance of 'fieldClassName' if ( String.class.isAssignableFrom( clazz ) || Number.class.isAssignableFrom( clazz ) || Boolean.class.isAssignableFrom( clazz ) ) { Constructor<?> constructor = clazz.getConstructor( String.class ); Object instance = constructor.newInstance( XMLHandler.getTagAttribute( child, "value" ) ); list.add( instance ); } else { // read child, the same way as the parent Object instance = clazz.newInstance(); // add the instance to the array list.add( instance ); read( instance, child ); } } } catch ( Throwable t ) { t.printStackTrace(); // TODO: log this } } else { // we're handling a regular field (not an array or list) try { Object value = XMLHandler.getTagValue( node, field.getName() ); if ( value == null ) { continue; } // System.out.println("Setting " + field.getName() + "(" + field.getType().getSimpleName() + ") = " + value + // " on: " + object.getClass().getName()); if ( !( field.getType().isPrimitive() && "".equals( value ) ) ) { // skip setting of primitives if we see null if ( "".equals( value ) ) { field.set( object, value ); } else if ( field.getType().isPrimitive() ) { // special primitive handling if ( double.class.isAssignableFrom( field.getType() ) ) { field.set( object, Double.parseDouble( value.toString() ) ); } else if ( float.class.isAssignableFrom( field.getType() ) ) { field.set( object, Float.parseFloat( value.toString() ) ); } else if ( long.class.isAssignableFrom( field.getType() ) ) { field.set( object, Long.parseLong( value.toString() ) ); } else if ( int.class.isAssignableFrom( field.getType() ) ) { field.set( object, Integer.parseInt( value.toString() ) ); } else if ( byte.class.isAssignableFrom( field.getType() ) ) { field.set( object, value.toString().getBytes() ); } else if ( boolean.class.isAssignableFrom( field.getType() ) ) { field.set( object, "true".equalsIgnoreCase( value.toString() ) ); } } else if ( String.class.isAssignableFrom( field.getType() ) || Number.class.isAssignableFrom( field.getType() ) ) { Constructor<?> constructor = field.getType().getConstructor( String.class ); Object instance = constructor.newInstance( value ); field.set( object, instance ); } else { // we don't know what we're handling, but we'll give it a shot Node fieldNode = XMLHandler.getSubNode( node, field.getName() ); if ( fieldNode == null ) { // doesn't exist (this is possible if fields were empty/null when persisted) continue; } // get the Java classname for the array elements String fieldClassName = XMLHandler.getTagAttribute( fieldNode, "class" ); Class<?> clazz = Class.forName( fieldClassName ); Object instance = clazz.newInstance(); field.set( object, instance ); read( instance, fieldNode ); } } } catch ( Throwable t ) { // TODO: log this t.printStackTrace(); } } } } /** * This method will perform the work that used to be done by hand in each kettle input meta for: getXML(). We handle * all primitive types, complex user types, arrays, lists and any number of nested object levels, via recursion of * this method. * * @param object * @param buffer */ @SuppressWarnings( "unchecked" ) public static void write( Object object, int indentLevel, StringBuilder buffer ) { // don't even attempt to persist if ( object == null ) { return; } // get this classes fields, public, private, protected, package, everything Field[] fields = object.getClass().getFields(); for ( Field field : fields ) { // ignore fields which are final, static or transient if ( Modifier.isFinal( field.getModifiers() ) || Modifier.isStatic( field.getModifiers() ) || Modifier.isTransient( field.getModifiers() ) ) { continue; } // if the field is not accessible (private), we'll open it up so we can operate on it if ( !field.isAccessible() ) { field.setAccessible( true ); } try { Object fieldValue = field.get( object ); // no value? null? skip it! if ( fieldValue == null || "".equals( fieldValue ) ) { continue; } if ( field.getType().isPrimitive() || String.class.isAssignableFrom( field.getType() ) || Number.class.isAssignableFrom( field.getType() ) ) { indent( buffer, indentLevel ); buffer.append( XMLHandler.addTagValue( field.getName(), fieldValue.toString() ) ); } else if ( field.getType().isArray() ) { // write array values int length = Array.getLength( fieldValue ); // open node (add class name attribute) indent( buffer, indentLevel ); buffer .append( "<" + field.getName() + " class=\"" + fieldValue.getClass().getComponentType().getName() + "\">" ) .append( Const.CR ); for ( int i = 0; i < length; i++ ) { Object childObject = Array.get( fieldValue, i ); // handle all strings/numbers if ( String.class.isAssignableFrom( childObject.getClass() ) || Number.class.isAssignableFrom( childObject.getClass() ) ) { indent( buffer, indentLevel + 1 ); buffer.append( "<" ).append( fieldValue.getClass().getComponentType().getSimpleName() ); buffer.append( " value=\"" + childObject.toString() + "\"/>" ).append( Const.CR ); } else if ( Boolean.class.isAssignableFrom( childObject.getClass() ) || boolean.class.isAssignableFrom( childObject.getClass() ) ) { // handle booleans (special case) indent( buffer, indentLevel + 1 ); buffer.append( "<" ).append( fieldValue.getClass().getComponentType().getSimpleName() ); buffer.append( " value=\"" + childObject.toString() + "\"/>" ).append( Const.CR ); } else { // array element is a user defined/complex type, recurse into it indent( buffer, indentLevel + 1 ); buffer.append( "<" + fieldValue.getClass().getComponentType().getSimpleName() + ">" ).append( Const.CR ); write( childObject, indentLevel + 1, buffer ); indent( buffer, indentLevel + 1 ); buffer.append( "</" + fieldValue.getClass().getComponentType().getSimpleName() + ">" ).append( Const.CR ); } } // close node buffer.append( " </" + field.getName() + ">" ).append( Const.CR ); } else if ( List.class.isAssignableFrom( field.getType() ) ) { // write list values List<Object> list = (List<Object>) fieldValue; if ( list.size() == 0 ) { continue; } Class<?> listClass = list.get( 0 ).getClass(); // open node (add class name attribute) indent( buffer, indentLevel ); buffer.append( "<" + field.getName() + " class=\"" + listClass.getName() + "\">" ).append( Const.CR ); for ( Object childObject : list ) { // handle all strings/numbers if ( String.class.isAssignableFrom( childObject.getClass() ) || Number.class.isAssignableFrom( childObject.getClass() ) ) { indent( buffer, indentLevel + 1 ); buffer.append( "<" ).append( listClass.getSimpleName() ); buffer.append( " value=\"" + childObject.toString() + "\"/>" ).append( Const.CR ); } else if ( Boolean.class.isAssignableFrom( childObject.getClass() ) || boolean.class.isAssignableFrom( childObject.getClass() ) ) { // handle booleans (special case) indent( buffer, indentLevel + 1 ); buffer.append( "<" ).append( listClass.getSimpleName() ); buffer.append( " value=\"" + childObject.toString() + "\"/>" ).append( Const.CR ); } else { // array element is a user defined/complex type, recurse into it indent( buffer, indentLevel + 1 ); buffer.append( "<" + listClass.getSimpleName() + ">" ).append( Const.CR ); write( childObject, indentLevel + 1, buffer ); indent( buffer, indentLevel + 1 ); buffer.append( "</" + listClass.getSimpleName() + ">" ).append( Const.CR ); } } // close node indent( buffer, indentLevel ); buffer.append( "</" + field.getName() + ">" ).append( Const.CR ); } else { // if we don't now what it is, let's treat it like a first class citizen and try to write it out // open node (add class name attribute) indent( buffer, indentLevel ); buffer.append( "<" + field.getName() + " class=\"" + fieldValue.getClass().getName() + "\">" ).append( Const.CR ); write( fieldValue, indentLevel + 1, buffer ); // close node indent( buffer, indentLevel ); buffer.append( "</" + field.getName() + ">" ).append( Const.CR ); } } catch ( Throwable t ) { t.printStackTrace(); // TODO: log this } } } /** * Handle saving of the input (object) to the kettle repository using the most simple method available, by calling * write and then saving the job-xml as a job attribute. * * @param object * @param rep * @param id_transformation * @param id_step * @throws KettleException */ public static void saveJobRep( Object object, Repository rep, ObjectId id_job, ObjectId id_job_entry ) throws KettleException { StringBuilder sb = new StringBuilder( 1024 ); write( object, 0, sb ); rep.saveJobEntryAttribute( id_job, id_job_entry, "job-xml", sb.toString() ); } /** * Handle reading of the input (object) from the kettle repository by getting the job-xml from the repository step * attribute string and then re-hydrate the job entry (object) with our already existing read method. * * @param object * @param rep * @param id_step * @param databases * @throws KettleException */ public static void readJobRep( Object object, Repository rep, ObjectId id_step, List<DatabaseMeta> databases ) throws KettleException { try { String jobXML = rep.getJobEntryAttributeString( id_step, "job-xml" ); ByteArrayInputStream bais = new ByteArrayInputStream( jobXML.getBytes() ); Document doc = XMLParserFactoryProducer.createSecureDocBuilderFactory().newDocumentBuilder().parse( bais ); read( object, doc.getDocumentElement() ); } catch ( ParserConfigurationException ex ) { throw new KettleException( ex.getMessage(), ex ); } catch ( SAXException ex ) { throw new KettleException( ex.getMessage(), ex ); } catch ( IOException ex ) { throw new KettleException( ex.getMessage(), ex ); } } /** * Handle saving of the input (object) to the kettle repository using the most simple method available, by calling * write and then saving the step-xml as a step attribute. * * @param object * @param rep * @param id_transformation * @param id_step * @throws KettleException */ public static void saveStepRep( Object object, Repository rep, ObjectId id_transformation, ObjectId id_step ) throws KettleException { StringBuilder sb = new StringBuilder( 1024 ); write( object, 0, sb ); rep.saveStepAttribute( id_transformation, id_step, "step-xml", sb.toString() ); } /** * Handle reading of the input (object) from the kettle repository by getting the step-xml from the repository step * attribute string and then re-hydrate the step (object) with our already existing read method. * * @param object * @param rep * @param id_step * @param databases * @param counters * @throws KettleException */ public static void readStepRep( Object object, Repository rep, ObjectId id_step, List<DatabaseMeta> databases ) throws KettleException { try { String stepXML = rep.getStepAttributeString( id_step, "step-xml" ); ByteArrayInputStream bais = new ByteArrayInputStream( stepXML.getBytes() ); Document doc = XMLParserFactoryProducer.createSecureDocBuilderFactory().newDocumentBuilder().parse( bais ); read( object, doc.getDocumentElement() ); } catch ( ParserConfigurationException ex ) { throw new KettleException( ex.getMessage(), ex ); } catch ( SAXException ex ) { throw new KettleException( ex.getMessage(), ex ); } catch ( IOException ex ) { throw new KettleException( ex.getMessage(), ex ); } } private static void indent( StringBuilder sb, int indentLevel ) { for ( int i = 0; i < indentLevel; i++ ) { sb.append( INDENT_STRING ); } } }