/*
* 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.brooklyn.core.objs.proxy;
import static com.google.common.base.Preconditions.checkNotNull;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.TaskAdaptable;
import org.apache.brooklyn.core.effector.EffectorWithBody;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.internal.EntityTransientCopyInternal;
import org.apache.brooklyn.core.entity.internal.EntityTransientCopyInternal.SpecialEntityTransientCopyInternal;
import org.apache.brooklyn.core.mgmt.internal.EffectorUtils;
import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal;
import org.apache.brooklyn.core.mgmt.internal.ManagementTransitionMode;
import org.apache.brooklyn.core.mgmt.rebind.RebindManagerImpl.RebindTracker;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.TaskTags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
/**
* A dynamic proxy for an entity. Other entities etc should use these proxies when interacting
* with the entity, rather than holding a reference to the specific object. That makes remoting
* etc much simpler.
*
* @author aled
*/
public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
// TODO Currently the proxy references the real entity and invokes methods on it directly.
// As we work on remoting/distribution, this will be replaced by RPC.
private static final Logger LOG = LoggerFactory.getLogger(EntityProxyImpl.class);
private Entity delegate;
private Boolean isMaster;
private WeakHashMap<Entity,Void> temporaryProxies = new WeakHashMap<Entity, Void>();
private static final Set<MethodSignature> OBJECT_METHODS = Sets.newLinkedHashSet();
static {
for (Method m : Object.class.getMethods()) {
OBJECT_METHODS.add(new MethodSignature(m));
}
}
private static final Set<MethodSignature> ENTITY_NON_EFFECTOR_METHODS = Sets.newLinkedHashSet();
static {
for (Method m : Entity.class.getMethods()) {
ENTITY_NON_EFFECTOR_METHODS.add(new MethodSignature(m));
}
for (Method m : EntityLocal.class.getMethods()) {
ENTITY_NON_EFFECTOR_METHODS.add(new MethodSignature(m));
}
for (Method m : EntityInternal.class.getMethods()) {
ENTITY_NON_EFFECTOR_METHODS.add(new MethodSignature(m));
}
}
private static final Set<MethodSignature> ENTITY_PERMITTED_READ_ONLY_METHODS = Sets.newLinkedHashSet();
static {
for (Method m : EntityTransientCopyInternal.class.getMethods()) {
ENTITY_PERMITTED_READ_ONLY_METHODS.add(new MethodSignature(m));
}
if (!ENTITY_NON_EFFECTOR_METHODS.containsAll(ENTITY_PERMITTED_READ_ONLY_METHODS)) {
Set<MethodSignature> extras = new LinkedHashSet<EntityProxyImpl.MethodSignature>(ENTITY_PERMITTED_READ_ONLY_METHODS);
extras.removeAll(ENTITY_NON_EFFECTOR_METHODS);
throw new IllegalStateException("Entity read-only methods contains items not known as Entity methods: "+
extras);
}
for (Method m : SpecialEntityTransientCopyInternal.class.getMethods()) {
ENTITY_PERMITTED_READ_ONLY_METHODS.add(new MethodSignature(m));
}
}
public EntityProxyImpl(Entity entity) {
this.delegate = checkNotNull(entity, "entity");
}
/** invoked to specify that a different underlying delegate should be used,
* e.g. because we are switching copy impls or switching primary/copy*/
public synchronized void resetDelegate(Entity thisProxy, Entity preferredProxy, Entity newDelegate) {
if (LOG.isTraceEnabled()) {
LOG.trace("updating "+Integer.toHexString(System.identityHashCode(thisProxy))
+" to be the same as "+Integer.toHexString(System.identityHashCode(preferredProxy))
+" pointing at "+Integer.toHexString(System.identityHashCode(newDelegate))
+" ("+temporaryProxies.size()+" temporary proxies)");
}
Entity oldDelegate = delegate;
this.delegate = newDelegate;
this.isMaster = null;
if (newDelegate==oldDelegate)
return;
/* we have to make sure that any newly created proxy of the newDelegate
* which have leaked (eg by being set as a child) also get repointed to this new delegate */
if (oldDelegate!=null) {
Entity temporaryProxy = ((AbstractEntity)oldDelegate).getProxy();
if (temporaryProxy!=null) temporaryProxies.put(temporaryProxy, null);
((AbstractEntity)oldDelegate).resetProxy(preferredProxy);
}
if (newDelegate!=null) {
Entity temporaryProxy = ((AbstractEntity)newDelegate).getProxy();
if (temporaryProxy!=null) temporaryProxies.put(temporaryProxy, null);
((AbstractEntity)newDelegate).resetProxy(preferredProxy);
}
// update any proxies which might be in use
for (Entity tp: temporaryProxies.keySet()) {
if (tp==thisProxy || tp==preferredProxy) continue;
((EntityProxyImpl)(Proxy.getInvocationHandler(tp))).resetDelegate(tp, preferredProxy, newDelegate);
}
}
@Override
public String toString() {
return delegate.toString();
}
protected boolean isMaster() {
if (isMaster!=null) return isMaster;
ManagementContext mgmt = ((EntityInternal)delegate).getManagementContext();
ManagementTransitionMode mode = ((EntityManagerInternal)mgmt.getEntityManager()).getLastManagementTransitionMode(delegate.getId());
Boolean ro = ((EntityInternal)delegate).getManagementSupport().isReadOnlyRaw();
if (mode==null || ro==null) {
// not configured yet
return false;
}
boolean isMasterX = !mode.isReadOnly();
if (isMasterX != !ro) {
LOG.warn("Inconsistent read-only state for "+delegate+" (possibly rebinding); "
+ "management thinks "+isMasterX+" but entity thinks "+!ro);
return false;
}
isMaster = isMasterX;
return isMasterX;
}
public Object invoke(Object proxy, final Method m, final Object[] args) throws Throwable {
if (proxy == null) {
throw new IllegalArgumentException("Static methods not supported via proxy on entity "+delegate);
}
MethodSignature sig = new MethodSignature(m);
Object result;
if (OBJECT_METHODS.contains(sig)) {
result = m.invoke(delegate, args);
} else if (ENTITY_PERMITTED_READ_ONLY_METHODS.contains(sig)) {
result = m.invoke(delegate, args);
} else {
if (!isMaster()) {
if (isMaster==null || RebindTracker.isRebinding()) {
// rebinding or caller manipulating before management; permit all access
// (as of this writing, things seem to work fine without the isRebinding check;
// but including in it may allow us to tighten the methods in EntityTransientCopyInternal)
result = m.invoke(delegate, args);
} else {
throw new UnsupportedOperationException("Call to '"+sig+"' not permitted on read-only entity "+delegate);
}
} else if (ENTITY_NON_EFFECTOR_METHODS.contains(sig)) {
result = m.invoke(delegate, args);
} else {
Object[] nonNullArgs = (args == null) ? new Object[0] : args;
Effector<?> eff = findEffector(m, nonNullArgs);
if (eff != null) {
@SuppressWarnings("rawtypes")
Map parameters = EffectorUtils.prepareArgsForEffectorAsMapFromArray(eff, nonNullArgs);
@SuppressWarnings({ "unchecked", "rawtypes" })
TaskAdaptable<?> task = ((EffectorWithBody)eff).getBody().newTask(delegate, eff, ConfigBag.newInstance(parameters));
// as per LocalManagementContext.runAtEntity(Entity entity, TaskAdaptable<T> task)
TaskTags.markInessential(task);
result = DynamicTasks.queueIfPossible(task.asTask()).orSubmitAsync(delegate).andWaitForSuccess();
} else {
result = m.invoke(delegate, nonNullArgs);
}
}
}
return (result == delegate && delegate instanceof AbstractEntity) ? ((AbstractEntity)result).getProxy() : result;
}
private Effector<?> findEffector(Method m, Object[] args) {
String name = m.getName();
Set<Effector<?>> effectors = delegate.getEntityType().getEffectors();
for (Effector<?> contender : effectors) {
if (name.equals(contender.getName())) {
return contender;
}
}
return null;
}
private static class MethodSignature {
private final String name;
private final Class<?>[] parameterTypes;
MethodSignature(Method m) {
name = m.getName();
parameterTypes = m.getParameterTypes();
}
@Override
public int hashCode() {
return Objects.hashCode(name, Arrays.hashCode(parameterTypes));
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof MethodSignature)) return false;
MethodSignature o = (MethodSignature) obj;
return name.equals(o.name) && Arrays.equals(parameterTypes, o.parameterTypes);
}
@Override
public String toString() {
return name+Arrays.toString(parameterTypes);
}
}
@VisibleForTesting
public Entity getDelegate() {
return delegate;
}
@Override
public boolean equals(Object obj) {
return delegate.equals(obj);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
}