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}