/* * Copyright 2008 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.services.fulltext; import java.io.File; import java.io.Reader; import java.io.StringReader; import java.util.Collections; import java.util.List; import java.util.Map; import ome.conditions.ApiUsageException; import ome.io.nio.OriginalFilesService; import ome.model.IObject; import ome.model.core.OriginalFile; import ome.services.messages.ReindexMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.Field.Index; import org.apache.lucene.document.Field.Store; import org.hibernate.proxy.HibernateProxy; import org.hibernate.search.bridge.FieldBridge; import org.hibernate.search.bridge.LuceneOptions; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; /** * Base class for building custom {@link FieldBridge} implementations. * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 */ public abstract class BridgeHelper implements FieldBridge, ApplicationEventPublisherAware { /** * Name of the {@link Field} which contains the union of all fields. This is * also the default search field, so users need not append the value to * search the full index. A field name need only be added to a search to * eliminate other fields. */ //TODO add to constants public final static String COMBINED = "combined_fields"; /** * Simpler wrapper to handle superclass proxy objects (e.g. Annotation) * which do * not behave properly with instanceof checks. * * @see <a href="http://trac.openmicroscopy.org/ome/ticket/5076">ticket:5076</a> */ @SuppressWarnings("unchecked") public static <T> T getProxiedObject(T proxy) { if (proxy instanceof HibernateProxy) { return (T) ((HibernateProxy) proxy).getHibernateLazyInitializer() .getImplementation(); } return proxy; } protected final Logger log = LoggerFactory.getLogger(getClass()); protected ApplicationEventPublisher publisher; public final Logger logger() { return log; } public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } /** * Method to be implemented by all {@link FieldBridge bridges}. The "value" * argument is an active Hibernate object, and so the full graph can be * walked. */ public abstract void set(final String name, final Object value, final Document document, final LuceneOptions opts); /** * Helper method which takes the parameters from the * {@link #set(String, Object, Document, LuceneOptions)} * method (possibly modified) as well as the parsed {@link String} value * which should be added to the index, and adds two fields. One with the * given field name and another to the {@link #COMBINED} field which is the * default search provided to users. In addition to storing the value as is, * another {@link Field} will be added for both the named and * {@link #COMBINED} cases using a {@link StringReader} to allow Lucene to * tokenize the {@link String}. * * @param d * Document as passed to the set method. Do not modify. * @param field * Field name which probably <em/>should</em> be modified. If * this value is null, then the "value" will only be added to the * {@link #COMBINED} field. * @param value * Value which has been parsed out for this field. Should * <em/>not</em> be null. If you need to store a null value in * the index, use a null token like "null". * @param opts * LuceneOptions, passed in from the runtime. If overriding on * the interface values is required, see {@link SimpleLuceneOptions} */ protected void add(Document d, String field, String value, LuceneOptions opts) { if (value == null) { throw new RuntimeException( "Value for indexing cannot be null. Use a null token instead."); } Float boost = opts.getBoost(); Store store = opts.getStore(); Index index = opts.getIndex(); // If the field == null, then we ignore it, to allow easy addition // of Fields as COMBINED if (field != null) { final Field named_field = new Field(field, value, store, index); if (boost != null) { named_field.setBoost(boost); } d.add(named_field); final Field named_parsed_field = new Field(field, new StringReader( value)); d.add(named_parsed_field); } // Never storing in combined fields, since it's duplicated final Field combined_field = new Field(COMBINED, value, Store.NO, index); if (boost != null) { combined_field.setBoost(boost); } d.add(combined_field); final Field combined_parsed_field = new Field(COMBINED, new StringReader(value)); d.add(combined_parsed_field); } /** * Second helper method used when parsing files. The {@link OriginalFile} * will be passed to {@link #parse(OriginalFile, OriginalFilesService, Map)} * to generate {@link Reader} instances, which will be read until they * signal an end, however it is not the responsibility of this instance to * close the Readers since this happens asynchronously. * * The contents of the file will be parsed both to {@link #COMBINED} and * "file.contents". * * @param d * {@link Document} as passed to set. Do not modify. * @param name String to be used as the name of the field. If null, then * the contents will only be added to the {@link #COMBINED} * {@link Field}. * @param file * Non-null, possibly unloaded {@link OriginalFile} which is used * to look up the file on disk. * @param files * {@link OriginalFilesService} which knows how to find where this * {@link OriginalFile} is stored on disk. * @param parsers * {@link Map} of {@link FileParser} instances to be used based * on the {@link OriginalFile#getMimetype() Format} of * the {@link OriginalFile} * @param opts * The search option. */ protected void addContents(final Document d, final String name, final OriginalFile file, final OriginalFilesService files, final Map<String, FileParser> parsers, final LuceneOptions opts) { if (file == null) { throw new RuntimeException( "File cannot be null. Either do not attempt to add " + "anything for this field, or use a null token like " + "\"null\" instead."); } Field f; Float boost = opts.getBoost(); if (name != null) { for (Reader parsed : parse(file, files, parsers)) { f = new Field(name, parsed); if (boost != null) { f.setBoost(boost); } d.add(f); } } for (Reader parsed : parse(file, files, parsers)) { f = new Field(COMBINED, parsed); if (boost != null) { f.setBoost(boost); } d.add(f); } } /** * Publishes a {@link ReindexMessage} which will get processed * asynchronously. */ protected <T extends IObject> void reindex(T object) { reindexAll(Collections.singletonList(object)); } /** * Publishes a {@link ReindexMessage} which will get processed * asynchronously. */ protected <T extends IObject> void reindexAll(List<T> list) { if (publisher == null) { throw new ApiUsageException( "Bridge is not configured for sending messages."); } for (T object : list) { if (object == null || object.getId() == null) { throw new ApiUsageException("Object cannot be null"); } } final ReindexMessage<T> rm = new ReindexMessage<T>(this, list); publisher.publishEvent(rm); } /** * Attempts to parse the given {@link OriginalFile}. If any of the * necessary components is null, then it will return an empty, but not null * {@link Iterable}. Also looks for the catch all parser under "*" * * @param file * Can be null. * @return will not be null. */ protected Iterable<Reader> parse(final OriginalFile file, final OriginalFilesService files, final Map<String, FileParser> parsers) { if (files != null && parsers != null) { if (file != null && file.getMimetype() != null) { String path = files.getFilesPath(file.getId()); String format = file.getMimetype(); FileParser parser = parsers.get(format); if (parser != null) { return parser.parse(new File(path)); } else { parser = parsers.get("*"); if (parser != null) { return parser.parse(new File(path)); } } } } return FileParser.EMPTY; } }