http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java new file mode 100644 index 0000000..107bde0 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/ObjectAdapterMemento.java @@ -0,0 +1,458 @@ +/* + * 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.mementos; + +import java.io.Serializable; +import java.util.List; + +import com.google.common.base.Function; + +import org.apache.isis.core.commons.ensure.Ensure; +import org.apache.isis.core.metamodel.adapter.ObjectAdapter; +import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; +import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking; +import org.apache.isis.core.metamodel.adapter.oid.Oid; +import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; +import org.apache.isis.core.metamodel.adapter.oid.RootOid; +import org.apache.isis.core.metamodel.adapter.oid.TypedOid; +import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet; +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.ObjectActionParameter; +import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation; +import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; +import org.apache.isis.core.runtime.memento.Memento; +import org.apache.isis.core.runtime.system.context.IsisContext; +import org.apache.isis.core.runtime.system.persistence.PersistenceSession; + +public class ObjectAdapterMemento implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Factory method + */ + public static ObjectAdapterMemento createOrNull(final ObjectAdapter adapter) { + if (adapter == null) { + return null; + } + return new ObjectAdapterMemento(adapter); + } + + /** + * Factory method + */ + public static ObjectAdapterMemento createPersistent(final RootOid rootOid) { + return new ObjectAdapterMemento(rootOid); + } + + + + enum Type { + /** + * The {@link ObjectAdapter} that this is the memento for directly has + * an {@link EncodableFacet} (it is almost certainly a value), and so is + * stored directly. + */ + ENCODEABLE { + @Override + ObjectAdapter recreateAdapter(final ObjectAdapterMemento oam, ConcurrencyChecking concurrencyChecking) { + ObjectSpecId objectSpecId = oam.objectSpecId; + ObjectSpecification objectSpec = SpecUtils.getSpecificationFor(objectSpecId); + final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class); + return encodableFacet.fromEncodedString(oam.encodableValue); + } + + @Override + public boolean equals(ObjectAdapterMemento oam, ObjectAdapterMemento other) { + return other.type == ENCODEABLE && oam.encodableValue.equals(other.encodableValue); + } + + @Override + public int hashCode(ObjectAdapterMemento oam) { + return oam.encodableValue.hashCode(); + } + + @Override + public String toString(final ObjectAdapterMemento oam) { + return oam.encodableValue; + } + + @Override + public void resetVersion(ObjectAdapterMemento objectAdapterMemento) { + } + }, + /** + * The {@link ObjectAdapter} that this is for is already known by its + * (persistent) {@link Oid}. + */ + PERSISTENT { + @Override + ObjectAdapter recreateAdapter(final ObjectAdapterMemento oam, ConcurrencyChecking concurrencyChecking) { + TypedOid oid = getOidMarshaller().unmarshal(oam.persistentOidStr, TypedOid.class); + try { + final ObjectAdapter adapter = getAdapterManager().adapterFor(oid, concurrencyChecking); + return adapter; + + } finally { + // a side-effect of AdapterManager#adapterFor(...) is that it will update the oid + // with the correct version, even when there is a concurrency exception + // we copy this updated oid string into our memento so that, if we retry, + // we will succeed second time around + + oam.persistentOidStr = oid.enString(getOidMarshaller()); + } + } + + @Override + public void resetVersion(ObjectAdapterMemento oam) { + // REVIEW: this may be redundant because recreateAdapter also guarantees the version will be reset. + final ObjectAdapter adapter = recreateAdapter(oam, ConcurrencyChecking.NO_CHECK); + Oid oid = adapter.getOid(); + oam.persistentOidStr = oid.enString(getOidMarshaller()); + } + + @Override + public boolean equals(ObjectAdapterMemento oam, ObjectAdapterMemento other) { + return other.type == PERSISTENT && oam.persistentOidStr.equals(other.persistentOidStr); + } + + @Override + public int hashCode(ObjectAdapterMemento oam) { + return oam.persistentOidStr.hashCode(); + } + + @Override + public String toString(final ObjectAdapterMemento oam) { + return oam.persistentOidStr; + } + + }, + /** + * Uses Isis' own {@link Memento}, to capture the state of a transient + * object. + */ + TRANSIENT { + /** + * {@link ConcurrencyChecking} is ignored for transients. + */ + @Override + ObjectAdapter recreateAdapter(final ObjectAdapterMemento oam, ConcurrencyChecking concurrencyChecking) { + return oam.transientMemento.recreateObject(); + } + + @Override + public boolean equals(ObjectAdapterMemento oam, ObjectAdapterMemento other) { + return other.type == TRANSIENT && oam.transientMemento.equals(other.transientMemento); + } + + @Override + public int hashCode(ObjectAdapterMemento oam) { + return oam.transientMemento.hashCode(); + } + + @Override + public String toString(final ObjectAdapterMemento oam) { + return oam.transientMemento.toString(); + } + + @Override + public void resetVersion(ObjectAdapterMemento objectAdapterMemento) { + } + }; + + public synchronized ObjectAdapter getAdapter(final ObjectAdapterMemento nom, ConcurrencyChecking concurrencyChecking) { + return recreateAdapter(nom, concurrencyChecking); + } + + abstract ObjectAdapter recreateAdapter(ObjectAdapterMemento nom, ConcurrencyChecking concurrencyChecking); + + public abstract boolean equals(ObjectAdapterMemento oam, ObjectAdapterMemento other); + public abstract int hashCode(ObjectAdapterMemento objectAdapterMemento); + + public abstract String toString(ObjectAdapterMemento adapterMemento); + + public abstract void resetVersion(ObjectAdapterMemento objectAdapterMemento); + } + + private Type type; + + private final ObjectSpecId objectSpecId; + private String titleHint; + + /** + * The current value, if {@link Type#ENCODEABLE}. + * + * <p> + * Will be <tt>null</tt> otherwise. + */ + private String encodableValue; + + /** + * The current value, if {@link Type#PERSISTENT}. + * + * <p> + * Will be <tt>null</tt> otherwise. + */ + private String persistentOidStr; + + /** + * The current value, if {@link Type#TRANSIENT}. + * + * <p> + * Will be <tt>null</tt> otherwise. + */ + private Memento transientMemento; + + private ObjectAdapterMemento(final RootOid rootOid) { + Ensure.ensureThatArg(rootOid, Oid.Matchers.isPersistent()); + this.persistentOidStr = rootOid.enString(getOidMarshaller()); + this.objectSpecId = rootOid.getObjectSpecId(); + this.type = Type.PERSISTENT; + } + + private ObjectAdapterMemento(final ObjectAdapter adapter) { + if (adapter == null) { + throw new IllegalArgumentException("adapter cannot be null"); + } + final ObjectSpecification specification = adapter.getSpecification(); + objectSpecId = specification.getSpecId(); + init(adapter); + } + + private void init(final ObjectAdapter adapter) { + + final ObjectSpecification specification = adapter.getSpecification(); + + final EncodableFacet encodableFacet = specification.getFacet(EncodableFacet.class); + final boolean isEncodable = encodableFacet != null; + if (isEncodable) { + encodableValue = encodableFacet.toEncodedString(adapter); + type = Type.ENCODEABLE; + return; + } + + final RootOid oid = (RootOid) adapter.getOid(); + if (oid.isTransient()) { + transientMemento = new Memento(adapter); + type = Type.TRANSIENT; + return; + } + + persistentOidStr = oid.enString(getOidMarshaller()); + type = Type.PERSISTENT; + } + + public void resetVersion() { + type.resetVersion(this); + } + + + /** + * Lazily looks up {@link ObjectAdapter} if required. + * + * <p> + * For transient objects, be aware that calling this method more than once + * will cause the underlying {@link ObjectAdapter} to be recreated, + * overwriting any changes that may have been made. In general then it's + * best to call once and then hold onto the value thereafter. Alternatively, + * can call {@link #setAdapter(ObjectAdapter)} to keep this memento in sync. + */ + public ObjectAdapter getObjectAdapter(ConcurrencyChecking concurrencyChecking) { + return type.getAdapter(this, concurrencyChecking); + } + + /** + * Updates the memento if the adapter's state has changed. + * + * <p> + * This is a no-op for + * + * @param adapter + */ + public void setAdapter(final ObjectAdapter adapter) { + init(adapter); + } + + public ObjectSpecId getObjectSpecId() { + return objectSpecId; + } + + /** + * Analogous to {@link List#contains(Object)}, but does not perform + * {@link ConcurrencyChecking concurrency checking} of the OID. + */ + public boolean containedIn(List<ObjectAdapterMemento> list) { + // REVIEW: heavy handed, ought to be possible to just compare the OIDs + // ignoring the concurrency checking + final ObjectAdapter currAdapter = getObjectAdapter(ConcurrencyChecking.NO_CHECK); + for (ObjectAdapterMemento each : list) { + if(each == null) { + continue; + } + final ObjectAdapter otherAdapter = each.getObjectAdapter(ConcurrencyChecking.NO_CHECK); + if(currAdapter == otherAdapter) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + return type.hashCode(this); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof ObjectAdapterMemento) && type.equals(this, (ObjectAdapterMemento)obj); + } + + + @Override + public String toString() { + return asString(); + } + + public String asString() { + return type.toString(this); + } + + + ////////////////////////////////////////////////// + // Functions + ////////////////////////////////////////////////// + + + public final static class Functions { + + private Functions() { + } + + public static Function<ObjectSpecification, ObjectSpecId> fromSpec() { + return new Function<ObjectSpecification, ObjectSpecId>() { + + @Override + public ObjectSpecId apply(final ObjectSpecification from) { + return from.getSpecId(); + } + }; + } + + public static Function<OneToOneAssociation, PropertyMemento> fromProperty() { + return new Function<OneToOneAssociation, PropertyMemento>() { + @Override + public PropertyMemento apply(final OneToOneAssociation from) { + return new PropertyMemento(from); + } + }; + } + + public static Function<OneToManyAssociation, CollectionMemento> fromCollection() { + return new Function<OneToManyAssociation, CollectionMemento>() { + @Override + public CollectionMemento apply(final OneToManyAssociation from) { + return new CollectionMemento(from); + } + }; + } + + public static Function<ObjectAction, ActionMemento> fromAction() { + return new Function<ObjectAction, ActionMemento>() { + @Override + public ActionMemento apply(final ObjectAction from) { + return new ActionMemento(from); + } + }; + } + + public static Function<ObjectActionParameter, ActionParameterMemento> fromActionParameter() { + return new Function<ObjectActionParameter, ActionParameterMemento>() { + @Override + public ActionParameterMemento apply(final ObjectActionParameter from) { + return new ActionParameterMemento(from); + } + }; + } + + public static Function<Object, ObjectAdapterMemento> fromPojo(final AdapterManager adapterManager) { + return new Function<Object, ObjectAdapterMemento>() { + @Override + public ObjectAdapterMemento apply(final Object pojo) { + final ObjectAdapter adapter = adapterManager.adapterFor(pojo); + return ObjectAdapterMemento.createOrNull(adapter); + } + }; + } + + public static Function<ObjectAdapter, ObjectAdapterMemento> fromAdapter() { + return new Function<ObjectAdapter, ObjectAdapterMemento>() { + @Override + public ObjectAdapterMemento apply(final ObjectAdapter adapter) { + return ObjectAdapterMemento.createOrNull(adapter); + } + }; + } + + + public static Function<ObjectAdapterMemento, ObjectAdapter> fromMemento(final ConcurrencyChecking concurrencyChecking) { + return new Function<ObjectAdapterMemento, ObjectAdapter>() { + @Override + public ObjectAdapter apply(final ObjectAdapterMemento from) { + return from.getObjectAdapter(concurrencyChecking); + } + }; + } + + public static Function<ObjectAdapter, ObjectAdapterMemento> toMemento() { + return new Function<ObjectAdapter, ObjectAdapterMemento>() { + + @Override + public ObjectAdapterMemento apply(ObjectAdapter from) { + return ObjectAdapterMemento.createOrNull(from); + } + + }; + } + + } + + + ////////////////////////////////////////////////// + // Dependencies (from context) + ////////////////////////////////////////////////// + + private static AdapterManager getAdapterManager() { + return getPersistenceSession().getAdapterManager(); + } + + private static PersistenceSession getPersistenceSession() { + return IsisContext.getPersistenceSession(); + } + + public static OidMarshaller getOidMarshaller() { + return IsisContext.getOidMarshaller(); + } + + + + +}
http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java new file mode 100644 index 0000000..75f1a64 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PageParameterNames.java @@ -0,0 +1,142 @@ +/* + * 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.mementos; + +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.util.string.StringValue; + +import org.apache.isis.core.commons.lang.StringExtensions; +import org.apache.isis.core.metamodel.adapter.oid.Oid; + +public enum PageParameterNames { + + /** + * The object's {@link Oid}. + * + * <p> + * Also encodes the object's spec, and whether the object is persistent or not. + */ + OBJECT_OID, + + /** + * Hints for the rendering of an entity. + */ + ANCHOR, + + /** + * Owning type of an action. + * + * <p> + * Whereas {@link #OBJECT_SPEC} is the concrete runtime type of the adapter, + * the owning type could be some superclass if the action has been + * inherited. + */ + ACTION_OWNING_SPEC, + + /** + * Whether user, exploration, prototype etc. + */ + ACTION_TYPE, + + /** + * The name of the action, along with its parameters. + */ + ACTION_ID, + + /** + * When a single object is returned, whether to redirect to it or simply inline it. + */ + ACTION_SINGLE_RESULTS_MODE, + + /** + * The argument acting as a context for a contributed action, if any. + * + * <p> + * In the format N=OBJECT_OID, where N is the 0-based action parameter + * index. + */ + ACTION_PARAM_CONTEXT, + + /** + * Action argument(s), if known. + */ + ACTION_ARGS; + + /** + * Returns the {@link #name()} formatted as + * {@link Strings#camelCase(String) camel case}. + * + * <p> + * For example, <tt>ACTION_TYPE</tt> becomes <tt>actionType</tt>. + */ + @Override + public String toString() { + return StringExtensions.toCamelCase(name()); + } + + public String getStringFrom(final PageParameters pageParameters) { + return getStringFrom(pageParameters, null); + } + + public String getStringFrom(PageParameters pageParameters, String defaultValue) { + if(pageParameters == null) { + return defaultValue; + } + return pageParameters.get(this.toString()).toString(defaultValue); + } + + public <T extends Enum<T>> T getEnumFrom(PageParameters pageParameters, Class<T> enumClass) { + String value = getStringFrom(pageParameters); + return value != null? Enum.valueOf(enumClass, value): null; + } + + public List<String> getListFrom(PageParameters pageParameters) { + return Lists.transform(pageParameters.getValues(this.toString()), new Function<StringValue, String>() { + @Override + public String apply(StringValue input) { + return input.toString(); + } + }); + } + + + public void addStringTo(final PageParameters pageParameters, final String value) { + pageParameters.add(this.toString(), value); + } + + public void addEnumTo(final PageParameters pageParameters, final Enum<?> someEnum) { + addStringTo(pageParameters, someEnum.name()); + } + + /** + * @param pageParameters + */ + public void removeFrom(PageParameters pageParameters) { + pageParameters.remove(this.toString()); + } + + + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java new file mode 100644 index 0000000..6504be6 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/PropertyMemento.java @@ -0,0 +1,123 @@ +/* + * 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.mementos; + +import java.io.Serializable; + +import org.apache.isis.core.metamodel.spec.ObjectSpecId; +import org.apache.isis.core.metamodel.spec.ObjectSpecification; +import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; +import org.apache.isis.core.runtime.system.context.IsisContext; +import org.apache.isis.viewer.wicket.model.models.EntityModel; + +public class PropertyMemento implements Serializable { + + private static final long serialVersionUID = 1L; + + private static ObjectSpecification owningSpecFor(final OneToOneAssociation association) { + return IsisContext.getSpecificationLoader().loadSpecification(association.getIdentifier().toClassIdentityString()); + } + + private final ObjectSpecId owningSpecId; + private final String identifier; + private final ObjectSpecId specId; + +// private transient OneToOneAssociation property; + + public PropertyMemento(final ObjectSpecId owningType, final String identifier) { + this(owningType, identifier, null); + } + + public PropertyMemento(final ObjectSpecId owningType, final String identifier, final ObjectSpecId type) { + this(owningType, identifier, type, propertyFor(owningType, identifier)); + } + + public PropertyMemento(final OneToOneAssociation property) { + this(owningSpecFor(property).getSpecId(), property.getIdentifier().toNameIdentityString(), property.getSpecification().getSpecId(), property); + } + + private PropertyMemento(final ObjectSpecId owningSpecId, final String name, final ObjectSpecId specId, final OneToOneAssociation property) { + this.owningSpecId = owningSpecId; + this.identifier = name; + this.specId = specId; +// this.property = property; + } + + public ObjectSpecId getOwningType() { + return owningSpecId; + } + + public ObjectSpecId getType() { + return specId; + } + + public String getIdentifier() { + return identifier; + } + + public OneToOneAssociation getProperty() { +// if (property == null) { +// property = propertyFor(owningSpecId, identifier); +// } +// return property; + return propertyFor(owningSpecId, identifier); + } + + private static OneToOneAssociation propertyFor(ObjectSpecId owningType, String identifier) { + return (OneToOneAssociation) SpecUtils.getSpecificationFor(owningType).getAssociation(identifier); + } + + /** + * Value semantics so can use as a key in {@link EntityModel} hash. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((identifier == null) ? 0 : identifier.hashCode()); + return result; + } + + /** + * Value semantics so can use as a key in {@link EntityModel} hash. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PropertyMemento other = (PropertyMemento) obj; + if (identifier == null) { + if (other.identifier != null) { + return false; + } + } else if (!identifier.equals(other.identifier)) { + return false; + } + return true; + } + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java new file mode 100644 index 0000000..8e34386 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/mementos/SpecUtils.java @@ -0,0 +1,44 @@ +/* + * 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.mementos; + +import org.apache.isis.core.metamodel.spec.ObjectSpecId; +import org.apache.isis.core.metamodel.spec.ObjectSpecification; +import org.apache.isis.core.metamodel.spec.SpecificationLoaderSpi; +import org.apache.isis.core.runtime.system.context.IsisContext; + +public final class SpecUtils { + + private SpecUtils(){} + + public static ObjectSpecification getSpecificationFor(ObjectSpecId objectSpecId) { + ObjectSpecification objectSpec = getSpecificationLoader().lookupBySpecId(objectSpecId); + if(objectSpec != null) { + return objectSpec; + } + + // attempt to load directly. + return getSpecificationLoader().loadSpecification(objectSpecId.asString()); + } + + protected static SpecificationLoaderSpi getSpecificationLoader() { + return IsisContext.getSpecificationLoader(); + } + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java new file mode 100644 index 0000000..7ca84ab --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/AboutModel.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * Model providing welcome text. + */ +public class AboutModel extends ModelAbstract<String> { + + private static final long serialVersionUID = 1L; + + public AboutModel(String message) { + super(); + setObject(message); + } + + @Override + protected String load() { + return getObject(); + } + + @Override + public void setObject(final String message) { + if(message == null) { + return; + } + super.setObject(message); + } + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java new file mode 100644 index 0000000..d634262 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionExecutor.java @@ -0,0 +1,35 @@ +/* + * 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 org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.form.Form; + +/** + * Decouples the {@link ActionModel}, which needs to delegate the actual + * execution of an action, from its implementor. + */ +public interface ActionExecutor extends Serializable { + + boolean executeActionAndProcessResults(AjaxRequestTarget target, Form<?> feedbackForm); + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java new file mode 100644 index 0000000..ee5e341 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionModel.java @@ -0,0 +1,663 @@ +/* + * 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.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler; +import org.apache.wicket.request.http.handler.RedirectRequestHandler; +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.request.resource.ContentDisposition; +import org.apache.wicket.util.resource.AbstractResourceStream; +import org.apache.wicket.util.resource.IResourceStream; +import org.apache.wicket.util.resource.ResourceStreamNotFoundException; +import org.apache.wicket.util.resource.StringResourceStream; +import org.apache.isis.applib.Identifier; +import org.apache.isis.applib.RecoverableException; +import org.apache.isis.applib.annotation.ActionSemantics; +import org.apache.isis.applib.annotation.BookmarkPolicy; +import org.apache.isis.applib.annotation.Where; +import org.apache.isis.applib.value.Blob; +import org.apache.isis.applib.value.Clob; +import org.apache.isis.applib.value.NamedWithMimeType; +import org.apache.isis.core.commons.authentication.AuthenticationSession; +import org.apache.isis.core.metamodel.adapter.ObjectAdapter; +import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager.ConcurrencyChecking; +import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; +import org.apache.isis.core.metamodel.adapter.oid.RootOid; +import org.apache.isis.core.metamodel.adapter.oid.RootOidDefault; +import org.apache.isis.core.metamodel.consent.Consent; +import org.apache.isis.core.metamodel.facets.object.bookmarkpolicy.BookmarkPolicyFacet; +import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet; +import org.apache.isis.core.metamodel.spec.ActionType; +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.ObjectActionParameter; +import org.apache.isis.core.runtime.system.context.IsisContext; +import org.apache.isis.viewer.wicket.model.common.PageParametersUtils; +import org.apache.isis.viewer.wicket.model.mementos.ActionMemento; +import org.apache.isis.viewer.wicket.model.mementos.ActionParameterMemento; +import org.apache.isis.viewer.wicket.model.mementos.ObjectAdapterMemento; +import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames; + +/** + * Models an action invocation, either the gathering of arguments for the + * action's {@link Mode#PARAMETERS parameters}, or the handling of the + * {@link Mode#RESULTS results} once invoked. + */ +public class ActionModel extends BookmarkableModel<ObjectAdapter> { + + private static final long serialVersionUID = 1L; + + private static final String NULL_ARG = "$nullArg$"; + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("([^=]+)=(.+)"); + + /** + * Whether we are obtaining arguments (eg in a dialog), or displaying the + * results + */ + private enum Mode { + PARAMETERS, + RESULTS + } + + + + ////////////////////////////////////////////////// + // Factory methods + ////////////////////////////////////////////////// + + /** + * @param objectAdapter + * @param action + * @return + */ + public static ActionModel create(final ObjectAdapter objectAdapter, final ObjectAction action) { + final ObjectAdapterMemento serviceMemento = ObjectAdapterMemento.Functions.fromAdapter().apply(objectAdapter); + final ActionMemento homePageActionMemento = ObjectAdapterMemento.Functions.fromAction().apply(action); + final Mode mode = action.getParameterCount() > 0? Mode.PARAMETERS : Mode.RESULTS; + return new ActionModel(serviceMemento, homePageActionMemento, mode); + } + + public static ActionModel createForPersistent(final PageParameters pageParameters) { + return new ActionModel(pageParameters); + } + + /** + * Factory method for creating {@link PageParameters}. + * + * see {@link #ActionModel(PageParameters)} + */ + public static PageParameters createPageParameters( + final ObjectAdapter adapter, final ObjectAction objectAction, final ConcurrencyChecking concurrencyChecking) { + + final PageParameters pageParameters = PageParametersUtils.newPageParameters(); + + final String oidStr = concurrencyChecking == ConcurrencyChecking.CHECK? + adapter.getOid().enString(getOidMarshaller()): + adapter.getOid().enStringNoVersion(getOidMarshaller()); + PageParameterNames.OBJECT_OID.addStringTo(pageParameters, oidStr); + + final ActionType actionType = objectAction.getType(); + PageParameterNames.ACTION_TYPE.addEnumTo(pageParameters, actionType); + + final ObjectSpecification actionOnTypeSpec = objectAction.getOnType(); + if (actionOnTypeSpec != null) { + PageParameterNames.ACTION_OWNING_SPEC.addStringTo(pageParameters, actionOnTypeSpec.getFullIdentifier()); + } + + final String actionId = determineActionId(objectAction); + PageParameterNames.ACTION_ID.addStringTo(pageParameters, actionId); + + return pageParameters; + } + + + public static Entry<Integer, String> parse(final String paramContext) { + final Matcher matcher = KEY_VALUE_PATTERN.matcher(paramContext); + if (!matcher.matches()) { + return null; + } + + final int paramNum; + try { + paramNum = Integer.parseInt(matcher.group(1)); + } catch (final Exception e) { + // ignore + return null; + } + + final String oidStr; + try { + oidStr = matcher.group(2); + } catch (final Exception e) { + return null; + } + + return new Map.Entry<Integer, String>() { + + @Override + public Integer getKey() { + return paramNum; + } + + @Override + public String getValue() { + return oidStr; + } + + @Override + public String setValue(final String value) { + return null; + } + }; + } + + ////////////////////////////////////////////////// + // BookmarkableModel + ////////////////////////////////////////////////// + + public PageParameters getPageParameters() { + final ObjectAdapter adapter = getTargetAdapter(); + final ObjectAction objectAction = getActionMemento().getAction(); + final PageParameters pageParameters = createPageParameters( + adapter, objectAction, ConcurrencyChecking.NO_CHECK); + + // capture argument values + final ObjectAdapter[] argumentsAsArray = getArgumentsAsArray(); + for(final ObjectAdapter argumentAdapter: argumentsAsArray) { + final String encodedArg = encodeArg(argumentAdapter); + PageParameterNames.ACTION_ARGS.addStringTo(pageParameters, encodedArg); + } + + return pageParameters; + } + + @Override + public String getTitle() { + final ObjectAdapter adapter = getTargetAdapter(); + final ObjectAction objectAction = getActionMemento().getAction(); + + final StringBuilder buf = new StringBuilder(); + final ObjectAdapter[] argumentsAsArray = getArgumentsAsArray(); + for(final ObjectAdapter argumentAdapter: argumentsAsArray) { + if(buf.length() > 0) { + buf.append(","); + } + buf.append(abbreviated(titleOf(argumentAdapter), 8)); + } + + return adapter.titleString(null) + "." + objectAction.getName() + (buf.length()>0?"(" + buf.toString() + ")":""); + } + + @Override + public boolean hasAsRootPolicy() { + return true; + } + + ////////////////////////////////////////////////// + // helpers + ////////////////////////////////////////////////// + + + private static String titleOf(final ObjectAdapter argumentAdapter) { + return argumentAdapter!=null?argumentAdapter.titleString(null):""; + } + + private static String abbreviated(final String str, final int maxLength) { + return str.length() < maxLength ? str : str.substring(0, maxLength - 3) + "..."; + } + + + private static String determineActionId(final ObjectAction objectAction) { + final Identifier identifier = objectAction.getIdentifier(); + if (identifier != null) { + return identifier.toNameParmsIdentityString(); + } + // fallback (used for action sets) + return objectAction.getId(); + } + + public static Mode determineMode(final ObjectAction action) { + return action.getParameterCount() > 0 ? Mode.PARAMETERS : Mode.RESULTS; + } + + private final ObjectAdapterMemento targetAdapterMemento; + private final ActionMemento actionMemento; + private Mode actionMode; + + + /** + * Lazily populated in {@link #getArgumentModel(ActionParameterMemento)} + */ + private final Map<Integer, ScalarModel> arguments = Maps.newHashMap(); + private ActionExecutor executor; + + + private ActionModel(final PageParameters pageParameters) { + this(newObjectAdapterMementoFrom(pageParameters), newActionMementoFrom(pageParameters), actionModeFrom(pageParameters)); + + setArgumentsIfPossible(pageParameters); + setContextArgumentIfPossible(pageParameters); + } + + private static ActionMemento newActionMementoFrom(final PageParameters pageParameters) { + final ObjectSpecId owningSpec = ObjectSpecId.of(PageParameterNames.ACTION_OWNING_SPEC.getStringFrom(pageParameters)); + final ActionType actionType = PageParameterNames.ACTION_TYPE.getEnumFrom(pageParameters, ActionType.class); + final String actionNameParms = PageParameterNames.ACTION_ID.getStringFrom(pageParameters); + return new ActionMemento(owningSpec, actionType, actionNameParms); + } + + private static Mode actionModeFrom(final PageParameters pageParameters) { + final ActionMemento actionMemento = newActionMementoFrom(pageParameters); + if(actionMemento.getAction().getParameterCount() == 0) { + return Mode.RESULTS; + } + final List<String> listFrom = PageParameterNames.ACTION_ARGS.getListFrom(pageParameters); + return !listFrom.isEmpty()? Mode.RESULTS: Mode.PARAMETERS; + } + + + private static ObjectAdapterMemento newObjectAdapterMementoFrom(final PageParameters pageParameters) { + final RootOid oid = oidFor(pageParameters); + if(oid.isTransient()) { + return null; + } else { + return ObjectAdapterMemento.createPersistent(oid); + } + } + + private static RootOid oidFor(final PageParameters pageParameters) { + final String oidStr = PageParameterNames.OBJECT_OID.getStringFrom(pageParameters); + return getOidMarshaller().unmarshal(oidStr, RootOid.class); + } + + + private ActionModel(final ObjectAdapterMemento adapterMemento, final ActionMemento actionMemento, final Mode actionMode) { + this.targetAdapterMemento = adapterMemento; + this.actionMemento = actionMemento; + this.actionMode = actionMode; + } + + /** + * Copy constructor, as called by {@link #copy()}. + */ + private ActionModel(final ActionModel actionModel) { + this.targetAdapterMemento = actionModel.targetAdapterMemento; + this.actionMemento = actionModel.actionMemento; + this.actionMode = actionModel.actionMode; + //this.actionPrompt = actionModel.actionPrompt; + + primeArgumentModels(); + final Map<Integer, ScalarModel> argumentModelByIdx = actionModel.arguments; + for (final Map.Entry<Integer,ScalarModel> argumentModel : argumentModelByIdx.entrySet()) { + setArgument(argumentModel.getKey(), argumentModel.getValue().getObject()); + } + + this.executor = actionModel.executor; + } + + private void setArgumentsIfPossible(final PageParameters pageParameters) { + final List<String> args = PageParameterNames.ACTION_ARGS.getListFrom(pageParameters); + + final ObjectAction action = actionMemento.getAction(); + final List<ObjectSpecification> parameterTypes = action.getParameterTypes(); + + for (int paramNum = 0; paramNum < args.size(); paramNum++) { + final String encoded = args.get(paramNum); + setArgument(paramNum, parameterTypes.get(paramNum), encoded); + } + } + + public boolean hasParameters() { + return actionMode == ActionModel.Mode.PARAMETERS; + } + + private boolean setContextArgumentIfPossible(final PageParameters pageParameters) { + final String paramContext = PageParameterNames.ACTION_PARAM_CONTEXT.getStringFrom(pageParameters); + if (paramContext == null) { + return false; + } + + final ObjectAction action = actionMemento.getAction(); + final List<ObjectSpecification> parameterTypes = action.getParameterTypes(); + final int parameterCount = parameterTypes.size(); + + final Map.Entry<Integer, String> mapEntry = parse(paramContext); + + final int paramNum = mapEntry.getKey(); + if (paramNum >= parameterCount) { + return false; + } + + final String encoded = mapEntry.getValue(); + setArgument(paramNum, parameterTypes.get(paramNum), encoded); + + return true; + } + + private void setArgument(final int paramNum, final ObjectSpecification argSpec, final String encoded) { + final ObjectAdapter argumentAdapter = decodeArg(argSpec, encoded); + setArgument(paramNum, argumentAdapter); + } + + private String encodeArg(final ObjectAdapter adapter) { + if(adapter == null) { + return NULL_ARG; + } + + final ObjectSpecification objSpec = adapter.getSpecification(); + if(objSpec.isEncodeable()) { + final EncodableFacet encodeable = objSpec.getFacet(EncodableFacet.class); + return encodeable.toEncodedString(adapter); + } + + return adapter.getOid().enStringNoVersion(getOidMarshaller()); + } + + private ObjectAdapter decodeArg(final ObjectSpecification objSpec, final String encoded) { + if(NULL_ARG.equals(encoded)) { + return null; + } + + if(objSpec.isEncodeable()) { + final EncodableFacet encodeable = objSpec.getFacet(EncodableFacet.class); + return encodeable.fromEncodedString(encoded); + } + + try { + final RootOid oid = RootOidDefault.deStringEncoded(encoded, getOidMarshaller()); + return getAdapterManager().adapterFor(oid); + } catch (final Exception e) { + return null; + } + } + + private void setArgument(final int paramNum, final ObjectAdapter argumentAdapter) { + final ObjectAction action = actionMemento.getAction(); + final ObjectActionParameter actionParam = action.getParameters().get(paramNum); + final ActionParameterMemento apm = new ActionParameterMemento(actionParam); + final ScalarModel argumentModel = getArgumentModel(apm); + argumentModel.setObject(argumentAdapter); + } + + + public ScalarModel getArgumentModel(final ActionParameterMemento apm) { + final int i = apm.getNumber(); + ScalarModel scalarModel = arguments.get(i); + if (scalarModel == null) { + scalarModel = new ScalarModel(targetAdapterMemento, apm); + final int number = scalarModel.getParameterMemento().getNumber(); + arguments.put(number, scalarModel); + } + return scalarModel; + } + + public ObjectAdapter getTargetAdapter() { + return targetAdapterMemento.getObjectAdapter(getConcurrencyChecking()); + } + + protected ConcurrencyChecking getConcurrencyChecking() { + return actionMemento.getConcurrencyChecking(); + } + + public ActionMemento getActionMemento() { + return actionMemento; + } + + @Override + protected ObjectAdapter load() { + + // from getObject()/reExecute + detach(); // force re-execute + + // TODO: think we need another field to determine if args have been populated. + final ObjectAdapter results = executeAction(); + this.actionMode = Mode.RESULTS; + + return results; + } + + // REVIEW: should provide this rendering context, rather than hardcoding. + // the net effect currently is that class members annotated with + // @Hidden(where=Where.ANYWHERE) or @Disabled(where=Where.ANYWHERE) will indeed + // be hidden/disabled, but will be visible/enabled (perhaps incorrectly) + // for any other value for Where + public static final Where WHERE_FOR_ACTION_INVOCATION = Where.ANYWHERE; + + private ObjectAdapter executeAction() { + + final ObjectAdapter targetAdapter = getTargetAdapter(); + final ObjectAdapter[] arguments = getArgumentsAsArray(); + final ObjectAction action = getActionMemento().getAction(); + + final AuthenticationSession session = getAuthenticationSession(); + return action.executeWithRuleChecking(targetAdapter, arguments, session, WHERE_FOR_ACTION_INVOCATION); + } + + public String getReasonInvalidIfAny() { + final ObjectAdapter targetAdapter = getTargetAdapter(); + final ObjectAdapter[] proposedArguments = getArgumentsAsArray(); + final ObjectAction objectAction = getActionMemento().getAction(); + final Consent validity = objectAction.isProposedArgumentSetValid(targetAdapter, proposedArguments); + return validity.isAllowed() ? null : validity.getReason(); + } + + @Override + public void setObject(final ObjectAdapter object) { + throw new UnsupportedOperationException("target adapter for ActionModel cannot be changed"); + } + + public ObjectAdapter[] getArgumentsAsArray() { + if(this.arguments.size() < this.getActionMemento().getAction().getParameterCount()) { + primeArgumentModels(); + } + + final ObjectAction objectAction = getActionMemento().getAction(); + final ObjectAdapter[] arguments = new ObjectAdapter[objectAction.getParameterCount()]; + for (int i = 0; i < arguments.length; i++) { + final ScalarModel scalarModel = this.arguments.get(i); + arguments[i] = scalarModel.getObject(); + } + return arguments; + } + + public ActionExecutor getExecutor() { + return executor; + } + + public void setExecutor(final ActionExecutor executor) { + this.executor = executor; + } + + public void reset() { + this.actionMode = determineMode(actionMemento.getAction()); + } + + public void clearArguments() { + for (final ScalarModel argumentModel : arguments.values()) { + argumentModel.reset(); + } + this.actionMode = determineMode(actionMemento.getAction()); + } + + /** + * Bookmarkable if the {@link ObjectAction action} has a {@link BookmarkPolicyFacet bookmark} policy + * of {@link BookmarkPolicy#AS_ROOT root}, and has safe {@link ObjectAction#getSemantics() semantics}. + */ + public boolean isBookmarkable() { + final ObjectAction action = getActionMemento().getAction(); + final BookmarkPolicyFacet bookmarkPolicy = action.getFacet(BookmarkPolicyFacet.class); + final boolean safeSemantics = action.getSemantics() == ActionSemantics.Of.SAFE; + return bookmarkPolicy.value() == BookmarkPolicy.AS_ROOT && safeSemantics; + } + + // ////////////////////////////////////// + + /** + * Executes the action, handling any {@link RecoverableException}s that + * might be encountered. + * + * <p> + * If an {@link RecoverableException} is encountered, then the application error will be + * {@link org.apache.isis.core.commons.authentication.MessageBroker#setApplicationError(String) set} so that a suitable message can be + * rendered higher up the call stack. + * + * <p> + * Any other types of exception will be ignored (to be picked up higher up in the callstack) + */ + public ObjectAdapter executeHandlingApplicationExceptions() { + try { + final ObjectAdapter resultAdapter = this.getObject(); + return resultAdapter; + + } catch (final RuntimeException ex) { + + // see if is an application-defined exception + // if so, is converted to an application error, + // equivalent to calling DomainObjectContainer#raiseError(...) + final RecoverableException appEx = getApplicationExceptionIfAny(ex); + if (appEx != null) { + IsisContext.getMessageBroker().setApplicationError(appEx.getMessage()); + + // there's no need to set the abort cause on the transaction, it will have already been done + // (in IsisTransactionManager#executeWithinTransaction(...)). + + return null; + } + + // not handled, so propagate + throw ex; + } + } + + + // ////////////////////////////////////// + + public static RecoverableException getApplicationExceptionIfAny(final Exception ex) { + final Iterable<RecoverableException> appEx = Iterables.filter(Throwables.getCausalChain(ex), RecoverableException.class); + final Iterator<RecoverableException> iterator = appEx.iterator(); + return iterator.hasNext() ? iterator.next() : null; + } + + public static IRequestHandler redirectHandler(final Object value) { + if(value instanceof java.net.URL) { + final java.net.URL url = (java.net.URL) value; + return new RedirectRequestHandler(url.toString()); + } + return null; + } + + public static IRequestHandler downloadHandler(final Object value) { + if(value instanceof Clob) { + final Clob clob = (Clob)value; + return handlerFor(resourceStreamFor(clob), clob); + } + if(value instanceof Blob) { + final Blob blob = (Blob)value; + return handlerFor(resourceStreamFor(blob), blob); + } + return null; + } + + private static IResourceStream resourceStreamFor(final Blob blob) { + final IResourceStream resourceStream = new AbstractResourceStream() { + + private static final long serialVersionUID = 1L; + + @Override + public InputStream getInputStream() throws ResourceStreamNotFoundException { + return new ByteArrayInputStream(blob.getBytes()); + } + + @Override + public String getContentType() { + return blob.getMimeType().toString(); + } + + @Override + public void close() throws IOException { + } + }; + return resourceStream; + } + + private static IResourceStream resourceStreamFor(final Clob clob) { + final IResourceStream resourceStream = new StringResourceStream(clob.getChars(), clob.getMimeType().toString()); + return resourceStream; + } + + private static IRequestHandler handlerFor(final IResourceStream resourceStream, final NamedWithMimeType namedWithMimeType) { + final ResourceStreamRequestHandler handler = + new ResourceStreamRequestHandler(resourceStream, namedWithMimeType.getName()); + handler.setContentDisposition(ContentDisposition.ATTACHMENT); + return handler; + } + + // ////////////////////////////////////// + + public List<ActionParameterMemento> primeArgumentModels() { + final ObjectAction objectAction = getActionMemento().getAction(); + + final List<ObjectActionParameter> parameters = objectAction.getParameters(); + final List<ActionParameterMemento> mementos = buildParameterMementos(parameters); + for (final ActionParameterMemento apm : mementos) { + getArgumentModel(apm); + } + + return mementos; + } + + + private static List<ActionParameterMemento> buildParameterMementos(final List<ObjectActionParameter> parameters) { + final List<ActionParameterMemento> parameterMementoList = Lists.transform(parameters, ObjectAdapterMemento.Functions.fromActionParameter()); + // we copy into a new array list otherwise we get lazy evaluation = + // reference to a non-serializable object + return Lists.newArrayList(parameterMementoList); + } + + ////////////////////////////////////////////////// + // Dependencies (from context) + ////////////////////////////////////////////////// + + private static OidMarshaller getOidMarshaller() { + return IsisContext.getOidMarshaller(); + } + + public ActionModel copy() { + return new ActionModel(this); + } + + + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java new file mode 100644 index 0000000..0e2747a --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPrompt.java @@ -0,0 +1,71 @@ +/* + * 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 org.apache.wicket.Component; +import org.apache.wicket.ajax.AjaxRequestTarget; + +/** + * Decouples the {@link ActionModel} from its (modal window) prompt. + */ +public interface ActionPrompt extends Serializable { + + /** + * Sets the component that should be used as a title for the modal window + * + * @param component The title component + * @param target The current Ajax request handler + */ + void setTitle(Component component, AjaxRequestTarget target); + + /** + * Sets the component that should be used as a body for the modal window + * + * @param component The body component + * @param target The current Ajax request handler + */ + void setPanel(Component component, AjaxRequestTarget target); + + /** + * Shows the modal window + * + * @param target The current Ajax request handler + */ + void showPrompt(AjaxRequestTarget target); + + /** + * @return the component id for the title component + */ + String getTitleId(); + + /** + * @return the component id for the body component + */ + String getContentId(); + + /** + * Closes the modal window + * + * @param target The current Ajax request handler + */ + void closePrompt(AjaxRequestTarget target); +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java new file mode 100644 index 0000000..42fbda5 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/ActionPromptProvider.java @@ -0,0 +1,44 @@ +/** + * 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 org.apache.wicket.Component; +import org.apache.wicket.Page; + +public interface ActionPromptProvider { + + public static class Util{ + + public static ActionPromptProvider getFrom(Component component) { + final Page page = component.getPage(); + if(page == null) { + throw new IllegalArgumentException("Programming error: component must be added to a page in order to locate the ActionPromptProvider"); + } + return getFrom(page); + } + public static ActionPromptProvider getFrom(Page page) { + if(page instanceof ActionPromptProvider) { + final ActionPromptProvider provider = (ActionPromptProvider) page; + return provider; + } + // else + throw new IllegalArgumentException("Programming error: all pages should inherit from PageAbstract, which serves as the ActionPromptProvider"); + } + } + + public ActionPrompt getActionPrompt(); +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java new file mode 100644 index 0000000..089eb88 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNode.java @@ -0,0 +1,270 @@ +/* + * 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.List; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.collect.Lists; + +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.isis.core.metamodel.adapter.ObjectAdapter; +import org.apache.isis.core.metamodel.adapter.oid.Oid; +import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; +import org.apache.isis.core.metamodel.adapter.oid.RootOid; +import org.apache.isis.core.metamodel.spec.feature.Contributed; +import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; +import org.apache.isis.core.runtime.system.context.IsisContext; +import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames; + +public class BookmarkTreeNode implements Serializable { + + private static final long serialVersionUID = 1L; + + private final List<BookmarkTreeNode> children = Lists.newArrayList(); + private final int depth; + + private final RootOid oidNoVer; + private final String oidNoVerStr; + private final PageType pageType; + + private String title; + private PageParameters pageParameters; + + public static BookmarkTreeNode newRoot( + BookmarkableModel<?> bookmarkableModel) { + return new BookmarkTreeNode(bookmarkableModel, 0); + } + + private BookmarkTreeNode( + final BookmarkableModel<?> bookmarkableModel, + final int depth) { + pageParameters = bookmarkableModel.getPageParameters(); + RootOid oid = oidFrom(pageParameters); + this.oidNoVerStr = getOidMarshaller().marshalNoVersion(oid); + this.oidNoVer = getOidMarshaller().unmarshal(oidNoVerStr, RootOid.class); + + // replace oid with the noVer equivalent. + PageParameterNames.OBJECT_OID.removeFrom(pageParameters); + PageParameterNames.OBJECT_OID.addStringTo(pageParameters, getOidNoVerStr()); + + this.title = bookmarkableModel.getTitle(); + this.pageType = bookmarkableModel instanceof EntityModel ? PageType.ENTITY : PageType.ACTION_PROMPT; + this.depth = depth; + + } + + public RootOid getOidNoVer() { + return oidNoVer; + } + + public String getOidNoVerStr() { + return oidNoVerStr; + } + + public String getTitle() { + return title; + } + private void setTitle(String title) { + this.title = title; + } + + public PageType getPageType() { + return pageType; + } + + public List<BookmarkTreeNode> getChildren() { + return children; + } + public BookmarkTreeNode addChild(BookmarkableModel<?> childModel) { + final BookmarkTreeNode childNode = new BookmarkTreeNode(childModel, depth+1); + children.add(childNode); + return childNode; + } + + /** + * Whether or not the provided {@link BookmarkableModel} matches that contained + * within this node, or any of its children. + * + * <p> + * If it does, then the matched node's title is updated to that of the provided + * {@link BookmarkableModel}. + * + * <p> + * The {@link PageParameters} (used for matching) is + * {@link BookmarkableModel#getPageParameters() obtained} from the {@link BookmarkableModel}. + * + * @return - whether the provided candidate is found or was added to this node's tree. + */ + public boolean matches(BookmarkableModel<?> candidateBookmarkableModel) { + if(candidateBookmarkableModel instanceof EntityModel) { + if(this.pageType != PageType.ENTITY) { + return false; + } + return matchAndUpdateTitleFor((EntityModel) candidateBookmarkableModel); + } else if(candidateBookmarkableModel instanceof ActionModel) { + if(this.pageType != PageType.ACTION_PROMPT) { + return false; + } + return matchFor((ActionModel) candidateBookmarkableModel); + } else { + return false; + } + } + + /** + * Whether or not the provided {@link EntityModel} matches that contained + * within this node, or any of its children. + * + * <p> + * If it does match, then the matched node's title is updated to that of the provided + * {@link EntityModel}. + * + * @return - whether the provided candidate is found or was added to this node's tree. + */ + private boolean matchAndUpdateTitleFor(final EntityModel candidateEntityModel) { + + // match only on the oid string + final String candidateOidStr = oidStrFrom(candidateEntityModel); + boolean inGraph = Objects.equal(this.oidNoVerStr, candidateOidStr); + if(inGraph) { + this.setTitle(candidateEntityModel.getTitle()); + } + + // and also match recursively down to all children and grandchildren. + if(candidateEntityModel.hasAsChildPolicy()) { + for(BookmarkTreeNode childNode: this.getChildren()) { + inGraph = childNode.matches(candidateEntityModel) || inGraph; // evaluate each + } + + if(!inGraph) { + inGraph = addToGraphIfParented(candidateEntityModel); + } + } + return inGraph; + } + + /** + * Whether or not the provided {@link ActionModel} matches that contained + * within this node (taking into account the action's arguments). + * + * If it does match, then the matched node's title is updated to that of the provided + * {@link ActionModel}. + * <p> + * + * @return - whether the provided candidate is found or was added to this node's tree. + */ + private boolean matchFor(final ActionModel candidateActionModel) { + + // check if target object of the action is the same (the oid str) + final String candidateOidStr = oidStrFrom(candidateActionModel); + if(!Objects.equal(this.oidNoVerStr, candidateOidStr)) { + return false; + } + + // check if args same + List<String> thisArgs = PageParameterNames.ACTION_ARGS.getListFrom(pageParameters); + PageParameters candidatePageParameters = candidateActionModel.getPageParameters(); + List<String> candidateArgs = PageParameterNames.ACTION_ARGS.getListFrom(candidatePageParameters); + if(!Objects.equal(thisArgs, candidateArgs)) { + return false; + } + + // ok, a match + return true; + } + + private boolean addToGraphIfParented(BookmarkableModel<?> candidateBookmarkableModel) { + + boolean whetherAdded = false; + // TODO: this ought to be move into a responsibility of BookmarkableModel, perhaps, rather than downcasting + if(candidateBookmarkableModel instanceof EntityModel) { + EntityModel entityModel = (EntityModel) candidateBookmarkableModel; + final ObjectAdapter candidateAdapter = entityModel.getObject(); + final List<ObjectAssociation> properties = candidateAdapter.getSpecification().getAssociations(Contributed.EXCLUDED, ObjectAssociation.Filters.REFERENCE_PROPERTIES); + for (ObjectAssociation objectAssoc : properties) { + final ObjectAdapter possibleParentAdapter = objectAssoc.get(candidateAdapter); + if(possibleParentAdapter == null) { + continue; + } + final Oid possibleParentOid = possibleParentAdapter.getOid(); + if(possibleParentOid == null) { + continue; + } + final String possibleParentOidStr = possibleParentOid.enStringNoVersion(getOidMarshaller()); + if(Objects.equal(this.oidNoVerStr, possibleParentOidStr)) { + this.addChild(candidateBookmarkableModel); + whetherAdded = true; + } + } + } + return whetherAdded; + } + + public void appendGraphTo(List<BookmarkTreeNode> list) { + list.add(this); + for (BookmarkTreeNode childNode : children) { + childNode.appendGraphTo(list); + } + } + + public int getDepth() { + return depth; + } + + + // ////////////////////////////////////// + + public PageParameters getPageParameters() { + return pageParameters; + } + + // ////////////////////////////////////// + + public static RootOid oidFrom(final PageParameters pageParameters) { + String oidStr = PageParameterNames.OBJECT_OID.getStringFrom(pageParameters); + if(oidStr == null) { + return null; + } + try { + return getOidMarshaller().unmarshal(oidStr, RootOid.class); + } catch(Exception ex) { + return null; + } + } + + public static String oidStrFrom(BookmarkableModel<?> candidateBookmarkableModel) { + final RootOid oid = oidFrom(candidateBookmarkableModel.getPageParameters()); + return oid != null? getOidMarshaller().marshalNoVersion(oid): null; + } + + + // ////////////////////////////////////// + + protected static OidMarshaller getOidMarshaller() { + return IsisContext.getOidMarshaller(); + } + + + + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java new file mode 100644 index 0000000..638968e --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkTreeNodeComparator.java @@ -0,0 +1,86 @@ +/* + * 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.util.Comparator; + +import org.apache.wicket.request.mapper.parameter.PageParameters; + +import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; +import org.apache.isis.core.metamodel.adapter.oid.RootOid; +import org.apache.isis.core.metamodel.spec.ObjectSpecId; +import org.apache.isis.core.metamodel.spec.SpecificationLoader; +import org.apache.isis.core.runtime.system.context.IsisContext; +import org.apache.isis.viewer.wicket.model.mementos.PageParameterNames; + +final class BookmarkTreeNodeComparator implements Comparator<BookmarkTreeNode> { + + @Override + public int compare(BookmarkTreeNode o1, BookmarkTreeNode o2) { + + final PageType pageType1 = o1.getPageType(); + final PageType pageType2 = o2.getPageType(); + + final int pageTypeComparison = pageType1.compareTo(pageType2); + if(pageTypeComparison != 0) { + return pageTypeComparison; + } + + final RootOid oid1 = o1.getOidNoVer(); + final RootOid oid2 = o2.getOidNoVer(); + + // sort by entity type + final String className1 = classNameOf(oid1); + final String className2 = classNameOf(oid2); + + final int classNameComparison = className1.compareTo(className2); + if(classNameComparison != 0) { + return classNameComparison; + } + + final String title1 = o1.getTitle(); + final String title2 = o2.getTitle(); + + return title1.compareTo(title2); + } + + private String classNameOf(RootOid oid) { + ObjectSpecId objectSpecId = oid.getObjectSpecId(); + return getSpecificationLoader().lookupBySpecId(objectSpecId).getIdentifier().getClassName(); + } + + private RootOid oidOf(PageParameters pp) { + String oidStr = PageParameterNames.OBJECT_OID.getStringFrom(pp); + return getOidMarshaller().unmarshal(oidStr, RootOid.class); + } + + ////////////////////////////////////////////////// + // Dependencies (from context) + ////////////////////////////////////////////////// + + protected OidMarshaller getOidMarshaller() { + return IsisContext.getOidMarshaller(); + } + + protected SpecificationLoader getSpecificationLoader() { + return IsisContext.getSpecificationLoader(); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java new file mode 100644 index 0000000..1dfa2ec --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkableModel.java @@ -0,0 +1,46 @@ +/* + * 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 org.apache.wicket.request.mapper.parameter.PageParameters; + +public abstract class BookmarkableModel<T> extends ModelAbstract<T> { + + private static final long serialVersionUID = 1L; + + public BookmarkableModel() { + super(); + } + + public BookmarkableModel(T t) { + super(t); + } + + + /** + * So can be bookmarked / added to <tt>BookmarkedPagesModel</tt>. + */ + public abstract PageParameters getPageParameters(); + + public abstract boolean hasAsRootPolicy(); + + public abstract String getTitle(); + +} http://git-wip-us.apache.org/repos/asf/isis/blob/d84c6609/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java ---------------------------------------------------------------------- diff --git a/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java new file mode 100644 index 0000000..1a5d614 --- /dev/null +++ b/core/viewer-wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/models/BookmarkedPagesModel.java @@ -0,0 +1,151 @@ +/* + * 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.util.Collections; +import java.util.Iterator; +import java.util.List; +import com.google.common.base.Objects; +import com.google.common.collect.Lists; +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.isis.core.commons.config.IsisConfiguration; +import org.apache.isis.core.metamodel.adapter.oid.RootOid; +import org.apache.isis.core.runtime.system.context.IsisContext; + + +public class BookmarkedPagesModel extends ModelAbstract<List<? extends BookmarkTreeNode>> { + + + private static final long serialVersionUID = 1L; + + private static final BookmarkTreeNodeComparator COMPARATOR = new BookmarkTreeNodeComparator(); + + private static final String MAX_SIZE_KEY = "isis.viewer.wicket.bookmarkedPages.maxSize"; + private static final int MAX_SIZE_DEFAULT_VALUE = 15; + + private final List<BookmarkTreeNode> rootNodes = Lists.newArrayList(); + + private transient PageParameters current; + + public void bookmarkPage(final BookmarkableModel<?> bookmarkableModel) { + + // hack: remove any garbage that might've got stored in 'rootNodes' + cleanUpGarbage(rootNodes); + + final PageParameters candidatePP = bookmarkableModel.getPageParameters(); + RootOid oid = BookmarkTreeNode.oidFrom(candidatePP); + if(oid == null) { + // ignore + return; + } + + BookmarkTreeNode rootNode = null; + for (BookmarkTreeNode eachNode : rootNodes) { + if(eachNode.matches(bookmarkableModel)) { + rootNode = eachNode; + } + } + // MRU/LRU algorithm + if(rootNode != null) { + rootNodes.remove(rootNode); + rootNodes.add(0, rootNode); + current = candidatePP; + } else { + if (bookmarkableModel.hasAsRootPolicy()) { + rootNode = BookmarkTreeNode.newRoot(bookmarkableModel); + rootNodes.add(0, rootNode); + current = candidatePP; + } + } + + trim(rootNodes, getMaxSize()); + } + + private int getMaxSize() { + return getConfiguration().getInteger(MAX_SIZE_KEY, MAX_SIZE_DEFAULT_VALUE); + } + + private static void trim(List<?> list, int requiredSize) { + int numToRetain = Math.min(list.size(), requiredSize); + list.retainAll(list.subList(0, numToRetain)); + } + + @Override + protected List<BookmarkTreeNode> load() { + List<BookmarkTreeNode> depthFirstGraph = Lists.newArrayList(); + + List<BookmarkTreeNode> sortedNodes = Lists.newArrayList(rootNodes); + Collections.sort(sortedNodes, COMPARATOR); + + for (BookmarkTreeNode rootNode : sortedNodes) { + rootNode.appendGraphTo(depthFirstGraph); + } + return depthFirstGraph; + } + + public boolean isCurrent(PageParameters pageParameters) { + return Objects.equal(current, pageParameters); + } + + private static void cleanUpGarbage(List<BookmarkTreeNode> rootNodes) { + final Iterator<BookmarkTreeNode> iter = rootNodes.iterator(); + while(iter.hasNext()) { + BookmarkTreeNode node = iter.next(); + // think this is redundant... + if(node.getOidNoVer() == null) { + iter.remove(); + } + } + } + + public void clear() { + rootNodes.clear(); + } + + public boolean isEmpty() { + return rootNodes.isEmpty(); + } + + public void remove(BookmarkTreeNode rootNode) { + this.rootNodes.remove(rootNode); + } + + public void remove(EntityModel entityModel) { + BookmarkTreeNode rootNode = null; + for (BookmarkTreeNode eachNode : rootNodes) { + if(eachNode.getOidNoVerStr().equals((entityModel).getObjectAdapterMemento().toString())) { + rootNode = eachNode; + } + } + if(rootNode != null) { + rootNodes.remove(rootNode); + } + } + + // ////////////////////////////////////// + + + protected IsisConfiguration getConfiguration() { + return IsisContext.getConfiguration(); + } + + + +}