[jboss-cvs] jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel ...

Christian Bauer christian at hibernate.org
Tue Jun 12 08:30:00 EDT 2007


  User: cbauer  
  Date: 07/06/12 08:30:00

  Added:       examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel       
                        SearchableEntity.java SearchableProperty.java
                        SearchablePropertySingle.java
                        SearchablePropertyComposite.java SearchSupport.java
                        SearchableEntityHandler.java SearchRegistry.java
  Log:
  Completed first iteration of search engine
  
  Revision  Changes    Path
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchableEntity.java
  
  Index: SearchableEntity.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * Meta-information about a particular entity that can be searched.
   * <p>
   * Provides global statistics properties which need to be set before they are read - this is
   * just a value holder.
   *
   * @author Christian Bauer
   */
  public class SearchableEntity implements Serializable, Comparable {
      private Class clazz;
      private String description;
      private SearchableEntityHandler handler;
      private List<SearchableProperty> properties = new ArrayList<SearchableProperty>();
  
      public SearchableEntity(Class clazz) {
          this.clazz = clazz;
      }
  
      public SearchableEntity(Class clazz, String description, SearchableEntityHandler handler) {
          this.clazz = clazz;
          this.description = description;
          this.handler = handler;
      }
  
      public Class getClazz() {
          return clazz;
      }
  
      public String getDescription() {
          return description;
      }
  
      public SearchableEntityHandler getHandler() {
          return handler;
      }
  
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
          SearchableEntity that = (SearchableEntity) o;
          if (clazz != null ? !clazz.equals(that.clazz) : that.clazz != null) return false;
          return true;
      }
  
      public int hashCode() {
          return (clazz != null ? clazz.hashCode() : 0);
      }
  
      public List<SearchableProperty> getProperties() {
          return properties;
      }
  
      public void setProperties(List<SearchableProperty> properties) {
          this.properties = properties;
      }
  
      // Some stats, used for admin screens
      private long numOfIndexedDocuments;
      private long numOfIndexedTerms;
      private long indexSizeInBytes;
  
      public long getNumOfIndexedDocuments() {
          return numOfIndexedDocuments;
      }
  
      public void setNumOfIndexedDocuments(long numOfIndexedDocuments) {
          this.numOfIndexedDocuments = numOfIndexedDocuments;
      }
  
      public long getNumOfIndexedTerms() {
          return numOfIndexedTerms;
      }
  
      public void setNumOfIndexedTerms(long numOfIndexedTerms) {
          this.numOfIndexedTerms = numOfIndexedTerms;
      }
  
      public long getIndexSizeInBytes() {
          return indexSizeInBytes;
      }
  
      public void setIndexSizeInBytes(long indexSizeInBytes) {
          this.indexSizeInBytes = indexSizeInBytes;
      }
  
      public int compareTo(Object o) {
          return getDescription().compareTo( ((SearchableEntity)o).getDescription() );
      }
  
      public String toString() {
          return getClazz().getName();
      }
  }
  
  
  
  
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchableProperty.java
  
  Index: SearchableProperty.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import org.apache.lucene.analysis.Token;
  import org.apache.lucene.analysis.TokenStream;
  import org.apache.lucene.analysis.standard.StandardAnalyzer;
  import org.apache.lucene.index.Term;
  import org.apache.lucene.search.*;
  import org.jboss.seam.log.Log;
  import org.jboss.seam.wiki.core.search.PropertySearch;
  import org.jboss.seam.wiki.core.search.annotations.SearchableType;
  
  import java.io.Serializable;
  import java.io.StringReader;
  import java.text.DateFormat;
  import java.text.SimpleDateFormat;
  import java.util.Calendar;
  import java.util.GregorianCalendar;
  import java.util.regex.Pattern;
  
  /**
   * Meta-information about a logical searchable property.
   * <p>
   * Generalized building of Lucene queries, called by subclasses.
   * <p>
   * TODO: Implement NUMRANGE query building
   *
   * @author Christian Bauer
   */
  public abstract class SearchableProperty implements Serializable, Comparable {
  
      public static final String TERM_INCLUDE             = "include";
      public static final String TERM_EXCLUDE             = "exclude";
      public static final String TERM_MATCHEXACTPHRASE    = "matchExactPhrase";
      public static final String TERM_NUMOFDAYS           = "numOfDays";
  
      protected Log log = org.jboss.seam.log.Logging.getLog(getClass());
  
      private String description;
      private SearchableType type;
  
      public SearchableProperty() {}
  
      public SearchableProperty(String description, SearchableType type) {
          this.description = description;
          this.type = type;
      }
  
      public String getDescription() {
          return description;
      }
  
      public void setDescription(String description) {
          this.description = description;
      }
  
      public SearchableType getType() {
          return type;
      }
  
      public void setType(SearchableType type) {
          this.type = type;
      }
  
      public int compareTo(Object o) {
          return getDescription().compareTo( ((SearchableProperty)o).getDescription() );
      }
  
      public abstract Query getQuery(PropertySearch search);
  
      protected Query buildIncludeQuery(String fieldName, PropertySearch search) {
          Query query = null;
  
          if (getType().equals(SearchableType.PHRASE)) {
  
              String includeString = (String)search.getTerms().get(TERM_INCLUDE);
              Boolean matchExactPhrase = (Boolean)search.getTerms().get(TERM_MATCHEXACTPHRASE);
              if (includeString != null && includeString.length() >0) {
  
                  if(matchExactPhrase != null && matchExactPhrase) {
                      log.debug("building include phrase query for field: " + fieldName);
                      query = buildPhraseQuery(fieldName, includeString);
                  } else {
                      log.debug("building include term query for field: " + fieldName);
                      query = buildTermQuery(fieldName, includeString);
                  }
  
              }
  
          } else if (getType().equals(SearchableType.PASTDATE)) {
  
              String numOfDays = (String)search.getTerms().get(TERM_NUMOFDAYS);
              if (numOfDays != null &&  numOfDays.length() >0) {
                  log.debug("building past date query for field: " + fieldName);
  
                  DateFormat df = new SimpleDateFormat("yyyyMMdd");
                  Calendar today = new GregorianCalendar();
                  Calendar startDate = new GregorianCalendar();
                  startDate.add(Calendar.DAY_OF_YEAR, -Integer.valueOf(numOfDays));
  
                  log.debug("date range query start: " + df.format(startDate.getTime()) );
                  log.debug("date range query end: " + df.format(today.getTime()) );
  
                  query = buildRangeQuery(fieldName, df.format(startDate.getTime()), df.format(today.getTime()));
              }
          }
          return query;
      }
  
      protected Query buildExcludeQuery(String fieldName, PropertySearch search) {
          Query query = null;
  
          if (getType().equals(SearchableType.PHRASE)) {
              log.debug("building exclude phrase query for field: " + fieldName);
  
              String includeString = (String)search.getTerms().get(TERM_INCLUDE);
              String excludeString = (String)search.getTerms().get(TERM_EXCLUDE);
              Boolean matchExactPhrase = (Boolean)search.getTerms().get(TERM_MATCHEXACTPHRASE);
              if (includeString != null && includeString.length() >0 && excludeString != null && excludeString.length() > 0) {
  
                  if(matchExactPhrase != null && matchExactPhrase) {
                      log.debug("building exclude phrase query for field: " + fieldName);
                      query = buildPhraseQuery(fieldName, includeString);
                  } else {
                      log.debug("building exclude term query for field: " + fieldName);
                      query = buildTermQuery(fieldName, excludeString);
                  }
  
              }
          }
          return query;
  
      }
  
      private Query buildPhraseQuery(String fieldName, String terms) {
          try {
              PhraseQuery query = new PhraseQuery();
              query.setSlop(0);
  
              TokenStream includeStream =
                      new StandardAnalyzer().tokenStream(null, new StringReader(escape(terms).toLowerCase()));
  
              while (true) {
                  Token t = includeStream.next();
                  if (t == null) break;
                  query.add( new Term(fieldName, t.termText()) );
              }
  
              return query.getTerms().length > 0 ? query : null;
          } catch (Exception ex) {
              throw new RuntimeException(ex);
          }
      }
  
      private Query buildTermQuery(String fieldName, String terms) {
          String[] termsArray = escape(terms).toLowerCase().split("\\s"); // Just split user input by whitespace
          if (termsArray.length > 1) {
              BooleanQuery query = new BooleanQuery();
              for (String s: termsArray) {
                  TermQuery termQuery = new TermQuery(new Term(fieldName, s) );
                  query.add(termQuery, BooleanClause.Occur.SHOULD);
              }
              return query.getClauses().length > 0 ? query : null;
          } else {
              return termsArray.length != 0 ? new TermQuery(new Term(fieldName, termsArray[0])) : null;
          }
      }
  
      private Query buildRangeQuery(String fieldName, String begin, String end) {
          return new ConstantScoreRangeQuery(fieldName, begin, end, true, true); // Inclusive of begin and end
      }
  
  
      private String escape(String userInput) {
          userInput = userInput.replaceAll(Pattern.quote("("), "\\(");
          userInput = userInput.replaceAll(Pattern.quote(")"), "\\)");
          return userInput;
      }
  
  
  }
  
  
  
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchablePropertySingle.java
  
  Index: SearchablePropertySingle.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import org.apache.lucene.search.Query;
  import org.apache.lucene.search.BooleanQuery;
  import org.apache.lucene.search.BooleanClause;
  import org.jboss.seam.wiki.core.search.PropertySearch;
  import org.jboss.seam.wiki.core.search.annotations.SearchableType;
  
  /**
   * A logical searchable property of a single indexed field.
   *
   * @author Christian Bauer
   */
  public class SearchablePropertySingle extends SearchableProperty {
  
      private String name;
  
      public SearchablePropertySingle(String name, String description, SearchableType type) {
          super(description, type);
          this.name = name;
      }
  
      public String getName() {
          return name;
      }
  
      public void setName(String name) {
          this.name = name;
      }
  
      public String toString() {
          return getName();
      }
  
      public Query getQuery(PropertySearch search) {
          BooleanQuery query = new BooleanQuery();
  
          Query iq = buildIncludeQuery(getName(), search);
          if (iq != null) {
              log.debug("include query: " + iq.toString());
              query.add(iq, BooleanClause.Occur.MUST );
              Query eq= buildExcludeQuery(getName(), search);
              if (eq != null) {
                  log.debug("exclude query: " + eq.toString());
                  query.add(eq, BooleanClause.Occur.MUST_NOT);
              }
          }
          
          return query.getClauses().length > 0 ? query : null;
      }
  }
  
  
  
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchablePropertyComposite.java
  
  Index: SearchablePropertyComposite.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import org.apache.lucene.search.BooleanClause;
  import org.apache.lucene.search.BooleanQuery;
  import org.apache.lucene.search.Query;
  import org.jboss.seam.wiki.core.search.PropertySearch;
  import org.jboss.seam.wiki.core.search.annotations.SearchableType;
  
  /**
   * A logical searchable property that is a composite of several indexed fields.
   *
   * @author Christian Bauer
   */
  public class SearchablePropertyComposite extends SearchableProperty {
  
      private String[] names;
  
      public SearchablePropertyComposite(String[] names, String description, SearchableType type) {
          super(description, type);
          this.names = names;
      }
  
      public String[] getNames() {
          return names;
      }
  
      public void setNames(String[] names) {
          this.names = names;
      }
  
      public String toString() {
          String name = "Composite: ";
          for (String s : getNames()) {
              name += s + " ";
          }
          return name;
      }
  
      public Query getQuery(PropertySearch search) {
          BooleanQuery query = new BooleanQuery();
  
          BooleanQuery includeQuery = new BooleanQuery();
          BooleanQuery excludeQuery = new BooleanQuery();
  
          for (String s : getNames()) {
              Query iq = buildIncludeQuery(s, search);
              if (iq != null) {
                  log.debug("include query: " + iq.toString());
                  includeQuery.add(iq, BooleanClause.Occur.SHOULD);
                  Query eq = buildExcludeQuery(s, search);
                  if (eq != null) {
                      log.debug("exclude query: " + eq.toString());
                      excludeQuery.add(eq, BooleanClause.Occur.SHOULD);
                  }
              }
          }
  
          if (includeQuery.getClauses().length > 0) query.add(includeQuery, BooleanClause.Occur.MUST);
          if (excludeQuery.getClauses().length > 0) query.add(excludeQuery, BooleanClause.Occur.MUST_NOT);
          return query.getClauses().length > 0 ? query : null;
      }
  
  }
  
  
  
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchSupport.java
  
  Index: SearchSupport.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import org.jboss.seam.annotations.Observer;
  import org.jboss.seam.annotations.Scope;
  import org.jboss.seam.ScopeType;
  import org.jboss.seam.wiki.util.WikiUtil;
  import org.apache.lucene.search.highlight.Highlighter;
  import org.apache.lucene.search.highlight.QueryScorer;
  import org.apache.lucene.search.highlight.Fragmenter;
  import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
  import org.apache.lucene.search.Query;
  import org.apache.lucene.analysis.TokenStream;
  import org.apache.lucene.analysis.standard.StandardAnalyzer;
  
  import java.util.Set;
  import java.io.StringReader;
  
  /**
   * Superclass for search support, extend it to add search options to the wiki.
   * <p>
   * Extend this class and return <tt>SearchableEntityHandler</tt> instances for each
   * entity you want to be able to search in the user interface. The handlers need to
   * be able to extract a <tt>SearchHit</tt> from a given query and the original entity
   * instance. This <tt>SearchHit</tt> is then displayed. If you have a string-based
   * property and you want to simply show the "best" fragments of a hit, use the
   * <tt>escapeBestFragments()</tt> convenience method.
   * <p>
   * Note that you also need to annotate any entity class and its properties
   * with <tt>@Searchable</tt>.
   *
   * @see org.jboss.seam.wiki.core.search.annotations.Searchable
   * @see SearchableEntityHandler
   * @see org.jboss.seam.wiki.core.search.SearchHit
   *
   * @author Christian Bauer
   */
  @Scope(ScopeType.APPLICATION)
  public abstract class SearchSupport {
  
      private static final String INTERNAL_BEGIN_HIT = "!!!BEGIN_HIT!!!";
      private static final String INTERNAL_END_HIT = "!!!END_HIT!!!";
  
      @Observer("Search.addSearchSupport")
      public void add(Set<SearchSupport> searchSupportComponents) {
          searchSupportComponents.add(this);
      }
  
      /**
       * Returns the hits of the given query as fragments, highlighted, concatenated, and separated.
       * <p>
       * Pass in a <tt>NullFragmenter</tt> if you don't want any fragmentation by terms but
       * simply the hits highlighted. Otherwise, you will most likely use <tt>SimpleFragmenter</tt>.
       * The text you supply must be the same that was indexed, it will be run through the same
       * analysis procedure to find the hits. Do not pass a different String than the one indexed
       * by Hibernate Search! If you use transparent string bridge with Hibernate Search, run the
       * bridge before passing the string into this method.
       * <p>
       * This method escapes any dangerous HTML characters in the indexed text and fragments by
       * replacing it with HTML entities. You can use the returned string directly to build a
       * <tt>SearchHit</tt>.
       *
       * @param query the query that produced hits
       * @param fragmenter a fragmenter that can split the indexed text
       * @param indexedText the original text that was analyzed and indexed by Hibernate Search (after any bridges!)
       * @param numOfFragments the number of fragments to include in the returned result
       * @param alternativeLength if there are no hits to highlight, how many characters of the original text to return
       * @return the fragmented, highglighted, and then concatenated substring of the indexed text
       */
      protected String escapeBestFragments(Query query, Fragmenter fragmenter,
                                           String indexedText, int numOfFragments, int alternativeLength) {
  
          // The HTML escaping forces us to first fragment with internal placeholders...
          Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter(INTERNAL_BEGIN_HIT, INTERNAL_END_HIT), new QueryScorer(query));
          highlighter.setTextFragmenter(fragmenter);
          try {
              // Use the same analyzer as the indexer!
              TokenStream tokenStream = new StandardAnalyzer().tokenStream(null, new StringReader(indexedText));
  
              String unescapedFragements =
                      highlighter.getBestFragments(tokenStream, indexedText, numOfFragments, getFragmentSeparator());
  
              String escapedFragments = WikiUtil.escapeHtml(unescapedFragements);
  
              // .. and then replace the internal placeholders with real tags after HTML has been escaped
              escapedFragments = escapedFragments.replaceAll(INTERNAL_BEGIN_HIT, getBeginHitTag());
              escapedFragments = escapedFragments.replaceAll(INTERNAL_END_HIT, getEndHitTag());
  
              // If no fragments were produced (no hits), return the original text as an alternative
              if (escapedFragments.length() == 0 && alternativeLength != 0) {
                  return WikiUtil.escapeHtml(
                      indexedText.substring(
                          0,
                          indexedText.length()>alternativeLength ? alternativeLength : indexedText.length()
                      )
                  );
              } else if (escapedFragments.length() == 0 && alternativeLength == 0){
                  return WikiUtil.escapeHtml(indexedText);
              }
  
              return escapedFragments;
  
          } catch (Exception ex) {
              throw new RuntimeException(ex);
          }
      }
  
      /**
       * String used to mark the beginning of a fragment.
       * <p>
       * Defaults to &lt:b&gt;, can be overriden by subclass.
       *
       * @return String used to mark the beginning of a fragment.
       */
      protected String getBeginHitTag() {
          return "<b>";
      }
  
      /**
       * String used to mark the end of a fragment.
       * <p>
       * Defaults to &lt:/b&gt;, can be overriden by subclass.
       *
       * @return String used to mark the end of a fragment.
       */
      protected String getEndHitTag() {
          return "</b>";
      }
  
      /**
       * Separator string between two fragments.
       * <p>
       * Defaults to <tt>... ...</tt> (just dots with a space).
       *
       * @return Separator string between two fragments.
       */
      protected String getFragmentSeparator() {
          return "... ...";
      }
  
      /**
       * Create an return any <tt>SearchableEntityHandler</tt> you require search functionality for.
       * <p>
       * This is called on startup only by the internal registry, to assemble all handlers.
       *
       * @return SearchableEntityHandler typed for a particular indexed/searchable entity class
       */
      public abstract Set<SearchableEntityHandler> getSearchableEntityHandlers();
  
  }
  
  
  
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchableEntityHandler.java
  
  Index: SearchableEntityHandler.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import org.apache.lucene.search.Query;
  import org.jboss.seam.wiki.core.search.SearchHit;
  
  import java.lang.reflect.ParameterizedType;
  
  /**
   * Superclass of a search handler for a particular searchable entity.
   * <p>
   * Extend this class to complete the search functionality for a particular entity, in
   * addition to placing <tt>@Searchable</tt> annotations on it. This handler extracts
   * hits from a search result (does highlighting, fragmentation, etc.) and supports
   * other dynamically overridable metadata (not much in this first incarnation).
   *
   * @author Christian Bauer
   */
  public abstract class SearchableEntityHandler<E> {
  
      private Class<E> searchableEntityClass;
  
      public SearchableEntityHandler() {
          //noinspection unchecked
          this.searchableEntityClass = (Class<E>) ((ParameterizedType) getClass()
                  .getGenericSuperclass()).getActualTypeArguments()[0];
      }
  
      public Class<E> getSearchableEntityClass() {
          return searchableEntityClass;
      }
  
      public boolean isReadAccessChecked() { return false; }
  
      public abstract SearchHit extractHit(Query query, E hit) throws Exception;
  
  }
  
  
  
  1.1      date: 2007/06/12 12:30:00;  author: cbauer;  state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/search/metamodel/SearchRegistry.java
  
  Index: SearchRegistry.java
  ===================================================================
  package org.jboss.seam.wiki.core.search.metamodel;
  
  import org.jboss.seam.annotations.*;
  import org.jboss.seam.ScopeType;
  import org.jboss.seam.wiki.core.search.annotations.*;
  import org.jboss.seam.core.Events;
  import org.jboss.seam.log.Log;
  
  import java.util.*;
  import java.lang.reflect.Method;
  import java.lang.reflect.Field;
  import java.lang.annotation.Annotation;
  import java.beans.Introspector;
  
  /**
   * Runs on startup and reads all <tt>@Searchable</tt> entities.
   * <p>
   * Extracts all annotated entities and properties and builds a registry of metadata that is
   * used by the search engine to render the UI, build queries, extract hits, etc.
   * <p>
   * TODO: Once the Seam scanner is public, all of this can be simplified and
   * even the SearchableEntityHandlers can be declared with annotations
   *
   * @author Christian Bauer
   */
  @Name("searchRegistry")
  @Scope(ScopeType.APPLICATION)
  @Startup(depends = "wikiInit")
  public class SearchRegistry {
  
      @Logger
      static Log log;
  
      Map<String, SearchableEntity> searchableEntitiesByName = new HashMap<String, SearchableEntity>();
      List<SearchableEntity> searchableEntities = new ArrayList<SearchableEntity>();
  
      @Create
      public void scanForSearchSupportComponents() {
  
          log.debug("initializing search registry");
          searchableEntities.clear();
          searchableEntitiesByName.clear();
  
          // Fire an event and let all listeners add themself into the given collection
          // TODO: Is this smarter than PreferenceRegistry scanning approach?
          Set<SearchSupport> searchSupportComponents = new HashSet<SearchSupport>();
          Events.instance().raiseEvent("Search.addSearchSupport", searchSupportComponents);
  
          log.debug("found search support components: " + searchSupportComponents.size());
  
          for (SearchSupport component : searchSupportComponents) {
  
              for (SearchableEntityHandler searchableEntityHandler : component.getSearchableEntityHandlers()) {
                  SearchableEntity searchableEntity =  extractSearchableEntity(searchableEntityHandler);
                  searchableEntities.add(searchableEntity);
                  searchableEntitiesByName.put(searchableEntity.getClazz().getName(), searchableEntity);
              }
          }
  
          // Sort entities
          Collections.sort(searchableEntities);
  
          log.debug("done extracting metadata for searchable entities: " + searchableEntities.size());
      }
  
      private SearchableEntity extractSearchableEntity(SearchableEntityHandler handler) {
          Class<?> entityClass = handler.getSearchableEntityClass();
  
          if (!entityClass.isAnnotationPresent(Searchable.class) ||
                  !entityClass.isAnnotationPresent(org.hibernate.search.annotations.Indexed.class)) {
              log.error("Not indexed or not searchable but configured as searchable: " + entityClass.getName());
              return null;
          }
  
          log.debug("extracting entity search information from: " + entityClass.getName());
          // Extract entity information
          String entityDescription = entityClass.getAnnotation(Searchable.class).description();
          SearchableEntity searchableEntity = new SearchableEntity(entityClass, entityDescription, handler);
  
          // Extract composite property information
          if (entityClass.isAnnotationPresent(CompositeSearchable.class)) {
              searchableEntity.getProperties().add(
                  extractCompositeSearchable(entityClass, entityClass.getAnnotation(CompositeSearchable.class))
              );
          }
          if (entityClass.isAnnotationPresent(CompositeSearchables.class)) {
              for (CompositeSearchable compositeSearchable : entityClass.getAnnotation(CompositeSearchables.class).value()) {
                  searchableEntity.getProperties().add(extractCompositeSearchable(entityClass, compositeSearchable));
              }
          }
  
          // Extract property information
          String propertyName;
          String indexFieldName;
          String propertyDescription;
          SearchableType type;
  
          // @Searchable getter methods
          for (Method method : getGetters(entityClass, Searchable.class, org.hibernate.search.annotations.Field.class)) {
  
              indexFieldName = method.getAnnotation(org.hibernate.search.annotations.Field.class).name();
              propertyName = Introspector.decapitalize(method.getName().substring(3));
              propertyDescription = method.getAnnotation(Searchable.class).description();
              type = method.getAnnotation(Searchable.class).type();
  
              SearchableProperty property = new SearchablePropertySingle(
                      indexFieldName != null && indexFieldName.length() > 0 ? indexFieldName : propertyName,
                      propertyDescription,
                      type
              );
              searchableEntity.getProperties().add(property);
          }
  
          // @Searchable fields
          for (Field field : getFields(entityClass, Searchable.class, org.hibernate.search.annotations.Field.class)) {
              indexFieldName = field.getAnnotation(org.hibernate.search.annotations.Field.class).name();
              propertyName = field.getName();
              propertyDescription = field.getAnnotation(Searchable.class).description();
              type = field.getAnnotation(Searchable.class).type();
  
              SearchableProperty property = new SearchablePropertySingle(
                      indexFieldName != null && indexFieldName.length() > 0? indexFieldName : propertyName,
                      propertyDescription,
                      type
              );
              searchableEntity.getProperties().add(property);
  
          }
  
          return searchableEntity;
      }
  
      private SearchablePropertyComposite extractCompositeSearchable(Class entityClass, CompositeSearchable compositeSearchable) {
  
          // Get all fields/getter methods with a Hibernate Search @Field annotation
          Set<String> searchableProperties = new HashSet<String>();
  
          for (Method method : getGetters(entityClass, org.hibernate.search.annotations.Field.class)) {
              String indexFieldName = method.getAnnotation(org.hibernate.search.annotations.Field.class).name();
              String propertyName = Introspector.decapitalize(method.getName().substring(3));
              searchableProperties.add(
                  indexFieldName != null && indexFieldName.length() > 0? indexFieldName : propertyName
              );
          }
          for (Field field : getFields(entityClass, org.hibernate.search.annotations.Field.class)) {
              String propertyName = field.getName();
              String indexFieldName = field.getAnnotation(org.hibernate.search.annotations.Field.class).name();
              searchableProperties.add(
                  indexFieldName != null && indexFieldName.length() > 0? indexFieldName : propertyName
              );
  
          }
  
          // Validate configured composite property names against fields/getters
          for (String s : compositeSearchable.properties()) {
              if (!searchableProperties.contains(s)) {
                  throw new RuntimeException(
                      "No indexed field/getter could be found for " +
                      "configured searchable composite property '" + s + "' in entity: " + entityClass.getName());
              }
          }
  
          return new SearchablePropertyComposite(
                  compositeSearchable.properties(),
                  compositeSearchable.description(),
                  compositeSearchable.type()
          );
      }
  
      private Method[] getGetters(Class clazz, Class<? extends Annotation>... requiredAnnotations) {
          List<Method> allMethods = new ArrayList<Method>();
          Class c = clazz;
          do {
              Method[] methods = c.getDeclaredMethods();
              for (Method method : methods) {
  
                  boolean annotationsPresent = true;
                  for (Class<? extends Annotation> requiredAnnotation : requiredAnnotations) {
                      if (!method.isAnnotationPresent(requiredAnnotation)) annotationsPresent = false;
                  }
  
                  if (annotationsPresent &&
                      method.getName().startsWith("get") &&
                      method.getParameterTypes().length == 0) {
  
                      allMethods.add(method);
                      if (!method.isAccessible()) method.setAccessible(true);
                  }
              }
              c = c.getSuperclass();
          } while (c != Object.class);
          return allMethods.toArray(new Method[allMethods.size()]);
      }
  
      private Field[] getFields(Class clazz, Class<? extends Annotation>... requiredAnnotations) {
          List<Field> allFields = new ArrayList<Field>();
          Class c = clazz;
          do {
              Field[] fields = c.getDeclaredFields();
              for (Field field : fields) {
  
                  boolean annotationsPresent = true;
                  for (Class<? extends Annotation> requiredAnnotation : requiredAnnotations) {
                      if (!field.isAnnotationPresent(requiredAnnotation)) annotationsPresent = false;
                  }
  
                  if (annotationsPresent) {
                      allFields.add(field);
                      if (!field.isAccessible()) field.setAccessible(true);
                  }
              }
              c = c.getSuperclass();
          } while (c != Object.class);
          return allFields.toArray(new Field[allFields.size()]);
      }
  
      public Map<String, SearchableEntity> getSearchableEntitiesByName() {
          return searchableEntitiesByName;
      }
  
      public List<SearchableEntity> getSearchableEntities() {
          return searchableEntities;
      }
  }
  
  
  



More information about the jboss-cvs-commits mailing list