blob: 68d518a4c86e56244602c5915724b2eab74aa404 [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.lang.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.compiler.RuntimeFunction;
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;
/**
* Runtime support for including resources in a HTL script through {@code data-sly-resource}.
*/
@Component(
service = RuntimeExtension.class,
property = {
RuntimeExtension.NAME + "=" + RuntimeFunction.RESOURCE
}
)
public class ResourceRuntimeExtension implements RuntimeExtension {
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_REPLACE_SELECTORS = "replaceSelectors";
private static final String OPTION_REQUEST_ATTRIBUTES = "requestAttributes";
@Override
public Object call(final RenderContext renderContext, Object... arguments) {
ExtensionUtils.checkArgumentCount(RuntimeFunction.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();
String resourceType = runtimeObjectModel.toString(getAndRemoveOption(opts, OPTION_RESOURCE_TYPE));
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
if (pathObj instanceof Resource) {
Resource includedResource = (Resource) pathObj;
Map<String, String> dispatcherOptionsMap = handleSelectors(request, new LinkedHashSet<String>(), opts, runtimeObjectModel);
String dispatcherOptions = createDispatcherOptions(dispatcherOptionsMap);
includeResource(bindings, printWriter, includedResource, dispatcherOptions, resourceType);
} 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) {
Map<String, String> dispatcherOptionsMap =
handleSelectors(request, new LinkedHashSet<String>(), opts, runtimeObjectModel);
String dispatcherOptions = createDispatcherOptions(dispatcherOptionsMap);
includeResource(bindings, printWriter, includedResource, dispatcherOptions, resourceType);
} else {
// analyse path and decompose potential selectors from the path
pathInfo = new PathInfo(includePath);
Map<String, String> dispatcherOptionsMap = handleSelectors(request, pathInfo.selectors, opts, runtimeObjectModel);
String dispatcherOptions = createDispatcherOptions(dispatcherOptionsMap);
includeResource(bindings, printWriter, pathInfo.path, dispatcherOptions, resourceType);
}
} else {
// use the current resource
Map<String, String> dispatcherOptionsMap = handleSelectors(request, new LinkedHashSet<String>(), opts, runtimeObjectModel);
String dispatcherOptions = createDispatcherOptions(dispatcherOptionsMap);
includeResource(bindings, printWriter, request.getResource(), dispatcherOptions, resourceType);
}
}
ExtensionUtils.setRequestAttributes(request, originalAttributes);
return writer.toString();
}
private Map<String, String> handleSelectors(SlingHttpServletRequest request, Set<String> selectors, Map<String, Object> options,
RuntimeObjectModel runtimeObjectModel) {
if (selectors.isEmpty()) {
selectors.addAll(Arrays.asList(request.getRequestPathInfo().getSelectors()));
}
Map<String, String> dispatcherOptionsMap = new HashMap<>();
dispatcherOptionsMap.put(OPTION_ADD_SELECTORS, getSelectorString(selectors));
dispatcherOptionsMap.put(OPTION_REPLACE_SELECTORS, " ");
if (options.containsKey(OPTION_SELECTORS)) {
Object selectorsObject = getAndRemoveOption(options, OPTION_SELECTORS);
selectors.clear();
addSelectors(selectors, selectorsObject, runtimeObjectModel);
dispatcherOptionsMap.put(OPTION_ADD_SELECTORS, getSelectorString(selectors));
dispatcherOptionsMap.put(OPTION_REPLACE_SELECTORS, " ");
}
if (options.containsKey(OPTION_ADD_SELECTORS)) {
Object selectorsObject = getAndRemoveOption(options, OPTION_ADD_SELECTORS);
addSelectors(selectors, selectorsObject, runtimeObjectModel);
dispatcherOptionsMap.put(OPTION_ADD_SELECTORS, getSelectorString(selectors));
dispatcherOptionsMap.put(OPTION_REPLACE_SELECTORS, " ");
}
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)) {
dispatcherOptionsMap.put(OPTION_REPLACE_SELECTORS, " ");
} else {
dispatcherOptionsMap.put(OPTION_ADD_SELECTORS, getSelectorString(selectors));
dispatcherOptionsMap.put(OPTION_REPLACE_SELECTORS, " ");
}
}
return dispatcherOptionsMap;
}
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 createDispatcherOptions(Map<String, String> options) {
if (options == null || options.isEmpty()) {
return null;
}
StringBuilder buffer = new StringBuilder();
boolean hasPreceding = false;
for (Map.Entry<String, String> option : options.entrySet()) {
if (hasPreceding) {
buffer.append(", ");
}
String key = option.getKey();
buffer.append(key).append("=");
String strVal = option.getValue();
if (strVal == null) {
strVal = "";
}
buffer.append(strVal);
hasPreceding = true;
}
return buffer.toString();
}
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++;
}
}
return sb.toString();
}
private void includeResource(final Bindings bindings, PrintWriter out, String path, String dispatcherOptions, String resourceType) {
if (StringUtils.isEmpty(path)) {
throw new SightlyException("Resource path cannot be empty");
} else {
SlingHttpServletRequest request = BindingsUtils.getRequest(bindings);
Resource includeRes = request.getResourceResolver().resolve(path);
if (ResourceUtil.isNonExistingResource(includeRes)) {
includeRes = new SyntheticResource(request.getResourceResolver(), path, resourceType);
}
includeResource(bindings, out, includeRes, dispatcherOptions, resourceType);
}
}
private void includeResource(final Bindings bindings, PrintWriter out, Resource includeRes, String dispatcherOptions, String resourceType) {
if (includeRes == null) {
throw new SightlyException("Resource cannot be null");
} else {
SlingHttpServletResponse customResponse = new PrintWriterResponseWrapper(out, BindingsUtils.getResponse(bindings));
SlingHttpServletRequest request = BindingsUtils.getRequest(bindings);
RequestDispatcherOptions opts = new RequestDispatcherOptions(dispatcherOptions);
if (StringUtils.isNotEmpty(resourceType)) {
opts.setForceResourceType(resourceType);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(includeRes, opts);
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;
}
}
}