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 GmbH & Co. KG, 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.commons;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProject;
032import org.opencms.file.CmsResource;
033import org.opencms.file.CmsResource.CmsResourceUndoMode;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.jsp.CmsJspActionElement;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.security.CmsPermissionSet;
039import org.opencms.util.CmsUUID;
040import org.opencms.workplace.CmsMultiDialog;
041import org.opencms.workplace.CmsWorkplaceSettings;
042
043import java.util.ArrayList;
044import java.util.Iterator;
045import java.util.List;
046
047import javax.servlet.http.HttpServletRequest;
048import javax.servlet.http.HttpServletResponse;
049import javax.servlet.jsp.JspException;
050import javax.servlet.jsp.PageContext;
051
052import org.apache.commons.logging.Log;
053
054/**
055 * Provides methods for the undo changes on a resource dialog.<p>
056 *
057 * The following files use this class:
058 * <ul>
059 * <li>/commons/undochanges.jsp
060 * </ul>
061 * <p>
062 *
063 * @since 6.0.0
064 */
065public class CmsUndoChanges extends CmsMultiDialog {
066
067    /** Value for the action: undo changes. */
068    public static final int ACTION_UNDOCHANGES = 100;
069
070    /** Value for the action: check for siblings and warn in case they exist. */
071    public static final int ACTION_CHECKSIBLINGS = 101;
072
073    /** Action string constant for the check siblings dialog. */
074    public static final String DIALOG_CHECKSIBLINGS = "checksiblings";
075
076    /** The dialog type. */
077    public static final String DIALOG_TYPE = "undochanges";
078
079    /** Request parameter name for the recursive flag.<p> */
080    public static final String PARAM_RECURSIVE = "recursive";
081
082    /** Request parameter name for the move flag.<p> */
083    public static final String PARAM_MOVE = "move";
084
085    /** The log object for this class. */
086    private static final Log LOG = CmsLog.getLog(CmsUndoChanges.class);
087
088    /** The single current resource. */
089    private CmsResource m_currentResource;
090
091    /** The undo move operation flag parameter value. */
092    private String m_paramMove;
093
094    /** The undo recursively flag parameter value. */
095    private String m_paramRecursive;
096
097    /**
098     * Public constructor with JSP action element.<p>
099     *
100     * @param jsp an initialized JSP action element
101     */
102    public CmsUndoChanges(CmsJspActionElement jsp) {
103
104        super(jsp);
105    }
106
107    /**
108     * Public constructor with JSP variables.<p>
109     *
110     * @param context the JSP page context
111     * @param req the JSP request
112     * @param res the JSP response
113     */
114    public CmsUndoChanges(PageContext context, HttpServletRequest req, HttpServletResponse res) {
115
116        this(new CmsJspActionElement(context, req, res));
117    }
118
119    /**
120     * Returns the original path of given resource, that is the online path for the resource.
121     * If it differs from the offline path, the resource has been moved.<p>
122     *
123     * @param cms the cms context
124     * @param resourceName a site relative resource name
125     *
126     * @return the online path, or <code>null</code> if resource has not been published
127     */
128    public static String resourceOriginalPath(CmsObject cms, String resourceName) {
129
130        CmsProject proj = cms.getRequestContext().getCurrentProject();
131        try {
132            CmsResource resource = cms.readResource(resourceName, CmsResourceFilter.ALL);
133            String result = cms.getSitePath(resource);
134            cms.getRequestContext().setCurrentProject(cms.readProject(CmsProject.ONLINE_PROJECT_ID));
135            result = cms.getSitePath(cms.readResource(resource.getStructureId()));
136            // remove '/' if needed
137            if (result.charAt(result.length() - 1) == '/') {
138                if (resourceName.charAt(resourceName.length() - 1) != '/') {
139                    result = result.substring(0, result.length() - 1);
140                }
141            }
142            return result;
143        } catch (CmsException e) {
144            return null;
145        } finally {
146            cms.getRequestContext().setCurrentProject(proj);
147        }
148    }
149
150    /**
151     * Performs the check for siblings action and returns false in case of existence.<p>
152     *
153     * @return true if siblings are found.
154     */
155    public boolean actionCheckSiblings() {
156
157        List<String> resourceList = getResourceList();
158        String resourcePath;
159        Iterator<String> itResourcePaths = resourceList.iterator();
160        boolean foundSibling = false;
161        while (itResourcePaths.hasNext()) {
162            resourcePath = itResourcePaths.next();
163            try {
164                foundSibling = recursiveCheckSiblings(resourcePath);
165                if (foundSibling) {
166                    break; // shortcut
167                }
168
169            } catch (CmsException e) {
170                LOG.error(Messages.get().getBundle(getLocale()).key(
171                    Messages.ERR_UNDO_CHANGES_1,
172                    new String[] {resourcePath}));
173            }
174
175        }
176        return foundSibling;
177    }
178
179    /**
180     * Performs the undo changes action, will be called by the JSP page.<p>
181     *
182     * @throws JspException if problems including sub-elements occur
183     */
184    public void actionUndoChanges() throws JspException {
185
186        // save initialized instance of this class in request attribute for included sub-elements
187        getJsp().getRequest().setAttribute(SESSION_WORKPLACE_CLASS, this);
188        try {
189            boolean isFolder = false;
190            String source = getResourceList().get(0);
191            if (!isMultiOperation()) {
192                CmsResource resource = getCms().readResource(source, CmsResourceFilter.ALL);
193                isFolder = resource.isFolder();
194            }
195            // get the folders to refresh
196            List<String> folderList = new ArrayList<String>(1 + getResourceList().size());
197            folderList.add(CmsResource.getParentFolder(source));
198            Iterator<String> it = getResourceList().iterator();
199            while (it.hasNext()) {
200                String res = it.next();
201                String target = resourceOriginalPath(getCms(), res);
202                if ((target != null) && !target.equals(res)) {
203                    CmsResource resource = getCms().readResource(res, CmsResourceFilter.ALL);
204                    if (resource.isFolder()) {
205                        folderList.add(CmsResource.getParentFolder(target));
206                    }
207                }
208            }
209            if (performDialogOperation()) {
210                // if no exception is caused and "true" is returned move operation was successful
211                if (isMultiOperation() || isFolder) {
212                    // set request attribute to reload the explorer tree view
213                    getJsp().getRequest().setAttribute(REQUEST_ATTRIBUTE_RELOADTREE, folderList);
214                }
215                actionCloseDialog();
216            } else {
217                // "false" returned, display "please wait" screen
218                getJsp().include(FILE_DIALOG_SCREEN_WAIT);
219            }
220        } catch (Throwable e) {
221            // error during deletion, show error dialog
222            includeErrorpage(this, e);
223        }
224    }
225
226    /**
227     * Returns the HTML for the undo changes options and detailed output for single resource operations.<p>
228     *
229     * @return the HTML for the undo changes options
230     */
231    public String buildDialogOptions() {
232
233        StringBuffer result = new StringBuffer(256);
234
235        boolean isMoved = isOperationOnMovedResource();
236        if (!isMultiOperation()) {
237            result.append(dialogSpacer());
238            result.append(key(
239                Messages.GUI_UNDO_LASTMODIFIED_INFO_3,
240                new Object[] {getFileName(), getLastModifiedDate(), getLastModifiedUser()}));
241            if (isMoved) {
242                result.append(dialogSpacer());
243                result.append(key(
244                    Messages.GUI_UNDO_MOVE_OPERATION_INFO_2,
245                    new Object[] {getFileName(), resourceOriginalPath(getCms(), getParamResource())}));
246            }
247        }
248        result.append(dialogSpacer());
249        result.append(key(Messages.GUI_UNDO_CONFIRMATION_0));
250        if (isMoved || isOperationOnFolder()) {
251            // show undo move option if both options are available
252            result.append(dialogSpacer());
253            result.append("<input type=\"checkbox\" name=\"");
254            result.append(PARAM_MOVE);
255            result.append("\" value=\"true\" checked='checked'>&nbsp;");
256            if (isMultiOperation()) {
257                result.append(key(Messages.GUI_UNDO_CHANGES_MOVE_MULTI_SUBRESOURCES_0));
258            } else {
259                result.append(key(Messages.GUI_UNDO_CHANGES_MOVE_SUBRESOURCES_0));
260            }
261        } else {
262            if (isMoved) {
263                result.append(dialogSpacer());
264                result.append("<input type=\"hidden\" name=\"");
265                result.append(PARAM_MOVE);
266                result.append("\" value=\"true\">&nbsp;");
267            }
268        }
269        if (isOperationOnFolder()) {
270            // show recursive option if folder(s) is/are selected
271            result.append(dialogSpacer());
272            result.append(dialogBlockStart(key(Messages.GUI_UNDO_CHANGES_RECURSIVE_TITLE_0)));
273            result.append("<input type=\"checkbox\" name=\"");
274            result.append(PARAM_RECURSIVE);
275            result.append("\" value=\"true\">&nbsp;");
276            if (isMultiOperation()) {
277                result.append(key(Messages.GUI_UNDO_CHANGES_RECURSIVE_MULTI_SUBRESOURCES_0));
278            } else {
279                result.append(key(Messages.GUI_UNDO_CHANGES_RECURSIVE_SUBRESOURCES_0));
280            }
281            result.append(dialogBlockEnd());
282        }
283        return result.toString();
284    }
285
286    /**
287     * Returns the undo move operation flag parameter value.<p>
288     *
289     * @return the undo move operation flag parameter value
290     */
291    public String getParamMove() {
292
293        return m_paramMove;
294    }
295
296    /**
297     * Returns the value of the recursive parameter,
298     * or <code>null</code> if this parameter was not provided.<p>
299     *
300     * The recursive parameter on folders decides if all subresources
301     * of the folder should be unchanged, too.<p>
302     *
303     * @return the value of the recursive parameter
304     */
305    public String getParamRecursive() {
306
307        return m_paramRecursive;
308    }
309
310    /**
311     * Sets the undo move operation flag parameter value.<p>
312     *
313     * @param paramMove the undo move operation flag to set
314     */
315    public void setParamMove(String paramMove) {
316
317        m_paramMove = paramMove;
318    }
319
320    /**
321     * Sets the value of the recursive parameter.<p>
322     *
323     * @param value the value to set
324     */
325    public void setParamRecursive(String value) {
326
327        m_paramRecursive = value;
328    }
329
330    /**
331     * Returns the current CmsResource.<p>
332     *
333     * @return the CmsResource
334     */
335    protected CmsResource getCurrentResource() {
336
337        return m_currentResource;
338    }
339
340    /**
341     * Returns the file name without path information of the current resource.<p>
342     *
343     * @return the name of the current resource
344     */
345    protected String getFileName() {
346
347        return CmsResource.getName(getParamResource());
348    }
349
350    /**
351     * Returns the last modified date of the current resource as localized String.<p>
352     *
353     * @return the date of last modification
354     */
355    protected String getLastModifiedDate() {
356
357        long dateLong = getCurrentResource().getDateLastModified();
358        return getMessages().getDateTime(dateLong);
359    }
360
361    /**
362     * Returns the user who made the last changes to the current resource.<p>
363     *
364     * @return the user who changed the resource
365     */
366    protected String getLastModifiedUser() {
367
368        CmsUUID userId = getCurrentResource().getUserLastModified();
369        try {
370            return getCms().readUser(userId).getName();
371        } catch (CmsException e) {
372            return "";
373        }
374    }
375
376    /**
377     * @see org.opencms.workplace.CmsWorkplace#initWorkplaceRequestValues(org.opencms.workplace.CmsWorkplaceSettings, javax.servlet.http.HttpServletRequest)
378     */
379    @Override
380    protected void initWorkplaceRequestValues(CmsWorkplaceSettings settings, HttpServletRequest request) {
381
382        // fill the parameter values in the get/set methods
383        fillParamValues(request);
384
385        // check the required permissions to undo changes of the resource
386        if (!checkResourcePermissions(CmsPermissionSet.ACCESS_WRITE, false)) {
387            // no write permissions for the resource, set cancel action to close dialog
388            setParamAction(DIALOG_CANCEL);
389        }
390
391        // set the dialog type
392        setParamDialogtype(DIALOG_TYPE);
393        // set the action for the JSP switch
394        if (DIALOG_TYPE.equals(getParamAction())) {
395            setAction(ACTION_UNDOCHANGES);
396        } else if (DIALOG_CHECKSIBLINGS.equals(getParamAction())) {
397            setAction(ACTION_CHECKSIBLINGS);
398        } else if (DIALOG_WAIT.equals(getParamAction())) {
399            setAction(ACTION_WAIT);
400        } else if (DIALOG_CANCEL.equals(getParamAction())) {
401            setAction(ACTION_CANCEL);
402        } else if (DIALOG_LOCKS_CONFIRMED.equals(getParamAction())) {
403            setAction(ACTION_LOCKS_CONFIRMED);
404        } else {
405            setAction(ACTION_DEFAULT);
406            // build title for undo changes dialog
407            setDialogTitle(Messages.GUI_UNDO_CHANGES_1, Messages.GUI_UNDO_CHANGES_MULTI_2);
408        }
409
410        if (!isMultiOperation()) {
411            // collect resource to display information on single operation dialog
412            try {
413                setCurrentResource(getCms().readResource(getParamResource(), CmsResourceFilter.ALL));
414            } catch (CmsException e) {
415                // should usually never happen
416                if (LOG.isInfoEnabled()) {
417                    LOG.info(e.getLocalizedMessage());
418                }
419            }
420        }
421
422    }
423
424    /**
425     * Checks if the resource operation is an operation on at least one moved resource.<p>
426     *
427     * @return true if the operation an operation on at least one moved resource, otherwise false
428     */
429    protected boolean isOperationOnMovedResource() {
430
431        Iterator<String> i = getResourceList().iterator();
432        while (i.hasNext()) {
433            String resName = i.next();
434            String target = resourceOriginalPath(getCms(), resName);
435            if ((target != null) && !target.equals(resName)) {
436                // found a moved resource
437                return true;
438            }
439        }
440        return false;
441    }
442
443    /**
444     * Performs the undo changes operation on a resource.<p>
445     *
446     * @return true, if the changes on a resource were undone, otherwise false
447     * @throws CmsException if undo changes is not successful
448     */
449    @Override
450    protected boolean performDialogOperation() throws CmsException {
451
452        // check if the current resource is a folder for single operation
453        boolean isFolder = isOperationOnFolder();
454        // on folder undo changes or multi operation display "please wait" screen, not for simple file undo changes
455        if ((isMultiOperation() || isFolder) && !DIALOG_WAIT.equals(getParamAction())) {
456            // return false, this will trigger the "please wait" screen
457            return false;
458        }
459
460        // get the flag if the undo changes is recursive from request parameter
461        boolean undoRecursive = Boolean.valueOf(getParamRecursive()).booleanValue();
462        boolean undoMove = Boolean.valueOf(getParamMove()).booleanValue();
463
464        CmsResourceUndoMode mode = CmsResource.UNDO_CONTENT;
465        if (undoRecursive) {
466            mode = CmsResource.UNDO_CONTENT_RECURSIVE;
467        }
468        if (undoMove) {
469            mode = mode.includeMove();
470        }
471
472        Iterator<String> i = getResourceList().iterator();
473        // iterate the resources to delete
474        while (i.hasNext()) {
475            String resName = i.next();
476            try {
477                // lock resource if autolock is enabled
478                checkLock(resName);
479                // undo changes on the resource
480                getCms().undoChanges(resName, mode);
481            } catch (CmsException e) {
482                if (isMultiOperation()) {
483                    // collect exceptions to create a detailed output
484                    addMultiOperationException(e);
485                } else {
486                    // for single operation, throw the exception immediately
487                    throw e;
488                }
489            }
490        }
491        // check if exceptions occurred
492        checkMultiOperationException(Messages.get(), Messages.ERR_UNDO_CHANGES_MULTI_0);
493
494        return true;
495    }
496
497    /**
498     * Sets the current CmsResource.<p>
499     *
500     * @param res the CmsResource
501     */
502    protected void setCurrentResource(CmsResource res) {
503
504        m_currentResource = res;
505    }
506
507    /**
508     * Depth first recursion for searching if a file has siblings with early termination.<p>
509     *
510     * This avoids to read a whole resource tree (which will be hard for e.g.  /sites/).<p>
511     *
512     * @param path the path to check the siblings in
513     *
514     * @return true if a resource which is a sibling was found.
515     *
516     * @throws CmsException if something goes wrong
517     */
518    private boolean recursiveCheckSiblings(String path) throws CmsException {
519
520        boolean result = false;
521        CmsObject cms = getCms();
522        /*
523         * No trailing slash for the multi dialogs... therefore a read resource first necessary:
524         */
525        CmsResource resource = cms.readResource(path);
526        path = cms.getSitePath(resource);
527        if (CmsResource.isFolder(path)) {
528            // only check subresources in case of recursive option checked.
529            boolean undoRecursive = Boolean.valueOf(getParamRecursive()).booleanValue();
530            if (undoRecursive) {
531                // don't read the whole tree, this is  expensive - most likely we will find a sibling faster step by step:
532                List<CmsResource> subResources = cms.readResources(path, CmsResourceFilter.ALL, false);
533                Iterator<CmsResource> itSubResources = subResources.iterator();
534                while (itSubResources.hasNext()) {
535                    resource = itSubResources.next();
536                    result = recursiveCheckSiblings(cms.getSitePath(resource));
537                    if (result) {
538                        break; // shortcut
539                    }
540                }
541            }
542
543        } else {
544            List<CmsResource> siblings = cms.readSiblings(path, CmsResourceFilter.ALL);
545            result = siblings.size() > 1;
546        }
547        return result;
548    }
549
550}