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}