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.workplace.editors.directedit;
029
030import org.opencms.acacia.shared.I_CmsSerialDateValue.PatternType;
031import org.opencms.ade.configuration.CmsResourceTypeConfig;
032import org.opencms.ade.containerpage.shared.CmsDialogOptions;
033import org.opencms.ade.containerpage.shared.CmsDialogOptions.Option;
034import org.opencms.file.CmsFile;
035import org.opencms.file.CmsObject;
036import org.opencms.file.CmsResource;
037import org.opencms.i18n.CmsMessageContainer;
038import org.opencms.i18n.CmsMessages;
039import org.opencms.main.CmsException;
040import org.opencms.main.CmsLog;
041import org.opencms.main.OpenCms;
042import org.opencms.search.galleries.CmsGallerySearch;
043import org.opencms.search.galleries.CmsGallerySearchResult;
044import org.opencms.util.CmsUUID;
045import org.opencms.widgets.serialdate.CmsSerialDateBeanFactory;
046import org.opencms.widgets.serialdate.CmsSerialDateValue;
047import org.opencms.widgets.serialdate.I_CmsSerialDateBean;
048import org.opencms.xml.containerpage.CmsContainerElementBean;
049import org.opencms.xml.content.CmsXmlContent;
050import org.opencms.xml.content.CmsXmlContentFactory;
051import org.opencms.xml.types.CmsXmlSerialDateValue;
052import org.opencms.xml.types.I_CmsXmlContentValue;
053
054import java.text.DateFormat;
055import java.util.ArrayList;
056import java.util.Date;
057import java.util.List;
058import java.util.Locale;
059import java.util.Map;
060import java.util.Objects;
061
062import org.apache.commons.logging.Log;
063
064/** Special edit handler for contents that define multiple instances in a date series. */
065public class CmsDateSeriesEditHandler implements I_CmsEditHandler {
066
067    /** Handler, that does the real work. We use an internal handler to allow for state. */
068    private static class InternalHandler {
069
070        /** Logger for the class. */
071        private static final Log LOG = CmsLog.getLog(InternalHandler.class);
072
073        /** Option: Edit / delete the only a single item instance of the series. */
074        private static final String OPTION_INSTANCE = "instance";
075
076        /** Edit option: Edit the whole series. */
077        private static final String OPTION_SERIES = "series";
078
079        /** The cms object with the current context. */
080        private CmsObject m_cms;
081
082        /** The content that should be edited/deleted. */
083        private CmsXmlContent m_content;
084
085        /** The content value that holds the definition of the date series. */
086        private I_CmsXmlContentValue m_contentValue;
087
088        /** The edited container page element. */
089        private CmsContainerElementBean m_elementBean;
090
091        /** The file of the content that should be edited/deleted. */
092        private CmsFile m_file;
093
094        /** The date of the current instance of the series. */
095        private Date m_instanceDate;
096
097        /** UUID of the container page we currently act on. */
098        private CmsUUID m_pageContextId;
099
100        /** The current request parameters. */
101        private Map<String, String[]> m_requestParameters;
102
103        /** The date series as defined in the content. */
104        private I_CmsSerialDateBean m_series;
105
106        /** The definition of the date series from the content. */
107        private CmsSerialDateValue m_value;
108
109        /**
110         * Constructor for the internal handler, basically taking the information that is provided in all methods of {@link I_CmsEditHandler} to do the common initialization.
111         * @param cms the current cms object.
112         * @param elementBean the currently edited container page element
113         * @param requestParams the current request parameters
114         * @param pageContextId the structure id of the container page where editing takes place
115         */
116        public InternalHandler(
117            CmsObject cms,
118            CmsContainerElementBean elementBean,
119            Map<String, String[]> requestParams,
120            CmsUUID pageContextId) {
121
122            try {
123                m_cms = cms;
124                m_elementBean = elementBean;
125                m_requestParameters = requestParams;
126                m_pageContextId = pageContextId;
127                elementBean.initResource(cms);
128                CmsResource res = elementBean.getResource();
129                m_file = cms.readFile(res);
130                m_content = CmsXmlContentFactory.unmarshal(cms, m_file);
131                m_contentValue = getSerialDateContentValue(m_content, null);
132                m_value = m_contentValue != null ? new CmsSerialDateValue(m_contentValue.getStringValue(cms)) : null;
133                m_series = CmsSerialDateBeanFactory.createSerialDateBean(m_value);
134                setInstanceDate();
135
136            } catch (Exception e) {
137                LOG.error("Failed to determine all information to edit the instance of the date series.", e);
138            }
139        }
140
141        /**
142         * Returns the options for the delete dialog or <code>null</code> if no special options are available.
143         * @return the options for the delete dialog or <code>null</code> if no special options are available.
144         */
145        public CmsDialogOptions getDeleteOptions() {
146
147            if (null != m_instanceDate) {
148                Locale wpl = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms);
149                CmsMessages messages = Messages.get().getBundle(wpl);
150                if (!m_value.getPatternType().equals(PatternType.NONE)) {
151                    List<Option> options = new ArrayList<>(2);
152                    String instanceDate = DateFormat.getDateInstance(DateFormat.LONG, wpl).format(m_instanceDate);
153                    Option oInstance = new Option(
154                        OPTION_INSTANCE,
155                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_INSTANCE_1, instanceDate),
156                        messages.key(
157                            Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_INSTANCE_HELP_ACTIVE_1,
158                            instanceDate),
159                        false);
160                    options.add(oInstance);
161                    Option oSeries = new Option(
162                        CmsDialogOptions.REGULAR_DELETE,
163                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_SERIES_0),
164                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_OPTION_SERIES_HELP_ACTIVE_0),
165                        false);
166                    options.add(oSeries);
167                    return new CmsDialogOptions(
168                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_DIALOG_HEADING_0),
169                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_DELETE_DIALOG_INFO_1, getTitle(wpl)),
170                        options);
171                }
172            }
173            return null;
174
175        }
176
177        /**
178         * Returns the options for the edit dialog or <code>null</code> if no special options are available.
179         * @param isListElement flag, indicating if the edited element is in a list.
180         * @return the options for the edit dialog or <code>null</code> if no special options are available.
181         */
182        public CmsDialogOptions getEditOptions(boolean isListElement) {
183
184            if (null != m_instanceDate) {
185                Locale wpl = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms);
186                CmsMessages messages = Messages.get().getBundle(wpl);
187                if (!m_value.getPatternType().equals(PatternType.NONE)) {
188                    List<Option> options = new ArrayList<>(2);
189                    String instanceDate = DateFormat.getDateInstance(DateFormat.LONG, wpl).format(m_instanceDate);
190                    Option oInstance;
191                    if (!isListElement && !isContainerPageLockable()) {
192                        oInstance = new Option(
193                            OPTION_INSTANCE,
194                            messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_1, instanceDate),
195                            messages.key(
196                                Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_HELP_INACTIVE_1,
197                                instanceDate),
198                            true);
199                    } else {
200                        oInstance = new Option(
201                            OPTION_INSTANCE,
202                            messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_1, instanceDate),
203                            messages.key(
204                                Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_INSTANCE_HELP_ACTIVE_1,
205                                instanceDate),
206                            false);
207                    }
208                    options.add(oInstance);
209                    Option oSeries = new Option(
210                        OPTION_SERIES,
211                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_SERIES_0),
212                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_OPTION_SERIES_HELP_ACTIVE_0),
213                        false);
214                    options.add(oSeries);
215                    return new CmsDialogOptions(
216                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_DIALOG_HEADING_0),
217                        messages.key(Messages.GUI_DATE_SERIES_HANDLER_EDIT_DIALOG_INFO_1, getTitle(wpl)),
218                        options);
219                }
220            }
221            return null;
222
223        }
224
225        /**
226         * Handles a delete operation.
227         * @param deleteOption the delete option.
228         * @throws CmsException thrown if deletion fails.
229         */
230        public void handleDelete(String deleteOption) throws CmsException {
231
232            if (Objects.equals(deleteOption, OPTION_INSTANCE)) {
233                addExceptionForInstance();
234            } else {
235                throw new CmsException(
236                    new CmsMessageContainer(
237                        Messages.get(),
238                        Messages.ERR_DATE_SERIES_HANDLER_INVALID_DELETE_OPTION_1,
239                        deleteOption));
240            }
241
242        }
243
244        /**
245         * Handles the preparation of the edit action, except for the series case, since in that case no specific handling is necessary.
246         * @param editOption the edit option.
247         * @return the structure id of the content that should be edited.
248         * @throws CmsException thrown if preparing the edit operation fails.
249         */
250        public CmsUUID prepareForEdit(String editOption) throws CmsException {
251
252            if (Objects.equals(OPTION_INSTANCE, editOption)) {
253                return extractDate();
254            } else {
255                throw new CmsException(
256                    new CmsMessageContainer(
257                        Messages.get(),
258                        Messages.ERR_DATE_SERIES_HANDLER_INVALID_EDIT_OPTION_1,
259                        editOption));
260            }
261
262        }
263
264        /**
265         * Adds an exception in the series content and writes the changed content back to the file.
266         * @throws CmsException thrown if something goes wrong.
267         */
268        private void addExceptionForInstance() throws CmsException {
269
270            if (null != m_instanceDate) {
271                try {
272                    m_cms.lockResource(m_file);
273                    m_value.addException(m_instanceDate);
274                    String stringValue = m_value.toString();
275                    for (Locale l : m_content.getLocales()) {
276                        I_CmsXmlContentValue contentValue = getSerialDateContentValue(m_content, l);
277                        contentValue.setStringValue(m_cms, stringValue);
278                    }
279                    m_file.setContents(m_content.marshal());
280                    m_cms.writeFile(m_file);
281                    m_cms.unlockResource(m_file);
282                } catch (Exception e) {
283                    throw new CmsException(
284                        new CmsMessageContainer(
285                            Messages.get(),
286                            Messages.ERR_DATE_SERIES_HANDLER_ADD_EXCEPTION_FAILED_0),
287                        e);
288                }
289            } else {
290                throw new CmsException(
291                    new CmsMessageContainer(
292                        Messages.get(),
293                        Messages.ERR_DATE_SERIES_HANDLER_ADD_EXCEPTION_FAILED_MISSING_DATE_0));
294            }
295        }
296
297        /**
298         * Creates a copy of the series content and adjusts the dates in the copy. As well, adds an exception to the original series content.
299         * @return the structure id of the newly created content.
300         * @throws CmsException thrown if something goes wrong.
301         */
302        private CmsUUID extractDate() throws CmsException {
303
304            if (null != m_instanceDate) {
305                try {
306                    CmsResource page = m_cms.readResource(m_pageContextId);
307                    CmsResourceTypeConfig typeConfig = OpenCms.getADEManager().lookupConfiguration(
308                        m_cms,
309                        page.getRootPath()).getResourceType(
310                            OpenCms.getResourceManager().getResourceType(m_file).getTypeName());
311                    String pattern = typeConfig.getNamePattern(true);
312                    String newSitePath = OpenCms.getResourceManager().getNameGenerator().getNewFileName(
313                        m_cms,
314                        CmsResource.getFolderPath(m_file.getRootPath()) + pattern,
315                        5);
316                    String oldSitePath = m_cms.getSitePath(m_file);
317                    m_cms.copyResource(oldSitePath, newSitePath);
318                    CmsFile newFile = m_cms.readFile(newSitePath);
319                    CmsXmlContent newContent = CmsXmlContentFactory.unmarshal(m_cms, newFile);
320                    CmsSerialDateValue newValue = new CmsSerialDateValue();
321                    newValue.setStart(m_instanceDate);
322                    if ((m_value.getEnd() != null) && (m_series.getEventDuration() != null)) {
323                        newValue.setEnd(new Date(m_instanceDate.getTime() + m_series.getEventDuration().longValue()));
324                    }
325                    newValue.setParentSeriesId(m_file.getStructureId());
326                    newValue.setWholeDay(Boolean.valueOf(m_value.isWholeDay()));
327                    newValue.setPatternType(PatternType.NONE);
328                    String newValueString = newValue.toString();
329                    for (Locale l : newContent.getLocales()) {
330                        I_CmsXmlContentValue newContentValue = getSerialDateContentValue(newContent, l);
331                        newContentValue.setStringValue(m_cms, newValueString);
332                    }
333                    newFile.setContents(newContent.marshal());
334                    m_cms.writeFile(newFile);
335                    m_cms.unlockResource(newFile);
336
337                    addExceptionForInstance();
338
339                    return newFile.getStructureId();
340                } catch (Exception e) {
341                    throw new CmsException(
342                        new CmsMessageContainer(
343                            Messages.get(),
344                            Messages.ERR_DATE_SERIES_HANDLER_EXTRACT_CONTENT_FAILED_0),
345                        e);
346                }
347            } else {
348                throw new CmsException(
349                    new CmsMessageContainer(
350                        Messages.get(),
351                        Messages.ERR_DATE_SERIES_HANDLER_EXTRACT_CONTENT_FAILED_MISSING_DATE_0));
352            }
353        }
354
355        /**
356         * Returns the content value that contains the date series definition.
357         * @param content the XML content to read the value from.
358         * @param locale the locale to get the value in.
359         * @return the content value that contains the date series definition.
360         */
361        private I_CmsXmlContentValue getSerialDateContentValue(CmsXmlContent content, Locale locale) {
362
363            if ((null == locale) && !content.getLocales().isEmpty()) {
364                locale = content.getLocales().get(0);
365            }
366            for (I_CmsXmlContentValue value : content.getValues(locale)) {
367                if (value.getTypeName().equals(CmsXmlSerialDateValue.TYPE_NAME)) {
368                    return value;
369                }
370            }
371            return null;
372        }
373
374        /**
375         * Returns the gallery title of the series content.
376         * @param l the locale to show the title in.
377         * @return the gallery title of the series content.
378         */
379        private String getTitle(Locale l) {
380
381            CmsGallerySearchResult result;
382            try {
383                result = CmsGallerySearch.searchById(m_cms, m_contentValue.getDocument().getFile().getStructureId(), l);
384                return result.getTitle();
385            } catch (CmsException e) {
386                LOG.error("Could not retrieve title of series content.", e);
387                return "";
388            }
389
390        }
391
392        /**
393         * Checks, if the container page the edit operation takes place on can be locked by the current user.
394         * @return a flag, indicating if the page can be locked by the current user.
395         */
396        private boolean isContainerPageLockable() {
397
398            try {
399                return m_cms.getLock(m_cms.readResource(m_pageContextId)).isLockableBy(
400                    m_cms.getRequestContext().getCurrentUser());
401            } catch (Exception e) {
402                LOG.error("Failed to check if the container page is lockable by the current user.", e);
403                return false;
404            }
405        }
406
407        /**
408         * Sets the date of the currently edited instance of the series.
409         */
410        private void setInstanceDate() {
411
412            String sl = null;
413            Map<String, String> settings = m_elementBean.getSettings();
414            if (settings.containsKey(PARAM_INSTANCEDATE)) {
415                sl = settings.get(PARAM_INSTANCEDATE);
416            } else if (m_requestParameters.containsKey(PARAM_INSTANCEDATE)) {
417                String[] sls = m_requestParameters.get(PARAM_INSTANCEDATE);
418                if ((sls != null) && (sls.length > 0)) {
419                    sl = sls[0];
420                }
421            }
422            if (sl != null) {
423                try {
424                    long l = Long.parseLong(sl);
425                    Date d = new Date(l);
426                    if (m_series.getDates().contains(d)) {
427                        m_instanceDate = d;
428                    } else {
429                        throw new Exception("Instance date is not a date of the series.");
430                    }
431                } catch (Exception e) {
432                    LOG.error(
433                        "Could not read valid date from setting or request parameter \"" + PARAM_INSTANCEDATE + "\".",
434                        e);
435                }
436            } else {
437                // TODO: Handle possible other element settings that could determine the actual instance date.
438            }
439        }
440
441    }
442
443    /** The key of the parameter/setting the instance date of the instance that should be edited is read from. */
444    public static final String PARAM_INSTANCEDATE = "instancedate";
445
446    /**
447     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#getDeleteOptions(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map)
448     */
449    @Override
450    public CmsDialogOptions getDeleteOptions(
451        CmsObject cms,
452        CmsContainerElementBean elementBean,
453        CmsUUID pageContextId,
454        Map<String, String[]> requestParams) {
455
456        InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId);
457        return internalHandler.getDeleteOptions();
458    }
459
460    /**
461     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#getEditOptions(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map, boolean)
462     */
463    @Override
464    public CmsDialogOptions getEditOptions(
465        CmsObject cms,
466        CmsContainerElementBean elementBean,
467        CmsUUID pageContextId,
468        Map<String, String[]> requestParams,
469        boolean isListElement) {
470
471        InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId);
472        return internalHandler.getEditOptions(isListElement);
473    }
474
475    /**
476     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#getNewOptions(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map)
477     */
478    public CmsDialogOptions getNewOptions(
479        CmsObject cms,
480        CmsContainerElementBean elementBean,
481        CmsUUID pageContextId,
482        Map<String, String[]> requestParam) {
483
484        return null;
485    }
486
487    /**
488     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#handleDelete(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, java.lang.String, org.opencms.util.CmsUUID, java.util.Map)
489     */
490    @Override
491    public void handleDelete(
492        CmsObject cms,
493        CmsContainerElementBean elementBean,
494        String deleteOption,
495        CmsUUID pageContextId,
496        Map<String, String[]> requestParams)
497    throws CmsException {
498
499        InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId);
500
501        internalHandler.handleDelete(deleteOption);
502
503    }
504
505    /**
506     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#handleNew(org.opencms.file.CmsObject, java.lang.String, java.util.Locale, java.lang.String, java.lang.String, java.lang.String, org.opencms.xml.containerpage.CmsContainerElementBean, org.opencms.util.CmsUUID, java.util.Map, java.lang.String)
507     */
508    public String handleNew(
509        CmsObject cms,
510        String newLink,
511        Locale locale,
512        String referenceSitePath,
513        String modelFileSitePath,
514        String postCreateHandler,
515        CmsContainerElementBean element,
516        CmsUUID pageId,
517        Map<String, String[]> requestParams,
518        String choice) {
519
520        return null;
521    }
522
523    /**
524     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#prepareForEdit(org.opencms.file.CmsObject, org.opencms.xml.containerpage.CmsContainerElementBean, java.lang.String, org.opencms.util.CmsUUID, java.util.Map)
525     */
526    @Override
527    public CmsUUID prepareForEdit(
528        CmsObject cms,
529        CmsContainerElementBean elementBean,
530        String editOption,
531        CmsUUID pageContextId,
532        Map<String, String[]> requestParams)
533    throws CmsException {
534
535        if (Objects.equals(InternalHandler.OPTION_SERIES, editOption)) {
536            return elementBean.getId();
537        }
538        InternalHandler internalHandler = new InternalHandler(cms, elementBean, requestParams, pageContextId);
539        return internalHandler.prepareForEdit(editOption);
540    }
541
542    /**
543     * @see org.opencms.workplace.editors.directedit.I_CmsEditHandler#setParameters(java.util.Map)
544     */
545    public void setParameters(Map<String, String> params) {
546        // this handler doesn't need parameters
547    }
548}