/* * 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.activemq.transport.amqp.client.util; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Map; import org.apache.qpid.proton.engine.Connection; import org.apache.qpid.proton.engine.Delivery; import org.apache.qpid.proton.engine.Link; import org.apache.qpid.proton.engine.Receiver; import org.apache.qpid.proton.engine.Sasl; import org.apache.qpid.proton.engine.Sender; import org.apache.qpid.proton.engine.Session; import org.apache.qpid.proton.engine.Transport; /** * Utility that creates proxy objects for the Proton objects which won't allow any mutating * operations to be applied so that the test code does not interact with the proton engine * outside the client serialization thread. */ public final class UnmodifiableProxy { private static ArrayList<String> blacklist = new ArrayList<>(); // These methods are mutating but don't take an arguments so they // aren't automatically filtered out. We will have to keep an eye // on proton API in the future and modify this list as it evolves. static { blacklist.add("close"); blacklist.add("free"); blacklist.add("open"); blacklist.add("sasl"); blacklist.add("session"); blacklist.add("close_head"); blacklist.add("close_tail"); blacklist.add("outputConsumed"); blacklist.add("process"); blacklist.add("processInput"); blacklist.add("unbind"); blacklist.add("settle"); blacklist.add("clear"); blacklist.add("detach"); blacklist.add("abort"); } private UnmodifiableProxy() { } public static Transport transportProxy(final Transport target) { Transport wrap = wrap(Transport.class, target); return wrap; } public static Sasl saslProxy(final Sasl target) { return wrap(Sasl.class, target); } public static Connection connectionProxy(final Connection target) { return wrap(Connection.class, target); } public static Session sessionProxy(final Session target) { return wrap(Session.class, target); } public static Delivery deliveryProxy(final Delivery target) { return wrap(Delivery.class, target); } public static Link linkProxy(final Link target) { return wrap(Link.class, target); } public static Receiver receiverProxy(final Receiver target) { return wrap(Receiver.class, target); } public static Sender senderProxy(final Sender target) { return wrap(Sender.class, target); } private static boolean isProtonType(Class<?> clazz) { String packageName = clazz.getPackage().getName(); if (packageName.startsWith("org.apache.qpid.proton.")) { return true; } return false; } private static <T> T wrap(Class<T> type, final Object target) { return type.cast(java.lang.reflect.Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { if ("toString".equals(method.getName()) && method.getParameterTypes().length == 0) { return "Unmodifiable proxy -> (" + method.invoke(target, objects) + ")"; } // Don't let methods that mutate be invoked. if (method.getParameterTypes().length > 0) { throw new UnsupportedOperationException("Cannot mutate outside the Client work thread"); } if (blacklist.contains(method.getName())) { throw new UnsupportedOperationException("Cannot mutate outside the Client work thread"); } Class<?> returnType = method.getReturnType(); try { Object result = method.invoke(target, objects); if (result == null) { return null; } if (returnType.isPrimitive() || returnType.isArray() || Object.class.equals(returnType)) { // Skip any other checks } else if (returnType.isAssignableFrom(ByteBuffer.class)) { // Buffers are modifiable but we can just return null to indicate // there's nothing there to access. result = null; } else if (returnType.isAssignableFrom(Map.class)) { // Prevent return of modifiable maps result = Collections.unmodifiableMap((Map<?, ?>) result); } else if (isProtonType(returnType) && returnType.isInterface()) { // Can't handle the crazy Source / Target types yet as there's two // different types for Source and Target the result can't be cast to // the one people actually want to use. if (!returnType.getName().equals("org.apache.qpid.proton.amqp.transport.Source") && !returnType.getName().equals("org.apache.qpid.proton.amqp.messaging.Source") && !returnType.getName().equals("org.apache.qpid.proton.amqp.transport.Target") && !returnType.getName().equals("org.apache.qpid.proton.amqp.messaging.Target")) { result = wrap(returnType, result); } } return result; } catch (InvocationTargetException e) { throw e.getTargetException(); } } })); } }