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        CmsJspStandardContextBean standardContext = CmsJspStandardContextBean.getInstance(req);
178        CmsContainerPageBean detailOnlyPage = standardContext.getDetailOnlyPage();
179        if (standardContext.isDetailRequest() && (detailOnlyPage == null)) {
180
181            try {
182                CmsObject rootCms = OpenCms.initCmsObject(cms);
183                rootCms.getRequestContext().setSiteRoot("");
184                String locale = getDetailContainerLocale(
185                    cms,
186                    cms.getRequestContext().getLocale().toString(),
187                    cms.readResource(cms.getRequestContext().getUri()));
188
189                String resourceName = getDetailOnlyPageNameWithoutLocaleCheck(
190                    standardContext.getDetailContent().getRootPath(),
191                    locale);
192                CmsResource resource = null;
193                if (rootCms.existsResource(resourceName)) {
194                    resource = rootCms.readResource(resourceName);
195                } else {
196                    // check if the deprecated locale independent detail container page exists
197                    resourceName = getDetailOnlyPageNameWithoutLocaleCheck(
198                        standardContext.getDetailContent().getRootPath(),
199                        null);
200                    if (rootCms.existsResource(resourceName)) {
201                        resource = rootCms.readResource(resourceName);
202                    }
203                }
204
205                CmsXmlContainerPage xmlContainerPage = null;
206                if (resource != null) {
207                    xmlContainerPage = CmsXmlContainerPageFactory.unmarshal(rootCms, resource, req);
208                }
209                if (xmlContainerPage != null) {
210                    detailOnlyPage = xmlContainerPage.getContainerPage(rootCms);
211                    detailOnlyPage = CmsTemplateMapper.get(req).transformContainerpageBean(
212                        rootCms,
213                        detailOnlyPage,
214                        pageRootPath);
215                    standardContext.setDetailOnlyPage(detailOnlyPage);
216                }
217            } catch (CmsException e) {
218                LOG.error(e.getLocalizedMessage(), e);
219            }
220        }
221        return detailOnlyPage;
222    }
223
224    /**
225     * Returns the site/root path to the detail only container page, for site/root path of the detail content.<p>
226     *
227     * @param cms the current cms context
228     * @param pageResource the detail page resource
229     * @param detailPath the site or root path to the detail content (accordingly site or root path's will be returned)
230     * @param locale the locale for which we want the detail only page
231     *
232     * @return the site or root path to the detail only container page (dependent on providing site or root path for the detailPath).
233     */
234    public static String getDetailOnlyPageName(
235        CmsObject cms,
236        CmsResource pageResource,
237        String detailPath,
238        String locale) {
239
240        return getDetailOnlyPageNameWithoutLocaleCheck(detailPath, getDetailContainerLocale(cms, locale, pageResource));
241    }
242
243    /**
244     * Gets the detail only resource for a given detail content and locale.<p>
245     *
246     * @param cms the current cms context
247     * @param contentLocale the locale for which we want the detail only resource
248     * @param detailContentRes the detail content resource
249     * @param pageRes the page resource
250     *
251     * @return an Optional wrapping a detail only resource
252     */
253    public static Optional<CmsResource> getDetailOnlyResource(
254        CmsObject cms,
255        String contentLocale,
256        CmsResource detailContentRes,
257        CmsResource pageRes) {
258
259        Optional<CmsResource> detailOnlyRes = getDetailOnlyPage(
260            cms,
261            detailContentRes,
262            getDetailContainerLocale(cms, contentLocale, pageRes));
263        return detailOnlyRes;
264    }
265
266    /**
267     * Returns a list of detail only container pages associated with the given resource.<p>
268     *
269     * @param cms the cms context
270     * @param resource the resource
271     *
272     * @return the list of detail only container pages
273     */
274    public static List<CmsResource> getDetailOnlyResources(CmsObject cms, CmsResource resource) {
275
276        List<CmsResource> result = new ArrayList<CmsResource>();
277        Set<String> resourcePaths = new HashSet<String>();
278        String sitePath = cms.getSitePath(resource);
279        for (Locale locale : OpenCms.getLocaleManager().getAvailableLocales()) {
280            resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, locale.toString()));
281        }
282        // in case the deprecated locale less detail container resource exists
283        resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, null));
284        // add the locale independent detail container resource
285        resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, LOCALE_ALL));
286        for (String path : resourcePaths) {
287            try {
288                CmsResource detailContainers = cms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION);
289                result.add(detailContainers);
290            } catch (CmsException e) {
291                // will happen in case resource does not exist, ignore
292            }
293        }
294        return result;
295    }
296
297    /**
298     * Checks whether the given resource path is of a detail containers page.<p>
299     *
300     * @param cms the cms context
301     * @param detailContainersPage the resource site path
302     *
303     * @return <code>true</code> if the given resource path is of a detail containers page
304     */
305    public static boolean isDetailContainersPage(CmsObject cms, String detailContainersPage) {
306
307        boolean result = false;
308        try {
309            String detailName = CmsResource.getName(detailContainersPage);
310            String parentFolder = CmsResource.getParentFolder(detailContainersPage);
311            if (!parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")) {
312                // this will be the case for locale dependent detail only pages, move one level up
313                parentFolder = CmsResource.getParentFolder(parentFolder);
314            }
315            detailName = CmsStringUtil.joinPaths(CmsResource.getParentFolder(parentFolder), detailName);
316            result = parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")
317                && cms.existsResource(detailName, CmsResourceFilter.IGNORE_EXPIRATION);
318        } catch (Throwable t) {
319            // may happen in case string operations fail
320            LOG.debug(t.getLocalizedMessage(), t);
321        }
322        return result;
323    }
324
325    /**
326     * Creates an empty detail-only page for a content, or just reads the resource if the detail-only page already exists.<p>
327     *
328     * @param cms the current CMS context
329     * @param detailId the structure id of the detail content
330     * @param detailOnlyRootPath the path of the detail only page
331     *
332     * @return the detail-only page
333     *
334     * @throws CmsException if something goes wrong
335     */
336    public static CmsResource readOrCreateDetailOnlyPage(CmsObject cms, CmsUUID detailId, String detailOnlyRootPath)
337    throws CmsException {
338
339        CmsObject rootCms = OpenCms.initCmsObject(cms);
340        rootCms.getRequestContext().setSiteRoot("");
341        CmsResource containerpage;
342        if (rootCms.existsResource(detailOnlyRootPath)) {
343            containerpage = rootCms.readResource(detailOnlyRootPath);
344        } else {
345            String parentFolder = CmsResource.getFolderPath(detailOnlyRootPath);
346            List<String> foldersToCreate = new ArrayList<String>();
347            // ensure the parent folder exists
348            while (!rootCms.existsResource(parentFolder)) {
349                foldersToCreate.add(0, parentFolder);
350                parentFolder = CmsResource.getParentFolder(parentFolder);
351            }
352            for (String folderName : foldersToCreate) {
353                CmsResource parentRes = rootCms.createResource(
354                    folderName,
355                    OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName()));
356                // set the search exclude property on parent folder
357                rootCms.writePropertyObject(
358                    folderName,
359                    new CmsProperty(
360                        CmsPropertyDefinition.PROPERTY_SEARCH_EXCLUDE,
361                        A_CmsSearchIndex.PROPERTY_SEARCH_EXCLUDE_VALUE_ALL,
362                        null));
363                CmsLockUtil.tryUnlock(rootCms, parentRes);
364            }
365            containerpage = rootCms.createResource(
366                detailOnlyRootPath,
367                OpenCms.getResourceManager().getResourceType(CmsResourceTypeXmlContainerPage.getStaticTypeName()));
368        }
369        CmsLockUtil.ensureLock(rootCms, containerpage);
370        try {
371            CmsResource detailResource = cms.readResource(detailId, CmsResourceFilter.IGNORE_EXPIRATION);
372            String title = cms.readPropertyObject(
373                detailResource,
374                CmsPropertyDefinition.PROPERTY_TITLE,
375                true).getValue();
376            if (title != null) {
377                title = Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
378                    Messages.GUI_DETAIL_CONTENT_PAGE_TITLE_1,
379                    title);
380                CmsProperty titleProp = new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, title, null);
381                cms.writePropertyObjects(containerpage, Arrays.asList(titleProp));
382            }
383
384            List<CmsRelation> relations = cms.readRelations(
385                CmsRelationFilter.relationsFromStructureId(detailId).filterType(CmsRelationType.DETAIL_ONLY));
386            boolean hasRelation = false;
387            for (CmsRelation relation : relations) {
388                if (relation.getTargetId().equals(containerpage.getStructureId())) {
389                    hasRelation = true;
390                    break;
391                }
392            }
393            if (!hasRelation) {
394                CmsLockActionRecord lockRecord = null;
395                try {
396                    lockRecord = CmsLockUtil.ensureLock(cms, detailResource);
397                    cms.addRelationToResource(detailResource, containerpage, CmsRelationType.DETAIL_ONLY.getName());
398                } finally {
399                    if ((lockRecord != null) && (lockRecord.getChange() == LockChange.locked)) {
400                        cms.unlockResource(detailResource);
401                    }
402                }
403            }
404        } catch (CmsException e) {
405            CmsContainerpageService.LOG.error(e.getLocalizedMessage(), e);
406        }
407        return containerpage;
408    }
409
410    /**
411     * Saves a detail-only page for a content.<p>
412     *
413     * If the detail-only page already exists, it is overwritten.
414     *
415     * @param cms the current CMS context
416     * @param content the content for which to save the detail-only page
417     * @param locale the locale
418     * @param page the container page data to save in the detail-only page
419    
420     * @throws CmsException if something goes wrong
421     * @return the container page that was saved
422     */
423    public static CmsXmlContainerPage saveDetailOnlyPage(
424        CmsObject cms,
425        CmsResource content,
426        String locale,
427        CmsContainerPageBean page)
428
429    throws CmsException {
430
431        String detailOnlyPath = getDetailOnlyPageNameWithoutLocaleCheck(content.getRootPath(), locale);
432        CmsResource resource = readOrCreateDetailOnlyPage(cms, content.getStructureId(), detailOnlyPath);
433        CmsXmlContainerPage xmlCntPage = CmsXmlContainerPageFactory.unmarshal(cms, cms.readFile(resource));
434        xmlCntPage.save(cms, page);
435        return xmlCntPage;
436    }
437
438    /**
439     * Checks whether single locale detail containers should be used for the given site root.<p>
440     *
441     * @param siteRoot the site root to check
442     *
443     * @return <code>true</code> if single locale detail containers should be used for the given site root
444     */
445    public static boolean useSingleLocaleDetailContainers(String siteRoot) {
446
447        boolean result = false;
448        if ((siteRoot != null)
449            && (OpenCms.getLocaleManager().getLocaleHandler() instanceof CmsSingleTreeLocaleHandler)) {
450            CmsSite site = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot);
451            result = (site != null) && CmsSite.LocalizationMode.singleTree.equals(site.getLocalizationMode());
452        }
453        return result;
454    }
455
456    /**
457     * Returns the site path to the detail only container page.<p>
458     *
459     * This does not perform any further checks regarding the locale and assumes that all these checks have been done before.
460     *
461     * @param detailContentSitePath the detail content site path
462     * @param contentLocale the content locale
463     *
464     * @return the site path to the detail only container page
465     */
466    static String getDetailOnlyPageNameWithoutLocaleCheck(String detailContentSitePath, String contentLocale) {
467
468        String result = CmsResource.getFolderPath(detailContentSitePath);
469        if (contentLocale != null) {
470            result = CmsStringUtil.joinPaths(
471                result,
472                DETAIL_CONTAINERS_FOLDER_NAME,
473                contentLocale.toString(),
474                CmsResource.getName(detailContentSitePath));
475        } else {
476            result = CmsStringUtil.joinPaths(
477                result,
478                DETAIL_CONTAINERS_FOLDER_NAME,
479                CmsResource.getName(detailContentSitePath));
480        }
481        return result;
482    }
483
484}