blob: 298380af1340b9312e8641c721ed01fd34aad522 [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 SF 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.hc.support.impl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.jcr.Session;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.script.SimpleScriptContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.hc.api.FormattingResultLog;
import org.apache.felix.hc.api.HealthCheck;
import org.apache.felix.hc.api.Result;
import org.apache.felix.hc.api.ResultLog;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
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.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;
/**
* {@link HealthCheck} that runs an arbitrary script.
*
* @deprecated for SLING-11445 - use the equivalent functionality from the org.apache.felix.healthcheck.generalchecks bundle instead
*/
@Component(service = HealthCheck.class, name = "org.apache.sling.hc.support.ScriptedHealthCheck", configurationPolicy = ConfigurationPolicy.REQUIRE)
@Designate(ocd = ScriptedHealthCheck.Config.class, factory = true)
@Deprecated
public class ScriptedHealthCheck implements HealthCheck {
private static final Logger LOG = LoggerFactory.getLogger(ScriptedHealthCheck.class);
public static final String HC_LABEL = "Health Check: Sling Script (deprecated)";
public static final String JCR_FILE_URL_PREFIX = "jcr:";
private static final String JCR_CONTENT = "/jcr:content";
@ObjectClassDefinition(name = HC_LABEL, description = "NOTE: This Sling pendant of org.apache.felix.hc.generalchecks.ScriptedHealthCheck allows to use scriptUrls with prefix 'jcr:' and has the additional bindings 'resourceResolver' and 'session'. "
+ "Runs an arbitrary script in given scriping language (via javax.script). "
+ "The script has the following default bindings available: 'log', 'scriptHelper', 'bundleContext', 'resourceResolver' and 'session'. "
+ "'log' is an instance of org.apache.felix.hc.api.FormattingResultLog and is used to define the result of the HC. "
+ "'scriptHelper.getService(classObj)' can be used as shortcut to retrieve a service."
+ "'scriptHelper.getServices(classObj, filter)' used to retrieve multiple services for a class using given filter. "
+ "For all services retrieved via scriptHelper, unget() is called automatically at the end of the script execution."
+ "'bundleContext' is available for advanced use cases. The script does not need to return any value, but if it does and it is "
+ "a org.apache.felix.hc.api.Result, that result and entries in 'log' are combined then).")
@interface Config {
@AttributeDefinition(name = "Name", description = "Name of this health check.")
String hc_name() default "Scripted Health Check"; // NOSONAR
@AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.")
String[] hc_tags() default {}; // NOSONAR
@AttributeDefinition(name = "Language", description = "The language the script is written in. To use e.g. 'groovy', ensure osgi bundle 'groovy-jsr223' is available.")
String language() default "groovy";
@AttributeDefinition(name = "Script", description = "The script itself (either use 'script' or 'scriptUrl').")
String script() default "log.info('ok'); log.warn('not so good'); log.critical('bad') // minimal example";
@AttributeDefinition(name = "Script Url", description = "Url to the script to be used as alternative source (either use 'script' or 'scriptUrl').")
String scriptUrl() default "";
@AttributeDefinition
String webconsole_configurationFactory_nameHint() default "Scripted HC (deprecated): {hc.name} (tags: {hc.tags}) {scriptUrl} language: {language}"; // NOSONAR
}
private String language;
private String script;
private String scriptUrl;
private BundleContext bundleContext;
@Reference
private ScriptEngineManager scriptEngineManager;
private ScriptHelper scriptHelper = new ScriptHelper();
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Activate
protected void activate(BundleContext context, Config config) {
this.bundleContext = context;
this.language = config.language().toLowerCase();
this.script = config.script();
this.scriptUrl = config.scriptUrl();
if (StringUtils.isNotBlank(script) && StringUtils.isNotBlank(scriptUrl)) {
LOG.info("Both 'script' and 'scriptUrl' (={}) are configured, ignoring 'scriptUrl'", scriptUrl);
scriptUrl = null;
}
LOG.info("Activated Scripted HC {} with {}", config.hc_name(),
(StringUtils.isNotBlank(script) ? "script " + script : "script url " + scriptUrl));
LOG.warn("This is deprecated. Please use the use the equivalent functionality from the org.apache.felix.healthcheck.generalchecks bundle instead.");
}
@Override
public Result execute() {
FormattingResultLog log = new FormattingResultLog();
ResourceResolver resourceResolver = null;
try {
resourceResolver = resourceResolverFactory.getServiceResourceResolver(null);
boolean urlIsUsed = StringUtils.isBlank(script);
String scriptToExecute;
if (urlIsUsed) {
if (scriptUrl.startsWith(JCR_FILE_URL_PREFIX)) {
String jcrPath = StringUtils.substringAfter(scriptUrl, JCR_FILE_URL_PREFIX);
scriptToExecute = getScriptFromRepository(resourceResolver, jcrPath);
} else {
scriptToExecute = scriptHelper.getFileContents(scriptUrl);
}
} else {
scriptToExecute = script;
}
log.info("Executing script {} ({} lines)...", (urlIsUsed ? scriptUrl : " as configured"), scriptToExecute.split("\n").length);
try {
ScriptEngine scriptEngine = getScriptEngine(language);
Map<String, Object> additionalBindings = new HashMap<>();
additionalBindings.put("resourceResolver", resourceResolver);
additionalBindings.put("session", resourceResolver.adaptTo(Session.class));
scriptHelper.evalScript(bundleContext, scriptEngine, scriptToExecute, log, additionalBindings, true);
} catch (Exception e) {
log.healthCheckError("Exception while executing script: " + e, e);
}
return new Result(log);
} catch (LoginException e) {
throw new IllegalStateException("Could not get resource resolver: " + e, e);
} finally {
if (resourceResolver != null) {
resourceResolver.close();
}
}
}
private String factoriesToString(List<ScriptEngineFactory> engineFactories) {
List<String> factoryArr = new ArrayList<>();
for (ScriptEngineFactory ef : engineFactories) {
factoryArr.add(ef.getEngineName() + " (" + StringUtils.join(ef.getExtensions(), ",") + ")");
}
return StringUtils.join(factoryArr, ", ");
}
private ScriptEngine getScriptEngine(String language) {
ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension(language);
if (scriptEngine == null) {
try {
scriptEngine = scriptHelper.getScriptEngine(scriptEngineManager, language);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Could not get script engine for " + language + " from available factories: "
+ factoriesToString(scriptEngineManager.getEngineFactories()) + ") nor from regular bundles: " + e.getMessage());
}
}
return scriptEngine;
}
private String getScriptFromRepository(ResourceResolver resourceResolver, String jcrPath) {
String fileContent;
try {
Resource dataResource = resourceResolver.getResource(jcrPath + JCR_CONTENT);
if (dataResource == null) {
throw new IllegalArgumentException("Could not load script from path " + jcrPath);
} else {
try (InputStream is = dataResource.adaptTo(InputStream.class);
ByteArrayOutputStream result = new ByteArrayOutputStream();) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
fileContent = result.toString(StandardCharsets.UTF_8.name());
}
}
} catch (IOException e) {
throw new IllegalStateException("Could not load script from path " + jcrPath + ": " + e, e);
}
return fileContent;
}
/**
* Copied and adapted from org.apache.felix.hc.generalchecks.util.ScriptHelper
*/
private static class ScriptHelper {
public String getFileContents(String url) {
String content;
try {
content = Files.readAllLines(Paths.get(new URI(url))).stream().collect(Collectors.joining("\n"));
return content;
}catch(IOException | URISyntaxException e) {
throw new IllegalArgumentException("Could not read file URL "+url+": "+e, e);
}
}
public ScriptEngine getScriptEngine(ScriptEngineManager scriptEngineManager, String language) {
List<ScriptEngineFactory> engineFactories = scriptEngineManager.getEngineFactories();
ScriptEngine scriptEngine = engineFactories.stream()
.filter(s -> language.equalsIgnoreCase(s.getLanguageName()))
.findFirst()
.map(ScriptEngineFactory::getScriptEngine)
.orElse(null);
if(scriptEngine == null) {
Set<String> availableLanguages = engineFactories.stream()
.map(ScriptEngineFactory::getLanguageName)
.collect(Collectors.toSet());
throw new IllegalArgumentException("No ScriptEngineFactory found for language " + language + " (available languages: " + availableLanguages + ")");
}
return scriptEngine;
}
public Object evalScript(BundleContext bundleContext, ScriptEngine scriptEngine, String scriptToExecute, FormattingResultLog log, Map<String,Object> additionalBindings, boolean logScriptResult) throws ScriptException, IOException {
final Bindings bindings = new SimpleBindings();
final ScriptHelperBinding scriptHelper = new ScriptHelperBinding(bundleContext);
StringWriter stdout = new StringWriter();
StringWriter stderr = new StringWriter();
bindings.put("scriptHelper", scriptHelper);
bindings.put("osgi", scriptHelper); // also register script helper like in web console script console
bindings.put("log", log);
bindings.put("bundleContext", bundleContext);
if (additionalBindings != null) {
for (Map.Entry<String, Object> additionalBinding : additionalBindings.entrySet()) {
bindings.put(additionalBinding.getKey(), additionalBinding.getValue());
}
}
SimpleScriptContext scriptContext = new SimpleScriptContext();
scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
scriptContext.setWriter(stdout);
scriptContext.setErrorWriter(stderr);
try {
log.debug(scriptToExecute);
Object scriptResult = scriptEngine.eval(scriptToExecute, scriptContext);
appendStreamsToResult(log, stdout, stderr, scriptContext);
if(scriptResult instanceof Result) {
Result result = (Result) scriptResult;
for(ResultLog.Entry entry: result) {
log.add(entry);
}
} else if(scriptResult != null && logScriptResult){
log.info("Script result: {}", scriptResult);
}
return scriptResult;
} finally {
scriptHelper.ungetServices();
}
}
private void appendStreamsToResult(FormattingResultLog log, StringWriter stdout, StringWriter stderr, SimpleScriptContext scriptContext)
throws IOException {
scriptContext.getWriter().flush();
String stdoutStr = stdout.toString();
if(StringUtils.isNotBlank(stdoutStr)) {
log.info("stdout of script: {}", stdoutStr);
}
scriptContext.getErrorWriter().flush();
String stderrStr = stderr.toString();
if(StringUtils.isNotBlank(stderrStr)) {
log.critical("stderr of script: {}", stderrStr);
}
}
// Script Helper for OSGi available as binding 'scriptHelper'
class ScriptHelperBinding {
private final BundleContext bundleContext;
private List<ServiceReference<?>> references;
private Map<String, Object> services;
public ScriptHelperBinding(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
@SuppressWarnings({ "unchecked", "unused" })
public <T> T getService(Class<T> type) {
T service = (this.services == null ? null : (T) this.services.get(type.getName()));
if (service == null) {
final ServiceReference<?> ref = this.bundleContext.getServiceReference(type.getName());
if (ref != null) {
service = (T) this.bundleContext.getService(ref);
if (service != null) {
if (this.services == null) {
this.services = new HashMap<>();
}
if (this.references == null) {
this.references = new ArrayList<>();
}
this.references.add(ref);
this.services.put(type.getName(), service);
}
}
}
return service;
}
@SuppressWarnings("unused")
public <T> T[] getServices(Class<T> serviceType, String filter) throws InvalidSyntaxException {
final ServiceReference<?>[] refs = this.bundleContext.getServiceReferences(serviceType.getName(), filter);
T[] result = null;
if (refs != null) {
final List<T> objects = new ArrayList<>();
for (int i = 0; i < refs.length; i++) {
@SuppressWarnings("unchecked")
final T service = (T) this.bundleContext.getService(refs[i]);
if (service != null) {
if (this.references == null) {
this.references = new ArrayList<>();
}
this.references.add(refs[i]);
objects.add(service);
}
}
if (!objects.isEmpty()) {
@SuppressWarnings("unchecked")
T[] srv = (T[]) Array.newInstance(serviceType, objects.size());
result = objects.toArray(srv);
}
}
return result;
}
public void ungetServices() {
if (this.references != null) {
final Iterator<ServiceReference<?>> i = this.references.iterator();
while (i.hasNext()) {
final ServiceReference<?> ref = i.next();
this.bundleContext.ungetService(ref);
}
this.references.clear();
}
if (this.services != null) {
this.services.clear();
}
}
}
}
}