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