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.xml.content;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.i18n.CmsLocaleManager;
037import org.opencms.loader.I_CmsFileNameGenerator;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.main.OpenCms;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.util.CmsUUID;
043
044import java.util.HashMap;
045import java.util.Iterator;
046import java.util.List;
047import java.util.Locale;
048import java.util.Map;
049import java.util.Set;
050
051import org.apache.commons.lang3.RandomStringUtils;
052import org.apache.commons.logging.Log;
053
054import com.google.common.collect.Lists;
055import com.google.common.collect.Sets;
056
057/**
058 * A class which represents the context for resolving all content value mappings of an XML content.<p>
059 *
060 * Since the content handler instance is shared between all contents of the same XML content type, we can't use
061 * it to store data which is only relevant for resolving the mappings of a single XML content, so this class was created.
062 */
063public class CmsMappingResolutionContext {
064
065    /**
066     * The attribute type.
067     */
068    public enum AttributeType {
069        expiration, release;
070    }
071
072    /**
073     * Internal bean used to keep track of URL name mappings.<p>
074     */
075    class InternalUrlNameMappingEntry {
076
077        /** Locale of the mapping. */
078        private Locale m_locale;
079
080        /** URL name of the mapping. */
081        private String m_name;
082
083        /** Structure ID of the mapping. */
084        private CmsUUID m_structureId;
085
086        /**
087         * Creates a new instance.<p>
088         *
089         * @param structureId the structure id
090         * @param name the URL name
091         * @param locale the locale
092         */
093        public InternalUrlNameMappingEntry(CmsUUID structureId, String name, Locale locale) {
094
095            m_name = name;
096            m_structureId = structureId;
097            m_locale = locale;
098        }
099
100        /**
101         * Returns the locale.<p>
102         *
103         * @return the locale
104         */
105        public Locale getLocale() {
106
107            return m_locale;
108        }
109
110        /**
111         * Returns the name.<p>
112         *
113         * @return the name
114         */
115        public String getName() {
116
117            return m_name;
118        }
119
120        /**
121         * Returns the structureId.<p>
122         *
123         * @return the structureId
124         */
125        public CmsUUID getStructureId() {
126
127            return m_structureId;
128        }
129
130    }
131
132    /** The log instance for this class. */
133    private static final Log LOG = CmsLog.getLog(CmsMappingResolutionContext.class);
134
135    /** The CMS context to use. */
136    private CmsObject m_cms;
137
138    /** The content being processed. */
139    private CmsXmlContent m_content;
140
141    /** Stored expiration dates. */
142    private Map<Locale, Long> m_dateExpired = new HashMap<>();
143
144    /** Stored release dates. */
145    private Map<Locale, Long> m_dateReleased = new HashMap<>();
146
147    /** True if the schema for the content has attribute mappings. */
148    private boolean m_hasAttributeMappings;
149
150    /** The list of URL name mappings. */
151    private List<InternalUrlNameMappingEntry> m_urlNameMappingEntries = Lists.newArrayList();
152
153    /**
154     * Creates a new instance.<p>
155     * @param content the xml content
156     * @param hasAttributeMappings true if the schema has attribute mappings
157     */
158    public CmsMappingResolutionContext(CmsXmlContent content, boolean hasAttributeMappings) {
159
160        m_content = content;
161        m_hasAttributeMappings = hasAttributeMappings;
162    }
163
164    /**
165     * Writes all the stored URL name mappings to the database.<p>
166     *
167     * @throws CmsException if something goes wrong
168     */
169    public void commitUrlNameMappings() throws CmsException {
170
171        Set<CmsUUID> structureIds = Sets.newHashSet();
172        for (InternalUrlNameMappingEntry entry : m_urlNameMappingEntries) {
173            structureIds.add(entry.getStructureId());
174        }
175
176        boolean urlnameReplace = false;
177        for (CmsUUID structureId : structureIds) {
178            try {
179                CmsResource resource = m_cms.readResource(structureId, CmsResourceFilter.ALL);
180                CmsProperty prop = m_cms.readPropertyObject(
181                    resource,
182                    CmsPropertyDefinition.PROPERTY_URLNAME_REPLACE,
183                    true);
184                if (!CmsStringUtil.isEmptyOrWhitespaceOnly(prop.getValue())) {
185                    urlnameReplace = Boolean.parseBoolean(prop.getValue());
186                }
187            } catch (CmsException e) {
188                LOG.error("Error while trying to read urlname.replace: " + e.getLocalizedMessage(), e);
189            }
190
191        }
192
193        I_CmsFileNameGenerator nameGen = OpenCms.getResourceManager().getNameGenerator();
194        for (InternalUrlNameMappingEntry entry : m_urlNameMappingEntries) {
195            Iterator<String> nameSeq = nameGen.getUrlNameSequence(entry.getName());
196            m_cms.writeUrlNameMapping(nameSeq, entry.getStructureId(), entry.getLocale().toString(), urlnameReplace);
197        }
198
199    }
200
201    /**
202     * Finalizes the mappings.<p>
203     *
204     * @throws CmsException if something goes wrong
205     */
206    public void finalizeMappings() throws CmsException {
207
208        commitUrlNameMappings();
209        if (m_hasAttributeMappings) {
210            // we do not want to change manually set attributes in case there are no attribute mappings
211            writeAttributes();
212        }
213    }
214
215    /**
216     * Stores the mapped expiration date for the given locale.
217     *
218     * @param locale the locale
219     * @param expiration the expiration date
220     */
221    public void putExpirationDate(Locale locale, long expiration) {
222
223        m_dateExpired.put(locale, Long.valueOf(expiration));
224    }
225
226    /**
227     * Stores the mapped release date for the given locale.
228     *
229     * @param locale the locale
230     * @param release the release date
231     */
232    public void putReleaseDate(Locale locale, long release) {
233
234        m_dateReleased.put(locale, Long.valueOf(release));
235    }
236
237    /**
238     * Helper method for setting release/expiration date.
239     *
240     * <p>Needs to also set the attributes on the resource of m_content because it's written later by the content handler.
241     *
242     * @param res the resource to set
243     * @param type the attribute type
244     * @param value the value to set (null for default value)
245     *
246     * @throws CmsException if something goes wrong
247     */
248    public void setAttribute(CmsResource res, AttributeType type, Long value) throws CmsException {
249
250        if (type == AttributeType.release) {
251            long actualValue = value != null ? value.longValue() : 0;
252            m_cms.setDateReleased(res, actualValue, false);
253            if ((m_content.getFile() != null) && res.getStructureId().equals(m_content.getFile().getStructureId())) {
254                m_content.getFile().setDateReleased(actualValue);
255            }
256        } else if (type == AttributeType.expiration) {
257            long actualValue = value != null ? value.longValue() : Long.MAX_VALUE;
258            m_cms.setDateExpired(res, actualValue, false);
259            if ((m_content.getFile() != null) && res.getStructureId().equals(m_content.getFile().getStructureId())) {
260                m_content.getFile().setDateExpired(actualValue);
261            }
262        }
263    }
264
265    /**
266     * Sets the CMS context to use.<p>
267     *
268     * @param cms the CMS context
269     */
270    public void setCmsObject(CmsObject cms) {
271
272        m_cms = cms;
273    }
274
275    /**
276     * Writes the mapped attributes.
277     */
278    protected void writeAttributes() {
279
280        String p = "[" + RandomStringUtils.randomAlphanumeric(6) + "] ";
281        CmsFile file = m_content.getFile();
282        LOG.info(p + "Processing attributes for " + file.getRootPath());
283        try {
284            for (CmsResource sibling : m_cms.readSiblings(file, CmsResourceFilter.IGNORE_EXPIRATION)) {
285                LOG.info(p + "Processing sibling " + sibling.getRootPath());
286                try {
287                    CmsProperty localeProp = m_cms.readPropertyObject(
288                        sibling,
289                        CmsPropertyDefinition.PROPERTY_LOCALE,
290                        true);
291                    List<Locale> locales = OpenCms.getLocaleManager().getDefaultLocales();
292                    if (!localeProp.isNullProperty()) {
293                        String localeStr = localeProp.getValue();
294                        List<Locale> tempLocales = CmsLocaleManager.getLocales(localeStr);
295                        if (!tempLocales.isEmpty()) {
296                            locales = tempLocales;
297                        }
298                    }
299                    LOG.info(p + "Using locale precedence " + locales);
300                    boolean foundLocale = false;
301                    for (Locale locale : locales) {
302                        // Use first locale from the property which is actually in the content, whether it has the values or not
303                        // (if it doesn't have them, i.e. null is passed to setAttribute, the default values wlll be set)
304                        if (m_content.hasLocale(locale)) {
305                            LOG.info(p + "Mapping attributes from locale " + locale);
306                            setAttribute(sibling, AttributeType.release, m_dateReleased.get(locale));
307                            setAttribute(sibling, AttributeType.expiration, m_dateExpired.get(locale));
308                            foundLocale = true;
309                            break;
310                        }
311                    }
312                    if (!foundLocale) {
313                        LOG.info(p + "No mapping locale found, resetting attributes");
314                        setAttribute(sibling, AttributeType.release, null);
315                        setAttribute(sibling, AttributeType.expiration, null);
316                    }
317                } catch (CmsException e) {
318                    LOG.error(p + e.getLocalizedMessage(), e);
319                }
320            }
321
322        } catch (Exception e) {
323            LOG.error(p + e.getLocalizedMessage(), e);
324        }
325    }
326
327    /**
328     * Adds an URL name mapping which should be written to the database later.<p>
329     *
330     * @param name the mapping name
331     * @param locale the locale
332     * @param structureId the structure ID
333     */
334    void addUrlNameMapping(String name, Locale locale, CmsUUID structureId) {
335
336        m_urlNameMappingEntries.add(new InternalUrlNameMappingEntry(structureId, name, locale));
337    }
338}