/*
* 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.viewer.wicket.model.models;
import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.wicket.Component;
import org.apache.isis.applib.layout.component.CollectionLayoutData;
import org.apache.isis.core.commons.factory.InstanceUtil;
import org.apache.isis.core.commons.lang.ClassUtil;
import org.apache.isis.core.commons.lang.Closure;
import org.apache.isis.core.commons.lang.IterableExtensions;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking;
import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
import org.apache.isis.core.metamodel.facets.collections.sortedby.SortedByFacet;
import org.apache.isis.core.metamodel.facets.object.paged.PagedFacet;
import org.apache.isis.core.metamodel.facets.object.plural.PluralFacet;
import org.apache.isis.core.metamodel.spec.ObjectSpecId;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation;
import org.apache.isis.core.runtime.system.session.IsisSessionFactory;
import org.apache.isis.viewer.wicket.model.hints.UiHintContainer;
import org.apache.isis.viewer.wicket.model.links.LinkAndLabel;
import org.apache.isis.viewer.wicket.model.links.LinksProvider;
import org.apache.isis.viewer.wicket.model.mementos.CollectionMemento;
import org.apache.isis.viewer.wicket.model.mementos.ObjectAdapterMemento;
/**
* Model representing a collection of entities, either {@link Type#STANDALONE
* standalone} (eg result of invoking an action) or {@link Type#PARENTED
* parented} (contents of the collection of an entity).
*
* <p>
* So that the model is {@link Serializable}, the {@link ObjectAdapter}s within
* the collection are stored as {@link ObjectAdapterMemento}s.
*/
public class EntityCollectionModel extends ModelAbstract<List<ObjectAdapter>> implements LinksProvider,
UiHintContainer {
private static final long serialVersionUID = 1L;
private static final int PAGE_SIZE_DEFAULT_FOR_PARENTED = 12;
private static final int PAGE_SIZE_DEFAULT_FOR_STANDALONE = 25;
public enum Type {
/**
* A simple list of object mementos, eg the result of invoking an action
*
* <p>
* This deals with both persisted and transient objects.
*/
STANDALONE {
@Override
List<ObjectAdapter> load(final EntityCollectionModel entityCollectionModel) {
return Lists.newArrayList(
Iterables.filter(
Iterables.transform(entityCollectionModel.mementoList,
ObjectAdapterMemento.Functions.fromMemento(ConcurrencyChecking.NO_CHECK,
entityCollectionModel.getPersistenceSession(), entityCollectionModel.getSpecificationLoader())),
Predicates.notNull()));
}
@Override
void setObject(final EntityCollectionModel entityCollectionModel, final List<ObjectAdapter> list) {
entityCollectionModel.mementoList = Lists.newArrayList(
Iterables.filter(
Iterables.transform(list, ObjectAdapterMemento.Functions.toMemento()),
Predicates.<ObjectAdapterMemento>notNull()));
}
@Override
public String getName(final EntityCollectionModel model) {
PluralFacet facet = model.getTypeOfSpecification().getFacet(PluralFacet.class);
return facet.value();
}
@Override
public int getCount(final EntityCollectionModel model) {
return model.mementoList.size();
}
},
/**
* A collection of an entity (eg Order/OrderDetail).
*/
PARENTED {
@Override
List<ObjectAdapter> load(final EntityCollectionModel entityCollectionModel) {
final ObjectAdapter adapter = entityCollectionModel.getParentObjectAdapterMemento().getObjectAdapter(
ConcurrencyChecking.NO_CHECK, entityCollectionModel.getPersistenceSession(),
entityCollectionModel.getSpecificationLoader());
final OneToManyAssociation collection = entityCollectionModel.collectionMemento.getCollection(
entityCollectionModel.getSpecificationLoader());
final ObjectAdapter collectionAsAdapter = collection.get(adapter, InteractionInitiatedBy.USER);
final List<Object> objectList = asIterable(collectionAsAdapter);
final Class<? extends Comparator<?>> sortedBy = entityCollectionModel.sortedBy;
if(sortedBy != null) {
@SuppressWarnings("unchecked")
final Comparator<Object> comparator = (Comparator<Object>) InstanceUtil.createInstance(sortedBy);
entityCollectionModel.getIsisSessionFactory().getServicesInjector().injectServicesInto(comparator);
Collections.sort(objectList, comparator);
}
final Iterable<ObjectAdapter> adapterIterable = Iterables.transform(objectList, ObjectAdapter.Functions.adapterForUsing( entityCollectionModel.getPersistenceSession()));
final List<ObjectAdapter> adapterList = Lists.newArrayList(adapterIterable);
return adapterList;
}
@SuppressWarnings("unchecked")
private List<Object> asIterable(final ObjectAdapter collectionAsAdapter) {
final Iterable<Object> objects = (Iterable<Object>) collectionAsAdapter.getObject();
return Lists.newArrayList(objects);
}
@Override
void setObject(EntityCollectionModel entityCollectionModel, List<ObjectAdapter> list) {
// no-op
throw new UnsupportedOperationException();
}
@Override
public String getName(EntityCollectionModel model) {
return model.getCollectionMemento().getName(model.getSpecificationLoader());
}
@Override
public int getCount(EntityCollectionModel model) {
return load(model).size();
}
};
abstract List<ObjectAdapter> load(EntityCollectionModel entityCollectionModel);
abstract void setObject(EntityCollectionModel entityCollectionModel, List<ObjectAdapter> list);
public abstract String getName(EntityCollectionModel entityCollectionModel);
public abstract int getCount(EntityCollectionModel entityCollectionModel);
}
static class LowestCommonSuperclassClosure implements Closure<Class<?>>{
private Class<?> common;
@Override
public Class<?> execute(final Class<?> value) {
if(common == null) {
common = value;
} else {
Class<?> current = common;
while(!current.isAssignableFrom(value)) {
current = current.getSuperclass();
}
common = current;
}
return common;
}
Class<?> getLowestCommonSuperclass() {
return common;
}
}
/**
* Factory.
*/
public static EntityCollectionModel createStandalone(
final ObjectAdapter collectionAsAdapter,
final IsisSessionFactory sessionFactory) {
final Iterable<Object> pojos = EntityCollectionModel.asIterable(collectionAsAdapter);
final List<ObjectAdapterMemento> mementoList =
Lists.newArrayList(Iterables.transform(pojos, ObjectAdapterMemento.Functions.fromPojo(
sessionFactory.getCurrentSession().getPersistenceSession())));
final ObjectSpecification elementSpec;
if(!Iterables.isEmpty(pojos)) {
// dynamically determine the spec of the elements
// (ie so a List<Object> can be rendered according to the runtime type of its elements,
// rather than the compile-time type
final LowestCommonSuperclassClosure closure = new LowestCommonSuperclassClosure();
Function<Object, Class<?>> function = new Function<Object, Class<?>>(){
@Override
public Class<?> apply(Object obj) {
return obj.getClass();
}
};
IterableExtensions.fold(Iterables.transform(pojos, function), closure);
elementSpec = sessionFactory.getSpecificationLoader().loadSpecification(closure.getLowestCommonSuperclass());
} else {
elementSpec = collectionAsAdapter.getElementSpecification();
}
final Class<?> elementType;
int pageSize = PAGE_SIZE_DEFAULT_FOR_STANDALONE;
if (elementSpec != null) {
elementType = elementSpec.getCorrespondingClass();
pageSize = pageSize(elementSpec.getFacet(PagedFacet.class), PAGE_SIZE_DEFAULT_FOR_STANDALONE);
} else {
elementType = Object.class;
}
return new EntityCollectionModel(elementType, mementoList, pageSize);
}
/**
* The {@link ActionModel model} of the {@link ObjectAction action}
* that generated this {@link EntityCollectionModel}.
*
* <p>
* Populated only for {@link Type#STANDALONE standalone} collections.
*
* @see #setActionHint(ActionModel)
*/
public ActionModel getActionModelHint() {
return actionModelHint;
}
/**
* Called only for {@link Type#STANDALONE standalone} collections.
*
* @see #getActionModelHint()
*/
public void setActionHint(ActionModel actionModelHint) {
this.actionModelHint = actionModelHint;
}
/**
* Factory.
*/
public static EntityCollectionModel createParented(final EntityModel modelWithCollectionLayoutMetadata) {
return new EntityCollectionModel(modelWithCollectionLayoutMetadata);
}
private final Type type;
private final Class<?> typeOf;
private transient ObjectSpecification typeOfSpec;
/**
* Populated only if {@link Type#STANDALONE}.
*/
private List<ObjectAdapterMemento> mementoList;
/**
* Populated only if {@link Type#STANDALONE}.
*/
private List<ObjectAdapterMemento> toggledMementosList;
/**
* Populated only if {@link Type#PARENTED}.
*/
private final EntityModel entityModel;
/**
* Populated only if {@link Type#PARENTED}.
*/
private CollectionMemento collectionMemento;
private final int pageSize;
/**
* Additional links to render (if any)
*/
private List<LinkAndLabel> linkAndLabels = Lists.newArrayList();
/**
* Optionally populated only if {@link Type#PARENTED}.
*/
private Class<? extends Comparator<?>> sortedBy;
/**
* Optionally populated, only if {@link Type#STANDALONE} (ie called from an action).
*/
private ActionModel actionModelHint;
private EntityCollectionModel(final Class<?> typeOf, final List<ObjectAdapterMemento> mementoList, final int pageSize) {
this.type = Type.STANDALONE;
this.entityModel = null;
this.typeOf = typeOf;
this.mementoList = mementoList;
this.pageSize = pageSize;
this.toggledMementosList = Lists.newArrayList();
}
private EntityCollectionModel(final EntityModel entityModel) {
this.type = Type.PARENTED;
this.entityModel = entityModel;
final OneToManyAssociation collection = collectionFor(entityModel.getObjectAdapterMemento(), getLayoutData());
this.typeOf = forName(collection.getSpecification());
this.collectionMemento = new CollectionMemento(collection, entityModel.getIsisSessionFactory());
this.pageSize = pageSize(collection.getFacet(PagedFacet.class), PAGE_SIZE_DEFAULT_FOR_PARENTED);
final SortedByFacet sortedByFacet = collection.getFacet(SortedByFacet.class);
this.sortedBy = sortedByFacet != null ? sortedByFacet.value(): null;
this.toggledMementosList = Lists.newArrayList();
}
private OneToManyAssociation collectionFor(
final ObjectAdapterMemento parentObjectAdapterMemento,
final CollectionLayoutData collectionLayoutData) {
if(collectionLayoutData == null) {
throw new IllegalArgumentException("EntityModel must have a CollectionLayoutMetadata");
}
final String collectionId = collectionLayoutData.getId();
final ObjectSpecId objectSpecId = parentObjectAdapterMemento.getObjectSpecId();
final ObjectSpecification objectSpec = getIsisSessionFactory().getSpecificationLoader().lookupBySpecId(objectSpecId);
final OneToManyAssociation otma = (OneToManyAssociation) objectSpec.getAssociation(collectionId);
return otma;
}
private static Class<?> forName(final ObjectSpecification objectSpec) {
final String fullName = objectSpec.getFullIdentifier();
return ClassUtil.forName(fullName);
}
private static int pageSize(final PagedFacet pagedFacet, final int defaultPageSize) {
return pagedFacet != null ? pagedFacet.value(): defaultPageSize;
}
public boolean isParented() {
return type == Type.PARENTED;
}
public boolean isStandalone() {
return type == Type.STANDALONE;
}
public int getPageSize() {
return pageSize;
}
/**
* The name of the collection (if has an entity, ie, if
* {@link #isParented() is parented}.)
*
* <p>
* If {@link #isStandalone()}, returns the {@link PluralFacet} of the {@link #getTypeOfSpecification() specification}
* (eg 'Customers').
*/
public String getName() {
return type.getName(this);
}
public int getCount() {
return this.type.getCount(this);
}
/**
* Populated only if {@link Type#PARENTED}.
*/
public CollectionLayoutData getLayoutData() {
return entityModel != null
? entityModel.getCollectionLayoutData()
: null;
}
@Override
protected List<ObjectAdapter> load() {
return type.load(this);
}
public ObjectSpecification getTypeOfSpecification() {
if (typeOfSpec == null) {
typeOfSpec = getSpecificationLoader().loadSpecification(typeOf);
}
return typeOfSpec;
}
@Override
public void setObject(List<ObjectAdapter> list) {
super.setObject(list);
type.setObject(this, list);
}
/**
* Not API, but to refresh the model list.
*/
public void setObjectList(ObjectAdapter resultAdapter) {
final Iterable<Object> pojos = EntityCollectionModel.asIterable(resultAdapter);
this.mementoList = Lists.newArrayList(
Iterables.transform(pojos, ObjectAdapterMemento.Functions.fromPojo(getPersistenceSession())));
}
/**
* Populated only if {@link Type#PARENTED}.
*/
public EntityModel getEntityModel() {
return entityModel;
}
/**
* Populated only if {@link Type#PARENTED}.
*/
public ObjectAdapterMemento getParentObjectAdapterMemento() {
return entityModel != null? entityModel.getObjectAdapterMemento(): null;
}
/**
* Populated only if {@link Type#PARENTED}.
*/
public CollectionMemento getCollectionMemento() {
return collectionMemento;
}
@SuppressWarnings("unchecked")
private static Iterable<Object> asIterable(final ObjectAdapter resultAdapter) {
return (Iterable<Object>) resultAdapter.getObject();
}
public void toggleSelectionOn(ObjectAdapter selectedAdapter) {
ObjectAdapterMemento selectedAsMemento = ObjectAdapterMemento.createOrNull(selectedAdapter);
// try to remove; if couldn't, then mustn't have been in there, in which case add.
boolean removed = toggledMementosList.remove(selectedAsMemento);
if(!removed) {
toggledMementosList.add(selectedAsMemento);
}
}
public List<ObjectAdapterMemento> getToggleMementosList() {
return Collections.unmodifiableList(this.toggledMementosList);
}
public void clearToggleMementosList() {
this.toggledMementosList.clear();
}
public void addLinkAndLabels(List<LinkAndLabel> linkAndLabels) {
this.linkAndLabels.addAll(linkAndLabels);
}
@Override
public List<LinkAndLabel> getLinks() {
return Collections.unmodifiableList(linkAndLabels);
}
public EntityCollectionModel asDummy() {
return new EntityCollectionModel(typeOf, Collections.<ObjectAdapterMemento>emptyList(), pageSize);
}
// //////////////////////////////////////
public static final String HINT_KEY_SELECTED_ITEM = "selectedItem";
/**
* Just delegates to the {@link #getEntityModel() entity model} (if parented, else no-op).
*/
@Override
public String getHint(final Component component, final String attributeName) {
if(getEntityModel() == null) {
return null;
}
return getEntityModel().getHint(component, attributeName);
}
/**
* Just delegates to the {@link #getEntityModel() entity model} (if parented, else no-op).
*/
@Override
public void setHint(final Component component, final String attributeName, final String attributeValue) {
if(getEntityModel() == null) {
return;
}
getEntityModel().setHint(component, attributeName, attributeValue);
}
/**
* Just delegates to the {@link #getEntityModel() entity model} (if parented, else no-op).
*/
@Override
public void clearHint(final Component component, final String attributeName) {
if(getEntityModel() == null) {
return;
}
getEntityModel().clearHint(component, attributeName);
}
}