<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like
We use a "strict" DTD to make IE follow the alignment rules. -->
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
<h3>Easy Id Select</h3>
In this example we've created a service that creates SelectModels that return id rather than an object.<br/>
It's called SelectIdModelFactory and it is almost identical to SelectModelFactory. In AppModule we add it as a service to Tapestry.
<div class="eg">
<t:form t:id="form" class="form-inline">
<div class="form-group">
Person:
<t:select model="personIdsModel" value="personId"/>
<t:submit/>
You chose personId: ${personId}
</div>
</t:form>
</div>
References:
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Select.html">Select</a>,
<a href="http://tapestry.apache.org/using-select-with-a-list.html">Using Select with a List</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/SelectModel.html">SelectModel</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/SelectModelFactory.html">SelectModelFactory</a>,
<a href="http://tapestry.apache.org/defining-tapestry-ioc-services.html">Defining Tapestry IOC Services</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/EventContext.html">EventContext</a>.<br/><br/>
<t:pagelink page="Index">Home</t:pagelink><br/><br/>
The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.<br/><br/>
<t:tabgroup>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/select/EasyIdSelect.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/select/EasyIdSelect.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/select.css"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/SelectIdModelFactory.java"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/SelectIdModelFactoryImpl.java"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
<t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/person/Person.java"/>
</t:tabgroup>
</body>
</html>
package jumpstart.web.pages.examples.select;
import java.util.List;
import javax.ejb.EJB;
import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.web.services.SelectIdModelFactory;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.SelectModel;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
@Import(stylesheet = "css/examples/select.css")
public class EasyIdSelect {
static private final int MAX_RESULTS = 30;
// The activation context
@Property
private Long personId;
// Screen fields
@Property
private SelectModel personIdsModel;
// Generally useful bits and pieces
@Inject
private SelectIdModelFactory selectIdModelFactory;
@EJB
private IPersonFinderServiceLocal personFinderService;
// The code
void onActivate(EventContext context) {
if (context.getCount() > 0) {
personId = context.get(Long.class, 0);
}
}
Long onPassivate() {
return personId;
}
void onPrepare() {
// Get all persons - ask business service to find them (from the database)
List<Person> persons = personFinderService.findPersons(MAX_RESULTS);
personIdsModel = selectIdModelFactory.create(persons, "firstName", "id");
}
}
.eg {
margin: 20px 0;
padding: 14px;
color: #333;
border: 1px solid #ddd;
border-radius: 6px;
-webkit-border-radius: 6px;
-mox-border-radius: 6px;
}
.eg select.form-control {
width: auto;
}
// Based on Tapestry's SelectModelFactory.
package jumpstart.web.services;
import java.util.List;
import org.apache.tapestry5.SelectModel;
/**
* Used to create an {@link org.apache.tapestry5.SelectModel}.
*/
public interface SelectIdModelFactory
{
/**
* Creates a {@link org.apache.tapestry5.SelectModel} from a list of objects of the same type and a label property name and an id property name.
* The returned model creates for every object in the list a selectable option and relies on existing
* {@link org.apache.tapestry5.ValueEncoder} for the object type. The value of the label property is used as user-presentable label for the option.
* The value of the id property is used as the hidden id for the option.
*
* @param objects objects to create model from
* @param labelProperty property for the client-side label
* @param idProperty property for the client-side value
* @return the model
*/
public SelectModel create(List<?> objects, String labelProperty, String idProperty);
}
// Based on Tapestry's SelectModelFactoryImpl.
package jumpstart.web.services;
import java.util.List;
import org.apache.tapestry5.OptionModel;
import org.apache.tapestry5.SelectModel;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.internal.OptionModelImpl;
import org.apache.tapestry5.internal.SelectModelImpl;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.services.ClassPropertyAdapter;
import org.apache.tapestry5.ioc.services.PropertyAccess;
import org.apache.tapestry5.ioc.services.PropertyAdapter;
import org.apache.tapestry5.services.ValueEncoderSource;
public class SelectIdModelFactoryImpl implements SelectIdModelFactory {
private final PropertyAccess propertyAccess;
private final ValueEncoderSource valueEncoderSource;
public SelectIdModelFactoryImpl(final PropertyAccess propertyAccess, final ValueEncoderSource valueEncoderSource) {
super();
this.propertyAccess = propertyAccess;
this.valueEncoderSource = valueEncoderSource;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public SelectModel create(final List<?> objects, final String labelProperty, final String idProperty) {
final List<OptionModel> options = CollectionFactory.newList();
for (final Object object : objects) {
final ClassPropertyAdapter classPropertyAdapter = this.propertyAccess.getAdapter(object);
final PropertyAdapter labelPropertyAdapter = classPropertyAdapter.getPropertyAdapter(labelProperty);
final ValueEncoder labelEncoder = this.valueEncoderSource.getValueEncoder(labelPropertyAdapter.getType());
final Object label = labelPropertyAdapter.get(object);
final PropertyAdapter idPropertyAdapter = classPropertyAdapter.getPropertyAdapter(idProperty);
final ValueEncoder idEncoder = this.valueEncoderSource.getValueEncoder(idPropertyAdapter.getType());
final Object id = idPropertyAdapter.get(object);
options.add(new OptionModelImpl(labelEncoder.toClient(label), idEncoder.toClient(id)));
}
return new SelectModelImpl(null, options);
}
}
package jumpstart.web.services;
import java.util.Map;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
import org.apache.tapestry5.internal.beanvalidator.BaseCCD;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.BeanBlockContribution;
import org.apache.tapestry5.services.ComponentRequestFilter;
import org.apache.tapestry5.services.DisplayBlockContribution;
import org.apache.tapestry5.services.EditBlockContribution;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.javascript.DataConstants;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.upload.services.UploadSymbols;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import jumpstart.business.validation.constraints.Letters;
import jumpstart.util.JodaTimeUtil;
import jumpstart.web.translators.MoneyTranslator;
import jumpstart.web.translators.YesNoTranslator;
/**
* This module is automatically included as part of the Tapestry IoC Registry, it's a good place to configure and extend
* Tapestry, or to place your own service definitions. See http://tapestry.apache.org/5.3.4/tapestry-ioc/module.html
*/
public class AppModule {
private static final String UPLOADS_PATH = "jumpstart.upload-path";
@Inject
@Symbol(SymbolConstants.PRODUCTION_MODE)
@Property(write = false)
private static boolean productionMode;
// Add 2 services to those provided by Tapestry.
// - CountryNames, and SelectIdModelFactory are used by pages which ask Tapestry to @Inject them.
public static void bind(ServiceBinder binder) {
binder.bind(CountryNames.class);
binder.bind(SelectIdModelFactory.class, SelectIdModelFactoryImpl.class);
}
// Tell Tapestry about our custom translators and their message file.
// We do this by contributing configuration to Tapestry's TranslatorAlternatesSource service, FieldValidatorSource
// service, and ComponentMessagesSource service.
@SuppressWarnings("rawtypes")
public static void contributeTranslatorAlternatesSource(MappedConfiguration<String, Translator> configuration,
ThreadLocale threadLocale) {
configuration.add("yesno", new YesNoTranslator("yesno"));
configuration.add("money2", new MoneyTranslator("money2", 2, threadLocale));
}
public void contributeComponentMessagesSource(OrderedConfiguration<String> configuration) {
configuration.add("myTranslationMessages", "jumpstart/web/translators/TranslationMessages");
}
// Tell Tapestry about the client-side (javascript) validators that corresponds to each server-side Bean Validator.
public static void contributeClientConstraintDescriptorSource(final JavaScriptSupport javaScriptSupport,
final Configuration<ClientConstraintDescriptor> configuration) {
configuration.add(new BaseCCD(Letters.class) {
public void applyClientValidation(MarkupWriter writer, String message, Map<String, Object> attributes) {
javaScriptSupport.require("beanvalidation/letters");
writer.attributes(DataConstants.VALIDATION_ATTRIBUTE, true, "data-validate-letters", true,
"data-letters-message", message);
}
});
}
// Tell Tapestry about our custom ValueEncoders.
// We do this by contributing configuration to Tapestry's ValueEncoderSource service.
// @SuppressWarnings("rawtypes")
// public static void contributeValueEncoderSource(MappedConfiguration<Class, Object> configuration) {
// configuration.addInstance(Person.class, PersonEncoder.class);
// }
// Tell Tapestry which locales we support, and tell Tapestry to use its jQuery implementation for its JavaScript.
// We do this by contributing configuration to Tapestry's ApplicationDefaults service.
public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration) {
configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en_US,en_GB,fr");
configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery");
}
// Tell Tapestry how to detect and protect pages that require security.
// We do this by contributing a custom ComponentRequestFilter to Tapestry's ComponentRequestHandler service.
// - ComponentRequestHandler is shown in
// http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview
// - Based on http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html
public void contributeComponentRequestHandler(OrderedConfiguration<ComponentRequestFilter> configuration) {
configuration.addInstance("PageProtectionFilter", PageProtectionFilter.class);
}
// Tell Tapestry how to handle WildFly 11's classpath URLs - WildFly uses a "virtual file system".
// We do this by overriding Tapestry's ClasspathURLConverter service.
// See "Running Tapestry on JBoss" (sic) in http://wiki.apache.org/tapestry/Tapestry5HowTos .
@SuppressWarnings("rawtypes")
public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration) {
// configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterJBoss7());
configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterWildFly11());
}
// Tell Tapestry how to handle @EJB in page and component classes.
// We do this by contributing configuration to Tapestry's ComponentClassTransformWorker service.
// - Based on http://wiki.apache.org/tapestry/JEE-Annotation.
@Primary
public static void contributeComponentClassTransformWorker(
OrderedConfiguration<ComponentClassTransformWorker2> configuration) {
configuration.addInstance("EJB", EJBAnnotationWorker.class, "before:Property");
}
// Tell Tapestry how to handle pages annotated with @WhitelistAccessOnly, eg. Tapestry's ServiceStatus and
// PageCatalog.
// The default WhitelistAnalyzer allows localhost only and only in non-production mode.
// Our aim is to make the servicestatus page available to ALL clients when not in production mode.
// We do this by contributing our own WhitelistAnalyzer to Tapestry's ClientWhitelist service.
public static void contributeClientWhitelist(OrderedConfiguration<WhitelistAnalyzer> configuration) {
if (!productionMode) {
configuration.add("NonProductionWhitelistAnalyzer", new WhitelistAnalyzer() {
@Override
public boolean isRequestOnWhitelist(Request request) {
if (request.getPath().startsWith("/core/servicestatus")) {
return true;
}
else {
// This is copied from org.apache.tapestry5.internal.services.security.LocalhostOnly
String remoteHost = request.getRemoteHost();
return remoteHost.equals("localhost") || remoteHost.equals("127.0.0.1")
|| remoteHost.equals("0:0:0:0:0:0:0:1%0") || remoteHost.equals("0:0:0:0:0:0:0:1");
}
}
}, "before:*");
}
}
// Tell Tapestry how to build our Filer service (used in the FileUpload example).
// Annotate it with EagerLoad to force resolution of symbols at startup rather than when it is first used.
@EagerLoad
public static IFiler buildFiler(Logger logger, @Inject @Symbol(UPLOADS_PATH) final String uploadsPath,
@Inject @Symbol(UploadSymbols.FILESIZE_MAX) final long fileSizeMax) {
return new Filer(logger, UPLOADS_PATH, uploadsPath, UploadSymbols.FILESIZE_MAX, fileSizeMax);
}
// Tell Tapestry how to coerce Joda Time types to and from Java Date types for the TypeCoercers example.
// We do this by contributing configuration to Tapestry's TypeCoercer service.
// - Based on http://tapestry.apache.org/typecoercer-service.html
@SuppressWarnings("rawtypes")
public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration) {
// From java.util.Date to DateMidnight
Coercion<java.util.Date, DateMidnight> toDateMidnight = new Coercion<java.util.Date, DateMidnight>() {
public DateMidnight coerce(java.util.Date input) {
// TODO - confirm this conversion always works, esp. across timezones
return JodaTimeUtil.toDateMidnight(input);
}
};
configuration.add(new CoercionTuple<>(java.util.Date.class, DateMidnight.class, toDateMidnight));
// From DateMidnight to java.util.Date
Coercion<DateMidnight, java.util.Date> fromDateMidnight = new Coercion<DateMidnight, java.util.Date>() {
public java.util.Date coerce(DateMidnight input) {
// TODO - confirm this conversion always works, esp. across timezones
return JodaTimeUtil.toJavaDate(input);
}
};
configuration.add(new CoercionTuple<>(DateMidnight.class, java.util.Date.class, fromDateMidnight));
// From java.util.Date to LocalDate
Coercion<java.util.Date, LocalDate> toLocalDate = new Coercion<java.util.Date, LocalDate>() {
public LocalDate coerce(java.util.Date input) {
// TODO - confirm this conversion always works, esp. across timezones
return JodaTimeUtil.toLocalDate(input);
}
};
configuration.add(new CoercionTuple<>(java.util.Date.class, LocalDate.class, toLocalDate));
// From LocalDate to java.util.Date
Coercion<LocalDate, java.util.Date> fromLocalDate = new Coercion<LocalDate, java.util.Date>() {
public java.util.Date coerce(LocalDate input) {
// TODO - confirm this conversion always works, esp. across timezones
return JodaTimeUtil.toJavaDate(input);
}
};
configuration.add(new CoercionTuple<>(LocalDate.class, java.util.Date.class, fromLocalDate));
}
// Tell Tapestry how its BeanDisplay and BeanEditor can handle the JodaTime types.
// We do this by contributing configuration to Tapestry's DefaultDataTypeAnalyzer and BeanBlockSource services.
// - Based on http://tapestry.apache.org/beaneditform-guide.html .
public static void contributeDefaultDataTypeAnalyzer(
@SuppressWarnings("rawtypes") MappedConfiguration<Class, String> configuration) {
configuration.add(DateTime.class, "dateTime");
configuration.add(DateMidnight.class, "dateMidnight");
configuration.add(LocalDateTime.class, "localDateTime");
configuration.add(LocalDate.class, "localDate");
configuration.add(LocalTime.class, "localTime");
}
public static void contributeBeanBlockSource(Configuration<BeanBlockContribution> configuration) {
configuration.add(new DisplayBlockContribution("dateTime", "infra/AppPropertyDisplayBlocks", "dateTime"));
configuration
.add(new DisplayBlockContribution("dateMidnight", "infra/AppPropertyDisplayBlocks", "dateMidnight"));
configuration
.add(new DisplayBlockContribution("localDateTime", "infra/AppPropertyDisplayBlocks", "localDateTime"));
configuration.add(new DisplayBlockContribution("localDate", "infra/AppPropertyDisplayBlocks", "localDate"));
configuration.add(new DisplayBlockContribution("localTime", "infra/AppPropertyDisplayBlocks", "localTime"));
configuration.add(new EditBlockContribution("dateMidnight", "infra/AppPropertyEditBlocks", "dateMidnight"));
configuration.add(new EditBlockContribution("localDate", "infra/AppPropertyEditBlocks", "localDate"));
}
}
package jumpstart.business.domain.person;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* The Person entity.
*/
@Entity
@SuppressWarnings("serial")
public class Person implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Long id;
@Version
@Column(nullable = false)
private Integer version;
@Column(length = 10, nullable = false)
@NotNull
@Size(max = 10)
private String firstName;
@Column(length = 10, nullable = false)
@NotNull
@Size(max = 10)
private String lastName;
@Enumerated(EnumType.STRING)
@NotNull
private Regions region;
@Temporal(TemporalType.DATE)
@NotNull
private Date startDate;
public String toString() {
final String DIVIDER = ", ";
StringBuilder buf = new StringBuilder();
buf.append(this.getClass().getSimpleName() + ": ");
buf.append("[");
buf.append("id=" + id + DIVIDER);
buf.append("version=" + version + DIVIDER);
buf.append("firstName=" + firstName + DIVIDER);
buf.append("lastName=" + lastName + DIVIDER);
buf.append("region=" + region + DIVIDER);
buf.append("startDate=" + startDate);
buf.append("]");
return buf.toString();
}
// Default constructor is required by JPA.
public Person() {
}
public Person(String firstName, String lastName, Regions region, Date startDate) {
super();
this.firstName = firstName;
this.lastName = lastName;
this.region = region;
this.startDate = startDate;
}
// The need for an equals() method is discussed at http://www.hibernate.org/109.html
@Override
public boolean equals(Object obj) {
return (obj == this) || (obj instanceof Person) && id != null && id.equals(((Person) obj).getId());
}
// The need for a hashCode() method is discussed at http://www.hibernate.org/109.html
@Override
public int hashCode() {
return id == null ? super.hashCode() : id.hashCode();
}
@PrePersist
@PreUpdate
public void validate() throws ValidationException {
}
public Long getId() {
return id;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Regions getRegion() {
return region;
}
public void setRegion(Regions region) {
this.region = region;
}
public Date getStartDate() {
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
}