blob: a123dce818fd88ca9b36bc81ae04119d97d0e573 [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.jmx.provider.impl;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.servlet.http.HttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceProvider;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
/**
* Brief summary of a "good" object name:
*
* Object names:
* - have a domain
* - should have a type property
* - could have a name property
* - additional props are not recommended
*
* Path to an MBean:
* {Domain}/{type property}/{name property}{all other props}
* where
* {Domain} : is a path consisting of the domain (dots replaced with slashes)
* {type property} : is the value of the type property or "{notype}" if no type property is set
* {name property} : is the value of the name property or "{noname}" if no name property is set
* {all other props} : name/value pairs containing all additional props
*/
@Component(service = {ResourceProvider.class})
@Designate(ocd = JMXResourceProvider.Config.class)
public class JMXResourceProvider implements ResourceProvider {
/** Configured root paths, ending with a slash */
private String[] rootsWithSlash;
/** Configured root paths, not ending with a slash */
private String[] roots;
/** The mbean server. */
private MBeanServer mbeanServer;
@SuppressWarnings("java:S100")
@ObjectClassDefinition(name = "Apache Sling JMX Resource Provider",
description = "This provider mounts JMX mbeans into the resource tree.")
public @interface Config {
@AttributeDefinition(name = "Root", description = "The mount point of the JMX beans")
String[] provider_roots() default {"/system/sling/monitoring/mbeans"};
@AttributeDefinition
boolean provider_useResourceAccessSecurity() default true;
@AttributeDefinition
boolean provider_ownsRoots() default true;
}
@Activate
protected void activate(final Config config) {
final String[] paths = config.provider_roots();
final List<String> rootsList = new ArrayList<>();
final List<String> rootsWithSlashList = new ArrayList<>();
if ( paths != null ) {
for(final String p : paths) {
if ( p.length() > 0 ) {
if ( p.endsWith("/") ) {
rootsList.add(p.substring(0, p.length() - 1));
rootsWithSlashList.add(p);
} else {
rootsList.add(p);
rootsWithSlashList.add(p + "/");
}
}
}
}
this.rootsWithSlash = rootsWithSlashList.toArray(new String[rootsWithSlashList.size()]);
this.roots = rootsList.toArray(new String[rootsList.size()]);
this.mbeanServer = ManagementFactory.getPlatformMBeanServer();
}
@Deactivate
protected void deactivate() {
this.mbeanServer = null;
}
/**
* @see org.apache.sling.api.resource.ResourceProvider#getResource(org.apache.sling.api.resource.ResourceResolver, javax.servlet.http.HttpServletRequest, java.lang.String)
*/
public Resource getResource(final ResourceResolver resourceResolver,
final HttpServletRequest request,
final String path) {
return getResource(resourceResolver, path);
}
/**
* @see org.apache.sling.api.resource.ResourceProvider#getResource(org.apache.sling.api.resource.ResourceResolver, java.lang.String)
*/
public Resource getResource(final ResourceResolver resourceResolver,
final String path) {
final PathInfo info = this.parse(path);
if ( info != null ) {
if ( info.isRoot ) {
return new RootResource(resourceResolver, path);
}
if ( info.mbeanInfo == null ) {
final Set<ObjectName> names = this.queryObjectNames(info.pathInfo);
if ( names.size() != 0 ) {
return new RootResource(resourceResolver, path);
}
} else {
if (info.pathInfo == null ) {
return new MBeanResource(this.mbeanServer, resourceResolver, this.convertObjectNameToResourcePath(info.objectName), path, info.mbeanInfo, info.objectName);
}
if ( info.pathInfo.equals("mbean:attributes") ) {
final MBeanResource parent = (MBeanResource)this.getResource(resourceResolver, ResourceUtil.getParent(path));
return new AttributesResource(resourceResolver, path, parent);
}
if ( info.pathInfo.startsWith("mbean:attributes/") ) {
final Resource parentRsrc = this.getResource(resourceResolver, ResourceUtil.getParent(path));
final AttributesResource parentAttributesResource;
final MBeanResource parentMBeanResource;
if ( parentRsrc instanceof AttributesResource ) {
parentAttributesResource = (AttributesResource) parentRsrc;
parentMBeanResource = (MBeanResource)parentRsrc.getParent();
} else {
final AttributeResource parent;
if ( parentRsrc instanceof AttributeResource) {
parent = (AttributeResource)parentRsrc;
} else {
parent = ((MapResource)parentRsrc).getAttributeResource();
}
parentAttributesResource = (AttributesResource) parent.getParent();
parentMBeanResource = (MBeanResource) parentAttributesResource.getParent();
}
final AttributeList result = parentMBeanResource.getAttributes();
final String attrPath = info.pathInfo.substring("mbean:attributes/".length());
final int pos = attrPath.indexOf('/');
final String attrName;
final String subPath;
if ( pos == -1 ) {
attrName = attrPath;
subPath = null;
} else {
attrName = attrPath.substring(0, pos);
subPath = attrPath.substring(pos + 1);
}
for(final MBeanAttributeInfo mai : info.mbeanInfo.getAttributes()) {
if ( mai.getName().equals(attrName) ) {
final Iterator iter = result.iterator();
Object value = null;
while ( iter.hasNext() && value == null ) {
final Attribute a = (Attribute) iter.next();
if ( a.getName().equals(attrName) ) {
value = a.getValue();
}
}
final AttributeResource rsrc = new AttributeResource(resourceResolver, path, mai, value, parentAttributesResource);
if ( subPath != null ) {
return rsrc.getChildResource(subPath);
}
return rsrc;
}
}
}
}
}
return null;
}
private Set<ObjectName> queryObjectNames(final String prefix) {
final Set<ObjectName> allNames = this.mbeanServer.queryNames(null, null);
Set<ObjectName> names = allNames;
if ( prefix != null ) {
final String pathPrefix = prefix + '/';
names = new HashSet<>();
for(final ObjectName name : allNames) {
final String path = this.convertObjectNameToResourcePath(name);
if ( path.startsWith(pathPrefix) ) {
names.add(name);
}
}
}
return names;
}
/**
* @see org.apache.sling.api.resource.ResourceProvider#listChildren(org.apache.sling.api.resource.Resource)
*/
public Iterator<Resource> listChildren(final Resource parent) {
final PathInfo info = this.parse(parent.getPath());
if ( info != null ) {
if ( info.isRoot || info.mbeanInfo == null ) {
// list all MBeans
final Set<ObjectName> names = this.queryObjectNames(info.isRoot ? null : info.pathInfo);
final Set<String> filteredNames = new HashSet<>();
final String prefix = (info.isRoot ? null : info.pathInfo + "/");
for(final ObjectName name : names) {
final String path = this.convertObjectNameToResourcePath(name);
final String testName = (info.isRoot ? path : path.substring(prefix.length()));
final int sep = testName.indexOf('/');
if ( sep == -1 ) {
filteredNames.add(":" + name.getCanonicalName());
} else {
filteredNames.add(testName.substring(0, sep));
}
}
final List<String> sortedNames = new ArrayList<String>(filteredNames);
Collections.sort(sortedNames);
final Iterator<String> iter = sortedNames.iterator();
return new Iterator<Resource>() {
private Resource next;
{
seek();
}
private void seek() {
while ( iter.hasNext() && this.next == null ) {
final String name = iter.next();
if ( name.startsWith(":") ) {
try {
final ObjectName on = new ObjectName(name.substring(1));
final MBeanInfo info = mbeanServer.getMBeanInfo(on);
final String path = convertObjectNameToResourcePath(on);
final int sep = path.lastIndexOf('/');
this.next = new MBeanResource(mbeanServer, parent.getResourceResolver(), path, parent.getPath() + "/" + path.substring(sep + 1), info, on);
} catch (final IntrospectionException e) {
// ignore
} catch (final InstanceNotFoundException e) {
// ignore
} catch (final ReflectionException e) {
// ignore
} catch (final MalformedObjectNameException e) {
// ignore
}
} else {
this.next = new RootResource(parent.getResourceResolver(), parent.getPath() + '/' + name);
}
}
}
public boolean hasNext() {
return next != null;
}
public Resource next() {
if ( next != null ) {
final Resource rsrc = next;
this.next = null;
seek();
return rsrc;
}
throw new NoSuchElementException();
}
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
} else {
if ( info.pathInfo == null ) {
final MBeanResource parentResource;
if ( parent instanceof MBeanResource ) {
parentResource = (MBeanResource)parent;
} else {
parentResource = (MBeanResource)this.getResource(parent.getResourceResolver(), parent.getPath());
}
final List<Resource> list = new ArrayList<>();
list.add(new AttributesResource(parent.getResourceResolver(), parent.getPath() + "/mbean:attributes", parentResource));
return list.iterator();
} else if ( info.pathInfo.equals("mbean:attributes") ) {
final AttributesResource parentResource;
if ( parent instanceof AttributesResource ) {
parentResource = (AttributesResource)parent;
} else {
parentResource = (AttributesResource) this.getResource(parent.getResourceResolver(), parent.getPath());
}
final MBeanResource parentMBeanResource = (MBeanResource)parentResource.getParent();
final AttributeList result = parentMBeanResource.getAttributes();
final MBeanAttributeInfo[] infos = info.mbeanInfo.getAttributes();
final Map<String, MBeanAttributeInfo> infoMap = new HashMap<>();
for(final MBeanAttributeInfo i : infos) {
infoMap.put(i.getName(), i);
}
final Iterator iter = result.iterator();
return new Iterator<Resource>() {
public void remove() {
throw new UnsupportedOperationException("remove");
}
public Resource next() {
final Attribute attr = (Attribute)iter.next();
return new AttributeResource(parent.getResourceResolver(),
parent.getPath() + "/" + attr.getName(),
infoMap.get(attr.getName()),
attr.getValue(),
parentResource);
}
public boolean hasNext() {
return iter.hasNext();
}
};
} else if ( info.pathInfo.startsWith("mbean:attributes/") ) {
Resource checkParentResource = parent;
if ( !(checkParentResource instanceof AttributeResource) && !(checkParentResource instanceof MapResource ) ) {
checkParentResource = this.getResource(parent.getResourceResolver(), parent.getPath());
}
final AttributeResource parentResource;
if ( checkParentResource instanceof AttributeResource ) {
parentResource = (AttributeResource)checkParentResource;
} else {
parentResource = ((MapResource)checkParentResource).getAttributeResource();
}
final String attrPath = info.pathInfo.substring("mbean:attributes/".length());
final int pos = attrPath.indexOf('/');
final String subPath;
if ( pos == -1 ) {
subPath = null;
} else {
subPath = attrPath.substring(pos + 1);
}
return parentResource.getChildren(parent.getPath(), subPath);
}
}
}
return null;
}
private static final String MARKER_NOTYPE = "{notype}";
private static final String MARKER_NONAME = "{noname}";
private String encode(final String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (final UnsupportedEncodingException uee) {
// this should never happen, UTF-8 is always supported
return value;
}
}
private String decode(final String value) {
try {
return URLDecoder.decode(value, "UTF-8");
} catch (final UnsupportedEncodingException uee) {
// this should never happen, UTF-8 is always supported
return value;
}
}
private String convertObjectNameToResourcePath(final ObjectName name) {
final StringBuilder sb = new StringBuilder(name.getDomain().replace('.', '/'));
sb.append('/');
if ( name.getKeyProperty("type") != null ) {
sb.append(encode(name.getKeyProperty("type")));
} else {
sb.append(MARKER_NOTYPE);
}
sb.append('/');
if ( name.getKeyProperty("name") != null ) {
sb.append(encode(name.getKeyProperty("name")));
} else {
sb.append(MARKER_NONAME);
}
final TreeMap<String, String> props = new TreeMap<String, String>(name.getKeyPropertyList());
props.remove("name");
props.remove("type");
boolean first = true;
for(final Map.Entry<String, String> entry : props.entrySet()) {
if ( first ) {
first = false;
sb.append(':');
} else {
sb.append(',');
}
sb.append(encode(entry.getKey()));
sb.append('=');
sb.append(encode(entry.getValue()));
}
return sb.toString();
}
private ObjectName convertResourcePathToObjectName(final String path) {
final int nameSlash = path.lastIndexOf('/');
if ( nameSlash != -1 ) {
final int typeSlash = path.lastIndexOf('/', nameSlash - 1);
if ( typeSlash != -1 ) {
final String domain = path.substring(0, typeSlash).replace('/', '.');
final String type = decode(path.substring(typeSlash + 1, nameSlash));
final String nameAndProps = path.substring(nameSlash + 1);
final int colonPos = nameAndProps.indexOf(':');
final String name;
final String props;
if ( colonPos == -1 ) {
name = decode(nameAndProps);
props = null;
} else {
name = decode(nameAndProps.substring(0, colonPos));
props = nameAndProps.substring(colonPos + 1);
}
final StringBuilder sb = new StringBuilder();
sb.append(domain);
sb.append(':');
boolean hasProps = false;
if ( !MARKER_NOTYPE.equals(type)) {
sb.append("type=");
sb.append(type);
hasProps = true;
}
if ( !MARKER_NONAME.equals(name) ) {
if ( hasProps ) {
sb.append(",");
}
sb.append("name=");
sb.append(name);
hasProps = true;
}
if ( props != null ) {
final String[] propArray = props.split(",");
for(final String keyValue : propArray) {
if ( hasProps ) {
sb.append(",");
}
final int pos = keyValue.indexOf('=');
if ( pos == -1 ) {
return null;
}
sb.append(decode(keyValue.substring(0, pos)));
sb.append('=');
sb.append(decode(keyValue.substring(pos+1)));
}
}
try {
return new ObjectName(sb.toString());
} catch (final MalformedObjectNameException e) {
// ignore
}
}
}
return null;
}
public final static class PathInfo {
public final boolean isRoot;
public final String pathInfo;
public ObjectName objectName;
public MBeanInfo mbeanInfo;
public PathInfo(final boolean isRoot) {
this.isRoot = isRoot;
this.pathInfo = null;
}
public PathInfo(final String info) {
this.isRoot = false;
this.pathInfo = info;
}
}
/**
* Parse the path
*/
private PathInfo parse(final String path) {
for(final String root : this.rootsWithSlash) {
if ( path.startsWith(root) ) {
final String subPath = path.substring(root.length());
if ( subPath.length() == 0 ) {
return new PathInfo(true);
}
// MBean name / path
String checkPath = subPath;
String pathInfo = null;
ObjectName objectName = null;
MBeanInfo mbi = null;
while ( checkPath.length() > 0 && mbi == null ) {
try {
objectName = this.convertResourcePathToObjectName(checkPath);
if ( objectName != null ) {
mbi = this.mbeanServer.getMBeanInfo(objectName);
}
} catch (final IntrospectionException e) {
// ignore
} catch (final InstanceNotFoundException e) {
// ignore
} catch (final ReflectionException e) {
// ignore
}
if ( mbi == null ) {
final int sep = checkPath.lastIndexOf('/');
if ( sep == -1 ) {
checkPath = "";
pathInfo = subPath;
} else {
checkPath = checkPath.substring(0, sep);
pathInfo = subPath.substring(sep + 1);
}
}
}
final PathInfo info = new PathInfo(pathInfo);
if ( mbi != null ) {
info.objectName = objectName;
info.mbeanInfo = mbi;
}
return info;
}
}
for(final String root : this.roots) {
if ( path.equals(root) ) {
return new PathInfo(true);
}
}
return null;
}
}