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