http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.java ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.java b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.java new file mode 100644 index 0000000..75f5583 --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.java @@ -0,0 +1,979 @@ +/* + * 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 dom.todo; + +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.jdo.JDOHelper; +import javax.jdo.annotations.IdentityType; +import javax.jdo.annotations.VersionStrategy; +import com.google.common.base.Objects; +import com.google.common.base.Predicate; +import com.google.common.collect.Ordering; +import org.joda.time.LocalDate; +import org.apache.isis.applib.DomainObjectContainer; +import org.apache.isis.applib.Identifier; +import org.apache.isis.applib.NonRecoverableException; +import org.apache.isis.applib.RecoverableException; +import org.apache.isis.applib.annotation.Action; +import org.apache.isis.applib.annotation.BookmarkPolicy; +import org.apache.isis.applib.annotation.Collection; +import org.apache.isis.applib.annotation.CollectionLayout; +import org.apache.isis.applib.annotation.DomainObject; +import org.apache.isis.applib.annotation.DomainObjectLayout; +import org.apache.isis.applib.annotation.Editing; +import org.apache.isis.applib.annotation.RestrictTo; +import org.apache.isis.applib.annotation.InvokeOn; +import org.apache.isis.applib.annotation.InvokedOn; +import org.apache.isis.applib.annotation.Optionality; +import org.apache.isis.applib.annotation.Parameter; +import org.apache.isis.applib.annotation.ParameterLayout; +import org.apache.isis.applib.annotation.Programmatic; +import org.apache.isis.applib.annotation.Property; +import org.apache.isis.applib.annotation.SemanticsOf; +import org.apache.isis.applib.annotation.Where; +import org.apache.isis.applib.security.UserMemento; +import org.apache.isis.applib.services.actinvoc.ActionInvocationContext; +import org.apache.isis.applib.services.eventbus.ActionDomainEvent; +import org.apache.isis.applib.services.eventbus.EventBusService; +import org.apache.isis.applib.services.scratchpad.Scratchpad; +import org.apache.isis.applib.services.wrapper.HiddenException; +import org.apache.isis.applib.services.wrapper.WrapperFactory; +import org.apache.isis.applib.util.ObjectContracts; +import org.apache.isis.applib.util.TitleBuffer; +import org.apache.isis.applib.value.Blob; +import org.apache.isis.applib.value.Clob; + [email protected](identityType=IdentityType.DATASTORE) [email protected]( + strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY, + column="id") [email protected]( + strategy=VersionStrategy.VERSION_NUMBER, + column="version") [email protected]({ + @javax.jdo.annotations.Unique( + name="ToDoItem_description_must_be_unique", + members={"ownedBy","description"}) +}) [email protected]( { + @javax.jdo.annotations.Query( + name = "findByOwnedBy", language = "JDOQL", + value = "SELECT " + + "FROM dom.todo.ToDoItem " + + "WHERE ownedBy == :ownedBy"), + @javax.jdo.annotations.Query( + name = "findByOwnedByAndCompleteIsFalse", language = "JDOQL", + value = "SELECT " + + "FROM dom.todo.ToDoItem " + + "WHERE ownedBy == :ownedBy " + + " && complete == false"), + @javax.jdo.annotations.Query( + name = "findByOwnedByAndCompleteIsTrue", language = "JDOQL", + value = "SELECT " + + "FROM dom.todo.ToDoItem " + + "WHERE ownedBy == :ownedBy " + + "&& complete == true"), + @javax.jdo.annotations.Query( + name = "findByOwnedByAndCategory", language = "JDOQL", + value = "SELECT " + + "FROM dom.todo.ToDoItem " + + "WHERE ownedBy == :ownedBy " + + "&& category == :category"), + @javax.jdo.annotations.Query( + name = "findByOwnedByAndDescriptionContains", language = "JDOQL", + value = "SELECT " + + "FROM dom.todo.ToDoItem " + + "WHERE ownedBy == :ownedBy && " + + "description.indexOf(:description) >= 0") +}) +@DomainObject( + autoCompleteRepository = ToDoItems.class, // for drop-downs, unless autoCompleteNXxx() is present + autoCompleteAction = "autoComplete", + // bounded = true, // for drop-downs if only a small number of instances only (overrides autoComplete) + objectType = "TODO" +) +@DomainObjectLayout( + bookmarking = BookmarkPolicy.AS_ROOT +) +public class ToDoItem implements Categorized, Comparable<ToDoItem> { + + //region > LOG + /** + * It isn't common for entities to log, but they can if required. + * Isis uses slf4j API internally (with log4j as implementation), and is the recommended API to use. + */ + private final static org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(ToDoItem.class); + //endregion + + // region > title, icon + public String title() { + final TitleBuffer buf = new TitleBuffer(); + buf.append(getDescription()); + if (isComplete()) { + buf.append("- Completed!"); + } else { + try { + final LocalDate dueBy = wrapperFactory.wrap(this).getDueBy(); + if (dueBy != null) { + buf.append(" due by", dueBy); + } + } catch(final HiddenException ignored) { + } + } + return buf.toString(); + } + + public String iconName() { + return !isComplete() ? "todo" : "done"; + } + + public String cssClass() { return iconName(); } + + //endregion + + //region > description (property) + private String description; + + @javax.jdo.annotations.Column(allowsNull="false", length=100) + @Property( + regexPattern = "\\w[@&:\\-\\,\\.\\+ \\w]*" + ) + public String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } + public void modifyDescription(final String description) { + setDescription(description); + } + public void clearDescription() { + setDescription(null); + } + //endregion + + //region > dueBy (property) + @javax.jdo.annotations.Persistent(defaultFetchGroup="true") + private LocalDate dueBy; + + @javax.jdo.annotations.Column(allowsNull="true") + public LocalDate getDueBy() { + return dueBy; + } + + /** + * Demonstrates how to perform security checks within the domain code. + * + * <p> + * Generally speaking this approach is not recommended; such checks should + * wherever possible be externalized in the security subsystem. + * </p> + */ + public boolean hideDueBy() { + final UserMemento user = container.getUser(); + return user.hasRole("realm1:noDueBy_role"); + } + + public void setDueBy(final LocalDate dueBy) { + this.dueBy = dueBy; + } + public void clearDueBy() { + setDueBy(null); + } + // proposed new value is validated before setting + public String validateDueBy(final LocalDate dueBy) { + if (dueBy == null) { + return null; + } + return toDoItems.validateDueBy(dueBy); + } + //endregion + + //region > category and subcategory (property) + + public static enum Category { + Professional { + @Override + public List<Subcategory> subcategories() { + return Arrays.asList(null, Subcategory.OpenSource, Subcategory.Consulting, Subcategory.Education, Subcategory.Marketing); + } + }, Domestic { + @Override + public List<Subcategory> subcategories() { + return Arrays.asList(null, Subcategory.Shopping, Subcategory.Housework, Subcategory.Garden, Subcategory.Chores); + } + }, Other { + @Override + public List<Subcategory> subcategories() { + return Arrays.asList(null, Subcategory.Other); + } + }; + + public abstract List<Subcategory> subcategories(); + } + + public static enum Subcategory { + // professional + OpenSource, Consulting, Education, Marketing, + // domestic + Shopping, Housework, Garden, Chores, + // other + Other; + + public static List<Subcategory> listFor(final Category category) { + return category != null? category.subcategories(): Collections.<Subcategory>emptyList(); + } + + static String validate(final Category category, final Subcategory subcategory) { + if(category == null) { + return "Enter category first"; + } + return !category.subcategories().contains(subcategory) + ? "Invalid subcategory for category '" + category + "'" + : null; + } + + public static Predicate<Subcategory> thoseFor(final Category category) { + return new Predicate<Subcategory>() { + + @Override + public boolean apply(final Subcategory subcategory) { + return category.subcategories().contains(subcategory); + } + }; + } + } + + // ////////////////////////////////////// + + private Category category; + + @javax.jdo.annotations.Column(allowsNull="false") + @Property( + editing = Editing.DISABLED, + editingDisabledReason = "Use action to update both category and subcategory" + ) + public Category getCategory() { + return category; + } + + public void setCategory(final Category category) { + this.category = category; + } + + // ////////////////////////////////////// + + private Subcategory subcategory; + + @javax.jdo.annotations.Column(allowsNull="true") + @Property( + editing = Editing.DISABLED, + editingDisabledReason = "Use action to update both category and subcategory" + ) + public Subcategory getSubcategory() { + return subcategory; + } + public void setSubcategory(final Subcategory subcategory) { + this.subcategory = subcategory; + } + //endregion + + //region > ownedBy (property) + + private String ownedBy; + + @javax.jdo.annotations.Column(allowsNull="false") + public String getOwnedBy() { + return ownedBy; + } + + public void setOwnedBy(final String ownedBy) { + this.ownedBy = ownedBy; + } + //endregion + + //region > complete (property), completed (action), notYetCompleted (action) + + private boolean complete; + + @Property( + editing = Editing.DISABLED + ) + public boolean isComplete() { + return complete; + } + + public void setComplete(final boolean complete) { + this.complete = complete; + } + + @Action( + domainEvent =CompletedEvent.class, + invokeOn = InvokeOn.OBJECT_AND_COLLECTION + ) + public ToDoItem completed() { + setComplete(true); + + // + // remainder of method just demonstrates the use of the Bulk.InteractionContext service + // + @SuppressWarnings("unused") + final List<Object> allObjects = actionInvocationContext.getDomainObjects(); + + LOG.debug("completed: " + + actionInvocationContext.getIndex() + + " [" + actionInvocationContext.getSize() + "]" + + (actionInvocationContext.isFirst() ? " (first)" : "") + + (actionInvocationContext.isLast() ? " (last)" : "")); + + // if invoked as a regular action, return this object; + // otherwise (if invoked as bulk), return null (so go back to the list) + return actionInvocationContext.getInvokedOn() == InvokedOn.OBJECT? this: null; + } + // disable action dependent on state of object + public String disableCompleted() { + return isComplete() ? "Already completed" : null; + } + + @Action( + invokeOn = InvokeOn.OBJECT_AND_COLLECTION + ) + public ToDoItem notYetCompleted() { + setComplete(false); + + // if invoked as a regular action, return this object; + // otherwise (if invoked as bulk), return null (so go back to the list) + return actionInvocationContext.getInvokedOn() == InvokedOn.OBJECT ? this: null; + } + // disable action dependent on state of object + public String disableNotYetCompleted() { + return !complete ? "Not yet completed" : null; + } + //endregion + + //region > completeSlowly (property) + // //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + @Action( + hidden = Where.EVERYWHERE + ) + public void completeSlowly(final int millis) { + try { + Thread.sleep(millis); + } catch (final InterruptedException ignored) { + } + setComplete(true); + } + //endregion + + //region > cost (property), updateCost (action) + private BigDecimal cost; + + @javax.jdo.annotations.Column(allowsNull="true", scale=2) + @javax.validation.constraints.Digits(integer=10, fraction=2) + @Property( + editing = Editing.DISABLED, + editingDisabledReason = "Update using action" + ) + public BigDecimal getCost() { + return cost; + } + + public void setCost(final BigDecimal cost) { + this.cost = cost!=null?cost.setScale(2, BigDecimal.ROUND_HALF_EVEN):null; + } + + @Action( + semantics = SemanticsOf.IDEMPOTENT + ) + public ToDoItem updateCost( + @Parameter(optionality = Optionality.OPTIONAL) + @ParameterLayout(named = "New cost") + @javax.validation.constraints.Digits(integer=10, fraction=2) + final BigDecimal cost) { + LOG.debug("%s: cost updated: %s -> %s", container.titleOf(this), getCost(), cost); + + // just to simulate a long-running action + try { + Thread.sleep(3000); + } catch (final InterruptedException ignored) { + } + + setCost(cost); + return this; + } + + // provide a default value for argument #0 + public BigDecimal default0UpdateCost() { + return getCost(); + } + + // validate action arguments + public String validateUpdateCost(final BigDecimal proposedCost) { + if(proposedCost == null) { return null; } + return proposedCost.compareTo(BigDecimal.ZERO) < 0? "Cost must be positive": null; + } + //endregion + + //region > notes (property) + private String notes; + + @javax.jdo.annotations.Column(allowsNull="true", length=400) + public String getNotes() { + return notes; + } + + public void setNotes(final String notes) { + this.notes = notes; + } + //endregion + + //region > attachment (property) + private Blob attachment; + @javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = { + @javax.jdo.annotations.Column(name = "attachment_name"), + @javax.jdo.annotations.Column(name = "attachment_mimetype"), + @javax.jdo.annotations.Column(name = "attachment_bytes", jdbcType = "BLOB", sqlType = "BLOB") + }) + @Property( + optionality = Optionality.OPTIONAL + ) + public Blob getAttachment() { + return attachment; + } + + public void setAttachment(final Blob attachment) { + this.attachment = attachment; + } + //endregion + + //region > doc (property) + private Clob doc; + @javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = { + @javax.jdo.annotations.Column(name = "doc_name"), + @javax.jdo.annotations.Column(name = "doc_mimetype"), + @javax.jdo.annotations.Column(name = "doc_chars", jdbcType = "CLOB", sqlType = "CLOB") + }) + @Property( + optionality = Optionality.OPTIONAL + ) + public Clob getDoc() { + return doc; + } + + public void setDoc(final Clob doc) { + this.doc = doc; + } + //endregion + + //region > version (derived property) + public Long getVersionSequence() { + if(!(this instanceof javax.jdo.spi.PersistenceCapable)) { + return null; + } + return (Long) JDOHelper.getVersion((javax.jdo.spi.PersistenceCapable) this); + } + // hide property (imperatively, based on state of object) + public boolean hideVersionSequence() { + return !(this instanceof javax.jdo.spi.PersistenceCapable); + } + //endregion + + //region > dependencies (property), add (action), remove (action) + + // overrides the natural ordering + public static class DependenciesComparator implements Comparator<ToDoItem> { + @Override + public int compare(final ToDoItem p, final ToDoItem q) { + final Ordering<ToDoItem> byDescription = new Ordering<ToDoItem>() { + public int compare(final ToDoItem p, final ToDoItem q) { + return Ordering.natural().nullsFirst().compare(p.getDescription(), q.getDescription()); + } + }; + return byDescription + .compound(Ordering.<ToDoItem>natural()) + .compare(p, q); + } + } + + @javax.jdo.annotations.Persistent(table="ToDoItemDependencies") + @javax.jdo.annotations.Join(column="dependingId") + @javax.jdo.annotations.Element(column="dependentId") + + private Set<ToDoItem> dependencies = new TreeSet<>(); + //private SortedSet<ToDoItem> dependencies = new TreeSet<>(); // not compatible with neo4j (as of DN v3.2.3) + + @Collection() + @CollectionLayout(/*sortedBy = DependenciesComparator.class*/) // not compatible with neo4j (as of DN v3.2.3) + public Set<ToDoItem> getDependencies() { + return dependencies; + } + + public void setDependencies(final Set<ToDoItem> dependencies) { + this.dependencies = dependencies; + } + + public void addToDependencies(final ToDoItem toDoItem) { + getDependencies().add(toDoItem); + } + public void removeFromDependencies(final ToDoItem toDoItem) { + getDependencies().remove(toDoItem); + } + + public ToDoItem add( + @ParameterLayout(typicalLength = 20) + final ToDoItem toDoItem) { + // By wrapping the call, Isis will detect that the collection is modified + // and it will automatically send CollectionInteractionEvents to the Event Bus. + // ToDoItemSubscriptions is a demo subscriber to this event + wrapperFactory.wrapSkipRules(this).addToDependencies(toDoItem); + return this; + } + public List<ToDoItem> autoComplete0Add( + @Parameter(minLength = 2) + final String search) { + final List<ToDoItem> list = toDoItems.autoComplete(search); + list.removeAll(getDependencies()); + list.remove(this); + return list; + } + + public String disableAdd(final ToDoItem toDoItem) { + if(isComplete()) { + return "Cannot add dependencies for items that are complete"; + } + return null; + } + // validate the provided argument prior to invoking action + public String validateAdd(final ToDoItem toDoItem) { + if(getDependencies().contains(toDoItem)) { + return "Already a dependency"; + } + if(toDoItem == this) { + return "Can't set up a dependency to self"; + } + return null; + } + + public ToDoItem remove( + @ParameterLayout(typicalLength = 20) + final ToDoItem toDoItem) { + // By wrapping the call, Isis will detect that the collection is modified + // and it will automatically send a CollectionInteractionEvent to the Event Bus. + // ToDoItemSubscriptions is a demo subscriber to this event + wrapperFactory.wrapSkipRules(this).removeFromDependencies(toDoItem); + return this; + } + // disable action dependent on state of object + public String disableRemove(final ToDoItem toDoItem) { + if(isComplete()) { + return "Cannot remove dependencies for items that are complete"; + } + return getDependencies().isEmpty()? "No dependencies to remove": null; + } + // validate the provided argument prior to invoking action + public String validateRemove(final ToDoItem toDoItem) { + if(!getDependencies().contains(toDoItem)) { + return "Not a dependency"; + } + return null; + } + // provide a drop-down + public java.util.Collection<ToDoItem> choices0Remove() { + return getDependencies(); + } + //endregion + + //region > clone (action) + + // the name of the action in the UI + // nb: method is not called "clone()" is inherited by java.lang.Object and + // (a) has different semantics and (b) is in any case automatically ignored + // by the framework + public ToDoItem duplicate( + @Parameter(regexPattern = "\\w[@&:\\-\\,\\.\\+ \\w]*" ) + @ParameterLayout(named="Description") + final String description, + @ParameterLayout(named="Category") + final Category category, + @ParameterLayout(named="Subcategory") + final Subcategory subcategory, + @Parameter(optionality = Optionality.OPTIONAL) + @ParameterLayout(named="Due by") + final LocalDate dueBy, + @Parameter(optionality = Optionality.OPTIONAL) + @ParameterLayout(named="Cost") + final BigDecimal cost) { + return toDoItems.newToDo(description, category, subcategory, dueBy, cost); + } + public String default0Duplicate() { + return getDescription() + " - Copy"; + } + public Category default1Duplicate() { + return getCategory(); + } + public Subcategory default2Duplicate() { + return getSubcategory(); + } + public LocalDate default3Duplicate() { + return getDueBy(); + } + public List<Subcategory> choices2Duplicate( + final String description, final Category category) { + return toDoItems.choices2NewToDo(description, category); + } + public String validateDuplicate( + final String description, + final Category category, final Subcategory subcategory, + final LocalDate dueBy, final BigDecimal cost) { + return toDoItems.validateNewToDo(description, category, subcategory, dueBy, cost); + } + //endregion + + //region > delete (action) + @Action( + domainEvent =DeletedEvent.class, + invokeOn = InvokeOn.OBJECT_AND_COLLECTION + ) + public List<ToDoItem> delete() { + + container.removeIfNotAlready(this); + + container.informUser("Deleted " + container.titleOf(this)); + + // invalid to return 'this' (cannot render a deleted object) + return toDoItems.notYetComplete(); + } + //endregion + + //region > totalCost (property) + @Action( + semantics = SemanticsOf.SAFE, + invokeOn = InvokeOn.COLLECTION_ONLY + ) + public BigDecimal totalCost() { + BigDecimal total = (BigDecimal) scratchpad.get("runningTotal"); + if(getCost() != null) { + total = total != null ? total.add(getCost()) : getCost(); + scratchpad.put("runningTotal", total); + } + return total.setScale(2, BigDecimal.ROUND_HALF_EVEN); + } + //endregion + + //region > openSourceCodeOnGithub (action) + @Action( + semantics = SemanticsOf.SAFE, + restrictTo = RestrictTo.PROTOTYPING + ) + public URL openSourceCodeOnGithub() throws MalformedURLException { + return new URL("https://github.com/apache/isis/tree/master/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.java"); + } + //endregion + + //region > demoException (action) + + static enum DemoExceptionType { + RecoverableException, + RecoverableExceptionAutoEscalated, + NonRecoverableException; + } + + @Action( + semantics = SemanticsOf.SAFE, + restrictTo = RestrictTo.PROTOTYPING + ) + public void demoException( + @ParameterLayout(named="Type") + final DemoExceptionType type) { + switch(type) { + case NonRecoverableException: + throw new NonRecoverableException("Demo throwing " + type.name()); + case RecoverableException: + throw new RecoverableException("Demo throwing " + type.name()); + case RecoverableExceptionAutoEscalated: + try { + // this will trigger an exception (because category cannot be null), causing the xactn to be aborted + setCategory(null); + container.flush(); + } catch(Exception e) { + // it's a programming mistake to throw only a recoverable exception here, because of the xactn's state. + // the framework should instead auto-escalate this to a non-recoverable exception + throw new RecoverableException("Demo throwing " + type.name(), e); + } + } + } + //endregion + + //region > lifecycle callbacks + + public void created() { + LOG.debug("lifecycle callback: created: " + this.toString()); + } + public void loaded() { + LOG.debug("lifecycle callback: loaded: " + this.toString()); + } + public void persisting() { + LOG.debug("lifecycle callback: persisting: " + this.toString()); + } + public void persisted() { + LOG.debug("lifecycle callback: persisted: " + this.toString()); + } + public void updating() { + LOG.debug("lifecycle callback: updating: " + this.toString()); + } + public void updated() { + LOG.debug("lifecycle callback: updated: " + this.toString()); + } + public void removing() { + LOG.debug("lifecycle callback: removing: " + this.toString()); + } + public void removed() { + LOG.debug("lifecycle callback: removed: " + this.toString()); + } + //endregion + + //region > object-level validation + + /** + * Prevent user from viewing another user's data. + */ + public boolean hidden() { + // uncomment to enable. As things stand, the disabled() method below instead will make object "read-only". + //return !Objects.equal(getOwnedBy(), container.getUser().getName()); + return false; + } + + /** + * Prevent user from modifying any other user's data. + */ + public String disabled(final Identifier.Type identifierType){ + final UserMemento currentUser = container.getUser(); + final String currentUserName = currentUser.getName(); + if(Objects.equal(getOwnedBy(), currentUserName)) { return null; } + return "This object is owned by '" + getOwnedBy() + "' and cannot be modified by you"; + } + + /** + * In a real app, if this were actually a rule, then we'd expect that + * invoking the {@link #completed() done} action would clear the {@link #getDueBy() dueBy} + * property (rather than require the user to have to clear manually). + */ + public String validate() { + if(isComplete() && getDueBy() != null) { + return "Due by date must be set to null if item has been completed"; + } + return null; + } + + + //endregion + + + //region > programmatic helpers + @Programmatic // excluded from the framework's metamodel + public boolean isDue() { + if (getDueBy() == null) { + return false; + } + return !toDoItems.isMoreThanOneWeekInPast(getDueBy()); + } + //endregion + + //region > events + + public static abstract class AbstractActionDomainEvent extends ActionDomainEvent<ToDoItem> { + private static final long serialVersionUID = 1L; + private final String description; + public AbstractActionDomainEvent( + final String description, + final ToDoItem source, + final Identifier identifier, + final Object... arguments) { + super(source, identifier, arguments); + this.description = description; + } + public String getEventDescription() { + return description; + } + } + + public static class CompletedEvent extends AbstractActionDomainEvent { + private static final long serialVersionUID = 1L; + public CompletedEvent( + final ToDoItem source, + final Identifier identifier, + final Object... arguments) { + super("completed", source, identifier, arguments); + } + } + + public static class NoLongerCompletedEvent extends AbstractActionDomainEvent { + private static final long serialVersionUID = 1L; + public NoLongerCompletedEvent( + final ToDoItem source, + final Identifier identifier, + final Object... arguments) { + super("no longer completed", source, identifier, arguments); + } + } + + public static class DeletedEvent extends AbstractActionDomainEvent { + private static final long serialVersionUID = 1L; + public DeletedEvent( + final ToDoItem source, + final Identifier identifier, + final Object... arguments) { + super("deleted", source, identifier, arguments); + } + } + + //endregion + + //region > predicates + + public static class Predicates { + + public static Predicate<ToDoItem> thoseOwnedBy(final String currentUser) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(final ToDoItem toDoItem) { + return Objects.equal(toDoItem.getOwnedBy(), currentUser); + } + }; + } + + public static Predicate<ToDoItem> thoseCompleted( + final boolean completed) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(final ToDoItem t) { + return Objects.equal(t.isComplete(), completed); + } + }; + } + + public static Predicate<ToDoItem> thoseWithSimilarDescription(final String description) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(final ToDoItem t) { + return t.getDescription().contains(description); + } + }; + } + + @SuppressWarnings("unchecked") + public static Predicate<ToDoItem> thoseSimilarTo(final ToDoItem toDoItem) { + return com.google.common.base.Predicates.and( + thoseNot(toDoItem), + thoseOwnedBy(toDoItem.getOwnedBy()), + thoseCategorised(toDoItem.getCategory())); + } + + public static Predicate<ToDoItem> thoseNot(final ToDoItem toDoItem) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(final ToDoItem t) { + return t != toDoItem; + } + }; + } + + public static Predicate<ToDoItem> thoseCategorised(final Category category) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(final ToDoItem toDoItem) { + return Objects.equal(toDoItem.getCategory(), category); + } + }; + } + + public static Predicate<ToDoItem> thoseSubcategorised( + final Subcategory subcategory) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(final ToDoItem t) { + return Objects.equal(t.getSubcategory(), subcategory); + } + }; + } + + public static Predicate<ToDoItem> thoseCategorised( + final Category category, final Subcategory subcategory) { + return com.google.common.base.Predicates.and( + thoseCategorised(category), + thoseSubcategorised(subcategory)); + } + + } + + //endregion + + //region > toString, compareTo + @Override + public String toString() { + return ObjectContracts.toString(this, "description,complete,dueBy,ownedBy"); + } + + /** + * Required so can store in {@link SortedSet sorted set}s (eg {@link #getDependencies()}). + */ + @Override + public int compareTo(final ToDoItem other) { + return ObjectContracts.compare(this, other, "complete,dueBy,description"); + } + //endregion + + //region > injected services + @javax.inject.Inject + DomainObjectContainer container; + + @javax.inject.Inject + ToDoItems toDoItems; + + @javax.inject.Inject + Scratchpad scratchpad; + + /** + * public only so can be injected from integ tests + */ + @javax.inject.Inject + public ActionInvocationContext actionInvocationContext; + + /** + * public only so can be injected from integ tests + */ + @javax.inject.Inject + public EventBusService eventBusService; + + @javax.inject.Inject + WrapperFactory wrapperFactory; + + //endregion + +}
http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.layout.json ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.layout.json b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.layout.json new file mode 100644 index 0000000..cda5fdc --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.layout.json @@ -0,0 +1,222 @@ +/** + * 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. + */ +{ + "columns": [ + { + "span": 6, + "memberGroups": { + "General": { + "members": { + "description": { + "propertyLayout": { + "cssClass": "x-key", + "labelPosition": "TOP" + } + }, + "category": { + "propertyLayout": { + "labelPosition": "TOP" + } + }, + "subcategory": { + "propertyLayout": { + "labelPosition": "NONE" + }, + "actions": { + "updateCategory": { + "actionLayout": { + "named": "Update" + } + }, + "analyseCategory": { + "actionLayout": { + "cssClass": "btn-default", + "cssClassFa": "fa fa-pie-chart" + } + } + } + }, + "ownedBy": { + "propertyLayout": { + "hidden": "EVERYWHERE" + } + }, + "complete": { + "propertyLayout": { + "named": "Whether this todo item has been <i>completed</i> or not.", + "namedEscaped": false, + "describedAs": "Whether this todo item has been completed, or not.", + "labelPosition": "LEFT" + }, + "actions": { + "completed": { + "actionLayout": { + "named": "Done", + "describedAs": "Update this todo item as complete", + "cssClass": "btn-success", + "cssClassFa": "fa fa-thumbs-up" + } + }, + "notYetCompleted": { + "actionLayout": { + "named": "Not done", + "describedAs": "Update this todo item as not yet complete", + "cssClass": "btn-info", + "cssClassFa": "fa fa-thumbs-down" + } + }, + "scheduleExplicitly": { + }, + "scheduleImplicitly": { + } + } + } + } + }, + "Misc": { + "members": { + "versionSequence": { + "propertyLayout": { + "named": "Version", + "hidden": "ALL_TABLES" + }, + "disabled": {} + } + } + } + } + }, + { + "span": 6, + "memberGroups": { + "Priority": { + "members": { + "relativePriority": { + "actions": { + "previous": { + "actionLayout": { + "cssClassFa": "fa fa-step-backward" + } + }, + "next": { + "actionLayout": { + "cssClassFa": "fa fa-step-forward", + "cssClassFaPosition": "right" + } + } + } + }, + "dueBy": { + "propertyLayout": { + "renderedAsDayBefore": "false" + } + } + } + }, + "Other": { + "members": { + "cost": { + "actions": { + "updateCost":{ + "actionLayout": { + "named": "Update", + "cssClassFa": "fa fa-dollar" + } + } + } + }, + "notes": { + "propertyLayout": { + "hidden": "ALL_TABLES", + "labelPosition": "TOP", + "multiLine": 5 + } + }, + "attachment": { + "propertyLayout": { + "hidden": "STANDALONE_TABLES" + } + }, + "doc": { + "propertyLayout": { + "hidden": "STANDALONE_TABLES" + } + } + } + } + } + }, + { + "span": 0 + }, + { + "span": 6, + "collections": { + "dependencies": { + "collectionLayout": { + "named": "Todo items that are <i>dependencies</i> of this item.", + "namedEscaped": false, + "describedAs": "Todo items that must be completed before this one", + "paged": 5, + "render": "EAGERLY" + }, + "disabled": {}, + "actions": { + "add":{ + "actionLayout": { + "cssClass": "btn-default", + "cssClassFa": "fa fa-plus-square" + } + }, + "remove": { + "actionLayout": { + "cssClass": "x-caution btn-default", + "cssClassFa": "fa fa-minus-square" + } + } + } + }, + "similarTo": { + "collectionLayout": { + "named": "Todo items that are <i>similar to</i> this one", + "namedEscaped": false, + "paged": 3, + "render": "LAZILY" + }, + "disabled": {} + } + } + } + ], + "actions": { + "totalCost": {}, + "delete": { + "actionLayout": { + "cssClass": "btn btn-danger", + "cssClassFa": "fa fa-trash fa-lg" + } + }, + "duplicate": { + "actionLayout": { + "named": "Clone", + "describedAs": "Create a new todo item from this one", + "cssClass": "btn-warn", + "cssClassFa": "fa fa-copy fa-lg" + } + } + } +} http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.png ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.png b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.png new file mode 100644 index 0000000..99a9fed Binary files /dev/null and b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItem.png differ http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java new file mode 100644 index 0000000..32b2aa4 --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemChangedPayloadFactory.java @@ -0,0 +1,48 @@ +/* + * 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 dom.todo; + +import org.apache.isis.applib.annotation.PublishingChangeKind; +import org.apache.isis.applib.annotation.PublishingPayloadFactoryForObject; +import org.apache.isis.applib.services.publish.EventPayload; +import org.apache.isis.applib.services.publish.EventPayloadForObjectChanged; + +public class ToDoItemChangedPayloadFactory implements PublishingPayloadFactoryForObject { + + public static class ToDoItemPayload extends EventPayloadForObjectChanged<ToDoItem> { + + public ToDoItemPayload(ToDoItem changed) { + super(changed); + } + + /** + * Expose the item's {@link ToDoItem#getDescription() description} more explicitly + * in the payload. + */ + public String getDescription() { + return getChanged().getDescription(); + } + } + + @Override + public EventPayload payloadFor(final Object changedObject, final PublishingChangeKind publishingChangeKind) { + return new ToDoItemPayload((ToDoItem) changedObject); + } + +} http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemContributions.java ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemContributions.java b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemContributions.java new file mode 100644 index 0000000..b35135c --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemContributions.java @@ -0,0 +1,276 @@ +/* + * 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 dom.todo; + +import dom.todo.ToDoItem.Category; +import dom.todo.ToDoItem.Subcategory; + +import java.util.List; +import java.util.concurrent.Callable; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import org.joda.time.LocalDate; +import org.apache.isis.applib.AbstractFactoryAndRepository; +import org.apache.isis.applib.annotation.Action; +import org.apache.isis.applib.annotation.ActionLayout; +import org.apache.isis.applib.annotation.Contributed; +import org.apache.isis.applib.annotation.DomainService; +import org.apache.isis.applib.annotation.Editing; +import org.apache.isis.applib.annotation.NatureOfService; +import org.apache.isis.applib.annotation.Optionality; +import org.apache.isis.applib.annotation.Parameter; +import org.apache.isis.applib.annotation.ParameterLayout; +import org.apache.isis.applib.annotation.Property; +import org.apache.isis.applib.annotation.SemanticsOf; +import org.apache.isis.applib.annotation.Where; +import org.apache.isis.applib.query.QueryDefault; +import org.apache.isis.applib.services.queryresultscache.QueryResultsCache; +import org.apache.isis.applib.value.Clob; + +@DomainService(nature = NatureOfService.VIEW_CONTRIBUTIONS_ONLY) +public class ToDoItemContributions extends AbstractFactoryAndRepository { + + //region > priority (contributed property) + @Action( + semantics = SemanticsOf.SAFE, + hidden = Where.ALL_TABLES + ) + @ActionLayout( + describedAs = "The relative priority of this item compared to others not yet complete (using 'due by' date)", + contributed = Contributed.AS_ASSOCIATION + ) + @Property( + editing = Editing.DISABLED, + editingDisabledReason = "Relative priority, derived from due date" + ) + public Integer relativePriority(final ToDoItem toDoItem) { + return queryResultsCache.execute(new Callable<Integer>(){ + @Override + public Integer call() throws Exception { + if(toDoItem.isComplete()) { + return null; + } + + // sort items, then locate this one + int i=1; + for (final ToDoItem each : sortedNotYetComplete()) { + if(each == toDoItem) { + return i; + } + i++; + } + return null; + }}, ToDoItemContributions.class, "relativePriority", toDoItem); + } + + private List<ToDoItem> sortedNotYetComplete() { + return ORDERING_DUE_BY + .compound(ORDERING_DESCRIPTION) + .sortedCopy(toDoItems.notYetComplete()); + } + + private static final Ordering<ToDoItem> ORDERING_DUE_BY = + Ordering.natural().nullsLast().onResultOf(new Function<ToDoItem, LocalDate>(){ + @Override + public LocalDate apply(final ToDoItem input) { + return input.getDueBy(); + } + }); + + private static final Ordering<ToDoItem> ORDERING_DESCRIPTION = + Ordering.natural().nullsLast().onResultOf(new Function<ToDoItem, String>(){ + @Override + public String apply(final ToDoItem input) { + return input.getDescription(); + } + }); + + + //endregion + + //region > next, previous (contributed actions) + @Action(semantics = SemanticsOf.SAFE) + @ActionLayout( + describedAs = "The next item not yet completed", + contributed = Contributed.AS_ACTION + ) + public ToDoItem next(final ToDoItem item) { + final Integer priority = relativePriority(item); + if(priority == null) { + return item; + } + int priorityOfNext = priority != null ? priority + 1 : 0; + return itemWithPriorityElse(priorityOfNext, item); + } + public String disableNext(final ToDoItem toDoItem) { + if (toDoItem.isComplete()) { + return "Completed"; + } + if(next(toDoItem) == null) { + return "No next item"; + } + return null; + } + + // ////////////////////////////////////// + + @ActionLayout( + describedAs = "The previous item not yet completed", + contributed = Contributed.AS_ACTION + ) + @Action(semantics = SemanticsOf.SAFE) + public ToDoItem previous(final ToDoItem item) { + final Integer priority = relativePriority(item); + if(priority == null) { + return item; + } + int priorityOfPrevious = priority != null? priority - 1 : 0; + return itemWithPriorityElse(priorityOfPrevious, item); + } + public String disablePrevious(final ToDoItem toDoItem) { + if (toDoItem.isComplete()) { + return "Completed"; + } + if(previous(toDoItem) == null) { + return "No previous item"; + } + return null; + } + + // ////////////////////////////////////// + + /** + * @param priority : 1-based priority + */ + private ToDoItem itemWithPriorityElse(int priority, final ToDoItem itemElse) { + if(priority < 1) { + return null; + } + final List<ToDoItem> items = sortedNotYetComplete(); + if(priority > items.size()) { + return null; + } + return priority>=0 && items.size()>=priority? items.get(priority-1): itemElse; + } + //endregion + + //region > similarTo (contributed collection) + @ActionLayout( + contributed = Contributed.AS_ASSOCIATION + ) + @Action(semantics = SemanticsOf.SAFE) + public List<ToDoItem> similarTo(final ToDoItem toDoItem) { + final List<ToDoItem> similarToDoItems = allMatches( + new QueryDefault<ToDoItem>(ToDoItem.class, + "findByOwnedByAndCategory", + "ownedBy", currentUserName(), + "category", toDoItem.getCategory())); + return Lists.newArrayList(Iterables.filter(similarToDoItems, excluding(toDoItem))); + } + + + private static Predicate<ToDoItem> excluding(final ToDoItem toDoItem) { + return new Predicate<ToDoItem>() { + @Override + public boolean apply(ToDoItem input) { + return input != toDoItem; + } + }; + } + //endregion + + //region > updateCategory (contributed action) + + @ActionLayout( + describedAs = "Update category and subcategory" + ) + @Action(semantics = SemanticsOf.IDEMPOTENT) + public Categorized updateCategory( + final Categorized item, + final @ParameterLayout(named="Category") Category category, + final @Parameter(optionality = Optionality.OPTIONAL) @ParameterLayout(named="Subcategory") Subcategory subcategory) { + item.setCategory(category); + item.setSubcategory(subcategory); + return item; + } + public Category default1UpdateCategory( + final Categorized item) { + return item != null? item.getCategory(): null; + } + public Subcategory default2UpdateCategory( + final Categorized item) { + return item != null? item.getSubcategory(): null; + } + + public List<Subcategory> choices2UpdateCategory( + final Categorized item, final Category category) { + return Subcategory.listFor(category); + } + + public String validateUpdateCategory( + final Categorized item, final Category category, final Subcategory subcategory) { + return Subcategory.validate(category, subcategory); + } + //endregion + + // region > exportAsJson (action) + /** + * Demonstrates functionality of streaming back Clob/Blob result within an action with a prompt, i.e. Ajax request + */ + @Action(semantics = SemanticsOf.SAFE) + public Clob exportAsJson( + final ToDoItem toDoItem, + @ParameterLayout(named = "File name") String fileName + ) { + if(!fileName.endsWith(".json")) { + fileName += ".json"; + } + return new Clob( + fileName, + "application/json", + "{" + + "\"description\": \"" + toDoItem.getDescription()+"\"" + + ",\"complete\": " + ""+toDoItem.isComplete() + + "}"); + } + + public String default1ExportAsJson() { + return "todo"; + } + //endregion + + + //region > helpers + protected String currentUserName() { + return getContainer().getUser().getName(); + } + //endregion + + //region > injected services + @javax.inject.Inject + private ToDoItems toDoItems; + + @javax.inject.Inject + private QueryResultsCache queryResultsCache; + //endregion + +} http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java new file mode 100644 index 0000000..91b0ee8 --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItemSubscriptions.java @@ -0,0 +1,399 @@ +/* + * 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 dom.todo; + +import java.util.EventObject; +import java.util.List; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Lists; +import com.google.common.eventbus.Subscribe; +import org.apache.isis.applib.DomainObjectContainer; +import org.apache.isis.applib.NonRecoverableException; +import org.apache.isis.applib.RecoverableException; +import org.apache.isis.applib.annotation.Action; +import org.apache.isis.applib.annotation.ActionLayout; +import org.apache.isis.applib.annotation.DomainService; +import org.apache.isis.applib.annotation.DomainServiceLayout; +import org.apache.isis.applib.annotation.MemberOrder; +import org.apache.isis.applib.annotation.NatureOfService; +import org.apache.isis.applib.annotation.ParameterLayout; +import org.apache.isis.applib.annotation.Programmatic; +import org.apache.isis.applib.annotation.RestrictTo; +import org.apache.isis.applib.annotation.SemanticsOf; +import org.apache.isis.applib.services.eventbus.ActionDomainEvent; +import org.apache.isis.applib.services.eventbus.CollectionDomainEvent; +import org.apache.isis.applib.services.eventbus.EventBusService; +import org.apache.isis.applib.services.eventbus.PropertyDomainEvent; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Lists.newArrayList; + +/** + * Subscribes to changes made to the {@link dom.todo.ToDoItem} entity. + * + * <p> + * (For demo purposes) the behaviour can be influenced using {@link #subscriberBehaviour(dom.todo.ToDoItemSubscriptions.Behaviour)}. + * In particular, the subscriber can be used to hide/disable/validate actions, or just to perform pre- or post-execute + * tasks. This also includes being set to throw an exception during the execution of the action (also in effect + * vetoing the change). + * </p> + */ +@DomainService(nature = NatureOfService.VIEW_MENU_ONLY) +@DomainServiceLayout(menuBar = DomainServiceLayout.MenuBar.SECONDARY, menuOrder = "30") +public class ToDoItemSubscriptions { + + //region > LOG + private final static org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(ToDoItemSubscriptions.class); + //endregion + + //region > postConstruct, preDestroy + + /** + * Registers this service with the {@link org.apache.isis.applib.services.eventbus.EventBusService}. + * + * <p> + * Because this service is a singleton, this is called during initial bootstrap. + * </p> + */ + @Programmatic + @PostConstruct + public void postConstruct() { + LOG.info("postConstruct: registering to event bus"); + eventBusService.register(this); + } + + /** + * Unregisters this service from the {@link org.apache.isis.applib.services.eventbus.EventBusService}. + * + * <p> + * Because this service is a singleton, this is only done when the system is shutdown. + * </p> + */ + @Programmatic + @PreDestroy + public void preDestroy() { + LOG.info("preDestroy: unregistering from event bus"); + eventBusService.unregister(this); + } + //endregion + + + //region > on(Event)... + + public static enum Behaviour { + AnyExecuteAccept, + AnyExecuteVetoWithRecoverableException, + AnyExecuteVetoWithNonRecoverableException, + AnyExecuteVetoWithOtherException, + UpdateCostActionHide, + UpdateCostActionDisable, + UpdateCostActionInvalidate, + DescriptionPropertyHide, + DescriptionPropertyDisable, + DescriptionPropertyInvalidate, + DependenciesCollectionHide, + // not implemented in Wicket viewer, but supported in wrapped objects + DependenciesCollectionDisable, + // not implemented in Wicket viewer, but supported in wrapped objects + DependenciesCollectionInvalidateAdd, + DependenciesCollectionInvalidateRemove, + SimilarToCollectionHide + } + + /** + * The desired behaviour of this service. + */ + private Behaviour behaviour = Behaviour.AnyExecuteAccept; + + /** + * To demo/test what occurs if a subscriber that might veto an event. + */ + @MemberOrder(name = "Prototyping", sequence = "80") + @ActionLayout( + named="Set subscriber behaviour" + ) + @Action( + semantics = SemanticsOf.IDEMPOTENT, + restrictTo = RestrictTo.PROTOTYPING + ) + public void subscriberBehaviour( + @ParameterLayout( + named="Behaviour" + ) + final Behaviour behaviour) { + this.behaviour = behaviour; + container.informUser("Subscriber behaviour set to: " + behaviour); + } + public Behaviour default0SubscriberBehaviour() { + return this.behaviour; + } + + @Programmatic + public Behaviour getSubscriberBehaviour() { + return behaviour; + } + + + private void onExecutedThrowExceptionIfSet(final ActionDomainEvent<?> ev) { + if(ev != null && ev.getSemantics().isSafe()) { + return; + } + onExecutedThrowExceptionIfSet(); + } + private void onExecutedThrowExceptionIfSet(final PropertyDomainEvent<?, ?> ev) { + onExecutedThrowExceptionIfSet(); + } + private void onExecutedThrowExceptionIfSet(final CollectionDomainEvent<?, ?> ev) { + onExecutedThrowExceptionIfSet(); + } + + + private void onExecutedThrowExceptionIfSet() { + if(behaviour == Behaviour.AnyExecuteVetoWithRecoverableException) { + throw new RecoverableException("Rejecting event (recoverable exception thrown)"); + } + if(behaviour == Behaviour.AnyExecuteVetoWithNonRecoverableException) { + throw new NonRecoverableException("Rejecting event (non-recoverable exception thrown)"); + } + if(behaviour == Behaviour.AnyExecuteVetoWithOtherException) { + throw new RuntimeException("Throwing some other exception"); + } + } + //endregion + + //region > on(Event) for ToDoItem-specific events + @Programmatic + @Subscribe + public void on(final ToDoItem.CompletedEvent ev) { + recordEvent(ev); + switch(ev.getEventPhase()) { + case HIDE: + break; + case DISABLE: + break; + case VALIDATE: + break; + case EXECUTING: + break; + case EXECUTED: + LOG.info("Received ToDoItem.CompletedEvent for : " + ev.getSource().toString()); + break; + } + } + //endregion + + //region > on(Event) ... general purpose + + @Programmatic + @Subscribe + public void on(final ActionDomainEvent<?> ev) { + recordEvent(ev); + switch(ev.getEventPhase()) { + case HIDE: + if(getSubscriberBehaviour() == Behaviour.UpdateCostActionHide) { + if(ev.getIdentifier().getMemberName().equals("updateCost")) { + ev.hide(); + } + } + break; + case DISABLE: + if(getSubscriberBehaviour() == Behaviour.UpdateCostActionDisable) { + if(ev.getIdentifier().getMemberName().equals("updateCost")) { + ev.disable("ToDoItemSubscriptions says: updateCost action disabled!"); + } + } + break; + case VALIDATE: + if(getSubscriberBehaviour() == Behaviour.UpdateCostActionInvalidate && + ev.getIdentifier().getMemberName().equals("updateCost")) { + ev.invalidate("ToDoItemSubscriptions says: can't invoke updateCost action with these args!"); + } + break; + case EXECUTING: + break; + case EXECUTED: + LOG.info("Received ActionDomainEvent, " + ev.getSource().toString() + ", invoked " + ev.getIdentifier().getMemberName()); + onExecutedThrowExceptionIfSet(ev); + break; + } + } + + @Programmatic + @Subscribe + public void on(PropertyDomainEvent<?,?> ev) { + recordEvent(ev); + switch(ev.getEventPhase()) { + case HIDE: + if(getSubscriberBehaviour() == Behaviour.DescriptionPropertyHide && + ev.getIdentifier().getMemberName().equals("description")) { + ev.veto(""); + } + break; + case DISABLE: + if(getSubscriberBehaviour() == Behaviour.DescriptionPropertyDisable && + ev.getIdentifier().getMemberName().equals("description")) { + ev.veto("ToDoItemSubscriptions says: description property disabled!"); + } + break; + case VALIDATE: + if(getSubscriberBehaviour() == Behaviour.DescriptionPropertyInvalidate && + ev.getIdentifier().getMemberName().equals("description")) { + ev.veto("ToDoItemSubscriptions says: can't change description property to this value!"); + } + break; + case EXECUTING: + break; + case EXECUTED: + LOG.info("Received PropertyDomainEvent, " + ev.getSource().toString() + ", changed " + ev.getIdentifier().getMemberName() + " : " + ev.getOldValue() + " -> " + ev.getNewValue()); + onExecutedThrowExceptionIfSet(ev); + + if(ev.getIdentifier().getMemberName().contains("description")) { + String newValue = (String) ev.getNewValue(); + if(newValue.matches(".*demo veto.*")) { + throw new RecoverableException("oh no you don't! " + ev.getNewValue()); + } + } + break; + } + } + + @Programmatic + @Subscribe + public void on(CollectionDomainEvent<?,?> ev) { + recordEvent(ev); + switch (ev.getEventPhase()) { + case HIDE: + if(getSubscriberBehaviour() == Behaviour.DependenciesCollectionHide && + ev.getIdentifier().getMemberName().equals("dependencies")) { + ev.veto(""); + } + if (getSubscriberBehaviour() == Behaviour.SimilarToCollectionHide && + ev.getIdentifier().getMemberName().equals("similarTo")) { + ev.veto(""); + } + break; + case DISABLE: + if (getSubscriberBehaviour() == Behaviour.DependenciesCollectionDisable && + ev.getIdentifier().getMemberName().equals("dependencies")) { + ev.veto("ToDoItemSubscriptions says: dependencies collection disabled!"); + } + break; + case VALIDATE: + if(getSubscriberBehaviour() == Behaviour.DependenciesCollectionInvalidateAdd && + ev.getIdentifier().getMemberName().equals("dependencies") && + ev.getOf() == CollectionDomainEvent.Of.ADD_TO ) { + ev.veto("ToDoItemSubscriptions says: can't add this object to dependencies collection!"); + } + if(getSubscriberBehaviour() == Behaviour.DependenciesCollectionInvalidateRemove && + ev.getIdentifier().getMemberName().equals("dependencies") && + ev.getOf() == CollectionDomainEvent.Of.REMOVE_FROM ) { + ev.veto("ToDoItemSubscriptions says: can't remove this object from dependencies collection!"); + } + break; + case EXECUTING: + break; + case EXECUTED: + if(ev.getOf() == CollectionDomainEvent.Of.ADD_TO) { + LOG.info("Received CollectionDomainEvent, " + ev.getSource().toString() + ", added to " + ev.getIdentifier().getMemberName() + " : " + ev.getValue()); + } else { + LOG.info("Received CollectionDomainEvent, " + ev.getSource().toString() + ", removed from " + ev.getIdentifier().getMemberName() + " : " + ev.getValue()); + } + onExecutedThrowExceptionIfSet(ev); + break; + } + + } + + //endregion + + //region > receivedEvents + private final List<java.util.EventObject> receivedEvents = Lists.newLinkedList(); + + /** + * Used in integration tests. + */ + @Programmatic + public List<java.util.EventObject> receivedEvents() { + return receivedEvents; + } + + /** + * Used in integration tests. + */ + @Programmatic + public <T extends java.util.EventObject> List<T> receivedEvents(final Class<T> expectedType) { + return newArrayList( + transform( + filter(receivedEvents, instanceOf(expectedType)), + castTo(expectedType))); + } + + private static <T extends EventObject> Function<EventObject, T> castTo(Class<T> expectedType) { + return new Function<EventObject, T>() { + @Override + public T apply(EventObject input) { + return (T) input; + } + }; + } + + private static <T extends EventObject> Predicate<EventObject> instanceOf(final Class<T> expectedType) { + return new Predicate<EventObject>() { + @Override + public boolean apply(EventObject input) { + return expectedType.isInstance(input); + } + }; + } + + /** + * Used in integration tests. + */ + @Programmatic + public <T extends java.util.EventObject> T mostRecentlyReceivedEvent(Class<T> expectedType) { + final List<T> receivedEvents = receivedEvents(expectedType); + return !receivedEvents.isEmpty() ? receivedEvents.get(0) : null; + } + private void recordEvent(final java.util.EventObject ev) { + receivedEvents.add(0, ev); + } + /** + * Used in integration tests. + */ + @Programmatic + public void reset() { + receivedEvents.clear(); + subscriberBehaviour(ToDoItemSubscriptions.Behaviour.AnyExecuteAccept); + } + //endregion + + //region > injected services + @javax.inject.Inject + private DomainObjectContainer container; + + @javax.inject.Inject + private EventBusService eventBusService; + //endregion + + +} http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItems.java ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItems.java b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItems.java new file mode 100644 index 0000000..da392a5 --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/main/java/dom/todo/ToDoItems.java @@ -0,0 +1,257 @@ +/* + * 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 dom.todo; + +import dom.todo.ToDoItem.Category; +import dom.todo.ToDoItem.Subcategory; + +import java.math.BigDecimal; +import java.util.List; +import com.google.common.base.Predicates; +import org.joda.time.LocalDate; +import org.apache.isis.applib.DomainObjectContainer; +import org.apache.isis.applib.annotation.Action; +import org.apache.isis.applib.annotation.ActionLayout; +import org.apache.isis.applib.annotation.BookmarkPolicy; +import org.apache.isis.applib.annotation.DomainService; +import org.apache.isis.applib.annotation.DomainServiceLayout; +import org.apache.isis.applib.annotation.RestrictTo; +import org.apache.isis.applib.annotation.MemberOrder; +import org.apache.isis.applib.annotation.Optionality; +import org.apache.isis.applib.annotation.Parameter; +import org.apache.isis.applib.annotation.ParameterLayout; +import org.apache.isis.applib.annotation.Programmatic; +import org.apache.isis.applib.annotation.SemanticsOf; +import org.apache.isis.applib.query.QueryDefault; +import org.apache.isis.applib.services.clock.ClockService; + +@DomainServiceLayout(named="ToDos", menuOrder = "10") +@DomainService(repositoryFor = ToDoItem.class) +public class ToDoItems { + + //region > notYetComplete (action) + @Action(semantics = SemanticsOf.SAFE) + @ActionLayout( + cssClassFa = "fa fa-thumbs-down", + bookmarking = BookmarkPolicy.AS_ROOT + ) + @MemberOrder(sequence = "10") + public List<ToDoItem> notYetComplete() { + final List<ToDoItem> items = notYetCompleteNoUi(); + if(items.isEmpty()) { + container.informUser("All to-do items have been completed :-)"); + } + return items; + } + + @Programmatic + public List<ToDoItem> notYetCompleteNoUi() { + return container.allMatches( + new QueryDefault<>(ToDoItem.class, + "findByOwnedByAndCompleteIsFalse", + "ownedBy", currentUserName())); + } + //endregion + + //region > complete (action) + @ActionLayout( + cssClassFa = "fa fa-thumbs-up" + ) + @Action(semantics = SemanticsOf.SAFE) + @MemberOrder(sequence = "20") + public List<ToDoItem> complete() { + final List<ToDoItem> items = completeNoUi(); + if(items.isEmpty()) { + container.informUser("No to-do items have yet been completed :-("); + } + return items; + } + + @Programmatic + public List<ToDoItem> completeNoUi() { + return container.allMatches( + new QueryDefault<>(ToDoItem.class, + "findByOwnedByAndCompleteIsTrue", + "ownedBy", currentUserName())); + } + //endregion + + //region > categorized (action) + @SuppressWarnings("unchecked") + @Action(semantics = SemanticsOf.SAFE) + @ActionLayout( + cssClassFa = "fa fa-question", + bookmarking = BookmarkPolicy.AS_ROOT + ) + @MemberOrder(sequence = "40") + public List<ToDoItem> categorized( + @ParameterLayout(named="Category") final Category category, + @ParameterLayout(named="Subcategory") final Subcategory subcategory, + @ParameterLayout(named="Completed?") final boolean completed) { + // an example "naive" implementation (filtered in Java code, not DBMS) + return container.allMatches(ToDoItem.class, + Predicates.and( + ToDoItem.Predicates.thoseOwnedBy(currentUserName()), + ToDoItem.Predicates.thoseCompleted(completed), + ToDoItem.Predicates.thoseCategorised(category, subcategory))); + } + public Category default0Categorized() { + return Category.Professional; + } + public Subcategory default1Categorized() { + return default0Categorized().subcategories().get(0); + } + public boolean default2Categorized() { + return false; + } + public List<Subcategory> choices1Categorized( + final Category category) { + return Subcategory.listFor(category); + } + public String validateCategorized( + final Category category, + final Subcategory subcategory, + final boolean completed) { + return Subcategory.validate(category, subcategory); + } + //endregion + + //region > newToDo (action) + @ActionLayout(cssClassFa = "fa fa-plus") + @MemberOrder(sequence = "5") + public ToDoItem newToDo( + @Parameter(regexPattern = "\\w[@&:\\-\\,\\.\\+ \\w]*") + @ParameterLayout(named="Description") + final String description, + @ParameterLayout(named="Category") + final Category category, + @Parameter(optionality = Optionality.OPTIONAL) + @ParameterLayout(named="Subcategory") + final Subcategory subcategory, + @Parameter(optionality = Optionality.OPTIONAL) + @ParameterLayout(named="Due by") + final LocalDate dueBy, + @Parameter(optionality = Optionality.OPTIONAL) + @ParameterLayout(named="Cost") + final BigDecimal cost) { + return newToDo(description, category, subcategory, currentUserName(), dueBy, cost); + } + public Category default1NewToDo() { + return Category.Professional; + } + public Subcategory default2NewToDo() { + return Category.Professional.subcategories().get(0); + } + public LocalDate default3NewToDo() { + return clockService.now().plusDays(14); + } + public List<Subcategory> choices2NewToDo( + final String description, final Category category) { + return Subcategory.listFor(category); + } + public String validateNewToDo( + final String description, + final Category category, final Subcategory subcategory, + final LocalDate dueBy, final BigDecimal cost) { + return Subcategory.validate(category, subcategory); + } + //endregion + + //region > allToDos (action) + @ActionLayout( + cssClassFa = "fa fa-globe" + ) + @Action( + semantics = SemanticsOf.SAFE, + restrictTo = RestrictTo.PROTOTYPING + ) + @MemberOrder(sequence = "50") + public List<ToDoItem> allToDos() { + final List<ToDoItem> items = container.allMatches( + new QueryDefault<>(ToDoItem.class, + "findByOwnedBy", + "ownedBy", currentUserName())); + if(items.isEmpty()) { + container.warnUser("No to-do items found."); + } + return items; + } + //endregion + + //region > autoComplete (programmatic) + @Programmatic // not part of metamodel + public List<ToDoItem> autoComplete(final String description) { + return container.allMatches( + new QueryDefault<>(ToDoItem.class, + "findByOwnedByAndDescriptionContains", + "ownedBy", currentUserName(), + "description", description)); + } + //endregion + + //region > helpers + @Programmatic // for use by fixtures + public ToDoItem newToDo( + final String description, + final Category category, + final Subcategory subcategory, + final String userName, + final LocalDate dueBy, final BigDecimal cost) { + final ToDoItem toDoItem = container.newTransientInstance(ToDoItem.class); + toDoItem.setDescription(description); + toDoItem.setCategory(category); + toDoItem.setSubcategory(subcategory); + toDoItem.setOwnedBy(userName); + toDoItem.setDueBy(dueBy); + toDoItem.setCost(cost); + + container.persist(toDoItem); + container.flush(); + + return toDoItem; + } + + private String currentUserName() { + return container.getUser().getName(); + } + + //endregion + + //region > common validation + private static final long ONE_WEEK_IN_MILLIS = 7 * 24 * 60 * 60 * 1000L; + + @Programmatic + public String validateDueBy(final LocalDate dueBy) { + return isMoreThanOneWeekInPast(dueBy) ? "Due by date cannot be more than one week old" : null; + } + @Programmatic + boolean isMoreThanOneWeekInPast(final LocalDate dueBy) { + return dueBy.toDateTimeAtStartOfDay().getMillis() < clockService.nowAsMillis() - ONE_WEEK_IN_MILLIS; + } + //endregion + + //region > injected services + @javax.inject.Inject + private DomainObjectContainer container; + + @javax.inject.Inject + private ClockService clockService; + //endregion + +} http://git-wip-us.apache.org/repos/asf/isis/blob/a4ec0b72/mothballed/example/application/todoapp/dom/src/test/java/dom/todo/ToDoItemTest.java ---------------------------------------------------------------------- diff --git a/mothballed/example/application/todoapp/dom/src/test/java/dom/todo/ToDoItemTest.java b/mothballed/example/application/todoapp/dom/src/test/java/dom/todo/ToDoItemTest.java new file mode 100644 index 0000000..b1f5168 --- /dev/null +++ b/mothballed/example/application/todoapp/dom/src/test/java/dom/todo/ToDoItemTest.java @@ -0,0 +1,133 @@ +/** + * 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 dom.todo; + +import org.jmock.Expectations; +import org.jmock.auto.Mock; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.apache.isis.applib.DomainObjectContainer; +import org.apache.isis.applib.services.actinvoc.ActionInvocationContext; +import org.apache.isis.applib.security.RoleMemento; +import org.apache.isis.applib.security.UserMemento; +import org.apache.isis.applib.services.eventbus.EventBusService; +import org.apache.isis.core.unittestsupport.jmocking.JUnitRuleMockery2; +import org.apache.isis.core.unittestsupport.jmocking.JUnitRuleMockery2.Mode; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +public abstract class ToDoItemTest { + + @Rule + public JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(Mode.INTERFACES_AND_CLASSES); + + @Mock + EventBusService eventBusService; + + ToDoItem toDoItem; + + @Before + public void setUp() throws Exception { + toDoItem = new ToDoItem(); + + toDoItem.actionInvocationContext = ActionInvocationContext.onObject(toDoItem); + toDoItem.eventBusService = eventBusService; + + context.ignoring(eventBusService); + } + + public static class Properties extends ToDoItemTest { + + @Mock + DomainObjectContainer mockContainer; + + public static class DueBy extends Properties { + + @Test + public void hiddenForNoDueByRole() { + final UserMemento userWithRole = new UserMemento("user", new RoleMemento("realm1:noDueBy_role")); + context.checking(new Expectations() {{ + allowing(mockContainer).getUser(); + will(returnValue(userWithRole)); + }}); + + toDoItem.container = mockContainer; + + assertThat(toDoItem.hideDueBy(), is(true)); + } + + @Test + public void notHiddenWithoutRole() { + final UserMemento userWithRole = new UserMemento("user", new RoleMemento("realm1:someOtherRole")); + context.checking(new Expectations() {{ + allowing(mockContainer).getUser(); + will(returnValue(userWithRole)); + }}); + + toDoItem.container = mockContainer; + + assertThat(toDoItem.hideDueBy(), is(false)); + } + } + + } + + public static class Actions extends ToDoItemTest { + + public static class Completed extends Actions { + + @Test + public void happyCase() throws Exception { + + // given + toDoItem.setComplete(false); + assertThat(toDoItem.disableCompleted(), is(nullValue())); + + // when + toDoItem.completed(); + + // then + assertThat(toDoItem.isComplete(), is(true)); + assertThat(toDoItem.disableCompleted(), is(not(nullValue()))); + } + } + + public static class NotYetCompleted extends Actions { + + @Test + public void happyCase() throws Exception { + + // given + toDoItem.setComplete(true); + assertThat(toDoItem.disableNotYetCompleted(), is(nullValue())); + + // when + toDoItem.notYetCompleted(); + + // then + assertThat(toDoItem.isComplete(), is(false)); + assertThat(toDoItem.disableNotYetCompleted(), is(not(nullValue()))); + } + } + } + + +}
