001/*
002 * Copyright (C) 2014 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package org.openstreetmap.josm.eventbus;
016
017import java.lang.reflect.InvocationTargetException;
018import java.lang.reflect.Method;
019import java.util.Objects;
020import java.util.concurrent.Executor;
021
022/**
023 * A subscriber method on a specific object, plus the executor that should be used for dispatching
024 * events to it.
025 *
026 * <p>Two subscribers are equivalent when they refer to the same method on the same object (not
027 * class). This property is used to ensure that no subscriber method is registered more than once.
028 *
029 * @author Colin Decker
030 */
031class Subscriber {
032
033  /**
034   * Creates a {@code Subscriber} for {@code method} on {@code listener}.
035   * @param bus event bus
036   * @param listener listener
037   * @param method method
038   * @return subscriber
039   */
040  static Subscriber create(EventBus bus, Object listener, Method method) {
041    return isDeclaredThreadSafe(method)
042        ? new Subscriber(bus, listener, method)
043        : new SynchronizedSubscriber(bus, listener, method);
044  }
045
046  /** The event bus this subscriber belongs to. */
047  private EventBus bus;
048
049  /** The object with the subscriber method. */
050  final Object target;
051
052  /** Subscriber method. */
053  private final Method method;
054
055  /** Executor to use for dispatching events to this subscriber. */
056  private final Executor executor;
057
058  private Subscriber(EventBus bus, Object target, Method method) {
059    this.bus = bus;
060    this.target = Objects.requireNonNull(target);
061    this.method = method;
062    method.setAccessible(true);
063
064    this.executor = bus.executor();
065  }
066
067  /**
068   * Dispatches {@code event} to this subscriber using the proper executor.
069   * @param event event to dispatch
070   */
071  final void dispatchEvent(final Object event) {
072    executor.execute(
073        () -> {
074            try {
075              invokeSubscriberMethod(event);
076            } catch (InvocationTargetException e) {
077              bus.handleSubscriberException(e.getCause(), context(event));
078            }
079          });
080  }
081
082  /**
083   * Invokes the subscriber method. This method can be overridden to make the invocation
084   * synchronized.
085   * @param event event to dispatch
086   * @throws InvocationTargetException if the invocation fails
087   */
088  void invokeSubscriberMethod(Object event) throws InvocationTargetException {
089    try {
090      method.invoke(target, Objects.requireNonNull(event));
091    } catch (IllegalArgumentException e) {
092      throw new Error("Method rejected target/argument: " + event, e);
093    } catch (IllegalAccessException e) {
094      throw new Error("Method became inaccessible: " + event, e);
095    } catch (InvocationTargetException e) {
096      if (e.getCause() instanceof Error) {
097        throw (Error) e.getCause();
098      }
099      throw e;
100    }
101  }
102
103  /**
104   * Gets the context for the given event.
105   * @param event event
106   * @return context for the given event
107   */
108  private SubscriberExceptionContext context(Object event) {
109    return new SubscriberExceptionContext(bus, event, target, method);
110  }
111
112  @Override
113  public final int hashCode() {
114    return (31 + method.hashCode()) * 31 + System.identityHashCode(target);
115  }
116
117  @Override
118  public final boolean equals(Object obj) {
119    if (obj instanceof Subscriber) {
120      Subscriber that = (Subscriber) obj;
121      // Use == so that different equal instances will still receive events.
122      // We only guard against the case that the same object is registered
123      // multiple times
124      return target == that.target && method.equals(that.method);
125    }
126    return false;
127  }
128
129  /**
130   * Checks whether {@code method} is thread-safe, as indicated by the presence of the {@link
131   * AllowConcurrentEvents} annotation.
132   * @param method method to check
133   * @return {@code true} if {@code method} is thread-safe
134   */
135  private static boolean isDeclaredThreadSafe(Method method) {
136    return method.getAnnotation(AllowConcurrentEvents.class) != null;
137  }
138
139  /**
140   * Subscriber that synchronizes invocations of a method to ensure that only one thread may enter
141   * the method at a time.
142   */
143  static final class SynchronizedSubscriber extends Subscriber {
144
145    private SynchronizedSubscriber(EventBus bus, Object target, Method method) {
146      super(bus, target, method);
147    }
148
149    @Override
150    void invokeSubscriberMethod(Object event) throws InvocationTargetException {
151      synchronized (this) {
152        super.invokeSubscriberMethod(event);
153      }
154    }
155  }
156}