SiteApiServlet.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2022 wcm.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package io.wcm.siteapi.processor.impl;

import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;

import java.io.IOException;

import javax.servlet.Servlet;
import javax.servlet.ServletException;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.PageManagerFactory;
import com.day.cq.wcm.api.WCMMode;

import io.wcm.siteapi.processor.JsonObjectProcessor;
import io.wcm.siteapi.processor.Processor;
import io.wcm.siteapi.processor.ProcessorManager;
import io.wcm.siteapi.processor.ProcessorRequestContext;
import io.wcm.siteapi.processor.SlingHttpServletProcessor;
import io.wcm.siteapi.processor.url.JsonSuffix;
import io.wcm.siteapi.processor.url.SiteApiConfiguration;
import io.wcm.siteapi.processor.util.JsonObjectMapper;

/**
 * Accepts all Site API calls and redirects processing to a {@link SlingHttpServletProcessor} based on the suffix.
 */
@Designate(ocd = SiteApiServlet.Config.class)
@Component(service = { Servlet.class, SiteApiConfiguration.class })
@SlingServletResourceTypes(
    resourceTypes = NameConstants.NT_PAGE,
    methods = HttpConstants.METHOD_GET)
@SuppressWarnings("squid:S1948") // servlet is not serialized
public class SiteApiServlet extends SlingSafeMethodsServlet implements SiteApiConfiguration {

  @ObjectClassDefinition(
      name = "wcm.io Site API Servlet",
      description = "Configures the request mapping of Site API Servlet.")
  @SuppressWarnings("java:S100")
  @interface Config {

    @AttributeDefinition(
        name = "Selector+Version",
        description = "Define Sling selector for matching Site API request for current version with syntax '<selector>.<version>'.")
    String sling_servlet_selectors() default "site";

    @AttributeDefinition(
        name = "Extension",
        description = "Extension that is used for servlet.")
    String sling_servlet_extensions() default "api";

  }

  static final String JSON_CONTENT_TYPE = "application/json;charset=UTF-8";

  private static final long serialVersionUID = 1L;
  private static final Logger log = LoggerFactory.getLogger(SiteApiServlet.class);

  @Reference
  private PageManagerFactory pageManagerFactory;
  @Reference
  private ProcessorManager processorManager;
  @Reference
  private JsonObjectMapper jsonObjectMapper;

  private String selector;
  private String extension;

  @Activate
  private void activate(Config config) {
    this.selector = config.sling_servlet_selectors();
    this.extension = config.sling_servlet_extensions();
  }


  @Override
  protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response)
      throws ServletException, IOException {
    Resource resource = request.getResource();

    // force disabled mode for proper media/link handling
    WCMMode.DISABLED.toRequest(request);

    // ensure selector matches exactly (no additional selectors allowed)
    if (!StringUtils.equals(request.getRequestPathInfo().getSelectorString(), this.selector)) {
      response.sendError(SC_NOT_FOUND);
      return;
    }

    // get processor matching given suffix
    Processor processor = null;
    JsonSuffix suffix = JsonSuffix.parse(request.getRequestPathInfo().getSuffix());
    if (suffix != null) {
      processor = processorManager.getMatching(suffix.getSuffix(), resource);
    }
    if (suffix == null || processor == null) {
      response.sendError(SC_NOT_FOUND);
      return;
    }

    // ensure valid page
    PageManager pageManager = pageManagerFactory.getPageManager(request.getResourceResolver());
    Page currentPage = getCurrentPage(resource, pageManager);
    if (currentPage == null || !ensurePageHasContent(currentPage)) {
      response.sendError(SC_NOT_FOUND);
      return;
    }

    // handle request using processor
    ProcessorRequestContext context = new ProcessorRequestContextImpl(suffix.getSuffix(), suffix.getSuffixExtension(),
        request, pageManager, currentPage);
    if (processor instanceof JsonObjectProcessor) {
      processJsonObject((JsonObjectProcessor)processor, context, response);
    }
    else if (processor instanceof SlingHttpServletProcessor) {
      ((SlingHttpServletProcessor)processor).process(context, response);
    }
    else {
      throw new ServletException("Processor does not implement JsonObjectProcessor or SlingHttpServletResponse: " + processor);
    }
  }

  private void processJsonObject(JsonObjectProcessor<?> processor, ProcessorRequestContext context,
      SlingHttpServletResponse response) throws IOException {
    Object result = processor.process(context);
    if (result == null) {
      response.sendError(SC_NOT_FOUND);
    }
    else {
      response.setContentType(JSON_CONTENT_TYPE);
      String jsonString = jsonObjectMapper.toJsonString(result);
      response.getWriter().write(jsonString);
    }
  }

  private @Nullable Page getCurrentPage(@NotNull Resource resource, @NotNull PageManager pageManager) {
    Page page = pageManager.getContainingPage(resource);
    if (page == null) {
      log.debug("No page found for given resource: {}", resource.getPath());
    }
    return page;
  }

  private boolean ensurePageHasContent(@NotNull Page page) {
    boolean hasContent = page.hasContent();
    if (!hasContent) {
      log.debug("Ignoring page without jcr:content node: {}", page.getPath());
    }
    return hasContent;
  }

  @Override
  public @NotNull String getSelector() {
    return selector;
  }

  @Override
  public @NotNull String getExtension() {
    return extension;
  }

  @Override
  public @NotNull String getContextPath() {
    return getServletContext().getContextPath();
  }

}