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;
029
030import org.opencms.configuration.CmsSchedulerConfiguration;
031import org.opencms.db.CmsResourceState;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsProject;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.lock.CmsLock;
037import org.opencms.main.CmsContextInfo;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.main.OpenCms;
041import org.opencms.scheduler.CmsScheduledJobInfo;
042import org.opencms.scheduler.jobs.CmsPublishScheduledJob;
043import org.opencms.security.CmsPermissionSet;
044import org.opencms.security.CmsRole;
045import org.opencms.ui.CmsVaadinUtils;
046import org.opencms.ui.I_CmsDialogContext;
047import org.opencms.ui.components.CmsBasicDialog;
048import org.opencms.ui.components.CmsDateField;
049import org.opencms.ui.components.CmsOkCancelActionHandler;
050import org.opencms.util.CmsDateUtil;
051import org.opencms.util.CmsUUID;
052import org.opencms.workplace.commons.Messages;
053
054import java.text.DateFormat;
055import java.time.LocalDateTime;
056import java.util.Calendar;
057import java.util.Collections;
058import java.util.Date;
059import java.util.HashSet;
060import java.util.List;
061import java.util.Set;
062import java.util.SortedMap;
063import java.util.TreeMap;
064import java.util.stream.Collectors;
065
066import org.apache.commons.logging.Log;
067
068import com.vaadin.ui.Button;
069import com.vaadin.ui.Button.ClickEvent;
070import com.vaadin.ui.Button.ClickListener;
071import com.vaadin.ui.CheckBox;
072import com.vaadin.ui.FormLayout;
073
074/**
075 * The publish schedule dialog.<p>
076 */
077public class CmsPublishScheduledDialog extends CmsBasicDialog {
078
079    /** The serial version id. */
080    private static final long serialVersionUID = 7488454443783670970L;
081
082    /** Logger instance for this class. */
083    private static final Log LOG = CmsLog.getLog(CmsPublishScheduledDialog.class);
084
085    /** The Admin CmsObject. */
086    private static CmsObject m_adminCms;
087
088    /** The dialog context. */
089    private I_CmsDialogContext m_context;
090
091    /** The date selection field. */
092    private CmsDateField m_dateField;
093
094    /** The OK button. */
095    private Button m_okButton;
096
097    /** The cancel button. */
098    private Button m_cancelButton;
099
100    /** Include sub-resources check box. */
101    private CheckBox m_includeSubResources;
102
103    /**
104     * Constructor.<p>
105     *
106     * @param context the dialog context
107     */
108    public CmsPublishScheduledDialog(I_CmsDialogContext context) {
109
110        m_context = context;
111        displayResourceInfo(context.getResources());
112        FormLayout form = initForm();
113        setContent(form);
114        m_okButton = new Button(CmsVaadinUtils.getMessageText(org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_OK_0));
115        m_okButton.addClickListener(new ClickListener() {
116
117            private static final long serialVersionUID = 1L;
118
119            public void buttonClick(ClickEvent event) {
120
121                submit();
122            }
123        });
124        addButton(m_okButton);
125        m_cancelButton = new Button(
126            CmsVaadinUtils.getMessageText(org.opencms.workplace.Messages.GUI_DIALOG_BUTTON_CANCEL_0));
127        m_cancelButton.addClickListener(new ClickListener() {
128
129            private static final long serialVersionUID = 1L;
130
131            public void buttonClick(ClickEvent event) {
132
133                cancel();
134            }
135        });
136        addButton(m_cancelButton);
137
138        setActionHandler(new CmsOkCancelActionHandler() {
139
140            private static final long serialVersionUID = 1L;
141
142            @Override
143            protected void cancel() {
144
145                CmsPublishScheduledDialog.this.cancel();
146            }
147
148            @Override
149            protected void ok() {
150
151                submit();
152            }
153        });
154
155        m_dateField.setRangeStart(LocalDateTime.now());
156
157    }
158
159    /**
160     * Sets the admin cms object. This must happen during start up.
161     * @param cms the admin cms.
162     */
163    public static void setAdminCms(CmsObject cms) {
164
165        m_adminCms = cms;
166    }
167
168    /**
169     * Cancels the dialog action.<p>
170     */
171    void cancel() {
172
173        m_context.finish(Collections.<CmsUUID> emptyList());
174    }
175
176    /**
177     * Submits the dialog action.<p>
178     */
179    void submit() {
180
181        //        if (!m_dateField.isValid()) {
182        //            return;
183        //        }
184        long current = System.currentTimeMillis();
185        Date dateValue = m_dateField.getDate();
186        long publishTime = dateValue.getTime();
187        if (current > publishTime) {
188            m_context.error(
189                new CmsException(Messages.get().container(Messages.ERR_PUBLISH_SCHEDULED_DATE_IN_PAST_1, dateValue)));
190        } else {
191            try {
192                // get the user cms object
193                CmsObject cms = OpenCms.initCmsObject(m_context.getCms());
194
195                // set the current user site to the admin cms object
196                CmsObject adminCms = getAdminCms();
197                adminCms.getRequestContext().setSiteRoot(cms.getRequestContext().getSiteRoot());
198                CmsProject tmpProject = createTempProject(adminCms, m_context.getResources(), dateValue);
199                // set project as current project
200                adminCms.getRequestContext().setCurrentProject(tmpProject);
201                cms.getRequestContext().setCurrentProject(tmpProject);
202
203                Set<CmsUUID> changeIds = new HashSet<CmsUUID>();
204                Set<CmsResource> needToUnlockLater = new HashSet<>();
205                for (CmsResource resource : m_context.getResources()) {
206                    addToTempProject(adminCms, cms, resource, tmpProject, needToUnlockLater);
207                    if (resource.isFolder() && m_includeSubResources.getValue().booleanValue()) {
208                        List<CmsResource> subResources = cms.readResources(
209                            resource,
210                            CmsResourceFilter.ONLY_VISIBLE.addExcludeState(CmsResourceState.STATE_UNCHANGED),
211                            true);
212                        for (CmsResource sub : subResources) {
213                            // check publish permissions on sub resource
214                            if (cms.hasPermissions(
215                                sub,
216                                CmsPermissionSet.ACCESS_DIRECT_PUBLISH,
217                                false,
218                                CmsResourceFilter.ALL)) {
219                                addToTempProject(adminCms, cms, sub, tmpProject, needToUnlockLater);
220                            }
221                        }
222                    }
223
224                    changeIds.add(resource.getStructureId());
225                }
226                // create a new scheduled job
227                CmsScheduledJobInfo job = new CmsScheduledJobInfo();
228                // the job name
229                String jobName = tmpProject.getName();
230                jobName = jobName.replace("&#47;", "/");
231                // set the job parameters
232                job.setJobName(jobName);
233                job.setClassName("org.opencms.scheduler.jobs.CmsPublishScheduledJob");
234                // create the cron expression
235                Calendar calendar = Calendar.getInstance();
236                calendar.setTime(dateValue);
237                String cronExpr = ""
238                    + calendar.get(Calendar.SECOND)
239                    + " "
240                    + calendar.get(Calendar.MINUTE)
241                    + " "
242                    + calendar.get(Calendar.HOUR_OF_DAY)
243                    + " "
244                    + calendar.get(Calendar.DAY_OF_MONTH)
245                    + " "
246                    + (calendar.get(Calendar.MONTH) + 1)
247                    + " "
248                    + "?"
249                    + " "
250                    + calendar.get(Calendar.YEAR);
251                // set the cron expression
252                job.setCronExpression(cronExpr);
253                // set the job active
254                job.setActive(true);
255                // create the context info
256                CmsContextInfo contextInfo = new CmsContextInfo();
257                contextInfo.setProjectName(tmpProject.getName());
258                contextInfo.setUserName(adminCms.getRequestContext().getCurrentUser().getName());
259                // create the job schedule parameter
260                SortedMap<String, String> params = new TreeMap<String, String>();
261                // the user to send mail to
262                String userName = m_context.getCms().getRequestContext().getCurrentUser().getName();
263                params.put(CmsPublishScheduledJob.PARAM_USER, userName);
264                // the job name
265                params.put(CmsPublishScheduledJob.PARAM_JOBNAME, jobName);
266                // the link check
267                params.put(CmsPublishScheduledJob.PARAM_LINKCHECK, "true");
268                String unlockIdList = needToUnlockLater.stream().map(res -> res.getStructureId().toString()).collect(
269                    Collectors.joining(","));
270                params.put(CmsPublishScheduledJob.PARAM_UNLOCK_LIST, unlockIdList);
271                // add the job schedule parameter
272                job.setParameters(params);
273                // add the context info to the scheduled job
274                job.setContextInfo(contextInfo);
275                // add the job to the scheduled job list
276                OpenCms.getScheduleManager().scheduleJob(adminCms, job);
277                // update the XML configuration
278                OpenCms.writeConfiguration(CmsSchedulerConfiguration.class);
279                m_context.finish(changeIds);
280            } catch (CmsException ex) {
281                LOG.error("Error performing publish scheduled dialog operation.", ex);
282                m_context.error(ex);
283            }
284        }
285
286    }
287
288    /**
289     * Adds the given resource to the temporary project.<p>
290     *
291     * @param adminCms the admin cms context
292     * @param userCms the user cms context
293     * @param resource the resource
294     * @param tmpProject the temporary project
295     * @param needToUnlockLater a set to which this method adds resources that should be explicitly unlocked later
296     *
297     * @throws CmsException in case something goes wrong
298     */
299    private void addToTempProject(
300        CmsObject adminCms,
301        CmsObject userCms,
302        CmsResource resource,
303        CmsProject tmpProject,
304        Set<CmsResource> needToUnlockLater)
305    throws CmsException {
306
307        // copy the resource to the project
308        adminCms.copyResourceToProject(resource);
309
310        // lock the resource in the current project
311        CmsLock lock = userCms.getLock(resource);
312        // prove is current lock from current but not in current project
313        if ((lock != null)
314            && lock.isOwnedBy(userCms.getRequestContext().getCurrentUser())
315            && !lock.isOwnedInProjectBy(
316                userCms.getRequestContext().getCurrentUser(),
317                userCms.getRequestContext().getCurrentProject())) {
318            // file is locked by current user but not in current project
319            // change the lock from this file
320            userCms.changeLock(resource);
321        }
322        // lock resource from current user in current project
323        userCms.lockResource(resource);
324        if (resource.getState().isUnchanged()) {
325            needToUnlockLater.add(resource);
326        }
327        // get current lock
328        lock = userCms.getLock(resource);
329    }
330
331    /**
332     * Creates the publish project's name for a given root path and publish date.<p>
333     *
334     * @param rootPath the publish resource's root path
335     * @param date the publish date
336     *
337     * @return the publish project name
338     */
339    private String computeProjectName(String rootPath, Date date) {
340
341        // create the temporary project, which is deleted after publishing
342        // the publish scheduled date in project name
343        String dateTime = CmsDateUtil.getDateTime(date, DateFormat.SHORT, getLocale());
344        // the resource name to publish scheduled
345        String projectName = CmsVaadinUtils.getMessageText(
346            Messages.GUI_PUBLISH_SCHEDULED_PROJECT_NAME_2,
347            rootPath,
348            dateTime);
349        // the HTML encoding for slashes is necessary because of the slashes in english date time format
350        // in project names slahes are not allowed, because these are separators for organizaional units
351        projectName = projectName.replace("/", "&#47;");
352        while (projectName.length() > 190) {
353            rootPath = "..." + rootPath.substring(5, rootPath.length());
354            projectName = computeProjectName(rootPath, date);
355        }
356        return projectName;
357    }
358
359    /**
360     * Creates the temporary project.<p>
361     *
362     * @param adminCms the admin cms context
363     * @param resources the resources
364     * @param date the publish date
365     *
366     * @return the project
367     *
368     * @throws CmsException in case writing the project fails
369     */
370    private CmsProject createTempProject(CmsObject adminCms, List<CmsResource> resources, Date date)
371    throws CmsException {
372
373        CmsProject tmpProject;
374
375        String rootPath = resources.get(0).getRootPath();
376        if (resources.size() > 1) {
377            rootPath = CmsResource.getParentFolder(rootPath);
378        }
379        String projectName = computeProjectName(rootPath, date);
380
381        try {
382            // create the project
383            tmpProject = adminCms.createProject(
384                projectName,
385                "",
386                CmsRole.WORKPLACE_USER.getGroupName(),
387                CmsRole.PROJECT_MANAGER.getGroupName(),
388                CmsProject.PROJECT_TYPE_TEMPORARY);
389        } catch (CmsException e) {
390            String resName = CmsResource.getName(rootPath);
391            if (resName.length() > 64) {
392                resName = resName.substring(0, 64) + "...";
393            }
394            // use UUID to make sure the project name is still unique
395            projectName = computeProjectName(resName, date) + " [" + new CmsUUID() + "]";
396            // create the project
397            tmpProject = adminCms.createProject(
398                projectName,
399                "",
400                CmsRole.WORKPLACE_USER.getGroupName(),
401                CmsRole.PROJECT_MANAGER.getGroupName(),
402                CmsProject.PROJECT_TYPE_TEMPORARY);
403        }
404        // make the project invisible for all users
405        tmpProject.setHidden(true);
406        // write the project to the database
407        adminCms.writeProject(tmpProject);
408        return tmpProject;
409    }
410
411    /**
412     * Returns a copy of the admin cms object.
413     * @return a copy of the admin cms object.
414     * @throws CmsException thrown if copying the cms object fails.
415     */
416    private CmsObject getAdminCms() throws CmsException {
417
418        return OpenCms.initCmsObject(m_adminCms);
419    }
420
421    /**
422     * Checks whether the resources list contains any folders.<p>
423     *
424     * @return <code>true</code> if the resources list contains any folders
425     */
426    private boolean hasFolders() {
427
428        for (CmsResource resource : m_context.getResources()) {
429            if (resource.isFolder()) {
430                return true;
431            }
432        }
433        return false;
434    }
435
436    /**
437     * Initializes the form fields.<p>
438     *
439     * @return the form component
440     */
441    private FormLayout initForm() {
442
443        FormLayout form = new FormLayout();
444        form.setWidth("100%");
445        m_dateField = new CmsDateField();
446        m_dateField.setCaption(
447            CmsVaadinUtils.getMessageText(org.opencms.workplace.commons.Messages.GUI_LABEL_DATE_PUBLISH_SCHEDULED_0));
448        form.addComponent(m_dateField);
449        m_includeSubResources = new CheckBox(
450            CmsVaadinUtils.getMessageText(org.opencms.workplace.commons.Messages.GUI_PUBLISH_MULTI_SUBRESOURCES_0));
451        if (hasFolders()) {
452            form.addComponent(m_includeSubResources);
453        }
454
455        return form;
456    }
457}