package org.jboss.seam.wiki.core.search.metamodel; import org.jboss.seam.annotations.*; import org.jboss.seam.annotations.Observer; 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) public class SearchRegistry { @Logger static Log log; Map<String, SearchableEntity> searchableEntitiesByName = new HashMap<String, SearchableEntity>(); List<SearchableEntity> searchableEntities = new ArrayList<SearchableEntity>(); @Observer("Wiki.startup") 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 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)) { throw new RuntimeException("Configured as searchable but missing @Searchable and/or @Indexed: " + entityClass.getName()); } 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 and embedded indexed getters for (Method method : getGetters(entityClass, Searchable.class, org.hibernate.search.annotations.IndexedEmbedded.class)) { String prefix = method.getAnnotation(org.hibernate.search.annotations.IndexedEmbedded.class).prefix(); propertyName = prefix + method.getAnnotation(Searchable.class).embeddedProperty(); if (propertyName.length() == 0) throw new RuntimeException("@IndexedEmbedded requires @Searchable(embeddedProperty) name on entity " + entityClass.getName()); propertyDescription = method.getAnnotation(Searchable.class).description(); type = method.getAnnotation(Searchable.class).type(); SearchableProperty property = new SearchablePropertySingle( 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); } // @Searchable and embedded indexed fields for (Field field : getFields(entityClass, Searchable.class, org.hibernate.search.annotations.IndexedEmbedded.class)) { String prefix = field.getAnnotation(org.hibernate.search.annotations.IndexedEmbedded.class).prefix(); propertyName = prefix + field.getAnnotation(Searchable.class).embeddedProperty(); if (propertyName.length() == 0) throw new RuntimeException("@IndexedEmbedded requires @Searchable(embeddedProperty) name on entity " + entityClass.getName()); propertyDescription = field.getAnnotation(Searchable.class).description(); type = field.getAnnotation(Searchable.class).type(); SearchableProperty property = new SearchablePropertySingle( 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; } }