/*
* Copyright 2000-2013 Enonic AS
* http://www.enonic.com/license
*/
package com.enonic.cms.core.content.imports;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.jdom.Document;
import org.jdom.transform.JDOMSource;
import net.sf.saxon.Configuration;
import net.sf.saxon.event.Receiver;
import net.sf.saxon.om.InscopeNamespaceResolver;
import net.sf.saxon.om.NamespaceResolver;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.sxpath.XPathEvaluator;
import net.sf.saxon.sxpath.XPathExpression;
import net.sf.saxon.trans.XPathException;
import com.enonic.cms.framework.xml.XMLDocument;
import com.enonic.cms.framework.xml.XMLDocumentFactory;
import com.enonic.cms.core.content.contenttype.CtyImportBlockConfig;
import com.enonic.cms.core.content.contenttype.CtyImportConfig;
import com.enonic.cms.core.content.contenttype.CtyImportMappingConfig;
import com.enonic.cms.core.content.imports.sourcevalueholders.AbstractSourceValue;
import com.enonic.cms.core.content.imports.sourcevalueholders.BinarySourceValue;
import com.enonic.cms.core.content.imports.sourcevalueholders.StringArraySourceValue;
import com.enonic.cms.core.content.imports.sourcevalueholders.StringSourceValue;
// TODO: improve implementation: read as we go
public class ImportDataReaderXml
extends AbstractImportDataReader
{
private final List<ImportDataEntry> entries = new ArrayList<ImportDataEntry>();
private final XPathEvaluator evaluator = new XPathEvaluator();
private ImportDataEntry prefetchedNextDataEntry;
public ImportDataReaderXml( final CtyImportConfig config, final InputStream data )
{
super( config );
try
{
readData( data );
}
catch ( Throwable e )
{
throw new ImportException( "Could not read import data: " + e.getMessage(), e );
}
}
public ImportDataEntry getNextEntry()
{
return fetchNextEntry();
}
public boolean hasMoreEntries()
{
ImportDataEntry next;
if ( prefetchedNextDataEntry != null )
{
next = prefetchedNextDataEntry;
}
else
{
next = fetchNextEntry();
prefetchedNextDataEntry = next;
}
return next != null;
}
private ImportDataEntry fetchNextEntry()
{
if ( prefetchedNextDataEntry != null )
{
ImportDataEntry next = prefetchedNextDataEntry;
prefetchedNextDataEntry = null;
return next;
}
if ( entries.size() == 0 )
{
return null;
}
return entries.remove( 0 );
}
private void readData( final InputStream data )
throws Exception
{
final List<NodeInfo> baseNodes = getBaseNodes( data );
if ( baseNodes.size() == 0 )
{
throw new ImportException( "No elements found at base: " + config.getBase() );
}
for ( NodeInfo baseNode : baseNodes )
{
final ImportDataEntry entry = new ImportDataEntry( config.getSyncMapping() );
addMappings( baseNode, entry );
addMetadataMappings( baseNode, entry );
addBlocks( baseNode, entry );
entries.add( entry );
}
}
private List<NodeInfo> getBaseNodes( final InputStream data )
throws Exception
{
final XMLDocument tep = XMLDocumentFactory.create( new InputStreamReader( data, "UTF-8" ) );
final Document doc = tep.getAsJDOMDocument();
this.evaluator.setDefaultElementNamespace( doc.getRootElement().getNamespace().getURI() );
final XPathExpression exprRoot = this.evaluator.createExpression( "*" );
final Object resultRoot = exprRoot.evaluateSingle( new JDOMSource( doc.getRootElement() ) );
return getBaseNodes( (NodeInfo) resultRoot, config.getBase() );
}
private List<NodeInfo> getBaseNodes( final NodeInfo nodeInfo, final String xpath )
throws Exception
{
final XPathExpression exprBase = getXPathExpression( nodeInfo, xpath );
final List resultBase = exprBase.evaluate( nodeInfo );
final List<NodeInfo> importEls = new LinkedList<NodeInfo>();
for ( final Object o : resultBase )
{
if ( o instanceof NodeInfo )
{
importEls.add( ( (NodeInfo) o ) );
}
}
return importEls;
}
private void addMappings( final NodeInfo nodeInfo, final ImportDataEntry entry )
throws Exception
{
addMappings( nodeInfo, entry, config.getMappings(), null );
}
private void addMappings( final NodeInfo nodeInfo, final ImportDataEntry entry, final List<CtyImportMappingConfig> mapings,
final String base )
throws Exception
{
for ( final CtyImportMappingConfig mapping : mapings )
{
final AbstractSourceValue value = getSourceValue( nodeInfo, mapping, base );
if ( value != null )
{
entry.add( mapping, value );
}
}
}
private void addMetadataMappings( final NodeInfo nodeInfo, final ImportDataEntry entry )
throws Exception
{
for ( final CtyImportMappingConfig metadataMapping : config.getMetadataMappings() )
{
final AbstractSourceValue value = getSourceValue( nodeInfo, metadataMapping, null );
if ( value != null )
{
entry.addMetadata( metadataMapping, value );
}
}
}
private void addBlocks( final NodeInfo nodeInfo, final ImportDataEntry entry )
throws Exception
{
for ( final CtyImportBlockConfig block : config.getBlocks() )
{
if ( block.getDestination() == null )
{
addMappings( nodeInfo, entry, block.getMappings(), block.getBase() );
}
else
{
for ( final NodeInfo blockNode : getBaseNodes( nodeInfo, block.getBase() ) )
{
final ImportDataEntry blockEntry = new ImportDataEntry( block.getSyncMapping() );
addMappings( blockNode, blockEntry, block.getMappings(), null );
entry.addBlock( block, blockEntry );
}
}
}
}
private AbstractSourceValue getSourceValue( final NodeInfo nodeInfo, final CtyImportMappingConfig mapping, final String base )
throws Exception
{
AbstractSourceValue value = null;
final String xpath = getXpathValue( base, mapping.getSource() );
if ( !mapping.isMetaDataMapping() && mapping.isBinary() )
{
final byte[] byteValue = getBinaryValue( nodeInfo, xpath );
if ( byteValue != null )
{
value = new BinarySourceValue( byteValue );
}
}
else if ( !mapping.isMetaDataMapping() && mapping.isXml() )
{
final String xmlValue = getXmlValue( nodeInfo, xpath );
if ( xmlValue != null )
{
value = new StringSourceValue( xmlValue );
}
}
else if ( !mapping.isMetaDataMapping() && mapping.isHtml() )
{
final String htmlValue = getHtmlValue( nodeInfo, xpath );
if ( htmlValue != null )
{
value = new StringSourceValue( htmlValue );
}
}
else if ( !mapping.isMetaDataMapping() && mapping.isMultiple() )
{
final LinkedHashSet<String> stringsValue = getStringValues( nodeInfo, xpath );
if ( stringsValue != null )
{
value = new StringArraySourceValue( stringsValue );
}
}
else
{
final String stringValue = getStringValue( nodeInfo, xpath );
if ( stringValue != null )
{
value = new StringSourceValue( stringValue );
}
}
if ( value != null && mapping.hasAdditionalSource() )
{
value.setAdditionalValue( getStringValue( nodeInfo, getXpathValue( base, mapping.getAdditionalSource() ) ) );
}
return value;
}
private String getXpathValue( final String base, final String source )
{
String xpath = base;
if ( xpath == null )
{
return source;
}
if ( !xpath.endsWith( "/" ) )
{
xpath += "/";
}
return xpath + source;
}
private byte[] getBinaryValue( final NodeInfo nodeInfo, final String xpath )
throws XPathException, DecoderException
{
String base64String = getStringValue( nodeInfo, xpath );
if ( base64String == null )
{
return null;
}
return Base64.decodeBase64( base64String.getBytes() );
}
private String getStringValue( final NodeInfo nodeInfo, final String xpath )
throws XPathException
{
final XPathExpression expr = getXPathExpression( nodeInfo, xpath );
final Object o = expr.evaluateSingle( nodeInfo );
return getStringValue( o );
}
private String getHtmlValue( final NodeInfo nodeInfo, final String xpath )
throws XPathException
{
final XPathExpression expr = getXPathExpression( nodeInfo, xpath + "/node()" );
if ( expr.evaluateSingle( nodeInfo ) == null )
{
return null;
}
final List<Object> nodes = expr.evaluate( nodeInfo );
return getHtmlValue( nodes );
}
private String getXmlValue( final NodeInfo nodeInfo, final String xpath )
throws XPathException
{
final XPathExpression expr = getXPathExpression( nodeInfo, xpath + "/descendant::*" );
final Object o = expr.evaluateSingle( nodeInfo );
return getXmlValue( o );
}
private LinkedHashSet<String> getStringValues( final NodeInfo nodeInfo, final String xpath )
throws XPathException
{
final XPathExpression expr = getXPathExpression( nodeInfo, xpath );
final List<Object> os = expr.evaluate( nodeInfo );
final LinkedHashSet<String> values = new LinkedHashSet<String>();
for ( Object o : os )
{
values.add( getStringValue( o ) );
}
return values;
}
private XPathExpression getXPathExpression( final NodeInfo nodeInfo, final String xpath )
throws XPathException
{
final NamespaceResolver resolver = new CombinedNamespaceResolver( getNamespaceResolvers( nodeInfo ) );
this.evaluator.setNamespaceResolver( resolver );
return this.evaluator.createExpression( xpath );
}
private String getStringValue( final Object o )
{
if ( o == null )
{
return null;
}
if ( o instanceof NodeInfo )
{
return ( (NodeInfo) o ).getStringValue();
}
return String.valueOf( o );
}
private String getHtmlValue( final List<Object> nodes )
throws XPathException
{
StringBuffer xhtml = new StringBuffer();
for ( Object node : nodes )
{
xhtml.append( serialize( node, "xhtml", false ) );
}
return xhtml.toString();
}
private String getXmlValue( final Object o )
throws XPathException
{
return serialize( o, "xml", false );
}
private String serialize( final Object o, final String method, final boolean includeProlog )
throws XPathException
{
if ( o instanceof NodeInfo )
{
final NodeInfo nodeInfo = ( (NodeInfo) o );
final Configuration config = nodeInfo.getConfiguration();
final StringWriter sw = new StringWriter();
final Properties props = new Properties();
props.setProperty( "method", method );
props.setProperty( "indent", "no" );
if ( !includeProlog )
{
props.setProperty( "omit-xml-declaration", "yes" );
}
else
{
props.setProperty( "omit-xml-declaration", "no" );
}
final Receiver serializer =
config.getSerializerFactory().getReceiver( new StreamResult( sw ), config.makePipelineConfiguration(), props );
nodeInfo.copy( serializer, NodeInfo.ALL_NAMESPACES, false, 0 );
return sw.toString();
}
return null;
}
private Set<NamespaceResolver> getNamespaceResolvers( final NodeInfo nodeInfo )
{
final Set<NamespaceResolver> resolvers = new HashSet<NamespaceResolver>();
// From import data
resolvers.add( new InscopeNamespaceResolver( nodeInfo ) );
// From import definition
final NamespaceResolver importDefNamespace = config.getNamespaceResolver();
if ( importDefNamespace != null )
{
resolvers.add( importDefNamespace );
}
// Default
final NamespaceResolver defaultNamespace = new XPathEvaluator().getNamespaceResolver();
if ( defaultNamespace != null )
{
resolvers.add( defaultNamespace );
}
return resolvers;
}
private class CombinedNamespaceResolver
implements NamespaceResolver
{
final private Set<NamespaceResolver> resolvers;
private CombinedNamespaceResolver( final Set<NamespaceResolver> resolvers )
{
this.resolvers = resolvers;
}
public Iterator iteratePrefixes()
{
return null;
}
public String getURIForPrefix( final String s, final boolean b )
{
for ( NamespaceResolver resolver : resolvers )
{
final String ns = resolver.getURIForPrefix( s, b );
if ( ns != null )
{
return ns;
}
}
return null;
}
}
}