/*
* Copyright (C) 2012 Google Inc.
*
* Licensed 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 interactivespaces.activity.component;
import interactivespaces.InteractiveSpacesException;
import interactivespaces.SimpleInteractiveSpacesException;
import interactivespaces.activity.SupportedActivity;
import interactivespaces.util.InteractiveSpacesUtilities;
import interactivespaces.util.graph.DependencyResolver;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A context for {@link ActivityComponent} instances to run in.
*
* @author Keith M. Hughes
*/
public class ActivityComponentContext {
/**
* The amount of time, in msecs, that handlers will wait for startup.
*/
public static final long STARTUP_LATCH_TIMEOUT = 5000;
/**
* The activity the components are running for.
*/
private final SupportedActivity activity;
/**
* Factory for new components.
*/
private final ActivityComponentFactory componentFactory;
/**
* The latch that threads can wait on for startup completion.
*/
private final CountDownLatch startupLatch = new CountDownLatch(1);
/**
* {@code true} if handlers are allowed to run.
*/
private final AtomicBoolean handlersAllowed = new AtomicBoolean();
/**
* Keeps count of the number of handlers running.
*/
private final AtomicInteger numberProcessingHandlers = new AtomicInteger();
/**
* All components in the activity.
*/
private final List<ActivityComponent> configuredComponents = Lists.newArrayList();
/**
* Set of component names in the activity.
*/
private final Map<String, ActivityComponent> addedComponents = Maps.newHashMap();
/**
* @param activity
* the activity which will use this context
* @param componentFactory
* the factory for any new components that will be needed
*/
public ActivityComponentContext(SupportedActivity activity, ActivityComponentFactory componentFactory) {
this.activity = activity;
this.componentFactory = componentFactory;
}
/**
* Get the activity which is running the components.
*
* @param <T>
* type of activity
*
* @return the activity
*/
@SuppressWarnings("unchecked")
public <T extends SupportedActivity> T getActivity() {
return (T) activity;
}
/**
* Get the component factory for this context.
*
* @return factor for creating activity components
*/
public ActivityComponentFactory getComponentFactory() {
return componentFactory;
}
/**
* Begin the setup phase.
*
* <p>
* Do nothing with configurations in this phase.
*/
public void beginStartupPhase() {
// Nothing required at the moment
}
/**
* End the setup phase.
*
* @param success
* {@code true} if the setup was successful
*/
public void endStartupPhase(boolean success) {
handlersAllowed.set(success);
startupLatch.countDown();
}
/**
* Shutdown has begun.
*/
public void beginShutdownPhase() {
handlersAllowed.set(false);
}
/**
* Are handler allowed to run?
*
* @return {@code true} if handlers can run
*/
public boolean areHandlersAllowed() {
return handlersAllowed.get();
}
/**
* A handler has been entered.
*/
public void enterHandler() {
numberProcessingHandlers.incrementAndGet();
}
/**
* A handler has been exited.
*/
public void exitHandler() {
if (numberProcessingHandlers.decrementAndGet() < 0) {
getActivity().getLog().error("There are more handler exits than enters");
}
}
/**
* Are there still handlers which are processing data?
*
* @return {@code true} if there are handlers in the midst of processing
*/
public boolean areProcessingHandlers() {
return numberProcessingHandlers.get() > 0;
}
/**
* Block until there are no longer handlers which are processing.
*
* @param sampleTime
* how often sampling should take place for whether there are processing handler, in milliseconds
* @param maxSamplingTime
* how long should sampling take place before punting, in msecs
*
* @return {@code true} if there are no more processing handlers
*/
public boolean waitOnNoProcessingHandlings(long sampleTime, long maxSamplingTime) {
long start = System.currentTimeMillis();
while (areProcessingHandlers() && (System.currentTimeMillis() - start) < maxSamplingTime) {
InteractiveSpacesUtilities.delay(sampleTime);
}
return !areProcessingHandlers();
}
/**
* Wait for the context to complete startup, whether successfully or unsuccessfully.
*
* <p>
* This method should be called before any handler runs, it will return immediately if startup has completed.
*
* <p>
* The await will not be for longer than a preset amount of time.
*
* @return {@code true} if startup was successful before timeout
*/
public boolean awaitStartup() {
try {
boolean succeed = startupLatch.await(STARTUP_LATCH_TIMEOUT, TimeUnit.MILLISECONDS);
if (!succeed) {
getActivity().getLog()
.warn(
String.format("Event handler timed out after %d msecs waiting for activity startup",
STARTUP_LATCH_TIMEOUT));
}
return succeed;
} catch (InterruptedException e) {
return false;
}
}
/**
* Can a handler run?
*
* <p>
* This call requires both {@link #areHandlersAllowed()} and {@link #awaitStartup()} to both be {@code true}.
*
* @return {@code true} if a handle can run.
*/
public boolean canHandlerRun() {
return awaitStartup() && areHandlersAllowed();
}
/**
* Add a new component to the collection.
*
* @param component
* the component to add
* @param <T>
* the type of the component
*
* @return the component added
*
* @throws InteractiveSpacesException
* component was already there
*/
public <T extends ActivityComponent> T addComponent(T component) throws InteractiveSpacesException {
checkIfComponentAdded(component.getName());
addCheckedComponent(component);
return component;
}
/**
* Add a component which has been checked for whether it has been added or not.
*
* @param component
* the component to add
*/
private void addCheckedComponent(ActivityComponent component) {
component.setComponentContext(this);
addedComponents.put(component.getName(), component);
}
/**
* Add new components to the collection.
*
* @param components
* the components to add
*
* @throws InteractiveSpacesException
* component was already there
*/
public void addComponents(ActivityComponent... components) throws InteractiveSpacesException {
for (ActivityComponent component : components) {
addComponent(component);
}
}
/**
* Add a new component to the activity.
*
* @param componentType
* the type of the component to add
* @param <T>
* specific activity component type
*
* @return created activity component
*
* @throws InteractiveSpacesException
* component was already there
*/
public <T extends ActivityComponent> T addComponent(String componentType) throws InteractiveSpacesException {
checkIfComponentAdded(componentType);
ActivityComponent component = componentFactory.newComponent(componentType);
addCheckedComponent(component);
@SuppressWarnings("unchecked")
T c = (T) component;
return c;
}
/**
* Add a set of new components to the activity.
*
* @param componentTypes
* the types of the components to add
*/
public void addComponents(String... componentTypes) {
for (String componentType : componentTypes) {
addComponent(componentType);
}
}
/**
* Check to see if a component has been added.
*
* @param componentName
* name of the component
*
* @throws InteractiveSpacesException
* the component was added already
*/
private void checkIfComponentAdded(String componentName) throws InteractiveSpacesException {
// There's a subtle behavior here in the case where two activity components
// with the same name are dependencies of a single dependent node. In that
// case, only one of the dependencies needs to be resolved before the
// dependent node, which isn't the correct behavior in the strict sense of a
// dependency. This behavior isn't really supported by the system since
// activity components are generally considered to be singletons. Therefore,
// help aid development by detecting the case of multiple components and
// throwing an error.
if (addedComponents.containsKey(componentName)) {
throw new SimpleInteractiveSpacesException("Multiple activity components added for name " + componentName);
}
}
/**
* Do the initial startup of components.
*
* @throws Throwable
* an internal startup error
*/
public void initialStartupComponents() throws Throwable {
configureComponents();
startupComponents();
}
/**
* Configure all the components in the in the collection in dependency order.
*/
private void configureComponents() {
if (!configuredComponents.isEmpty()) {
throw new SimpleInteractiveSpacesException("Attempt to configure already configured components");
}
DependencyResolver<String, ActivityComponent> resolver = new DependencyResolver<String, ActivityComponent>();
for (ActivityComponent component : addedComponents.values()) {
resolver.addNode(component.getName(), component);
List<String> dependencies = component.getDependencies();
if (dependencies != null) {
resolver.addNodeDependencies(component.getName(), dependencies);
}
}
resolver.resolve();
for (ActivityComponent component : resolver.getOrdering()) {
// There will be null components if addNode() was never called.
// This means the component wasn't added but was a dependency.
// Only the component which had it as a dependency knows if it
// is required or not.
if (component != null) {
component.configureComponent(activity.getConfiguration());
configuredComponents.add(component);
}
}
}
/**
* Startup all components in the container.
*
* @throws Throwable
* on internal startup error
*/
private void startupComponents() throws Throwable {
List<ActivityComponent> startedComponents = Lists.newArrayList();
try {
for (ActivityComponent component : configuredComponents) {
startupComponent(component);
startedComponents.add(component);
}
} catch (Throwable e) {
// Every component that was actually started up should be shut down.
for (ActivityComponent component : startedComponents) {
try {
component.shutdownComponent();
} catch (Throwable t) {
handleComponentError(component, "Error shutting down after startup failure", t);
}
}
throw e;
}
}
/**
* Start up a component.
*
* @param component
* the component to start
*
* @throws Exception
* something bad happened
*/
private void startupComponent(ActivityComponent component) throws Exception {
try {
activity.getLog().info("Starting component " + component.getName());
component.startupComponent();
} catch (Exception e) {
handleComponentError(component, "Error starting component", e);
throw e;
}
}
/**
* Shutdown all components in the container.
*
* @return {@code true} if all components properly shut down.
*/
public boolean shutdownComponents() {
boolean properlyShutDown = true;
for (ActivityComponent component : configuredComponents) {
try {
component.shutdownComponent();
} catch (Exception e) {
properlyShutDown = false;
handleComponentError(component, "Error during activity component shutdown", e);
}
}
return properlyShutDown;
}
/**
* Clear all components from the container.
*/
public void clear() {
addedComponents.clear();
configuredComponents.clear();
}
/**
* Shutdown all components from the container and then clear them.
*
* @return {@code true} if all components properly shut down.
*/
public boolean shutdownAndClear() {
boolean result = shutdownComponents();
clear();
return result;
}
/**
* Shutdown all running components from the container and then clear them.
*
* @return {@code true} if all components properly shut down.
*/
public boolean shutdownAndClearRunningComponents() {
boolean properlyShutDown = true;
for (ActivityComponent component : configuredComponents) {
try {
if (component.isComponentRunning()) {
component.shutdownComponent();
}
} catch (Exception e) {
properlyShutDown = false;
handleComponentError(component, "Error during activity component shutdown", e);
}
}
clear();
return properlyShutDown;
}
/**
* Are all required components running?
*
* @return {@code true} if all required components are running.
*/
public boolean areAllComponentsRunning() {
boolean areAllRunning = true;
for (ActivityComponent component : configuredComponents) {
if (!component.isComponentRunning()) {
areAllRunning = false;
handleComponentError(component, "Activity component not running when expected", null);
}
}
return areAllRunning;
}
/**
* Get an activity component from the collection.
*
* @param componentType
* type of the component
* @param <T>
* specific type of activity component
*
* @return the component with the given name or {@code null} if not present.
*/
@SuppressWarnings("unchecked")
public <T extends ActivityComponent> T getActivityComponent(String componentType) {
return (T) addedComponents.get(componentType);
}
/**
* Get an activity component from the collection.
*
* @param componentType
* type of the component
* @param <T>
* type of activity component
*
* @return the component with the given name
*
* @throws SimpleInteractiveSpacesException
* if named component is not present
*/
public <T extends ActivityComponent> T getRequiredActivityComponent(String componentType)
throws SimpleInteractiveSpacesException {
T component = getActivityComponent(componentType);
if (component == null) {
throw new SimpleInteractiveSpacesException("Could not find component " + componentType);
}
return component;
}
/**
* Return all the configured components.
*
* @return all configured components
*/
public Collection<ActivityComponent> getConfiguredComponents() {
return Lists.newArrayList(configuredComponents);
}
/**
* Handle a component error for the given throwable. Makes sure the log object from the component is valid, otherwise
* use the global log.
*
* @param component
* component with error
* @param message
* error message
* @param t
* cause of the error/exception
*/
private void handleComponentError(ActivityComponent component, String message, Throwable t) {
activity.getLog().error(String.format("%s (%s)", message, component.getName()), t);
}
}