dotCMS / core

Headless/Hybrid Content Management System for Enterprises
http://dotcms.com
Other
864 stars 468 forks source link

Spike: Identify Changes Required in PageResource for UVE - Future Time Machine Feature #29816

Closed nollymar closed 1 month ago

nollymar commented 2 months ago

Parent Issue

29574

Task

As a developer, I want to identify all the changes needed in the PageResource endpoint, so that it can support the "Future Time Machine" feature for UVE, allowing pages to be pulled based on a specific date.

Proposed Objective

Same as Parent Issue

Proposed Priority

Same as Parent Issue

Acceptance Criteria

Given the current PageResource implementation, when the spike is completed, then a list of actionable cards (tasks or user stories) required to implement the feature is produced.

Ensure that any proposed changes maintain backward compatibility with existing API consumers.

External Links... Slack Conversations, Support Tickets, Figma Designs, etc.

No response

Assumptions & Initiation Needs

No response

Quality Assurance Notes & Workarounds

No response

Sub-Tasks & Estimates

No response

jgambarios commented 2 months ago

Future Time Machine can be handled (enabled and disabled) using session attributes. These are the attributes:

    req.getSession().setAttribute("tm_host", host);
    req.getSession().setAttribute("tm_date", dateInMillisecondsAsString);
    req.getSession().setAttribute("tm_lang", lanaguageId);
    req.getSession().setAttribute("dotcache", "refresh");

I recommend adding query parameters to the PageResource methods that need to use Time Machine. For example, to retrieve the Time Machine date to use. If these methods in the PageResource detect that Time Machine needs to be used, the session attributes should be set.

[!IMPORTANT]
it is very important to clean up these attributes after each use to avoid permanently affecting the session:

    request.getSession(false).removeAttribute("tm_host");
    request.getSession(false).removeAttribute("tm_date");
    request.getSession(false).removeAttribute("tm_lang");
    request.getSession(false).removeAttribute("dotcache");

Example

Here's a very simple proof-of-concept example. Please note that this is not production-ready code and should not be emulated directly. It's intended only to demonstrate the concept. Using this basic implementation, I was able to add Time Machine support to the /api/v1/page/render/ and /api/v1/page/json/ methods.

diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java
--- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java
+++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java
@@ -1,22 +1,24 @@
 package com.dotcms.rest.api.v1.page;

 @Path("/v1/page")
 @Tag(name = "Page", 
         description = "Endpoints that operate on pages",
         externalDocs = @ExternalDocumentation(description = "Additional Page API information", 
                                                 url = "https://www.dotcms.com/docs/latest/page-rest-api-layout-as-a-service-laas"))

 public class PageResource {

@@ -321,40 +320,52 @@ public class PageResource {
      * @param deviceInode     The {@link java.lang.String}'s inode to render the page. This is used
      *                        to render the page with specific width and height dimensions.
      * @param asJson          If only the HTML PAge's metadata must be returned in the JSON
      *                        response, set this to {@code true}. Otherwise, if the rendered
      *                        Containers and page content must be returned as well, set it to
      *                        {@code false}.
      *
      * @return The HTML Page's metadata -- or the associated Vanity URL data -- in JSON format.
      *
      * @throws DotDataException     An error occurred when accessing information in the database.
      * @throws DotSecurityException The currently logged-in user does not have the necessary
      *                              permissions to call this action.
      */
     private Response getPageRender(final HttpServletRequest originalRequest,
                                    final HttpServletRequest request,
                                    final HttpServletResponse response, final User user,
                                    final String uri, final String languageId,
                                    final String modeParam, final String deviceInode,
                                    final boolean asJson) throws DotDataException,
             DotSecurityException {
+
+        if (request.getParameter("timeMachine") != null) {
+            long twentyDaysFromNowMillis = Instant.now().plus(20, ChronoUnit.DAYS).toEpochMilli();
+            request.getSession(false).setAttribute("tm_host",
+                    APILocator.getHostAPI()
+                            .find(APILocator.getHostAPI().findDefaultHost(user, false), user,
+                                    false));
+            request.getSession(false).setAttribute("tm_date", String.valueOf(twentyDaysFromNowMillis));
+            request.getSession(false).setAttribute("tm_lang", languageId);
+            request.getSession(false).setAttribute("dotcache", "refresh");
+        }
+
         String resolvedUri = uri;
         final Optional<CachedVanityUrl> cachedVanityUrlOpt =
                 this.pageResourceHelper.resolveVanityUrlIfPresent(originalRequest, uri, languageId);
         if (cachedVanityUrlOpt.isPresent()) {
             response.setHeader(VanityUrlAPI.VANITY_URL_RESPONSE_HEADER, cachedVanityUrlOpt.get().vanityUrlId);
             if (cachedVanityUrlOpt.get().isTemporaryRedirect() || cachedVanityUrlOpt.get().isPermanentRedirect()) {
                 Logger.debug(this, () -> String.format("Incoming Vanity URL is a %d Redirect",
                         cachedVanityUrlOpt.get().response));
                 final EmptyPageView emptyPageView =
                         new EmptyPageView.Builder().vanityUrl(cachedVanityUrlOpt.get()).build();
                 return Response.ok(new ResponseEntityView<>(emptyPageView)).build();
             } else {
                 final VanityUrlResult vanityUrlResult = cachedVanityUrlOpt.get().handle(uri);
                 resolvedUri = vanityUrlResult.getRewrite();
                 Logger.debug(this, () -> String.format("Incoming Vanity URL resolved to URI: %s",
                         vanityUrlResult.getRewrite()));
             }
         }
         final PageMode mode = modeParam != null
                 ? PageMode.get(modeParam)
@@ -367,50 +378,58 @@ public class PageResource {
         }
         PageView pageRendered;
         final PageContextBuilder pageContextBuilder = PageContextBuilder.builder()
                 .setUser(user)
                 .setPageUri(resolvedUri)
                 .setPageMode(mode);
         cachedVanityUrlOpt.ifPresent(cachedVanityUrl
                 -> pageContextBuilder.setVanityUrl(new VanityURLView.Builder().vanityUrl(cachedVanityUrl).build()));
         if (asJson) {
             pageRendered = this.htmlPageAssetRenderedAPI.getPageMetadata(
                     pageContextBuilder
                             .setParseJSON(true)
                             .build(),
                     request, response
             );
         } else {
             final HttpSession session = request.getSession(false);
             if (null != session) {
                 // Time Machine-Date affects the logic on the VTLs that conform parts of the
                 // rendered pages. So, we better get rid of it
-                session.removeAttribute("tm_date");
+                if (request.getParameter("timeMachine") == null) {
+                    session.removeAttribute("tm_date");
+                }
             }
             pageRendered = this.htmlPageAssetRenderedAPI.getPageRendered(
                     pageContextBuilder.build(), request, response
             );
         }
         final Host site = APILocator.getHostAPI().find(pageRendered.getPage().getHost(), user,
                 PageMode.get(request).respectAnonPerms);
         request.setAttribute(WebKeys.CURRENT_HOST, site);
         request.getSession().setAttribute(WebKeys.CURRENT_HOST, site);
+
+        request.getSession(false).removeAttribute("tm_host");
+        request.getSession(false).removeAttribute("tm_date");
+        request.getSession(false).removeAttribute("tm_lang");
+        request.getSession(false).removeAttribute("dotcache");
+
         return Response.ok(new ResponseEntityView<>(pageRendered)).build();
     }

After implementing the sample code and adding some test blog posts with future publish dates, I was able to successfully test the Time Machine functionality using the following URLs:

https://local.dotcms.site:8443/api/v1/page/render/blog?language_id=1&timeMachine=true&mode=LIVE
https://local.dotcms.site:8443/api/v1/page/render/blog?language_id=1&timeMachine=true&mode=EDIT_MODE

https://local.dotcms.site:8443/api/v1/page/render/blog?language_id=1&mode=LIVE
https://local.dotcms.site:8443/api/v1/page/render/blog?language_id=1&mode=EDIT_MODE

https://local.dotcms.site:8443/api/v1/page/json/blog?language_id=1&timeMachine=true&mode=LIVE
https://local.dotcms.site:8443/api/v1/page/json/blog?language_id=1&timeMachine=true&mode=EDIT_MODE

https://local.dotcms.site:8443/api/v1/page/json/blog?language_id=1&mode=LIVE
https://local.dotcms.site:8443/api/v1/page/json/blog?language_id=1&mode=EDIT_MODE