001/*
002 *  Copyright 2014 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.contentio.in.xml;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.regex.Pattern;
027
028import javax.xml.transform.TransformerException;
029
030import org.apache.avalon.framework.configuration.Configuration;
031import org.apache.avalon.framework.configuration.ConfigurationException;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.commons.lang3.LocaleUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.w3c.dom.Document;
037import org.w3c.dom.Node;
038import org.w3c.dom.NodeList;
039
040import org.ametys.cms.contenttype.ContentTypesHelper;
041import org.ametys.cms.contenttype.MetadataDefinition;
042import org.ametys.cms.contenttype.RepeaterDefinition;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.repository.ModifiableContent;
045import org.ametys.plugins.contentio.ContentImporterHelper;
046import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
047import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
048import org.ametys.plugins.repository.version.VersionableAmetysObject;
049
050import com.opensymphony.workflow.WorkflowException;
051
052/**
053 * Default implementation of an XML content importer.
054 */
055public class DefaultXmlContentImporter extends AbstractXmlContentImporter
056{
057    
058    /** Default content types key in the import map. */
059    protected static final String _DEFAULT_CONTENT_TYPES_KEY = DefaultXmlContentImporter.class.getName() + "$defaultContentTypes";
060    /** Default content mixins key in the import map. */
061    protected static final String _DEFAULT_CONTENT_MIXINS_KEY = DefaultXmlContentImporter.class.getName() + "$defaultMixins";
062    /** Default content language key in the import map. */
063    protected static final String _DEFAULT_CONTENT_LANG_KEY = DefaultXmlContentImporter.class.getName() + "$defaultLanguage";
064    
065    /** The content type helper. */
066    protected ContentTypesHelper _cTypeHelper;
067    
068    /** The XPath expression to match. */
069    protected String _matchPath;
070    
071    /** The XPath value to match. */
072    protected String _matchValue;
073    
074    /** The XPath value to match. */
075    protected Pattern _matchRegex;
076    
077    @Override
078    public void service(ServiceManager serviceManager) throws ServiceException
079    {
080        super.service(serviceManager);
081        _cTypeHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
082    }
083    
084    @Override
085    public void configure(Configuration configuration) throws ConfigurationException
086    {
087        super.configure(configuration);
088        
089        configureXmlMatch(configuration.getChild("xml").getChild("match"));
090    }
091    
092    /**
093     * Configure the matching value.
094     * @param configuration the matching configuration.
095     * @throws ConfigurationException if an error occurs.
096     */
097    protected void configureXmlMatch(Configuration configuration) throws ConfigurationException
098    {
099        _matchPath = configuration.getAttribute("path");
100        _matchValue = configuration.getAttribute("value", null);
101        String regex = configuration.getAttribute("regex", null);
102        if (StringUtils.isNotBlank(regex))
103        {
104            _matchRegex = Pattern.compile(regex);
105        }
106    }
107    
108    @Override
109    public boolean supports(Document document) throws IOException
110    {
111        Node node = _xPathProcessor.selectSingleNode(document, _matchPath, getPrefixResolver());
112        
113        if (_matchValue == null && _matchRegex == null)
114        {
115            return node != null;
116        }
117        else if (_matchRegex != null)
118        {
119            String text = getTextContent(node, null, true);
120            return text != null ? _matchRegex.matcher(text).matches() : false;
121        }
122        else
123        {
124            return _matchValue.equals(getTextContent(node, null, true));
125        }
126    }
127    
128    @Override
129    protected Set<String> importContents(Document node, Map<String, Object> params) throws IOException
130    {
131        Set<String> contentIds = new HashSet<>();
132        
133        Node root = node.getFirstChild();
134        
135        if (root != null)
136        {
137            String[] defaultTypes = StringUtils.split(getAttributeValue(root, "default-types", ""), ", ");
138            String[] defaultMixins = StringUtils.split(getAttributeValue(root, "default-mixins", ""), ", ");
139            String defaultLang = getAttributeValue(root, "default-language", getLanguage(params));
140            if (defaultTypes.length == 0)
141            {
142                defaultTypes = getContentTypes(params);
143            }
144            if (defaultMixins.length == 0)
145            {
146                defaultMixins = getMixins(params);
147            }
148            
149            params.put(_DEFAULT_CONTENT_TYPES_KEY, defaultTypes);
150            params.put(_DEFAULT_CONTENT_MIXINS_KEY, defaultMixins);
151            params.put(_DEFAULT_CONTENT_LANG_KEY, defaultLang);
152            
153            // Import all the contents.
154            NodeList contents = _xPathProcessor.selectNodeList(root, "content", getPrefixResolver());
155            
156            for (int i = 0; i < contents.getLength(); i++)
157            {
158                Node contentNode = contents.item(i);
159                
160                try
161                {
162                    Content content = importContent(contentNode, defaultTypes, defaultMixins, defaultLang, params);
163                    
164                    if (content != null)
165                    {
166                        contentIds.add(content.getId());
167                    }
168                }
169                catch (WorkflowException e)
170                {
171                    getLogger().error("Error importing a content.", e);
172                }
173            }
174            
175            // Second pass: restore all the content links.
176            restoreContentReferences(params);
177            
178            params.remove(_DEFAULT_CONTENT_TYPES_KEY);
179            params.remove(_DEFAULT_CONTENT_MIXINS_KEY);
180            params.remove(_DEFAULT_CONTENT_LANG_KEY);
181        }
182        
183        return contentIds;
184    }
185    
186    /**
187     * Import a content from a XML node.
188     * @param contentNode the content XML node.
189     * @param defaultTypes the default content types.
190     * @param defaultMixins the default mixins.
191     * @param defaultLang the default content language.
192     * @param params the import parameters.
193     * @return the Content or null if not created.
194     * @throws IOException if an error occurs during the import.
195     * @throws WorkflowException if an error occurs creating the Content.
196     */
197    protected Content importContent(Node contentNode, String[] defaultTypes, String[] defaultMixins, String defaultLang, Map<String, Object> params) throws IOException, WorkflowException
198    {
199        String localId = getAttributeValue(contentNode, "id", "");
200        
201        String cTypesStr = getAttributeValue(contentNode, "types", "");
202        String mixinsStr = getAttributeValue(contentNode, "mixins", "");
203        
204        String[] contentTypes = StringUtils.isEmpty(cTypesStr) ? defaultTypes : StringUtils.split(cTypesStr, ", ");
205        String[] contentMixins = StringUtils.isEmpty(mixinsStr) ? defaultMixins : StringUtils.split(mixinsStr, ", ");
206        String language = getAttributeValue(contentNode, "language", defaultLang);
207        
208        String title = _xPathProcessor.evaluateAsString(contentNode, "metadata/title", getPrefixResolver());
209        
210        Content content = createContent(title, contentTypes, contentMixins, language, params);
211        
212        if (content instanceof ModifiableContent)
213        {
214            importMetadata((ModifiableContent) content, contentNode, "", params);
215            
216            ((ModifiableContent) content).saveChanges();
217            
218            if (content instanceof VersionableAmetysObject)
219            {
220                ((VersionableAmetysObject) content).checkpoint();
221            }
222        }
223        
224        // If the content contains a "local" ID (local to the import file), remember it.
225        if (StringUtils.isNotBlank(localId))
226        {
227            Map<String, String> contentIdMap = getContentIdMap(params);
228            contentIdMap.put(localId, content.getId());
229        }
230        
231        return content;
232    }
233    
234    /**
235     * Import metadata from a content node.
236     * @param content the content to populate.
237     * @param contentNode the content DOM node.
238     * @param prefix the metadata prefix.
239     * @param params the import parameters.
240     * @throws IOException if an error occurs.
241     */
242    protected void importMetadata(ModifiableContent content, Node contentNode, String prefix, Map<String, Object> params) throws IOException
243    {
244        try
245        {
246            Map<String, MetadataDefinition> metadataDefinitions = _cTypeHelper.getMetadataDefinitions(content.getTypes());
247            
248            ModifiableCompositeMetadata meta = content.getMetadataHolder();
249            
250            Node metadataRootNode = _xPathProcessor.selectSingleNode(contentNode, "metadata", getPrefixResolver());
251            
252            if (metadataRootNode != null)
253            {
254                NodeList metaNodes = metadataRootNode.getChildNodes();
255                for (int i = 0; i < metaNodes.getLength(); i++)
256                {
257                    Node metaNode = metaNodes.item(i);
258                    if (metaNode.getNodeType() == Node.ELEMENT_NODE)
259                    {
260                        String metaName = metaNode.getLocalName();
261                        MetadataDefinition metaDef = metadataDefinitions.get(metaName);
262                        
263                        String subMetadataPath = prefix + (StringUtils.isEmpty(prefix) ? "" : "/") + metaName;
264                        
265                        importMetadata(content, meta, metaNode, metaDef, subMetadataPath, params);
266                    }
267                }
268            }
269        }
270        catch (ConfigurationException e)
271        {
272            throw new IOException("Error retrieving metadata definitions.", e);
273        }
274    }
275    
276    /**
277     * Import metadata from a DOM node.
278     * @param content The content being imported.
279     * @param meta the metadata holder to populate.
280     * @param metaNode the metadata DOM node.
281     * @param metaDef the metadata definition.
282     * @param metadataPath the metadata path.
283     * @param params the import parameters.
284     * @throws IOException if an error occurs.
285     */
286    protected void importMetadata(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, MetadataDefinition metaDef, String metadataPath, Map<String, Object> params) throws IOException
287    {
288        String name = metaNode.getLocalName();
289        NodeList valueNodes = _xPathProcessor.selectNodeList(metaNode, "value", getPrefixResolver());
290        String[] values = new String[valueNodes.getLength()];
291        for (int i = 0; i < valueNodes.getLength(); i++)
292        {
293            values[i] = valueNodes.item(i).getTextContent();
294        }
295        
296        try
297        {
298            if (metaDef != null)
299            {
300                if (metaDef instanceof RepeaterDefinition)
301                {
302                    setRepeater(content, meta, metaNode, (RepeaterDefinition) metaDef, name, metadataPath, params);
303                }
304                else
305                {
306                    setMetadata(content, meta, metaNode, metaDef, name, values, metadataPath, params);
307                }
308            }
309        }
310        catch (Exception e)
311        {
312            String message = "The values for metadata '" + name + "' are invalid and will be ignored: " + StringUtils.join(values, ", ");
313            getLogger().warn(message, e);
314        }
315    }
316    
317    /**
318     * Set the values of a metadata.
319     * @param content The content being imported.
320     * @param meta the metadata holder.
321     * @param metaNode the metadata DOM node.
322     * @param metaDef the metadata definition.
323     * @param name the metadata name.
324     * @param values the metadata values.
325     * @param metadataPath the metadata path.
326     * @param params the import parameters.
327     * @throws IOException if an error occurs.
328     */
329    protected void setMetadata(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, MetadataDefinition metaDef, String name, String[] values, String metadataPath, Map<String, Object> params) throws IOException
330    {
331        switch (metaDef.getType())
332        {
333            case STRING:
334                setStringMetadata(meta, name, metaDef, values);
335                break;
336            case BOOLEAN:
337                setBooleanMetadata(meta, name, metaDef, values);
338                break;
339            case LONG:
340                setLongMetadata(meta, name, metaDef, values);
341                break;
342            case DOUBLE:
343                setDoubleMetadata(meta, name, metaDef, values);
344                break;
345            case DATE:
346            case DATETIME:
347                setDateMetadata(meta, name, metaDef, values);
348                break;
349            case GEOCODE:
350                String latitude = getAttributeValue(metaNode, "latitude", null);
351                String longitude = getAttributeValue(metaNode, "longitude", null);
352                if (latitude != null && longitude != null)
353                {
354                    setGeocodeMetadata(meta, name, metaDef, latitude, longitude);
355                }
356                break;
357            case RICH_TEXT:
358                setRichText(meta, metaNode, name);
359                break;
360            case COMPOSITE:
361                setComposite(content, meta, metaNode, metaDef, name, metadataPath, params);
362                break;
363            case BINARY:
364            case FILE:
365                if (values.length > 0)
366                {
367                    setBinaryMetadata(meta, name, metaDef, values[0]);
368                }
369                break;
370            case CONTENT:
371                setContentReferences(content, meta, metaNode, name, metaDef, values, metadataPath, params);
372                break;
373            case SUB_CONTENT:
374                setSubContents(meta, metaNode, name, content.getId(), metadataPath, params);                
375                break;
376            case MULTILINGUAL_STRING:
377                setMultilingualString(meta, metaNode, name);
378                break;
379            case USER:
380            case REFERENCE:
381            default:
382                break;
383        }
384    }
385    
386    /**
387     * Set a composite metadata.
388     * @param content The content being imported.
389     * @param meta the metadata holder.
390     * @param metaNode the metadata DOM node.
391     * @param metaDef the metadata definition.
392     * @param name the metadata name.
393     * @param metadataPath the metadata path.
394     * @param params the import parameters.
395     * @throws IOException if an error occurs.
396     */
397    protected void setComposite(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, MetadataDefinition metaDef, String name, String metadataPath, Map<String, Object> params) throws IOException
398    {
399        NodeList subMetaNodes = metaNode.getChildNodes();
400        if (subMetaNodes.getLength() > 0)
401        {
402            ModifiableCompositeMetadata composite = meta.getCompositeMetadata(name, true);
403            for (int i = 0; i < subMetaNodes.getLength(); i++)
404            {
405                Node subMetaNode = subMetaNodes.item(i);
406                if (subMetaNode.getNodeType() == Node.ELEMENT_NODE)
407                {
408                    String subMetaName = subMetaNode.getLocalName();
409                    MetadataDefinition childDef = metaDef.getMetadataDefinition(subMetaName);
410                    String subMetaPath = metadataPath + "/" + subMetaName;
411                    
412                    importMetadata(content, composite, subMetaNode, childDef, subMetaPath, params);
413                }
414            }
415        }
416    }
417    
418    /**
419     * Set a RichText metadata.
420     * @param meta the metadata holder.
421     * @param metaNode the metadata node.
422     * @param name the metadata name.
423     * @throws IOException if an error occurs.
424     */
425    protected void setMultilingualString(ModifiableCompositeMetadata meta, Node metaNode, String name) throws IOException
426    {
427        NodeList localeNodes = metaNode.getChildNodes();
428        for (int i = 0; i < localeNodes.getLength(); i++)
429        {
430            Node localeNode = localeNodes.item(i);
431            if (localeNode.getNodeType() == Node.ELEMENT_NODE)
432            {
433                String localeAsStr = localeNode.getNodeName();
434                String value = localeNode.getNodeValue();
435                
436                try
437                {
438                    meta.setMetadata(name, value, LocaleUtils.toLocale(localeAsStr));
439                }
440                catch (IllegalArgumentException e)
441                {
442                    throw new IOException(localeAsStr + " is not a valid locale", e);
443                }
444            }
445        }
446    }
447    
448    /**
449     * Set a RichText metadata.
450     * @param meta the metadata holder.
451     * @param metaNode the metadata node.
452     * @param name the metadata name.
453     * @throws IOException if an error occurs.
454     */
455    protected void setRichText(ModifiableCompositeMetadata meta, Node metaNode, String name) throws IOException
456    {
457        NodeList docbookNodes = metaNode.getChildNodes();
458        for (int i = 0; i < docbookNodes.getLength(); i++)
459        {
460            Node docbookNode = docbookNodes.item(i);
461            if (docbookNode.getNodeType() == Node.ELEMENT_NODE && "article".equals(docbookNode.getLocalName()))
462            {
463                try
464                {
465                    String docbook = ContentImporterHelper.serializeNode(docbookNode);
466                    setRichText(meta, name, new ByteArrayInputStream(docbook.getBytes("UTF-8")));
467                }
468                catch (TransformerException e)
469                {
470                    throw new IOException("Error serializing a docbook node.", e);
471                }
472            }
473        }
474    }
475    
476    /**
477     * Set a repeater metadata.
478     * @param content The content being imported.
479     * @param meta the metadata holder.
480     * @param metaNode the metadata DOM node.
481     * @param repeaterDef the repeater definition.
482     * @param name the metadata name.
483     * @param metadataPath the metadata path.
484     * @param params the import parameters.
485     * @throws IOException if an error occurs.
486     */
487    protected void setRepeater(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, RepeaterDefinition repeaterDef, String name, String metadataPath, Map<String, Object> params) throws IOException
488    {
489        ModifiableCompositeMetadata repeaterMeta = meta.getCompositeMetadata(name, true);
490        
491        NodeList entryNodes = _xPathProcessor.selectNodeList(metaNode, "entry");
492        int repeaterSize = entryNodes.getLength();
493        setRepeaterSize(content, metadataPath, repeaterSize, params);
494        
495        for (int i = 0; i < repeaterSize; i++)
496        {
497            Node entryNode = entryNodes.item(i);
498            String entryName = Integer.toString(i + 1);
499            
500            ModifiableCompositeMetadata entryMeta = repeaterMeta.getCompositeMetadata(entryName, true);
501            NodeList subMetaNodes = entryNode.getChildNodes();
502            for (int j = 0; j < subMetaNodes.getLength(); j++)
503            {
504                Node subMetaNode = subMetaNodes.item(j);
505                if (subMetaNode.getNodeType() == Node.ELEMENT_NODE)
506                {
507                    String subMetaName = subMetaNode.getLocalName();
508                    MetadataDefinition childDef = repeaterDef.getMetadataDefinition(subMetaName);
509                    String subMetaPath = metadataPath + "/" + entryName + "/" + subMetaName;
510                    
511                    importMetadata(content, entryMeta, subMetaNode, childDef, subMetaPath, params);
512                }
513            }
514        }
515    }
516    
517    /**
518     * Set a content metadata.
519     * @param content The content being imported.
520     * @param meta the metadata holder.
521     * @param metaNode the metadata DOM node.
522     * @param name the metadata name.
523     * @param metaDef the metadata definition
524     * @param values the values array 
525     * @param metadataPath the metadata path.
526     * @param params the import parameters.
527     * @throws IOException if an error occurs.
528     */
529    protected void setContentReferences(ModifiableContent content, ModifiableCompositeMetadata meta, Node metaNode, String name, MetadataDefinition metaDef, String[] values, String metadataPath, Map<String, Object> params) throws IOException
530    {
531        if (values != null)
532        {
533            Map<Content, Map<String, Object>> contentRefMap = getContentRefMap(params);
534            
535            List<ContentReference> references = new ArrayList<>();
536            
537            NodeList valueNodes = _xPathProcessor.selectNodeList(metaNode, "value", getPrefixResolver());
538            for (int i = 0; i < valueNodes.getLength(); i++)
539            {
540                Node valueNode = valueNodes.item(i);
541                
542                String refType = getAttributeValue(valueNode, "type", "local");
543                String textContent = valueNode.getTextContent();
544                
545                if (refType.equals("local") && StringUtils.isNotBlank(textContent))
546                {
547                    // The reference is local to the file.
548                    references.add(new ContentReference(ContentReference.TYPE_LOCAL_ID, textContent.trim()));
549                }
550                else if (refType.equals("repository") && StringUtils.isNotBlank(textContent))
551                {
552                    // The reference point to a content in the repository.
553                    references.add(new ContentReference(ContentReference.TYPE_CONTENT_ID, textContent.trim()));
554                }
555                else if (refType.equals("properties"))
556                {
557                    // No text content, parse the child nodes.
558                    Map<String, String> refValues = new HashMap<>();
559                    NodeList refNodes = _xPathProcessor.selectNodeList(valueNode, "meta", getPrefixResolver());
560                    for (int j = 0; j < refNodes.getLength(); j++)
561                    {
562                        Node refNode = refNodes.item(j);
563                        String metaName = getAttributeValue(refNode, "name", null);
564                        String value = getAttributeValue(refNode, "value", null);
565                        
566                        if (StringUtils.isNotBlank(metaName) && value != null)
567                        {
568                            refValues.put(metaName, value);
569                        }
570                    }
571                    
572                    references.add(new ContentReference(ContentReference.TYPE_CONTENT_VALUES, refValues));
573                }
574            }
575            
576            if (!references.isEmpty())
577            {
578                if (metaDef.isMultiple())
579                {
580                    addContentReferences(contentRefMap, content, metadataPath, references);
581                }
582                else
583                {
584                    addContentReference(contentRefMap, content, metadataPath, references.get(0));
585                }
586            }
587        }
588    }
589    
590    /**
591     * Set a sub-content metadata.
592     * @param meta the metadata holder.
593     * @param metaNode the metadata DOM node.
594     * @param name the metadata name.
595     * @param parentContentId the parent content ID.
596     * @param metadataPath the metadata path.
597     * @param params the import parameters.
598     * @throws IOException if an error occurs.
599     */
600    protected void setSubContents(ModifiableCompositeMetadata meta, Node metaNode, String name, String parentContentId, String metadataPath, Map<String, Object> params) throws IOException
601    {
602        NodeList contentNodes = _xPathProcessor.selectNodeList(metaNode, "value/content");
603        if (contentNodes.getLength() > 0)
604        {
605            ModifiableTraversableAmetysObject subContentMeta = meta.getObjectCollection(name, true);
606            
607            for (int i = 0; i < contentNodes.getLength(); i++)
608            {
609                Node contentNode = contentNodes.item(i);
610                importSubContent(subContentMeta, contentNode, parentContentId, metadataPath, params);
611            }
612        }
613    }
614    
615    /**
616     * Import a sub-content.
617     * @param subContentMeta the content collection metadata.
618     * @param contentNode the DOM node representing the sub-content.
619     * @param parentContentId the parent content ID.
620     * @param metadataPath the content collection metadata path.
621     * @param params the import parameters.
622     * @return the created Content.
623     * @throws IOException if an error occurs.
624     */
625    protected Content importSubContent(ModifiableTraversableAmetysObject subContentMeta, Node contentNode, String parentContentId, String metadataPath, Map<String, Object> params) throws IOException
626    {
627        String[] defaultTypes = (String[]) params.get(_DEFAULT_CONTENT_TYPES_KEY);
628        String[] defaultMixins = (String[]) params.get(_DEFAULT_CONTENT_MIXINS_KEY);
629        String defaultLanguage = (String) params.get(_DEFAULT_CONTENT_LANG_KEY);
630        
631        String cTypesStr = getAttributeValue(contentNode, "types", "");
632        String mixinsStr = getAttributeValue(contentNode, "mixins", "");
633        
634        String[] contentTypes = StringUtils.isEmpty(cTypesStr) ? defaultTypes : StringUtils.split(cTypesStr, ", ");
635        String[] mixins = StringUtils.isEmpty(mixinsStr) ? defaultMixins : StringUtils.split(mixinsStr, ", ");
636        String language = getAttributeValue(contentNode, "language", defaultLanguage);
637        
638        String title = _xPathProcessor.evaluateAsString(contentNode, "metadata/title");
639        
640        try
641        {
642            Content content = createContent(title, contentTypes, mixins, language, parentContentId, metadataPath, params);
643            
644            if (content instanceof ModifiableContent)
645            {
646                importMetadata((ModifiableContent) content, contentNode, metadataPath, params);
647                
648                ((ModifiableContent) content).saveChanges();
649            }
650            
651            return content;
652        }
653        catch (WorkflowException e)
654        {
655            getLogger().error("Error creating sub-content.", e);
656            throw new IOException("Error creating sub-content.", e);
657        }
658    }
659    
660}