/*
* RHQ Management Platform
* Copyright (C) 2005-2013 Red Hat, Inc.
* All rights reserved.
*
* This program 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 version 2 of the License.
*
* This program 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 this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package org.rhq.test.arquillian;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.rhq.core.clientapi.agent.upgrade.ResourceUpgradeRequest;
import org.rhq.core.clientapi.agent.upgrade.ResourceUpgradeResponse;
import org.rhq.core.clientapi.server.discovery.InventoryReport;
import org.rhq.core.domain.discovery.MergeInventoryReportResults;
import org.rhq.core.domain.discovery.MergeResourceResponse;
import org.rhq.core.domain.discovery.PlatformSyncInfo;
import org.rhq.core.domain.discovery.ResourceSyncInfo;
import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.DisplayType;
import org.rhq.core.domain.measurement.MeasurementDefinition;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.measurement.NumericType;
import org.rhq.core.domain.measurement.ResourceMeasurementScheduleRequest;
import org.rhq.core.domain.resource.InventoryStatus;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceError;
import org.rhq.core.domain.resource.ResourceErrorType;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.pc.inventory.InventoryManager;
/**
* This class represents a server side database store of the inventory for the purposes
* of the unit tests that need to mock out the server functionality.
* <p>
* The methods are not exhaustive and were added on as-needed basis. If this class doesn't
* cover what you need, either add the functionality directly to it, subclass or roll your
* own. It is only meant as a helper.
* <p>
* This impl uses mockito for defining the answers to various calls.
*
* @author Lukas Krejci
*/
public class FakeServerInventory {
private static final Log LOG = LogFactory.getLog(FakeServerInventory.class);
/**
* You can {@link #waitForDiscoveryComplete()} on an instance of this class
* for the complete discovery to finish in case your
* fake server commits some resources (which starts off
* asynchronous discovery of children).
*
*
* @author Lukas Krejci
*/
public static class CompleteDiscoveryChecker {
private volatile boolean depthReached;
private final int expectedDepth;
private final Object sync = new Object();
private volatile boolean finished;
public CompleteDiscoveryChecker(int expectedDepth) {
this.expectedDepth = expectedDepth;
this.depthReached = expectedDepth == 0;
}
public void waitForDiscoveryComplete() throws InterruptedException {
waitForDiscoveryComplete(0);
}
public void waitForDiscoveryComplete(long timeoutMillis) throws InterruptedException {
synchronized (sync) {
if (!depthReached) {
if (LOG.isDebugEnabled()) {
LOG.debug("Waiting for the discovery depth to be reached on " + this);
}
sync.wait(timeoutMillis);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Discovery depth already reached... no need to wait on " + this);
}
}
finished = true;
}
}
public int getExpectedDepth() {
return expectedDepth;
}
private void setDepth(int resourceTreeDepth) {
synchronized (sync) {
if (LOG.isDebugEnabled()) {
LOG.debug("Current tree depth is " + resourceTreeDepth + ", this checker is waiting for "
+ expectedDepth + " on " + this);
}
if (!depthReached && resourceTreeDepth >= expectedDepth) {
depthReached = true;
new Thread(new Runnable() {
@Override
public void run() {
try {
//just way some more to give the PC time to really finish
//the handling of the inventory report.
//I know this sucks and is prone to races but we cannot
//properly implement this without modifying the plugin container.
if (LOG.isDebugEnabled()) {
LOG.debug("Ad-hoc wait for discovery to really complete...");
}
Thread.sleep(500);
} catch (InterruptedException e) {
//well, we are going to finish in a few anyway
} finally {
synchronized (sync) {
if (!finished) {
if (LOG.isDebugEnabled()) {
LOG.debug("Notifying about discovery complete on "
+ CompleteDiscoveryChecker.this);
}
sync.notifyAll();
}
}
}
}
}).start();
}
}
}
}
private Resource platform;
private Map<String, Resource> resourceStore = new HashMap<String, Resource>();
//private int counter;
private AtomicInteger metricScheduleCounter = new AtomicInteger(1); // used when creating measurement schedules
private boolean failing;
private boolean failUpgrade;
private CompleteDiscoveryChecker discoveryChecker;
private static final Comparator<Resource> ID_COMPARATOR = new Comparator<Resource>() {
@Override
public int compare(Resource o1, Resource o2) {
return o1.getId() - o2.getId();
}
};
private static final Comparator<Resource> RESOURCE_TYPE_COMPARATOR = new Comparator<Resource>() {
@Override
public int compare(Resource o1, Resource o2) {
return o1.getResourceType().equals(o2.getResourceType()) ? 0 : o1.getId() - o2.getId();
}
};
public FakeServerInventory() {
this(false);
}
public FakeServerInventory(boolean failing) {
this.failing = failing;
}
public synchronized void prepopulateInventory(Resource platform, Collection<Resource> topLevelServers) {
this.platform = fakePersist(platform, InventoryStatus.COMMITTED, new HashSet<String>());
for (Resource res : topLevelServers) {
res.setParentResource(this.platform);
fakePersist(res, InventoryStatus.COMMITTED, new HashSet<String>());
}
}
public CompleteDiscoveryChecker createAsyncDiscoveryCompletionChecker(int expectedResourceTreeDepth) {
discoveryChecker = new CompleteDiscoveryChecker(expectedResourceTreeDepth);
if (LOG.isDebugEnabled()) {
LOG.debug("Created a new discovery complete checker with tree depth " + expectedResourceTreeDepth + ": "
+ discoveryChecker);
}
return discoveryChecker;
}
public synchronized Answer<MergeResourceResponse> addResource() {
return new Answer<MergeResourceResponse>() {
@Override
public MergeResourceResponse answer(InvocationOnMock invocation) throws Throwable {
Resource r = (Resource) invocation.getArguments()[0];
int subjectId = (Integer) invocation.getArguments()[1];
LOG.debug("A request to add a resource [" + r + "] made by subject with id " + subjectId);
boolean exists = getResourceStore().containsKey(r.getUuid());
r = fakePersist(r, InventoryStatus.COMMITTED, new HashSet<String>());
return new MergeResourceResponse(r.getId(), (exists ? r.getMtime() : r.getCtime()), exists);
}
};
}
public synchronized Answer<MergeInventoryReportResults> mergeInventoryReport(
final InventoryStatus requiredInventoryStatus) {
return new Answer<MergeInventoryReportResults>() {
@Override
public MergeInventoryReportResults answer(InvocationOnMock invocation) throws Throwable {
synchronized (FakeServerInventory.this) {
InventoryReport inventoryReport = (InventoryReport) invocation.getArguments()[0];
try {
throwIfFailing();
for (Resource res : inventoryReport.getAddedRoots()) {
Resource persisted = fakePersist(res, requiredInventoryStatus, new HashSet<String>());
if (res.getParentResource() == Resource.ROOT) {
platform = persisted;
}
}
return new MergeInventoryReportResults(getPlatformSyncInfo(), null);
} finally {
if (discoveryChecker != null && !inventoryReport.getAddedRoots().isEmpty()) {
discoveryChecker.setDepth(getResourceTreeDepth());
}
}
}
}
};
}
private void printPersistedResource(Resource r) {
String indent = "PERSISTED ";
Resource parent = r.getParentResource();
while (null != parent) {
indent += "..";
parent = parent.getParentResource();
}
ResourceType rt = r.getResourceType();
System.out.println(indent + r.getName() + ":" + r.getResourceKey() + ", type=["
+ ((null == rt) ? "unknown" : (rt.getPlugin() + ":" + rt.getName())) + "] at " + (new Date()));
}
public synchronized Answer<Collection<ResourceSyncInfo>> getResourceSyncInfo() {
return new Answer<Collection<ResourceSyncInfo>>() {
@Override
public Collection<ResourceSyncInfo> answer(InvocationOnMock invocation) throws Throwable {
synchronized (FakeServerInventory.this) {
Integer resourceId = (Integer) invocation.getArguments()[0];
try {
throwIfFailing();
for (Resource c : platform.getChildResources()) {
if (c.getId() == resourceId) {
return getResourceSyncInfo(c);
}
}
return null;
} finally {
// TODO: We may need to actually do a check here to make sure we've been invoked once
// for every top level server, before setting depth...
if (discoveryChecker != null) {
discoveryChecker.setDepth(getResourceTreeDepth());
}
}
}
}
};
}
public synchronized int getResourceTreeDepth() {
if (platform == null) {
return 0;
}
// dumpTree(platform, "");
return getTreeDepth(platform);
}
/*
private static void dumpTree(Resource r, String indent) {
System.out.println(indent + r.getName());
indent += " ";
for (Resource c : r.getChildResources()) {
dumpTree(c, indent);
}
}
*/
private static int getTreeDepth(Resource root) {
int maxDepth = 0;
for (Resource c : root.getChildResources()) {
int childDepth = getTreeDepth(c);
if (maxDepth < childDepth) {
maxDepth = childDepth;
}
}
return maxDepth + 1;
}
public synchronized Answer<PlatformSyncInfo> clearPlatform() {
return new Answer<PlatformSyncInfo>() {
@Override
public PlatformSyncInfo answer(InvocationOnMock invocation) throws Throwable {
synchronized (FakeServerInventory.this) {
throwIfFailing();
platform = null;
return getPlatformSyncInfo();
}
}
};
}
public synchronized Answer<Void> setResourceError() {
return new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
synchronized (FakeServerInventory.this) {
throwIfFailing();
ResourceError error = (ResourceError) invocation.getArguments()[0];
Resource serverSideResource = resourceStore.get(error.getResource().getUuid());
if (serverSideResource != null) {
List<ResourceError> currentErrors = serverSideResource.getResourceErrors();
currentErrors.add(error);
}
return null;
}
}
};
}
public synchronized Answer<Set<ResourceUpgradeResponse>> upgradeResources() {
return new Answer<Set<ResourceUpgradeResponse>>() {
@Override
@SuppressWarnings({ "serial", "unchecked" })
public Set<ResourceUpgradeResponse> answer(InvocationOnMock invocation) throws Throwable {
synchronized (FakeServerInventory.this) {
throwIfFailing();
if (failUpgrade) {
throw new RuntimeException("Failing the upgrade purposefully.");
}
Object[] args = invocation.getArguments();
Set<ResourceUpgradeRequest> requests = (Set<ResourceUpgradeRequest>) args[0];
Set<ResourceUpgradeResponse> responses = new HashSet<ResourceUpgradeResponse>();
for (final ResourceUpgradeRequest request : requests) {
Resource resource = findResource(platform, new Resource() {
@Override
public int getId() {
return request.getResourceId();
}
}, ID_COMPARATOR);
if (resource != null) {
ResourceUpgradeResponse resp = new ResourceUpgradeResponse();
resp.setResourceId(resource.getId());
if (request.getNewDescription() != null) {
resource.setDescription(request.getNewDescription());
resp.setUpgradedResourceDescription(resource.getDescription());
}
if (request.getNewVersion() != null) {
resource.setVersion(request.getNewVersion());
resp.setUpgradedResourceVersion(resource.getVersion());
}
if (request.getNewName() != null) {
resource.setName(request.getNewName());
resp.setUpgradedResourceName(resource.getName());
}
if (request.getNewResourceKey() != null) {
resource.setResourceKey(request.getNewResourceKey());
resp.setUpgradedResourceKey(resource.getResourceKey());
}
if (request.getNewPluginConfiguration() != null) {
resource.setPluginConfiguration(request.getNewPluginConfiguration());
resp.setUpgradedResourcePluginConfiguration(resource.getPluginConfiguration());
}
if (request.getUpgradeErrorMessage() != null) {
ResourceError error = new ResourceError(resource, ResourceErrorType.UPGRADE,
request.getUpgradeErrorMessage(), request.getUpgradeErrorStackTrace(),
request.getTimestamp());
resource.getResourceErrors().add(error);
}
responses.add(resp);
}
}
return responses;
}
}
};
}
public synchronized Answer<Set<Resource>> getResources() {
return new Answer<Set<Resource>>() {
@Override
@SuppressWarnings("unchecked")
public Set<Resource> answer(InvocationOnMock invocation) throws Throwable {
synchronized (FakeServerInventory.this) {
throwIfFailing();
Object[] args = invocation.getArguments();
Set<Integer> resourceIds = (Set<Integer>) args[0];
boolean includeDescendants = (Boolean) args[1];
return getResources(resourceIds, includeDescendants);
}
}
};
}
// this expects the mock invocation to have two parameters - Set<Integer> resourceIds, Boolean getChildSchedules
public synchronized Answer<Set<ResourceMeasurementScheduleRequest>> getLatestSchedulesForResourceIds() {
return new Answer<Set<ResourceMeasurementScheduleRequest>>() {
@Override
@SuppressWarnings("unchecked")
public Set<ResourceMeasurementScheduleRequest> answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
Set<Integer> resourceIds = (Set<Integer>) args[0];
Boolean getChildSchedules = (Boolean) args[1];
Set<Resource> resources = getResources(resourceIds, getChildSchedules);
Set<ResourceMeasurementScheduleRequest> allSchedules = new HashSet<ResourceMeasurementScheduleRequest>();
for (Resource resource : resources) {
ResourceMeasurementScheduleRequest resourceSchedules = getDefaultMeasurementSchedules(resource);
if (resourceSchedules != null) {
allSchedules.add(resourceSchedules);
}
}
return allSchedules;
}
};
}
// this expects the mock invocation to have one parameter - Set<Integer> resourceIds
public synchronized Answer<Set<ResourceMeasurementScheduleRequest>> postProcessNewlyCommittedResources() {
return new Answer<Set<ResourceMeasurementScheduleRequest>>() {
@Override
@SuppressWarnings("unchecked")
public Set<ResourceMeasurementScheduleRequest> answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
Set<Integer> resourceIds = (Set<Integer>) args[0];
Set<Resource> resources = getResources(resourceIds, false);
Set<ResourceMeasurementScheduleRequest> allSchedules = new HashSet<ResourceMeasurementScheduleRequest>();
for (Resource resource : resources) {
ResourceMeasurementScheduleRequest resourceSchedules = getDefaultMeasurementSchedules(resource);
if (resourceSchedules != null) {
allSchedules.add(resourceSchedules);
}
}
return allSchedules;
}
};
}
private ResourceMeasurementScheduleRequest getDefaultMeasurementSchedules(Resource resource) {
ResourceType rt = resource.getResourceType();
Set<MeasurementDefinition> metrics = rt.getMetricDefinitions();
if (metrics == null || metrics.isEmpty()) {
return null;
}
ResourceMeasurementScheduleRequest resourceSchedules = new ResourceMeasurementScheduleRequest(resource.getId());
for (MeasurementDefinition metric : metrics) {
int id = this.metricScheduleCounter.getAndIncrement();
String name = metric.getName();
long interval = metric.getDefaultInterval();
boolean enabled = metric.isDefaultOn() || metric.getDisplayType() == DisplayType.SUMMARY;
DataType dataType = metric.getDataType();
NumericType nDataType = metric.getNumericType();
MeasurementScheduleRequest schedule = new MeasurementScheduleRequest(id, name, interval, enabled, dataType,
nDataType);
resourceSchedules.addMeasurementScheduleRequest(schedule);
}
return resourceSchedules;
}
public synchronized boolean isFailing() {
return failing;
}
public synchronized void setFailing(boolean failing) {
this.failing = failing;
}
public synchronized boolean isFailUpgrade() {
return failUpgrade;
}
public synchronized void setFailUpgrade(boolean failUpgrade) {
this.failUpgrade = failUpgrade;
}
@SuppressWarnings("serial")
public synchronized Set<Resource> findResourcesByType(final ResourceType type) {
Set<Resource> result = new HashSet<Resource>();
if (platform != null) {
findResources(platform, new Resource() {
@Override
public ResourceType getResourceType() {
return type;
}
}, result, RESOURCE_TYPE_COMPARATOR);
}
return result;
}
@SuppressWarnings("serial")
private Set<Resource> getResources(Set<Integer> resourceIds, boolean includeDescendants) {
//it is important to keep the hierarchical order of the resource in the returned set
//so that plugin container can merge the resources from top to bottom.
Set<Resource> result = new LinkedHashSet<Resource>();
for (final Integer id : resourceIds) {
Resource r = findResource(platform, new Resource() {
@Override
public int getId() {
return id;
}
}, ID_COMPARATOR);
if (r != null) {
result.add(r);
if (includeDescendants) {
for (Resource child : r.getChildResources()) {
result.addAll(getResources(Collections.singleton(child.getId()), true));
}
}
}
}
return result;
}
public void removeResource(Resource r) {
resourceStore.remove(r.getUuid());
HashSet<Resource> safeChildren = new HashSet<Resource>(r.getChildResources());
Resource parent = r.getParentResource();
if (parent != null) {
parent.removeChildResource(r);
r.setParentResourceWithoutAncestry(null);
}
for (Resource child : safeChildren) {
removeResource(child);
}
}
public Map<String, Resource> getResourceStore() {
return resourceStore;
}
private Resource fakePersist(Resource agentSideResource, InventoryStatus requiredInventoryStatus,
Set<String> inProgressUUIds) {
Resource persisted = resourceStore.get(agentSideResource.getUuid());
if (!inProgressUUIds.add(agentSideResource.getUuid())) {
return persisted;
}
boolean added = false;
if (persisted == null) {
persisted = new Resource();
if (agentSideResource.getId() != 0) {
persisted.setId(agentSideResource.getId());
} else {
persisted.setId(agentSideResource.getUuid().hashCode());
}
persisted.setUuid(agentSideResource.getUuid());
persisted.setAgent(agentSideResource.getAgent());
persisted.setCurrentAvailability(agentSideResource.getCurrentAvailability());
persisted.setDescription(agentSideResource.getDescription());
persisted.setName(agentSideResource.getName());
persisted.setPluginConfiguration(agentSideResource.getPluginConfiguration().clone());
persisted.setResourceConfiguration(InventoryManager.getResourceConfiguration(agentSideResource).clone());
persisted.setVersion(agentSideResource.getVersion());
persisted.setInventoryStatus(requiredInventoryStatus);
persisted.setResourceKey(agentSideResource.getResourceKey());
persisted.setResourceType(agentSideResource.getResourceType());
resourceStore.put(persisted.getUuid(), persisted);
added = true;
}
Resource parent = agentSideResource.getParentResource();
if (parent != null && parent != Resource.ROOT) {
parent = fakePersist(agentSideResource.getParentResource(), requiredInventoryStatus, inProgressUUIds);
persisted.setParentResource(parent);
parent.addChildResource(persisted);
} else {
persisted.setParentResource(parent);
}
if (added) {
printPersistedResource(persisted);
}
//persist the children
Set<Resource> childResources = new LinkedHashSet<Resource>();
for (Resource child : agentSideResource.getChildResources()) {
childResources.add(fakePersist(child, requiredInventoryStatus, inProgressUUIds));
}
//now update the list with whatever the persisted resource contained in the past
//i.e. we prefer the current results from the agent but keep the children we used to
//have in the past. This is the same behavior as the actual RHQ server has.
childResources.addAll(persisted.getChildResources());
persisted.setChildResources(childResources);
inProgressUUIds.remove(agentSideResource.getUuid());
return persisted;
}
private PlatformSyncInfo getPlatformSyncInfo() {
return platform == null ? null : PlatformSyncInfo.buildPlatformSyncInfo(platform);
}
private Collection<ResourceSyncInfo> getResourceSyncInfo(Resource resource) {
return resource == null ? null : convert(resource);
}
private static Collection<ResourceSyncInfo> convert(Resource root) {
Set<ResourceSyncInfo> result = new HashSet<ResourceSyncInfo>();
convertInternal(root, result);
return result;
}
private static void convertInternal(Resource root, Collection<ResourceSyncInfo> result) {
ResourceSyncInfo rootSyncInfo = ResourceSyncInfo.buildResourceSyncInfo(root);
if (result.contains(rootSyncInfo)) {
return;
}
try {
result.add(rootSyncInfo);
for (Resource child : root.getChildResources()) {
convertInternal(child, result);
}
} catch (Exception e) {
throw new IllegalStateException("Failed to convert resource " + root
+ " to a ResourceSyncInfo. This should not happen.", e);
}
}
private void throwIfFailing() {
if (failing) {
throw new RuntimeException("Fake server inventory is in the failing mode.");
}
}
private static Resource findResource(Resource root, Resource template, Comparator<Resource> comparator) {
if (root == null)
return null;
if (comparator.compare(root, template) == 0) {
return root;
} else {
for (Resource child : root.getChildResources()) {
Resource found = findResource(child, template, comparator);
if (found != null)
return found;
}
}
return null;
}
private static void findResources(Resource root, Resource template, Set<Resource> result,
Comparator<Resource> comparator) {
if (root == null)
return;
if (comparator.compare(root, template) == 0) {
result.add(root);
} else {
for (Resource child : root.getChildResources()) {
findResources(child, template, result, comparator);
}
}
}
public Resource getPlatform() {
return platform;
}
}