Author: hardy.ferentschik
Date: 2008-11-19 09:17:48 -0500 (Wed, 19 Nov 2008)
New Revision: 15592
Added:
search/trunk/src/test/org/hibernate/search/test/inheritance/Fish.java
Modified:
search/trunk/src/java/org/hibernate/search/engine/MultiClassesQueryLoader.java
search/trunk/src/java/org/hibernate/search/engine/ProjectionLoader.java
search/trunk/src/java/org/hibernate/search/engine/SearchFactoryImplementor.java
search/trunk/src/java/org/hibernate/search/impl/FullTextSessionImpl.java
search/trunk/src/java/org/hibernate/search/impl/SearchFactoryImpl.java
search/trunk/src/java/org/hibernate/search/query/FullTextQueryImpl.java
search/trunk/src/test/org/hibernate/search/test/SearchTestCase.java
search/trunk/src/test/org/hibernate/search/test/filter/FilterTest.java
search/trunk/src/test/org/hibernate/search/test/inheritance/Animal.java
search/trunk/src/test/org/hibernate/search/test/inheritance/Being.java
search/trunk/src/test/org/hibernate/search/test/inheritance/InheritanceTest.java
search/trunk/src/test/org/hibernate/search/test/inheritance/Mammal.java
Log:
HSEARCH-160 HSEARCH-265
- Added new data structure in order to support polymorphic queries
- Added warning for abstract classes annotated with @Indexed (no DocumentBuilder will be
created)
Modified: search/trunk/src/java/org/hibernate/search/engine/MultiClassesQueryLoader.java
===================================================================
---
search/trunk/src/java/org/hibernate/search/engine/MultiClassesQueryLoader.java 2008-11-19
14:11:26 UTC (rev 15591)
+++
search/trunk/src/java/org/hibernate/search/engine/MultiClassesQueryLoader.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -1,16 +1,15 @@
// $Id$
package org.hibernate.search.engine;
-import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.HashMap;
-import java.util.Arrays;
-import org.hibernate.Session;
import org.hibernate.Criteria;
+import org.hibernate.Session;
import org.hibernate.annotations.common.AssertionFailure;
/**
@@ -30,13 +29,12 @@
this.objectLoader.init( session, searchFactoryImplementor );
}
- public void setEntityTypes(Class[] entityTypes) {
- List<Class> safeEntityTypes;
+ public void setEntityTypes(Set<Class<?>> entityTypes) {
+ List<Class<?>> safeEntityTypes = new ArrayList<Class<?>>();
//TODO should we go find the root entity for a given class rather than just checking
for it's root status?
// root entity could lead to quite inefficient queries in Hibernate when using
table per class
- if ( entityTypes.length == 0 ) {
+ if ( entityTypes.size() == 0 ) {
//support all classes
- safeEntityTypes = new ArrayList<Class>();
for( Map.Entry<Class<?>, DocumentBuilder<?>> entry :
searchFactoryImplementor.getDocumentBuilders().entrySet() ) {
//get only root entities to limit queries
if ( entry.getValue().isRoot() ) {
@@ -45,7 +43,7 @@
}
}
else {
- safeEntityTypes = Arrays.asList(entityTypes);
+ safeEntityTypes.addAll(entityTypes);
}
entityMatadata = new ArrayList<RootEntityMetadata>( safeEntityTypes.size() );
for (Class clazz : safeEntityTypes) {
Modified: search/trunk/src/java/org/hibernate/search/engine/ProjectionLoader.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/engine/ProjectionLoader.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/java/org/hibernate/search/engine/ProjectionLoader.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import org.hibernate.Session;
import org.hibernate.transform.ResultTransformer;
@@ -21,7 +22,7 @@
private Boolean projectThis;
private ResultTransformer transformer;
private String[] aliases;
- private Class[] entityTypes;
+ private Set<Class<?>> entityTypes;
public void init(Session session, SearchFactoryImplementor searchFactoryImplementor) {
this.session = session;
@@ -34,7 +35,7 @@
this.aliases = aliases;
}
- public void setEntityTypes(Class[] entityTypes) {
+ public void setEntityTypes(Set<Class<?>> entityTypes) {
this.entityTypes = entityTypes;
}
Modified: search/trunk/src/java/org/hibernate/search/engine/SearchFactoryImplementor.java
===================================================================
---
search/trunk/src/java/org/hibernate/search/engine/SearchFactoryImplementor.java 2008-11-19
14:11:26 UTC (rev 15591)
+++
search/trunk/src/java/org/hibernate/search/engine/SearchFactoryImplementor.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -58,5 +58,7 @@
void addDirectoryProvider(DirectoryProvider<?> provider);
- int getFilterCacheBitResultsSize();
+ int getFilterCacheBitResultsSize();
+
+ Set<Class<?>> getIndexedTypesPolymorphic(Class<?>[] classes);
}
Modified: search/trunk/src/java/org/hibernate/search/impl/FullTextSessionImpl.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/impl/FullTextSessionImpl.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/java/org/hibernate/search/impl/FullTextSessionImpl.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -109,30 +109,34 @@
// accessing the document builders is not strictly necessary but a small optimization
plus let's make sure the
// client didn't mess something up.
SearchFactoryImplementor searchFactoryImplementor = getSearchFactoryImplementor();
- DocumentBuilder builder = searchFactoryImplementor.getDocumentBuilder( entityType );
- if ( builder == null ) {
- String msg = "Entity to index is not an @Indexed entity: " +
entityType.getName();
- throw new IllegalArgumentException( msg );
- }
+ Set<Class<?>> targetedClasses =
searchFactoryImplementor.getIndexedTypesPolymorphic( new Class[] {entityType} );
- Work<T> work;
- if ( id == null ) {
- // purge the main entity
- work = new Work<T>( entityType, id, WorkType.PURGE_ALL );
- searchFactoryImplementor.getWorker().performWork( work, transactionContext );
+ for ( Class clazz : targetedClasses ) {
+ DocumentBuilder builder = searchFactoryImplementor.getDocumentBuilder( clazz );
+ if ( builder == null ) {
+ String msg = "Entity to index is not an @Indexed entity: " +
clazz.getName();
+ throw new IllegalArgumentException( msg );
+ }
- // purge the subclasses
- Set<Class<?>> subClasses = builder.getMappedSubclasses();
- for ( Class clazz : subClasses ) {
- @SuppressWarnings( "unchecked" )
- Work subClassWork = new Work( clazz, id, WorkType.PURGE_ALL );
- searchFactoryImplementor.getWorker().performWork( subClassWork, transactionContext
);
+ Work<T> work;
+ if ( id == null ) {
+ // purge the main entity
+ work = new Work<T>( clazz, id, WorkType.PURGE_ALL );
+ searchFactoryImplementor.getWorker().performWork( work, transactionContext );
+
+ // purge the subclasses
+ Set<Class<?>> subClasses = builder.getMappedSubclasses();
+ for ( Class subClazz : subClasses ) {
+ @SuppressWarnings( "unchecked" )
+ Work subClassWork = new Work( subClazz, id, WorkType.PURGE_ALL );
+ searchFactoryImplementor.getWorker().performWork( subClassWork, transactionContext
);
+ }
}
+ else {
+ work = new Work<T>( clazz, id, WorkType.PURGE );
+ searchFactoryImplementor.getWorker().performWork( work, transactionContext );
+ }
}
- else {
- work = new Work<T>( entityType, id, WorkType.PURGE );
- searchFactoryImplementor.getWorker().performWork( work, transactionContext );
- }
}
/**
Modified: search/trunk/src/java/org/hibernate/search/impl/SearchFactoryImpl.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/impl/SearchFactoryImpl.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/java/org/hibernate/search/impl/SearchFactoryImpl.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -12,6 +12,7 @@
import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
@@ -75,8 +76,11 @@
private Map<String, Analyzer> analyzers;
private final AtomicBoolean stopped = new AtomicBoolean( false );
private final int cacheBitResultsSize;
+
+ private final PolymorphicIndexHierarchy indexHierarchy = new
PolymorphicIndexHierarchy();
+
/*
- * used as a barrier (piggyback usage) between initialization and subsequent usage of
searchFactory in different threads
+ * Used as a barrier (piggyback usage) between initialization and subsequent usage of
searchFactory in different threads
* this is due to our use of the initialize pattern is a few areas
* subsequent reads on volatiles should be very cheap on most platform especially since
we don't write after init
*
@@ -90,12 +94,13 @@
* Each directory provider (index) can have its own performance settings.
*/
private Map<DirectoryProvider, LuceneIndexingParameters> dirProviderIndexingParams
=
- new HashMap<DirectoryProvider, LuceneIndexingParameters>();
+ new HashMap<DirectoryProvider, LuceneIndexingParameters>();
private final String indexingStrategy;
public BackendQueueProcessorFactory getBackendQueueProcessorFactory() {
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return backendQueueProcessorFactory;
}
@@ -113,56 +118,60 @@
initDocumentBuilders( cfg, reflectionManager );
Set<Class<?>> indexedClasses = documentBuilders.keySet();
- for (DocumentBuilder builder : documentBuilders.values()) {
+ for ( DocumentBuilder builder : documentBuilders.values() ) {
builder.postInitialize( indexedClasses );
}
//not really necessary today
- for (DocumentBuilder builder : containedInOnlyBuilders.values()) {
+ for ( DocumentBuilder builder : containedInOnlyBuilders.values() ) {
builder.postInitialize( indexedClasses );
}
this.worker = WorkerFactory.createWorker( cfg, this );
this.readerProvider = ReaderProviderFactory.createReaderProvider( cfg, this );
this.filterCachingStrategy = buildFilterCachingStrategy( cfg.getProperties() );
- this.cacheBitResultsSize = ConfigurationParseHelper.getIntValue( cfg.getProperties(),
Environment.CACHE_BIT_RESULT_SIZE, CachingWrapperFilter.DEFAULT_SIZE );
+ this.cacheBitResultsSize = ConfigurationParseHelper.getIntValue(
+ cfg.getProperties(), Environment.CACHE_BIT_RESULT_SIZE,
CachingWrapperFilter.DEFAULT_SIZE
+ );
this.barrier = 1; //write barrier
}
private static String defineIndexingStrategy(SearchConfiguration cfg) {
String indexingStrategy = cfg.getProperties().getProperty(
Environment.INDEXING_STRATEGY, "event" );
- if ( ! ("event".equals( indexingStrategy ) || "manual".equals(
indexingStrategy ) ) ) {
+ if ( !( "event".equals( indexingStrategy ) || "manual".equals(
indexingStrategy ) ) ) {
throw new SearchException( Environment.INDEXING_STRATEGY + " unknown: " +
indexingStrategy );
}
return indexingStrategy;
}
public String getIndexingStrategy() {
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return indexingStrategy;
}
public void close() {
- if (barrier != 0) { } //read barrier
- if ( stopped.compareAndSet( false, true) ) {
+ if ( barrier != 0 ) {
+ } //read barrier
+ if ( stopped.compareAndSet( false, true ) ) {
try {
worker.close();
}
- catch (Exception e) {
+ catch ( Exception e ) {
log.error( "Worker raises an exception on close()", e );
}
try {
readerProvider.destroy();
}
- catch (Exception e) {
+ catch ( Exception e ) {
log.error( "ReaderProvider raises an exception on destroy()", e );
}
//TODO move to DirectoryProviderFactory for cleaner
- for (DirectoryProvider dp : getDirectoryProviders() ) {
+ for ( DirectoryProvider dp : getDirectoryProviders() ) {
try {
dp.stop();
}
- catch (Exception e) {
+ catch ( Exception e ) {
log.error( "DirectoryProvider raises an exception on stop() ", e );
}
}
@@ -171,17 +180,18 @@
public void addClassToDirectoryProvider(Class<?> clazz, DirectoryProvider<?>
directoryProvider) {
//no need to set a read barrier, we only use this class in the init thread
- DirectoryProviderData data = dirProviderData.get(directoryProvider);
- if (data == null) {
+ DirectoryProviderData data = dirProviderData.get( directoryProvider );
+ if ( data == null ) {
data = new DirectoryProviderData();
dirProviderData.put( directoryProvider, data );
}
- data.classes.add(clazz);
+ data.classes.add( clazz );
}
public Set<Class<?>>
getClassesInDirectoryProvider(DirectoryProvider<?> directoryProvider) {
- if (barrier != 0) { } //read barrier
- return Collections.unmodifiableSet( dirProviderData.get(directoryProvider).classes );
+ if ( barrier != 0 ) {
+ } //read barrier
+ return Collections.unmodifiableSet( dirProviderData.get( directoryProvider ).classes
);
}
private void bindFilterDefs(XClass mappedXClass) {
@@ -190,7 +200,7 @@
bindFilterDef( defAnn, mappedXClass );
}
FullTextFilterDefs defsAnn = mappedXClass.getAnnotation( FullTextFilterDefs.class );
- if (defsAnn != null) {
+ if ( defsAnn != null ) {
for ( FullTextFilterDef def : defsAnn.value() ) {
bindFilterDef( def, mappedXClass );
}
@@ -199,35 +209,45 @@
private void bindFilterDef(FullTextFilterDef defAnn, XClass mappedXClass) {
if ( filterDefinitions.containsKey( defAnn.name() ) ) {
- throw new SearchException("Multiple definition of @FullTextFilterDef.name="
+ defAnn.name() + ": "
- + mappedXClass.getName() );
+ throw new SearchException(
+ "Multiple definition of @FullTextFilterDef.name=" + defAnn.name() +
": "
+ + mappedXClass.getName()
+ );
}
- FilterDef filterDef = new FilterDef(defAnn);
+ FilterDef filterDef = new FilterDef( defAnn );
try {
filterDef.getImpl().newInstance();
}
- catch (IllegalAccessException e) {
- throw new SearchException("Unable to create Filter class: " +
filterDef.getImpl().getName(), e);
+ catch ( IllegalAccessException e ) {
+ throw new SearchException( "Unable to create Filter class: " +
filterDef.getImpl().getName(), e );
}
- catch (InstantiationException e) {
- throw new SearchException("Unable to create Filter class: " +
filterDef.getImpl().getName(), e);
+ catch ( InstantiationException e ) {
+ throw new SearchException( "Unable to create Filter class: " +
filterDef.getImpl().getName(), e );
}
for ( Method method : filterDef.getImpl().getMethods() ) {
if ( method.isAnnotationPresent( Factory.class ) ) {
if ( filterDef.getFactoryMethod() != null ) {
- throw new SearchException("Multiple @Factory methods found" +
defAnn.name() + ": "
- + filterDef.getImpl().getName() + "." + method.getName() );
+ throw new SearchException(
+ "Multiple @Factory methods found" + defAnn.name() + ": "
+ + filterDef.getImpl().getName() + "." + method.getName()
+ );
}
- if ( !method.isAccessible() ) method.setAccessible( true );
+ if ( !method.isAccessible() ) {
+ method.setAccessible( true );
+ }
filterDef.setFactoryMethod( method );
}
if ( method.isAnnotationPresent( Key.class ) ) {
if ( filterDef.getKeyMethod() != null ) {
- throw new SearchException("Multiple @Key methods found" + defAnn.name() +
": "
- + filterDef.getImpl().getName() + "." + method.getName() );
+ throw new SearchException(
+ "Multiple @Key methods found" + defAnn.name() + ": "
+ + filterDef.getImpl().getName() + "." + method.getName()
+ );
}
- if ( !method.isAccessible() ) method.setAccessible( true );
+ if ( !method.isAccessible() ) {
+ method.setAccessible( true );
+ }
filterDef.setKeyMethod( method );
}
@@ -241,36 +261,41 @@
public Map<Class<?>, DocumentBuilder<?>> getDocumentBuilders() {
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return documentBuilders;
}
- @SuppressWarnings( "unchecked" )
+ @SuppressWarnings("unchecked")
public <T> DocumentBuilder<T> getDocumentBuilder(Class<T> entityType)
{
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return ( DocumentBuilder<T> ) documentBuilders.get( entityType );
}
- @SuppressWarnings( "unchecked" )
+ @SuppressWarnings("unchecked")
public <T> DocumentBuilder<T> getContainedInOnlyBuilder(Class<T>
entityType) {
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return ( DocumentBuilder<T> ) containedInOnlyBuilders.get( entityType );
}
public Set<DirectoryProvider<?>> getDirectoryProviders() {
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return this.dirProviderData.keySet();
}
public Worker getWorker() {
- if (barrier != 0) { } //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return worker;
}
public void addOptimizerStrategy(DirectoryProvider<?> provider, OptimizerStrategy
optimizerStrategy) {
//no need to set a read barrier, we run this method on the init thread
- DirectoryProviderData data = dirProviderData.get(provider);
- if (data == null) {
+ DirectoryProviderData data = dirProviderData.get( provider );
+ if ( data == null ) {
data = new DirectoryProviderData();
dirProviderData.put( provider, data );
}
@@ -283,63 +308,74 @@
}
public OptimizerStrategy getOptimizerStrategy(DirectoryProvider<?> provider) {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return dirProviderData.get( provider ).optimizerStrategy;
}
- public LuceneIndexingParameters getIndexingParameters(DirectoryProvider<?>
provider ) {
- if (barrier != 0) {} //read barrier
+ public LuceneIndexingParameters getIndexingParameters(DirectoryProvider<?>
provider) {
+ if ( barrier != 0 ) {
+ } //read barrier
return dirProviderIndexingParams.get( provider );
}
public ReaderProvider getReaderProvider() {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return readerProvider;
}
public DirectoryProvider[] getDirectoryProviders(Class<?> entity) {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
DocumentBuilder<?> documentBuilder = getDocumentBuilder( entity );
return documentBuilder == null ? null : documentBuilder.getDirectoryProviders();
}
public void optimize() {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
Set<Class<?>> clazzs = getDocumentBuilders().keySet();
- for (Class clazz : clazzs) {
+ for ( Class clazz : clazzs ) {
optimize( clazz );
}
}
public void optimize(Class entityType) {
- if (barrier != 0) {} //read barrier
- if ( ! getDocumentBuilders().containsKey( entityType ) ) {
- throw new SearchException("Entity not indexed: " + entityType);
+ if ( barrier != 0 ) {
+ } //read barrier
+ if ( !getDocumentBuilders().containsKey( entityType ) ) {
+ throw new SearchException( "Entity not indexed: " + entityType );
}
- List<LuceneWork> queue = new ArrayList<LuceneWork>(1);
+ List<LuceneWork> queue = new ArrayList<LuceneWork>( 1 );
queue.add( new OptimizeLuceneWork( entityType ) );
getBackendQueueProcessorFactory().getProcessor( queue ).run();
}
public Analyzer getAnalyzer(String name) {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
final Analyzer analyzer = analyzers.get( name );
- if ( analyzer == null) throw new SearchException( "Unknown Analyzer definition:
" + name);
+ if ( analyzer == null ) {
+ throw new SearchException( "Unknown Analyzer definition: " + name );
+ }
return analyzer;
}
-
+
public Analyzer getAnalyzer(Class clazz) {
- if ( clazz == null) {
+ if ( clazz == null ) {
throw new IllegalArgumentException( "A class has to be specified for retrieving a
scoped analyzer" );
}
-
+
DocumentBuilder<?> builder = documentBuilders.get( clazz );
if ( builder == null ) {
- throw new IllegalArgumentException( "Entity for which to retrieve the scoped
analyzer is not an @Indexed entity: " + clazz.getName() );
+ throw new IllegalArgumentException(
+ "Entity for which to retrieve the scoped analyzer is not an @Indexed entity:
" + clazz.getName()
+ );
}
-
+
return builder.getAnalyzer();
- }
+ }
private void initDocumentBuilders(SearchConfiguration cfg, ReflectionManager
reflectionManager) {
InitContext context = new InitContext( cfg );
@@ -348,35 +384,48 @@
while ( iter.hasNext() ) {
Class mappedClass = iter.next();
- if (mappedClass != null) {
- XClass mappedXClass = reflectionManager.toXClass(mappedClass);
- if ( mappedXClass != null) {
- if ( mappedXClass.isAnnotationPresent( Indexed.class ) ) {
- DirectoryProviderFactory.DirectoryProviders providers =
factory.createDirectoryProviders( mappedXClass, cfg, this, reflectionManager );
- //FIXME DocumentBuilder needs to be built by a helper method receiving
Class<T> to infer T properly
- //XClass unfortunately is not (yet) genericized: TODO?
- final DocumentBuilder<?> documentBuilder = new DocumentBuilder(
- mappedXClass, context, providers.getProviders(),
providers.getSelectionStrategy(),
- reflectionManager
- );
+ if ( mappedClass == null ) {
+ continue;
+ }
+ @SuppressWarnings( "unchecked" )
+ XClass mappedXClass = reflectionManager.toXClass( mappedClass );
+ if ( mappedXClass == null ) {
+ continue;
+ }
- documentBuilders.put( mappedClass, documentBuilder );
- }
- else {
- //FIXME DocumentBuilder needs to be built by a helper method receiving
Class<T> to infer T properly
- //XClass unfortunately is not (yet) genericized: TODO?
- final DocumentBuilder<?> documentBuilder = new DocumentBuilder(
- mappedXClass, context, reflectionManager
- );
- //TODO enhance that, I don't like to expose EntityState
- if ( documentBuilder.getEntityState() != EntityState.NON_INDEXABLE ) {
- containedInOnlyBuilders.put( mappedClass, documentBuilder );
- }
- }
- bindFilterDefs(mappedXClass);
- //TODO should analyzer def for classes at tyher sqme level???
+ if ( mappedXClass.isAnnotationPresent( Indexed.class ) ) {
+
+ if ( mappedXClass.isAbstract() ) {
+ log.warn( "Abstract classes can never insert index documents. Remove
@Indexed." );
+ continue;
}
+
+ DirectoryProviderFactory.DirectoryProviders providers =
factory.createDirectoryProviders(
+ mappedXClass, cfg, this, reflectionManager
+ );
+ //FIXME DocumentBuilder needs to be built by a helper method receiving Class<T>
to infer T properly
+ //XClass unfortunately is not (yet) genericized: TODO?
+ final DocumentBuilder<?> documentBuilder = new DocumentBuilder(
+ mappedXClass, context, providers.getProviders(), providers.getSelectionStrategy(),
+ reflectionManager
+ );
+
+ indexHierarchy.addIndexedClass( mappedClass );
+ documentBuilders.put( mappedClass, documentBuilder );
}
+ else {
+ //FIXME DocumentBuilder needs to be built by a helper method receiving Class<T>
to infer T properly
+ //XClass unfortunately is not (yet) genericized: TODO?
+ final DocumentBuilder<?> documentBuilder = new DocumentBuilder(
+ mappedXClass, context, reflectionManager
+ );
+ //TODO enhance that, I don't like to expose EntityState
+ if ( documentBuilder.getEntityState() != EntityState.NON_INDEXABLE ) {
+ containedInOnlyBuilders.put( mappedClass, documentBuilder );
+ }
+ }
+ bindFilterDefs( mappedXClass );
+ //TODO should analyzer def for classes at tyher sqme level???
}
analyzers = context.initLazyAnalyzers();
factory.startDirectoryProviders();
@@ -390,16 +439,21 @@
}
else {
try {
- Class filterCachingStrategyClass =
org.hibernate.annotations.common.util.ReflectHelper.classForName( impl,
SearchFactoryImpl.class );
- filterCachingStrategy = (FilterCachingStrategy)
filterCachingStrategyClass.newInstance();
+ Class filterCachingStrategyClass = org.hibernate
+ .annotations
+ .common
+ .util
+ .ReflectHelper
+ .classForName( impl, SearchFactoryImpl.class );
+ filterCachingStrategy = ( FilterCachingStrategy )
filterCachingStrategyClass.newInstance();
}
- catch (ClassNotFoundException e) {
+ catch ( ClassNotFoundException e ) {
throw new SearchException( "Unable to find filterCachingStrategy class: " +
impl, e );
}
- catch (IllegalAccessException e) {
+ catch ( IllegalAccessException e ) {
throw new SearchException( "Unable to instantiate filterCachingStrategy class:
" + impl, e );
}
- catch (InstantiationException e) {
+ catch ( InstantiationException e ) {
throw new SearchException( "Unable to instantiate filterCachingStrategy class:
" + impl, e );
}
}
@@ -408,23 +462,26 @@
}
public FilterCachingStrategy getFilterCachingStrategy() {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return filterCachingStrategy;
}
public FilterDef getFilterDefinition(String name) {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return filterDefinitions.get( name );
}
private static class DirectoryProviderData {
public final ReentrantLock dirLock = new ReentrantLock();
public OptimizerStrategy optimizerStrategy;
- public Set<Class<?>> classes = new HashSet<Class<?>>(2);
+ public Set<Class<?>> classes = new HashSet<Class<?>>( 2 );
}
public ReentrantLock getDirectoryProviderLock(DirectoryProvider<?> dp) {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return this.dirProviderData.get( dp ).dirLock;
}
@@ -434,7 +491,58 @@
}
public int getFilterCacheBitResultsSize() {
- if (barrier != 0) {} //read barrier
+ if ( barrier != 0 ) {
+ } //read barrier
return cacheBitResultsSize;
}
+
+ public Set<Class<?>> getIndexedTypesPolymorphic(Class<?>[] classes) {
+ return indexHierarchy.getIndexedClasses( classes );
+ }
+
+ /**
+ * Helper class which keeps track of all super classes and interfaces of the indexed
entities.
+ */
+ private static class PolymorphicIndexHierarchy {
+ private Map<Class<?>, Set<Class<?>>> classToIndexedClass;
+
+ PolymorphicIndexHierarchy() {
+ classToIndexedClass = new HashMap<Class<?>, Set<Class<?>>>();
+ }
+
+ void addIndexedClass(Class indexedClass) {
+ addClass( indexedClass, indexedClass );
+ Class superClass = indexedClass.getSuperclass();
+ while ( superClass != null ) {
+ addClass( superClass, indexedClass );
+ superClass = superClass.getSuperclass();
+ }
+ for ( Class clazz : indexedClass.getInterfaces() ) {
+ addClass( clazz, indexedClass );
+ }
+ }
+
+ private void addClass(Class superclass, Class indexedClass) {
+ Set<Class<?>> classesSet = classToIndexedClass.get( superclass );
+ if ( classesSet == null ) {
+ classesSet = new HashSet<Class<?>>();
+ classToIndexedClass.put( superclass, classesSet );
+ }
+ classesSet.add( indexedClass );
+ }
+
+ Set<Class<?>> getIndexedClasses(Class<?>[] classes) {
+ Set<Class<?>> classesSet = new HashSet<Class<?>>();
+ for ( Class clazz : classes ) {
+ Set<Class<?>> set = classToIndexedClass.get( clazz );
+ if ( set != null ) {
+ classesSet.addAll( set );
+ }
+ }
+ if ( log.isTraceEnabled() ) {
+ log.trace( "Targeted indexed classes for {}: {}", Arrays.toString( classes
), classesSet );
+ }
+ return classesSet;
+ }
+ }
}
Modified: search/trunk/src/java/org/hibernate/search/query/FullTextQueryImpl.java
===================================================================
--- search/trunk/src/java/org/hibernate/search/query/FullTextQueryImpl.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/java/org/hibernate/search/query/FullTextQueryImpl.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -72,7 +72,7 @@
public class FullTextQueryImpl extends AbstractQueryImpl implements FullTextQuery {
private static final Logger log = LoggerFactory.make();
private final org.apache.lucene.search.Query luceneQuery;
- private Class<?>[] classes;
+ private Set<Class<?>> targetedEntities;
private Set<Class<?>> classesAndSubclasses;
//optimization: if we can avoid the filter clause (we can most of the time) do it as it
has a significant perf impact
private boolean needClassFilterClause;
@@ -93,17 +93,17 @@
/**
* Constructs a <code>FullTextQueryImpl</code> instance.
*
- * @param query The Lucene query
- * @param classes Array of classes (must be immutable) used to filter the results to
the given class types.
+ * @param query The Lucene query.
+ * @param classes Array of classes (must be immutable) used to filter the results to the
given class types.
* @param session Access to the Hibernate session.
- * @param parameterMetadata Additional query metadata.
+ * @param parameterMetadata Additional query metadata.
*/
public FullTextQueryImpl(org.apache.lucene.search.Query query, Class[] classes,
SessionImplementor session,
ParameterMetadata parameterMetadata) {
//TODO handle flushMode
super( query.toString(), null, session, parameterMetadata );
this.luceneQuery = query;
- this.classes = classes;
+ this.targetedEntities = getSearchFactoryImplementor().getIndexedTypesPolymorphic(
classes );
}
/**
@@ -132,7 +132,7 @@
//user stop using it
//scrollable is better in this area
- SearchFactoryImplementor searchFactoryImplementor =
ContextHelper.getSearchFactoryBySFI( session );
+ SearchFactoryImplementor searchFactoryImplementor = getSearchFactoryImplementor();
//find the directories
IndexSearcher searcher = buildSearcher( searchFactoryImplementor );
if ( searcher == null ) {
@@ -173,22 +173,23 @@
if ( indexProjection != null ) {
ProjectionLoader loader = new ProjectionLoader();
loader.init( session, searchFactoryImplementor, resultTransformer, indexProjection );
- loader.setEntityTypes( classes );
+ loader.setEntityTypes( targetedEntities );
return loader;
}
if ( criteria != null ) {
- if ( classes.length > 1 ) {
+ if ( targetedEntities.size() > 1 ) {
throw new SearchException( "Cannot mix criteria and multiple entity types"
);
}
if ( criteria instanceof CriteriaImpl ) {
String targetEntity = ( ( CriteriaImpl ) criteria ).getEntityOrClassName();
- if ( classes.length == 1 && !classes[0].getName().equals( targetEntity ) ) {
+ if ( targetedEntities.size() == 1 &&
!targetedEntities.iterator().next().getName().equals( targetEntity ) ) {
throw new SearchException( "Criteria query entity should match query
entity" );
}
else {
try {
Class entityType = ReflectHelper.classForName( targetEntity );
- classes = new Class[] { entityType };
+ targetedEntities = new HashSet<Class<?>>( 1 );
+ targetedEntities.add( entityType );
}
catch ( ClassNotFoundException e ) {
throw new SearchException( "Unable to load entity class from criteria: "
+ targetEntity, e );
@@ -197,20 +198,20 @@
}
QueryLoader loader = new QueryLoader();
loader.init( session, searchFactoryImplementor );
- loader.setEntityType( classes[0] );
+ loader.setEntityType( targetedEntities.iterator().next() );
loader.setCriteria( criteria );
return loader;
}
- else if ( classes.length == 1 ) {
+ else if ( targetedEntities.size() == 1 ) {
final QueryLoader loader = new QueryLoader();
loader.init( session, searchFactoryImplementor );
- loader.setEntityType( classes[0] );
+ loader.setEntityType( targetedEntities.iterator().next() );
return loader;
}
else {
final MultiClassesQueryLoader loader = new MultiClassesQueryLoader();
loader.init( session, searchFactoryImplementor );
- loader.setEntityTypes( classes );
+ loader.setEntityTypes( targetedEntities );
return loader;
}
}
@@ -227,7 +228,7 @@
int first = first();
int max = max( first, queryHits.totalHits );
DocumentExtractor extractor = new DocumentExtractor(
- queryHits, searchFactory, indexProjection,
idFieldNames,allowFieldSelectionInProjection
+ queryHits, searchFactory, indexProjection, idFieldNames,
allowFieldSelectionInProjection
);
Loader loader = getLoader( ( Session ) this.session, searchFactory );
return new ScrollableResultsImpl(
@@ -574,8 +575,9 @@
* Build the index searcher for this fulltext query.
*
* @param searchFactoryImplementor the search factory.
+ *
* @return the <code>IndexSearcher</code> for this query (can be
<code>null</code>.
- * TODO change classesAndSubclasses by side effect, which is a mismatch with the
Searcher return, fix that.
+ * TODO change classesAndSubclasses by side effect, which is a mismatch with the
Searcher return, fix that.
*/
private IndexSearcher buildSearcher(SearchFactoryImplementor searchFactoryImplementor)
{
Map<Class<?>, DocumentBuilder<?>> builders =
searchFactoryImplementor.getDocumentBuilders();
@@ -583,9 +585,9 @@
Set<String> idFieldNames = new HashSet<String>();
Similarity searcherSimilarity = null;
- //TODO check if caching this work for the last n list of classes makes a perf boost
- if ( classes == null || classes.length == 0 ) {
- // empty classes array means search over all indexed enities,
+ //TODO check if caching this work for the last n list of targetedEntities makes a perf
boost
+ if ( targetedEntities.size() == 0 ) {
+ // empty targetedEntities array means search over all indexed enities,
// but we have to make sure there is at least one
if ( builders.isEmpty() ) {
throw new HibernateException(
@@ -606,9 +608,9 @@
classesAndSubclasses = null;
}
else {
- Set<Class<?>> involvedClasses = new HashSet<Class<?>>(
classes.length );
- Collections.addAll( involvedClasses, classes );
- for ( Class<?> clazz : classes ) {
+ Set<Class<?>> involvedClasses = new HashSet<Class<?>>(
targetedEntities.size() );
+ involvedClasses.addAll( targetedEntities );
+ for ( Class<?> clazz : targetedEntities ) {
DocumentBuilder<?> builder = builders.get( clazz );
if ( builder != null ) {
involvedClasses.addAll( builder.getMappedSubclasses() );
Modified: search/trunk/src/test/org/hibernate/search/test/SearchTestCase.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/SearchTestCase.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/test/org/hibernate/search/test/SearchTestCase.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -6,11 +6,19 @@
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.store.Directory;
import org.hibernate.HibernateException;
+import org.hibernate.Transaction;
import org.hibernate.event.PostInsertEventListener;
import org.hibernate.impl.SessionFactoryImpl;
import org.hibernate.search.Environment;
+import org.hibernate.search.FullTextSession;
+import org.hibernate.search.Search;
+import org.hibernate.search.annotations.Indexed;
+import org.hibernate.search.test.inheritance.Animal;
+import org.hibernate.search.test.inheritance.Mammal;
import org.hibernate.search.event.FullTextIndexEventListener;
import org.hibernate.search.store.RAMDirectoryProvider;
+import org.hibernate.search.store.FSDirectoryProvider;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,6 +66,18 @@
return listener;
}
+ protected void ensureIndexesAreEmpty() {
+ FullTextSession s = Search.getFullTextSession( openSession() );
+ Transaction tx;
+ tx = s.beginTransaction();
+ for ( Class clazz : getMappings() ) {
+ if ( clazz.getAnnotation( Indexed.class ) != null ) {
+ s.purgeAll( clazz );
+ }
+ }
+ tx.commit();
+ }
+
protected void configure(org.hibernate.cfg.Configuration cfg) {
cfg.setProperty( "hibernate.search.default.directory_provider",
RAMDirectoryProvider.class.getName() );
cfg.setProperty( Environment.ANALYZER_CLASS, StopAnalyzer.class.getName() );
Modified: search/trunk/src/test/org/hibernate/search/test/filter/FilterTest.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/filter/FilterTest.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/test/org/hibernate/search/test/filter/FilterTest.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -96,11 +96,7 @@
//success
}
-
s.getTransaction().commit();
-
-
-
s.close();
deleteData();
}
Modified: search/trunk/src/test/org/hibernate/search/test/inheritance/Animal.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/inheritance/Animal.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/test/org/hibernate/search/test/inheritance/Animal.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -1,6 +1,7 @@
//$Id$
package org.hibernate.search.test.inheritance;
+import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@@ -15,8 +16,7 @@
* @author Emmanuel Bernard
*/
@Entity
-@Indexed
-public class Animal extends Being {
+public abstract class Animal extends Being {
private Long id;
private String name;
Modified: search/trunk/src/test/org/hibernate/search/test/inheritance/Being.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/inheritance/Being.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/test/org/hibernate/search/test/inheritance/Being.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -6,6 +6,7 @@
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.FieldBridge;
+import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.test.bridge.PaddedIntegerBridge;
/**
Copied: search/trunk/src/test/org/hibernate/search/test/inheritance/Fish.java (from rev
15588, search/trunk/src/test/org/hibernate/search/test/inheritance/Mammal.java)
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/inheritance/Fish.java
(rev 0)
+++ search/trunk/src/test/org/hibernate/search/test/inheritance/Fish.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -0,0 +1,28 @@
+//$Id$
+package org.hibernate.search.test.inheritance;
+
+import javax.persistence.Entity;
+
+import org.hibernate.search.annotations.Field;
+import org.hibernate.search.annotations.Index;
+import org.hibernate.search.annotations.Indexed;
+import org.hibernate.search.annotations.Store;
+
+/**
+ * @author Hardy Ferentschik
+ */
+@Entity
+@Indexed
+public class Fish extends Animal {
+
+ private int numberOfDorsalFins;
+
+ @Field(index = Index.UN_TOKENIZED, store = Store.YES)
+ public int getNumberOfDorsalFins() {
+ return numberOfDorsalFins;
+ }
+
+ public void setNumberOfDorsalFins(int numberOfDorsalFins) {
+ this.numberOfDorsalFins = numberOfDorsalFins;
+ }
+}
\ No newline at end of file
Property changes on:
search/trunk/src/test/org/hibernate/search/test/inheritance/Fish.java
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:mergeinfo
+
Modified:
search/trunk/src/test/org/hibernate/search/test/inheritance/InheritanceTest.java
===================================================================
---
search/trunk/src/test/org/hibernate/search/test/inheritance/InheritanceTest.java 2008-11-19
14:11:26 UTC (rev 15591)
+++
search/trunk/src/test/org/hibernate/search/test/inheritance/InheritanceTest.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -2,6 +2,7 @@
package org.hibernate.search.test.inheritance;
import java.util.List;
+import java.io.Serializable;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.index.Term;
@@ -26,41 +27,76 @@
public void testInheritance() throws Exception {
createTestData();
+
FullTextSession s = Search.getFullTextSession( openSession() );
Transaction tx = s.beginTransaction();
+
QueryParser parser = new QueryParser( "name", new StopAnalyzer() );
+ Query query = parser.parse( "Elephant" );
+ org.hibernate.Query hibQuery = s.createFullTextQuery( query, Mammal.class );
+ assertItsTheElephant( hibQuery.list() );
- Query query;
- org.hibernate.Query hibQuery;
+ query = parser.parse( "hasSweatGlands:false" );
+ hibQuery = s.createFullTextQuery( query, Animal.class, Mammal.class );
+ assertItsTheElephant( hibQuery.list() );
- query = parser.parse( "Elephant" );
- hibQuery = s.createFullTextQuery( query, Mammal.class );
+ query = parser.parse( "Elephant OR White Pointer" );
+ hibQuery = s.createFullTextQuery( query, Being.class );
List result = hibQuery.list();
assertNotNull( result );
- assertEquals( "Query subclass by superclass attribute", 1, result.size() );
+ assertEquals( "Query filtering on superclass return mapped subclasses", 2,
result.size() );
- query = parser.parse( "mammalNbr:[2 TO 2]" );
- hibQuery = s.createFullTextQuery( query, Animal.class, Mammal.class );
- result = hibQuery.list();
- assertNotNull( result );
- assertEquals( "Query subclass by subclass attribute", 1, result.size() );
-
- query = parser.parse( "Jr" );
+ query = new RangeQuery( new Term( "weight", "04000" ), new Term(
"weight", "05000" ), true );
hibQuery = s.createFullTextQuery( query, Animal.class );
- result = hibQuery.list();
- assertNotNull( result );
- assertEquals( "Query filtering on superclass return mapped subclasses", 2,
result.size() );
+ assertItsTheElephant( hibQuery.list() );
- query = new RangeQuery( new Term( "weight", "00200" ), null, true
);
+ query = parser.parse( "Elephant" );
+ hibQuery = s.createFullTextQuery( query, Being.class );
+ assertItsTheElephant( hibQuery.list() );
+
+ tx.commit();
+ s.close();
+ }
+
+
+ public void testPolymorphicQueries() throws Exception {
+ createTestData();
+
+ FullTextSession s = Search.getFullTextSession( openSession() );
+ Transaction tx = s.beginTransaction();
+ QueryParser parser = new QueryParser( "name", new StopAnalyzer() );
+ Query query = parser.parse( "Elephant" );
+
+ org.hibernate.Query hibQuery = s.createFullTextQuery( query, Mammal.class );
+ assertItsTheElephant( hibQuery.list() );
+
hibQuery = s.createFullTextQuery( query, Animal.class );
- result = hibQuery.list();
- assertNotNull( result );
- assertEquals( "Query on non @Indexed superclass property", 1, result.size()
);
+ assertItsTheElephant( hibQuery.list() );
+ hibQuery = s.createFullTextQuery( query, Being.class );
+ assertItsTheElephant( hibQuery.list() );
+
+ hibQuery = s.createFullTextQuery( query, Object.class );
+ assertItsTheElephant( hibQuery.list() );
+
+ hibQuery = s.createFullTextQuery( query, Serializable.class );
+ assertItsTheElephant( hibQuery.list() );
+
+ hibQuery = s.createFullTextQuery( query, Mammal.class, Animal.class, Being.class,
Object.class, Serializable.class );
+ assertItsTheElephant( hibQuery.list() );
+
tx.commit();
s.close();
}
+ private void assertItsTheElephant(List result) {
+ assertNotNull( result );
+ assertEquals( "Wrong number of results", 1, result.size() );
+ assertTrue( "Wrong result type", result.get( 0 ) instanceof Mammal );
+ Mammal mammal = ( Mammal ) result.get( 0 );
+ assertEquals( "Wrong animal name", "Elephant", mammal.getName() );
+ }
+
/**
* Tests that purging the index of a class also purges the index of the subclasses. See
also HSEARCH-262.
*
@@ -72,10 +108,10 @@
Transaction tx = s.beginTransaction();
QueryParser parser = new QueryParser( "name", new StopAnalyzer() );
- Query query = parser.parse( "Jr" );
+ Query query = parser.parse( "Elephant OR White Pointer OR Chimpanzee" );
List result = s.createFullTextQuery( query, Animal.class ).list();
assertNotNull( result );
- assertEquals( "Wrong number of hits. There should be one elephant and one
shark.", 2, result.size() );
+ assertEquals( "Wrong number of hits. There should be one elephant and one
shark.", 3, result.size() );
s.purgeAll( Animal.class );
tx.commit();
@@ -87,38 +123,40 @@
);
tx.commit();
-
s.close();
}
private void createTestData() {
FullTextSession s = Search.getFullTextSession( openSession() );
Transaction tx = s.beginTransaction();
- Animal a = new Animal();
- a.setName( "Shark Jr" );
- s.save( a );
- Mammal m = new Mammal();
- m.setMammalNbr( 2 );
- m.setName( "Elephant Jr" );
- m.setWeight( 400 );
- s.save( m );
- tx.commit();//post commit events for lucene
- s.clear();
- }
- private void ensureIndexesAreEmpty() {
- FullTextSession s = Search.getFullTextSession( openSession() );
- Transaction tx;
- tx = s.beginTransaction();
- s.purgeAll( Animal.class );
- s.purgeAll( Mammal.class );
+ Fish shark = new Fish();
+ shark.setName( "White Pointer" );
+ shark.setNumberOfDorsalFins( 2 );
+ shark.setWeight( 1500 );
+ s.save( shark );
+
+ Mammal elephant = new Mammal();
+ elephant.setName( "Elephant" );
+ elephant.setHasSweatGlands( false );
+ elephant.setWeight( 4500 );
+ s.save( elephant );
+
+ Mammal chimp = new Mammal();
+ chimp.setName( "Chimpanzee" );
+ chimp.setHasSweatGlands( true );
+ chimp.setWeight( 50 );
+ s.save( chimp );
+
tx.commit();
+ s.clear();
}
protected Class[] getMappings() {
return new Class[] {
Animal.class,
- Mammal.class
+ Mammal.class,
+ Fish.class
};
}
}
Modified: search/trunk/src/test/org/hibernate/search/test/inheritance/Mammal.java
===================================================================
--- search/trunk/src/test/org/hibernate/search/test/inheritance/Mammal.java 2008-11-19
14:11:26 UTC (rev 15591)
+++ search/trunk/src/test/org/hibernate/search/test/inheritance/Mammal.java 2008-11-19
14:17:48 UTC (rev 15592)
@@ -1,6 +1,7 @@
//$Id$
package org.hibernate.search.test.inheritance;
+import java.io.Serializable;
import javax.persistence.Entity;
import org.hibernate.search.annotations.Field;
@@ -13,15 +14,15 @@
*/
@Entity
@Indexed
-public class Mammal extends Animal {
- private int mammalNbr;
+public class Mammal extends Animal implements Serializable {
+ private boolean hasSweatGlands;
@Field(index= Index.UN_TOKENIZED, store= Store.YES)
- public int getMammalNbr() {
- return mammalNbr;
- }
+ public boolean isHasSweatGlands() {
+ return hasSweatGlands;
+ }
- public void setMammalNbr(int mammalNbr) {
- this.mammalNbr = mammalNbr;
- }
+ public void setHasSweatGlands(boolean hasSweatGlands) {
+ this.hasSweatGlands = hasSweatGlands;
+ }
}