/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2008-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.provision.service.lifecycle;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.Vector;
import java.util.concurrent.Executors;
import org.junit.Before;
import org.junit.Test;
import org.opennms.core.tasks.DefaultTaskCoordinator;
import org.opennms.core.test.MockLogAppender;
import org.opennms.netmgt.provision.service.lifecycle.annotations.Activity;
import org.opennms.netmgt.provision.service.lifecycle.annotations.ActivityProvider;
import org.opennms.netmgt.provision.service.lifecycle.annotations.Attribute;
/**
* LifecycleTest
*
* @author brozow
*/
public class LifeCycleInstanceTest {
/*
* TODO
* - Use parameter type info to inject the appropriate arguments
* - Add return values to attribute lists
* - Use annotations to disambiguate attribute parameters
* - Use annotations to assign attribute name to return values
* - LifeCycle return value automatically triggered
* - Task return value automatically scheduled
* - Use a CompletionService and Executor to run phases in the background
* - Make waitFor really work for things in the background
* - Run phases asynchronously
* - Make phases depend-on previous phases
* - Make a lifeCycle definition DAO
* - provide a way to locate ActivityProviders
* - Wrap the annotation driven strategy in an ActitivyProvider class
* programmatic providers can all be used. This will enable
* 'publishing' a provider as an interface in OSGI
*
*
*/
// activity(LifeCycle lifeCycle, OnmsIpInterface iface)
// if there is only one attribute that contains that type then
// pass it in as an argument. Should be able to annotate the
// type as well
// if would be good if return values could be automatically added
// to the lifecycle don't know what attribute that would use but
// we could define that with an annotation... if it matters.. could
// be that type info is enough
// need a way to handle nested lifecycles without waiting...
//
// mark an activity as 'asynchronous=true' this would run
// without waiting...
// mark an activity as 'depends-on='previous task' this would
// force it to wait for other activities that are running in the background
// Activities can return LifeCycles (or maybe more generic Tasks??). If the
// activities are synchronous then the next phase doesn't run into those tasks
// are all complete.
// If they are asynchronous then activities that depend on the phase cannot
// run until all the return LifeCycles/Tasks are completed
// Does having subphases make sense? Or having 'foreach' tasks that get fired
// when something gets added?
// Another NOTE: Callbacks and task scheduling implemented around a CompletionService
// The CompletionService has and executor behind it running tasks and a thread whose job
// it is to process completed tasks and schedule the next. This would make the schronization
// requirements of the task tracking much easy to keep thread safe.
// Can the importer be implemented with this mechanism?
/* class ImportActitivies {
*
* public SpecFile parseResource(OnmsResource resource) {
* // parse and create SpecFile
* // assert SpecFile
* }
*
* public void auditNode(SpecFile specFile) {
* // retrieve ForeignId -> NodeId Map
* // as I go thru the nodes in the specFile
* // I end up with three groups of nodes
* // one set to delete
* // one set to scan and then update
* // one set to scan and then insert
* //
* // I could make each of these a 'lifecycle' of their own
* // a trick is that deletes have to complete, then updates, then inserts
* // but... scanning for the nodes can happen anytime...
* //
* // also... the scan/update or scan/insert should be a 'scanNode' lifecycle
* // for sure
* //
* // so.. we could do this:
* //
* // for each node
* // /---
* // |
* // |-- scan phase... start scans for update and insert nodes... make it asynchronous
* // |
* // |-- delete phase... delete the nodes that need to be deleted (synchronous)
* // |
* // |-- update phase... update the nodes that need to be updated... depends on the scan for the node (synchronous)
* // |
* // |-- insert phase... insert the nodes that need to be inserted... depends on the scan for the node (synchronous)
* // |
* // \---
* //
* // relateNodes... relate the nodes to each other.. after they have been committed to the DB.
* }
*/
// how do we build scanners when we need to run them
// how do we find the scanners
// lifecycles should be defined in configuration
// how do we pass data into the scanner methods
// scanners should be dependency injected
// resource scanners will take the 'resource' in the scan method
// node scanners will take the 'node' in the scan method
// import scanner can take the foreign source.... how do I get the URL?
// persist lifecycles that are in progress?
/*
* A run of a lifecycle has a 'trigger' of some kind....
* 1. newSuspectEvent
* 2. forceRescanEvent
* 3. 'scheduled' rescan
* 4. 'import' event
* 5. 'scheduled' import
* 6. import triggers a node scan
* 7 node scan triggers a service scan
*
* We can pass a trigger object into each
*
* Need to use generics in some way so that I end up with the correct type for triggers and/or other parameters
*
* I could pass the Lifecycle object into each phase method... and I could set attributes on the lifecycle object
*
* I would need to define a set of attributes that could be set for each lifecycle
*
* I could use annotations to pass those attributes as methods
*
* Maybe 'resourceFactories' could be annotated as well and implement a simple interface
*
*
*
*/
private static final String PHASE_DATA = "phaseData";
public static final String NESTED_DATA = "nestedData";
public static final String NEST_LEVEL = "nestLevel";
public static final String MAX_DEPTH = "maxDepth";
private LifeCycleRepository m_lifeCycleFactory;
@Before
public void setUp() {
MockLogAppender.setupLogging();
DefaultTaskCoordinator coordinator = new DefaultTaskCoordinator("LifeCycleInstanceTest", Executors.newFixedThreadPool(10));
DefaultLifeCycleRepository repository = new DefaultLifeCycleRepository(coordinator);
LifeCycle lifeCycle = new LifeCycle("sample")
.addPhases("phase1", "phase2", "phase3");
LifeCycle injection = new LifeCycle("injection")
.addPhases("phase1", "phase2", "phase3");
repository.addLifeCycle(lifeCycle);
repository.addLifeCycle(injection);
m_lifeCycleFactory = repository;
}
@ActivityProvider
public static class PhaseTestActivities extends ActivityProviderSupport {
private void appendPhase(LifeCycleInstance lifecycle, final String value) {
appendToStringAttribute(lifecycle, PHASE_DATA, value);
}
// this should be called first
@Activity(phase="phase1", lifecycle="sample")
public void doPhaseOne(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, "phase1 ");
}
// this should be called last
@Activity(phase="phase3", lifecycle = "sample")
public void doPhaseThree(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, "phase3");
}
// this should be called in the middle
@Activity(phase="phase2", lifecycle = "sample")
public void doPhaseTwo(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, "phase2 ");
}
// this should not be called
@Activity(phase="phase3", lifecycle = "invalidLifecycle")
public void doPhaseInvalidLifeCycle(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, " invalidLifecycle");
}
// this should not be called
@Activity(phase="invalidPhase", lifecycle = "sample")
public void doPhaseInvalid(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, " invalidPhase");
}
}
// waitFor should throw an exception if its not been triggered
// if we don't call trigger then the getAttribute should return ""
@Test
public void testLifeCycleAttributes() {
LifeCycleInstance lifecycle = m_lifeCycleFactory.createLifeCycleInstance("sample", new PhaseTestActivities());
lifecycle.setAttribute(PHASE_DATA, "phase1 phase2 phase3");
assertEquals("phase1 phase2 phase3", lifecycle.getAttribute(PHASE_DATA, String.class));
}
@Test
public void testTriggerLifeCycle() throws Exception {
LifeCycleInstance lifecycle = m_lifeCycleFactory.createLifeCycleInstance("sample", new PhaseTestActivities());
lifecycle.trigger();
lifecycle.waitFor();
assertEquals("phase1 phase2 phase3", lifecycle.getAttribute(PHASE_DATA, String.class));
}
@ActivityProvider
public static class InjectionTestActivities {
// this should be called first
@Activity(phase="phase1", lifecycle="injection")
@Attribute("one")
public Integer doPhaseOne(Phase phase1, Vector<String> dataAccumulator) {
dataAccumulator.add(phase1.getName());
return 1;
}
// this should be called in the middle
@Activity(phase="phase2", lifecycle = "injection")
@Attribute("two")
public Integer doPhaseTwo(Phase phase2, Vector<String> dataAccumulator) {
dataAccumulator.add(phase2.getName());
return 2;
}
// this should be called last
@Activity(phase="phase3", lifecycle = "injection")
public void doPhaseThree(@Attribute("one") Integer one, Phase phase3, Vector<String> dataAccumulator, @Attribute("two") Integer two) {
dataAccumulator.add(phase3.getName());
dataAccumulator.add(one.toString());
dataAccumulator.add(two.toString());
}
}
@Test
public void testInjectionLifeCycle() throws Exception {
LifeCycleInstance lifecycle = m_lifeCycleFactory.createLifeCycleInstance("injection", new InjectionTestActivities());
lifecycle.setAttribute("dataAccumulator", new Vector<String>());
lifecycle.trigger();
lifecycle.waitFor();
@SuppressWarnings("unchecked")
Vector<String> results = lifecycle.getAttribute("dataAccumulator" , Vector.class);
assertNotNull(results);
assertEquals(Integer.valueOf(1), lifecycle.getAttribute("one", Integer.class));
assertEquals("phase1", results.get(0));
assertEquals("phase2", results.get(1));
assertEquals("phase3", results.get(2));
assertEquals("1", results.get(3));
assertEquals("2", results.get(4));
assertEquals(5, results.size());
}
@ActivityProvider
public static class NestedLifeCycleActivites extends ActivityProviderSupport {
@SuppressWarnings("unused")
private final LifeCycleRepository m_lifeCycleRepository;
public NestedLifeCycleActivites(LifeCycleRepository lifeCycleRepository) {
m_lifeCycleRepository = lifeCycleRepository;
}
private void appendPhase(LifeCycleInstance lifecycle, final String phase) {
appendToStringAttribute(lifecycle, NESTED_DATA, phase);
}
// this should be called first
@Activity(phase="phase1", lifecycle="sample")
public void doPhaseOne(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, getPrefix(lifecycle)+"phase1 ");
}
// this should be called last
@Activity(phase="phase3", lifecycle = "sample")
public void doPhaseThree(LifeCycleInstance lifecycle) {
appendPhase(lifecycle, getPrefix(lifecycle)+"phase3 ");
}
// this should be called in the middle
@Activity(phase="phase2", lifecycle = "sample")
public LifeCycleInstance doPhaseTwo(LifeCycleInstance lifecycle, Phase currentPhase) throws Exception {
appendPhase(lifecycle, getPrefix(lifecycle)+"phase2start ");
LifeCycleInstance nested = null;
int nestLevel = lifecycle.getAttribute(NEST_LEVEL, 0);
int maxDepth = lifecycle.getAttribute(MAX_DEPTH, 0);
if (nestLevel < maxDepth) {
nested = currentPhase.createNestedLifeCycle("sample");
nested.setAttribute(MAX_DEPTH, maxDepth);
nested.setAttribute(NEST_LEVEL, nestLevel+1);
//fail("I'd like to have trigger by called by the framework rather than here...");
nested.trigger();
nested.waitFor();
appendPhase(lifecycle, nested.getAttribute(NESTED_DATA, String.class));
}
appendPhase(lifecycle, getPrefix(lifecycle)+"phase2end ");
return nested;
}
private String getPrefix(LifeCycleInstance lifecycle) {
int nestLevel = lifecycle.getAttribute(NEST_LEVEL, 0);
return buildPrefix(nestLevel);
}
private String buildPrefix(int nestLevel) {
StringBuilder buf = new StringBuilder();
buildPrefixHelper(nestLevel, buf);
return buf.toString();
}
private void buildPrefixHelper(int nestLevel, StringBuilder buf) {
if (nestLevel == 0) {
return;
} else {
buildPrefixHelper(nestLevel-1, buf);
buf.append("level").append(nestLevel).append('.');
}
}
}
@Test
public void testNestedLifeCycle() throws Exception {
LifeCycleInstance lifecycle = m_lifeCycleFactory.createLifeCycleInstance("sample", new NestedLifeCycleActivites(m_lifeCycleFactory));
lifecycle.setAttribute(MAX_DEPTH, 1);
lifecycle.trigger();
lifecycle.waitFor();
assertEquals("phase1 phase2start level1.phase1 level1.phase2start level1.phase2end level1.phase3 phase2end phase3 ", lifecycle.getAttribute(NESTED_DATA, String.class));
}
public static class ActivityProviderSupport {
protected void appendToStringAttribute(LifeCycleInstance lifecycle, String key, String value) {
lifecycle.setAttribute(key, lifecycle.getAttribute(key, "")+value);
}
}
}