blob: f6217b86752ca982b35b303c608766238bd196ab [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.nifi.controller.service;
import org.apache.commons.lang3.ClassUtils;
import org.apache.nifi.controller.ControllerService;
import org.apache.nifi.controller.ControllerServiceProxyWrapper;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.NarCloseable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
public class StandardControllerServiceInvocationHandler implements ControllerServiceInvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(StandardControllerServiceInvocationHandler.class);
private static final Method PROXY_WRAPPER_GET_WRAPPED_METHOD;
private static final Set<Method> validDisabledMethods;
static {
// methods that are okay to be called when the service is disabled.
final Set<Method> validMethods = new HashSet<>();
validMethods.addAll(Arrays.asList(ControllerService.class.getMethods()));
validMethods.addAll(Arrays.asList(Object.class.getMethods()));
validDisabledMethods = Collections.unmodifiableSet(validMethods);
try {
PROXY_WRAPPER_GET_WRAPPED_METHOD = ControllerServiceProxyWrapper.class.getMethod("getWrapped");
} catch (final NoSuchMethodException e) {
throw new AssertionError("Could not find getWrapped Method for ProxyWrapper");
}
}
private final ControllerService originalService;
private final AtomicReference<ControllerServiceNode> serviceNodeHolder = new AtomicReference<>(null);
private final ExtensionManager extensionManager;
/**
* @param originalService the original service being proxied
*/
public StandardControllerServiceInvocationHandler(final ExtensionManager extensionManager, final ControllerService originalService) {
this(extensionManager, originalService, null);
}
/**
* @param originalService the original service being proxied
* @param serviceNode the node holding the original service which will be used for checking the state (disabled vs running)
*/
public StandardControllerServiceInvocationHandler(final ExtensionManager extensionManager, final ControllerService originalService, final ControllerServiceNode serviceNode) {
this.extensionManager = extensionManager;
this.originalService = originalService;
this.serviceNodeHolder.set(serviceNode);
}
@Override
public void setServiceNode(final ControllerServiceNode serviceNode) {
this.serviceNodeHolder.set(serviceNode);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final String methodName = method.getName();
if ("initialize".equals(methodName) || "onPropertyModified".equals(methodName)) {
// We can blindly throw UnsupportedOperationException because these methods will only ever be invoked by the framework directly
// on the controller service implementation, not on this proxy object.
throw new UnsupportedOperationException(method + " may only be invoked by the NiFi framework");
}
final ControllerServiceNode node = serviceNodeHolder.get();
final ControllerServiceState state = node.getState();
final boolean disabled = state != ControllerServiceState.ENABLED; // only allow method call if service state is ENABLED.
if (disabled && !validDisabledMethods.contains(method)) {
throw new ControllerServiceDisabledException(node.getIdentifier(), "Cannot invoke method " + method + " on Controller Service with identifier "
+ serviceNodeHolder.get().getIdentifier() + " because the Controller Service's State is currently " + state);
}
final ClassLoader callerClassLoader = Thread.currentThread().getContextClassLoader();
try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, originalService.getClass(), originalService.getIdentifier())) {
// If any objects are proxied, unwrap them so that we provide the unproxied object to the Controller Service.
ClassLoader serviceClassLoader = Thread.currentThread().getContextClassLoader();
return invoke(originalService, method, args, serviceClassLoader, callerClassLoader);
} catch (final InvocationTargetException e) {
// If the ControllerService throws an Exception, it'll be wrapped in an InvocationTargetException. We want
// to instead re-throw what the ControllerService threw, so we pull it out of the InvocationTargetException.
throw e.getCause();
}
}
private boolean isInHierarchy(final ClassLoader objectClassLoader, final ClassLoader classLoaderHierarchy) {
if (classLoaderHierarchy == null) {
return false;
}
if (objectClassLoader == classLoaderHierarchy) {
return true;
}
return isInHierarchy(objectClassLoader, classLoaderHierarchy.getParent());
}
private Object proxy(final Object bareObject, final Class<?> declaredType) {
if (bareObject == null) {
return null;
}
// We only want to proxy the object if the object is defined by the method that
// was invoked as being an interface. For example, if a method is expected to return a java.lang.String,
// we do not want to instead return a proxy because the Proxy won't be a String.
if (declaredType == null || !declaredType.isInterface()) {
return bareObject;
}
// If the ClassLoader is null, we have a primitive type, which we can't proxy.
if (bareObject.getClass().getClassLoader() == null) {
return bareObject;
}
// The proxy that is to be returned needs to ensure that it implements all interfaces that are defined by the
// object. We cannot simply implement the return that that is defined, because the code that receives the object
// may perform further inspection. For example, consider that a javax.jms.Message is returned. If this method proxies
// only that method, but the object itself is a javax.jms.BytesMessage, then code such as the following will result in `isBytes == false`
// when it should be `true`:
//
// final javax.jms.Message myMessage = controllerService.getMessage();
// final boolean isBytes = myMessage instanceof javax.jms.BytesMessage;
final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(bareObject.getClass());
if (interfaces == null || interfaces.isEmpty()) {
return bareObject;
}
// Add the ControllerServiceProxyWrapper to the List of interfaces to implement. See javadocs for ControllerServiceProxyWrapper
// to understand why this is needed.
if (!interfaces.contains(ControllerServiceProxyWrapper.class)) {
interfaces.add(ControllerServiceProxyWrapper.class);
}
final Class<?>[] interfaceTypes = interfaces.toArray(new Class<?>[0]);
final InvocationHandler invocationHandler = new ProxiedReturnObjectInvocationHandler(bareObject);
return Proxy.newProxyInstance(bareObject.getClass().getClassLoader(), interfaceTypes, invocationHandler);
}
private Object[] unwrapProxies(final Object[] values, final ClassLoader expectedClassLoader, final Method method) {
if (!containsWrappedProxy(values)) {
return values;
}
final Object[] unwrappedValues = new Object[values.length];
for (int i=0; i < values.length; i++) {
unwrappedValues[i] = unwrap(values[i], expectedClassLoader, method);
}
return unwrappedValues;
}
private Object unwrap(final Object value, final ClassLoader expectedClassLoader, final Method method) {
if (!isWrappedProxy(value)) {
return value;
}
final ControllerServiceProxyWrapper<?> wrapper = (ControllerServiceProxyWrapper<?>) value;
final Object wrapped = wrapper.getWrapped();
if (wrapped == null) {
return null;
}
final ClassLoader wrappedClassLoader = wrapped.getClass().getClassLoader();
if (isInHierarchy(wrappedClassLoader, expectedClassLoader)) {
logger.trace("Unwrapped {} to be used by {} when calling {}", wrapped, originalService, method);
return wrapped;
}
logger.trace("Will not unwrap {} because even though it is a wrapped proxy object, the wrapped object's ClassLoader is {}, not {}", value, wrappedClassLoader, expectedClassLoader);
return value;
}
private boolean containsWrappedProxy(final Object[] values) {
if (values == null || values.length == 0) {
return false;
}
for (final Object value : values) {
if (isWrappedProxy(value)) {
return true;
}
}
return false;
}
private boolean isWrappedProxy(final Object value) {
if (value == null) {
return false;
}
final Class<?> valueClass = value.getClass();
return ControllerServiceProxyWrapper.class.isAssignableFrom(valueClass) && Proxy.isProxyClass(valueClass);
}
private class ProxiedReturnObjectInvocationHandler implements InvocationHandler {
private final Object bareObject;
private ClassLoader bareObjectClassLoader;
public ProxiedReturnObjectInvocationHandler(final Object bareObject) {
this.bareObject = bareObject;
this.bareObjectClassLoader = bareObject.getClass().getClassLoader();
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (PROXY_WRAPPER_GET_WRAPPED_METHOD.equals(method)) {
return this.bareObject;
}
final ClassLoader callerClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(this.bareObjectClassLoader);
return StandardControllerServiceInvocationHandler.this.invoke(this.bareObject, method, args, this.bareObjectClassLoader, callerClassLoader);
} catch (final InvocationTargetException ite) {
throw ite.getCause();
} finally {
Thread.currentThread().setContextClassLoader(callerClassLoader);
}
}
}
private Object invoke(Object bareObject, Method method, Object[] args, ClassLoader bareObjectClassLoader, ClassLoader callerClassLoader) throws IllegalAccessException, InvocationTargetException {
// If any objects are proxied, unwrap them so that we provide the unproxied object to the Controller Service.
final Object[] unwrappedArgs = unwrapProxies(args, bareObjectClassLoader, method);
// Invoke the method on the underlying implementation
final Object returnedFromBareObject = method.invoke(bareObject, unwrappedArgs);
// If the return object is known to the caller, it can be returned directly. Otherwise, proxy the object so that
// calls into the proxy are called through the appropriate ClassLoader.
if (returnedFromBareObject == null || isInHierarchy(returnedFromBareObject.getClass().getClassLoader(), callerClassLoader)) {
return returnedFromBareObject;
}
return proxy(returnedFromBareObject, method.getReturnType());
}
}