001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.file.types;
029
030import org.opencms.configuration.CmsParameterConfiguration;
031import org.opencms.db.CmsSecurityManager;
032import org.opencms.file.CmsFile;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsProperty;
035import org.opencms.file.CmsRequestContext;
036import org.opencms.file.CmsResource;
037import org.opencms.file.CmsResource.CmsResourceDeleteMode;
038import org.opencms.file.CmsResourceFilter;
039import org.opencms.loader.CmsXmlContentLoader;
040import org.opencms.lock.CmsLockActionRecord;
041import org.opencms.lock.CmsLockActionRecord.LockChange;
042import org.opencms.lock.CmsLockUtil;
043import org.opencms.main.CmsException;
044import org.opencms.main.CmsIllegalArgumentException;
045import org.opencms.main.CmsLog;
046import org.opencms.main.OpenCms;
047import org.opencms.relations.CmsLink;
048import org.opencms.relations.CmsRelation;
049import org.opencms.relations.CmsRelationFilter;
050import org.opencms.relations.CmsRelationType;
051import org.opencms.security.CmsPermissionSet;
052import org.opencms.staticexport.CmsLinkTable;
053import org.opencms.util.CmsMacroResolver;
054import org.opencms.util.CmsStringUtil;
055import org.opencms.workplace.editors.I_CmsPreEditorActionDefinition;
056import org.opencms.workplace.editors.directedit.I_CmsEditHandler;
057import org.opencms.xml.CmsXmlContentDefinition;
058import org.opencms.xml.CmsXmlEntityResolver;
059import org.opencms.xml.CmsXmlException;
060import org.opencms.xml.containerpage.CmsFormatterConfiguration;
061import org.opencms.xml.content.CmsDefaultXmlContentHandler;
062import org.opencms.xml.content.CmsXmlContent;
063import org.opencms.xml.content.CmsXmlContentFactory;
064import org.opencms.xml.content.I_CmsXmlContentHandler;
065import org.opencms.xml.types.CmsXmlHtmlValue;
066import org.opencms.xml.types.CmsXmlVarLinkValue;
067import org.opencms.xml.types.CmsXmlVfsFileValue;
068import org.opencms.xml.types.I_CmsXmlContentValue;
069
070import java.util.ArrayList;
071import java.util.Collections;
072import java.util.Iterator;
073import java.util.LinkedHashSet;
074import java.util.List;
075import java.util.Locale;
076import java.util.Set;
077
078import org.apache.commons.logging.Log;
079
080import com.google.common.collect.Lists;
081
082/**
083 * Resource type descriptor for the type "xmlcontent".<p>
084 *
085 * @since 6.0.0
086 */
087public class CmsResourceTypeXmlContent extends A_CmsResourceTypeLinkParseable {
088
089    /** Configuration key for the (optional) schema. */
090    public static final String CONFIGURATION_SCHEMA = "schema";
091
092    /** The name for the choose model file form action. */
093    public static final String DIALOG_CHOOSEMODEL = "choosemodel";
094
095    /** The log object for this class. */
096    private static final Log LOG = CmsLog.getLog(CmsResourceTypeXmlContent.class);
097
098    /** The serial version id. */
099    private static final long serialVersionUID = 2271469830431937731L;
100
101    /** The (optional) schema of this resource. */
102    private String m_schema;
103
104    /**
105     * Returns the possible model files for the new resource.<p>
106     *
107     * @param cms the current users context to work with
108     * @param currentFolder the folder
109     * @param newResourceTypeName the resource type name for the new resource to create
110     * @return the possible model files for the new resource
111     */
112    public static List<CmsResource> getModelFiles(CmsObject cms, String currentFolder, String newResourceTypeName) {
113
114        try {
115
116            I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(newResourceTypeName);
117            I_CmsPreEditorActionDefinition preEditorAction = OpenCms.getWorkplaceManager().getPreEditorConditionDefinition(
118                resType);
119            // get the global master folder if configured
120            String masterFolder = preEditorAction.getConfiguration().getString(
121                CmsDefaultXmlContentHandler.APPINFO_MODELFOLDER,
122                null);
123            // get the schema for the resource type to create
124            String schema = resType.getConfiguration().get(CmsResourceTypeXmlContent.CONFIGURATION_SCHEMA);
125            CmsXmlContentDefinition contentDefinition = CmsXmlContentDefinition.unmarshal(cms, schema);
126            // get the content handler for the resource type to create
127            I_CmsXmlContentHandler handler = contentDefinition.getContentHandler();
128            String individualModelFolder = handler.getModelFolder();
129            if (CmsStringUtil.isNotEmpty(individualModelFolder)) {
130                masterFolder = individualModelFolder;
131            }
132
133            if (CmsStringUtil.isNotEmpty(masterFolder)) {
134                // store the original URI
135                String uri = cms.getRequestContext().getUri();
136                try {
137                    // set URI to current folder
138                    cms.getRequestContext().setUri(currentFolder);
139                    CmsMacroResolver resolver = CmsMacroResolver.newInstance().setCmsObject(cms);
140                    // resolve eventual macros
141                    masterFolder = resolver.resolveMacros(masterFolder);
142                } finally {
143                    // switch back to stored URI
144                    cms.getRequestContext().setUri(uri);
145                }
146
147                if (CmsStringUtil.isNotEmpty(masterFolder) && cms.existsResource(masterFolder)) {
148                    // folder for master files exists, get all files of the same resource type
149                    CmsResourceFilter filter = CmsResourceFilter.ONLY_VISIBLE_NO_DELETED.addRequireType(
150                        resType.getTypeId());
151                    return cms.readResources(masterFolder, filter, false);
152                }
153            }
154        } catch (Throwable t) {
155            // error determining resource type, should never happen
156        }
157        return Collections.emptyList();
158    }
159
160    /**
161     * Returns <code>true</code> in case the given resource is an XML content.<p>
162     *
163     * @param resource the resource to check
164     *
165     * @return <code>true</code> in case the given resource is an XML content
166     *
167     * @since 7.0.2
168     */
169    public static boolean isXmlContent(CmsResource resource) {
170
171        boolean result = false;
172        if (resource != null) {
173            // avoid array index out of bound exception:
174            if (!resource.isFolder()) {
175                result = OpenCms.getResourceManager().getResourceType(resource) instanceof CmsResourceTypeXmlContent;
176            }
177        }
178        return result;
179    }
180
181    /**
182     * @see org.opencms.file.types.A_CmsResourceType#addConfigurationParameter(java.lang.String, java.lang.String)
183     */
184    @Override
185    public void addConfigurationParameter(String paramName, String paramValue) {
186
187        super.addConfigurationParameter(paramName, paramValue);
188        if (CONFIGURATION_SCHEMA.equalsIgnoreCase(paramName)) {
189            m_schema = paramValue.trim();
190        }
191    }
192
193    /**
194     * @see org.opencms.file.types.I_CmsResourceType#createResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, java.lang.String, byte[], java.util.List)
195     */
196    @Override
197    public CmsResource createResource(
198        CmsObject cms,
199        CmsSecurityManager securityManager,
200        String resourcename,
201        byte[] content,
202        List<CmsProperty> properties)
203    throws CmsException {
204
205        boolean hasModelUri = false;
206        CmsXmlContent newContent = null;
207        if ((m_schema != null) && ((content == null) || (content.length == 0))) {
208            // unmarshal the content definition for the new resource
209            CmsXmlContentDefinition contentDefinition = CmsXmlContentDefinition.unmarshal(cms, m_schema);
210
211            // read the default locale for the new resource
212            Locale locale = getLocaleForNewContent(cms, securityManager, resourcename, properties);
213            String modelUri = (String)cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_MODEL);
214
215            // must set URI of OpenCms user context to parent folder of created resource,
216            // in order to allow reading of properties for default values
217            CmsObject newCms = OpenCms.initCmsObject(cms);
218            newCms.getRequestContext().setUri(CmsResource.getParentFolder(resourcename));
219            if (modelUri != null) {
220                // create the new content from the model file
221                newContent = CmsXmlContentFactory.createDocument(newCms, locale, modelUri);
222                hasModelUri = true;
223            } else {
224                // create the new content from the content definition
225                newContent = CmsXmlContentFactory.createDocument(
226                    newCms,
227                    locale,
228                    OpenCms.getSystemInfo().getDefaultEncoding(),
229                    contentDefinition);
230            }
231            // get the bytes from the created content
232            content = newContent.marshal();
233        }
234
235        // now create the resource using the super class
236        CmsResource resource = super.createResource(cms, securityManager, resourcename, content, properties);
237
238        // a model file was used, call the content handler for post-processing
239        if (hasModelUri) {
240            CmsFile file = cms.readFile(resource);
241            newContent = CmsXmlContentFactory.unmarshal(cms, file);
242            newContent.setAutoCorrectionEnabled(true);
243            resource = newContent.getHandler().prepareForWrite(cms, newContent, file);
244        }
245
246        return resource;
247    }
248
249    /**
250     * @see org.opencms.file.types.A_CmsResourceType#deleteResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, org.opencms.file.CmsResource, org.opencms.file.CmsResource.CmsResourceDeleteMode)
251     */
252    @Override
253    public void deleteResource(
254        CmsObject cms,
255        CmsSecurityManager securityManager,
256        CmsResource resource,
257        CmsResourceDeleteMode siblingMode)
258    throws CmsException {
259
260        List<CmsResource> detailOnlyPages = null;
261        if (isPossiblyDetailContent(resource)) {
262            detailOnlyPages = getDetailContainerResources(cms, resource);
263        }
264        super.deleteResource(cms, securityManager, resource, siblingMode);
265        if (detailOnlyPages != null) {
266            for (CmsResource page : detailOnlyPages) {
267                if (page.getState().isDeleted()) {
268                    continue;
269                }
270                try {
271                    CmsLockUtil.ensureLock(cms, page);
272                    cms.deleteResource(page, CmsResource.DELETE_PRESERVE_SIBLINGS);
273                } catch (CmsException e) {
274                    LOG.error(e.getLocalizedMessage(), e);
275                }
276            }
277        }
278    }
279
280    /**
281     * @see org.opencms.file.types.I_CmsResourceType#getCachePropertyDefault()
282     */
283    @Override
284    public String getCachePropertyDefault() {
285
286        return "element;locale;";
287    }
288
289    /**
290     * @see org.opencms.file.types.A_CmsResourceType#getConfiguration()
291     */
292    @Override
293    public CmsParameterConfiguration getConfiguration() {
294
295        CmsParameterConfiguration result = new CmsParameterConfiguration();
296        CmsParameterConfiguration additional = super.getConfiguration();
297        if (additional != null) {
298            result.putAll(additional);
299        }
300        if (m_schema != null) {
301            result.put(CONFIGURATION_SCHEMA, m_schema);
302        }
303        return result;
304    }
305
306    /**
307     * Returns the edit handler if configured.<p>
308     *
309     * @param cms the cms context
310     *
311     * @return the edit handler
312     */
313    public I_CmsEditHandler getEditHandler(CmsObject cms) {
314
315        String schema = getSchema();
316
317        try {
318            CmsXmlContentDefinition contentDefinition = CmsXmlContentDefinition.unmarshal(cms, schema);
319            // get the content handler for the resource type to create
320            I_CmsXmlContentHandler handler = contentDefinition.getContentHandler();
321            return handler.getEditHandler();
322
323        } catch (CmsXmlException e) {
324            LOG.error(e.getMessage(), e);
325        }
326        return null;
327    }
328
329    /**
330     * @see org.opencms.file.types.A_CmsResourceType#getFormattersForResource(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
331     */
332    @Override
333    public CmsFormatterConfiguration getFormattersForResource(CmsObject cms, CmsResource resource) {
334
335        CmsFormatterConfiguration result = null;
336        CmsXmlContentDefinition cd = null;
337        try {
338            cd = CmsXmlContentDefinition.getContentDefinitionForResource(cms, resource);
339            result = cd.getContentHandler().getFormatterConfiguration(cms, resource);
340        } catch (CmsException e) {
341            // no content definition found, use the preview formatter
342        }
343        if (result == null) {
344            LOG.warn(
345                Messages.get().getBundle().key(
346                    Messages.LOG_WARN_NO_FORMATTERS_DEFINED_1,
347                    cd == null ? resource.getRootPath() : cd.getSchemaLocation()));
348            result = CmsFormatterConfiguration.EMPTY_CONFIGURATION;
349        }
350        return result;
351    }
352
353    /**
354     * @see org.opencms.file.types.A_CmsResourceType#getGalleryPreviewProvider()
355     */
356    @Override
357    public String getGalleryPreviewProvider() {
358
359        if (m_galleryPreviewProvider == null) {
360            m_galleryPreviewProvider = getConfiguration().getString(
361                CONFIGURATION_GALLERY_PREVIEW_PROVIDER,
362                DEFAULT_GALLERY_PREVIEW_PROVIDER);
363        }
364        return m_galleryPreviewProvider;
365    }
366
367    /**
368     * @see org.opencms.file.types.I_CmsResourceType#getLoaderId()
369     */
370    @Override
371    public int getLoaderId() {
372
373        return CmsXmlContentLoader.RESOURCE_LOADER_ID;
374    }
375
376    /**
377     * Returns the configured xsd schema uri.<p>
378     *
379     * @return the configured xsd schema uri, or <code>null</code> if not set
380     */
381    public String getSchema() {
382
383        return m_schema;
384    }
385
386    /**
387     * @see org.opencms.file.types.A_CmsResourceType#initialize(org.opencms.file.CmsObject)
388     */
389    @Override
390    public void initialize(CmsObject cms) {
391
392        super.initialize(cms);
393        if (m_schema != null) {
394            // unmarshal the XML schema, this is required to update the resource bundle cache
395            try {
396                if (cms.existsResource(m_schema)) {
397                    CmsXmlContentDefinition.unmarshal(cms, m_schema);
398                } else {
399                    LOG.debug(
400                        Messages.get().getBundle().key(
401                            Messages.LOG_WARN_SCHEMA_RESOURCE_DOES_NOT_EXIST_2,
402                            m_schema,
403                            getTypeName()));
404                }
405            } catch (Throwable e) {
406                // unable to unmarshal the XML schema configured
407                LOG.error(Messages.get().getBundle().key(Messages.ERR_BAD_XML_SCHEMA_2, m_schema, getTypeName()), e);
408            }
409        }
410    }
411
412    /**
413     * @see org.opencms.file.types.A_CmsResourceType#moveResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, org.opencms.file.CmsResource, java.lang.String)
414     */
415    @Override
416    public void moveResource(
417        CmsObject cms,
418        CmsSecurityManager securityManager,
419        CmsResource resource,
420        String destination)
421    throws CmsException, CmsIllegalArgumentException {
422
423        super.moveResource(cms, securityManager, resource, destination);
424        if (isPossiblyDetailContent(resource)) {
425            String rootDest = cms.getRequestContext().addSiteRoot(destination);
426            CmsObject rootCms = OpenCms.initCmsObject(cms);
427            rootCms.getRequestContext().setSiteRoot("");
428            String srcParent = CmsResource.getParentFolder(resource.getRootPath());
429            String srcName = CmsResource.getName(resource.getRootPath());
430            String destParent = CmsResource.getParentFolder(rootDest);
431            String destName = CmsResource.getName(rootDest);
432            if (srcParent.equals(destParent) && !srcName.equals(destName)) {
433                List<CmsResource> detailOnlyPages = getDetailContainerResources(cms, resource);
434                for (CmsResource page : detailOnlyPages) {
435                    if (page.getState().isDeleted()) {
436                        continue;
437                    }
438                    String newPath = CmsStringUtil.joinPaths(CmsResource.getParentFolder(page.getRootPath()), destName);
439                    CmsLockActionRecord lockRecord = null;
440                    try {
441                        lockRecord = CmsLockUtil.ensureLock(cms, page);
442                        rootCms.moveResource(page.getRootPath(), newPath);
443                    } catch (Exception e) {
444                        LOG.error(e.getLocalizedMessage(), e);
445                    } finally {
446                        if ((lockRecord != null) && (lockRecord.getChange() == LockChange.locked)) {
447                            try {
448                                CmsLockUtil.tryUnlock(
449                                    rootCms,
450                                    rootCms.readResource(page.getStructureId(), CmsResourceFilter.ALL));
451                            } catch (Exception e) {
452                                LOG.error(e.getLocalizedMessage(), e);
453                            }
454                        }
455                    }
456                }
457            }
458        }
459    }
460
461    /**
462     * @see org.opencms.relations.I_CmsLinkParseable#parseLinks(org.opencms.file.CmsObject, org.opencms.file.CmsFile)
463     */
464    public List<CmsLink> parseLinks(CmsObject cms, CmsFile file) {
465
466        if (file.getLength() == 0) {
467            return Collections.emptyList();
468        }
469        CmsXmlContent xmlContent;
470        long requestTime = cms.getRequestContext().getRequestTime();
471        try {
472            // prevent the check rules to remove the broken links
473            cms.getRequestContext().setRequestTime(CmsResource.DATE_RELEASED_EXPIRED_IGNORE);
474            xmlContent = CmsXmlContentFactory.unmarshal(cms, file);
475        } catch (CmsException e) {
476            if (LOG.isErrorEnabled()) {
477                LOG.error(
478                    org.opencms.db.Messages.get().getBundle().key(
479                        org.opencms.db.Messages.ERR_READ_RESOURCE_1,
480                        cms.getSitePath(file)),
481                    e);
482            }
483            return Collections.emptyList();
484        } finally {
485            cms.getRequestContext().setRequestTime(requestTime);
486        }
487        // using linked set to keep the link order
488        Set<CmsLink> links = new LinkedHashSet<CmsLink>();
489
490        // add XSD link
491        CmsLink xsdLink = getXsdLink(cms, xmlContent);
492        if (xsdLink != null) {
493            links.add(xsdLink);
494        }
495
496        // iterate over all languages
497        List<Locale> locales = xmlContent.getLocales();
498        Iterator<Locale> i = locales.iterator();
499        while (i.hasNext()) {
500            Locale locale = i.next();
501            List<I_CmsXmlContentValue> values = xmlContent.getValues(locale);
502
503            // iterate over all body elements per language
504            Iterator<I_CmsXmlContentValue> j = values.iterator();
505            while (j.hasNext()) {
506                I_CmsXmlContentValue value = j.next();
507                if (value instanceof CmsXmlHtmlValue) {
508                    CmsXmlHtmlValue htmlValue = (CmsXmlHtmlValue)value;
509                    CmsLinkTable linkTable = htmlValue.getLinkTable();
510
511                    // iterate over all links inside a body element
512                    Iterator<CmsLink> k = linkTable.iterator();
513                    while (k.hasNext()) {
514                        CmsLink link = k.next();
515
516                        // external links are omitted
517                        if (link.isInternal()) {
518                            link.checkConsistency(cms);
519                            links.add(link);
520                        }
521                    }
522                } else if (value instanceof CmsXmlVfsFileValue) {
523                    CmsXmlVfsFileValue refValue = (CmsXmlVfsFileValue)value;
524                    CmsLink link = refValue.getLink(cms);
525                    if (link != null) {
526                        links.add(link);
527                    }
528                } else if (value instanceof CmsXmlVarLinkValue) {
529                    CmsXmlVarLinkValue refValue = (CmsXmlVarLinkValue)value;
530                    CmsLink link = refValue.getLink(cms);
531                    if ((link != null) && link.isInternal()) {
532                        links.add(link);
533                    }
534                }
535            }
536        }
537        return new ArrayList<CmsLink>(links);
538    }
539
540    /**
541     * @see org.opencms.file.types.I_CmsResourceType#writeFile(org.opencms.file.CmsObject, CmsSecurityManager, CmsFile)
542     */
543    @Override
544    public CmsFile writeFile(CmsObject cms, CmsSecurityManager securityManager, CmsFile resource) throws CmsException {
545
546        // check if the user has write access and if resource is locked
547        // done here so that all the XML operations are not performed if permissions not granted
548        securityManager.checkPermissions(
549            cms.getRequestContext(),
550            resource,
551            CmsPermissionSet.ACCESS_WRITE,
552            true,
553            CmsResourceFilter.ALL);
554        // read the XML content, use the encoding set in the property
555        CmsXmlContent xmlContent = CmsXmlContentFactory.unmarshal(cms, resource, true);
556        // call the content handler for post-processing
557        resource = xmlContent.getHandler().prepareForWrite(cms, xmlContent, resource);
558
559        // now write the file
560        return super.writeFile(cms, securityManager, resource);
561    }
562
563    /**
564     * Gets the locale which should be used for creating an empty content.<p>
565     *
566     * @param cms the current CMS context
567     * @param securityManager the security manager
568     * @param resourcename the name of the resource to create
569     * @param properties the properties for the resource to create
570     *
571     * @return the locale to use
572     */
573    protected Locale getLocaleForNewContent(
574        CmsObject cms,
575        CmsSecurityManager securityManager,
576        String resourcename,
577        List<CmsProperty> properties) {
578
579        Locale locale = (Locale)(cms.getRequestContext().getAttribute(CmsRequestContext.ATTRIBUTE_NEW_RESOURCE_LOCALE));
580        if (locale != null) {
581            return locale;
582        }
583        List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales(
584            cms,
585            CmsResource.getParentFolder(resourcename));
586        return locales.get(0);
587    }
588
589    /**
590     * Creates a new link object for the schema definition.<p>
591     *
592     * @param cms the current CMS context
593     * @param xmlContent the xml content to crete the link for
594     *
595     * @return the generated link
596     */
597    protected CmsLink getXsdLink(CmsObject cms, CmsXmlContent xmlContent) {
598
599        String schema = xmlContent.getContentDefinition().getSchemaLocation();
600        if (schema.startsWith(CmsXmlEntityResolver.OPENCMS_SCHEME)) {
601            if (CmsXmlEntityResolver.isInternalId(schema)) {
602                return null;
603            }
604            schema = schema.substring(CmsXmlEntityResolver.OPENCMS_SCHEME.length() - 1);
605        } else if (CmsXmlEntityResolver.isCachedSystemId(schema)) {
606            // schema may not exist as a VFS file because it has just been cached (some test cases do this)
607            return null;
608        }
609        try {
610            CmsResource schemaRes = cms.readResource(cms.getRequestContext().removeSiteRoot(schema));
611            CmsLink xsdLink = new CmsLink(
612                null,
613                CmsRelationType.XSD,
614                schemaRes.getStructureId(),
615                schemaRes.getRootPath(),
616                true);
617            return xsdLink;
618        } catch (CmsException e) {
619            LOG.error(e.getLocalizedMessage(), e);
620        }
621        return null;
622    }
623
624    /**
625     * Checks if the resource is possibly a detail content.<p>
626     *
627     * @param resource the resource to check
628     * @return true if the resource is possibly a detail content
629     */
630    boolean isPossiblyDetailContent(CmsResource resource) {
631
632        if (CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
633            return false;
634        }
635        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(resource);
636        if (type instanceof CmsResourceTypeXmlAdeConfiguration) {
637            return false;
638        }
639        return true;
640    }
641
642    /**
643     * Reads the detail container resources which are connected by relations to the given resource.
644     *
645     * @param cms the current CMS context
646     * @param res the detail content
647     *
648     * @return the list of detail only container resources
649     *
650     * @throws CmsException if something goes wrong
651     */
652    private List<CmsResource> getDetailContainerResources(CmsObject cms, CmsResource res) throws CmsException {
653
654        CmsRelationFilter filter = CmsRelationFilter.relationsFromStructureId(res.getStructureId()).filterType(
655            CmsRelationType.DETAIL_ONLY);
656        List<CmsResource> result = Lists.newArrayList();
657        List<CmsRelation> relations = cms.readRelations(filter);
658        for (CmsRelation relation : relations) {
659            try {
660                result.add(relation.getTarget(cms, CmsResourceFilter.ALL));
661            } catch (Exception e) {
662                LOG.error(e.getLocalizedMessage(), e);
663            }
664        }
665        return result;
666    }
667}