/*
* Copyright 2000-2013 Enonic AS
* http://www.enonic.com/license
*/
package com.enonic.cms.core.content.contenttype;
import com.enonic.cms.core.content.contenttype.dataentryconfig.DataEntryConfig;
import com.enonic.cms.core.content.contenttype.dataentryconfig.DataEntryConfigType;
import com.enonic.cms.core.content.contenttype.dataentryconfig.RelatedContentDataEntryConfig;
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 org.apache.commons.lang.StringUtils;
import org.jdom.Attribute;
import org.jdom.Element;
import org.jdom.transform.JDOMSource;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ContentTypeImportConfigParser
{
private String importName;
public static List<CtyImportConfig> parseAllImports( final CtyFormConfig form, final Element configEl )
throws InvalidImportConfigException
{
final List<CtyImportConfig> importConfigs = new ArrayList<CtyImportConfig>();
final Element importsEl = configEl.getChild( "imports" );
if ( importsEl != null )
{
final List<Element> importEls = importsEl.getChildren( "import" );
for ( Element importEl : importEls )
{
final String importName = importEl.getAttributeValue( "name" );
if ( importName == null || importName.length() == 0 )
{
throw new InvalidImportConfigException( "Could not find required attribute \"name\" in an import configuration." );
}
ContentTypeImportConfigParser parser = new ContentTypeImportConfigParser( importName );
importConfigs.add( parser.parseImport( form, importEl ) );
}
}
verifyImports( importConfigs, configEl );
return importConfigs;
}
private ContentTypeImportConfigParser( String importName )
{
this.importName = importName;
}
private CtyImportConfig parseImport( final CtyFormConfig form, final Element importEl )
throws InvalidImportConfigException
{
final String syncInputField = importEl.getAttributeValue( "sync" );
final CtyImportConfig importConfig = new CtyImportConfig( form, importName, syncInputField, getNamespaceResolver( importEl ) );
parseModeSetting( importEl, importConfig );
if ( importConfig.getMode() == CtyImportModeConfig.CSV )
{
parseCSVSeparator( importEl, importConfig );
parseCSVSkip( importEl, importConfig );
if ( importEl.getAttribute( "base" ) != null )
{
throw new InvalidImportConfigException( importName,
"The base setting is only used when the import source is of XML format." );
}
}
else if ( importConfig.getMode() == CtyImportModeConfig.XML )
{
if ( importEl.getAttribute( "separator" ) != null )
{
throw new InvalidImportConfigException( importName,
"The separator setting is only used when the import source is of CSV format." );
}
if ( importEl.getAttribute( "skip" ) != null )
{
throw new InvalidImportConfigException( importName,
"The skip setting is only used when the import source is of CSV format." );
}
parseBase( importEl, importConfig );
}
final String diff = importEl.getAttributeValue( "diff" );
if ( diff != null )
{
throw new InvalidImportConfigException( importName, "The diff setting is obsolote, please remove it." );
}
if ( !importConfig.isSyncEnabled() && importEl.getAttribute( "purge" ) != null )
{
throw new InvalidImportConfigException( importName, "Purge setting is only needed when synchronization is enabled." );
}
final CtyImportPurgeConfig impurtPurgeConfig =
CtyImportPurgeConfig.parse( importEl.getAttributeValue( "purge" ), importConfig.getName() );
importConfig.setPurge( impurtPurgeConfig );
final CtyImportStatusConfig importStatusConfig = CtyImportStatusConfig.parse( importName, importEl.getAttributeValue( "status" ) );
importConfig.setStatus( importStatusConfig );
Attribute updateStrategyAttribute = importEl.getAttribute( "update-strategy" );
if ( updateStrategyAttribute != null )
{
if ( StringUtils.isEmpty( updateStrategyAttribute.getValue() ) )
{
throw new InvalidImportConfigException( importName, "Missing value for 'update-strategy' setting" );
}
importConfig.setUpdateStrategy( CtyImportUpdateStrategyConfig.parse( importName, updateStrategyAttribute.getValue() ) );
}
parseUpdateContentNameSetting( importConfig, importEl );
parseImportMappings( importConfig, importEl, importEl );
parseImportMappingBlocks( importConfig, importEl );
verifyImportConfig( importConfig );
return importConfig;
}
private void verifyImportConfig( CtyImportConfig importConfig )
{
verifyReferredSyncFieldExist( importConfig );
verifyMappingDestinationsExist( importConfig );
verifyMappingSettingEXSRCIsOnlyUsedWhenInputTypeIsImageOrUploadfile( importConfig );
final CtyFormConfig form = importConfig.getForm();
/* Check that mapping to binary type is not used in cvs import */
if ( importConfig.getMode() == CtyImportModeConfig.CSV )
{
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
final DataEntryConfigType destinationType = form.getInputConfig( mappingConfig.getDestination() ).getType();
if ( destinationType == DataEntryConfigType.BINARY )
{
throw new InvalidImportConfigException( importName,
"Mapping to content input field '" + mappingConfig.getDestination() +
"' is not allowed." + " Mapping to fields of type '" + destinationType +
"' is only allowed when importing from XML formatted sources." );
}
}
}
}
private void verifyMappingSettingEXSRCIsOnlyUsedWhenInputTypeIsImageOrUploadfile( CtyImportConfig importConfig )
{
final CtyFormConfig form = importConfig.getForm();
/* Check that mapping attribute exsrc is only used on mappings of type image or binary (used as imagetext and binaryname) */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
final DataEntryConfigType destinationType = form.getInputConfig( mappingConfig.getDestination() ).getType();
if ( mappingConfig.getAdditionalSource() != null &&
!( destinationType == DataEntryConfigType.IMAGE || destinationType == DataEntryConfigType.BINARY ) )
{
throw new InvalidImportConfigException( importName,
"Mapping setting exsrc is only applicaple for mappings whose destinations refers to a content input field of type '" +
DataEntryConfigType.IMAGE.getName() + "' or \'" +
DataEntryConfigType.BINARY.getName() + "'. Type was: " + destinationType );
}
}
}
private void verifyMappingDestinationsExist( CtyImportConfig importConfig )
{
final CtyFormConfig form = importConfig.getForm();
/* Check that import mappings exist */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
String dest = mappingConfig.getDestination();
if ( form.getInputConfig( dest ) == null )
{
throw new InvalidImportConfigException( importName, "Mapping destination (content input field) does not exist: " + dest );
}
}
}
private void verifyReferredSyncFieldExist( CtyImportConfig importConfig )
{
if ( importConfig.isSyncEnabled() && !importConfig.isSyncMappedToContentKey() && importConfig.getSyncMapping() == null )
{
throw new InvalidImportConfigException( importName, "Referred input field '" + importConfig.getSync() +
"' in sync setting does not exist." );
}
}
private void parseBase( Element importEl, CtyImportConfig importConfig )
{
final String base = importEl.getAttributeValue( "base" );
if ( base != null )
{
importConfig.setBase( base );
}
}
private void parseCSVSkip( Element importEl, CtyImportConfig importConfig )
{
final String skip = importEl.getAttributeValue( "skip" );
if ( skip != null )
{
importConfig.setSkip( Integer.valueOf( skip ) );
}
}
private void parseCSVSeparator( Element importEl, CtyImportConfig importConfig )
{
final String separator = importEl.getAttributeValue( "separator" );
if ( separator != null )
{
importConfig.setSeparator( separator );
}
}
private void parseModeSetting( Element importEl, CtyImportConfig importConfig )
{
final String mode = importEl.getAttributeValue( "mode" );
final CtyImportModeConfig impurtModeConfig = CtyImportModeConfig.parse( mode );
if ( mode == null )
{
throw new InvalidImportConfigException( importName, "Invalid mode setting: " + mode );
}
importConfig.setMode( impurtModeConfig );
}
private void parseUpdateContentNameSetting( CtyImportConfig importConfig, Element importEl )
{
final String settingAsString = importEl.getAttributeValue( "update-content-name", (String) null );
if ( StringUtils.isEmpty( settingAsString ) )
{
return;
}
if ( "true".equals( settingAsString ) )
{
importConfig.setUpdateContentName( true );
}
else if ( "false".equals( settingAsString ) )
{
importConfig.setUpdateContentName( false );
}
else
{
throw new InvalidImportConfigException( importName,
"Setting 'update-content-name' must either be true or false or be removed: " +
settingAsString );
}
}
private NamespaceResolver getNamespaceResolver( final Element importEl )
{
try
{
final XPathEvaluator evaluator = new XPathEvaluator();
final XPathExpression expr = evaluator.createExpression( "*" );
final Object o = expr.evaluateSingle( new JDOMSource( importEl ) );
if ( o instanceof NodeInfo )
{
NodeInfo node = (NodeInfo) o;
if ( node != null )
{
return new InscopeNamespaceResolver( node );
}
}
}
catch ( XPathException ex )
{
}
return null;
}
private void parseImportMappings( final CtyImportMappingContainer mappingContainer, final Element importEl, final Element mappingsEl )
{
final List<Element> mappingEls = mappingsEl.getChildren( "mapping" );
if ( mappingEls.size() == 0 )
{
throw new InvalidImportConfigException( importName, "No mapping elements found." );
}
int mappingCount = 0;
for ( final Element mappingEl : mappingEls )
{
mappingCount++;
final CtyImportMappingConfig mapping = parseImportMapping( mappingContainer, mappingEl, mappingCount );
final boolean mappingAdded = mappingContainer.addMapping( mapping );
if ( !mappingAdded )
{
throw new InvalidImportConfigException( importName,
"Duplicate mapping destination \"" + mapping.getDestination() + "\" found." );
}
}
}
private void parseImportMappingBlocks( final CtyImportConfig importConfig, final Element importEl )
{
final List<Element> blockEls = importEl.getChildren( "block" );
if ( blockEls.size() > 0 && importConfig.getMode() == CtyImportModeConfig.CSV )
{
throw new InvalidImportConfigException( importName, "The \"block\" functionality cannot be used in csv mode" );
}
for ( final Element blockEl : blockEls )
{
final String base = blockEl.getAttributeValue( "base" );
if ( base == null )
{
throw new InvalidImportConfigException( importName, "Could not find the required \"base\" attribute in block" );
}
final String sync = blockEl.getAttributeValue( "sync" );
final String dest = blockEl.getAttributeValue( "dest" );
String purgeStr = blockEl.getAttributeValue( "purge" );
boolean purge = false;
if ( purgeStr != null )
{
purge = Boolean.parseBoolean( purgeStr );
}
final CtyImportBlockConfig block = new CtyImportBlockConfig( importConfig, base, dest, sync, purge );
parseImportMappings( block, importEl, blockEl );
importConfig.addBlock( block );
}
}
private CtyImportMappingConfig parseImportMapping( final CtyImportMappingContainer mappingContainer, final Element mappingEl,
final int mappingCount )
{
final String dest = mappingEl.getAttributeValue( "dest" );
if ( StringUtils.isEmpty( dest ) )
{
throw new InvalidImportConfigException( importName,
"Missing destination attribute (\"dest\") in mapping number " + mappingCount + "." );
}
final String src = mappingEl.getAttributeValue( "src" );
if ( StringUtils.isEmpty( src ) )
{
throw new InvalidImportConfigException( importName,
"Missing source attribute (\"src\") in mapping number " + mappingCount + "." );
}
final CtyImportMappingConfig mapping = new CtyImportMappingConfig( mappingContainer.getImportConfig(), src, dest );
final String relatedContentType = mappingEl.getAttributeValue( "relatedcontenttype" );
if ( relatedContentType != null )
{
mapping.setRelatedContentType( relatedContentType );
}
String relatedField = mappingEl.getAttributeValue( "relatedfield" );
if ( relatedField != null )
{
mapping.setRelatedField( relatedField );
}
String format = mappingEl.getAttributeValue( "format" );
if ( format != null )
{
try
{
String s = new SimpleDateFormat( format ).format( new Date() );
}
catch ( IllegalArgumentException ex )
{
throw new InvalidImportConfigException( importName, "Illegal \"format\" value: " + format + " in import \"" +
mappingContainer.getName() + "\"" );
}
mapping.setFormat( format );
}
String separator = mappingEl.getAttributeValue( "separator" );
if ( separator != null )
{
mapping.setSeparator( separator );
}
String additionalSrc = mappingEl.getAttributeValue( "exsrc" );
if ( additionalSrc != null )
{
mapping.setAdditionalSource( additionalSrc );
}
return mapping;
}
private static void verifyImports( final List<CtyImportConfig> importConfigs, final Element configEl )
{
for ( CtyImportConfig importConfig : importConfigs )
{
final CtyFormConfig form = importConfig.getForm();
/* Check that mapping attribute format is only used on mappings of type date */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
final DataEntryConfigType destinationType = form.getInputConfig( mappingConfig.getDestination() ).getType();
if ( mappingConfig.getFormat() != null && destinationType != DataEntryConfigType.DATE )
{
throw new InvalidContentTypeConfigException(
"Import mapping attribute \"format\" can only be specified for mapping of type \"" + DataEntryConfigType.DATE +
"\". Attribute found in mapping of type \"" + destinationType + "\" in import \"" + importConfig.getName() +
"\".", configEl );
}
}
/* Check meta data mapping */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMetadataMappings() )
{
String dest = mappingConfig.getDestination();
if ( !( dest.equals( "@publishfrom" ) || dest.equals( "@publishto" ) || dest.equals( "@key" ) ) )
{
throw new InvalidContentTypeConfigException(
"Invalid metadata mapping \"" + dest + "\" in import \"" + importConfig.getName() +
"\". Only \"@publishfrom\" and \"@publishto\" are supported.", configEl );
}
}
/* Check that mapping separator is not used in xml import */
if ( importConfig.getMode() == CtyImportModeConfig.XML )
{
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
if ( mappingConfig.getSeparator() != null )
{
throw new InvalidContentTypeConfigException(
"Invalid mapping attribute \"separator\" in mapping \"" + mappingConfig.getDestination() + "\" in import \"" +
importConfig.getName() + ". Attribute can only be specified for import of type \"" +
CtyImportModeConfig.CSV + "\". Attribute found in import of type \"" + CtyImportModeConfig.XML + "\".",
configEl );
}
}
}
else if ( importConfig.getMode() == CtyImportModeConfig.CSV )
{
/* Check that mapping separator is only used in mapping of "multiple" types (related content with mulitple = true
and keywords) and that it differ from import separator */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
final DataEntryConfig inputConfig = form.getInputConfig( mappingConfig.getDestination() );
final DataEntryConfigType destinationType = inputConfig.getType();
if ( mappingConfig.getSeparator() != null )
{
boolean multiple = false;
if ( destinationType == DataEntryConfigType.RELATEDCONTENT )
{
multiple = ( (RelatedContentDataEntryConfig) inputConfig ).isMultiple();
}
else if ( destinationType == DataEntryConfigType.KEYWORDS )
{
multiple = true;
}
if ( !multiple )
{
throw new InvalidContentTypeConfigException(
"Invalid mapping attribute \"separator\" in mapping \"" + mappingConfig.getDestination() +
"\" in import \"" + importConfig.getName() +
"\". Attribute can only be specified for mapping of type \"" + DataEntryConfigType.KEYWORDS +
"\" or \"" + DataEntryConfigType.RELATEDCONTENT + "\" (with \"multiple\" set to \"true\"). " +
"Attribute found in mapping of type \"" + destinationType + "\"" +
( destinationType == DataEntryConfigType.RELATEDCONTENT
? " (with \"multiple\" set to \"false\")"
: "" ) + ".", configEl );
}
if ( mappingConfig.getSeparator().equals( importConfig.getSeparator() ) )
{
throw new InvalidContentTypeConfigException(
"Invalid mapping attribute \"separator\" in mapping \"" + mappingConfig.getDestination() +
"\" in import \"" + importConfig.getName() +
"\". Mapping separator value must differ from import separator value.", configEl );
}
}
}
}
/* Check that mapping attribute "relatedcontenttype" is only used on mappings of type related content and that "relatedfield" are specified as well */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
if ( mappingConfig.getRelatedContentType() != null )
{
final DataEntryConfigType destinationType = form.getInputConfig( mappingConfig.getDestination() ).getType();
if ( destinationType != DataEntryConfigType.RELATEDCONTENT )
{
throw new InvalidContentTypeConfigException(
"Import mapping attribute \"relatedcontenttype\" can only be specified for mapping of type \"" +
DataEntryConfigType.RELATEDCONTENT + "\". Attribute found in mapping \"" + mappingConfig.getDestination() +
"\" of type \"" + destinationType + "\" in import \"" + importConfig.getName() + "\".", configEl );
}
if ( mappingConfig.getRelatedField() == null )
{
throw new InvalidContentTypeConfigException(
"Import mapping attribute \"relatedcontenttype\" cannot be specified without specifying mapping attribute \"relatedfield\". " +
"Attribute found in mapping \"" + mappingConfig.getDestination() + "\" in import \"" +
importConfig.getName() + "\".", configEl );
}
}
}
/* Check that mapping attribute "relatedfield" is only used on mappings of type related content and that "relatedcontenttype" are specified as well */
for ( CtyImportMappingConfig mappingConfig : importConfig.getMappings() )
{
if ( mappingConfig.getRelatedField() != null )
{
final DataEntryConfigType destinationType = form.getInputConfig( mappingConfig.getDestination() ).getType();
if ( destinationType != DataEntryConfigType.RELATEDCONTENT )
{
throw new InvalidContentTypeConfigException(
"Import mapping attribute \"relatedfield\" can only be specified for mapping of type \"" +
DataEntryConfigType.RELATEDCONTENT + "\". Attribute found in mapping \"" + mappingConfig.getDestination() +
"\" of type \"" + destinationType + "\" in import \"" + importConfig.getName() + "\".", configEl );
}
if ( mappingConfig.getRelatedContentType() == null )
{
throw new InvalidContentTypeConfigException(
"Import mapping attribute \"relatedfield\" cannot be specified without specifying mapping attribute \"relatedcontenttype\". " +
"Attribute found in mapping \"" + mappingConfig.getDestination() + "\" in import \"" +
importConfig.getName() + "\".", configEl );
}
}
}
for ( final CtyImportBlockConfig blockConfig : importConfig.getBlocks() )
{
/* Check that block attribute "destination" is of type "block group"*/
final String blockDest = blockConfig.getDestination();
if ( blockDest != null )
{
final CtySetConfig setConfig = form.getSetConfig( blockDest );
if ( setConfig == null )
{
throw new InvalidContentTypeConfigException(
"Could not find input config for destination \"" + blockDest + "\" in import \"" + importConfig.getName() +
"\".", configEl );
}
else if ( setConfig.getGroupXPath() == null )
{
throw new InvalidContentTypeConfigException(
"Invalid block destination \"" + blockDest + "\" in import \"" + importConfig.getName() + "\" found. " +
"The destination must be a block group.", configEl );
}
}
/* Check that block attribute "sync" exists */
final String blockSync = blockConfig.getSync();
if ( blockSync != null )
{
if ( form.getInputConfig( blockSync ) == null )
{
throw new InvalidContentTypeConfigException(
"Could not find input config for block sync mapping \"" + blockSync + "\" in import \"" +
importConfig.getName() + "\".", configEl );
}
}
}
}
}
}