001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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.workflow;
029
030import org.opencms.ade.publish.CmsCurrentPageProject;
031import org.opencms.ade.publish.CmsDirectPublishProject;
032import org.opencms.ade.publish.CmsMyChangesProject;
033import org.opencms.ade.publish.CmsPublish;
034import org.opencms.ade.publish.CmsRealProjectVirtualWrapper;
035import org.opencms.ade.publish.CmsTooManyPublishResourcesException;
036import org.opencms.ade.publish.I_CmsVirtualProject;
037import org.opencms.ade.publish.shared.CmsProjectBean;
038import org.opencms.ade.publish.shared.CmsPublishListToken;
039import org.opencms.ade.publish.shared.CmsPublishOptions;
040import org.opencms.ade.publish.shared.CmsPublishResource;
041import org.opencms.ade.publish.shared.CmsWorkflow;
042import org.opencms.ade.publish.shared.CmsWorkflowAction;
043import org.opencms.ade.publish.shared.CmsWorkflowResponse;
044import org.opencms.file.CmsObject;
045import org.opencms.file.CmsProject;
046import org.opencms.file.CmsResource;
047import org.opencms.file.CmsResourceFilter;
048import org.opencms.i18n.CmsMessages;
049import org.opencms.main.CmsException;
050import org.opencms.main.CmsLog;
051import org.opencms.main.OpenCms;
052import org.opencms.security.CmsOrganizationalUnit;
053import org.opencms.security.CmsRole;
054import org.opencms.util.CmsUUID;
055
056import java.util.ArrayList;
057import java.util.Collections;
058import java.util.Iterator;
059import java.util.LinkedHashMap;
060import java.util.List;
061import java.util.Map;
062import java.util.concurrent.Callable;
063import java.util.concurrent.ExecutionException;
064import java.util.concurrent.FutureTask;
065import java.util.concurrent.TimeUnit;
066import java.util.concurrent.TimeoutException;
067
068import org.apache.commons.logging.Log;
069
070import com.google.common.collect.Maps;
071
072/**
073 * The default implementation of the workflow manager interface, which offers only publish functionality.<p>
074 */
075public class CmsDefaultWorkflowManager extends A_CmsWorkflowManager {
076
077    /** The forced publish workflow action. */
078    public static final String ACTION_FORCE_PUBLISH = "forcepublish";
079
080    /** The publish workflow action. */
081    public static final String ACTION_PUBLISH = "publish";
082
083    /** Default value for the maximum number of resources in the initial publish list. */
084    public static int DEFAULT_RESOURCE_LIMIT = 1000;
085
086    /** The parameter name for the resource limit. */
087    public static final String PARAM_RESOURCE_LIMIT = "resourceLimit";
088
089    /** The name for the publish action. */
090    public static final String WORKFLOW_PUBLISH = "WORKFLOW_PUBLISH";
091
092    /** The log instance for this class. */
093    private static final Log LOG = CmsLog.getLog(CmsDefaultWorkflowManager.class);
094
095    /**
096     * If a request context attribute of this name is set, some internal methods used
097     * to collect lists of resources for publishing will 'give up' and throw an exception
098     * when the number of resources exceeds the resource limit of the workflow manager.
099     */
100    public static final String ATTR_CHECK_PUBLISH_RESOURCE_LIMIT = "CHECK_PUBLISH_RESOURCE_LIMIT";
101
102    /** The map of registered virtual  projects. */
103    protected Map<CmsUUID, I_CmsVirtualProject> m_virtualProjects = Maps.newHashMap();
104
105    /** The number of resources in the initial publish list above which the resources are not being displayed to the user. */
106    private int m_resourceLimit = DEFAULT_RESOURCE_LIMIT;
107
108    /**
109     * Constructor.<p>
110     */
111    public CmsDefaultWorkflowManager() {
112
113        m_virtualProjects.put(CmsCurrentPageProject.ID, new CmsCurrentPageProject());
114        m_virtualProjects.put(CmsMyChangesProject.ID, new CmsMyChangesProject());
115        m_virtualProjects.put(CmsDirectPublishProject.ID, new CmsDirectPublishProject());
116    }
117
118    /**
119     * Creates a project bean from a real project.<p>
120     *
121     * @param cms the CMS context
122     * @param project the project
123     *
124     * @return the bean containing the project information
125     */
126    public static CmsProjectBean createProjectBeanFromProject(CmsObject cms, CmsProject project) {
127
128        CmsProjectBean manProj = new CmsProjectBean(
129            project.getUuid(),
130            project.getType().getMode(),
131            org.opencms.ade.publish.Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key(
132                org.opencms.ade.publish.Messages.GUI_NORMAL_PROJECT_1,
133                getOuAwareName(cms, project.getName())),
134            project.getDescription());
135        return manProj;
136    }
137
138    /**
139     * Returns the simple name if the ou is the same as the current user's ou.<p>
140     *
141     * @param cms the CMS context
142     * @param name the fully qualified name to check
143     *
144     * @return the simple name if the ou is the same as the current user's ou
145     */
146    protected static String getOuAwareName(CmsObject cms, String name) {
147
148        String ou = CmsOrganizationalUnit.getParentFqn(name);
149        if (ou.equals(cms.getRequestContext().getCurrentUser().getOuFqn())) {
150            return CmsOrganizationalUnit.getSimpleName(name);
151        }
152        return CmsOrganizationalUnit.SEPARATOR + name;
153    }
154
155    /**
156     * @see org.opencms.workflow.I_CmsWorkflowManager#createFormatter(org.opencms.file.CmsObject, org.opencms.ade.publish.shared.CmsWorkflow, org.opencms.ade.publish.shared.CmsPublishOptions)
157     */
158    public I_CmsPublishResourceFormatter createFormatter(
159        CmsObject cms,
160        CmsWorkflow workflow,
161        CmsPublishOptions options) {
162
163        CmsDefaultPublishResourceFormatter formatter = new CmsDefaultPublishResourceFormatter(cms);
164        return formatter;
165    }
166
167    /**
168     * @see org.opencms.workflow.I_CmsWorkflowManager#executeAction(org.opencms.file.CmsObject, org.opencms.ade.publish.shared.CmsWorkflowAction, org.opencms.ade.publish.shared.CmsPublishListToken)
169     */
170    public CmsWorkflowResponse executeAction(CmsObject cms, CmsWorkflowAction action, CmsPublishListToken token)
171    throws CmsException {
172
173        if (action.getAction().equals(CmsWorkflowAction.ACTION_CANCEL)) {
174            // Don't need to get the resource list for canceling
175            return new CmsWorkflowResponse(true, action.getAction(), null, null, null);
176        }
177        List<CmsResource> resources = getWorkflowResources(
178            cms,
179            token.getWorkflow(),
180            token.getOptions(),
181            false,
182            true).getWorkflowResources();
183        // We only automatically clean up the invalid resources in the case where the list of publish resources was too long to display (i.e. where we use a publish list token),
184        // in the other case it's already handled by CmsPublishService#executeAction.
185        List<CmsResource> filteredResources = cleanUpInvalidResourcesFromUserPublishList(cms, resources);
186        return executeAction(cms, action, token.getOptions(), filteredResources);
187    }
188
189    /**
190     * @see org.opencms.workflow.I_CmsWorkflowManager#executeAction(org.opencms.file.CmsObject, org.opencms.ade.publish.shared.CmsWorkflowAction, org.opencms.ade.publish.shared.CmsPublishOptions, java.util.List)
191     */
192    @Override
193    public CmsWorkflowResponse executeAction(
194        CmsObject userCms,
195        CmsWorkflowAction action,
196        CmsPublishOptions options,
197        List<CmsResource> resources)
198    throws CmsException {
199
200        String actionKey = action.getAction();
201        if (CmsWorkflowAction.ACTION_CANCEL.equals(actionKey)) {
202            return new CmsWorkflowResponse(true, actionKey, null, null, null);
203        } else if (ACTION_PUBLISH.equals(actionKey)) {
204            return actionPublish(userCms, options, resources);
205        } else if (ACTION_FORCE_PUBLISH.equals(actionKey)) {
206            return actionForcePublish(userCms, options, resources);
207        }
208        throw new CmsInvalidActionException(actionKey);
209    }
210
211    /**
212     * Gets the localized label for a given CMS context and key.<p>
213     *
214     * @param cms the CMS context
215     * @param key the localization key
216     *
217     * @return the localized label
218     */
219    public String getLabel(CmsObject cms, String key) {
220
221        CmsMessages messages = Messages.get().getBundle(getLocale(cms));
222        return messages.key(key);
223    }
224
225    /**
226     * @see org.opencms.workflow.I_CmsWorkflowManager#getManageableProjects(org.opencms.file.CmsObject, java.util.Map)
227     */
228    public List<CmsProjectBean> getManageableProjects(CmsObject cms, Map<String, String> params) {
229
230        List<CmsProjectBean> manProjs = new ArrayList<CmsProjectBean>();
231
232        List<CmsProject> projects;
233        try {
234            projects = OpenCms.getOrgUnitManager().getAllManageableProjects(cms, "", true);
235        } catch (CmsException e) {
236            // should never happen
237            LOG.error(e.getLocalizedMessage(), e);
238            return manProjs;
239        }
240
241        for (CmsProject project : projects) {
242            CmsProjectBean manProj = createProjectBeanFromProject(cms, project);
243            manProjs.add(manProj);
244        }
245
246        for (I_CmsVirtualProject handler : m_virtualProjects.values()) {
247            CmsProjectBean projectBean = handler.getProjectBean(cms, params);
248            if (projectBean != null) {
249                manProjs.add(projectBean);
250            }
251        }
252
253        return manProjs;
254    }
255
256    /**
257     * @see org.opencms.workflow.I_CmsWorkflowManager#getPublishListToken(org.opencms.file.CmsObject, org.opencms.ade.publish.shared.CmsWorkflow, org.opencms.ade.publish.shared.CmsPublishOptions)
258     */
259    public CmsPublishListToken getPublishListToken(CmsObject cms, CmsWorkflow workflow, CmsPublishOptions options) {
260
261        return new CmsPublishListToken(workflow, options);
262    }
263
264    /**
265     * @see org.opencms.workflow.I_CmsWorkflowManager#getRealOrVirtualProject(org.opencms.util.CmsUUID)
266     */
267    public I_CmsVirtualProject getRealOrVirtualProject(CmsUUID projectId) {
268
269        I_CmsVirtualProject project = m_virtualProjects.get(projectId);
270        if (project == null) {
271            project = new CmsRealProjectVirtualWrapper(projectId);
272        }
273        return project;
274    }
275
276    /**
277     * @see org.opencms.workflow.I_CmsWorkflowManager#getResourceLimit()
278     */
279    public int getResourceLimit() {
280
281        return m_resourceLimit;
282    }
283
284    /**
285     * @see org.opencms.workflow.I_CmsWorkflowManager#getWorkflowForWorkflowProject(org.opencms.util.CmsUUID)
286     */
287    public String getWorkflowForWorkflowProject(CmsUUID projectId) {
288
289        return WORKFLOW_PUBLISH;
290    }
291
292    /**
293     * @see org.opencms.workflow.I_CmsWorkflowManager#getWorkflowResources(org.opencms.file.CmsObject, org.opencms.ade.publish.shared.CmsWorkflow, org.opencms.ade.publish.shared.CmsPublishOptions, boolean, boolean)
294     */
295    @Override
296    public CmsWorkflowResources getWorkflowResources(
297        CmsObject cms,
298        CmsWorkflow workflow,
299        CmsPublishOptions options,
300        boolean canOverride,
301        boolean ignoreLimit) {
302
303        try {
304            if (!ignoreLimit) {
305                cms.getRequestContext().setAttribute(
306                    CmsDefaultWorkflowManager.ATTR_CHECK_PUBLISH_RESOURCE_LIMIT,
307                    Boolean.TRUE);
308            }
309            List<CmsResource> rawResourceList = new ArrayList<CmsResource>();
310            I_CmsVirtualProject projectHandler = null;
311            projectHandler = getRealOrVirtualProject(options.getProjectId());
312            if (projectHandler != null) {
313                rawResourceList = projectHandler.getResources(cms, options.getParameters(), workflow.getId());
314                return new CmsWorkflowResources(rawResourceList, null, null);
315            }
316            return new CmsWorkflowResources(rawResourceList, null, null);
317        } catch (CmsTooManyPublishResourcesException e) {
318            return new CmsWorkflowResources(Collections.<CmsResource> emptyList(), null, Integer.valueOf(e.getCount()));
319        } catch (Exception e) {
320            LOG.error(e.getLocalizedMessage(), e);
321            return new CmsWorkflowResources(Collections.<CmsResource> emptyList(), null, null);
322        } finally {
323            cms.getRequestContext().removeAttribute(CmsDefaultWorkflowManager.ATTR_CHECK_PUBLISH_RESOURCE_LIMIT);
324        }
325    }
326
327    /**
328     * @see org.opencms.workflow.I_CmsWorkflowManager#getWorkflows(org.opencms.file.CmsObject)
329     */
330    public Map<String, CmsWorkflow> getWorkflows(CmsObject cms) {
331
332        Map<String, CmsWorkflow> result = new LinkedHashMap<String, CmsWorkflow>();
333        List<CmsWorkflowAction> actions = new ArrayList<CmsWorkflowAction>();
334        String publishLabel = getLabel(cms, Messages.GUI_WORKFLOW_ACTION_PUBLISH_0);
335        CmsWorkflowAction publishAction = new CmsWorkflowAction(ACTION_PUBLISH, publishLabel, true, true);
336        actions.add(publishAction);
337        String workflowLabel = getLabel(cms, Messages.GUI_WORKFLOW_PUBLISH_0);
338        CmsWorkflow publishWorkflow = new CmsWorkflow(WORKFLOW_PUBLISH, workflowLabel, actions);
339        result.put(WORKFLOW_PUBLISH, publishWorkflow);
340        return result;
341    }
342
343    /**
344     * @see org.opencms.workflow.A_CmsWorkflowManager#initialize(org.opencms.file.CmsObject)
345     */
346    @Override
347    public void initialize(CmsObject adminCms) {
348
349        super.initialize(adminCms);
350        String resourceLimitStr = getParameter(PARAM_RESOURCE_LIMIT, "invalid").trim();
351        try {
352            m_resourceLimit = Integer.parseInt(resourceLimitStr);
353        } catch (NumberFormatException e) {
354            // ignore, resource limit will remain at the default setting
355        }
356    }
357
358    /**
359     * The implementation of the "forcepublish" workflow action.<p>
360     *
361     * @param userCms the user CMS context
362     * @param resources the resources which the action should process
363     * @param options the publish options to use
364     * @return the workflow response
365     *
366     * @throws CmsException if something goes wrong
367     */
368    protected CmsWorkflowResponse actionForcePublish(
369        CmsObject userCms,
370        CmsPublishOptions options,
371        List<CmsResource> resources)
372    throws CmsException {
373
374        CmsPublish publish = new CmsPublish(userCms, options.getParameters());
375        publish.publishResources(resources);
376        CmsWorkflowResponse response = new CmsWorkflowResponse(
377            true,
378            "",
379            new ArrayList<CmsPublishResource>(),
380            new ArrayList<CmsWorkflowAction>(),
381            null);
382        return response;
383    }
384
385    /**
386     * The implementation of the "publish" workflow action.<p>
387     *
388     * @param userCms the user CMS context
389     * @param options the publish options
390     * @param resources the resources which the action should process
391     *
392     * @return the workflow response
393     * @throws CmsException if something goes wrong
394     */
395    protected CmsWorkflowResponse actionPublish(
396        CmsObject userCms,
397        CmsPublishOptions options,
398        final List<CmsResource> resources)
399    throws CmsException {
400
401        final CmsPublish publish = new CmsPublish(userCms, options);
402        // use FutureTask to get the broken links, because we can then use a different thread if it takes too long
403        final FutureTask<List<CmsPublishResource>> brokenResourcesGetter = new FutureTask<List<CmsPublishResource>>(
404            new Callable<List<CmsPublishResource>>() {
405
406                public List<CmsPublishResource> call() throws Exception {
407
408                    return publish.getBrokenResources(resources);
409                }
410            });
411
412        Thread brokenResourcesThread = new Thread(brokenResourcesGetter);
413        brokenResourcesThread.start();
414        try {
415            List<CmsPublishResource> brokenResources = brokenResourcesGetter.get(10, TimeUnit.SECONDS);
416            if (brokenResources.size() == 0) {
417                publish.publishResources(resources);
418                CmsWorkflowResponse response = new CmsWorkflowResponse(
419                    true,
420                    "",
421                    new ArrayList<CmsPublishResource>(),
422                    new ArrayList<CmsWorkflowAction>(),
423                    null);
424                return response;
425            } else {
426                String brokenResourcesLabel = getLabel(userCms, Messages.GUI_BROKEN_LINKS_0);
427                boolean canForcePublish = OpenCms.getWorkplaceManager().getDefaultUserSettings().isAllowBrokenRelations()
428                    || OpenCms.getRoleManager().hasRole(userCms, CmsRole.VFS_MANAGER);
429                List<CmsWorkflowAction> actions = new ArrayList<CmsWorkflowAction>();
430                if (canForcePublish) {
431                    String forceLabel = getLabel(userCms, Messages.GUI_WORKFLOW_ACTION_FORCE_PUBLISH_0);
432                    actions.add(new CmsWorkflowAction(ACTION_FORCE_PUBLISH, forceLabel, true, true));
433                }
434                CmsWorkflowResponse response = new CmsWorkflowResponse(
435                    false,
436                    brokenResourcesLabel,
437                    brokenResources,
438                    actions,
439                    null);
440                return response;
441            }
442        } catch (TimeoutException e) {
443            // Things are taking too long, do them in a different thread and just return "OK" to the client
444            Thread thread = new Thread() {
445
446                @SuppressWarnings("synthetic-access")
447                @Override
448                public void run() {
449
450                    LOG.info(
451                        "Checking broken relations is taking too long, using a different thread for checking and publishing now.");
452                    try {
453                        // Make sure the computation is finished by calling get() without a timeout parameter
454                        // We don't need the actual result of the get(), though; we just get the set of resource paths from the validator object
455                        brokenResourcesGetter.get();
456                        List<CmsResource> resourcesToPublish = new ArrayList<CmsResource>(resources);
457                        Iterator<CmsResource> resIter = resourcesToPublish.iterator();
458                        while (resIter.hasNext()) {
459                            CmsResource currentRes = resIter.next();
460                            if (publish.getRelationValidator().keySet().contains(currentRes.getRootPath())) {
461                                resIter.remove();
462                                LOG.info(
463                                    "Excluding resource from publish list because relations would be broken: "
464                                        + currentRes.getRootPath());
465                            }
466                        }
467                        publish.publishResources(resourcesToPublish);
468                    } catch (Exception ex) {
469                        LOG.error(ex.getLocalizedMessage(), ex);
470                    }
471                }
472            };
473            thread.start();
474            CmsWorkflowResponse response = new CmsWorkflowResponse(
475                true,
476                "",
477                new ArrayList<CmsPublishResource>(),
478                new ArrayList<CmsWorkflowAction>(),
479                null);
480            return response;
481        } catch (InterruptedException e) {
482            // shouldn't happen; log exception
483            LOG.error(e.getLocalizedMessage());
484            return null;
485        } catch (ExecutionException e) {
486            // shouldn't happen; log exception
487            LOG.error(e.getLocalizedMessage());
488            return null;
489        }
490    }
491
492    /**
493     * Removes invalid publish resources (those that are unchanged, or can't be found) from the user's publish list, and only returns those which are not invalid.
494     *
495     * @param cms the CMS context
496     * @param resources the resources to filter
497     * @return the valid resources to publish
498     */
499    protected List<CmsResource> cleanUpInvalidResourcesFromUserPublishList(CmsObject cms, List<CmsResource> resources) {
500
501        List<CmsResource> filteredResources = new ArrayList<>();
502
503        List<CmsUUID> removeIds = new ArrayList<>();
504        for (CmsResource resource : resources) {
505            try {
506                if (resource.getState().isUnchanged()
507                    || !cms.existsResource(resource.getStructureId(), CmsResourceFilter.ALL)) {
508                    removeIds.add(resource.getStructureId());
509                } else {
510                    filteredResources.add(resource);
511                }
512            } catch (Exception e) {
513                LOG.error(e.getLocalizedMessage(), e);
514            }
515        }
516        if (removeIds.size() > 0) {
517            try {
518                OpenCms.getPublishManager().removeResourceFromUsersPubList(cms, removeIds);
519            } catch (Exception e) {
520                LOG.error(e.getLocalizedMessage(), e);
521            }
522        }
523
524        return filteredResources;
525    }
526}