content: /*
   * This library is part of OpenCms -
   * the Open Source Content Management System
   *
   * Copyright (c) Alkacon Software GmbH & Co. KG (https://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 & Co. KG, please see the
   * company website: https://www.alkacon.com
   *
   * For further information about OpenCms, please see the
   * project website: https://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.flex;
  
  import org.opencms.file.CmsObject;
  import org.opencms.file.CmsPropertyDefinition;
  import org.opencms.file.CmsResource;
  import org.opencms.file.CmsVfsResourceNotFoundException;
  import org.opencms.loader.I_CmsResourceLoader;
  import org.opencms.main.CmsException;
  import org.opencms.main.CmsLog;
  import org.opencms.main.OpenCms;
  
  import java.io.IOException;
  import java.util.List;
  import java.util.Map;
  
  import javax.servlet.RequestDispatcher;
  import javax.servlet.ServletException;
  import javax.servlet.ServletRequest;
  import javax.servlet.ServletResponse;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  
  import org.apache.commons.logging.Log;
  
  /**
   * Implementation of the <code>{@link javax.servlet.RequestDispatcher}</code> interface to allow JSPs to be loaded
   * from the OpenCms VFS.<p>
   *
   * This dispatcher will load data from 3 different data sources:
   * <ol>
   * <li>Form the "real" os File system (e.g. for JSP pages)
   * <li>From the OpenCms VFS
   * <li>From the Flex cache
   * </ol>
   * <p>
   *
   * @since 6.0.0
   */
  public class CmsFlexRequestDispatcher implements RequestDispatcher {
  
      /** The log object for this class. */
      private static final Log LOG = CmsLog.getLog(CmsFlexRequestDispatcher.class);
  
      /** The external target that will be included by the RequestDispatcher, needed if this is not a dispatcher to a cms resource. */
      private String m_extTarget;
  
      /** The "real" RequestDispatcher, used when a true include (to the file system) is needed. */
      private RequestDispatcher m_rd;
  
      /** The OpenCms VFS target that will be included by the RequestDispatcher. */
      private String m_vfsTarget;
  
      /**
       * Creates a new instance of CmsFlexRequestDispatcher.<p>
       *
       * @param rd the "real" dispatcher, used for include call to file system
       * @param vfs_target the cms resource that represents the external target
       * @param ext_target the external target that the request will be dispatched to
       */
      public CmsFlexRequestDispatcher(RequestDispatcher rd, String vfs_target, String ext_target) {
  
          m_rd = rd;
          m_vfsTarget = vfs_target;
          m_extTarget = ext_target;
      }
  
      /**
       * Wrapper for the standard servlet API call.<p>
       *
       * Forward calls are actually NOT wrapped by OpenCms as of now.
       * So they should not be used in JSP pages or servlets.<p>
       *
       * @param req the servlet request
       * @param res the servlet response
       * @throws ServletException in case something goes wrong
       * @throws IOException in case something goes wrong
       *
       * @see javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
       */
      public void forward(ServletRequest req, ServletResponse res) throws ServletException, IOException {
  
          CmsFlexController controller = CmsFlexController.getController(req);
          controller.setForwardMode(true);
          m_rd.forward(req, res);
      }
  
      /**
       * Wrapper for dispatching to a file from the OpenCms VFS.<p>
       *
       * This method will dispatch to cache, to real file system or
       * to the OpenCms VFS, whatever is needed.<p>
       *
       * This method is much more complex than it should be because of the internal standard
       * buffering of JSP pages.
       * Because of that I can not just intercept and buffer the stream, since I don't have
       * access to it (it is wrapped internally in the JSP pages, which have their own buffer).
       * That leads to a solution where the data is first written to the buffered stream,
       * but without includes. Then it is parsed again later
       * in the <code>{@link CmsFlexResponse}</code>, enriched with the
       * included elements that have been omitted in the first case.
       * I would love to see a simpler solution, but this works for now.<p>
       *
       * @param req the servlet request
       * @param res the servlet response
       *
       * @throws ServletException in case something goes wrong
       * @throws IOException in case something goes wrong
       */
      public void include(ServletRequest req, ServletResponse res) throws ServletException, IOException {
  
          if (LOG.isDebugEnabled()) {
              LOG.debug(
                  Messages.get().getBundle().key(
                      Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDING_TARGET_2,
                      m_vfsTarget,
                      m_extTarget));
          }
  
          CmsFlexController controller = CmsFlexController.getController(req);
          CmsResource resource = null;
  
          if ((m_extTarget == null) && (controller != null)) {
              // check if the file exists in the VFS, if not set external target
              try {
                  resource = controller.getCmsObject().readResource(m_vfsTarget);
              } catch (CmsVfsResourceNotFoundException e) {
                  // file not found in VFS, treat it as external file
                  m_extTarget = m_vfsTarget;
              } catch (CmsException e) {
                  // if other OpenCms exception occurred we are in trouble
                  throw new ServletException(
                      Messages.get().getBundle().key(Messages.ERR_FLEXREQUESTDISPATCHER_VFS_ACCESS_EXCEPTION_0),
                      e);
              }
          }
  
          if ((m_extTarget != null) || (controller == null)) {
              includeExternal(req, res);
          } else if (controller.isForwardMode()) {
              includeInternalNoCache(req, res, controller, controller.getCmsObject(), resource);
          } else {
              includeInternalWithCache(req, res, controller, controller.getCmsObject(), resource);
          }
      }
  
      /**
       * Include an external (non-OpenCms) file using the standard dispatcher.<p>
       *
       * @param req the servlet request
       * @param res the servlet response
       *
       * @throws ServletException in case something goes wrong
       * @throws IOException in case something goes wrong
       */
      private void includeExternal(ServletRequest req, ServletResponse res) throws ServletException, IOException {
  
          // This is an external include, probably to a JSP page, dispatch with system dispatcher
          if (LOG.isInfoEnabled()) {
              LOG.info(
                  Messages.get().getBundle().key(
                      Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDING_EXTERNAL_TARGET_1,
                      m_extTarget));
          }
          m_rd.include(req, res);
      }
  
      /**
       * Includes the requested resource, ignoring the Flex cache.<p>
       *
       * @param req the servlet request
       * @param res the servlet response
       * @param controller the flex controller
       * @param cms the cms context
       * @param resource the requested resource (may be <code>null</code>)
       *
       * @throws ServletException in case something goes wrong
       * @throws IOException in case something goes wrong
       */
      private void includeInternalNoCache(
          ServletRequest req,
          ServletResponse res,
          CmsFlexController controller,
          CmsObject cms,
          CmsResource resource)
      throws ServletException, IOException {
  
          // load target with the internal resource loader
          I_CmsResourceLoader loader;
  
          try {
              if (resource == null) {
                  resource = cms.readResource(m_vfsTarget);
              }
              if (LOG.isDebugEnabled()) {
                  LOG.debug(
                      Messages.get().getBundle().key(
                          Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_TYPE_1,
                          Integer.valueOf(resource.getTypeId())));
              }
              loader = OpenCms.getResourceManager().getLoader(resource);
          } catch (CmsException e) {
              // file might not exist or no read permissions
              controller.setThrowable(e, m_vfsTarget);
              throw new ServletException(
                  Messages.get().getBundle().key(
                      Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_READING_RESOURCE_1,
                      m_vfsTarget),
                  e);
          }
  
          if (LOG.isDebugEnabled()) {
              LOG.debug(
                  Messages.get().getBundle().key(Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDE_RESOURCE_1, m_vfsTarget));
          }
          try {
              loader.service(cms, resource, req, res);
          } catch (CmsException e) {
              // an error occurred during access to OpenCms
              controller.setThrowable(e, m_vfsTarget);
              throw new ServletException(e);
          }
      }
  
      /**
       * Includes the requested resource, ignoring the Flex cache.<p>
       *
       * @param req the servlet request
       * @param res the servlet response
       * @param controller the Flex controller
       * @param cms the current users OpenCms context
       * @param resource the requested resource (may be <code>null</code>)
       *
       * @throws ServletException in case something goes wrong
       * @throws IOException in case something goes wrong
       */
      private void includeInternalWithCache(
          ServletRequest req,
          ServletResponse res,
          CmsFlexController controller,
          CmsObject cms,
          CmsResource resource)
      throws ServletException, IOException {
  
          CmsFlexCache cache = controller.getCmsCache();
  
          // this is a request through the CMS
          CmsFlexRequest f_req = controller.getCurrentRequest();
          CmsFlexResponse f_res = controller.getCurrentResponse();
  
          if (f_req.exceedsCallLimit(m_vfsTarget)) {
              // this resource was already included earlier, so we have a (probably endless) inclusion loop
              throw new ServletException(
                  Messages.get().getBundle().key(Messages.ERR_FLEXREQUESTDISPATCHER_INCLUSION_LOOP_1, m_vfsTarget));
          } else {
              f_req.addInlucdeCall(m_vfsTarget);
          }
  
          // do nothing if response is already finished (probably as a result of an earlier redirect)
          if (f_res.isSuspended()) {
              // remove this include call if response is suspended (e.g. because of redirect)
              f_res.setCmsIncludeMode(false);
              f_req.removeIncludeCall(m_vfsTarget);
              return;
          }
  
          // indicate to response that all further output or headers are result of include calls
          f_res.setCmsIncludeMode(true);
  
          // create wrapper for request & response
          CmsFlexRequest w_req = new CmsFlexRequest((HttpServletRequest)req, controller, m_vfsTarget);
          CmsFlexResponse w_res = new CmsFlexResponse((HttpServletResponse)res, controller);
  
          // push req/res to controller stack
          controller.push(w_req, w_res);
  
          // now that the req/res are on the stack, we need to make sure that they are removed later
          // that's why we have this try { ... } finally { ... } clause here
          try {
              CmsFlexCacheEntry entry = null;
              if (f_req.isCacheable()) {
                  // caching is on, check if requested resource is already in cache
                  entry = cache.get(w_req.getCmsCacheKey());
                  if (entry != null) {
                      // the target is already in the cache
                      try {
                          if (LOG.isDebugEnabled()) {
                              LOG.debug(
                                  Messages.get().getBundle().key(
                                      Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_FROM_CACHE_1,
                                      m_vfsTarget));
                          }
                          controller.updateDates(entry.getDateLastModified(), entry.getDateExpires());
                          entry.service(w_req, w_res);
                      } catch (CmsException e) {
                          Throwable t;
                          if (e.getCause() != null) {
                              t = e.getCause();
                          } else {
                              t = e;
                          }
                          t = controller.setThrowable(e, m_vfsTarget);
                          throw new ServletException(
                              Messages.get().getBundle().key(
                                  Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_LOADING_RESOURCE_FROM_CACHE_1,
                                  m_vfsTarget),
                              t);
                      }
                  } else {
                      // cache is on and resource is not yet cached, so we need to read the cache key for the response
                      CmsFlexCacheKey res_key = cache.getKey(CmsFlexCacheKey.getKeyName(m_vfsTarget, w_req.isOnline()));
                      if (res_key != null) {
                          // key already in cache, reuse it
                          w_res.setCmsCacheKey(res_key);
                      } else {
                          // cache key is unknown, read key from properties
                          String cacheProperty = null;
                          try {
                              // read caching property from requested VFS resource
                              if (resource == null) {
                                  resource = cms.readResource(m_vfsTarget);
                              }
                              cacheProperty = cms.readPropertyObject(
                                  resource,
                                  CmsPropertyDefinition.PROPERTY_CACHE,
                                  true).getValue();
                              if (cacheProperty == null) {
                                  // caching property not set, use default for resource type
                                  cacheProperty = OpenCms.getResourceManager().getResourceType(
                                      resource.getTypeId()).getCachePropertyDefault();
                              }
                              cache.putKey(
                                  w_res.setCmsCacheKey(
                                      cms.getRequestContext().addSiteRoot(m_vfsTarget),
                                      cacheProperty,
                                      f_req.isOnline()));
                          } catch (CmsFlexCacheException e) {
  
                              // invalid key is ignored but logged, used key is cache=never
                              if (LOG.isWarnEnabled()) {
                                  LOG.warn(
                                      Messages.get().getBundle().key(
                                          Messages.LOG_FLEXREQUESTDISPATCHER_INVALID_CACHE_KEY_2,
                                          m_vfsTarget,
                                          cacheProperty));
                              }
                              // there will be a valid key in the response ("cache=never") even after an exception
                              cache.putKey(w_res.getCmsCacheKey());
                          } catch (CmsException e) {
  
                              // all other errors are not handled here
                              controller.setThrowable(e, m_vfsTarget);
                              throw new ServletException(
                                  Messages.get().getBundle().key(
                                      Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_LOADING_CACHE_PROPERTIES_1,
                                      m_vfsTarget),
                                  e);
                          }
                          if (LOG.isDebugEnabled()) {
                              LOG.debug(
                                  Messages.get().getBundle().key(
                                      Messages.LOG_FLEXREQUESTDISPATCHER_ADDING_CACHE_PROPERTIES_2,
                                      m_vfsTarget,
                                      cacheProperty));
                          }
                      }
                  }
              }
  
              if (entry == null) {
                  boolean ignore = (w_res.getCmsCacheKey() != null) && w_res.getCmsCacheKey().isIgnore();
                  // the target is not cached (or caching off), so load it with the internal resource loader
                  I_CmsResourceLoader loader = null;
  
                  String variation = null;
                  // check cache keys to see if the result can be cached
                  if (w_req.isCacheable()) {
                      variation = w_res.getCmsCacheKey().matchRequestKey(w_req.getCmsCacheKey());
                  }
                  // indicate to the response if caching is not required
                  w_res.setCmsCachingRequired(!controller.isForwardMode() && (variation != null));
  
                  try {
                      if (resource == null) {
                          resource = cms.readResource(m_vfsTarget);
                      }
                      if (LOG.isDebugEnabled()) {
                          LOG.debug(
                              Messages.get().getBundle().key(
                                  Messages.LOG_FLEXREQUESTDISPATCHER_LOADING_RESOURCE_TYPE_1,
                                  Integer.valueOf(resource.getTypeId())));
                      }
                      loader = OpenCms.getResourceManager().getLoader(resource);
                  } catch (ClassCastException e) {
                      controller.setThrowable(e, m_vfsTarget);
                      throw new ServletException(
                          Messages.get().getBundle().key(
                              Messages.ERR_FLEXREQUESTDISPATCHER_CLASSCAST_EXCEPTION_1,
                              m_vfsTarget),
                          e);
                  } catch (CmsException e) {
                      // file might not exist or no read permissions
                      controller.setThrowable(e, m_vfsTarget);
                      throw new ServletException(
                          Messages.get().getBundle().key(
                              Messages.ERR_FLEXREQUESTDISPATCHER_ERROR_READING_RESOURCE_1,
                              m_vfsTarget),
                          e);
                  }
  
                  if (LOG.isDebugEnabled()) {
                      LOG.debug(
                          Messages.get().getBundle().key(
                              Messages.LOG_FLEXREQUESTDISPATCHER_INCLUDE_RESOURCE_1,
                              m_vfsTarget));
                  }
                  try {
                      loader.service(cms, resource, w_req, w_res);
                  } catch (Exception e) {
                      LOG.debug(e.getLocalizedMessage(), e);
                      // an error occurred
                      if (f_res.hasIncludeList()) {
                          // to prevent include list and include result list indices going out of sync, add an empty byte array
                          f_res.addToIncludeResults(new byte[] {});
                      }
                      controller.setThrowable(e, m_vfsTarget);
                      throw new ServletException(e);
                  }
  
                  entry = w_res.processCacheEntry();
                  if ((entry != null) && (variation != null) && w_req.isCacheable()) {
                      // the result can be cached
                      if (w_res.getCmsCacheKey().getTimeout() > 0) {
                          // cache entry has a timeout, set last modified to time of last creation
                          entry.setDateLastModifiedToPreviousTimeout(w_res.getCmsCacheKey().getTimeout());
                          entry.setDateExpiresToNextTimeout(w_res.getCmsCacheKey().getTimeout());
                          // if expiration date from controller comes before timeout, don't wait until timeout
                          entry.limitDateExpires(controller.getDateExpires());
                          controller.updateDates(entry.getDateLastModified(), entry.getDateExpires());
                      } else {
                          // no timeout, use last modified date from files in VFS
                          entry.setDateLastModified(controller.getDateLastModified());
                          entry.setDateExpires(controller.getDateExpires());
                      }
                      cache.put(w_res.getCmsCacheKey(), entry, variation, w_req.getCmsCacheKey());
                  } else if (!ignore) {
                      // result can not be cached, do not use "last modified" optimization
                      controller.updateDates(-1, controller.getDateExpires());
                  }
              }
  
              if (f_res.hasIncludeList()) {
                  // special case: this indicates that the output was not yet displayed
                  Map<String, List<String>> headers = w_res.getHeaders();
                  byte[] result = w_res.getWriterBytes();
                  if (LOG.isDebugEnabled()) {
                      LOG.debug(
                          Messages.get().getBundle().key(
                              Messages.LOG_FLEXREQUESTDISPATCHER_RESULT_1,
                              new String(result)));
                  }
                  CmsFlexResponse.processHeaders(headers, f_res);
                  f_res.addToIncludeResults(result);
                  result = null;
              }
          } finally {
              // indicate to response that include is finished
              f_res.setCmsIncludeMode(false);
              f_req.removeIncludeCall(m_vfsTarget);
  
              // pop req/res from controller stack
              controller.pop();
          }
      }
  
  }