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.ade.publish;
029
030import org.opencms.ade.publish.CmsPublishRelationFinder.ResourceMap;
031import org.opencms.ade.publish.shared.CmsProjectBean;
032import org.opencms.ade.publish.shared.CmsPublishData;
033import org.opencms.ade.publish.shared.CmsPublishGroup;
034import org.opencms.ade.publish.shared.CmsPublishGroupList;
035import org.opencms.ade.publish.shared.CmsPublishListToken;
036import org.opencms.ade.publish.shared.CmsPublishOptions;
037import org.opencms.ade.publish.shared.CmsPublishResource;
038import org.opencms.ade.publish.shared.CmsWorkflow;
039import org.opencms.ade.publish.shared.CmsWorkflowAction;
040import org.opencms.ade.publish.shared.CmsWorkflowActionParams;
041import org.opencms.ade.publish.shared.CmsWorkflowResponse;
042import org.opencms.ade.publish.shared.rpc.I_CmsPublishService;
043import org.opencms.db.CmsUserSettings;
044import org.opencms.file.CmsObject;
045import org.opencms.file.CmsResource;
046import org.opencms.file.CmsResourceFilter;
047import org.opencms.file.CmsUser;
048import org.opencms.flex.CmsFlexController;
049import org.opencms.gwt.CmsGwtService;
050import org.opencms.gwt.CmsRpcException;
051import org.opencms.main.CmsException;
052import org.opencms.main.CmsLog;
053import org.opencms.main.OpenCms;
054import org.opencms.ui.CmsVaadinUtils;
055import org.opencms.util.CmsStringUtil;
056import org.opencms.util.CmsUUID;
057import org.opencms.workflow.CmsWorkflowResources;
058import org.opencms.workflow.I_CmsPublishResourceFormatter;
059import org.opencms.workflow.I_CmsWorkflowManager;
060import org.opencms.workplace.CmsDialog;
061import org.opencms.workplace.CmsWorkplace;
062
063import java.util.ArrayList;
064import java.util.HashMap;
065import java.util.HashSet;
066import java.util.List;
067import java.util.Locale;
068import java.util.Map;
069import java.util.Set;
070
071import javax.servlet.http.HttpServletRequest;
072
073import org.apache.commons.logging.Log;
074
075import com.google.common.collect.Lists;
076import com.google.common.collect.Maps;
077import com.google.common.collect.Sets;
078
079/**
080 * The implementation of the publish service. <p>
081 *
082 * @since 8.0.0
083 *
084 */
085public class CmsPublishService extends CmsGwtService implements I_CmsPublishService {
086
087    /** Name for the request parameter to control display of the confirmation dialog. */
088    public static final String PARAM_CONFIRM = "confirm";
089
090    /** The publish project id parameter name. */
091    public static final String PARAM_PUBLISH_PROJECT_ID = "publishProjectId";
092
093    /** The workflow id parameter name. */
094    public static final String PARAM_WORKFLOW_ID = "workflowId";
095
096    /** The logger for this class. */
097    private static final Log LOG = CmsLog.getLog(CmsPublishService.class);
098
099    /** The version id for serialization. */
100    private static final long serialVersionUID = 3852074177607037076L;
101
102    /** Session attribute name constant. */
103    private static final String SESSION_ATTR_ADE_PUB_OPTS_CACHE = "__OCMS_ADE_PUB_OPTS_CACHE__";
104
105    /**
106     * Fetches the publish data.<p>
107     *
108     * @param request the servlet request
109     *
110     * @return the publish data
111     *
112     * @throws CmsRpcException if something goes wrong
113     */
114    public static CmsPublishData prefetch(HttpServletRequest request) throws CmsRpcException {
115
116        CmsPublishService srv = new CmsPublishService();
117        srv.setCms(CmsFlexController.getCmsObject(request));
118        srv.setRequest(request);
119        CmsPublishData result = null;
120        HashMap<String, String> params = Maps.newHashMap();
121        params.put("prefetch", "true");
122        try {
123            result = srv.getInitData(params);
124        } finally {
125            srv.clearThreadStorage();
126        }
127        return result;
128    }
129
130    /**
131     * Wraps the project name in a message string.<p>
132     *
133     * @param cms the CMS context
134     * @param name the project name
135     *
136     * @return the message for the given project name
137     */
138    public static String wrapProjectName(CmsObject cms, String name) {
139
140        return Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
141            Messages.GUI_NORMAL_PROJECT_1,
142            name);
143    }
144
145    /**
146     * @see org.opencms.ade.publish.shared.rpc.I_CmsPublishService#executeAction(org.opencms.ade.publish.shared.CmsWorkflowAction, org.opencms.ade.publish.shared.CmsWorkflowActionParams)
147     */
148    public CmsWorkflowResponse executeAction(CmsWorkflowAction action, CmsWorkflowActionParams params)
149    throws CmsRpcException {
150
151        CmsWorkflowResponse response = null;
152        try {
153            CmsObject cms = getCmsObject();
154            if (params.getToken() == null) {
155                CmsPublishOptions options = getCachedOptions();
156                CmsPublish pub = new CmsPublish(cms, options);
157                List<CmsResource> publishResources = idsToResources(cms, params.getPublishIds());
158                Set<CmsUUID> toRemove = new HashSet<CmsUUID>(params.getRemoveIds());
159                pub.removeResourcesFromPublishList(toRemove);
160                response = OpenCms.getWorkflowManager().executeAction(cms, action, options, publishResources);
161            } else {
162                response = OpenCms.getWorkflowManager().executeAction(cms, action, params.getToken());
163            }
164        } catch (Throwable e) {
165            error(e);
166        }
167        return response;
168    }
169
170    /**
171     * @see org.opencms.ade.publish.shared.rpc.I_CmsPublishService#getInitData(java.util.HashMap)
172     */
173    public CmsPublishData getInitData(HashMap<String, String> params) throws CmsRpcException {
174
175        CmsPublishData result = null;
176        CmsObject cms = getCmsObject();
177        String closeLink = getRequest().getParameter(CmsDialog.PARAM_CLOSELINK);
178        if ((closeLink == null) && "true".equals(params.get("prefetch"))) {
179            closeLink = CmsVaadinUtils.getWorkplaceLink();
180        }
181        String confirmStr = getRequest().getParameter(PARAM_CONFIRM);
182        boolean confirm = Boolean.parseBoolean(confirmStr);
183        String workflowId = getRequest().getParameter(PARAM_WORKFLOW_ID);
184        String projectParam = getRequest().getParameter(PARAM_PUBLISH_PROJECT_ID);
185        String filesParam = getRequest().getParameter(CmsWorkplace.PARAM_RESOURCELIST);
186        if (CmsStringUtil.isEmptyOrWhitespaceOnly(filesParam)) {
187            filesParam = getRequest().getParameter(CmsDialog.PARAM_RESOURCE);
188        }
189        List<String> pathList = Lists.newArrayList();
190        if (!CmsStringUtil.isEmptyOrWhitespaceOnly(filesParam)) {
191            pathList = CmsStringUtil.splitAsList(filesParam, "|");
192        }
193        try {
194            result = getPublishData(cms, params, workflowId, projectParam, pathList, closeLink, confirm);
195        } catch (Throwable e) {
196            error(e);
197        }
198        return result;
199    }
200
201    /**
202     * Gets the publish data for the given parameters.<p>
203     *
204     * @param cms the CMS context
205     * @param params other publish parameters
206     * @param workflowId the workflow id
207     * @param projectParam the project
208     * @param pathList the list of direct publish resource site paths
209     * @param closeLink the close link
210     * @param confirm true if confirmation dialog should be displayed after closing the dialog
211     *
212     * @return the publish data
213     *
214     * @throws Exception if something goes wrong
215     */
216    public CmsPublishData getPublishData(
217        CmsObject cms,
218        HashMap<String, String> params,
219        String workflowId,
220        String projectParam,
221        List<String> pathList,
222        String closeLink,
223        boolean confirm)
224    throws Exception {
225
226        CmsPublishData result;
227        boolean canOverrideWorkflow = true;
228        Map<String, CmsWorkflow> workflows = OpenCms.getWorkflowManager().getWorkflows(cms);
229        if (workflows.isEmpty()) {
230            throw new Exception("No workflow available for the current user");
231        }
232        if (CmsStringUtil.isEmptyOrWhitespaceOnly(workflowId) || !workflows.containsKey(workflowId)) {
233            workflowId = getLastWorkflowForUser();
234            if (CmsStringUtil.isEmptyOrWhitespaceOnly(workflowId) || !workflows.containsKey(workflowId)) {
235                workflowId = workflows.values().iterator().next().getId();
236            }
237        } else {
238            canOverrideWorkflow = false;
239        }
240        setLastWorkflowForUser(workflowId);
241
242        // need to put this into params here so that the virtual project for direct publishing is included in the result of getManageableProjects()
243        if (!pathList.isEmpty()) {
244            params.put(CmsPublishOptions.PARAM_FILES, CmsStringUtil.listAsString(pathList, "|"));
245        }
246        boolean useCurrentPageAsDefault = params.containsKey(CmsPublishOptions.PARAM_START_WITH_CURRENT_PAGE);
247        CmsPublishOptions options = getCachedOptions();
248        List<CmsProjectBean> projects = OpenCms.getWorkflowManager().getManageableProjects(cms, params);
249        Set<CmsUUID> availableProjectIds = Sets.newHashSet();
250        for (CmsProjectBean projectBean : projects) {
251            availableProjectIds.add(projectBean.getId());
252        }
253        CmsUUID defaultProjectId = CmsUUID.getNullUUID();
254        if (useCurrentPageAsDefault && availableProjectIds.contains(CmsCurrentPageProject.ID)) {
255            defaultProjectId = CmsCurrentPageProject.ID;
256        }
257
258        boolean foundProject = false;
259        CmsUUID selectedProject = null;
260        if (!pathList.isEmpty()) {
261            params.put(CmsPublishOptions.PARAM_ENABLE_INCLUDE_CONTENTS, Boolean.TRUE.toString());
262            params.put(CmsPublishOptions.PARAM_INCLUDE_CONTENTS, Boolean.TRUE.toString());
263            selectedProject = CmsDirectPublishProject.ID;
264            foundProject = true;
265        } else {
266            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(projectParam) && CmsUUID.isValidUUID(projectParam)) {
267                selectedProject = new CmsUUID(projectParam);
268                // check if the selected project is a manageable project
269                for (CmsProjectBean project : projects) {
270                    if (selectedProject.equals(project.getId())) {
271                        foundProject = true;
272                        if (project.isWorkflowProject()) {
273                            canOverrideWorkflow = false;
274                            workflowId = OpenCms.getWorkflowManager().getWorkflowForWorkflowProject(selectedProject);
275                        }
276                        break;
277                    }
278                }
279            }
280            if (!foundProject) {
281                selectedProject = options.getProjectId();
282                if (selectedProject == null) {
283                    selectedProject = defaultProjectId;
284                    foundProject = true;
285                } else {
286                    // check if the selected project is a manageable project
287                    for (CmsProjectBean project : projects) {
288                        if (selectedProject.equals(project.getId())) {
289                            foundProject = true;
290                            if (project.isWorkflowProject()) {
291                                canOverrideWorkflow = false;
292                                workflowId = OpenCms.getWorkflowManager().getWorkflowForWorkflowProject(
293                                    selectedProject);
294                            }
295                            break;
296                        }
297                    }
298                }
299            }
300        }
301        if (foundProject) {
302            options.setProjectId(selectedProject);
303        } else {
304            options.setProjectId(CmsUUID.getNullUUID());
305        }
306
307        options.setParameters(params);
308        result = new CmsPublishData(
309            options,
310            projects,
311            getResourceGroups(workflows.get(workflowId), options, canOverrideWorkflow),
312            workflows,
313            workflowId);
314        result.setCloseLink(closeLink);
315        result.setShowConfirmation(confirm);
316        return result;
317    }
318
319    /**
320     * @see org.opencms.ade.publish.shared.rpc.I_CmsPublishService#getResourceGroups(org.opencms.ade.publish.shared.CmsWorkflow, org.opencms.ade.publish.shared.CmsPublishOptions, boolean)
321     */
322    public CmsPublishGroupList getResourceGroups(
323        CmsWorkflow workflow,
324        CmsPublishOptions options,
325        boolean projectChanged)
326    throws CmsRpcException {
327
328        List<CmsPublishGroup> results = null;
329        CmsObject cms = getCmsObject();
330        String overrideWorkflowId = null;
331        try {
332            Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
333            I_CmsWorkflowManager workflowManager = OpenCms.getWorkflowManager();
334            I_CmsPublishResourceFormatter formatter = workflowManager.createFormatter(cms, workflow, options);
335            CmsWorkflowResources workflowResources = null;
336            workflowResources = workflowManager.getWorkflowResources(cms, workflow, options, projectChanged, false);
337            if (workflowResources.getOverrideWorkflow() != null) {
338                overrideWorkflowId = workflowResources.getOverrideWorkflow().getId();
339                formatter = workflowManager.createFormatter(cms, workflowResources.getOverrideWorkflow(), options);
340            }
341            if (workflowResources.isTooMany()) {
342                // too many resources, send a publish list token to the client which can be used later to restore the resource list
343                CmsPublishListToken token = workflowManager.getPublishListToken(cms, workflow, options);
344                CmsPublishGroupList result = new CmsPublishGroupList(token);
345                result.setTooManyResourcesMessage(
346                    Messages.get().getBundle(locale).key(
347                        Messages.GUI_TOO_MANY_RESOURCES_2,
348                        "" + workflowResources.getLowerBoundForSize(),
349                        "" + OpenCms.getWorkflowManager().getResourceLimit()));
350                return result;
351            }
352            I_CmsVirtualProject virtualProject = workflowManager.getRealOrVirtualProject(options.getProjectId());
353            I_CmsPublishRelatedResourceProvider relProvider = virtualProject.getRelatedResourceProvider(
354                getCmsObject(),
355                options);
356            ResourceMap resourcesAndRelated = getResourcesAndRelated(
357                workflowResources.getWorkflowResources(),
358                options.isIncludeRelated(),
359                options.isIncludeSiblings(),
360                (options.getProjectId() == null) || options.getProjectId().isNullUUID(),
361                relProvider);
362            formatter.initialize(options, resourcesAndRelated);
363            List<CmsPublishResource> publishResources = formatter.getPublishResources();
364            A_CmsPublishGroupHelper<CmsPublishResource, CmsPublishGroup> groupHelper;
365            boolean autoSelectable = true;
366            if ((options.getProjectId() == null) || options.getProjectId().isNullUUID()) {
367                groupHelper = new CmsDefaultPublishGroupHelper(locale);
368            } else {
369                String title = "";
370                CmsProjectBean projectBean = virtualProject.getProjectBean(cms, options.getParameters());
371                title = projectBean.getDefaultGroupName();
372                if (title == null) {
373                    title = "";
374                }
375                autoSelectable = virtualProject.isAutoSelectable();
376                groupHelper = new CmsSinglePublishGroupHelper(locale, title);
377            }
378            results = groupHelper.getGroups(publishResources);
379            for (CmsPublishGroup group : results) {
380                group.setAutoSelectable(autoSelectable);
381            }
382            setCachedOptions(options);
383            setLastWorkflowForUser(workflow.getId());
384        } catch (Throwable e) {
385            error(e);
386        }
387        CmsPublishGroupList wrapper = new CmsPublishGroupList();
388        wrapper.setOverrideWorkflowId(overrideWorkflowId);
389        wrapper.setGroups(results);
390        return wrapper;
391    }
392
393    /**
394     * Retrieves the publish options.<p>
395     *
396     * @return the publish options last used
397     *
398     * @throws CmsRpcException if something goes wrong
399     */
400    public CmsPublishOptions getResourceOptions() throws CmsRpcException {
401
402        CmsPublishOptions result = null;
403        try {
404            result = getCachedOptions();
405        } catch (Throwable e) {
406            error(e);
407        }
408        return result;
409    }
410
411    /**
412     * Adds siblings to a set of publish resources.<p>
413     *
414     * @param publishResources the set to which siblings should be added
415     */
416    protected void addSiblings(Set<CmsResource> publishResources) {
417
418        List<CmsResource> toAdd = Lists.newArrayList();
419        for (CmsResource resource : publishResources) {
420            if (!resource.getState().isUnchanged()) {
421                try {
422                    List<CmsResource> changedSiblings = getCmsObject().readSiblings(
423                        resource,
424                        CmsResourceFilter.ALL_MODIFIED);
425                    toAdd.addAll(changedSiblings);
426                } catch (CmsException e) {
427                    LOG.error(e.getLocalizedMessage(), e);
428                }
429            }
430        }
431        publishResources.addAll(toAdd);
432    }
433
434    /**
435     * Returns the cached publish options, creating it if it doesn't already exist.<p>
436     *
437     * @return the cached publish options
438     */
439    private CmsPublishOptions getCachedOptions() {
440
441        CmsPublishOptions cache = (CmsPublishOptions)getRequest().getSession().getAttribute(
442            SESSION_ATTR_ADE_PUB_OPTS_CACHE);
443        if (cache == null) {
444            CmsUserSettings settings = new CmsUserSettings(getCmsObject());
445            cache = new CmsPublishOptions();
446            cache.setIncludeSiblings(settings.getDialogPublishSiblings());
447            getRequest().getSession().setAttribute(SESSION_ATTR_ADE_PUB_OPTS_CACHE, cache);
448        }
449        return cache;
450
451    }
452
453    /**
454     * Returns the id of the last used workflow for the current user.<p>
455     *
456     * @return the workflow id
457     */
458    private String getLastWorkflowForUser() {
459
460        CmsUser user = getCmsObject().getRequestContext().getCurrentUser();
461        return (String)user.getAdditionalInfo(PARAM_WORKFLOW_ID);
462    }
463
464    /**
465     * Gets the resource map containing the publish resources together with their related resources.<p>
466     *
467     * @param resources the base list of publish resources
468     * @param includeRelated flag to control whether related resources should be included
469     * @param includeSiblings flag to control whether siblings should be included
470     * @param keepOriginalUnchangedResources flag which determines whether unchanged resources in the original resource list should be kept or removed
471     * @param relProvider the provider for additional related resources
472     *
473     * @return the resources together with their related resources
474     */
475    private ResourceMap getResourcesAndRelated(
476        List<CmsResource> resources,
477        boolean includeRelated,
478        boolean includeSiblings,
479        boolean keepOriginalUnchangedResources,
480        I_CmsPublishRelatedResourceProvider relProvider) {
481
482        Set<CmsResource> publishResources = new HashSet<CmsResource>();
483        publishResources.addAll(resources);
484        if (includeSiblings) {
485            addSiblings(publishResources);
486        }
487        ResourceMap result;
488        if (includeRelated) {
489            CmsPublishRelationFinder relationFinder = new CmsPublishRelationFinder(
490                getCmsObject(),
491                publishResources,
492                keepOriginalUnchangedResources,
493                relProvider);
494
495            result = relationFinder.getPublishRelatedResources();
496        } else {
497            result = new ResourceMap();
498            for (CmsResource resource : publishResources) {
499                if (keepOriginalUnchangedResources || !resource.getState().isUnchanged()) {
500                    result.put(resource, new HashSet<CmsResource>());
501                }
502            }
503        }
504        return result;
505    }
506
507    /**
508     * Converts a list of IDs to resources.<p>
509     *
510     * @param cms the CmObject used for reading the resources
511     * @param ids the list of IDs
512     *
513     * @return a list of resources
514     */
515    private List<CmsResource> idsToResources(CmsObject cms, List<CmsUUID> ids) {
516
517        List<CmsResource> result = new ArrayList<CmsResource>();
518        for (CmsUUID id : ids) {
519            try {
520                CmsResource resource = cms.readResource(id, CmsResourceFilter.ALL);
521                result.add(resource);
522            } catch (CmsException e) {
523                // should never happen
524                logError(e);
525            }
526        }
527        return result;
528    }
529
530    /**
531     * Saves the given options to the session.<p>
532     *
533     * @param options the options to save
534     */
535    private void setCachedOptions(CmsPublishOptions options) {
536
537        getRequest().getSession().setAttribute(SESSION_ATTR_ADE_PUB_OPTS_CACHE, options);
538    }
539
540    /**
541     * Writes the id of the last used workflow to the current user.<p>
542     *
543     * @param workflowId the workflow id
544     *
545     * @throws CmsException if something goes wrong writing the user object
546     */
547    private void setLastWorkflowForUser(String workflowId) throws CmsException {
548
549        CmsUser user = getCmsObject().getRequestContext().getCurrentUser();
550        user.setAdditionalInfo(PARAM_WORKFLOW_ID, workflowId);
551        getCmsObject().writeUser(user);
552    }
553}