Author: epbernard
Date: 2007-09-12 23:48:51 -0400 (Wed, 12 Sep 2007)
New Revision: 14009
Added:
search/trunk/src/java/org/hibernate/search/annotations/ClassBridge.java
search/trunk/src/test/org/hibernate/search/test/bridge/CatFieldsClassBridge.java
search/trunk/src/test/org/hibernate/search/test/bridge/ClassBridgeTest.java
search/trunk/src/test/org/hibernate/search/test/bridge/Department.java
Modified:
search/trunk/src/java/org/hibernate/search/bridge/BridgeFactory.java
search/trunk/src/java/org/hibernate/search/engine/DocumentBuilder.java
search/trunk/src/test/org/hibernate/search/test/PurgeTest.java
Log:
HSEARCH-81 Add @ClassBridge support (John Griffin)
Added: search/trunk/src/java/org/hibernate/search/annotations/ClassBridge.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/annotations/ClassBridge.java
(rev 0)
+++ search/trunk/src/java/org/hibernate/search/annotations/ClassBridge.java 2007-09-13
03:48:51 UTC (rev 14009)
@@ -0,0 +1,61 @@
+package org.hibernate.search.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation allows a user to apply an implementation
+ * class to a Lucene document to manipulate it in any way
+ * the usersees fit.
+ *
+ * @author John Griffin
+ */
+@Retention( RetentionPolicy.RUNTIME )
+@Target( ElementType.TYPE )
+@Documented
+public @interface ClassBridge {
+ /**
+ * Field name, default to the JavaBean property name.
+ */
+ String name() default "";
+
+ /**
+ * Should the value be stored in the document.
+ * defaults to no.
+ */
+ Store store() default Store.NO;
+
+ /**
+ * Define an analyzer for the field, default to
+ * the inherited analyzer.
+ */
+ Analyzer analyzer() default @Analyzer;
+
+ /**
+ * Defines how the Field should be indexed
+ * defaults to tokenized.
+ */
+ Index index() default Index.TOKENIZED;
+
+ /**
+ * A float value of the amount of lucene defined
+ * boost to apply to a field.
+ */
+ Boost boost() default @Boost(value=1.0F);
+
+ /**
+ * User supplied class to manipulate document in
+ * whatever mysterious ways they wish to.
+ */
+ public Class impl() default void.class;
+
+ /**
+ * Array of fields to work with. The imnpl class
+ * above will work on these fields.
+ */
+ public Parameter[] params() default {};
+
+}
\ No newline at end of file
Modified: search/trunk/src/java/org/hibernate/search/bridge/BridgeFactory.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/bridge/BridgeFactory.java 2007-09-09
10:41:51 UTC (rev 14008)
+++ search/trunk/src/java/org/hibernate/search/bridge/BridgeFactory.java 2007-09-13
03:48:51 UTC (rev 14009)
@@ -23,6 +23,7 @@
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Parameter;
import org.hibernate.search.annotations.Field;
+import org.hibernate.search.annotations.ClassBridge;
import org.hibernate.search.SearchException;
import org.hibernate.search.util.BinderHelper;
import org.hibernate.annotations.common.reflection.XClass;
@@ -30,6 +31,7 @@
/**
* @author Emmanuel Bernard
+ * @author John Griffin
*/
public class BridgeFactory {
private static Map<String, FieldBridge> builtInBridges = new HashMap<String,
FieldBridge>();
@@ -83,7 +85,45 @@
builtInBridges.put( Date.class.getName(), DATE_MILLISECOND );
}
+
+ /**
+ * This extracts and instantiates the implementation class from a ClassBridge
+ * annotation.
+ *
+ * @param cb the ClassBridge
+ * @return FieldBridge
+ */
+ public static FieldBridge extractType(ClassBridge cb)
+ {
+ FieldBridge bridge = null;
+ if ( cb != null ) {
+ Class impl = cb.impl();
+
+ if (impl != null) {
+ try {
+ Object instance = impl.newInstance();
+ if ( FieldBridge.class.isAssignableFrom( impl ) ) {
+ bridge = (FieldBridge) instance;
+ }
+ if ( cb.params().length > 0 &&
ParameterizedBridge.class.isAssignableFrom( impl ) ) {
+ Map params = new HashMap( cb.params().length );
+ for ( Parameter param : cb.params() ) {
+ params.put( param.name(), param.value() );
+ }
+ ( (ParameterizedBridge) instance ).setParameterValues( params );
+ }
+ }
+ catch (Exception e) {
+ throw new HibernateException( "Unable to instantiate FieldBridge for " +
ClassBridge.class.getName(), e );
+ }
+ }
+ }
+ if ( bridge == null ) throw new SearchException( "Unable to guess FieldBridge for
" + ClassBridge.class.getName() );
+
+ return bridge;
+ }
+
public static FieldBridge guessType(Field field, XMember member) {
FieldBridge bridge = null;
org.hibernate.search.annotations.FieldBridge bridgeAnn;
Modified: search/trunk/src/java/org/hibernate/search/engine/DocumentBuilder.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/engine/DocumentBuilder.java 2007-09-09
10:41:51 UTC (rev 14008)
+++ search/trunk/src/java/org/hibernate/search/engine/DocumentBuilder.java 2007-09-13
03:48:51 UTC (rev 14009)
@@ -33,6 +33,7 @@
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.Store;
+import org.hibernate.search.annotations.ClassBridge;
import org.hibernate.search.backend.AddLuceneWork;
import org.hibernate.search.backend.DeleteLuceneWork;
import org.hibernate.search.backend.LuceneWork;
@@ -139,6 +140,11 @@
if ( analyzer != null ) {
propertiesMetadata.analyzer = analyzer;
}
+ // Check for any ClassBridge style of annotations.
+ ClassBridge classBridgeAnn = currClass.getAnnotation(ClassBridge.class);
+ if (classBridgeAnn != null) {
+ bindClassAnnotation(prefix, propertiesMetadata, classBridgeAnn);
+ }
//rejecting non properties because the object is loaded from Hibernate, so indexing a
non property does not make sense
List<XProperty> methods = currClass.getDeclaredProperties(
XClass.ACCESS_PROPERTY );
for (XProperty method : methods) {
@@ -279,6 +285,21 @@
}
}
+ private void bindClassAnnotation(String prefix, PropertiesMetadata propertiesMetadata,
ClassBridge ann) {
+ //FIXME name should be prefixed
+ String fieldName = prefix + ann.name();
+ propertiesMetadata.classNames.add( fieldName );
+ propertiesMetadata.classStores.add( getStore( ann.store() ) );
+ propertiesMetadata.classIndexes.add( getIndex( ann.index() ) );
+ propertiesMetadata.classBridges.add( BridgeFactory.extractType( ann ) );
+ propertiesMetadata.classBoosts.add( ann.boost().value() );
+
+ Analyzer analyzer = getAnalyzer( ann.analyzer() );
+ if ( analyzer == null ) analyzer = propertiesMetadata.analyzer;
+ if ( analyzer == null ) throw new AssertionFailure( "Analyzer should not be
undefined" );
+ this.analyzer.addScopedAnalyzer( fieldName, analyzer );
+ }
+
private void bindFieldAnnotation(XProperty member, PropertiesMetadata
propertiesMetadata, String prefix, org.hibernate.search.annotations.Field fieldAnn) {
setAccessible( member );
propertiesMetadata.fieldGetters.add( member );
@@ -487,6 +508,16 @@
if ( instance == null ) return;
//needed for field access: I cannot work in the proxied version
Object unproxiedInstance = unproxy( instance );
+ for (int i = 0; i < propertiesMetadata.classBridges.size(); i++) {
+ FieldBridge fb = propertiesMetadata.classBridges.get( i );
+
+ fb.set( propertiesMetadata.classNames.get(i),
+ unproxiedInstance,
+ doc,
+ propertiesMetadata.classStores.get(i),
+ propertiesMetadata.classIndexes.get(i),
+ propertiesMetadata.classBoosts.get(i));
+ }
for (int i = 0; i < propertiesMetadata.fieldNames.size(); i++) {
XMember member = propertiesMetadata.fieldGetters.get( i );
Object value = getMemberValue( unproxiedInstance, member );
@@ -677,6 +708,11 @@
public final List<PropertiesMetadata> embeddedPropertiesMetadata = new
ArrayList<PropertiesMetadata>();
public final List<Container> embeddedContainers = new
ArrayList<Container>();
public final List<XMember> containedInGetters = new ArrayList<XMember>();
+ public final List<String> classNames = new ArrayList<String>();
+ public final List<Field.Store> classStores = new ArrayList<Field.Store>();
+ public final List<Field.Index> classIndexes = new
ArrayList<Field.Index>();
+ public final List<FieldBridge> classBridges = new
ArrayList<FieldBridge>();
+ public final List<Float> classBoosts = new ArrayList<Float>();
public enum Container {
OBJECT,
Modified: search/trunk/src/test/org/hibernate/search/test/PurgeTest.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/PurgeTest.java 2007-09-09 10:41:51 UTC
(rev 14008)
+++ search/trunk/src/test/org/hibernate/search/test/PurgeTest.java 2007-09-13 03:48:51 UTC
(rev 14009)
@@ -88,11 +88,16 @@
tx = s.beginTransaction();
- Query query = parser.parse( "brand:Festina or brand:Seiko" );
+ Query query = parser.parse( "brand:Festina or brand:Seiko or brand:Longine or
brand:Rolex" );
org.hibernate.Query hibQuery = s.createFullTextQuery( query, Clock.class, Book.class
);
List results = hibQuery.list();
- assertEquals("incorrect test record count", 0, results.size());
+ assertEquals("class not completely purged", 0, results.size());
+ query = parser.parse( "summary:Festina or summary:gloire" );
+ hibQuery = s.createFullTextQuery( query, Clock.class, Book.class );
+ results = hibQuery.list();
+ assertEquals("incorrect class purged", 2, results.size());
+
for (Object element : s.createQuery( "from java.lang.Object" ).list())
s.delete( element );
tx.commit();
s.close();
Added: search/trunk/src/test/org/hibernate/search/test/bridge/CatFieldsClassBridge.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/bridge/CatFieldsClassBridge.java
(rev 0)
+++
search/trunk/src/test/org/hibernate/search/test/bridge/CatFieldsClassBridge.java 2007-09-13
03:48:51 UTC (rev 14009)
@@ -0,0 +1,42 @@
+package org.hibernate.search.test.bridge;
+
+import java.util.Map;
+
+import org.hibernate.search.bridge.StringBridge;
+import org.hibernate.search.bridge.ParameterizedBridge;
+import org.hibernate.search.bridge.FieldBridge;
+import org.hibernate.util.StringHelper;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+
+/**
+ * @author John Griffin
+ */
+public class CatFieldsClassBridge implements FieldBridge, ParameterizedBridge {
+
+ private String sepChar;
+
+ public void setParameterValues(Map parameters) {
+ this.sepChar = (String) parameters.get( "sepChar" );
+ }
+
+ public void set(String name, Object value, Document document, Field.Store store,
Field.Index index, Float boost) {
+ // In this particular class the name of the new field was passed
+ // from the name field of the ClassBridge Annotation. This is not
+ // a requirement. It just works that way in this instance. The
+ // actual name could be supplied by hard coding it below.
+ Department dep = (Department) value;
+ String fieldValue1 = dep.getBranch();
+ if ( fieldValue1 == null ) {
+ fieldValue1 = "";
+ }
+ String fieldValue2 = dep.getNetwork();
+ if ( fieldValue2 == null ) {
+ fieldValue2 = "";
+ }
+ String fieldValue = fieldValue1 + sepChar + fieldValue2;
+ Field field = new Field( name, fieldValue, store, index );
+ if ( boost != null ) field.setBoost( boost );
+ document.add( field );
+ }
+}
\ No newline at end of file
Added: search/trunk/src/test/org/hibernate/search/test/bridge/ClassBridgeTest.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/bridge/ClassBridgeTest.java
(rev 0)
+++ search/trunk/src/test/org/hibernate/search/test/bridge/ClassBridgeTest.java 2007-09-13
03:48:51 UTC (rev 14009)
@@ -0,0 +1,129 @@
+package org.hibernate.search.test.bridge;
+
+import java.util.List;
+
+import org.apache.lucene.analysis.SimpleAnalyzer;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.search.Query;
+import org.hibernate.Transaction;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.search.Environment;
+import org.hibernate.search.FullTextSession;
+import org.hibernate.search.Search;
+import org.hibernate.search.test.SearchTestCase;
+
+/**
+ * @author John Griffin
+ */
+public class ClassBridgeTest extends SearchTestCase {
+ /**
+ * This test checks for two fields being concatentated by the user-supplied
+ * CatFieldsClassBridge class which is specified as the implementation class
+ * in the ClassBridge annotation of the Department class.
+ *
+ * @throws Exception
+ */
+ public void testClassBridge() throws Exception {
+ org.hibernate.Session s = openSession();
+ Transaction tx = s.beginTransaction();
+ s.persist( getDept1() );
+ s.persist( getDept2() );
+ s.persist( getDept3() );
+ s.flush();
+ tx.commit();
+
+ tx = s.beginTransaction();
+ FullTextSession session = Search.createFullTextSession( s );
+
+ // The branchnetwork field is the concatenation of both
+ // the branch field and the network field of the Department
+ // class. This is in the Lucene document but not in the
+ // Department entity itself.
+ QueryParser parser = new QueryParser( "branchnetwork", new SimpleAnalyzer()
);
+
+ Query query = parser.parse( "branchnetwork:layton 2B" );
+ org.hibernate.search.FullTextQuery hibQuery = session.createFullTextQuery( query,
Department.class );
+ List result = hibQuery.list();
+ assertNotNull( result );
+ assertEquals( "incorrect entity returned, wrong network", "2B", (
(Department) result.get( 0 ) ).getNetwork() );
+ assertEquals( "incorrect entity returned, wrong branch", "Layton",
( (Department) result.get( 0 ) ).getBranch() );
+ assertEquals( "incorrect number of results returned", 1, result.size() );
+
+ // Partial match.
+ query = parser.parse( "branchnetwork:3c" );
+ hibQuery = session.createFullTextQuery( query, Department.class );
+ result = hibQuery.list();
+ assertNotNull( result );
+ assertEquals( "incorrect entity returned, wrong network", "3C", (
(Department) result.get( 0 ) ).getNetwork() );
+ assertEquals( "incorrect entity returned, wrong branch", "West
Valley", ( (Department) result.get( 0 ) ).getBranch() );
+ assertEquals( "incorrect number of results returned", 1, result.size() );
+
+ // No data cross-ups .
+ query = parser.parse( "branchnetwork:Kent Lewin" );
+ hibQuery = session.createFullTextQuery( query, Department.class );
+ result = hibQuery.list();
+ assertNotNull( result );
+ assertTrue( "problem with field cross-ups", result.size() == 0 );
+
+ // Non-ClassBridge field.
+ parser = new QueryParser( "branchHead", new SimpleAnalyzer() );
+ query = parser.parse( "branchHead:Kent Lewin" );
+ hibQuery = session.createFullTextQuery( query, Department.class );
+ result = hibQuery.list();
+ assertNotNull( result );
+ assertTrue( "incorrect entity returned, wrong branch head", result.size() ==
1 );
+ assertEquals("incorrect entity returned", "Kent Lewin", (
(Department) result.get( 0 ) ).getBranchHead());
+
+ //cleanup
+ for (Object element : s.createQuery( "from " + Department.class.getName()
).list()) s.delete( element );
+ tx.commit();
+ s.close();
+ }
+
+ private Department getDept1() {
+ Department dept = new Department();
+
+// dept.setId( 1000 );
+ dept.setBranch( "Salt Lake City" );
+ dept.setBranchHead( "Kent Lewin" );
+ dept.setMaxEmployees( 100 );
+ dept.setNetwork( "1A" );
+
+ return dept;
+ }
+
+ private Department getDept2() {
+ Department dept = new Department();
+
+// dept.setId( 1001 );
+ dept.setBranch( "Layton" );
+ dept.setBranchHead( "Terry Poperszky" );
+ dept.setMaxEmployees( 20 );
+ dept.setNetwork( "2B" );
+
+ return dept;
+ }
+
+ private Department getDept3() {
+ Department dept = new Department();
+
+// dept.setId( 1002 );
+ dept.setBranch( "West Valley" );
+ dept.setBranchHead( "Pat Kelley" );
+ dept.setMaxEmployees( 15 );
+ dept.setNetwork( "3C" );
+
+ return dept;
+ }
+
+ protected Class[] getMappings() {
+ return new Class[] {
+ Department.class
+ };
+ }
+
+ protected void configure(Configuration cfg) {
+ super.configure( cfg );
+ cfg.setProperty( Environment.ANALYZER_CLASS, SimpleAnalyzer.class.getName() );
+ }
+}
\ No newline at end of file
Added: search/trunk/src/test/org/hibernate/search/test/bridge/Department.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/bridge/Department.java
(rev 0)
+++ search/trunk/src/test/org/hibernate/search/test/bridge/Department.java 2007-09-13
03:48:51 UTC (rev 14009)
@@ -0,0 +1,79 @@
+package org.hibernate.search.test.bridge;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+import org.hibernate.search.annotations.ClassBridge;
+import org.hibernate.search.annotations.DocumentId;
+import org.hibernate.search.annotations.Field;
+import org.hibernate.search.annotations.Index;
+import org.hibernate.search.annotations.Indexed;
+import org.hibernate.search.annotations.Parameter;
+import org.hibernate.search.annotations.Store;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+
+/**
+ * @author John Griffin
+ */
+@Entity
+@Indexed
+@ClassBridge(name="branchnetwork",
+ index=Index.TOKENIZED,
+ store=Store.YES,
+ impl = CatFieldsClassBridge.class,
+ params = @Parameter( name="sepChar", value=" " ) )
+public class Department {
+ private int id;
+ private String network;
+ private String branchHead;
+ private String branch;
+ private Integer maxEmployees;
+
+ @Id
+ @GeneratedValue
+ @DocumentId
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ @Field(index=Index.TOKENIZED, store=Store.YES)
+ public String getBranchHead() {
+ return branchHead;
+ }
+
+ public void setBranchHead(String branchHead) {
+ this.branchHead = branchHead;
+ }
+
+ @Field(index=Index.TOKENIZED, store=Store.YES)
+ public String getNetwork() {
+ return network;
+ }
+
+ public void setNetwork(String network) {
+ this.network = network;
+ }
+
+ @Field(index=Index.TOKENIZED, store=Store.YES)
+ public String getBranch() {
+ return branch;
+ }
+
+ public void setBranch(String branch) {
+ this.branch = branch;
+ }
+
+ @Field(index=Index.UN_TOKENIZED, store=Store.YES)
+ public Integer getMaxEmployees() {
+ return maxEmployees;
+ }
+
+ public void setMaxEmployees(Integer maxEmployees) {
+ this.maxEmployees = maxEmployees;
+ }
+}
\ No newline at end of file