/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.isis.core.specsupport.scenarios;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.hamcrest.Description;
import org.jmock.api.Action;
import org.jmock.api.Invocation;
import org.apache.isis.applib.DomainObjectContainer;
/**
* Utility class to support the writing of unit-scope specs.
*
* <p>
* The {@link #finds(Class, Strategy)} provides an implementation of a JMock
* {@link Action} that can simulate searching for an object from a database, and
* optionally automatically creating a new one if {@link Strategy specified}.
*
* <p>
* If objects are created, then (mock) services are automatically injected. This is performed by
* searching for <tt>injectXxx()</tt> methods. The (mock) {@link DomainObjectContainer container}
* is also automatically injected, through the <tt>setXxx</tt> method.
*
* <p>
* Finally, note that the {@link #init(Object, String) init} hook method allows subclasses to
* customize the state of any objects created.
*/
public class InMemoryDB {
private final ScenarioExecution scenarioExecution;
public InMemoryDB(ScenarioExecution scenarioExecution) {
this.scenarioExecution = scenarioExecution;
}
public static class EntityId {
private final Class<?> type;
private final String id;
public EntityId(Class<?> type, String id) {
this.type = type;
this.id = id;
}
Class<?> getType() {
return type;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
InMemoryDB.EntityId other = (InMemoryDB.EntityId) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (type == null) {
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
return true;
}
@Override
public String toString() {
return "EntityId [type=" + type + ", id=" + id + "]";
}
}
private Map<InMemoryDB.EntityId, Object> objectsById = Maps.newHashMap();
/**
* Returns the object if exists, but will NOT instantiate a new one if not present.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> T getNoCreate(final Class<T> cls, final String id) {
Class type = cls;
while(type != null) {
// search for this class and all superclasses
final InMemoryDB.EntityId entityId = new EntityId(cls, id);
final Object object = objectsById.get(entityId);
if(object != null) {
return (T) object;
}
type = type.getSuperclass();
}
return null;
}
/**
* Returns the object if exists, else will instantiate and save a new one if not present.
*
* <p>
* The new object will have services injected into it (through the {@link ScenarioExecution#injectServices(Object)})
* and will be initialized through the {@link #init(Object, String) init hook} method.
*/
@SuppressWarnings({ "unchecked" })
public <T> T getElseCreate(final Class<T> cls, final String id) {
final Object object = getNoCreate(cls, id);
if(object != null) {
return (T) object;
}
Object obj = instantiateAndInject(cls);
init(obj, id);
return put(cls, id, obj);
}
/**
* Put an object into the database.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public <T> T put(final Class<T> cls, final String id, Object obj) {
Class type = cls;
// put for this class and all superclasses
while(type != null) {
final InMemoryDB.EntityId entityId = new EntityId(cls, id);
objectsById.put(entityId, obj);
type = type.getSuperclass();
}
return (T) obj;
}
private Object instantiateAndInject(Class<?> cls) {
try {
return scenarioExecution.injectServices(cls.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Returns a JMock {@link Action} to return an instance of the provided class.
*
* <p>
* If the object is not yet held in memory, it will be automatically created,
* as per {@link #getElseCreate(Class, String)}.
*
* <p>
* This {@link Action} can only be set for expectations to invoke a method
* accepting a single string argument. This string argument is taken to be an
* identifier for the object (and is used in the caching of that object in memory).
*/
public Action finds(final Class<?> cls) {
return new Action() {
@Override
public Object invoke(Invocation invocation) throws Throwable {
if(invocation.getParameterCount() != 1) {
throw new IllegalArgumentException("intended for action of findByXxx");
}
final Object argObj = invocation.getParameter(0);
if(!(argObj instanceof String)) {
throw new IllegalArgumentException("Argument must be a string");
}
String arg = (String) argObj;
return getElseCreate(cls, arg);
}
@Override
public void describeTo(Description description) {
description.appendText("finds an instance of " + cls.getName());
}
};
}
public <T> List<T> findAll(Class<T> cls) {
return find(cls, Predicates.<T>alwaysTrue());
}
@SuppressWarnings("unchecked")
public <T> List<T> find(Class<T> cls, Predicate<T> predicate) {
final List<T> list = Lists.newArrayList();
for (EntityId entityId : objectsById.keySet()) {
if(cls.isAssignableFrom(entityId.getType())) {
final T object = (T) objectsById.get(entityId);
if(predicate.apply(object)) {
list.add(object);
}
}
}
return list;
}
/**
* Hook to initialize if possible.
*
* <p>
* The provided string is usually taken to be some sort of unique identifier for the object
* (unique in the context of any given scenario, that is).
*/
protected void init(Object obj, String str) {
}
}