diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageRenderParams.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageRenderParams.java new file mode 100644 index 000000000000..a67238eeda67 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageRenderParams.java @@ -0,0 +1,85 @@ +package com.dotcms.rest.api.v1.page; + +import com.dotmarketing.business.APILocator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.liferay.portal.model.User; +import java.time.Instant; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.immutables.value.Value; + +@JsonSerialize(as = ImmutablePageRenderParams.class) +@JsonDeserialize(as = ImmutablePageRenderParams.class) +@Value.Immutable +public interface PageRenderParams { + + /** + * The original request that was made to the server. This is the request that was made to the server before any + * @return the original {@link HttpServletRequest} object + */ + HttpServletRequest originalRequest(); + + /** + * The decorated {@link HttpServletRequest} object, which includes + * special handling in some of its properties. + * @return the decorated {@link HttpServletRequest} object + */ + HttpServletRequest request(); + + /** + * The {@link HttpServletResponse} object that will be used to send the response back to the client. + * @return the {@link HttpServletResponse} object + */ + HttpServletResponse response(); + + /** + * The {@link User} object that represents the user that is making the request. + * @return the {@link User} object + */ + User user(); + + /** + * The {@link Host} object that represents the host that the request is being made to. + * @return the {@link Host} object + */ + String uri(); + + /** + * The language id of the language that the request is being made in. + * @return the language id + */ + @Value.Default + default String languageId(){ + return String.valueOf(APILocator.getLanguageAPI().getDefaultLanguage().getId()); + } + + /** + * The current {@link PageMode} used to render the page. + * @return the current {@link PageMode} + */ + Optional modeParam(); + + /** + * The {@link java.lang.String}'s inode to render the page. This is used + * @return the {@link java.lang.String}'s inode + */ + Optional deviceInode(); + + /** + * 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 {@code true} if only the HTML Page's metadata must be + */ + @Value.Default + default boolean asJson(){ return false; } + + /** + * Time machine date to use for rendering the page. If this is set, the page will be rendered as if it was published at this date. + * @return + */ + Optional timeMachineDate(); +} 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 index cf0c32b57d78..06bc5c37c5b4 100644 --- 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,5 +1,7 @@ package com.dotcms.rest.api.v1.page; +import static com.dotcms.util.DotPreconditions.checkNotNull; + import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.content.elasticsearch.business.ESSearchResults; import com.dotcms.contenttype.business.ContentTypeAPI; @@ -15,6 +17,7 @@ import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.rest.api.v1.page.ImmutablePageRenderParams.Builder; import com.dotcms.rest.api.v1.personalization.PersonalizationPersonaPageViewPaginator; import com.dotcms.rest.api.v1.workflow.WorkflowResource; import com.dotcms.rest.exception.BadRequestException; @@ -34,6 +37,7 @@ import com.dotmarketing.business.APILocator; import com.dotmarketing.business.PermissionLevel; import com.dotmarketing.business.Permissionable; +import com.dotmarketing.business.web.HostWebAPI; import com.dotmarketing.business.web.WebAPILocator; import com.dotmarketing.cms.urlmap.URLMapInfo; import com.dotmarketing.cms.urlmap.UrlMapContextBuilder; @@ -68,19 +72,30 @@ import com.liferay.portal.PortalException; import com.liferay.portal.SystemException; import com.liferay.portal.model.User; -import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.ExternalDocumentation; -import org.apache.commons.collections.keyvalue.MultiKey; -import org.glassfish.jersey.server.JSONP; - +import io.swagger.v3.oas.annotations.tags.Tag; +import io.vavr.control.Try; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -96,22 +111,8 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; - -import static com.dotcms.util.DotPreconditions.checkNotNull; +import org.apache.commons.collections.keyvalue.MultiKey; +import org.glassfish.jersey.server.JSONP; /** * Provides different methods to access information about HTML Pages in dotCMS. For example, @@ -132,11 +133,16 @@ public class PageResource { + public static final String TM_DATE = "tm_date"; + public static final String TM_LANG = "tm_lang"; + public static final String TM_HOST = "tm_host"; + public static final String DOT_CACHE = "dotcache"; + private final PageResourceHelper pageResourceHelper; private final WebResource webResource; private final HTMLPageAssetRenderedAPI htmlPageAssetRenderedAPI; private final ContentletAPI esapi; - + private final HostWebAPI hostWebAPI; /** * Creates an instance of this REST end-point. */ @@ -146,7 +152,8 @@ public PageResource() { PageResourceHelper.getInstance(), new WebResource(), APILocator.getHTMLPageAssetRenderedAPI(), - APILocator.getContentletAPI() + APILocator.getContentletAPI(), + WebAPILocator.getHostWebAPI() ); } @@ -155,12 +162,14 @@ public PageResource() { final PageResourceHelper pageResourceHelper, final WebResource webResource, final HTMLPageAssetRenderedAPI htmlPageAssetRenderedAPI, - final ContentletAPI esapi) { + final ContentletAPI esapi, + final HostWebAPI hostWebAPI) { this.pageResourceHelper = pageResourceHelper; this.webResource = webResource; this.htmlPageAssetRenderedAPI = htmlPageAssetRenderedAPI; this.esapi = esapi; + this.hostWebAPI = hostWebAPI; } /** @@ -209,11 +218,12 @@ public Response loadJson(@Context final HttpServletRequest originalRequest, @QueryParam(WebKeys.PAGE_MODE_PARAMETER) final String modeParam, @QueryParam(WebKeys.CMS_PERSONA_PARAMETER) final String personaId, @QueryParam("language_id") final String languageId, - @QueryParam("device_inode") final String deviceInode) throws DotDataException, DotSecurityException { + @QueryParam("device_inode") final String deviceInode, + @QueryParam(TM_DATE) final String timeMachineDateAsISO8601 + ) throws DotDataException, DotSecurityException { Logger.debug(this, () -> String.format( - "Rendering page as JSON: uri -> %s , mode -> %s , language -> %s , persona -> %s , device_inode -> %s", - uri, modeParam, languageId, personaId, deviceInode)); - + "Rendering page as JSON: uri -> %s , mode -> %s , language -> %s , persona -> %s , device_inode -> %s, timeMachineDate -> %s", + uri, modeParam, languageId, personaId, deviceInode, timeMachineDateAsISO8601)); final HttpServletRequest request = this.pageResourceHelper.decorateRequest (originalRequest); // Force authentication final InitDataObject initData = @@ -222,8 +232,17 @@ public Response loadJson(@Context final HttpServletRequest originalRequest, .rejectWhenNoUser(true) .init(); final User user = initData.getUser(); - return getPageRender(originalRequest, request, response, user, uri, languageId, - modeParam, deviceInode, true); + final Builder builder = ImmutablePageRenderParams.builder(); + builder + .originalRequest(originalRequest) + .request(request) + .response(response) + .user(user) + .uri(uri) + .asJson(true); + final PageRenderParams renderParams = optionalRenderParams(modeParam, + languageId, deviceInode, timeMachineDateAsISO8601, builder); + return getPageRender(renderParams); } /** @@ -276,19 +295,20 @@ public Response render(@Context final HttpServletRequest originalRequest, @QueryParam(WebKeys.PAGE_MODE_PARAMETER) final String modeParam, @QueryParam(WebKeys.CMS_PERSONA_PARAMETER) final String personaId, @QueryParam(WebKeys.LANGUAGE_ID_PARAMETER) final String languageId, - @QueryParam("device_inode") final String deviceInode) throws DotSecurityException, DotDataException { - if (HttpRequestDataUtil.getAttribute(originalRequest, EMAWebInterceptor.EMA_REQUEST_ATTR, false) + @QueryParam("device_inode") final String deviceInode, + @QueryParam(TM_DATE) final String timeMachineDateAsISO8601 ) throws DotSecurityException, DotDataException { + if (Boolean.TRUE.equals(HttpRequestDataUtil.getAttribute(originalRequest, EMAWebInterceptor.EMA_REQUEST_ATTR, false)) && !this.includeRenderedAttrFromEMA(originalRequest, uri)) { final String depth = HttpRequestDataUtil.getAttribute(originalRequest, EMAWebInterceptor.DEPTH_PARAM, null); if (UtilMethods.isSet(depth)) { HttpServletRequestThreadLocal.INSTANCE.getRequest().setAttribute(WebKeys.HTMLPAGE_DEPTH, depth); } return this.loadJson(originalRequest, response, uri, modeParam, personaId, languageId - , deviceInode); + , deviceInode, timeMachineDateAsISO8601); } Logger.debug(this, () -> String.format( - "Rendering page: uri -> %s , mode -> %s , language -> %s , persona -> %s , device_inode -> %s", - uri, modeParam, languageId, personaId, deviceInode)); + "Rendering page: uri -> %s , mode -> %s , language -> %s , persona -> %s , device_inode -> %s , timeMachineDate -> %s", + uri, modeParam, languageId, personaId, deviceInode, timeMachineDateAsISO8601)); final HttpServletRequest request = this.pageResourceHelper.decorateRequest(originalRequest); // Force authentication final InitDataObject initData = @@ -297,8 +317,43 @@ public Response render(@Context final HttpServletRequest originalRequest, .rejectWhenNoUser(true) .init(); final User user = initData.getUser(); - return getPageRender(originalRequest, request, response, user, uri, languageId, - modeParam, deviceInode, false); + final Builder builder = ImmutablePageRenderParams.builder(); + builder.originalRequest(originalRequest) + .request(request) + .response(response) + .user(user) + .uri(uri); + final PageRenderParams renderParams = optionalRenderParams(modeParam, + languageId, deviceInode, timeMachineDateAsISO8601, builder); + return getPageRender(renderParams); + } + + /** + * Returns the metadata -- i.e.; the objects that make up an HTML Page -- in the form of a JSON + * @param modeParam The current {@link PageMode} used to render the page. + * @param languageId The {@link com.dotmarketing.portlets.languagesmanager.model.Language}'s + * @param deviceInode The {@link java.lang.String}'s inode to render the page. + * @param timeMachineDateAsISO8601 The date to set the Time Machine to. + * @param builder The builder to use to create the {@link PageRenderParams}. + * @return The {@link PageRenderParams} object. + */ + private PageRenderParams optionalRenderParams(final String modeParam, + final String languageId, final String deviceInode, final String timeMachineDateAsISO8601, + Builder builder) { + if (null != languageId){ + builder.languageId(languageId); + } + if (null != modeParam){ + builder.modeParam(modeParam); + } + if (null != deviceInode){ + builder.deviceInode(deviceInode); + } + if(null != timeMachineDateAsISO8601){ + final Instant date = Try.of(()->Instant.parse(timeMachineDateAsISO8601)).getOrElseThrow(e->new IllegalArgumentException("time machine date must be ISO-8601 compliant")); + builder.timeMachineDate(date); + } + return builder.build(); } /** @@ -306,95 +361,150 @@ public Response render(@Context final HttpServletRequest originalRequest, * its metadata in the form of a JSON object. Vanity URLs are taken into account, in which case * Temporary or Permanent Redirects will include an "empty" page object; whereas a Forward will * return the metadata of the page that the Vanity URL points to. - * - * @param originalRequest The original {@link HttpServletRequest} object. - * @param request The decorated {@link HttpServletRequest} object, which includes - * special handling in some of its properties. - * @param response The {@link HttpServletResponse} object. - * @param user The currently logged-in {@link User}. - * @param uri The path to the HTML Page whose information will be retrieved, or a - * Vanity URL. - * @param languageId The - * {@link com.dotmarketing.portlets.languagesmanager.model.Language}'s ID - * to render the page. - * @param modeParam The current {@link PageMode} used to render the page. - * @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}. - * + * @param renderParams The parameters used to render the page. * @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, + private Response getPageRender(final PageRenderParams renderParams) throws DotDataException, DotSecurityException { - String resolvedUri = uri; - final Optional 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(); + + final HttpServletRequest request = renderParams.request(); + final HttpServletResponse response = renderParams.response(); + + //Let's set up the Time Machine if needed + setUpTimeMachineIfPresent(renderParams); + try { + String resolvedUri = renderParams.uri(); + final Optional cachedVanityUrlOpt = + this.pageResourceHelper.resolveVanityUrlIfPresent( + renderParams.originalRequest(), renderParams.uri(), + renderParams.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(renderParams.uri()); + resolvedUri = vanityUrlResult.getRewrite(); + Logger.debug(this, + () -> String.format("Incoming Vanity URL resolved to URI: %s", + vanityUrlResult.getRewrite())); + } + } + final Optional modeParam = renderParams.modeParam(); + final PageMode mode = modeParam.isPresent() + ? PageMode.get(modeParam.get()) + : this.htmlPageAssetRenderedAPI.getDefaultEditPageMode(renderParams.user(), + request, resolvedUri); + PageMode.setPageMode(renderParams.request(), mode); + final Optional deviceInode = renderParams.deviceInode(); + if (deviceInode.isPresent() && StringUtils.isSet(deviceInode.get())) { + request.getSession() + .setAttribute(WebKeys.CURRENT_DEVICE,deviceInode.get()); } 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())); + request.getSession().removeAttribute(WebKeys.CURRENT_DEVICE); } + final PageView pageRendered; + final PageContextBuilder pageContextBuilder = PageContextBuilder.builder() + .setUser(renderParams.user()) + .setPageUri(resolvedUri) + .setPageMode(mode); + cachedVanityUrlOpt.ifPresent(cachedVanityUrl + -> pageContextBuilder.setVanityUrl( + new VanityURLView.Builder().vanityUrl(cachedVanityUrl).build())); + if (renderParams.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); + } + pageRendered = this.htmlPageAssetRenderedAPI.getPageRendered( + pageContextBuilder.build(), request, response + ); + } + final Host site = APILocator.getHostAPI() + .find(pageRendered.getPage().getHost(), renderParams.user(), + PageMode.get(request).respectAnonPerms); + request.setAttribute(WebKeys.CURRENT_HOST, site); + request.getSession().setAttribute(WebKeys.CURRENT_HOST, site); + return Response.ok(new ResponseEntityView<>(pageRendered)).build(); + } finally { + // Let's reset the Time Machine if needed + resetTimeMachineIfPresent(request); } - final PageMode mode = modeParam != null - ? PageMode.get(modeParam) - : this.htmlPageAssetRenderedAPI.getDefaultEditPageMode(user, request, resolvedUri); - PageMode.setPageMode(request, mode); - if (StringUtils.isSet(deviceInode)) { - request.getSession().setAttribute(WebKeys.CURRENT_DEVICE, deviceInode); - } else { - request.getSession().removeAttribute(WebKeys.CURRENT_DEVICE); - } - 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 { + + } + + /** + * Sets up the Time Machine if the request includes a Time Machine date. + * @param renderParams The parameters used to render the page. + */ + private void setUpTimeMachineIfPresent(final PageRenderParams renderParams) { + final Optional timeMachineDate = renderParams.timeMachineDate(); + if(timeMachineDate.isPresent()){ + final HttpServletRequest request = renderParams.request(); 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"); + session.setAttribute(TM_DATE, String.valueOf(timeMachineDate.get().toEpochMilli())); + session.setAttribute(TM_LANG, renderParams.languageId()); + session.setAttribute(DOT_CACHE, "refresh"); + final Optional host = currentHost(renderParams); + if(host.isPresent()){ + session.setAttribute(TM_HOST, host.get()); + } else { + throw new IllegalArgumentException("Unable to set a host for the Time Machine"); + } + } + } + } + + /** + * Removes the Time Machine attributes from the session. + * @param request The current instance of the {@link HttpServletRequest}. + */ + private void resetTimeMachineIfPresent(final HttpServletRequest request) { + final HttpSession session = request.getSession(false); + if (null != session) { + session.removeAttribute(TM_DATE); + session.removeAttribute(TM_LANG); + session.removeAttribute(TM_HOST); + session.removeAttribute(DOT_CACHE); + } + } + + /** + * Returns the metadata of an HTML Page based on the specified URI. If such a URI maps to a + * @param renderParams The parameters used to render the page. + * @return current host if any, otherwise the default host. + */ + private Optional currentHost(final PageRenderParams renderParams) { + Host currentHost = hostWebAPI.getCurrentHostNoThrow(renderParams.request()); + if (null == currentHost) { + try { + currentHost = hostWebAPI.findDefaultHost(renderParams.user(), false); + } catch ( DotSecurityException | DotDataException e) { + Logger.error(this, "Error getting default host", e); } - 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); - return Response.ok(new ResponseEntityView<>(pageRendered)).build(); + return Optional.ofNullable(currentHost); } /** @@ -539,7 +649,7 @@ public Response saveLayout( try { final Template templateSaved = this.pageResourceHelper.saveTemplate(user, form); - res = Response.ok(new ResponseEntityView(templateSaved)).build(); + res = Response.ok(new ResponseEntityView<>(templateSaved)).build(); } catch (DotSecurityException e) { final String errorMsg = String.format("DotSecurityException on PageResource.saveLayout, parameters: %s, %s: ", @@ -632,7 +742,7 @@ public final Response addContent(@Context final HttpServletRequest request, pageResourceHelper.saveContent(pageId, this.reduce(pageContainerForm.getContainerEntries()), language, variantName); - return Response.ok(new ResponseEntityView("ok")).build(); + return Response.ok(new ResponseEntityView<>("ok")).build(); } catch(HTMLPageAssetNotFoundException e) { final String errorMsg = String.format("HTMLPageAssetNotFoundException on PageResource.addContent, pageId: %s: ", pageId); diff --git a/dotcms-integration/src/test/java/com/dotcms/datagen/TestDataUtils.java b/dotcms-integration/src/test/java/com/dotcms/datagen/TestDataUtils.java index df9654c2e29d..b508989796d3 100644 --- a/dotcms-integration/src/test/java/com/dotcms/datagen/TestDataUtils.java +++ b/dotcms-integration/src/test/java/com/dotcms/datagen/TestDataUtils.java @@ -1,5 +1,7 @@ package com.dotcms.datagen; +import static org.hamcrest.Matchers.equalTo; + import com.dotcms.business.WrapInTransaction; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.exception.NotFoundInDbException; @@ -57,10 +59,6 @@ import com.liferay.portal.model.User; import com.liferay.util.FileUtil; import io.vavr.control.Try; -import org.awaitility.Awaitility; -import org.awaitility.core.ConditionTimeoutException; -import org.junit.Assert; - import java.io.File; import java.io.IOException; import java.net.URISyntaxException; @@ -74,10 +72,12 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; - -import static org.hamcrest.Matchers.equalTo; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionTimeoutException; +import org.junit.Assert; /** * @author Jonathan Gamba 2019-04-16 @@ -249,6 +249,14 @@ public static ContentType getBlogLikeContentType(final String contentTypeName, .type(DateField.class) .next() ); + fields.add( + new FieldDataGen() + .name("postingDate") + .velocityVarName("publishDate") + .defaultValue(null) + .type(DateField.class) + .next() + ); fields.add( new FieldDataGen() .name("seo") @@ -302,6 +310,7 @@ public static ContentType getBlogLikeContentType(final String contentTypeName, .velocityVarName(contentTypeName) .fields(fields) .workflowId(workflowIds) + .publishDateFieldVarName("publishDate") .nextPersisted(); } } catch (Exception e) { @@ -904,12 +913,16 @@ public static ContentType getWikiLikeContentType(final String contentTypeName, } public static ContentType getWidgetLikeContentType() { - return getWidgetLikeContentType("SimpleWidget" + System.currentTimeMillis(), null); + return getWidgetLikeContentType("SimpleWidget" + System.currentTimeMillis(), null, null); + } + + public static ContentType getWidgetLikeContentType(final Supplier widgetCode) { + return getWidgetLikeContentType("SimpleWidget" + System.currentTimeMillis(), null, widgetCode); } @WrapInTransaction public static ContentType getWidgetLikeContentType(final String contentTypeName, - final Set workflowIds) { + final Set workflowIds, final Supplier widgetCode) { ContentType simpleWidgetContentType = null; try { @@ -929,6 +942,17 @@ public static ContentType getWidgetLikeContentType(final String contentTypeName, .required(true) .next() ); + if(null != widgetCode){ //If the widgetCode is not null, then add the field + fields.add( + new FieldDataGen() + .name("WidgetCode") + .velocityVarName("widgetCode") + .type(ConstantField.class) + .required(false) + .values(widgetCode.get()) + .next() + ); + } simpleWidgetContentType = new ContentTypeDataGen() .baseContentType(BaseContentType.WIDGET) diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java index b62ed7c0c4e7..7f627605c5ba 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java @@ -1,6 +1,23 @@ package com.dotcms.rest.api.v1.page; +import static com.dotcms.util.CollectionsUtils.list; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.dotcms.api.web.HttpServletRequestThreadLocal; +import com.dotcms.api.web.HttpServletResponseThreadLocal; import com.dotcms.content.elasticsearch.business.ESSearchResults; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.model.field.TextField; @@ -19,6 +36,7 @@ import com.dotcms.datagen.TemplateLayoutDataGen; import com.dotcms.datagen.TestDataUtils; import com.dotcms.datagen.UserDataGen; +import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtils; import com.dotcms.repackage.org.apache.struts.config.ModuleConfig; import com.dotcms.rest.EmptyHttpResponse; import com.dotcms.rest.InitDataObject; @@ -33,8 +51,11 @@ import com.dotmarketing.beans.Host; import com.dotmarketing.beans.MultiTree; import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.VersionableAPI; +import com.dotmarketing.business.web.HostWebAPI; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.exception.WebAssetException; import com.dotmarketing.factories.MultiTreeAPI; import com.dotmarketing.portlets.containers.business.FileAssetContainerUtil; import com.dotmarketing.portlets.containers.model.Container; @@ -61,47 +82,42 @@ import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; import com.dotmarketing.util.PaginatedArrayList; +import com.dotmarketing.util.PaginatedContentList; import com.dotmarketing.util.UUIDGenerator; import com.dotmarketing.util.UtilMethods; import com.dotmarketing.util.WebKeys; import com.dotmarketing.util.json.JSONException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.liferay.portal.PortalException; import com.liferay.portal.SystemException; import com.liferay.portal.model.User; import com.liferay.util.StringPool; import io.vavr.Tuple; -import org.elasticsearch.action.search.SearchResponse; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.ws.rs.core.Response; import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; - -import static com.dotcms.util.CollectionsUtils.list; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.ws.rs.core.Response; +import org.elasticsearch.action.search.SearchResponse; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; /** * {@link PageResource} test @@ -125,6 +141,11 @@ public class PageResourceTest { private Container container2; private InitDataObject initDataObject; private ContentType contentGenericType; + private HostWebAPI hostWebAPI; + + private final Map sessionAttributes = new ConcurrentHashMap<>( + Map.of("clickstream",new Clickstream()) + ); @BeforeClass public static void prepare() throws Exception { @@ -133,7 +154,8 @@ public static void prepare() throws Exception { } @Before - public void init() throws DotSecurityException, DotDataException { + public void init() + throws DotSecurityException, DotDataException, SystemException, PortalException { // Collection to store attributes keys/values final Map attributes = new ConcurrentHashMap<>(); @@ -156,24 +178,49 @@ public void init() throws DotSecurityException, DotDataException { when(webResource.init(false, request, true)).thenReturn(initDataObject); when(webResource.init(any(WebResource.InitBuilder.class))).thenReturn(initDataObject); when(initDataObject.getUser()).thenReturn(user); - pageResource = new PageResource(pageResourceHelper, webResource, htmlPageAssetRenderedAPI, esapi); - this.pageResourceWithHelper = new PageResource(PageResourceHelper.getInstance(), webResource, htmlPageAssetRenderedAPI, this.esapi); + host = new SiteDataGen().name(hostName).nextPersisted(); + hostWebAPI = mock(HostWebAPI.class); + when(hostWebAPI.getCurrentHost(request, user)).thenReturn(host); + when(hostWebAPI.getCurrentHost(request)).thenReturn(host); + when(hostWebAPI.getCurrentHost()).thenReturn(host); + when(hostWebAPI.getCurrentHostNoThrow(any(HttpServletRequest.class))).thenReturn(host); + when(hostWebAPI.findDefaultHost(any(User.class),anyBoolean())).thenReturn(host); + + pageResource = new PageResource(pageResourceHelper, webResource, htmlPageAssetRenderedAPI, esapi, hostWebAPI); + this.pageResourceWithHelper = new PageResource(PageResourceHelper.getInstance(), webResource, htmlPageAssetRenderedAPI, this.esapi, hostWebAPI); when(request.getRequestURI()).thenReturn("/test"); when(request.getSession()).thenReturn(session); when(request.getSession(false)).thenReturn(session); when(request.getSession(true)).thenReturn(session); - when(session.getAttribute("clickstream")).thenReturn(new Clickstream()); + + //Set up the behavior of setAttribute to store values in the map + doAnswer(invocation -> { + final String key = invocation.getArgument(0); + final Object value = invocation.getArgument(1); + sessionAttributes.put(key, value); + return null; + }).when(session).setAttribute(anyString(), any()); + + // Set up the behavior of getAttribute to retrieve values from the map + when(session.getAttribute(anyString())).thenAnswer(invocation -> sessionAttributes.get(invocation.getArgument(0))); + + //Set up the behavior of removeAttribute to remove values from the map + doAnswer(invocation -> { + String key = invocation.getArgument(0); + sessionAttributes.remove(key); + return null; + }).when(session).removeAttribute(anyString()); final Language defaultLang = APILocator.getLanguageAPI().getDefaultLanguage(); - host = new SiteDataGen().name(hostName).nextPersisted(); + APILocator.getVersionableAPI().setWorking(host); APILocator.getVersionableAPI().setLive(host); when(request.getParameter("host_id")).thenReturn(host.getIdentifier()); when(request.getAttribute(WebKeys.CURRENT_HOST)).thenReturn(host); when(request.getAttribute("com.dotcms.repackage.org.apache.struts.action.MODULE")).thenReturn(moduleConfig); // Mock setAttribute - Mockito.doAnswer((InvocationOnMock invocation)-> { + doAnswer((InvocationOnMock invocation)-> { String key = invocation.getArgument(0, String.class); Object value = invocation.getArgument(1, Object.class); @@ -181,7 +228,7 @@ public void init() throws DotSecurityException, DotDataException { attributes.put(key, value); } return null; - }).when(request).setAttribute(Mockito.anyString(), Mockito.any()); + }).when(request).setAttribute(anyString(), Mockito.any()); Folder aboutUs = APILocator.getFolderAPI().findFolderByPath(String.format("/%s/",folderName), host, APILocator.systemUser(), false); @@ -456,7 +503,7 @@ public void testRender() throws DotDataException, DotSecurityException { final Contentlet checkin = APILocator.getContentletAPIImpl().checkin(checkout, systemUser, false); final Response response = pageResource .loadJson(request, this.response, pageUri, null, null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); RestUtilTest.verifySuccessResponse(response); @@ -537,7 +584,7 @@ public void testRenderWithContent() throws DotDataException, DotSecurityExceptio final Response response = pageResource .loadJson(request, this.response, pagePath, "PREVIEW_MODE", null, - "1", null); + "1", null, null); RestUtilTest.verifySuccessResponse(response); @@ -582,14 +629,14 @@ public void shouldReturnPageByURLPattern() final Response response = pageResource .render(request, this.response, String.format("%s/text", baseUrl), "PREVIEW_MODE", null, - "1", null); + "1", null, null); RestUtilTest.verifySuccessResponse(response); } /** - * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)} + * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)} * Given Scenario: Create a page with URL Pattern, with a no publish content, and try to get it in ADMIN_MODE * ExpectedResult: Should return a 404 HTTP error * @@ -630,11 +677,11 @@ public void shouldReturn404ForPageWithURLPatternWithNotLIVEContentInAdminMode() pageResource .render(request, this.response, String.format("%s/text", baseUrl), PageMode.ADMIN_MODE.toString(), null, - "1", null); + "1", null, null); } /** - * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)} + * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)} * Given Scenario: Create a page with URL Pattern, with a no publish content, and try to get it in ADMIN_MODE * ExpectedResult: Should return a 404 HTTP error * @@ -675,7 +722,7 @@ public void shouldReturn404ForPageWithURLPatternWithNotLIVEContentInLiveMode() pageResource .render(request, this.response, String.format("%s/text", baseUrl), PageMode.LIVE.toString(), null, - "1", null); + "1", null, null); } /** @@ -731,7 +778,7 @@ public void testNumberContentWithNotDrawTemplate() throws DotDataException, DotS final Response response = pageResource .loadJson(request, this.response, pageUri, null, null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); RestUtilTest.verifySuccessResponse(response); @@ -771,7 +818,7 @@ public void testRenderNotPersonalizationVersion() final Response response = pageResource .render(request, this.response, page.getURI(), modeParam, persona.getIdentifier(), - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); final PageView pageView = (PageView) ((ResponseEntityView) response.getEntity()).getEntity(); PageRenderVerifier.verifyPageView(pageView, pageRenderTest, APILocator.systemUser()); @@ -818,7 +865,7 @@ public void testRenderPersonalizationVersion() final Response response = pageResource .render(request, this.response, page.getURI(), null, persona.getIdentifier(), - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); final PageView pageView = (PageView) ((ResponseEntityView) response.getEntity()).getEntity(); PageRenderVerifier.verifyPageView(pageView, pageRenderTest, user); @@ -827,7 +874,7 @@ public void testRenderPersonalizationVersion() } /*** - * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)} + * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)} * Given Scenario: Create a page with two containers and a content in each of then * ExpectedResult: Should render the containers with the contents, the check it look into the render code the * content div
assertTrue(code.indexOf("data-dot-object=\"contentlet\"") != -1)
@@ -859,22 +906,22 @@ public void testShouldRenderContainers() final Response response = pageResource .render(request, this.response, page.getURI(), "EDIT_MODE", null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); final PageView pageView = (PageView) ((ResponseEntityView) response.getEntity()).getEntity(); PageRenderVerifier.verifyPageView(pageView, pageRenderTest, APILocator.systemUser()); final Collection containers = (Collection) pageView.getContainers(); - assertTrue(containers.size() > 0); + assertFalse(containers.isEmpty()); for (ContainerRendered container : containers) { final Map rendered = container.getRendered(); final Collection codes = rendered.values(); - assertTrue(codes.size() > 0); + assertFalse(codes.isEmpty()); for (final Object code : codes) { - assertTrue(code.toString().indexOf("data-dot-object=\"container\"") != -1); - assertTrue(code.toString().indexOf("data-dot-object=\"contentlet\"") != -1); + assertTrue(code.toString().contains("data-dot-object=\"container\"")); + assertTrue(code.toString().contains("data-dot-object=\"contentlet\"")); } } assertNull(pageView.getViewAs().getPersona()); @@ -882,7 +929,7 @@ public void testShouldRenderContainers() /** - * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)} + * methodToTest {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)} * Given Scenario: Create a page with not LIVE version, then publish the page, and then update the page to crate a * new working version * ExpectedResult: Should return a LIVE attribute to true just in after the page is publish @@ -904,7 +951,7 @@ public void shouldReturnLIVE() Response response = pageResource .render(request, this.response, page.getURI(), PageMode.PREVIEW_MODE.toString(), null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); PageView pageView = (PageView) ((ResponseEntityView) response.getEntity()).getEntity(); assertFalse(pageView.isLive()); @@ -915,7 +962,7 @@ public void shouldReturnLIVE() response = pageResource .render(request, this.response, page.getURI(), PageMode.PREVIEW_MODE.toString(), null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); pageView = (PageView) ((ResponseEntityView) response.getEntity()).getEntity(); assertTrue(pageView.isLive()); @@ -926,7 +973,7 @@ public void shouldReturnLIVE() response = pageResource .render(request, this.response, page.getURI(), PageMode.PREVIEW_MODE.toString(), null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); pageView = (PageView) ((ResponseEntityView) response.getEntity()).getEntity(); assertTrue(pageView.isLive()); @@ -986,7 +1033,7 @@ public void shouldKeepTheAParserContainerContentAfterLayoutSaved() final Response response = pageResource .render(request, this.response, page.getURI(), modeParam, null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); final HTMLPageAssetRendered htmlPageAssetRendered = (HTMLPageAssetRendered) ((ResponseEntityView) response.getEntity()).getEntity(); @@ -1092,7 +1139,7 @@ public void shouldResponseWith() final Response response = pageResource .render(request, this.response, page.getURI(), modeParam, null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); final HTMLPageAssetRendered htmlPageAssetRendered = (HTMLPageAssetRendered) ((ResponseEntityView) response.getEntity()).getEntity(); @@ -1126,7 +1173,7 @@ private PageContainerForm createPageContainerForm(final String containerId, fina /** *
    - *
  • Method to Test: {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)}
  • + *
  • Method to Test: {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)}
  • *
  • Given Scenario: In Edit Mode, test the rest API
  • *
  • Expected Result: Receive the on-number-of-pages data attribute for the contentlet object inside rendered element.
  • *
@@ -1148,7 +1195,7 @@ public void testOnNumberOfPagesDataAttribute_render() throws DotDataException, S final Container container = pageRenderTestOne.getFirstContainer(); final Contentlet testContent = pageRenderTestOne.addContent(container); Response pageResponse = this.pageResource.render(this.request, this.response, pageOne.getURI(), modeParam, null, - String.valueOf(languageId), null); + String.valueOf(languageId), null, null); final HTMLPageAssetRendered htmlPageAssetRendered = (HTMLPageAssetRendered) ((ResponseEntityView) pageResponse.getEntity()).getEntity(); @@ -1160,7 +1207,7 @@ public void testOnNumberOfPagesDataAttribute_render() throws DotDataException, S /** *
    - *
  • Method to Test: {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)}
  • + *
  • Method to Test: {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)}
  • *
  • Given Scenario: The deviceInode is not set as part of the request
  • *
  • Expected Result: The {@link WebKeys#CURRENT_DEVICE} is removed from session
  • *
@@ -1169,14 +1216,14 @@ public void testOnNumberOfPagesDataAttribute_render() throws DotDataException, S public void testCleanUpSessionWhenDeviceInodeIsNull() throws Exception { when(request.getAttribute(com.liferay.portal.util.WebKeys.USER)).thenReturn(user); - pageResource.render(request, response, pagePath, null, null, APILocator.getLanguageAPI().getDefaultLanguage().getLanguage(), null); + pageResource.render(request, response, pagePath, null, null, APILocator.getLanguageAPI().getDefaultLanguage().getLanguage(), null, null); verify(session).removeAttribute(WebKeys.CURRENT_DEVICE); } /** *
    - *
  • Method to Test: {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String)}
  • + *
  • Method to Test: {@link PageResource#render(HttpServletRequest, HttpServletResponse, String, String, String, String, String, String)}
  • *
  • Given Scenario: The deviceInode in the request is blank
  • *
  • Expected Result: The {@link WebKeys#CURRENT_DEVICE} is removed from session
  • *
@@ -1185,8 +1232,263 @@ public void testCleanUpSessionWhenDeviceInodeIsNull() throws Exception { public void testCleanUpSessionWhenDeviceInodeIsBlank() throws Exception { when(request.getAttribute(com.liferay.portal.util.WebKeys.USER)).thenReturn(user); - pageResource.render(request, response, pagePath, null, null, APILocator.getLanguageAPI().getDefaultLanguage().getLanguage(), ""); + pageResource.render(request, response, pagePath, null, null, APILocator.getLanguageAPI().getDefaultLanguage().getLanguage(), "", null); verify(session).removeAttribute(WebKeys.CURRENT_DEVICE); } + + /** + * This is probably the most intricate test in this class. + * It tests the behavior of the page rendering when a contentlet is published in the future. + * This explains the complexity of the test: + * 1. We need to create a page with a container and a contentlet. + * 2. The container has to hold a widget that will render the contentlet in the future using the dotcontent velocity tool. + * 3. The container and the widget must be published. + * 4. Then we need to create a contentlet that will be published in the future. in this case we're going to use a blog + * 5. We need to publish the page and everything else. But the blog. The blog is set to be published in the future using the publishDate property. + * 6. The blog content-type must be prepared indicating what property will be used as the publish date. + * Given scenario: A page with a container and a contentlet is created. The contentlet is published in a future date. + * Expected result: The contentlet should be rendered in the page once we pass the future date as a parameter. + * @throws DotDataException + * @throws DotSecurityException + */ + @Test + public void testRenderWithTimeMachine() + throws DotDataException, DotSecurityException, WebAssetException, JsonProcessingException { + + final TimeZone defaultZone = TimeZone.getDefault(); + try { + final TimeZone utc = TimeZone.getTimeZone("UTC"); + TimeZone.setDefault(utc); + + // Calculate the date relative to today + // Publish Date is 10 days from now + final LocalDateTime relativeDate1 = LocalDateTime.now().plusDays(10); + final Instant relativeInstant1 = relativeDate1.atZone(utc.toZoneId()).toInstant(); + final Date publishDate = Date.from(relativeInstant1); + + // Time Machine Date is 11 days from now + final LocalDateTime relativeDate2 = LocalDateTime.now().plusDays(10).plusHours(1); + final Instant relativeInstant2 = relativeDate2.atZone(utc.toZoneId()).toInstant(); + final Date timeMachineDate = Date.from(relativeInstant2); + + final LocalDateTime relativeDate3 = LocalDateTime.now().plusDays(4); + final Instant relativeInstant3 = relativeDate3.atZone(utc.toZoneId()).toInstant(); + final Date timeMachineDateBefore = Date.from(relativeInstant3); + + //Then we should get the content after publish date + validatePageRendering(PageMode.LIVE, false, null, null); + validatePageRendering(PageMode.PREVIEW_MODE, false, null, null); + validatePageRendering(PageMode.EDIT_MODE, false, null, null); + validatePageRendering(PageMode.ADMIN_MODE, false, null, null); + + validatePageRendering(PageMode.LIVE, true, publishDate, timeMachineDate); + validatePageRendering(PageMode.PREVIEW_MODE, true, publishDate, timeMachineDate); + validatePageRendering(PageMode.EDIT_MODE, true, publishDate, timeMachineDate); + validatePageRendering(PageMode.ADMIN_MODE, true, publishDate, timeMachineDate); + + validatePageRendering(PageMode.LIVE, false, publishDate, timeMachineDateBefore); + validatePageRendering(PageMode.PREVIEW_MODE, false, publishDate, timeMachineDateBefore); + validatePageRendering(PageMode.EDIT_MODE, false, publishDate, timeMachineDateBefore); + validatePageRendering(PageMode.ADMIN_MODE, false, publishDate, timeMachineDateBefore); + } finally { + TimeZone.setDefault(defaultZone); + } + } + + /** + * Widget code that will render the contentlet in the future + * This mirrors the code we ship with the dotCMS this is how we actually render the contentlets in the future + * @param host the host our content is in + * @param contentType the content type of the content we want to render + * @return the widget code + */ + String widgetCode(final Host host, final ContentType contentType) { + return String.format( + "#set($blogs = $dotcontent.pullPerPage(\"+contentType:%s +(conhost:%s conhost:SYSTEM_HOST) +variant:default\", 0, 100, null))\n" + + "#set($resultList = []) \n" + + "#foreach($con in $blogs)\n" + + " #set($resultdoc = {})\n" + + " #set($resultdoc.identifier = ${con.identifier})\n" + + " #set($resultdoc.inode = ${con.inode})\n" + + " #set($resultdoc.title = $!{con.title})\n" + + " #set($notUsedValue = $resultList.add($resultdoc))\n" + + "#end\n" + + "!$dotJSON.put(\"posts\", $resultList)", + contentType.variable(), host.getIdentifier() + ); + } + + /** + * + * @param mode PageMode + * @param expectContentlet true if we expect a contentlet to be rendered, false otherwise + * @param publishDate the publish-date of the contentlet null if the contentlet is needed to be published right away + * @param timeMachineDate the date to be used as time machine + * @throws DotDataException + * @throws DotSecurityException + * @throws WebAssetException + */ + private void validatePageRendering(final PageMode mode, final boolean expectContentlet, final Date publishDate, final Date timeMachineDate) + throws DotDataException, DotSecurityException, WebAssetException, JsonProcessingException { + + final User systemUser = APILocator.getUserAPI().getSystemUser(); + final long languageId = 1L; + final ContentType blogLikeContentType = TestDataUtils.getBlogLikeContentType(); + + final Structure structure = new StructureDataGen().nextPersisted(); + final Container myContainer = new ContainerDataGen() + .withStructure(structure, "") + .friendlyName("container-friendly-name" + System.currentTimeMillis()) + .title("container-title") + .site(host) + .nextPersisted(); + + ContainerDataGen.publish(myContainer); + + final TemplateLayout templateLayout = TemplateLayoutDataGen.get() + .withContainer(myContainer.getIdentifier()) + .next(); + + final Template newTemplate = new TemplateDataGen() + .drawedBody(templateLayout) + .withContainer(myContainer.getIdentifier()) + .nextPersisted(); + + final VersionableAPI versionableAPI = APILocator.getVersionableAPI(); + versionableAPI.setWorking(newTemplate); + versionableAPI.setLive(newTemplate); + + final String myFolderName = "folder-" + System.currentTimeMillis(); + final Folder myFolder = new FolderDataGen().name(myFolderName).site(host).nextPersisted(); + final String myPageName = "my-testPage-" + System.currentTimeMillis(); + final HTMLPageAsset myPage = new HTMLPageDataGen(myFolder, newTemplate) + .languageId(languageId) + .pageURL(myPageName) + .title(myPageName) + .nextPersisted(); + + final ContentletAPI contentletAPI = APILocator.getContentletAPI(); + contentletAPI.publish(myPage, systemUser, false); + // These are the blogs that will be shown in the widget + // if it's published then it'll show immediately otherwise it'll show in the future + // if it's set to show then we need to pass the time machine date to show it + //Our blog content type has to have a publishDate field set otherwise it will never make it properly into the index + assertNotNull(blogLikeContentType.publishDateVar()); + + final ContentletDataGen blogsDataGen = new ContentletDataGen(blogLikeContentType.id()) + .languageId(languageId) + .host(host) + .setProperty("title", "myBlogTest") + .setProperty("body", TestDataUtils.BLOCK_EDITOR_DUMMY_CONTENT) + .setPolicy(IndexPolicy.WAIT_FOR) + .languageId(languageId) + .setProperty(Contentlet.IS_TEST_MODE, true); + + if (null != publishDate) { + blogsDataGen.setProperty("publishDate", publishDate); // Set the publish-date in the future + } + final Contentlet blog = blogsDataGen.nextPersisted(); + assertNotNull(blog.getIdentifier()); + if (null != publishDate) { + assertFalse(blog.isLive()); + } + + //Here we're testing the time machine query will return contentlets that are published in the future + if(null != timeMachineDate ) { + final String query = String.format( + "+contentType:%s +(conhost:%s conhost:SYSTEM_HOST) +variant:default +live:true", + blogLikeContentType.variable(), host.getIdentifier()); + final PaginatedContentList pull = ContentUtils.pullPerPage(query, 0, 10, + null, APILocator.systemUser(), String.valueOf(timeMachineDate.getTime())); + if(expectContentlet) { + assertFalse("We should get items from using the time-machine date", pull.isEmpty()); + } else { + assertTrue("We should not get items from using the time-machine date", pull.isEmpty()); + } + } + + // Create a widget to show the blog + //The widget hold the code that calls the $dotcontent.pullPerPage view tool which takes into consideration the tm date + //in a nutshell, the widget will show the blog if the tm date is greater than the publish date + //This is how we accomplish the time machine feature + final ContentType widgetLikeContentType = TestDataUtils.getWidgetLikeContentType(()-> widgetCode(host, blogLikeContentType)); + final Contentlet myWidget = new ContentletDataGen(widgetLikeContentType) + .languageId(languageId) + .host(host) + .setProperty("widgetTitle", "myWidgetThatCallsDotContent#pullPerPageSoThatTimeMachineWorks") + .setProperty("code","meh.") + .nextPersisted(); + ContentletDataGen.publish(myWidget); + + final MultiTreeAPI multiTreeAPI = APILocator.getMultiTreeAPI(); + final MultiTree multiTree = new MultiTree(myPage.getIdentifier(), + myContainer.getIdentifier(), myWidget.getIdentifier(), "1", 1); + multiTreeAPI.saveMultiTree(multiTree); + + when(request.getAttribute(WebKeys.HTMLPAGE_LANGUAGE)).thenReturn( + String.valueOf(languageId)); + + String futureIso8601 = null; + if (null != timeMachineDate) { + // Convert the date to ISO 8601 format if necessary + futureIso8601 = timeMachineDate.toInstant().toString(); + } + + HttpServletResponseThreadLocal.INSTANCE.setResponse(this.response); + HttpServletRequestThreadLocal.INSTANCE.setRequest(this.request); + + //This param is required to be live to behave correctly when building the query + when(request.getAttribute(WebKeys.PAGE_MODE_PARAMETER)).thenReturn(PageMode.LIVE); + when(request.getAttribute(com.liferay.portal.util.WebKeys.USER)).thenReturn(systemUser); + + final String myPagePath = String.format("/%s/%s", myFolderName, myPageName); + final Response myResponse = pageResource + .loadJson(this.request, this.response, myPagePath, mode.name(), null, + String.valueOf(languageId), null, futureIso8601); + + RestUtilTest.verifySuccessResponse(myResponse); + + final PageView pageView = (PageView) ((ResponseEntityView) myResponse.getEntity()).getEntity(); + final ObjectMapper objectMapper = new ObjectMapper(); + final String json = objectMapper.writeValueAsString(pageView); + final JsonNode node = objectMapper.readTree(json); + final Optional widgetCodeJSON = findNode(node, "widgetCodeJSON"); + + if(widgetCodeJSON.isPresent()){ + final JsonNode posts = widgetCodeJSON.get().get("posts"); + if(expectContentlet){ + assertFalse(posts.isEmpty()); + } else { + assertTrue(posts.isEmpty()); + } + } else { + assertFalse(expectContentlet); + } + } + + /** + * Utility method to find a node in a JSON tree + * @param currentNode + * @param nodeName + * @return + */ + public static Optional findNode(final JsonNode currentNode, final String nodeName) { + if (currentNode.has(nodeName)) { + return Optional.of(currentNode.get(nodeName)); // Node found in the current level + } + + // if the current node is an object or array, iterate over its children + Iterator elements = currentNode.elements(); + while (elements.hasNext()) { + JsonNode child = elements.next(); + Optional result = findNode(child, nodeName); // recursive call + if (result.isPresent()) { + return result; + } + } + + return Optional.empty(); // Node not found + } + }