001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://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.db; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProject; 032import org.opencms.file.CmsRequestContext; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsResourceFilter; 035import org.opencms.file.CmsVfsResourceNotFoundException; 036import org.opencms.main.CmsContextInfo; 037import org.opencms.main.CmsEvent; 038import org.opencms.main.CmsException; 039import org.opencms.main.CmsLog; 040import org.opencms.main.I_CmsEventListener; 041import org.opencms.main.OpenCms; 042import org.opencms.report.CmsLogReport; 043import org.opencms.report.I_CmsReport; 044import org.opencms.util.CmsStringUtil; 045import org.opencms.util.CmsUUID; 046 047import java.util.ArrayList; 048import java.util.HashMap; 049import java.util.HashSet; 050import java.util.Locale; 051import java.util.Map; 052import java.util.Set; 053import java.util.concurrent.locks.ReentrantLock; 054 055import org.apache.commons.lang3.function.FailableSupplier; 056import org.apache.commons.logging.Log; 057 058/** 059 * A helper class used by CmsSecurityManager to keep track of modified resources for the 'online folders' feature and 'publish' them when we are done with them. 060 * 061 * <p>This class is meant to be used with the try-with-resources syntax from Java 7: Use the acquire() method in the try expression (e.g. 'try (CmsModificationContext modContext = CmsModificationContext.acquire(requestContext)) { ... }'. 062 * Then use the add() method on the modification context to add any modified resources. Once the close() method is automatically invoked by the try-with-resources statement, *and* we are in the outermost nesting level of such try-with-resources statements, 063 * the modified resources are published if they belong to a configured online folder. 064 */ 065public class CmsModificationContext implements AutoCloseable { 066 067 /** A lock used to prevent concurrent execution of the resource publication that happens when closing the context. */ 068 private static final ReentrantLock LOCK = new ReentrantLock(true); 069 070 /** Logger instance for this class. */ 071 private static final Log LOG = CmsLog.getLog(CmsModificationContext.class); 072 073 /** Static admin CmsObject. */ 074 private static CmsObject m_adminCms; 075 076 /** The configuration. */ 077 private static CmsOnlineFolderOptions m_options; 078 079 /** The security manager. */ 080 private static CmsSecurityManager m_securityManager; 081 082 /** The active instance for the current thread. */ 083 private static final ThreadLocal<CmsModificationContext> threadLocalInstance = new ThreadLocal<>(); 084 085 /** The added structure ids (not necessarily for resources in the online folder). */ 086 private Set<CmsUUID> m_ids = new HashSet<>(); 087 088 /** The current 'nesting level' of modification contexts. */ 089 private int m_nestingLevel = 0; 090 091 /** The current request context for which the modification context was acquired. */ 092 private CmsRequestContext m_requestContext; 093 094 /** The added resources in the online folder. */ 095 private Set<CmsResource> m_resources = new HashSet<>(); 096 097 /** 098 * Creates a new instance. 099 * 100 * @param context the request context 101 */ 102 private CmsModificationContext(CmsRequestContext context) { 103 104 m_requestContext = context; 105 106 } 107 108 /** 109 * Executes the given action and returns the result, wrapping the execution in a modification context. 110 * 111 * @param <T> the result type 112 * @param requestContext the current request context 113 * @param runnable the action to execute 114 * @return the result of the action 115 * 116 * @throws CmsException if something goes wrong 117 */ 118 public static <T> T doWithModificationContext( 119 CmsRequestContext requestContext, 120 FailableSupplier<T, CmsException> runnable) 121 throws CmsException { 122 123 try (CmsModificationContext modContext = acquire(requestContext)) { 124 return runnable.get(); 125 } 126 } 127 128 public static CmsOnlineFolderOptions getOnlineFolderOptions() { 129 130 return m_options; 131 } 132 133 /** 134 * Initializes this class. 135 * 136 * @param securityManager the security manager instance 137 * @param adminCms a CmsObject with admin privileges 138 * @param onlineFolderPath the online folder path (nay be null) 139 */ 140 public static void initialize( 141 CmsSecurityManager securityManager, 142 CmsObject adminCms, 143 CmsOnlineFolderOptions options) { 144 145 m_securityManager = securityManager; 146 m_adminCms = adminCms; 147 m_options = options; 148 } 149 150 /** 151 * Checks if the given path is below the configured online folder. 152 * 153 * <p>If no online folder is configured, this will return false. 154 * 155 * @param path the path to check 156 * @return true if the given path is below the configured online folder 157 */ 158 public static boolean isInOnlineFolder(String path) { 159 160 return m_options.getPaths().stream().anyMatch(onlineFolder -> CmsStringUtil.isPrefixPath(onlineFolder, path)); 161 } 162 163 /** 164 * Checks if an 'instant publish' operation is currently running. 165 * 166 * @return true if an 'instant publish' operation is running 167 */ 168 public static boolean isInstantPublishing() { 169 170 return LOCK.isHeldByCurrentThread(); 171 } 172 173 /** 174 * Acquires a modification context. 175 * 176 * <p>If a modification context was acquired in a higher stack frame, the existing context will be returned, but its level counter will be increased by 1, otherwise a new context 177 * will be created with a level counter of 1. 178 * 179 * @param context the request context for which to get a modification context 180 * @return the modification context 181 */ 182 protected static CmsModificationContext acquire(CmsRequestContext context) { 183 184 CmsModificationContext instance = threadLocalInstance.get(); 185 if (instance == null) { 186 instance = new CmsModificationContext(context); 187 threadLocalInstance.set(instance); 188 } 189 instance.m_nestingLevel += 1; 190 return instance; 191 } 192 193 /** 194 * Checks if the given resource is in the online only folder, and if so, adds it to the set of resources that should be instant-published when the modification context is closed. 195 * 196 * @param resource the resource to add 197 */ 198 public void add(CmsResource resource) { 199 200 if (resource == null) { 201 return; 202 } 203 if (!isInOnlineFolder(resource.getRootPath())) { 204 return; 205 } 206 m_resources.add(resource); 207 } 208 209 /** 210 * Alternative to add(CmsResource) for methods where the resource is not read. 211 * 212 * <p>Avoid using this if possible, in favor of add(CmsResource) 213 * 214 * @param structureId the structure id of the resource to add 215 */ 216 public void addId(CmsUUID structureId) { 217 218 m_ids.add(structureId); 219 } 220 221 /** 222 * Decrements the modification context's level counter by 1, and finally closes it if the counter reaches zero. 223 * <p>If the counter reaches zero, all resources added with the add() method will be published synchronously, without going through 224 * the publish queue. 225 * 226 * @see java.lang.AutoCloseable#close() 227 */ 228 @Override 229 public void close() throws CmsException { 230 231 if (m_nestingLevel <= 0) { 232 throw new IllegalStateException(CmsModificationContext.class.getSimpleName() + " closed too often!"); 233 } 234 m_nestingLevel -= 1; 235 if (m_nestingLevel == 0) { 236 threadLocalInstance.remove(); 237 Set<CmsResource> resources = m_resources; 238 if (resources.size() == 0) { 239 return; 240 } 241 if (LOG.isDebugEnabled()) { 242 LOG.debug("closing modification context with resources " + resources.toString()); 243 if (m_ids.size() > 0) { 244 LOG.debug("additional ids: " + m_ids); 245 } 246 } 247 LOCK.lock(); 248 try { 249 CmsProject project = m_requestContext.getCurrentProject(); 250 CmsObject adminCms = OpenCms.initCmsObject(m_adminCms); 251 252 // We use an admin CmsObject to read the resources without any further permission checks 253 254 adminCms.getRequestContext().setCurrentProject(project); 255 Set<CmsResource> resources2 = new HashSet<>(); 256 Set<CmsUUID> idsOfResources = new HashSet<>(); 257 for (CmsResource resource : resources) { 258 try { 259 idsOfResources.add(resource.getStructureId()); 260 resources2.add(adminCms.readResource(resource.getStructureId(), CmsResourceFilter.ALL)); 261 } catch (CmsVfsResourceNotFoundException e) { 262 LOG.debug( 263 "Could not find modified resource: " 264 + resource.getRootPath() 265 + " " 266 + resource.getStructureId()); 267 } 268 } 269 for (CmsUUID id : m_ids) { 270 if (idsOfResources.contains(id)) { 271 continue; 272 } 273 try { 274 CmsResource resource = adminCms.readResource(id, CmsResourceFilter.ALL); 275 if (isInOnlineFolder(resource.getRootPath())) { 276 resources2.add(resource); 277 } 278 } catch (CmsVfsResourceNotFoundException e) { 279 LOG.debug("Could not find modified resource for id: " + id); 280 } 281 282 } 283 CmsPublishList pubList = new CmsPublishList(true, new ArrayList<>(resources2), false); 284 pubList.setUserPublishList(true); 285 m_securityManager.fillPublishList(m_requestContext, pubList); 286 if (pubList.size() > 0) { 287 CmsDbContext dbc1 = m_securityManager.m_dbContextFactory.getDbContext(m_requestContext); 288 I_CmsReport report = new CmsLogReport(Locale.ENGLISH, CmsModificationContext.class); 289 m_securityManager.publishJob( 290 OpenCms.initCmsObject(m_adminCms, new CmsContextInfo(m_requestContext)), 291 dbc1, 292 pubList, 293 report); 294 if (report.hasError() || report.hasWarning()) { 295 for (Object o : report.getErrors()) { 296 if (o instanceof Throwable) { 297 Throwable t = (Throwable)o; 298 LOG.error("Report error: " + t.getMessage(), t); 299 } 300 } 301 for (Object o : report.getWarnings()) { 302 if (o instanceof Throwable) { 303 Throwable t = (Throwable)o; 304 LOG.warn("Report warning: " + t.getMessage(), t); 305 } 306 } 307 } 308 CmsDbContext dbc2 = m_securityManager.m_dbContextFactory.getDbContext(m_requestContext); 309 try { 310 // fire an event that a project has been published 311 Map<String, Object> eventData = new HashMap<String, Object>(); 312 eventData.put(I_CmsEventListener.KEY_REPORT, report); 313 eventData.put(I_CmsEventListener.KEY_PUBLISHID, pubList.getPublishHistoryId().toString()); 314 eventData.put(I_CmsEventListener.KEY_PROJECTID, dbc2.currentProject().getUuid()); 315 eventData.put(I_CmsEventListener.KEY_INSTANT_PUBLISH, Boolean.TRUE); 316 eventData.put(I_CmsEventListener.KEY_DBCONTEXT, dbc2); 317 CmsEvent afterPublishEvent = new CmsEvent(I_CmsEventListener.EVENT_PUBLISH_PROJECT, eventData); 318 OpenCms.fireCmsEvent(afterPublishEvent); 319 } catch (Throwable t) { 320 LOG.error(t.getLocalizedMessage(), t); 321 } finally { 322 dbc2.clear(); 323 } 324 } 325 } finally { 326 LOCK.unlock(); 327 } 328 } 329 } 330 331}