FilteringContentProcessor.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2023 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.content;
import static com.adobe.cq.export.json.ExporterConstants.SLING_MODEL_EXTENSION;
import static com.adobe.cq.export.json.ExporterConstants.SLING_MODEL_SELECTOR;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
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.ConfigurationPolicy;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.propertytypes.ServiceRanking;
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 io.wcm.siteapi.processor.JsonObjectProcessor;
import io.wcm.siteapi.processor.Processor;
import io.wcm.siteapi.processor.ProcessorRequestContext;
import io.wcm.siteapi.processor.util.JsonObjectMapper;
/**
* Filtered view of page content.
* Generates subset of "model.json" version view of the page.
*
* <p>
* PLEASE NOTE: This implementation requires Sling Engine 2.11 or higher (which is not part of AEM 6.5 atm).
* Otherwise an error "SlingHttpServletResponse not of correct type" is thrown, see SLING-11569.
* </p>
*/
@Designate(ocd = FilteringContentProcessor.Config.class, factory = true)
@Component(service = Processor.class, configurationPolicy = ConfigurationPolicy.REQUIRE, property = {
"webconsole.configurationFactory.nameHint={suffix}"
})
@ServiceRanking(-500)
public class FilteringContentProcessor implements JsonObjectProcessor<Map<String, Object>> {
@ObjectClassDefinition(
name = "wcm.io Site API Filtering Content Processor",
description = "Provides a filtered view of model.json content of a page. This implementation requires Sling Engine 2.11 or higher.")
@interface Config {
@AttributeDefinition(
name = "Suffix",
description = "Suffix for this processor.",
required = true)
String suffix();
@AttributeDefinition(
name = "Include Paths",
description = "List of paths to include in the response. Paths are absolute paths relative to the /jcr:content node of the page.")
String[] includePaths() default {};
@AttributeDefinition(
name = "Exclude Paths",
description = "List of paths to exclude from the response. Paths are absolute paths relative to the /jcr:content node of the page.")
String[] excludePaths() default {};
}
@Reference
private JsonObjectMapper jsonObjectMapper;
private ModelJsonPathFilter modelJsonPathFilter;
private final Logger log = LoggerFactory.getLogger(FilteringContentProcessor.class);
@Activate
private void activate(Config config) {
this.modelJsonPathFilter = new ModelJsonPathFilter(config.includePaths(), config.excludePaths());
}
@Override
public @Nullable Map<String, Object> process(@NotNull ProcessorRequestContext context) {
String modelJsonUri = context.getPage().getPath() + "." + SLING_MODEL_SELECTOR + "." + SLING_MODEL_EXTENSION;
try {
// get model.json output for current page
String modelJson = getRequestOutput(context.getRequest(), modelJsonUri);
if (modelJson == null) {
return null;
}
// filter JSON
Map<String, Object> json = jsonObjectMapper.parseToMap(modelJson);
json = modelJsonPathFilter.filter(json);
return json;
}
catch (IOException | ServletException ex) {
log.error("Unable to get content from {}", modelJsonUri, ex);
return null;
}
}
/**
* Gets request output from given Sling URI using request dispatcher and a synthetic response.
* Assumes returned content is UTF-8.
* @param request Request
* @param uri URI to call
* @return String output of request or null if invalid
*/
private @Nullable String getRequestOutput(SlingHttpServletRequest request, String uri) throws IOException, ServletException {
/*
* use dynamic reflection proxy to capture response output.
* it would be nicer to use https://sling.apache.org/apidocs/sling12/org/apache/sling/api/request/builder/Builders.html
* but this is only available in Sling API 2.24, and AEM 6.5 ships with Sling API 2.22.
*/
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(bos, StandardCharsets.UTF_8);
PrintWriter printWriter = new PrintWriter(writer)) {
SlingHttpServletResponse responseProxy = (SlingHttpServletResponse)Proxy.newProxyInstance(
SlingHttpServletResponse.class.getClassLoader(),
new Class[] { SlingHttpServletResponse.class },
(proxy, method, methodArgs) -> {
if (StringUtils.equals(method.getName(), "getWriter")) {
return printWriter;
}
return null;
});
RequestDispatcher requestDispatcher = request.getRequestDispatcher(uri);
if (requestDispatcher != null) {
requestDispatcher.include(request, responseProxy);
printWriter.flush();
return bos.toString(StandardCharsets.UTF_8);
}
else {
return null;
}
}
}
}