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.ui.dialogs.availability;
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.CmsResourceTypeXmlContent;
036import org.opencms.file.types.I_CmsResourceType;
037import org.opencms.gwt.shared.CmsPrincipalBean;
038import org.opencms.loader.CmsLoaderException;
039import org.opencms.lock.CmsLockActionRecord;
040import org.opencms.lock.CmsLockActionRecord.LockChange;
041import org.opencms.lock.CmsLockException;
042import org.opencms.lock.CmsLockUtil;
043import org.opencms.main.CmsException;
044import org.opencms.main.CmsLog;
045import org.opencms.main.OpenCms;
046import org.opencms.security.CmsAccessControlEntry;
047import org.opencms.security.I_CmsPrincipal;
048import org.opencms.ui.A_CmsUI;
049import org.opencms.ui.CmsVaadinUtils;
050import org.opencms.ui.I_CmsDialogContext;
051import org.opencms.ui.components.CmsBasicDialog;
052import org.opencms.ui.components.CmsDateField;
053import org.opencms.ui.components.CmsOkCancelActionHandler;
054import org.opencms.ui.components.CmsResourceInfo;
055import org.opencms.util.CmsStringUtil;
056import org.opencms.util.CmsUUID;
057import org.opencms.workplace.CmsWorkplace;
058import org.opencms.workplace.CmsWorkplaceSettings;
059
060import java.util.ArrayList;
061import java.util.Collections;
062import java.util.Date;
063import java.util.HashMap;
064import java.util.Iterator;
065import java.util.List;
066import java.util.Map;
067
068import org.apache.commons.logging.Log;
069
070import com.vaadin.ui.Button;
071import com.vaadin.ui.Button.ClickEvent;
072import com.vaadin.ui.Button.ClickListener;
073import com.vaadin.ui.Panel;
074import com.vaadin.v7.data.Property.ValueChangeEvent;
075import com.vaadin.v7.data.Property.ValueChangeListener;
076import com.vaadin.v7.data.Validator;
077import com.vaadin.v7.ui.CheckBox;
078import com.vaadin.v7.ui.TextField;
079import com.vaadin.v7.ui.VerticalLayout;
080
081/**
082 * Availability dialog.<p>
083 */
084public class CmsAvailabilityDialog extends CmsBasicDialog {
085
086    /** Preference key for the option to automatically include children. */
087    public static final String PREF_AVAILABILITY_INCLUDE_CHILDREN_DEFAULT = "availabilityIncludeChildrenDefault";
088
089    /** Logger for this class. */
090    private static final Log LOG = CmsLog.getLog(CmsAvailabilityDialog.class);
091
092    /** Serial version id. */
093    private static final long serialVersionUID = 1L;
094
095    /** Availability info. */
096    private CmsAvailabilityInfoBean m_availabilityInfo;
097
098    /** The cancel button. */
099    private Button m_cancelButton;
100
101    /** The dialog context. */
102    private I_CmsDialogContext m_dialogContext;
103
104    /** Date field. */
105    private CmsDateField m_expiredField;
106
107    /** Initial value for 'notification enabled. */
108    private Boolean m_initialNotificationEnabled = Boolean.FALSE;
109
110    /** Initial value for notification interval. */
111    private String m_initialNotificationInterval = "";
112
113    /** 'Modify siblings' check box. */
114    private CheckBox m_modifySiblingsField;
115
116    /**  'enable notification' check box. */
117    private CheckBox m_notificationEnabledField;
118
119    /** Field for the notification interval. */
120    private TextField m_notificationIntervalField;
121
122    /** Panel for notifications. */
123    private Panel m_notificationPanel;
124
125    /** OK button. */
126    private Button m_okButton;
127
128    /** Date field. */
129    private CmsDateField m_releasedField;
130
131    /** Option to reset the expiration. */
132    private CheckBox m_resetExpired;
133
134    /** Option to reset the relase date. */
135    private CheckBox m_resetReleased;
136
137    /** Container for responsibles widgets. */
138    private VerticalLayout m_responsiblesContainer;
139
140    /** Panel for 'Responsibles'. */
141    private Panel m_responsiblesPanel;
142
143    /** Option to enable subresource modification. */
144    private CheckBox m_subresourceModificationField;
145
146    /**
147     * Creates a new instance.<p>
148     *
149     * @param dialogContext the dialog context
150     */
151    public CmsAvailabilityDialog(I_CmsDialogContext dialogContext) {
152
153        super();
154        m_dialogContext = dialogContext;
155        CmsObject cms = dialogContext.getCms();
156        CmsVaadinUtils.readAndLocalizeDesign(
157            this,
158            OpenCms.getWorkplaceManager().getMessages(A_CmsUI.get().getLocale()),
159            null);
160        List<CmsResource> resources = dialogContext.getResources();
161        m_notificationIntervalField.addValidator(new Validator() {
162
163            /** Serial version id. */
164            private static final long serialVersionUID = 1L;
165
166            public void validate(Object value) throws InvalidValueException {
167
168                String strValue = ((String)value).trim();
169                if (!strValue.matches("[0-9]*")) {
170                    throw new InvalidValueException(
171                        CmsVaadinUtils.getMessageText(org.opencms.ui.Messages.GUI_VALIDATOR_EMPTY_OR_NUMBER_0));
172
173                }
174
175            }
176        });
177
178        boolean hasSiblings = false;
179        for (CmsResource resource : m_dialogContext.getResources()) {
180            hasSiblings |= resource.getSiblingCount() > 1;
181            if (hasSiblings) {
182                break;
183            }
184        }
185        m_modifySiblingsField.setVisible(hasSiblings);
186
187        if (resources.size() == 1) {
188            CmsResource onlyResource = resources.get(0);
189            if (onlyResource.getDateReleased() != CmsResource.DATE_RELEASED_DEFAULT) {
190                m_releasedField.setDate(new Date(onlyResource.getDateReleased()));
191            }
192            if (onlyResource.getDateExpired() != CmsResource.DATE_EXPIRED_DEFAULT) {
193                m_expiredField.setDate(new Date(onlyResource.getDateExpired()));
194            }
195            initNotification();
196            Map<CmsPrincipalBean, String> responsibles = m_availabilityInfo.getResponsibles();
197            if (!responsibles.isEmpty()) {
198                m_responsiblesPanel.setVisible(true);
199                m_notificationPanel.setVisible(true);
200                for (Map.Entry<CmsPrincipalBean, String> entry : responsibles.entrySet()) {
201                    CmsPrincipalBean principal = entry.getKey();
202                    String icon = principal.isGroup()
203                    ? CmsWorkplace.getResourceUri("buttons/group.png")
204                    : CmsWorkplace.getResourceUri("buttons/user.png");
205                    String subtitle = "";
206                    if (entry.getValue() != null) {
207                        subtitle = cms.getRequestContext().removeSiteRoot(entry.getValue());
208                    }
209                    CmsResourceInfo infoWidget = new CmsResourceInfo(entry.getKey().getName(), subtitle, icon);
210                    m_responsiblesContainer.addComponent(infoWidget);
211
212                }
213            }
214        } else {
215            boolean showNotification = false;
216            resourceLoop: for (CmsResource resource : resources) {
217                try {
218                    List<CmsAccessControlEntry> aces = cms.getAccessControlEntries(cms.getSitePath(resource));
219                    for (CmsAccessControlEntry ace : aces) {
220                        if (ace.isResponsible()) {
221                            showNotification = true;
222                            break resourceLoop;
223                        }
224                    }
225                } catch (CmsException e) {
226                    LOG.error(e.getLocalizedMessage(), e);
227                }
228            }
229            m_notificationPanel.setVisible(showNotification);
230        }
231        m_notificationEnabledField.setValue(m_initialNotificationEnabled);
232        m_notificationIntervalField.setValue(m_initialNotificationInterval);
233        boolean hasFolders = false;
234        for (CmsResource resource : resources) {
235            if (resource.isFolder()) {
236                hasFolders = true;
237            }
238        }
239
240        CmsWorkplaceSettings wpSettings = A_CmsUI.get().getWorkplaceSettings();
241        boolean includeChildren = Boolean.parseBoolean(
242            wpSettings.getUserSettings().getAdditionalPreference(PREF_AVAILABILITY_INCLUDE_CHILDREN_DEFAULT, true));
243        m_subresourceModificationField.setVisible(hasFolders);
244        m_subresourceModificationField.setValue(Boolean.valueOf(includeChildren));
245
246        initResetCheckbox(m_resetReleased, m_releasedField);
247        initResetCheckbox(m_resetExpired, m_expiredField);
248        m_okButton.addClickListener(new ClickListener() {
249
250            private static final long serialVersionUID = 1L;
251
252            public void buttonClick(ClickEvent event) {
253
254                submit();
255            }
256
257        });
258
259        m_cancelButton.addClickListener(new ClickListener() {
260
261            private static final long serialVersionUID = 1L;
262
263            public void buttonClick(ClickEvent event) {
264
265                cancel();
266            }
267        });
268        displayResourceInfo(m_dialogContext.getResources());
269
270        setActionHandler(new CmsOkCancelActionHandler() {
271
272            private static final long serialVersionUID = 1L;
273
274            @Override
275            protected void cancel() {
276
277                CmsAvailabilityDialog.this.cancel();
278            }
279
280            @Override
281            protected void ok() {
282
283                submit();
284            }
285        });
286    }
287
288    /**
289     * Initializes the values for the notification widgets.<p>
290     */
291    public void initNotification() {
292
293        if (m_dialogContext.getResources().size() == 1) {
294            CmsResource resource = m_dialogContext.getResources().get(0);
295            try {
296                m_availabilityInfo = getAvailabilityInfo(A_CmsUI.getCmsObject(), resource);
297                m_initialNotificationInterval = "" + m_availabilityInfo.getNotificationInterval();
298                m_initialNotificationEnabled = Boolean.valueOf(m_availabilityInfo.isNotificationEnabled());
299            } catch (CmsLoaderException e) {
300                LOG.error(e.getLocalizedMessage(), e);
301            } catch (CmsException e) {
302                LOG.error(e.getLocalizedMessage(), e);
303            }
304
305        }
306    }
307
308    /**
309     * Actually performs the availability change.<p>
310     *
311     * @return the ids of the changed resources
312     *
313     * @throws CmsException if something goes wrong
314     */
315    protected List<CmsUUID> changeAvailability() throws CmsException {
316
317        Date released = m_releasedField.getDate();
318        Date expired = m_expiredField.getDate();
319        boolean resetReleased = m_resetReleased.getValue().booleanValue();
320        boolean resetExpired = m_resetExpired.getValue().booleanValue();
321        boolean modifySubresources = m_subresourceModificationField.getValue().booleanValue();
322        List<CmsUUID> changedIds = new ArrayList<CmsUUID>();
323        for (CmsResource resource : m_dialogContext.getResources()) {
324            changeAvailability(resource, released, resetReleased, expired, resetExpired, modifySubresources);
325            changedIds.add(resource.getStructureId());
326        }
327
328        String notificationInterval = m_notificationIntervalField.getValue().trim();
329        int notificationIntervalInt = 0;
330        try {
331            notificationIntervalInt = Integer.parseInt(notificationInterval);
332        } catch (NumberFormatException e) {
333            LOG.warn(e.getLocalizedMessage(), e);
334        }
335        Boolean notificationEnabled = m_notificationEnabledField.getValue();
336        boolean notificationSettingsUnchanged = notificationInterval.equals(m_initialNotificationInterval)
337            && notificationEnabled.equals(m_initialNotificationEnabled);
338
339        CmsObject cms = A_CmsUI.getCmsObject();
340        if (!notificationSettingsUnchanged) {
341            for (CmsResource resource : m_dialogContext.getResources()) {
342                performSingleResourceNotification(
343                    A_CmsUI.getCmsObject(),
344                    cms.getSitePath(resource),
345                    notificationEnabled.booleanValue(),
346                    notificationIntervalInt,
347                    m_modifySiblingsField.getValue().booleanValue());
348            }
349
350        }
351        return changedIds;
352    }
353
354    /**
355     * Cancels the dialog.<p>
356     */
357    void cancel() {
358
359        m_dialogContext.finish(new ArrayList<CmsUUID>());
360    }
361
362    /**
363     * Returns the availability info.<p>
364     *
365     * @param cms the cms context
366     * @param res the resource
367     *
368     * @return the info
369     *
370     * @throws CmsException if reading the info fails
371     */
372    CmsAvailabilityInfoBean getAvailabilityInfo(CmsObject cms, CmsResource res) throws CmsException {
373
374        CmsAvailabilityInfoBean result = new CmsAvailabilityInfoBean();
375        I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(res.getTypeId());
376        result.setResType(type.getTypeName());
377
378        result.setDateReleased(res.getDateReleased());
379        result.setDateExpired(res.getDateExpired());
380
381        String notificationInterval = cms.readPropertyObject(
382            res,
383            CmsPropertyDefinition.PROPERTY_NOTIFICATION_INTERVAL,
384            false).getValue();
385        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(notificationInterval)) {
386            result.setNotificationInterval(Integer.valueOf(notificationInterval).intValue());
387        }
388
389        String notificationEnabled = cms.readPropertyObject(
390            res,
391            CmsPropertyDefinition.PROPERTY_ENABLE_NOTIFICATION,
392            false).getValue();
393        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(notificationEnabled)) {
394            result.setNotificationEnabled(Boolean.valueOf(notificationEnabled).booleanValue());
395        }
396
397        result.setHasSiblings(cms.readSiblings(res, CmsResourceFilter.ALL).size() > 1);
398
399        result.setResponsibles(getResponsibles(cms, res));
400
401        return result;
402    }
403
404    /**
405     * Returns the responsibles.<p>
406     *
407     * @param cms the cms context
408     * @param res the resource
409     *
410     * @return the responsibles
411     */
412    Map<CmsPrincipalBean, String> getResponsibles(CmsObject cms, CmsResource res) {
413
414        Map<CmsPrincipalBean, String> result = new HashMap<CmsPrincipalBean, String>();
415        List<CmsResource> parentResources = new ArrayList<CmsResource>();
416        // get all parent folders of the current file
417        try {
418            parentResources = cms.readPath(res, CmsResourceFilter.IGNORE_EXPIRATION);
419        } catch (CmsException e) {
420            LOG.error(e.getLocalizedMessage(), e);
421        }
422
423        for (CmsResource resource : parentResources) {
424            String storedSiteRoot = cms.getRequestContext().getSiteRoot();
425            String sitePath = cms.getRequestContext().removeSiteRoot(resource.getRootPath());
426            try {
427                cms.getRequestContext().setSiteRoot("/");
428                List<CmsAccessControlEntry> entries = cms.getAccessControlEntries(resource.getRootPath(), false);
429                for (CmsAccessControlEntry ace : entries) {
430                    if (ace.isResponsible()) {
431                        I_CmsPrincipal principal = cms.lookupPrincipal(ace.getPrincipal());
432                        if (principal != null) {
433                            CmsPrincipalBean prinBean = new CmsPrincipalBean(
434                                principal.getName(),
435                                principal.getDescription(),
436                                principal.isGroup());
437                            if (!resource.getRootPath().equals(res.getRootPath())) {
438                                if (resource.getRootPath().startsWith(storedSiteRoot)) {
439                                    result.put(prinBean, sitePath);
440                                } else {
441                                    result.put(prinBean, resource.getRootPath());
442                                }
443                            } else {
444                                result.put(prinBean, null);
445                            }
446                        }
447                    }
448                }
449            } catch (CmsException e) {
450                LOG.info(
451                    "Problem with reading responsible users for "
452                        + resource.getName()
453                        + " : "
454                        + e.getLocalizedMessage(),
455                    e);
456            } finally {
457                cms.getRequestContext().setSiteRoot(storedSiteRoot);
458            }
459        }
460        return result;
461    }
462
463    /**
464     * Submits the dialog.<p>
465     */
466    void submit() {
467
468        try {
469            m_dialogContext.finish(changeAvailability());
470        } catch (Throwable t) {
471            m_dialogContext.error(t);
472        }
473    }
474
475    /**
476     * Changes availability.<p>
477     *
478     * @param resource the resource
479     * @param released release date
480     * @param resetReleased reset release date
481     * @param expired expiration date
482     * @param resetExpired reset expiration date
483     * @param modifySubresources modify children
484     *
485     * @throws CmsException if something goes wrong
486     */
487    private void changeAvailability(
488        CmsResource resource,
489        Date released,
490        boolean resetReleased,
491        Date expired,
492        boolean resetExpired,
493        boolean modifySubresources)
494    throws CmsException {
495
496        CmsObject cms = m_dialogContext.getCms();
497        CmsLockActionRecord lockActionRecord = CmsLockUtil.ensureLock(cms, resource);
498        try {
499            cms.getRequestContext().setAttribute(
500                CmsResourceTypeXmlContent.ATTR_REVERSE_AVAILABILITY_MAPPING,
501                Boolean.TRUE);
502            long newDateReleased;
503            if (resetReleased || (released != null)) {
504                newDateReleased = released != null ? released.getTime() : CmsResource.DATE_RELEASED_DEFAULT;
505                cms.setDateReleased(resource, newDateReleased, modifySubresources);
506            }
507            long newDateExpired;
508            if (resetExpired || (expired != null)) {
509                newDateExpired = expired != null ? expired.getTime() : CmsResource.DATE_EXPIRED_DEFAULT;
510                cms.setDateExpired(resource, newDateExpired, modifySubresources);
511            }
512        } finally {
513            cms.getRequestContext().removeAttribute(CmsResourceTypeXmlContent.ATTR_REVERSE_AVAILABILITY_MAPPING);
514            if (lockActionRecord.getChange() == LockChange.locked) {
515                try {
516                    cms.unlockResource(resource);
517                } catch (CmsLockException e) {
518                    LOG.warn(e.getLocalizedMessage(), e);
519                }
520            }
521
522        }
523    }
524
525    /**
526     * Creates a reset checkbox which can enable / disable a date field.<p>
527     *
528     * @param box the check box
529     * @param field the date field
530     */
531    private void initResetCheckbox(CheckBox box, final CmsDateField field) {
532
533        box.addValueChangeListener(new ValueChangeListener() {
534
535            private static final long serialVersionUID = 1L;
536
537            public void valueChange(ValueChangeEvent event) {
538
539                Boolean value = (Boolean)(event.getProperty().getValue());
540                if (value.booleanValue()) {
541                    field.clear();
542                    field.setEnabled(false);
543                } else {
544                    field.setEnabled(true);
545                }
546            }
547        });
548    }
549
550    /**
551     * Performs the notification operations on a single resource.<p>
552     *
553     * @param cms the CMS context
554     * @param resName the VFS path of the resource
555     * @param enableNotification if the notification is activated
556     * @param notificationInterval the notification interval in days
557     * @param modifySiblings flag indicating to include resource siblings
558     *
559     * @throws CmsException if the availability and notification operations fail
560     */
561    private void performSingleResourceNotification(
562        CmsObject cms,
563        String resName,
564        boolean enableNotification,
565        int notificationInterval,
566        boolean modifySiblings)
567    throws CmsException {
568
569        List<CmsResource> resources = new ArrayList<CmsResource>();
570        if (modifySiblings) {
571            // modify all siblings of a resource
572            resources = cms.readSiblings(resName, CmsResourceFilter.IGNORE_EXPIRATION);
573        } else {
574            // modify only resource without siblings
575            resources.add(cms.readResource(resName, CmsResourceFilter.IGNORE_EXPIRATION));
576        }
577        Iterator<CmsResource> i = resources.iterator();
578        while (i.hasNext()) {
579            CmsResource resource = i.next();
580            // lock resource if auto lock is enabled
581            CmsLockActionRecord lockRecord = CmsLockUtil.ensureLock(cms, resource);
582            try {
583                // write notification settings
584                writeProperty(
585                    cms,
586                    resource,
587                    CmsPropertyDefinition.PROPERTY_NOTIFICATION_INTERVAL,
588                    String.valueOf(notificationInterval));
589                writeProperty(
590                    cms,
591                    resource,
592                    CmsPropertyDefinition.PROPERTY_ENABLE_NOTIFICATION,
593                    String.valueOf(enableNotification));
594            } finally {
595                if (lockRecord.getChange() == LockChange.locked) {
596                    cms.unlockResource(resource);
597                }
598            }
599        }
600    }
601
602    /**
603     * Writes a property value for a resource.<p>
604     *
605     * @param cms the cms context
606     * @param resource the path of the resource
607     * @param propertyName the name of the property
608     * @param propertyValue the new value of the property
609     *
610     * @throws CmsException if something goes wrong
611     */
612    private void writeProperty(CmsObject cms, CmsResource resource, String propertyName, String propertyValue)
613    throws CmsException {
614
615        if (CmsStringUtil.isEmpty(propertyValue)) {
616            propertyValue = CmsProperty.DELETE_VALUE;
617        }
618
619        CmsProperty newProp = new CmsProperty();
620        newProp.setName(propertyName);
621        CmsProperty oldProp = cms.readPropertyObject(resource, propertyName, false);
622        if (oldProp.isNullProperty()) {
623            // property value was not already set
624            if (OpenCms.getWorkplaceManager().isDefaultPropertiesOnStructure()) {
625                newProp.setStructureValue(propertyValue);
626            } else {
627                newProp.setResourceValue(propertyValue);
628            }
629        } else {
630            if (oldProp.getStructureValue() != null) {
631                newProp.setStructureValue(propertyValue);
632                newProp.setResourceValue(oldProp.getResourceValue());
633            } else {
634                newProp.setResourceValue(propertyValue);
635            }
636        }
637
638        newProp.setAutoCreatePropertyDefinition(true);
639
640        String oldStructureValue = oldProp.getStructureValue();
641        String newStructureValue = newProp.getStructureValue();
642        if (CmsStringUtil.isEmpty(oldStructureValue)) {
643            oldStructureValue = CmsProperty.DELETE_VALUE;
644        }
645        if (CmsStringUtil.isEmpty(newStructureValue)) {
646            newStructureValue = CmsProperty.DELETE_VALUE;
647        }
648
649        String oldResourceValue = oldProp.getResourceValue();
650        String newResourceValue = newProp.getResourceValue();
651        if (CmsStringUtil.isEmpty(oldResourceValue)) {
652            oldResourceValue = CmsProperty.DELETE_VALUE;
653        }
654        if (CmsStringUtil.isEmpty(newResourceValue)) {
655            newResourceValue = CmsProperty.DELETE_VALUE;
656        }
657
658        // change property only if it has been changed
659        if (!oldResourceValue.equals(newResourceValue) || !oldStructureValue.equals(newStructureValue)) {
660            cms.writePropertyObjects(resource, Collections.singletonList(newProp));
661        }
662    }
663}