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, 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.ade.containerpage;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProperty;
032import org.opencms.file.CmsPropertyDefinition;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.types.CmsResourceTypeFolder;
036import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
037import org.opencms.i18n.CmsSingleTreeLocaleHandler;
038import org.opencms.jsp.util.CmsJspStandardContextBean;
039import org.opencms.lock.CmsLockActionRecord;
040import org.opencms.lock.CmsLockActionRecord.LockChange;
041import org.opencms.lock.CmsLockUtil;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.OpenCms;
045import org.opencms.relations.CmsRelation;
046import org.opencms.relations.CmsRelationFilter;
047import org.opencms.relations.CmsRelationType;
048import org.opencms.search.A_CmsSearchIndex;
049import org.opencms.site.CmsSite;
050import org.opencms.util.CmsStringUtil;
051import org.opencms.util.CmsUUID;
052import org.opencms.xml.containerpage.CmsContainerPageBean;
053import org.opencms.xml.containerpage.CmsXmlContainerPage;
054import org.opencms.xml.containerpage.CmsXmlContainerPageFactory;
055import org.opencms.xml.templatemapper.CmsTemplateMapper;
056
057import java.util.ArrayList;
058import java.util.Arrays;
059import java.util.HashSet;
060import java.util.List;
061import java.util.Locale;
062import java.util.Set;
063
064import javax.servlet.ServletRequest;
065
066import org.apache.commons.logging.Log;
067
068import com.google.common.base.Optional;
069
070/**
071 * Static utility class for functions related to detail-only containers.<p>
072 */
073public final class CmsDetailOnlyContainerUtil {
074
075    /** The detail containers folder name. */
076    public static final String DETAIL_CONTAINERS_FOLDER_NAME = ".detailContainers";
077
078    /** Use this locale string for locale independent detail only container resources. */
079    public static final String LOCALE_ALL = "ALL";
080
081    /** Logger instance for this class. */
082    private static final Log LOG = CmsLog.getLog(CmsDetailOnlyContainerUtil.class);
083
084    /**
085     * Private constructor.<p>
086     */
087    private CmsDetailOnlyContainerUtil() {
088
089        // do nothing
090    }
091
092    /**
093     * Returns the detail container resource locale appropriate for the given detail page.<p>
094     *
095     * @param cms the cms context
096     * @param contentLocale the content locale
097     * @param resource the detail page resource
098     *
099     * @return the locale String
100     */
101    public static String getDetailContainerLocale(CmsObject cms, String contentLocale, CmsResource resource) {
102
103        boolean singleLocale = useSingleLocaleDetailContainers(cms.getRequestContext().getSiteRoot());
104        if (!singleLocale) {
105            try {
106                CmsProperty prop = cms.readPropertyObject(
107                    resource,
108                    CmsPropertyDefinition.PROPERTY_LOCALE_INDEPENDENT_DETAILS,
109                    true);
110                singleLocale = Boolean.parseBoolean(prop.getValue());
111            } catch (Exception e) {
112                LOG.warn(e.getMessage(), e);
113            }
114        }
115        return singleLocale ? LOCALE_ALL : contentLocale;
116    }
117
118    /**
119     * Returns the path to the associated detail content.<p>
120     *
121     * @param detailContainersPage the detail containers page path
122     *
123     * @return the path to the associated detail content
124     */
125    public static String getDetailContentPath(String detailContainersPage) {
126
127        String detailName = CmsResource.getName(detailContainersPage);
128        String parentFolder = CmsResource.getParentFolder(CmsResource.getParentFolder(detailContainersPage));
129        if (parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")) {
130            // this will be the case for locale dependent detail only pages, move one level up
131            parentFolder = CmsResource.getParentFolder(parentFolder);
132        }
133        detailName = CmsStringUtil.joinPaths(parentFolder, detailName);
134        return detailName;
135    }
136
137    /**
138     * Gets the detail only page for a detail content.<p>
139     *
140     * @param cms the CMS context
141     * @param detailContent the detail content
142     * @param contentLocale the content locale
143     *
144     * @return the detail only page, or Optional.absent() if there is no detail only page
145     */
146    public static Optional<CmsResource> getDetailOnlyPage(
147        CmsObject cms,
148        CmsResource detailContent,
149        String contentLocale) {
150
151        try {
152            CmsObject rootCms = OpenCms.initCmsObject(cms);
153            rootCms.getRequestContext().setSiteRoot("");
154            String path = getDetailOnlyPageNameWithoutLocaleCheck(detailContent.getRootPath(), contentLocale);
155            if (rootCms.existsResource(path, CmsResourceFilter.ALL)) {
156                CmsResource detailOnlyRes = rootCms.readResource(path, CmsResourceFilter.ALL);
157                return Optional.of(detailOnlyRes);
158            }
159            return Optional.absent();
160        } catch (CmsException e) {
161            LOG.warn(e.getLocalizedMessage(), e);
162            return Optional.absent();
163        }
164    }
165
166    /**
167     * Returns the detail only container page bean or <code>null</code> if none available.<p>
168     *
169     * @param cms the cms context
170     * @param req the current request
171     * @param pageRootPath the root path of the page
172     *
173     * @return the container page bean
174     */
175    public static CmsContainerPageBean getDetailOnlyPage(CmsObject cms, ServletRequest req, String pageRootPath) {
176
177        return getDetailOnlyPage(cms, req, pageRootPath, true);
178    }
179
180    /**
181     * Returns the detail only container page bean or <code>null</code> if none available.<p>
182     *
183     * @param cms the cms context
184     * @param req the current request
185     * @param pageRootPath the root path of the page
186     * @param lookupContextFirst flag, indicating if the bean should be looked up in the standard context first.
187     *
188     * @return the container page bean
189     */
190    public static CmsContainerPageBean getDetailOnlyPage(
191        CmsObject cms,
192        ServletRequest req,
193        String pageRootPath,
194        boolean lookupContextFirst) {
195
196        CmsJspStandardContextBean standardContext = CmsJspStandardContextBean.getInstance(req);
197        CmsContainerPageBean detailOnlyPage = lookupContextFirst ? standardContext.getDetailOnlyPage() : null;
198        if (standardContext.isDetailRequest() && (detailOnlyPage == null)) {
199
200            try {
201                CmsObject rootCms = OpenCms.initCmsObject(cms);
202                rootCms.getRequestContext().setSiteRoot("");
203                String locale = getDetailContainerLocale(
204                    cms,
205                    cms.getRequestContext().getLocale().toString(),
206                    cms.readResource(cms.getRequestContext().getUri()));
207
208                String resourceName = getDetailOnlyPageNameWithoutLocaleCheck(
209                    standardContext.getDetailContent().getRootPath(),
210                    locale);
211                CmsResource resource = null;
212                if (rootCms.existsResource(resourceName)) {
213                    resource = rootCms.readResource(resourceName);
214                } else {
215                    // check if the deprecated locale independent detail container page exists
216                    resourceName = getDetailOnlyPageNameWithoutLocaleCheck(
217                        standardContext.getDetailContent().getRootPath(),
218                        null);
219                    if (rootCms.existsResource(resourceName)) {
220                        resource = rootCms.readResource(resourceName);
221                    }
222                }
223
224                CmsXmlContainerPage xmlContainerPage = null;
225                if (resource != null) {
226                    xmlContainerPage = CmsXmlContainerPageFactory.unmarshal(rootCms, resource, req);
227                }
228                if (xmlContainerPage != null) {
229                    detailOnlyPage = xmlContainerPage.getContainerPage(rootCms);
230                    detailOnlyPage = CmsTemplateMapper.get(req).transformContainerpageBean(
231                        rootCms,
232                        detailOnlyPage,
233                        pageRootPath);
234                    standardContext.setDetailOnlyPage(detailOnlyPage);
235                }
236            } catch (CmsException e) {
237                LOG.error(e.getLocalizedMessage(), e);
238            }
239        }
240        return detailOnlyPage;
241    }
242
243    /**
244     * Returns the site/root path to the detail only container page, for site/root path of the detail content.<p>
245     *
246     * @param cms the current cms context
247     * @param pageResource the detail page resource
248     * @param detailPath the site or root path to the detail content (accordingly site or root path's will be returned)
249     * @param locale the locale for which we want the detail only page
250     *
251     * @return the site or root path to the detail only container page (dependent on providing site or root path for the detailPath).
252     */
253    public static String getDetailOnlyPageName(
254        CmsObject cms,
255        CmsResource pageResource,
256        String detailPath,
257        String locale) {
258
259        return getDetailOnlyPageNameWithoutLocaleCheck(detailPath, getDetailContainerLocale(cms, locale, pageResource));
260    }
261
262    /**
263     * Gets the detail only resource for a given detail content and locale.<p>
264     *
265     * @param cms the current cms context
266     * @param contentLocale the locale for which we want the detail only resource
267     * @param detailContentRes the detail content resource
268     * @param pageRes the page resource
269     *
270     * @return an Optional wrapping a detail only resource
271     */
272    public static Optional<CmsResource> getDetailOnlyResource(
273        CmsObject cms,
274        String contentLocale,
275        CmsResource detailContentRes,
276        CmsResource pageRes) {
277
278        Optional<CmsResource> detailOnlyRes = getDetailOnlyPage(
279            cms,
280            detailContentRes,
281            getDetailContainerLocale(cms, contentLocale, pageRes));
282        return detailOnlyRes;
283    }
284
285    /**
286     * Returns a list of detail only container pages associated with the given resource.<p>
287     *
288     * @param cms the cms context
289     * @param resource the resource
290     *
291     * @return the list of detail only container pages
292     */
293    public static List<CmsResource> getDetailOnlyResources(CmsObject cms, CmsResource resource) {
294
295        List<CmsResource> result = new ArrayList<CmsResource>();
296        Set<String> resourcePaths = new HashSet<String>();
297        String sitePath = cms.getSitePath(resource);
298        for (Locale locale : OpenCms.getLocaleManager().getAvailableLocales()) {
299            resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, locale.toString()));
300        }
301        // in case the deprecated locale less detail container resource exists
302        resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, null));
303        // add the locale independent detail container resource
304        resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, LOCALE_ALL));
305        for (String path : resourcePaths) {
306            try {
307                CmsResource detailContainers = cms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION);
308                result.add(detailContainers);
309            } catch (CmsException e) {
310                // will happen in case resource does not exist, ignore
311            }
312        }
313        return result;
314    }
315
316    /**
317     * Checks whether the given resource path is of a detail containers page.<p>
318     *
319     * @param cms the cms context
320     * @param detailContainersPage the resource site path
321     *
322     * @return <code>true</code> if the given resource path is of a detail containers page
323     */
324    public static boolean isDetailContainersPage(CmsObject cms, String detailContainersPage) {
325
326        boolean result = false;
327        try {
328            String detailName = CmsResource.getName(detailContainersPage);
329            String parentFolder = CmsResource.getParentFolder(detailContainersPage);
330            if (!parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")) {
331                // this will be the case for locale dependent detail only pages, move one level up
332                parentFolder = CmsResource.getParentFolder(parentFolder);
333            }
334            detailName = CmsStringUtil.joinPaths(CmsResource.getParentFolder(parentFolder), detailName);
335            result = parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")
336                && cms.existsResource(detailName, CmsResourceFilter.IGNORE_EXPIRATION);
337        } catch (Throwable t) {
338            // may happen in case string operations fail
339            LOG.debug(t.getLocalizedMessage(), t);
340        }
341        return result;
342    }
343
344    /**
345     * Creates an empty detail-only page for a content, or just reads the resource if the detail-only page already exists.<p>
346     *
347     * @param cms the current CMS context
348     * @param detailId the structure id of the detail content
349     * @param detailOnlyRootPath the path of the detail only page
350     *
351     * @return the detail-only page
352     *
353     * @throws CmsException if something goes wrong
354     */
355    public static CmsResource readOrCreateDetailOnlyPage(CmsObject cms, CmsUUID detailId, String detailOnlyRootPath)
356    throws CmsException {
357
358        CmsObject rootCms = OpenCms.initCmsObject(cms);
359        rootCms.getRequestContext().setSiteRoot("");
360        CmsResource containerpage;
361        if (rootCms.existsResource(detailOnlyRootPath)) {
362            containerpage = rootCms.readResource(detailOnlyRootPath);
363        } else {
364            String parentFolder = CmsResource.getFolderPath(detailOnlyRootPath);
365            List<String> foldersToCreate = new ArrayList<String>();
366            // ensure the parent folder exists
367            while (!rootCms.existsResource(parentFolder)) {
368                foldersToCreate.add(0, parentFolder);
369                parentFolder = CmsResource.getParentFolder(parentFolder);
370            }
371            for (String folderName : foldersToCreate) {
372                CmsResource parentRes = rootCms.createResource(
373                    folderName,
374                    OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName()));
375                // set the search exclude property on parent folder
376                rootCms.writePropertyObject(
377                    folderName,
378                    new CmsProperty(
379                        CmsPropertyDefinition.PROPERTY_SEARCH_EXCLUDE,
380                        A_CmsSearchIndex.PROPERTY_SEARCH_EXCLUDE_VALUE_ALL,
381                        null));
382                CmsLockUtil.tryUnlock(rootCms, parentRes);
383            }
384            containerpage = rootCms.createResource(
385                detailOnlyRootPath,
386                OpenCms.getResourceManager().getResourceType(CmsResourceTypeXmlContainerPage.getStaticTypeName()));
387            // after creation, the file has a non-temporary exclusive lock. Unlock it so we can lock it with a temporary lock after this.
388            CmsLockUtil.tryUnlock(rootCms, containerpage);
389        }
390        CmsLockUtil.ensureLock(rootCms, containerpage);
391        try {
392            CmsResource detailResource = cms.readResource(detailId, CmsResourceFilter.IGNORE_EXPIRATION);
393            String title = cms.readPropertyObject(
394                detailResource,
395                CmsPropertyDefinition.PROPERTY_TITLE,
396                true).getValue();
397            if (title != null) {
398                title = Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
399                    Messages.GUI_DETAIL_CONTENT_PAGE_TITLE_1,
400                    title);
401                CmsProperty titleProp = new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, title, null);
402                cms.writePropertyObjects(containerpage, Arrays.asList(titleProp));
403            }
404
405            List<CmsRelation> relations = cms.readRelations(
406                CmsRelationFilter.relationsFromStructureId(detailId).filterType(CmsRelationType.DETAIL_ONLY));
407            boolean hasRelation = false;
408            for (CmsRelation relation : relations) {
409                if (relation.getTargetId().equals(containerpage.getStructureId())) {
410                    hasRelation = true;
411                    break;
412                }
413            }
414            if (!hasRelation) {
415                CmsLockActionRecord lockRecord = null;
416                try {
417                    lockRecord = CmsLockUtil.ensureLock(cms, detailResource);
418                    cms.addRelationToResource(detailResource, containerpage, CmsRelationType.DETAIL_ONLY.getName());
419                } finally {
420                    if ((lockRecord != null) && (lockRecord.getChange() == LockChange.locked)) {
421                        cms.unlockResource(detailResource);
422                    }
423                }
424            }
425        } catch (CmsException e) {
426            CmsContainerpageService.LOG.error(e.getLocalizedMessage(), e);
427        }
428        return containerpage;
429    }
430
431    /**
432     * Saves a detail-only page for a content.<p>
433     *
434     * If the detail-only page already exists, it is overwritten.
435     *
436     * @param cms the current CMS context
437     * @param content the content for which to save the detail-only page
438     * @param locale the locale
439     * @param page the container page data to save in the detail-only page
440
441     * @throws CmsException if something goes wrong
442     * @return the container page that was saved
443     */
444    public static CmsXmlContainerPage saveDetailOnlyPage(
445        CmsObject cms,
446        CmsResource content,
447        String locale,
448        CmsContainerPageBean page)
449
450    throws CmsException {
451
452        String detailOnlyPath = getDetailOnlyPageNameWithoutLocaleCheck(content.getRootPath(), locale);
453        CmsResource resource = readOrCreateDetailOnlyPage(cms, content.getStructureId(), detailOnlyPath);
454        CmsXmlContainerPage xmlCntPage = CmsXmlContainerPageFactory.unmarshal(cms, cms.readFile(resource));
455        xmlCntPage.save(cms, page);
456        return xmlCntPage;
457    }
458
459    /**
460     * Checks whether single locale detail containers should be used for the given site root.<p>
461     *
462     * @param siteRoot the site root to check
463     *
464     * @return <code>true</code> if single locale detail containers should be used for the given site root
465     */
466    public static boolean useSingleLocaleDetailContainers(String siteRoot) {
467
468        boolean result = false;
469        if ((siteRoot != null)
470            && (OpenCms.getLocaleManager().getLocaleHandler() instanceof CmsSingleTreeLocaleHandler)) {
471            CmsSite site = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot);
472            result = (site != null) && CmsSite.LocalizationMode.singleTree.equals(site.getLocalizationMode());
473        }
474        return result;
475    }
476
477    /**
478     * Returns the site path to the detail only container page.<p>
479     *
480     * This does not perform any further checks regarding the locale and assumes that all these checks have been done before.
481     *
482     * @param detailContentSitePath the detail content site path
483     * @param contentLocale the content locale
484     *
485     * @return the site path to the detail only container page
486     */
487    static String getDetailOnlyPageNameWithoutLocaleCheck(String detailContentSitePath, String contentLocale) {
488
489        String result = CmsResource.getFolderPath(detailContentSitePath);
490        if (contentLocale != null) {
491            result = CmsStringUtil.joinPaths(
492                result,
493                DETAIL_CONTAINERS_FOLDER_NAME,
494                contentLocale.toString(),
495                CmsResource.getName(detailContentSitePath));
496        } else {
497            result = CmsStringUtil.joinPaths(
498                result,
499                DETAIL_CONTAINERS_FOLDER_NAME,
500                CmsResource.getName(detailContentSitePath));
501        }
502        return result;
503    }
504
505}