blob: 3a71e136bd98dff81c2361214c89c15ebdd8dd4c [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
******************************************************************************/
package org.apache.sling.scripting.sightly.impl.engine.extension;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.script.Bindings;
import javax.servlet.RequestDispatcher;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestDispatcherOptions;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.SyntheticResource;
import org.apache.sling.scripting.sightly.SightlyException;
import org.apache.sling.scripting.sightly.extension.RuntimeExtension;
import org.apache.sling.scripting.sightly.impl.utils.BindingsUtils;
import org.apache.sling.scripting.sightly.render.RenderContext;
import org.apache.sling.scripting.sightly.render.RuntimeObjectModel;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Runtime support for including resources in a HTL script through {@code data-sly-resource}.
*/
@Component(
service = RuntimeExtension.class,
property = {
RuntimeExtension.NAME + "=" + RuntimeExtension.RESOURCE
}
)
public class ResourceRuntimeExtension implements RuntimeExtension {
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceRuntimeExtension.class);
private static final String OPTION_RESOURCE_TYPE = "resourceType";
private static final String OPTION_PATH = "path";
private static final String OPTION_PREPEND_PATH = "prependPath";
private static final String OPTION_APPEND_PATH = "appendPath";
private static final String OPTION_SELECTORS = "selectors";
private static final String OPTION_REMOVE_SELECTORS = "removeSelectors";
private static final String OPTION_ADD_SELECTORS = "addSelectors";
private static final String OPTION_REQUEST_ATTRIBUTES = "requestAttributes";
@Override
public Object call(final RenderContext renderContext, Object... arguments) {
ExtensionUtils.checkArgumentCount(RuntimeExtension.RESOURCE, arguments, 2);
return provideResource(renderContext, arguments[0], (Map<String, Object>) arguments[1]);
}
private String provideResource(final RenderContext renderContext, Object pathObj, Map<String, Object> options) {
Map<String, Object> opts = new HashMap<>(options);
final Bindings bindings = renderContext.getBindings();
SlingHttpServletRequest request = BindingsUtils.getRequest(bindings);
Map originalAttributes = ExtensionUtils.setRequestAttributes(request, (Map)options.remove(OPTION_REQUEST_ATTRIBUTES));
RuntimeObjectModel runtimeObjectModel = renderContext.getObjectModel();
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
if (pathObj instanceof Resource) {
Resource includedResource = (Resource) pathObj;
RequestDispatcherOptions requestDispatcherOptions = handleDispatcherOptions(request, new LinkedHashSet<String>(), opts, runtimeObjectModel);
includeResource(request, bindings, printWriter, includedResource, requestDispatcherOptions);
} else {
String includePath = runtimeObjectModel.toString(pathObj);
// build path completely
includePath = buildPath(includePath, options, request.getResource());
if (includePath != null) {
// check if path identifies an existing resource
Resource includedResource = request.getResourceResolver().getResource(includePath);
PathInfo pathInfo;
if (includedResource != null) {
RequestDispatcherOptions requestDispatcherOptions =
handleDispatcherOptions(request, new LinkedHashSet<String>(), opts, runtimeObjectModel);
includeResource(request, bindings, printWriter, includedResource, requestDispatcherOptions);
} else {
// analyse path and decompose potential selectors from the path
pathInfo = new PathInfo(includePath);
RequestDispatcherOptions requestDispatcherOptions = handleDispatcherOptions(request, pathInfo.selectors, opts, runtimeObjectModel);
includeResource(request, bindings, printWriter, pathInfo.path, requestDispatcherOptions);
}
} else {
// use the current resource
RequestDispatcherOptions requestDispatcherOptions = handleDispatcherOptions(request, new LinkedHashSet<String>(), opts, runtimeObjectModel);
includeResource(request, bindings, printWriter, request.getResource(), requestDispatcherOptions);
}
}
ExtensionUtils.setRequestAttributes(request, originalAttributes);
return writer.toString();
}
private RequestDispatcherOptions handleDispatcherOptions(SlingHttpServletRequest request, Set<String> selectors, Map<String, Object> options,
RuntimeObjectModel runtimeObjectModel) {
RequestDispatcherOptions requestDispatcherOptions = new RequestDispatcherOptions();
if (selectors.isEmpty()) {
selectors.addAll(Arrays.asList(request.getRequestPathInfo().getSelectors()));
}
requestDispatcherOptions.setAddSelectors(getSelectorString(selectors));
requestDispatcherOptions.setReplaceSelectors("");
if (options.containsKey(OPTION_SELECTORS)) {
Object selectorsObject = getAndRemoveOption(options, OPTION_SELECTORS);
selectors.clear();
addSelectors(selectors, selectorsObject, runtimeObjectModel);
requestDispatcherOptions.setAddSelectors(getSelectorString(selectors));
requestDispatcherOptions.setReplaceSelectors("");
}
if (options.containsKey(OPTION_ADD_SELECTORS)) {
Object selectorsObject = getAndRemoveOption(options, OPTION_ADD_SELECTORS);
addSelectors(selectors, selectorsObject, runtimeObjectModel);
requestDispatcherOptions.setAddSelectors(getSelectorString(selectors));
requestDispatcherOptions.setReplaceSelectors("");
}
if (options.containsKey(OPTION_REMOVE_SELECTORS)) {
Object selectorsObject = getAndRemoveOption(options, OPTION_REMOVE_SELECTORS);
if (selectorsObject instanceof String) {
String selectorString = (String) selectorsObject;
String[] parts = selectorString.split("\\.");
for (String s : parts) {
selectors.remove(s);
}
} else if (selectorsObject instanceof Object[]) {
for (Object s : (Object[]) selectorsObject) {
String selector = runtimeObjectModel.toString(s);
if (StringUtils.isNotEmpty(selector)) {
selectors.remove(selector);
}
}
} else if (selectorsObject == null) {
selectors.clear();
}
String selectorString = getSelectorString(selectors);
if (StringUtils.isEmpty(selectorString)) {
requestDispatcherOptions.setReplaceSelectors("");
} else {
requestDispatcherOptions.setAddSelectors(getSelectorString(selectors));
requestDispatcherOptions.setReplaceSelectors("");
}
}
if (options.containsKey(OPTION_RESOURCE_TYPE)) {
String resourceType = runtimeObjectModel.toString(getAndRemoveOption(options, OPTION_RESOURCE_TYPE));
if (StringUtils.isNotEmpty(resourceType)) {
requestDispatcherOptions.setForceResourceType(resourceType);
}
}
return requestDispatcherOptions;
}
private void addSelectors(Set<String> selectors, Object selectorsObject, RuntimeObjectModel runtimeObjectModel) {
if (selectorsObject instanceof String) {
String selectorString = (String) selectorsObject;
String[] parts = selectorString.split("\\.");
selectors.addAll(Arrays.asList(parts));
} else if (selectorsObject instanceof Object[]) {
for (Object s : (Object[]) selectorsObject) {
String selector = runtimeObjectModel.toString(s);
if (StringUtils.isNotEmpty(selector)) {
selectors.add(selector);
}
}
}
}
private String buildPath(String path, Map<String, Object> options, Resource currentResource) {
String prependPath = getOption(OPTION_PREPEND_PATH, options, StringUtils.EMPTY);
if (prependPath == null) {
prependPath = StringUtils.EMPTY;
}
if (StringUtils.isNotEmpty(prependPath)) {
if (!prependPath.startsWith("/")) {
prependPath = "/" + prependPath;
}
if (!prependPath.endsWith("/")) {
prependPath += "/";
}
}
path = getOption(OPTION_PATH, options, StringUtils.isNotEmpty(path) ? path : StringUtils.EMPTY);
String appendPath = getOption(OPTION_APPEND_PATH, options, StringUtils.EMPTY);
if (appendPath == null) {
appendPath = StringUtils.EMPTY;
}
if (StringUtils.isNotEmpty(appendPath)) {
if (!appendPath.startsWith("/")) {
appendPath = "/" + appendPath;
}
}
String finalPath = prependPath + path + appendPath;
if (!finalPath.startsWith("/")) {
finalPath = currentResource.getPath() + "/" + finalPath;
}
return ResourceUtil.normalize(finalPath);
}
private String getOption(String option, Map<String, Object> options, String defaultValue) {
if (options.containsKey(option)) {
return (String) options.get(option);
}
return defaultValue;
}
private Object getAndRemoveOption(Map<String, Object> options, String property) {
return options.remove(property);
}
private String getSelectorString(Set<String> selectors) {
StringBuilder sb = new StringBuilder();
int i = 0;
for (String s : selectors) {
sb.append(s);
if (i < selectors.size() - 1) {
sb.append(".");
i++;
}
}
String selectorString = sb.toString();
return StringUtils.isNotEmpty(selectorString) ? selectorString : null;
}
private void includeResource(SlingHttpServletRequest request, final Bindings bindings, PrintWriter out, String path,
RequestDispatcherOptions requestDispatcherOptions) {
if (StringUtils.isEmpty(path)) {
throw new SightlyException("Resource path cannot be empty");
} else {
Resource includeRes = request.getResourceResolver().resolve(path);
if (ResourceUtil.isNonExistingResource(includeRes)) {
String resourceType = request.getResource().getResourceType();
if (requestDispatcherOptions.containsKey(RequestDispatcherOptions.OPT_FORCE_RESOURCE_TYPE)) {
resourceType = requestDispatcherOptions.getForceResourceType();
}
includeRes = new SyntheticResource(request.getResourceResolver(), path, resourceType);
}
includeResource(request, bindings, out, includeRes, requestDispatcherOptions);
}
}
private void includeResource(SlingHttpServletRequest request, final Bindings bindings, PrintWriter out, Resource includeRes,
RequestDispatcherOptions requestDispatcherOptions) {
if (includeRes == null) {
throw new SightlyException("Resource cannot be null");
} else {
if (request.getResource().getPath().equals(includeRes.getPath())) {
String requestSelectorString = request.getRequestPathInfo().getSelectorString();
String requestDispatcherAddSelectors = requestDispatcherOptions.getAddSelectors();
if (
(requestSelectorString == null ? requestDispatcherAddSelectors == null : requestSelectorString.equals(requestDispatcherAddSelectors)) &&
StringUtils.EMPTY.equals(requestDispatcherOptions.getReplaceSelectors()) &&
(requestDispatcherOptions.getForceResourceType() == null || requestDispatcherOptions.getForceResourceType().equals(request.getResource().getResourceType()))
) {
LOGGER.warn("Will not include resource {} since this will lead to a {} exception.", includeRes.getPath(), "org.apache.sling.api.request.RecursionTooDeepException");
return;
}
}
SlingHttpServletResponse customResponse = new PrintWriterResponseWrapper(out, BindingsUtils.getResponse(bindings));
RequestDispatcher dispatcher = request.getRequestDispatcher(includeRes, requestDispatcherOptions);
try {
if (dispatcher != null) {
dispatcher.include(request, customResponse);
} else {
throw new SightlyException("Failed to include resource " + includeRes.getPath());
}
} catch (Exception e) {
throw new SightlyException("Failed to include resource " + includeRes.getPath(), e);
}
}
}
private class PathInfo {
private String path;
private Set<String> selectors;
PathInfo(String path) {
selectors = getSelectorsFromPath(path);
if (selectors.isEmpty()) {
this.path = path;
} else {
String selectorString = getSelectorString(selectors);
this.path = path.replace("." + selectorString, "");
}
}
private Set<String> getSelectorsFromPath(String path) {
Set<String> selectors = new LinkedHashSet<>();
if (path != null) {
String processingPath = path;
int lastSlashPos = path.lastIndexOf('/');
if (lastSlashPos > -1) {
processingPath = path.substring(lastSlashPos + 1, path.length());
}
int dotPos = processingPath.indexOf('.');
if (dotPos > -1) {
int lastDotPos = processingPath.lastIndexOf('.');
// We're expecting selectors only when an extension is also present. If there's
// one dot it means we only have the extension
if (lastDotPos > dotPos) {
String selectorString = processingPath.substring(dotPos + 1, lastDotPos);
String[] selectorParts = selectorString.split("\\.");
selectors.addAll(Arrays.asList(selectorParts));
}
}
}
return selectors;
}
}
}