Objective: Create a single form interface for an entity, submitting to, and generating a single listing, of its persisted instances, supporting CRUDS-QBE (More….).
This toy application, crudsqbe, can be developed on any old machine supporting Java, just need a text editor, a browser and access to the cloud (for downloads). It builds on top of Hibernate, Lucence using Tapestry. Any database can be used, instructions below are for PostgreSQL.
IMPORTANT: Do not, obviously, publish this on a web-accessible (production) server, without PROPERLY securing it (NOT covered in this post). This will open up your database to the outside world. Matter of fact, use everything below AT YOUR OWN RISK…this is just a proof-of-concept, i.e. NOT meant to be thread-safe etc…MUCH work needed for prime-time. Comments welcome, may be posted below, will be moderated periodically, edited, perhaps even ignored. Link to source-code is somewhere at the end of this blog…so, read first ;-)…
Assume clean start (if not applicable, just skip, adapt as you go along):
1. Download the Java Development Kit (JDK) from
http://www.oracle.com/technetwork/java/javase/downloads/index.html
Java SE 6 Update 32 Windows x64 (64-bit) jdk-6u32-windows-x64.exe (59.66 MB). Installed to c:/jdk1.6.0_32.
2. Download Maven from
File apache-maven-3.0.4-bin.zip and extracted everything under apache-maven-3.0.4 folder to c:/maven3.0.4.
3. Set environment variables JAVA_HOME and M2_HOME, then add to path: Control Panel –> System and Maint –> System –> Advanced System Settings –> Environment Variables. Click New under System Variables. Enter variable JAVE_HOME with value c:\jdk1.6.0_32, M2_HOME with value c:\maven3.0.4. After that append %JAVA_HOME%\bin;%M2_HOME%\bin to the Path environment variable.
4. Verifying steps 1-3. Open a command shell. Type
mvn -version
Ensure all environment variables and paths are working, as follows:
Apache Maven 3.0.4 (r1232337; 2012-01-17 02:44:56-0600) Maven home: c:\maven3.0.4 Java version: 1.6.0_32, vendor: Sun Microsystems Inc. Java home: c:\jdk1.6.0_32\jre Default locale: en_US, platform encoding: Cp1252 OS name: "windows vista", version: "6.0", arch: "amd64", family: "windows"
5. Create your projects directory, say, c:/projects and change into it, type
mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org
(The highlights below indicate interactive user input with the maven build process to select the version 5 archetype, version 5.3.3, set groupId, artifactID etc)…
[INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Stub Project (No POM) 1 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >> > [INFO] [INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom << < [INFO] [INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom -- - [INFO] Generating project in Interactive mode [INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven. archetypes:maven-archetype-quickstart:1.0) Choose archetype: 1: http://tapestry.apache.org -> org.apache.tapestry:quickstart (Tapestry 5 Quickstart Project) 2: http://tapestry.apache.org -> org.apache.tapestry:tapestry-archetype (Tapestry 4.1.6 Archetype) Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 1 Choose org.apache.tapestry:quickstart version: 1: 5.0.19 2: 5.1.0.5 3: 5.2.6 4: 5.3 5: 5.3.1 6: 5.3.2 7: 5.3.3 Choose a number: 7: 7 Downloading: http://tapestry.apache.org/org/apache/tapestry/quickstart/5.3.3/qui ckstart-5.3.3.jar Downloading: http://repo.maven.apache.org/maven2/org/apache/tapestry/quickstart/ 5.3.3/quickstart-5.3.3.jar Downloaded: http://repo.maven.apache.org/maven2/org/apache/tapestry/quickstart/5 .3.3/quickstart-5.3.3.jar (88 KB at 59.6 KB/sec) Downloading: http://tapestry.apache.org/org/apache/tapestry/quickstart/5.3.3/qui ckstart-5.3.3.pom Downloading: http://repo.maven.apache.org/maven2/org/apache/tapestry/quickstart/ 5.3.3/quickstart-5.3.3.pom Downloaded: http://repo.maven.apache.org/maven2/org/apache/tapestry/quickstart/5 .3.3/quickstart-5.3.3.pom (402 B at 0.8 KB/sec) Define value for property 'groupId': : org.ideademo Define value for property 'artifactId': : crudsqbe Define value for property 'version': 1.0-SNAPSHOT: : Define value for property 'package': org.ideademo: : org.ideademo.crudsqbe Confirm properties configuration: groupId: org.ideademo artifactId: crudsqbe version: 1.0-SNAPSHOT package: org.ideademo.crudsqbe Y: : [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating project from Archetype: quickstart:5.3.3 [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: org.ideademo [INFO] Parameter: artifactId, Value: crudsqbe [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: package, Value: org.ideademo.crudsqbe [INFO] Parameter: packageInPathFormat, Value: org/ideademo/crudsqbe [INFO] Parameter: package, Value: org.ideademo.crudsqbe [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: groupId, Value: org.ideademo [INFO] Parameter: artifactId, Value: crudsqbe [WARNING] Don't override file C:\projects\crudsqbe\src\test\java [WARNING] Don't override file C:\projects\crudsqbe\src\main\webapp [WARNING] Don't override file C:\projects\crudsqbe\src\main\resources\org\ideademo\crudsqbe [WARNING] Don't override file C:\projects\crudsqbe\src\test\resources [WARNING] Don't override file C:\projects\crudsqbe\src\test\conf [WARNING] Don't override file C:\projects\crudsqbe\src\site [INFO] project created from Archetype in dir: C:\projects\crudsqbe [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4:01.004s [INFO] Finished at: Sat Apr 28 02:12:42 CDT 2012 [INFO] Final Memory: 9M/80M [INFO] ------------------------------------------------------------------------
6. Change directory to c:\projects\crudsqbe, FIRST, and, THEN AND ONLY THEN, type mvn jetty:run [Enter]
[This will generate plenty of console output that ends in the jetty server running as below]
Application 'app' (version 1.0-SNAPSHOT-DEV) startup time: 296 ms to build IoC Registry, 1,497 ms overall. ______ __ ____ /_ __/__ ____ ___ ___ / /_______ __ / __/ / / / _ `/ _ \/ -_|_-</ __/ __/ // / /__ \ /_/ \_,_/ .__/\__/___/\__/_/ \_, / /____/ /_/ /___/ 5.3.3 (development mode) 2012-04-28 02:28:57.467::INFO: Started SelectChannelConnector@0.0.0.0:8080 [INFO] Started Jetty Server
7. Open browser, Verify http://localhost:8080/crudsqbe.
Within cmd shell, do CTRL+C, Yes [Enter]
to stop Jetty, for now.
8. Setup Database Server.
Going with Postgres 9.1.3 from
Downloaded the 64-bit version: postgresql-9.1.3-1-windows-x64.exe
Installed in C:\PostgreSQL\9.1. Password for postgres super user is welcome123, rest defaults. No need for stackbuilder. Open pgAdmin and create a database called “crudsqbe”. Start –> PostgreSQL –> pgAdmin III. Right click on PostgresSQL, choose connect. Type the password welcome123, save it. Right click on Databases, choose New Database and type crudsqbe, OK. Close/minimize PgAdminIII
OPTIONAL (skip, just FYI). Since just command shell was promised, the equivalent pgAdmin creation of NEW crudsqbe database would be:
C:\PostgreSQL\9.1\bin\createdb --username postgres --no-password --host localhost --port 5432 -T template0 crudsqbe
9. Modify pom.xml with latest info from source like http://mvnrepository.com/ to activate tapestry hibernate, add jdbc driver and lucene search api. PARTIAL listing is shown below (note that you would simply CHANGE tapestry-core to tapestry-hibernate):
<!-- Too set up an application with a database, change the artifactId below to tapestry-hibernate, and add a dependency on your JDBC driver. You'll also need to add Hibernate configuration files, such as hibernate.cfg.xml. --> <dependency> <groupId>org.apache.tapestry</groupId> <artifactId>tapestry-hibernate</artifactId> <version>${tapestry-release-version}</version> </dependency> <!-- JDBC driver --> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901.jdbc4</version> </dependency> <!-- Search API --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search</artifactId> <version>3.4.2.Final</version> </dependency>
10. Place the hibernate.cfg.xml
under crudsqbe/src/main/resource
s directory (where you should also see a log4j.properties
file):
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class">org.postgresql.Driver</property> <property name="hibernate.connection.url">jdbc:postgresql:crudsqbe</property> <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property> <property name="hibernate.connection.username">postgres</property> <property name="hibernate.connection.password">welcome123</property> <property name="hbm2ddl.auto">update</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <property name="hibernate.search.default.directory_provider">filesystem</property> <property name="hibernate.search.default.indexBase">lucene/indexes</property> </session-factory> </hibernate-configuration>
11. Create directory called “entities” under C:\projects\crudsqbe\src\main\java\org\ideademo\crudsqbe
. This folder will be scanned by Tapestry-hibernate for persistence.
Directory of C:\projects\crudsqbe\src\main\java\org\ideademo\crudsqbe 04/28/2012 03:42 AM <DIR> . 04/28/2012 03:42 AM <DIR> .. 04/28/2012 02:12 AM <DIR> components 04/28/2012 03:42 AM <DIR> entities 04/28/2012 02:12 AM <DIR> pages 04/28/2012 02:12 AM <DIR> services
12. Create the example entity class Person.java within the new entities
directory.
package org.ideademo.crudsqbe.entities; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.apache.tapestry5.beaneditor.NonVisual; @Entity @Indexed public class Person { ///////////////////////////////////////////////////////////////////////////// // Internal auto generated Id field, also document id for lucene indexing // @Id @GeneratedValue @DocumentId @NonVisual private Long id; /////////////////////////////////////////////////////////// // Field annotation is used by Lucene to power text search // Hibernate QBE search can also do text over this field @Field private String name=""; //////////////////////////////////////////////////////// // QBE Search fields. Lucene does not care about these private boolean politician = false; private boolean actor = false; ////////////////////////////////// // Getters, Setters // public Long getId() { return id; } public void setId(Long Id) { this.id = Id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isPolitician() { return politician; } public void setPolitician(boolean politician) { this.politician = politician; } public boolean isActor() { return actor; } public void setActor(boolean actor) { this.actor = actor; } public String toString() { return "Person.name = " + this.getName() + ", actor? = " + this.isActor() + ", politician? = " + this.isPolitician(); } }
13. Create the input form for Person. As a tapestry template called EditPerson.tml within the following directory
C:\projects\crudsqbe\src\main\resources\org\ideademo\crudsqbe\pages\person
(need to mkdir the “person” under “pages” directory)
<html t:type="layout" title="Add/Update Person" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter"> <t:form t:id="personForm"> <t:errors /> <label for="name">Name</label> <t:textarea t:id="name" t:validate="maxlength=255" rows="1" cols="50" value="person.name" /><br /><br /> <t:checkbox t:id="politician" t:value="person.politician" /> <t:label for="politician">Politician</t:label> <t:checkbox t:id="actor" t:value="person.actor" /> <t:label for="actor">Actor</t:label><br /><br /> <t:submit value="Add/Update" /> <t:submit t:id="search" value="Search" /> (<t:pagelink page="prop:componentResources.pageName">CLEAR - form</t:pagelink>) (<t:pagelink page="persons">CANCEL - go to list</t:pagelink>) </t:form> </html>
Without the need for reuse as QBE, i.e. the search button, this input form could have been as simple as
<t:beaneditform object="person" />
It is based on Beaneditor, which could also be used, if there there was not much requirement regarding custom labels, styles etc. Aside: the style elements (class=appnitro etc will not work in this demo, which will just be using default styles.
<t:form> <t:errors /> <t:beaneditor object="person" /> <t:submit value="Create/Edit" /><t:submit t:id="search" value="Search"/> </t:form>
14. Create corresponding Tapestry java file EditPerson.java within
C:\projects\crudsqbe\src\main\java\org\ideademo\crudsqbe\pages\person
(need to mkdir the “person” under “pages” directory)
package org.ideademo.crudsqbe.pages.person; import org.hibernate.Session; import org.apache.tapestry5.annotations.InjectPage; import org.apache.tapestry5.annotations.PageActivationContext; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.hibernate.annotations.CommitAfter; import org.apache.tapestry5.ioc.annotations.Inject; import org.ideademo.crudsqbe.entities.Person; import org.ideademo.crudsqbe.pages.Persons; import org.apache.log4j.Logger; public class EditPerson { private static Logger logger = Logger.getLogger(EditPerson.class); @Inject private Session session; @PageActivationContext @Property private Person person; @InjectPage private Persons persons; ////////////////////////////// // Search Buttom pressed Object onSelectedFromSearch() { logger.info("QBE: " + person); persons.setPersonExample(person); return persons; } @CommitAfter Object onSuccess() { logger.info("Created/Updated: " + person); session.saveOrUpdate(person); return persons; } //////////////////////////////////////////////////////////////////////////////////// // The following event handlers permit reuse of Edit form as Create and also do QBE // by providing the form with person object to bind with (overlay) its data fields // // Not needed for BeanEditor which will instance the object if NULL when // rendering or handling a form submission void onPrepareForRender() { if (this.person == null) { this.person = new Person(); } } /////////////////////////////////////////////// // obviates persisting person object in session void onPrepareForSubmit() { if (this.person == null) { this.person = new Person(); } } }
15. Persons.tml within pages directory under resources
C:\projects\crudsqbe\src\main\resources\org\ideademo\crudsqbe\pages\
<html t:type="layout" title="Persons List" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter"> <body> <div align="center"> <t:form t:id="searchForm"> <t:textField t:id="searchText" size="30" value="searchText" /> <t:submit t:id="search" value="Search" /><t:submit t:id="clear" value="Clear/Show All" /> </t:form> </div> <div style="background-color:orange;text-align:center;font-family:Verdana, Geneva, sans-serif"> <t:pagelink page="person/edit">Create NEW Personal Record (OR Advanced Search)</t:pagelink> </div> <div align="center"> <t:grid source="persons" row="personRow" add="delete"> <p:deleteCell> <t:actionlink t:id="delete" context="personRow.id">Delete</t:actionlink> </p:deleteCell> <p:nameCell> <t:pagelink page="person/edit" context="personRow.id">${personRow.name}</t:pagelink> </p:nameCell> <p:empty> <p>There are no people (matching the criteria); you can <t:pagelink page="person/edit">add some</t:pagelink> (or <t:actionlink t:id="showAll">show all</t:actionlink>)</p> </p:empty> </t:grid> </div> </body> </html>
16. Persons.java within the following pages directory under main/java
C:\projects\crudsqbe\src\main\java\org\ideademo\crudsqbe\pages\
package org.ideademo.crudsqbe.pages; import java.util.List; import org.apache.tapestry5.PersistenceConstants; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.annotations.Persist; import org.apache.tapestry5.hibernate.annotations.CommitAfter; import org.apache.tapestry5.hibernate.HibernateSessionManager; import org.apache.tapestry5.ioc.annotations.Inject; import org.hibernate.Session; import org.hibernate.criterion.Example; import org.hibernate.criterion.MatchMode; import org.hibernate.search.FullTextSession; import org.hibernate.search.Search; import org.hibernate.search.query.dsl.QueryBuilder; import org.ideademo.crudsqbe.entities.Person; import org.apache.log4j.Logger; public class Persons { private static Logger logger = Logger.getLogger(Persons.class); ///////////////////////////// // Drives QBE Search @Persist (PersistenceConstants.FLASH) private Person personExample; ////////////////////////////// // Used in rendering Grid Row @SuppressWarnings("unused") @Property private Person personRow; @Property @Persist (PersistenceConstants.FLASH) private String searchText; @Inject private Session session; @Inject private HibernateSessionManager sessionManager; //////////////////////////////////////////////////////////////////////////////////////////////////////// // Entity List generator - QBE, Text Search or Show All // @SuppressWarnings("unchecked") public List<Person> getPersons() { if(personExample != null) { Example example = Example.create(personExample).excludeFalse().ignoreCase().enableLike(MatchMode.ANYWHERE); return session.createCriteria(Person.class).add(example).list(); } else if (searchText != null && searchText.trim().length() > 0) { FullTextSession fullTextSession = Search.getFullTextSession(sessionManager.getSession()); try { fullTextSession.createIndexer().startAndWait(); } catch (java.lang.InterruptedException e) { logger.warn("Lucene Indexing was interrupted by something " + e); } QueryBuilder qb = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity( Person.class ).get(); org.apache.lucene.search.Query luceneQuery = qb .keyword() .onFields("name") .matching(searchText) .createQuery(); return fullTextSession.createFullTextQuery(luceneQuery, Person.class).list(); } else { // default - unfiltered - all entitites list return session.createCriteria(Person.class).list(); } } //////////////////////////////////////////////////// // Action/Event Handlers // @CommitAfter void onActionFromDelete(long id) { Person a = (Person) session.get(Person.class, id); session.delete(a); logger.info("Deleted " + a.getName()); } /** * resets the list */ void onActionFromShowAll() { this.searchText = ""; this.personExample = null; } Object onSelectedFromSearch() { return null; } Object onSelectedFromClear() { onActionFromShowAll(); return null; } /////////////////////////////////////////// // Setters // public void setPersonExample(Person person) { this.personExample = person; } }
17. Need to add search capability to Hibernate to filter out boolean properties that are set to false (JIRA). Specifically, in the context of form-driven QBE, false corresponds to an empty checkbox. This is to be treated just like an empty text field – ignored.
This nuance was added by first downloading the source code from
http://grepcode.com/snapshot/repo1.maven.org/maven2/org.hibernate/hibernate-search/3.4.2.Final
Create package org.hibernate.criterion under the main/java. Include the following two source files as-is.
/** * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2010, Red Hat Inc. or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Inc. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.hibernate.criterion; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.hibernate.Criteria; import org.hibernate.EntityMode; import org.hibernate.HibernateException; import org.hibernate.engine.TypedValue; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.util.StringHelper; /** * Support for query by example. * <pre> * List results = session.createCriteria(Parent.class) * .add( Example.create(parent).ignoreCase() ) * .createCriteria("child") * .add( Example.create( parent.getChild() ) ) * .list(); * </pre> * "Examples" may be mixed and matched with "Expressions" in the same <tt>Criteria</tt>. * @see org.hibernate.Criteria * @author Gavin King */ public class Example implements Criterion { private final Object entity; private final Set excludedProperties = new HashSet(); private PropertySelector selector; private boolean isLikeEnabled; private Character escapeCharacter; private boolean isIgnoreCaseEnabled; private MatchMode matchMode; /** * A strategy for choosing property values for inclusion in the query * criteria */ public static interface PropertySelector extends Serializable { public boolean include(Object propertyValue, String propertyName, Type type); } private static final PropertySelector NOT_NULL = new NotNullPropertySelector(); private static final PropertySelector ALL = new AllPropertySelector(); private static final PropertySelector NOT_NULL_OR_ZERO = new NotNullOrZeroPropertySelector(); //////////////////////////////////////////////////////////// // Edits for enabling excludeFalse() // private static final PropertySelector NOT_FALSE = new NotFalsePropertySelector(); static final class NotFalsePropertySelector implements PropertySelector { public boolean include(Object object, String propertyName, Type type) { if (object == null) { return false; } else if (object instanceof Boolean) { return ((Boolean) object).booleanValue(); } else { return true; } } } /** * Exclude false-valued properties */ public Example excludeFalse() { setPropertySelector(NOT_FALSE); return this; } // // End ///////////////////////////////////////// static final class AllPropertySelector implements PropertySelector { public boolean include(Object object, String propertyName, Type type) { return true; } private Object readResolve() { return ALL; } } static final class NotNullPropertySelector implements PropertySelector { public boolean include(Object object, String propertyName, Type type) { return object!=null; } private Object readResolve() { return NOT_NULL; } } static final class NotNullOrZeroPropertySelector implements PropertySelector { public boolean include(Object object, String propertyName, Type type) { return object!=null && ( !(object instanceof Number) || ( (Number) object ).longValue()!=0 ); } private Object readResolve() { return NOT_NULL_OR_ZERO; } } /** * Set escape character for "like" clause */ public Example setEscapeCharacter(Character escapeCharacter) { this.escapeCharacter = escapeCharacter; return this; } /** * Set the property selector */ public Example setPropertySelector(PropertySelector selector) { this.selector = selector; return this; } /** * Exclude zero-valued properties */ public Example excludeZeroes() { setPropertySelector(NOT_NULL_OR_ZERO); return this; } /** * Don't exclude null or zero-valued properties */ public Example excludeNone() { setPropertySelector(ALL); return this; } /** * Use the "like" operator for all string-valued properties */ public Example enableLike(MatchMode matchMode) { isLikeEnabled = true; this.matchMode = matchMode; return this; } /** * Use the "like" operator for all string-valued properties */ public Example enableLike() { return enableLike(MatchMode.EXACT); } /** * Ignore case for all string-valued properties */ public Example ignoreCase() { isIgnoreCaseEnabled = true; return this; } /** * Exclude a particular named property */ public Example excludeProperty(String name) { excludedProperties.add(name); return this; } /** * Create a new instance, which includes all non-null properties * by default * @param entity * @return a new instance of <tt>Example</tt> */ public static Example create(Object entity) { if (entity==null) throw new NullPointerException("null example"); return new Example(entity, NOT_NULL); } protected Example(Object entity, PropertySelector selector) { this.entity = entity; this.selector = selector; } public String toString() { return "example (" + entity + ')'; } private boolean isPropertyIncluded(Object value, String name, Type type) { return !excludedProperties.contains(name) && !type.isAssociationType() && selector.include(value, name, type); } public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { StringBuffer buf = new StringBuffer().append('('); EntityPersister meta = criteriaQuery.getFactory().getEntityPersister( criteriaQuery.getEntityName(criteria) ); String[] propertyNames = meta.getPropertyNames(); Type[] propertyTypes = meta.getPropertyTypes(); //TODO: get all properties, not just the fetched ones! Object[] propertyValues = meta.getPropertyValues( entity, getEntityMode(criteria, criteriaQuery) ); for (int i=0; i<propertyNames.length; i++) { Object propertyValue = propertyValues[i]; String propertyName = propertyNames[i]; boolean isPropertyIncluded = i!=meta.getVersionProperty() && isPropertyIncluded( propertyValue, propertyName, propertyTypes[i] ); if (isPropertyIncluded) { if ( propertyTypes[i].isComponentType() ) { appendComponentCondition( propertyName, propertyValue, (CompositeType) propertyTypes[i], criteria, criteriaQuery, buf ); } else { appendPropertyCondition( propertyName, propertyValue, criteria, criteriaQuery, buf ); } } } if ( buf.length()==1 ) buf.append("1=1"); //yuck! return buf.append(')').toString(); } private static final Object[] TYPED_VALUES = new TypedValue[0]; public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { EntityPersister meta = criteriaQuery.getFactory() .getEntityPersister( criteriaQuery.getEntityName(criteria) ); String[] propertyNames = meta.getPropertyNames(); Type[] propertyTypes = meta.getPropertyTypes(); //TODO: get all properties, not just the fetched ones! Object[] values = meta.getPropertyValues( entity, getEntityMode(criteria, criteriaQuery) ); List list = new ArrayList(); for (int i=0; i<propertyNames.length; i++) { Object value = values[i]; Type type = propertyTypes[i]; String name = propertyNames[i]; boolean isPropertyIncluded = i!=meta.getVersionProperty() && isPropertyIncluded(value, name, type); if (isPropertyIncluded) { if ( propertyTypes[i].isComponentType() ) { addComponentTypedValues(name, value, (CompositeType) type, list, criteria, criteriaQuery); } else { addPropertyTypedValue(value, type, list); } } } return (TypedValue[]) list.toArray(TYPED_VALUES); } private EntityMode getEntityMode(Criteria criteria, CriteriaQuery criteriaQuery) { EntityPersister meta = criteriaQuery.getFactory() .getEntityPersister( criteriaQuery.getEntityName(criteria) ); EntityMode result = meta.guessEntityMode(entity); if (result==null) { throw new ClassCastException( entity.getClass().getName() ); } return result; } protected void addPropertyTypedValue(Object value, Type type, List list) { if ( value!=null ) { if ( value instanceof String ) { String string = (String) value; if (isIgnoreCaseEnabled) string = string.toLowerCase(); if (isLikeEnabled) string = matchMode.toMatchString(string); value = string; } list.add( new TypedValue(type, value, null) ); } } protected void addComponentTypedValues( String path, Object component, CompositeType type, List list, Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { if (component!=null) { String[] propertyNames = type.getPropertyNames(); Type[] subtypes = type.getSubtypes(); Object[] values = type.getPropertyValues( component, getEntityMode(criteria, criteriaQuery) ); for (int i=0; i<propertyNames.length; i++) { Object value = values[i]; Type subtype = subtypes[i]; String subpath = StringHelper.qualify( path, propertyNames[i] ); if ( isPropertyIncluded(value, subpath, subtype) ) { if ( subtype.isComponentType() ) { addComponentTypedValues(subpath, value, (CompositeType) subtype, list, criteria, criteriaQuery); } else { addPropertyTypedValue(value, subtype, list); } } } } } protected void appendPropertyCondition( String propertyName, Object propertyValue, Criteria criteria, CriteriaQuery cq, StringBuffer buf) throws HibernateException { Criterion crit; if ( propertyValue!=null ) { boolean isString = propertyValue instanceof String; if ( isLikeEnabled && isString ) { crit = new LikeExpression( propertyName, ( String ) propertyValue, matchMode, escapeCharacter, isIgnoreCaseEnabled ); } else { crit = new SimpleExpression( propertyName, propertyValue, "=", isIgnoreCaseEnabled && isString ); } } else { crit = new NullExpression(propertyName); } String critCondition = crit.toSqlString(criteria, cq); if ( buf.length()>1 && critCondition.trim().length()>0 ) buf.append(" and "); buf.append(critCondition); } protected void appendComponentCondition( String path, Object component, CompositeType type, Criteria criteria, CriteriaQuery criteriaQuery, StringBuffer buf) throws HibernateException { if (component!=null) { String[] propertyNames = type.getPropertyNames(); Object[] values = type.getPropertyValues( component, getEntityMode(criteria, criteriaQuery) ); Type[] subtypes = type.getSubtypes(); for (int i=0; i<propertyNames.length; i++) { String subpath = StringHelper.qualify( path, propertyNames[i] ); Object value = values[i]; if ( isPropertyIncluded( value, subpath, subtypes[i] ) ) { Type subtype = subtypes[i]; if ( subtype.isComponentType() ) { appendComponentCondition( subpath, value, (CompositeType) subtype, criteria, criteriaQuery, buf ); } else { appendPropertyCondition( subpath, value, criteria, criteriaQuery, buf ); } } } } } }
And Criterion.java (no changes, just because Example assumes it is there)
/** * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Middleware LLC. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA * */ package org.hibernate.criterion; import java.io.Serializable; import org.hibernate.Criteria; import org.hibernate.HibernateException; import org.hibernate.engine.TypedValue; /** * An object-oriented representation of a query criterion that may be used * as a restriction in a <tt>Criteria</tt> query. * Built-in criterion types are provided by the <tt>Restrictions</tt> factory * class. This interface might be implemented by application classes that * define custom restriction criteria. * * @see Restrictions * @see Criteria * @author Gavin King */ public interface Criterion extends Serializable { /** * Render the SQL fragment * * @param criteria The local criteria * @param criteriaQuery The overal criteria query * * @return The generated SQL fragment * @throws org.hibernate.HibernateException Problem during rendering. */ public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException; /** * Return typed values for all parameters in the rendered SQL fragment * * @param criteria The local criteria * @param criteriaQuery The overal criteria query * * @return The types values (for binding) * @throws HibernateException Problem determining types. */ public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException; }
18. Do mvn jetty:run
19. Pull up Persons list (initially empty) http://localhost:8080/crudsqbe/persons
20. Click on “Create New” or “Add Some” http://localhost:8080/crudsqbe/person/edit
21. The edit form can be used create a new record or search using the QBE.
22. The links rendering the person names will pull up the particular record for edit/save.
22. Search field above the persons list will drive Lucene indexed text search.
23. Code (crudsqbe.zip) may be downloaded from Google Docs. File –> Download (Ctrl+S). If you have JDK, Maven setup as Steps 1-2 and database setup as per step 8, just do mvn jetty:run
after unzipping to, say, c:/projects/crudsqbe
.
24. Eclipse. These are just brief high-level instructions. From eclipse.org, download Eclipse IDE for Java EE Developers, 212 MB
Download eclipse-jee-indigo-SR2-win32-x86_64.zip
Use something like 7-zip to uncompress it into c:/eclipse, create shortcut to C:\eclipse\eclipse.exe
Change directory to projects/crudsqbe FIRST and THEN do the following:
mvn eclipse:clean mvn eclipse:eclipse -DdownloadSources=true
Now open eclipse using shortcut, make c:/projects your workspace, add crudsqbe as a java project. Should be good to go.
For even more agility, you may consider eclipse add-ons:
1. Run Jetty Run Plugin for eclipse. After installing, right click on crudsqbe project to simply run Jetty. Enjoy the live class reloading.
Everytime you touch a class or template, the server will reboot, with the code changes immediately verifiable.
2. TML Code Completion. Auto completes your template code.
3. SQL Explorer. View what is happening in your database.
25. Among several things needed to scale this application would be to create RESTful service using Hibernate, specifically, Data Access Object (DAO), to do all the CRUDS-QBE. This would make the API reusable across all pages, including the form interface, which is typically an Admin interface.