/** * 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) { } }