/* * This library is part of OpenCms - * the Open Source Content Management System * * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * For further information about Alkacon Software GmbH, please see the * company website: http://www.alkacon.com * * For further information about OpenCms, please see the * project website: http://www.opencms.org * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.opencms.publish; import org.opencms.db.CmsDbContext; import org.opencms.db.CmsDriverManager; import org.opencms.db.CmsPublishList; import org.opencms.db.CmsUserSettings; import org.opencms.db.I_CmsDbContextFactory; import org.opencms.file.CmsDataAccessException; import org.opencms.file.CmsObject; import org.opencms.file.CmsRequestContext; import org.opencms.file.CmsResource; import org.opencms.file.CmsUser; import org.opencms.lock.CmsLockType; import org.opencms.main.CmsEvent; import org.opencms.main.CmsException; import org.opencms.main.CmsInitException; import org.opencms.main.CmsLog; import org.opencms.main.I_CmsEventListener; import org.opencms.main.OpenCms; import org.opencms.monitor.CmsMemoryMonitor; import org.opencms.report.I_CmsReport; import org.opencms.security.CmsAuthentificationException; import org.opencms.security.CmsRole; import org.opencms.util.CmsUUID; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; /** * This class is responsible for the publish process.<p> * * @since 6.5.5 */ public final class CmsPublishEngine { /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsPublishEngine.class); /** The id of the admin user. */ private CmsUUID m_adminUserId; /** The current running publish job. */ private CmsPublishThread m_currentPublishThread; /** The runtime info factory used during publishing. */ private final I_CmsDbContextFactory m_dbContextFactory; /** The driver manager instance. */ private CmsDriverManager m_driverManager; /** The engine state. */ private CmsPublishEngineState m_engineState; /** The publish listeners. */ private final CmsPublishListenerCollection m_listeners; /** The publish history list with already published jobs. */ private final CmsPublishHistory m_publishHistory; /** The queue with still waiting publish job. */ private final CmsPublishQueue m_publishQueue; /** The amount of time the system will wait for a running publish job during shutdown. */ private int m_publishQueueShutdowntime; /** Is set during shutdown. */ private boolean m_shuttingDown; /** * Default constructor.<p> * * @param dbContextFactory the initialized OpenCms runtime info factory * * @throws CmsInitException if the configured path to store the publish reports is not accessible */ public CmsPublishEngine(I_CmsDbContextFactory dbContextFactory) throws CmsInitException { if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) { // OpenCms is already initialized throw new CmsInitException(org.opencms.main.Messages.get().container( org.opencms.main.Messages.ERR_ALREADY_INITIALIZED_0)); } if (dbContextFactory == null) { throw new CmsInitException(org.opencms.main.Messages.get().container( org.opencms.main.Messages.ERR_CRITICAL_NO_DB_CONTEXT_0)); } // initialize the db context factory m_dbContextFactory = dbContextFactory; // initialize publish queue m_publishQueue = new CmsPublishQueue(this); // initialize publish history m_publishHistory = new CmsPublishHistory(this); // initialize event handling m_listeners = new CmsPublishListenerCollection(this); // set engine state to normal processing m_engineState = CmsPublishEngineState.ENGINE_STARTED; if (CmsLog.INIT.isInfoEnabled()) { CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_PUBLISH_ENGINE_READY_0)); } } /** * Abandons the given publish thread.<p> */ public void abandonThread() { if (!m_currentPublishThread.isAlive()) { // thread is dead if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_PUBLISH_ENGINE_DEAD_JOB_0)); } } else { // thread is not dead, and we suppose it hangs :( if (LOG.isWarnEnabled()) { LOG.warn(Messages.get().getBundle().key( Messages.LOG_THREADSTORE_PUBLISH_THREAD_INTERRUPT_2, m_currentPublishThread.getName(), m_currentPublishThread.getUUID())); } m_currentPublishThread.interrupt(); } // just throw it away m_currentPublishThread = null; // and try again checkCurrentPublishJobThread(); } /** * Controls the publish process.<p> */ public synchronized void checkCurrentPublishJobThread() { // give the finishing publish thread enough time to clean up try { Thread.sleep(200); } catch (InterruptedException e) { // ignore } // create a publish thread only if engine is started if (m_engineState != CmsPublishEngineState.ENGINE_STARTED) { return; } if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_PUBLISH_ENGINE_RUNNING_0)); } // check the driver manager if ((m_driverManager == null) || (m_dbContextFactory == null)) { LOG.error(Messages.get().getBundle().key(Messages.ERR_PUBLISH_ENGINE_NOT_INITIALIZED_0)); // without these there is nothing we can do return; } // there is no running publish job if (m_currentPublishThread == null) { // but something is waiting in the queue if (!m_publishQueue.isEmpty()) { // start the next waiting publish job CmsPublishJobInfoBean publishJob = m_publishQueue.next(); m_currentPublishThread = new CmsPublishThread(this, publishJob); m_currentPublishThread.start(); } else { // nothing to do if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_PUBLISH_ENGINE_NO_RUNNING_JOB_0)); } } return; } if (m_currentPublishThread.isAlive()) { // normal running // wait until it is finished if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_PUBLISH_ENGINE_WAITING_0)); } } else { // clean up the dead thread abandonThread(); } } /** * Enqueues a new publish job with the given information in publish queue.<p> * * All resources should already be locked.<p> * * If possible, the publish job starts immediately.<p> * * @param cms the cms context to publish for * @param publishList the resources to publish * @param report the report to write to * * @throws CmsException if something goes wrong while cloning the cms context */ public void enqueuePublishJob(CmsObject cms, CmsPublishList publishList, I_CmsReport report) throws CmsException { // check the driver manager if ((m_driverManager == null) || (m_dbContextFactory == null)) { // the resources are unlocked in the driver manager throw new CmsPublishException(Messages.get().container(Messages.ERR_PUBLISH_ENGINE_NOT_INITIALIZED_0)); } // prevent new jobs if the engine is disabled if (m_shuttingDown || (!isEnabled() && !OpenCms.getRoleManager().hasRole(cms, CmsRole.ROOT_ADMIN))) { // the resources are unlocked in the driver manager throw new CmsPublishException(Messages.get().container(Messages.ERR_PUBLISH_ENGINE_DISABLED_0)); } // create the publish job CmsPublishJobInfoBean publishJob = new CmsPublishJobInfoBean(cms, publishList, report); try { // enqueue it and m_publishQueue.add(publishJob); // notify all listeners m_listeners.fireEnqueued(new CmsPublishJobBase(publishJob)); } catch (Throwable t) { // we really really need to catch everything here, or else the queue status is broken if (m_publishQueue.contains(publishJob)) { m_publishQueue.remove(publishJob); } // throw the exception again throw new CmsException(Messages.get().container( Messages.ERR_PUBLISH_ENGINE_QUEUE_1, publishJob.getPublishHistoryId()), t); } // try to start the publish job immediately checkCurrentPublishJobThread(); } /** * Returns a publish job based on its publish history id.<p> * * The returned publish job may be an enqueued, running or finished publish job.<p> * * @param publishHistoryId the publish history id to search for * * @return the publish job with the given publish history id, or <code>null</code> */ public CmsPublishJobBase getJobByPublishHistoryId(CmsUUID publishHistoryId) { // try current running job if ((m_currentPublishThread != null) && m_currentPublishThread.getPublishJob().getPublishHistoryId().equals(publishHistoryId)) { return new CmsPublishJobRunning(m_currentPublishThread.getPublishJob()); } // try enqueued jobs Iterator<CmsPublishJobEnqueued> itEnqueuedJobs = getPublishQueue().asList().iterator(); while (itEnqueuedJobs.hasNext()) { CmsPublishJobEnqueued enqueuedJob = itEnqueuedJobs.next(); if (enqueuedJob.getPublishList().getPublishHistoryId().equals(publishHistoryId)) { return enqueuedJob; } } // try finished jobs Iterator<CmsPublishJobFinished> itFinishedJobs = getPublishHistory().asList().iterator(); while (itFinishedJobs.hasNext()) { CmsPublishJobFinished finishedJob = itFinishedJobs.next(); if (finishedJob.getPublishHistoryId().equals(publishHistoryId)) { return finishedJob; } } return null; } /** * Sets the driver manager instance.<p> * * @param driverManager the driver manager instance */ public void setDriverManager(CmsDriverManager driverManager) { m_driverManager = driverManager; CmsDbContext dbc = m_dbContextFactory.getDbContext(); try { m_adminUserId = m_driverManager.readUser(dbc, OpenCms.getDefaultUsers().getUserAdmin()).getId(); } catch (CmsException e) { dbc.rollback(); LOG.error(e.getLocalizedMessage(), e); } finally { dbc.clear(); } } /** * Shuts down all this static export manager.<p> * * NOTE: this method may or may NOT be called (i.e. kill -9 in the stop script), if a system is stopped.<p> * * This is required since there may still be a thread running when the system is being shut down.<p> */ public void shutDown() { if (CmsLog.INIT.isInfoEnabled()) { CmsLog.INIT.info(org.opencms.main.Messages.get().getBundle().key( org.opencms.main.Messages.INIT_SHUTDOWN_START_1, this.getClass().getName())); } // prevent new publish jobs are accepted m_shuttingDown = true; // if a job is currently running, // wait the specified amount of time, // then write an abort message to the report if (m_currentPublishThread != null) { // if a shutdown time is defined, wait if a publish process is running if (m_publishQueueShutdowntime > 0) { synchronized (this) { try { Thread.sleep(m_publishQueueShutdowntime * 1000); } catch (InterruptedException exc) { // ignore } } } if (m_currentPublishThread != null) { CmsPublishJobInfoBean publishJob = m_currentPublishThread.getPublishJob(); try { abortPublishJob(m_adminUserId, new CmsPublishJobEnqueued(publishJob), false); } catch (CmsException e) { if (LOG.isErrorEnabled()) { LOG.error(e.getLocalizedMessage(), e); } } } } // write the log CmsDbContext dbc = getDbContext(null); try { m_driverManager.updateLog(dbc); } catch (CmsDataAccessException e) { if (LOG.isErrorEnabled()) { LOG.error(e.getLocalizedMessage(), e); } } finally { dbc.clear(); } if (CmsLog.INIT.isInfoEnabled()) { CmsLog.INIT.info(org.opencms.staticexport.Messages.get().getBundle().key( org.opencms.staticexport.Messages.INIT_SHUTDOWN_1, this.getClass().getName())); } } /** * Aborts the given publish job.<p> * * @param userId the id of user that wants to abort the given publish job * @param publishJob the publish job to abort * @param removeJob indicates if the job will be removed or added to history * * @throws CmsException if there is some problem during unlocking the resources * @throws CmsPublishException if the publish job can not be aborted */ protected void abortPublishJob(CmsUUID userId, CmsPublishJobEnqueued publishJob, boolean removeJob) throws CmsException, CmsPublishException { // abort event should be raised before the job is removed implicitly m_listeners.fireAbort(userId, publishJob); if ((m_currentPublishThread == null) || !publishJob.m_publishJob.equals(m_currentPublishThread.getPublishJob())) { // engine is currently publishing another job or is not publishing if (!m_publishQueue.abortPublishJob(publishJob.m_publishJob)) { // job not found throw new CmsPublishException(Messages.get().container( Messages.ERR_PUBLISH_ENGINE_MISSING_PUBLISH_JOB_0)); } } else if (!m_shuttingDown) { // engine is currently publishing the job to abort m_currentPublishThread.abort(); } else if (m_shuttingDown && (m_currentPublishThread != null)) { // aborting the current job during shut down I_CmsReport report = m_currentPublishThread.getReport(); report.println(); report.println(); report.println( Messages.get().container(Messages.RPT_PUBLISH_JOB_ABORT_SHUTDOWN_0), I_CmsReport.FORMAT_ERROR); report.println(); } // unlock all resources if (publishJob.getPublishList() != null) { unlockPublishList(publishJob.m_publishJob); } // keep job if requested if (!removeJob) { // set finish info publishJob.m_publishJob.finish(); getPublishHistory().add(publishJob.m_publishJob); } else { getPublishQueue().remove(publishJob.m_publishJob); } } /** * Adds a publish listener to listen on publish events.<p> * * @param listener the publish listener to add */ protected void addPublishListener(I_CmsPublishEventListener listener) { m_listeners.add(listener); } /** * Disables the publish engine, i.e. publish jobs are not accepted.<p> */ protected void disableEngine() { m_engineState = CmsPublishEngineState.ENGINE_DISABLED; } /** * Enables the publish engine, i.e. publish jobs are accepted.<p> */ protected void enableEngine() { m_engineState = CmsPublishEngineState.ENGINE_STARTED; // start publish job if jobs waiting if ((m_currentPublishThread == null) && !m_publishQueue.isEmpty()) { checkCurrentPublishJobThread(); } } /** * Returns the current running publish job.<p> * * @return the current running publish job */ protected CmsPublishThread getCurrentPublishJob() { return m_currentPublishThread; } /** * Returns the a new db context object.<p> * * @param ctx optional request context, can be <code>null</code> * * @return the a new db context object */ protected CmsDbContext getDbContext(CmsRequestContext ctx) { return m_dbContextFactory.getDbContext(ctx); } /** * Returns the driver manager instance.<p> * * @return the driver manager instance */ protected CmsDriverManager getDriverManager() { return m_driverManager; } /** * Returns the publish history list with already publish job.<p> * * @return the publish history list with already publish job */ protected CmsPublishHistory getPublishHistory() { return m_publishHistory; } /** * Returns the queue with still waiting publish job.<p> * * @return the queue with still waiting publish job */ protected CmsPublishQueue getPublishQueue() { return m_publishQueue; } /** * Returns the content of the publish report assigned to the given publish job.<p> * * @param publishJob the published job * @return the content of the assigned publish report * * @throws CmsException if something goes wrong */ protected byte[] getReportContents(CmsPublishJobFinished publishJob) throws CmsException { byte[] result = null; CmsDbContext dbc = m_dbContextFactory.getDbContext(); try { result = m_driverManager.readPublishReportContents(dbc, publishJob.getPublishHistoryId()); } catch (CmsException e) { dbc.rollback(); LOG.error(e.getLocalizedMessage(), e); throw e; } finally { dbc.clear(); } return result; } /** * Returns the user identified by the given id.<p> * * @param userId the id of the user to retrieve * * @return the user identified by the given id */ protected CmsUser getUser(CmsUUID userId) { CmsDbContext dbc = m_dbContextFactory.getDbContext(); try { return m_driverManager.readUser(dbc, userId); } catch (CmsException e) { dbc.rollback(); LOG.error(e.getLocalizedMessage(), e); } finally { dbc.clear(); } return null; } /** * Initializes the publish engine.<p> * * @param adminCms the admin cms * @param publishQueuePersistance flag if the queue is persisted * @param publishQueueShutdowntime amount of time to wait for a publish job during shutdown * * @throws CmsException if something goes wrong */ protected void initialize(CmsObject adminCms, boolean publishQueuePersistance, int publishQueueShutdowntime) throws CmsException { // check the driver manager if ((m_driverManager == null) || (m_dbContextFactory == null)) { throw new CmsPublishException(Messages.get().container(Messages.ERR_PUBLISH_ENGINE_NOT_INITIALIZED_0)); } m_publishQueueShutdowntime = publishQueueShutdowntime; // initially the engine is stopped, must be restartet after full system initialization m_engineState = CmsPublishEngineState.ENGINE_STOPPED; // read the publish history from the repository m_publishHistory.initialize(); // read the queue from the repository m_publishQueue.initialize(adminCms, publishQueuePersistance); } /** * Returns the working state, that is if no publish job * is waiting to be processed and there is no current running * publish job.<p> * * @return the working state */ protected boolean isRunning() { return (((m_engineState == CmsPublishEngineState.ENGINE_STARTED) && !m_publishQueue.isEmpty()) || (m_currentPublishThread != null)); } /** * Sets publish locks of resources in a publish list.<p> * * @param publishJob the publish job * @throws CmsException if something goes wrong */ protected void lockPublishList(CmsPublishJobInfoBean publishJob) throws CmsException { CmsPublishList publishList = publishJob.getPublishList(); // lock them CmsDbContext dbc = getDbContext(publishJob.getCmsObject().getRequestContext()); try { Iterator<CmsResource> itResources = publishList.getAllResources().iterator(); while (itResources.hasNext()) { CmsResource resource = itResources.next(); m_driverManager.lockResource(dbc, resource, CmsLockType.PUBLISH); } } catch (CmsException e) { dbc.rollback(); LOG.error(e.getLocalizedMessage(), e); throw e; } finally { dbc.clear(); } } /** * Signalizes that the publish thread finishes.<p> * * @param publishJob the finished publish job */ protected void publishJobFinished(CmsPublishJobInfoBean publishJob) { // in order to avoid not removable publish locks, unlock all assigned resources again try { unlockPublishList(publishJob); } catch (Throwable t) { // log failure, most likely a database problem LOG.error(t.getLocalizedMessage(), t); } // trigger the old event mechanism CmsDbContext dbc = m_dbContextFactory.getDbContext(publishJob.getCmsObject().getRequestContext()); try { // fire an event that a project has been published Map<String, Object> eventData = new HashMap<String, Object>(); eventData.put(I_CmsEventListener.KEY_REPORT, publishJob.getPublishReport()); eventData.put( I_CmsEventListener.KEY_PUBLISHID, publishJob.getPublishList().getPublishHistoryId().toString()); eventData.put(I_CmsEventListener.KEY_PROJECTID, dbc.currentProject().getUuid()); eventData.put(I_CmsEventListener.KEY_DBCONTEXT, dbc); CmsEvent afterPublishEvent = new CmsEvent(I_CmsEventListener.EVENT_PUBLISH_PROJECT, eventData); OpenCms.fireCmsEvent(afterPublishEvent); } catch (Throwable t) { dbc.rollback(); LOG.error(t); // catch every thing including runtime exceptions publishJob.getPublishReport().println(t); } finally { try { dbc.clear(); } catch (Throwable t) { // ignore } dbc = null; } try { // fire the publish finish event m_listeners.fireFinish(new CmsPublishJobRunning(publishJob)); } catch (Throwable t) { // log failure, most likely a database problem LOG.error(t.getLocalizedMessage(), t); } try { // finish the job publishJob.finish(); } catch (Throwable t) { // log failure, most likely a database problem LOG.error(t.getLocalizedMessage(), t); } try { // put the publish job into the history list m_publishHistory.add(publishJob); } catch (Throwable t) { // log failure, most likely a database problem LOG.error(t.getLocalizedMessage(), t); } if (Thread.currentThread() == m_currentPublishThread) { // wipe the dead thread, only if this thread has not been abandoned m_currentPublishThread = null; } // clear the published resources cache OpenCms.getMemoryMonitor().flushCache(CmsMemoryMonitor.CacheType.PUBLISHED_RESOURCES); // try to start a new publish job checkCurrentPublishJobThread(); } /** * A publish job has been permanently removed from the history.<p> * * @param publishJob the removed publish job */ protected void publishJobRemoved(CmsPublishJobInfoBean publishJob) { // a publish job has been removed m_listeners.fireRemove(new CmsPublishJobFinished(publishJob)); } /** * Signalizes that the publish thread starts.<p> * * @param publishJob the started publish job */ protected void publishJobStarted(CmsPublishJobInfoBean publishJob) { // update the job m_publishQueue.update(publishJob); // fire the publish start event m_listeners.fireStart(new CmsPublishJobEnqueued(publishJob)); } /** * Removes the given publish listener.<p> * * @param listener the publish listener to remove */ protected void removePublishListener(I_CmsPublishEventListener listener) { m_listeners.remove(listener); } /** * Sends a message to the given user, if publish notification is enabled or an error is shown in the message.<p> * * @param toUserId the id of the user to send the message to * @param message the message to send * @param hasErrors flag to determine if the message to send shows an error */ protected void sendMessage(CmsUUID toUserId, String message, boolean hasErrors) { CmsDbContext dbc = m_dbContextFactory.getDbContext(); try { CmsUser toUser = m_driverManager.readUser(dbc, toUserId); CmsUserSettings settings = new CmsUserSettings(toUser); if (settings.getShowPublishNotification() || hasErrors) { // only show message if publish notification is enabled or the message shows an error OpenCms.getSessionManager().sendBroadcast(null, message, toUser); } } catch (CmsException e) { dbc.rollback(); LOG.error(e.getLocalizedMessage(), e); } finally { dbc.clear(); } } /** * Starts the publish engine, i.e. publish jobs are accepted and processed.<p> */ protected void startEngine() { if (m_engineState != CmsPublishEngineState.ENGINE_STARTED) { m_engineState = CmsPublishEngineState.ENGINE_STARTED; // start publish job if jobs waiting if ((m_currentPublishThread == null) && !m_publishQueue.isEmpty()) { checkCurrentPublishJobThread(); } } } /** * Stops the publish engine, i.e. publish jobs are still accepted but not published.<p> */ protected void stopEngine() { m_engineState = CmsPublishEngineState.ENGINE_STOPPED; } /** * Removes all publish locks of resources in a publish list of a publish job.<p> * * @param publishJob the publish job * @throws CmsException if something goes wrong */ protected void unlockPublishList(CmsPublishJobInfoBean publishJob) throws CmsException { CmsPublishList publishList = publishJob.getPublishList(); List<CmsResource> allResources = publishList.getAllResources(); // unlock them CmsDbContext dbc = getDbContext(publishJob.getCmsObject().getRequestContext()); try { Iterator<CmsResource> itResources = allResources.iterator(); while (itResources.hasNext()) { CmsResource resource = itResources.next(); m_driverManager.unlockResource(dbc, resource, true, true); } } catch (CmsException e) { dbc.rollback(); LOG.error(e.getLocalizedMessage(), e); throw e; } finally { dbc.clear(); } } /** * Returns <code>true</code> if the login manager allows login.<p> * * @return if enabled */ private boolean isEnabled() { try { if ((m_engineState == CmsPublishEngineState.ENGINE_STOPPED) || (m_engineState == CmsPublishEngineState.ENGINE_STARTED)) { OpenCms.getLoginManager().checkLoginAllowed(); return true; } else { return false; } } catch (CmsAuthentificationException e) { return false; } } }