From d97f85547a4c01c3fa2f59ea28558faf80a8a18d Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Mon, 8 Apr 2013 11:38:43 -0300 Subject: [PATCH] FORGE-848: Added @CommandScoped support --- .../forge/shell/command/CommandScoped.java | 38 ++++ .../forge/shell/events/CommandExecuted.java | 19 +- .../forge/shell/events/CommandVetoed.java | 71 +++++++ .../shell/events/PreCommandExecution.java | 17 +- .../shell/command/CommandScopedContext.java | 177 ++++++++++++++++++ .../shell/command/CommandScopedExtension.java | 28 +++ .../jboss/forge/shell/command/Execution.java | 20 +- .../javax.enterprise.inject.spi.Extension | 1 + .../test/command/CommandScopedObject.java | 44 +++++ .../test/command/CommandScopedPlugin.java | 33 ++++ .../shell/test/command/CommandScopedTest.java | 26 +++ 11 files changed, 456 insertions(+), 18 deletions(-) create mode 100644 shell-api/src/main/java/org/jboss/forge/shell/command/CommandScoped.java create mode 100644 shell-api/src/main/java/org/jboss/forge/shell/events/CommandVetoed.java create mode 100644 shell/src/main/java/org/jboss/forge/shell/command/CommandScopedContext.java create mode 100644 shell/src/main/java/org/jboss/forge/shell/command/CommandScopedExtension.java create mode 100644 shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedObject.java create mode 100644 shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedPlugin.java create mode 100644 shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedTest.java diff --git a/shell-api/src/main/java/org/jboss/forge/shell/command/CommandScoped.java b/shell-api/src/main/java/org/jboss/forge/shell/command/CommandScoped.java new file mode 100644 index 0000000000..828c203565 --- /dev/null +++ b/shell-api/src/main/java/org/jboss/forge/shell/command/CommandScoped.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.jboss.forge.shell.command; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.enterprise.context.NormalScope; + +import org.jboss.forge.shell.plugins.Command; + +/** + * Declares a bean as being scoped to the current {@link Command}. Beans using this scope will be destroyed when the + * current {@link Command} finishes executing. The scope is active as long as the command is being executed. + * + * @author George Gastaldi + */ +@NormalScope(passivating = false) +@Inherited +@Documented +@Target({ TYPE, METHOD, FIELD }) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface CommandScoped +{ + +} diff --git a/shell-api/src/main/java/org/jboss/forge/shell/events/CommandExecuted.java b/shell-api/src/main/java/org/jboss/forge/shell/events/CommandExecuted.java index 511a2d9a28..8c57fc15b3 100644 --- a/shell-api/src/main/java/org/jboss/forge/shell/events/CommandExecuted.java +++ b/shell-api/src/main/java/org/jboss/forge/shell/events/CommandExecuted.java @@ -7,14 +7,16 @@ package org.jboss.forge.shell.events; +import java.util.Map; + import org.jboss.forge.shell.command.CommandMetadata; /** * Fired after a plugin/command has been executed and has finished processing. - * + * * @author Lincoln Baxter, III * @author Koen Aers - * + * */ public final class CommandExecuted { @@ -27,18 +29,16 @@ public enum Status private CommandMetadata command; private Object[] parameters; private String originalStatement; - - public CommandExecuted() - { - } + private Map context; public CommandExecuted(final Status status, final CommandMetadata command, final String originalStatement, - Object[] parameters) + Object[] parameters, Map context) { this.status = status; this.command = command; this.originalStatement = originalStatement; this.parameters = parameters; + this.context = context; } public Status getStatus() @@ -60,4 +60,9 @@ public String getOriginalStatement() { return originalStatement; } + + public Map getContext() + { + return context; + } } diff --git a/shell-api/src/main/java/org/jboss/forge/shell/events/CommandVetoed.java b/shell-api/src/main/java/org/jboss/forge/shell/events/CommandVetoed.java new file mode 100644 index 0000000000..28fd08572d --- /dev/null +++ b/shell-api/src/main/java/org/jboss/forge/shell/events/CommandVetoed.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.jboss.forge.shell.events; + +import java.util.Map; + +import org.jboss.forge.shell.command.CommandMetadata; + +/** + * Fired when a command is vetoed + * + * @author George Gastaldi + * + */ +public class CommandVetoed +{ + private CommandMetadata command; + private Object[] parameters; + private String originalStatement; + private Map context; + + public CommandVetoed(CommandMetadata command, Object[] parameters, String originalStatement, + Map context) + { + super(); + this.command = command; + this.parameters = parameters; + this.originalStatement = originalStatement; + this.context = context; + } + + public CommandMetadata getCommand() + { + return command; + } + + public void setCommand(CommandMetadata command) + { + this.command = command; + } + + public Object[] getParameters() + { + return parameters; + } + + public void setParameters(Object[] parameters) + { + this.parameters = parameters; + } + + public String getOriginalStatement() + { + return originalStatement; + } + + public void setOriginalStatement(String originalStatement) + { + this.originalStatement = originalStatement; + } + + public Map getContext() + { + return context; + } +} diff --git a/shell-api/src/main/java/org/jboss/forge/shell/events/PreCommandExecution.java b/shell-api/src/main/java/org/jboss/forge/shell/events/PreCommandExecution.java index 1db4963097..7b2255c150 100644 --- a/shell-api/src/main/java/org/jboss/forge/shell/events/PreCommandExecution.java +++ b/shell-api/src/main/java/org/jboss/forge/shell/events/PreCommandExecution.java @@ -7,11 +7,13 @@ package org.jboss.forge.shell.events; +import java.util.Map; + import org.jboss.forge.shell.command.CommandMetadata; /** * Fired before a plugin/command is executed - * + * * @author George Gastaldi */ public final class PreCommandExecution @@ -20,17 +22,15 @@ public final class PreCommandExecution private Object[] parameters; private String originalStatement; private boolean vetoed; - - public PreCommandExecution() - { - } + private Map context; public PreCommandExecution(final CommandMetadata command, final String originalStatement, - Object[] parameters) + Object[] parameters, Map context) { this.command = command; this.originalStatement = originalStatement; this.parameters = parameters; + this.context = context; } public CommandMetadata getCommand() @@ -60,4 +60,9 @@ public void veto() { this.vetoed = true; } + + public Map getContext() + { + return context; + } } diff --git a/shell/src/main/java/org/jboss/forge/shell/command/CommandScopedContext.java b/shell/src/main/java/org/jboss/forge/shell/command/CommandScopedContext.java new file mode 100644 index 0000000000..b6920f861b --- /dev/null +++ b/shell/src/main/java/org/jboss/forge/shell/command/CommandScopedContext.java @@ -0,0 +1,177 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.jboss.forge.shell.command; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Stack; +import java.util.concurrent.ConcurrentHashMap; + +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.spi.Context; +import javax.enterprise.context.spi.Contextual; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.event.Observes; +import javax.inject.Singleton; + +import org.jboss.forge.shell.events.CommandExecuted; +import org.jboss.forge.shell.events.CommandVetoed; +import org.jboss.forge.shell.events.PreCommandExecution; + +/** + * This class provides lifecycle management for {@link CommandScoped} objects + * + * @author George Gastaldi + */ +@Singleton +public class CommandScopedContext implements Context +{ + private final static String COMPONENT_MAP_NAME = CommandScopedContext.class.getName() + ".componentInstanceMap"; + private final static String CREATIONAL_MAP_NAME = CommandScopedContext.class.getName() + ".creationalInstanceMap"; + private static final Stack> contextStack = new Stack>(); + + private void assertActive() + { + if (!isActive()) + { + throw new ContextNotActiveException( + "Context with scope annotation @CommandScoped is not active since no command is in execution."); + } + } + + public Map getCurrentContext() + { + return contextStack.peek(); + } + + public void create(@Observes final PreCommandExecution execution) + { + contextStack.push(execution.getContext()); + } + + public void destroy(@Observes final CommandExecuted event) + { + destroyCurrentContext(); + contextStack.pop(); + } + + public void destroy(@Observes final CommandVetoed event) + { + destroyCurrentContext(); + contextStack.pop(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void destroyCurrentContext() + { + Map, Object> componentInstanceMap = getComponentInstanceMap(); + Map, CreationalContext> creationalContextMap = getCreationalContextMap(); + + if ((componentInstanceMap != null) && (creationalContextMap != null)) + { + for (Entry, Object> componentEntry : componentInstanceMap.entrySet()) + { + Contextual contextual = componentEntry.getKey(); + Object instance = componentEntry.getValue(); + CreationalContext creational = creationalContextMap.get(contextual); + + contextual.destroy(instance, creational); + } + } + getCurrentContext().clear(); + } + + /* + * Context Methods + */ + + @Override + public boolean isActive() + { + return !contextStack.isEmpty(); + } + + @Override + public Class getScope() + { + return CommandScoped.class; + } + + @Override + @SuppressWarnings("unchecked") + public T get(final Contextual component) + { + assertActive(); + return (T) getComponentInstanceMap().get(component); + } + + @Override + @SuppressWarnings("unchecked") + public T get(final Contextual component, final CreationalContext creationalContext) + { + assertActive(); + + T instance = get(component); + + if (instance == null) + { + Map, CreationalContext> creationalContextMap = getCreationalContextMap(); + Map, Object> componentInstanceMap = getComponentInstanceMap(); + + synchronized (componentInstanceMap) + { + instance = (T) componentInstanceMap.get(component); + if (instance == null) + { + instance = component.create(creationalContext); + + if (instance != null) + { + componentInstanceMap.put(component, instance); + creationalContextMap.put(component, creationalContext); + } + } + } + } + + return instance; + } + + /* + * Helpers for manipulating the Component/Context maps. + */ + @SuppressWarnings("unchecked") + private Map, Object> getComponentInstanceMap() + { + ConcurrentHashMap, Object> map = (ConcurrentHashMap, Object>) getCurrentContext() + .get(COMPONENT_MAP_NAME); + + if (map == null) + { + map = new ConcurrentHashMap, Object>(); + getCurrentContext().put(COMPONENT_MAP_NAME, map); + } + + return map; + } + + @SuppressWarnings("unchecked") + private Map, CreationalContext> getCreationalContextMap() + { + Map, CreationalContext> map = (ConcurrentHashMap, CreationalContext>) getCurrentContext() + .get(CREATIONAL_MAP_NAME); + + if (map == null) + { + map = new ConcurrentHashMap, CreationalContext>(); + getCurrentContext().put(CREATIONAL_MAP_NAME, map); + } + + return map; + } +} \ No newline at end of file diff --git a/shell/src/main/java/org/jboss/forge/shell/command/CommandScopedExtension.java b/shell/src/main/java/org/jboss/forge/shell/command/CommandScopedExtension.java new file mode 100644 index 0000000000..a835bbca24 --- /dev/null +++ b/shell/src/main/java/org/jboss/forge/shell/command/CommandScopedExtension.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.jboss.forge.shell.command; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Extension; + +/** + * An extension to provide {@link CommandScoped} support. + * + * @author George Gastaldi + * + */ +public class CommandScopedExtension implements Extension +{ + + public void registerContext(@Observes final AfterBeanDiscovery event) + { + CommandScopedContext context = new CommandScopedContext(); + event.addContext(context); + } + +} diff --git a/shell/src/main/java/org/jboss/forge/shell/command/Execution.java b/shell/src/main/java/org/jboss/forge/shell/command/Execution.java index ed54f1c2ae..5fe309a7b1 100644 --- a/shell/src/main/java/org/jboss/forge/shell/command/Execution.java +++ b/shell/src/main/java/org/jboss/forge/shell/command/Execution.java @@ -9,6 +9,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import javax.enterprise.context.spi.CreationalContext; @@ -22,6 +24,7 @@ import org.jboss.forge.shell.events.CommandExecuted; import org.jboss.forge.shell.events.CommandExecuted.Status; import org.jboss.forge.shell.events.CommandMissing; +import org.jboss.forge.shell.events.CommandVetoed; import org.jboss.forge.shell.events.PreCommandExecution; import org.jboss.forge.shell.exceptions.CommandExecutionException; import org.jboss.forge.shell.plugins.AliasLiteral; @@ -92,7 +95,7 @@ public void perform(final PipeOut pipeOut) { paramStaging[i] = Enums.valueOf(parmTypes[i], parameterArray[i]); } - else if(parmTypes[i].isArray() && parmTypes[i].getComponentType().isEnum()) + else if (parmTypes[i].isArray() && parmTypes[i].getComponentType().isEnum()) { Object[] array = (Object[]) parameterArray[i]; if (array != null) @@ -143,11 +146,13 @@ else if(parmTypes[i].isArray() && parmTypes[i].getComponentType().isEnum()) Status status = Status.FAILURE; ClassLoader current = Thread.currentThread().getContextClassLoader(); + Map executionContext = new HashMap(); boolean vetoed = false; try { Thread.currentThread().setContextClassLoader(plugin.getClass().getClassLoader()); - PreCommandExecution event = new PreCommandExecution(command, originalStatement, parameterArray); + PreCommandExecution event = new PreCommandExecution(command, originalStatement, parameterArray, + executionContext); manager.fireEvent(event, new Annotation[0]); vetoed = event.isVetoed(); if (!vetoed) @@ -163,11 +168,16 @@ else if(parmTypes[i].isArray() && parmTypes[i].getComponentType().isEnum()) finally { Thread.currentThread().setContextClassLoader(current); - if (!vetoed) + if (vetoed) + { + manager.fireEvent(new CommandVetoed(command, parameterArray, originalStatement, executionContext)); + } + else { - manager.fireEvent(new CommandExecuted(status, command, originalStatement, parameterArray), - new Annotation[0]); + manager.fireEvent(new CommandExecuted(status, command, originalStatement, parameterArray, + executionContext)); } + executionContext.clear(); } } } diff --git a/shell/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/shell/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension index df2440de0c..fedd92af95 100644 --- a/shell/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension +++ b/shell/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -1,4 +1,5 @@ org.jboss.forge.shell.command.CommandLibraryExtension +org.jboss.forge.shell.command.CommandScopedExtension org.jboss.forge.shell.project.ProjectScopedExtension org.jboss.forge.shell.project.resources.ResourceProducerExtension org.jboss.forge.shell.project.resources.ResourceScopedExtension \ No newline at end of file diff --git a/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedObject.java b/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedObject.java new file mode 100644 index 0000000000..d50ec81a75 --- /dev/null +++ b/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedObject.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.jboss.forge.shell.test.command; + +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.jboss.forge.shell.command.CommandScoped; + +@CommandScoped +public class CommandScopedObject +{ + private static final AtomicInteger COUNTER = new AtomicInteger(); + private int value; + + public CommandScopedObject() + { + } + + @PostConstruct + void create() + { + value = COUNTER.incrementAndGet(); + + } + + public int getValue() + { + return value; + } + + @PreDestroy + void destroy() + { + COUNTER.decrementAndGet(); + } +} diff --git a/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedPlugin.java b/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedPlugin.java new file mode 100644 index 0000000000..bb9008159e --- /dev/null +++ b/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedPlugin.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.jboss.forge.shell.test.command; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import org.jboss.forge.shell.plugins.Alias; +import org.jboss.forge.shell.plugins.DefaultCommand; +import org.jboss.forge.shell.plugins.PipeOut; +import org.jboss.forge.shell.plugins.Plugin; + +/** + * @author Lincoln Baxter, III + */ +@Alias("cmdscope") +public class CommandScopedPlugin implements Plugin +{ + + @Inject + Instance cmdScopedObj; + + @DefaultCommand + public void testMock(PipeOut out) + { + int value = cmdScopedObj.get().getValue(); + out.println("" + value); + } +} diff --git a/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedTest.java b/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedTest.java new file mode 100644 index 0000000000..65646ea27a --- /dev/null +++ b/shell/src/test/java/org/jboss/forge/shell/test/command/CommandScopedTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Eclipse Public License version 1.0, available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package org.jboss.forge.shell.test.command; + +import org.jboss.forge.test.AbstractShellTest; +import org.junit.Assert; +import org.junit.Test; + +public class CommandScopedTest extends AbstractShellTest +{ + + @Test + public void testCommandScoped() throws Exception + { + getShell().execute("cmdscope"); + Assert.assertTrue(getOutput().contains("1")); + resetOutput(); + getShell().execute("cmdscope"); + Assert.assertTrue(getOutput().contains("1")); + } +}