#set( $symbol_pound = '#' )
#set( $symbol_dollar = '$' )
#set( $symbol_escape = '\' )
/*
* 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.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.jdo.JDOHelper;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.VersionStrategy;
import javax.jdo.spi.PersistenceCapable;
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.annotation.Audited;
import org.apache.isis.applib.annotation.AutoComplete;
import org.apache.isis.applib.annotation.Bookmarkable;
import org.apache.isis.applib.annotation.Bulk;
import org.apache.isis.applib.annotation.Bulk.InteractionContext;
import org.apache.isis.applib.annotation.CssClass;
import org.apache.isis.applib.annotation.Disabled;
import org.apache.isis.applib.annotation.Hidden;
import org.apache.isis.applib.annotation.MinLength;
import org.apache.isis.applib.annotation.MultiLine;
import org.apache.isis.applib.annotation.Named;
import org.apache.isis.applib.annotation.ObjectType;
import org.apache.isis.applib.annotation.Optional;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.PublishedAction;
import org.apache.isis.applib.annotation.PublishedObject;
import org.apache.isis.applib.annotation.RegEx;
import org.apache.isis.applib.annotation.Render;
import org.apache.isis.applib.annotation.Render.Type;
import org.apache.isis.applib.annotation.SortedBy;
import org.apache.isis.applib.annotation.Where;
import org.apache.isis.applib.clock.Clock;
import org.apache.isis.applib.util.ObjectContracts;
import org.apache.isis.applib.util.TitleBuffer;
import org.apache.isis.applib.value.Blob;
import services.ClockService;
@javax.jdo.annotations.PersistenceCapable(identityType=IdentityType.DATASTORE)
@javax.jdo.annotations.DatastoreIdentity(
strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY,
column="id")
@javax.jdo.annotations.Version(
strategy=VersionStrategy.VERSION_NUMBER,
column="version")
@javax.jdo.annotations.Uniques({
@javax.jdo.annotations.Unique(
name="ToDoItem_description_must_be_unique",
members={"ownedBy","description"})
})
@javax.jdo.annotations.Queries( {
@javax.jdo.annotations.Query(
name = "todo_all", language = "JDOQL",
value = "SELECT "
+ "FROM dom.todo.ToDoItem "
+ "WHERE ownedBy == :ownedBy"),
@javax.jdo.annotations.Query(
name = "todo_notYetComplete", language = "JDOQL",
value = "SELECT "
+ "FROM dom.todo.ToDoItem "
+ "WHERE ownedBy == :ownedBy "
+ " && complete == false"),
@javax.jdo.annotations.Query(
name = "todo_complete", language = "JDOQL",
value = "SELECT "
+ "FROM dom.todo.ToDoItem "
+ "WHERE ownedBy == :ownedBy "
+ "&& complete == true"),
@javax.jdo.annotations.Query(
name = "todo_similarTo", language = "JDOQL",
value = "SELECT "
+ "FROM dom.todo.ToDoItem "
+ "WHERE ownedBy == :ownedBy "
+ "&& category == :category"),
@javax.jdo.annotations.Query(
name = "todo_autoComplete", language = "JDOQL",
value = "SELECT "
+ "FROM dom.todo.ToDoItem "
+ "WHERE ownedBy == :ownedBy && "
+ "description.indexOf(:description) >= 0")
})
@ObjectType("TODO")
@Audited
@PublishedObject(ToDoItemChangedPayloadFactory.class)
@AutoComplete(repository=ToDoItems.class, action="autoComplete") // default unless overridden by autoCompleteNXxx() method
//@Bounded - if there were a small number of instances only (overrides autoComplete functionality)
@Bookmarkable
public class ToDoItem implements Comparable<ToDoItem> /*, Locatable*/ { // GMAP3: uncomment to use https://github.com/danhaywood/isis-wicket-gmap3
/**
* It isn't common for entities to log, but they can if required.
* Isis uses the slf4j internally, and is the recommended API to use.
*/
private final static org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(ToDoItem.class);
// //////////////////////////////////////
// Identification in the UI
// //////////////////////////////////////
public String title() {
final TitleBuffer buf = new TitleBuffer();
buf.append(getDescription());
if (isComplete()) {
buf.append("- Completed!");
} else {
if (getDueBy() != null) {
buf.append(" due by", getDueBy());
}
}
return buf.toString();
}
public String iconName() {
return "ToDoItem-" + (!isComplete() ? "todo" : "done");
}
// //////////////////////////////////////
// Description (property)
// //////////////////////////////////////
private String description;
@javax.jdo.annotations.Column(allowsNull="false", length=30)
@RegEx(validation = "${symbol_escape}${symbol_escape}w[@&:${symbol_escape}${symbol_escape}-${symbol_escape}${symbol_escape},${symbol_escape}${symbol_escape}.${symbol_escape}${symbol_escape}+ ${symbol_escape}${symbol_escape}w]*")
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
// //////////////////////////////////////
// DueBy (property)
// //////////////////////////////////////
@javax.jdo.annotations.Persistent(defaultFetchGroup="true")
private LocalDate dueBy;
@javax.jdo.annotations.Column(allowsNull="true")
@CssClass("x-key")
public LocalDate getDueBy() {
return dueBy;
}
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 isMoreThanOneWeekInPast(dueBy) ? "Due by date cannot be more than one week old" : null;
}
// //////////////////////////////////////
// Category and Subcategory (property)
// //////////////////////////////////////
public static enum Category {
Professional {
@Override
public List<Subcategory> subcategories() {
return Arrays.asList(Subcategory.OpenSource, Subcategory.Consulting, Subcategory.Education);
}
}, Domestic {
@Override
public List<Subcategory> subcategories() {
return Arrays.asList(Subcategory.Shopping, Subcategory.Housework, Subcategory.Garden, Subcategory.Chores);
}
}, Other {
@Override
public List<Subcategory> subcategories() {
return Arrays.asList(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(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(Subcategory subcategory) {
return category.subcategories().contains(subcategory);
}
};
}
}
// //////////////////////////////////////
private Category category;
@javax.jdo.annotations.Column(allowsNull="false")
@Disabled(reason="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="false")
@Disabled(reason="Use action to update both category and subcategory")
public Subcategory getSubcategory() {
return subcategory;
}
public void setSubcategory(final Subcategory subcategory) {
this.subcategory = subcategory;
}
// //////////////////////////////////////
// OwnedBy (property)
// //////////////////////////////////////
private String ownedBy;
@javax.jdo.annotations.Column(allowsNull="false")
@Hidden
public String getOwnedBy() {
return ownedBy;
}
public void setOwnedBy(final String ownedBy) {
this.ownedBy = ownedBy;
}
// //////////////////////////////////////
// Complete (property),
// Done (action), Undo (action)
// //////////////////////////////////////
private boolean complete;
@Disabled
public boolean isComplete() {
return complete;
}
public void setComplete(final boolean complete) {
this.complete = complete;
}
@Named("Done")
@PublishedAction
@Bulk
@CssClass("x-highlight")
public ToDoItem completed() {
setComplete(true);
// demonstrating the use of ...
final InteractionContext ctxt = InteractionContext.current.get();
@SuppressWarnings("unused")
List<Object> allObjects = ctxt.getDomainObjects();
LOG.debug("completed: "
+ ctxt.getIndex() +
" [" + ctxt.getSize() + "]"
+ (ctxt.isFirst() ? " (first)" : "")
+ (ctxt.isLast() ? " (last)" : ""));
return this;
}
// disable action dependent on state of object
public String disableCompleted() {
return isComplete() ? "Already completed" : null;
}
@Named("Not done")
@PublishedAction
@Bulk
public ToDoItem notYetCompleted() {
setComplete(false);
return this;
}
// disable action dependent on state of object
public String disableNotYetCompleted() {
return !complete ? "Not yet completed" : null;
}
// //////////////////////////////////////
// Cost (property), UpdateCost (action)
// //////////////////////////////////////
private BigDecimal cost;
@javax.jdo.annotations.Column(allowsNull="true", scale=2)
@Disabled(reason="Update using action")
public BigDecimal getCost() {
return cost;
}
public void setCost(final BigDecimal cost) {
this.cost = cost!=null?cost.setScale(2):null;
}
@Named("Update")
public ToDoItem updateCost(@Named("New cost") @Optional final BigDecimal cost) {
LOG.debug("%s: cost updated: %s -> %s", this.container.titleOf(this), getCost(), cost);
setCost(cost);
return this;
}
// provide a default value for argument ${symbol_pound}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;
}
// //////////////////////////////////////
// Notes (property)
// //////////////////////////////////////
private String notes;
@javax.jdo.annotations.Column(allowsNull="true", length=400)
@Hidden(where=Where.ALL_TABLES)
@MultiLine(numberOfLines=5)
public String getNotes() {
return notes;
}
public void setNotes(final String notes) {
this.notes = notes;
}
// //////////////////////////////////////
// Attachment (property)
// //////////////////////////////////////
private Blob attachment;
@javax.jdo.annotations.Persistent(defaultFetchGroup="false")
@javax.jdo.annotations.Column(allowsNull="true")
@Hidden(where=Where.STANDALONE_TABLES)
public Blob getAttachment() {
return attachment;
}
public void setAttachment(final Blob attachment) {
this.attachment = attachment;
}
// //////////////////////////////////////
// Version (derived property)
// //////////////////////////////////////
@Hidden(where=Where.ALL_TABLES)
@Disabled
@Named("Version")
public Long getVersionSequence() {
if(!(this instanceof PersistenceCapable)) {
return null;
}
PersistenceCapable persistenceCapable = (PersistenceCapable) this;
final Long version = (Long) JDOHelper.getVersion(persistenceCapable);
return version;
}
// hide property (imperatively, based on state of object)
public boolean hideVersionSequence() {
return !(this instanceof PersistenceCapable);
}
// //////////////////////////////////////
// Dependencies (collection),
// Add (action), Remove (action)
// //////////////////////////////////////
// overrides the natural ordering
public static class DependenciesComparator implements Comparator<ToDoItem> {
@Override
public int compare(ToDoItem p, ToDoItem q) {
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 SortedSet<ToDoItem> dependencies = new TreeSet<ToDoItem>();
@SortedBy(DependenciesComparator.class)
@Disabled
@Render(Type.EAGERLY)
public SortedSet<ToDoItem> getDependencies() {
return dependencies;
}
public void setDependencies(final SortedSet<ToDoItem> dependencies) {
this.dependencies = dependencies;
}
@PublishedAction
public ToDoItem add(final ToDoItem toDoItem) {
getDependencies().add(toDoItem);
return this;
}
public List<ToDoItem> autoComplete0Add(final @MinLength(2) 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;
}
@CssClass("x-caution")
public ToDoItem remove(final ToDoItem toDoItem) {
getDependencies().remove(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 Collection<ToDoItem> choices0Remove() {
return getDependencies();
}
// //////////////////////////////////////
// Clone (action)
// //////////////////////////////////////
@Named("Clone")
// 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(
@Named("Description")
String description,
@Named("Category")
ToDoItem.Category category,
@Named("Subcategory")
ToDoItem.Subcategory subcategory,
@Named("Due by")
@Optional
LocalDate dueBy,
@Named("Cost")
@Optional
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();
}
// //////////////////////////////////////
// Delete (action)
// //////////////////////////////////////
@Bulk
@CssClass("x-caution")
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();
}
// //////////////////////////////////////
// Programmatic Helpers
// //////////////////////////////////////
private static final long ONE_WEEK_IN_MILLIS = 7 * 24 * 60 * 60 * 1000L;
@Programmatic // excluded from the framework's metamodel
public boolean isDue() {
if (getDueBy() == null) {
return false;
}
return !isMoreThanOneWeekInPast(getDueBy());
}
private static boolean isMoreThanOneWeekInPast(final LocalDate dueBy) {
return dueBy.toDateTimeAtStartOfDay().getMillis() < Clock.getTime() - ONE_WEEK_IN_MILLIS;
}
// //////////////////////////////////////
// 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> thoseNotYetComplete() {
return com.google.common.base.Predicates.not(thoseComplete());
}
public static Predicate<ToDoItem> thoseComplete() {
return new Predicate<ToDoItem>() {
@Override
public boolean apply(final ToDoItem t) {
return t.isComplete();
}
};
}
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));
}
}
// //////////////////////////////////////
// toString
// //////////////////////////////////////
@Override
public String toString() {
return ObjectContracts.toString(this, "description,complete,dueBy,ownedBy");
}
// //////////////////////////////////////
// compareTo
// //////////////////////////////////////
/**
* Required so can store in {@link SortedSet sorted set}s (eg {@link ${symbol_pound}getDependencies()}).
*/
@Override
public int compareTo(final ToDoItem other) {
return ObjectContracts.compare(this, other, "complete,dueBy,description");
}
// //////////////////////////////////////
// Injected Services
// //////////////////////////////////////
private DomainObjectContainer container;
public void injectDomainObjectContainer(final DomainObjectContainer container) {
this.container = container;
}
private ToDoItems toDoItems;
public void injectToDoItems(final ToDoItems toDoItems) {
this.toDoItems = toDoItems;
}
private ClockService clockService;
public void injectClockService(ClockService clockService) {
this.clockService = clockService;
}
// //////////////////////////////////////
// Extensions
// //////////////////////////////////////
// GMAP3: uncomment to use https://github.com/danhaywood/isis-wicket-gmap3
//
// @Persistent
// private Location location;
//
// @MemberOrder(name="Detail", sequence = "10")
// @Optional
// public Location getLocation() {
// return location;
// }
// public void setLocation(Location location) {
// this.location = location;
// }
}