alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/expand/index/IndexBasedTextExpander.java
public class IndexBasedTextExpander implements TextExpander {
	public static final String FIELD_PROPERTY = "__PROPERTY";

	private final IndexReader reader;
	private final Map<String,ExplanationFactory> explanationFactories = new HashMap<String,ExplanationFactory>();

	public IndexBasedTextExpander(IndexReader reader) {
		super();
		this.reader = reader;
	}

	@Override
	public TextExpansionResult searchExpansion(String text) throws IOException {
		TextExpansionResult synonyms = createTextExpansionResult(AlvisIRConstants.EXPANDER_FIELD_SYNONYMS, text);
		TextExpansionResult canonical = createTextExpansionResult(AlvisIRConstants.EXPANDER_FIELD_CANONICAL, text);
		return new CompoundTextExpansionResult(synonyms, canonical);
	}
	
	private TextExpansionResult createTextExpansionResult(String fieldName, String text) throws IOException {
		Term term = new Term(fieldName, text);
		TermDocs termDocs = reader.termDocs(term);
		return new TermDocsExpansionResult(termDocs);
	}

	TextExpansionResult searchSubPaths(String prefix) throws IOException, ExpanderException {
//		System.err.println("prefix = " + prefix);
		Term prefixTerm = getCanonicalTerm(prefix);
		TermEnum termEnum = new PrefixTermEnum(reader, prefixTerm);
		return new TermEnumExpansionResult(termEnum);
	}
	
	TextExpansionResult searchLimitedDepthSubPaths(String prefix, int depth, char separator) throws IOException, ExpanderException {
		String sep = Pattern.quote(Character.toString(separator));
		String pattern = Pattern.quote(prefix) + "(?:" + "[^" + sep + "]*" + sep + "){0," + depth + "}$";
//		System.err.println("pattern = " + pattern);
		Term regexTerm = getCanonicalTerm(pattern);
		TermEnum termEnum = new RegexTermEnum(reader, regexTerm, new PrefixRegexCapabilities(prefix));
		return new TermEnumExpansionResult(termEnum);
	}
	
	private static final class PrefixRegexCapabilities implements RegexCapabilities {
		private final RegexCapabilities delegate;
		private final String prefix;
		
		private PrefixRegexCapabilities(RegexCapabilities delegate, String prefix) {
			super();
			this.delegate = delegate;
			this.prefix = prefix;
		}
		
		private PrefixRegexCapabilities(String prefix) {
			this(new JavaUtilRegexCapabilities(), prefix);
		}

		@Override
		public void compile(String pattern) {
			delegate.compile(pattern);
		}

		@Override
		public boolean match(String s) {
			return delegate.match(s);
		}

		@Override
		public String prefix() {
			return prefix;
		}
	}

	private static Term getCanonicalTerm(String text) {
		return new Term(AlvisIRConstants.EXPANDER_FIELD_CANONICAL, text);
	}

	@Override
	public TextExpansionResult searchCanonical(String text) throws IOException, ExpanderException {
		Term term = getCanonicalTerm(text);
//		System.err.println("term = " + term);
		TermEnum termEnum = new SingleTermEnum(reader, term);
		return new TermEnumExpansionResult(termEnum);
	}

	private final class TermEnumExpansionResult extends IndexBasedTextExpansionResult {
		private final TermEnum termEnum;

		private TermEnumExpansionResult(TermEnum termEnum) throws IOException, ExpanderException {
			super();
			this.termEnum = termEnum;
			if (termEnum.term() != null) {
				updateDoc();
			}
		}
		
		private void updateDoc() throws IOException, ExpanderException {
			Term term = termEnum.term();
//			System.err.println("term = " + term);
			try (TermDocs termDocs = reader.termDocs(term)) {
				if (!termDocs.next()) {
					throw new ExpanderException("no expansion for term: " + term);
				}
				int docId = termDocs.doc();
//				System.err.println("docId = " + docId);
				doc = reader.document(docId);
			}
		}

		@Override
		public boolean next() throws IOException, ExpanderException {
			if (termEnum.next()) {
				updateDoc();
				return true;
			}
			termEnum.close();
			return false;
		}
	}
	
	private final class TermDocsExpansionResult extends IndexBasedTextExpansionResult {
		private final TermDocs termDocs;
		
		private TermDocsExpansionResult(TermDocs termDocs) throws IOException {
			super();
			this.termDocs = termDocs;
			next();
		}

		@Override
		public boolean next() throws IOException {
			if (termDocs.next()) {
				int docId = termDocs.doc();
				doc = reader.document(docId);
				return true;
			}
			doc = null;
			return false;
		}
	}
	
	private static abstract class IndexBasedTextExpansionResult implements TextExpansionResult {
		protected Document doc;

		protected IndexBasedTextExpansionResult() {
			super();
		}

		@Override
		public boolean any() {
			return doc != null;
		}

		@Override
		public String getType() {
			return doc.get(AlvisIRConstants.EXPANDER_FIELD_TYPE);
		}

		@Override
		public String getCanonical() {
			return doc.get(AlvisIRConstants.EXPANDER_FIELD_CANONICAL);
		}

		@Override
		public String[] getSynonyms() {
			return doc.getValues(AlvisIRConstants.EXPANDER_FIELD_SYNONYMS);
		}

		@Override
		public String getLabel() {
			return doc.get(AlvisIRConstants.EXPANDER_FIELD_LABEL);
		}
	}

	@Override
	public ExplanationFactory getExplanationFactory(String type) throws ExpanderException {
		if (explanationFactories.containsKey(type)) {
			return explanationFactories.get(type);
		}
		throw new ExpanderException("unhandled expansion type: " + type);
	}

	@Override
	public void setExplanationFactory(String type, Properties properties) throws ExpanderException {
		ExplanationFactory factory = ensureExplanationFactory(type, properties);
		setExplanationFactoryProperties(factory, properties);
	}
	
	private ExplanationFactory ensureExplanationFactory(String type, Properties props) throws ExpanderIndexerException {
		if (props.containsKey("method")) {
			String method = props.getProperty("method");
			ExplanationFactory result = newExplanationFactory(props, method, type);
			explanationFactories.put(type, result);
			return result;
		}
		if (explanationFactories.containsKey(type)) {
			return explanationFactories.get(type);
		}
		throw new ExpanderIndexerException("unhandled expansion type: " + type);
	}
	
	private static void setExplanationFactoryProperties(ExplanationFactory factory, Properties props) throws ExpanderIndexerException {
		for (Object key0 : props.keySet()) {
			String key = (String) key0;
			if (key.equals("method")) {
				continue;
			}
			factory.setProperty(key, props.getProperty(key));
		}
	}
	
	private ExplanationFactory newExplanationFactory(Properties props, String method, String type) throws ExpanderIndexerException {
		switch (method) {
		case "term":
			return new TermExplanationFactory(type);
		case "path":
			String separator = props.getProperty("separator", "/");
			return new PathExplanationFactory(this, type, separator.charAt(0));
		default:
			throw new ExpanderIndexerException("unknown explanation method: " + method);
		}
	}

	@Override
	public String getProperty(String key) throws IOException {
		Term term = new Term(FIELD_PROPERTY, key);
		TermQuery termQuery = new TermQuery(term);
		try (IndexSearcher searcher = new IndexSearcher(reader)) {
			TopDocs topDocs = searcher.search(termQuery, 1);
			if (topDocs.totalHits < 1) {
				return null;
			}
			Document doc = reader.document(topDocs.scoreDocs[0].doc);
			return doc.get(FIELD_PROPERTY);
		}
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/expand/ExpanderException.java
@SuppressWarnings("serial")
public class ExpanderException extends Exception {
	public ExpanderException() {
		super();
	}

	public ExpanderException(String message, Throwable cause) {
		super(message, cause);
	}

	public ExpanderException(String message) {
		super(message);
	}

	public ExpanderException(Throwable cause) {
		super(cause);
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/FacetLabelFactory.java
public interface FacetLabelFactory {
	String getFacetLabel(String text) throws Exception;
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/RegexGroupFacetLabelFactory.java
public class RegexGroupFacetLabelFactory implements FacetLabelFactory {
	private final Pattern pattern;
	private final int labelGroup;
	
	public RegexGroupFacetLabelFactory(Pattern pattern, int labelGroup) {
		super();
		this.pattern = pattern;
		this.labelGroup = labelGroup;
	}

	@Override
	public String getFacetLabel(String text) throws Exception {
		Matcher m = pattern.matcher(text);
		if (m.find()) {
			return m.group(labelGroup);
		}
		return text;
	}

}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/UpperCaseFacetLabelFactory.java
public class UpperCaseFacetLabelFactory implements FacetLabelFactory {
	private final FacetLabelFactory facetLabelFactory;

	public UpperCaseFacetLabelFactory(FacetLabelFactory facetLabelFactory) {
		super();
		this.facetLabelFactory = facetLabelFactory;
	}

	@Override
	public String getFacetLabel(String text) throws Exception {
		String facetLabel = facetLabelFactory.getFacetLabel(text);
		return facetLabel.toUpperCase();
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/expand/TextExpander.java
public interface TextExpander {
	TextExpansionResult searchExpansion(String text) throws IOException, ExpanderException;
	TextExpansionResult searchCanonical(String text) throws IOException, ExpanderException;
	ExplanationFactory getExplanationFactory(String type) throws ExpanderException;
	void setExplanationFactory(String type, Properties properties) throws ExpanderException;
	String getProperty(String key) throws IOException;
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/index/NormalizationOptions.java
public abstract class NormalizationOptions {
	/**
	 * Returns a stream of normalized tokens.
	 * @param source raw token stream
	 * @return a stream of normalized tokens
	 */
	public abstract TokenStream getTokenStream(TokenStream source);
	
	private static final class SingleTokenStream extends TokenStream {
		private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
		private final String synonym;
		private boolean available = true;

		private SingleTokenStream(String synonym) {
			super();
			this.synonym = synonym;
		}

		@Override
		public boolean incrementToken() {
			if (available) {
				clearAttributes();
				termAtt.setLength(0);
				termAtt.append(synonym);
				available = false;
				return true;
			}
			return false;
		}

		@Override
		public void close() {
		}
	}
	
	/**
	 * Normalizes the specified text.
	 * @param text token to normalize
	 * @return a stream containing a single normalized token
	 */
	public final TokenStream getTokenStream(String text) {
		TokenStream source = new SingleTokenStream(text);
		return getTokenStream(source);
	}
	
	/**
	 * Normalizes the specified tokens.
	 * @param texts tokens to normalize
	 * @return a stream containing one token for each element of the specified token collection
	 */
	public final TokenStream getTokenStream(Collection<String> texts) {
		TokenStream source = new IteratorTokenStream(texts);
		return getTokenStream(source);
	}
	
	/**
	 * Normalizes the specified text.
	 * @param text token to normalize
	 * @return the normalized text
	 */
	public final String normalize(String text) {
		try {
			TokenStream tokenStream = getTokenStream(text);
			tokenStream.incrementToken();
			CharTermAttribute attr = tokenStream.getAttribute(CharTermAttribute.class);
			String result = attr.toString();
//			System.err.println("result = " + result);
			return result;
		}
		catch (IOException e) {
			// incrementToken() should not throw IOException
			throw new RuntimeException();
		}
	}
	
	private static final class IteratorTokenStream extends TokenStream {
		private final CharTermAttribute attrTermForm = addAttribute(CharTermAttribute.class);
		private final Iterator<String> iterator;
		
		private IteratorTokenStream(Iterator<String> iterator) {
			super();
			this.iterator = iterator;
		}
		
		private IteratorTokenStream(Collection<String> strings) {
			this(strings.iterator());
		}
		
		private IteratorTokenStream(String... strings) {
			this(Arrays.asList(strings));
		}

		@Override
		public boolean incrementToken() throws IOException {
			if (iterator.hasNext()) {
				String token = iterator.next();
				attrTermForm.setLength(0);
				attrTermForm.append(token);
				return true;
			}
			return false;
		}
	}
	
	/**
	 * Normalize the specified texts.
	 * @param texts tokens to normalize
	 * @return the normalized tokens
	 */
	public final String[] normalize(String[] texts) {
		try {
			TokenStream source = new IteratorTokenStream(texts);
			TokenStream tokenStream = getTokenStream(source);
			String[] result = new String[texts.length];
			for (int i = 0; i < texts.length; ++i) {
				boolean increment = tokenStream.incrementToken();
				assert increment;
				CharTermAttribute attr = tokenStream.getAttribute(CharTermAttribute.class);
				result[i] = attr.toString();
			}
			return result;
		}
		catch (IOException e) {
			// incrementToken() should not throw IOException
			throw new RuntimeException();			
		}
	}
	
	/**
	 * Normalize the specified texts.
	 * @param texts tokens to normalize
	 * @return the normalized tokens
	 */
	public final List<String> normalize(Collection<String> texts) {
		try {
			TokenStream source = new IteratorTokenStream(texts);
			TokenStream tokenStream = getTokenStream(source);
			List<String> result = new ArrayList<String>(texts.size());
			while (tokenStream.incrementToken()) {
				CharTermAttribute attr = tokenStream.getAttribute(CharTermAttribute.class);
				result.add(attr.toString());
			}
			assert result.size() == texts.size();
			return result;
		}
		catch (IOException e) {
			// incrementToken() should not throw IOException
			throw new RuntimeException();			
		}
	}

	/**
	 * Instance that does not normalize, passes tokens as is.
	 */
	public static final NormalizationOptions NONE = new NormalizationOptions() {
		@Override
		public TokenStream getTokenStream(TokenStream source) {
			return source;
		}
	};
	
	/**
	 * For multiple normalization filters.
	 * @author rbossy
	 *
	 */
	public static abstract class TokenFilterFactory extends NormalizationOptions {
		private final NormalizationOptions next;

		protected TokenFilterFactory(NormalizationOptions next) {
			super();
			this.next = next;
		}

		@Override
		public TokenStream getTokenStream(TokenStream source) {
			TokenStream next = this.next.getTokenStream(source);
			return createTokenFilter(next);
		}
		
		protected abstract TokenFilter createTokenFilter(TokenStream source);
	}
	
	private static final Version VERSION = Version.LUCENE_36;

	/**
	 * Case-folding normalization.
	 * @author rbossy
	 *
	 */
	public static final class CaseFolding extends TokenFilterFactory {
		public CaseFolding(NormalizationOptions next) {
			super(next);
		}

		@Override
		protected TokenFilter createTokenFilter(TokenStream source) {
			return new LowerCaseFilter(VERSION, source);
		}
	}
	
	/**
	 * ASCII-folding normalization.
	 * @author rbossy
	 *
	 */
	public static final class ASCIIFolding extends TokenFilterFactory {
		public ASCIIFolding(NormalizationOptions next) {
			super(next);
		}

		@Override
		protected TokenFilter createTokenFilter(TokenStream source) {
			return new ASCIIFoldingFilter(source);
		}
	}
	
	/**
	 * Stemming.
	 * @author rbossy
	 *
	 */
	public static final class Stemming extends TokenFilterFactory {
		private final SnowballProgram stemmer;

		public Stemming(NormalizationOptions next, SnowballProgram stemmer) {
			super(next);
			this.stemmer = stemmer;
		}

		@Override
		protected TokenFilter createTokenFilter(TokenStream source) {
			return new SnowballFilter(source, stemmer);
		}
	}
	
	// EnglishPossessiveFilter, ElisionFilter, 

	/**
	 * Converts the specified string as a normalization filter.
	 * @param norm normalization name
	 * @param next other normalization
	 * @return
	 */
	public static NormalizationOptions getFilter(String norm, NormalizationOptions next) {
		NormalizationOptions result = next;
		switch (norm) {
			case "case":
			case "lowercase":
			case "lower":
			case "case-folding":
				result = new CaseFolding(result);
				break;
			case "ascii":
			case "ascii-folding":
				result = new ASCIIFolding(result);
				break;
			case "english":
			case "english-stemming":
				result = new Stemming(result, new EnglishStemmer());
				break;
			case "french":
			case "french-stemming":
				result = new Stemming(result, new FrenchStemmer());
				break;
			default:
				return null;
		}
		return result;
	}

	/**
	 * Default normalization: ASCII folding, case folding and english stemmer.
	 */
	public static final NormalizationOptions DEFAULT = new Stemming(new ASCIIFolding(new CaseFolding(NONE)), new EnglishStemmer());
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/expand/QueryNodeExpanderFactory.java
public enum QueryNodeExpanderFactory {
	BASIC {
		@Override
		public AlvisIRQueryNodeExpander createAlvisIRQueryNodeExpander(TextExpander textExpander) {
			return new BasicQueryNodeExpander(textExpander);
		}

		@Override
		public FacetSubQueryType getDefaultQueryNodeExpanderFactory() {
			return FacetSubQueryType.PHRASE;
		}
	},
	
	ADVANCED {
		@Override
		public AlvisIRQueryNodeExpander createAlvisIRQueryNodeExpander(TextExpander textExpander) {
			return new AdvancedQueryNodeExpander(textExpander);
		}

		@Override
		public FacetSubQueryType getDefaultQueryNodeExpanderFactory() {
			return FacetSubQueryType.RAW;
		}
	};
	
	public abstract AlvisIRQueryNodeExpander createAlvisIRQueryNodeExpander(TextExpander textExpander);
	
	public abstract FacetSubQueryType getDefaultQueryNodeExpanderFactory();
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/FacetSubQueryType.java
public enum FacetSubQueryType {
	TERM {
		@Override
		public String getSubQuery(String field, String text) {
			return field + "=" + QueryParserUtils.quoteTerm(text);
		}

		@Override
		public String toString() {
			return "term";
		}
	},
	
	RAW {
		@Override
		public String getSubQuery(String field, String text) {
			return field + "=(" + QueryParserUtils.quotePhrase(text) + ")";
		}
	},
	
	PHRASE {
		@Override
		public String getSubQuery(String field, String text) {
			return field + "=\"" + QueryParserUtils.quotePhrase(text) + "\"";
		}

		@Override
		public String toString() {
			return "phrase";
		}
	},
	
	PREFIX {
		@Override
		public String getSubQuery(String field, String text) {
			return field + "=" + QueryParserUtils.quoteTerm(text) + "*";
		}

		@Override
		public String toString() {
			return "prefix";
		}
	};

	public abstract String getSubQuery(String field, String text);
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/LowerCaseFacetLabelFactory.java
public class LowerCaseFacetLabelFactory implements FacetLabelFactory {
	private final FacetLabelFactory facetLabelFactory;

	public LowerCaseFacetLabelFactory(FacetLabelFactory facetLabelFactory) {
		super();
		this.facetLabelFactory = facetLabelFactory;
	}

	@Override
	public String getFacetLabel(String text) throws Exception {
		String facetLabel = facetLabelFactory.getFacetLabel(text);
		return facetLabel.toLowerCase();
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/FacetSort.java
public enum FacetSort implements Comparator<FacetTerm> {
	/**
	 * Sort facet terms by frequency (descending)
	 */
	TERM_FREQUENCY {
		@Override
		long getCriterion(FacetTerm info) {
			return info.get();
		}
	},
	
	/**
	 * Sort facet terms by document frequency (descending)
	 */
	DOCUMENT_FREQUENCY {
		@Override
		long getCriterion(FacetTerm info) {
			return info.getDocFreq();
		}
	};

	abstract long getCriterion(FacetTerm info);

	@Override
	public int compare(FacetTerm a, FacetTerm b) {
		return Long.compare(getCriterion(b), getCriterion(a));
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/PrefixFacetSpecification.java
public class PrefixFacetSpecification extends FacetSpecification {
	private final String termPrefix;

	/**
	 * Creates a prefix facet specification.
	 * @param name facet category name
	 * @param fieldName name of the field of terms
	 * @param sort sort criterion
	 * @param cutoff minimal score for a facet term
	 * @param maxTerms maximal number of facet terms
	 * @param termPrefix prefix of facet terms
	 */
	public PrefixFacetSpecification(String name, String fieldName, FacetLabelFactory labelFactory, String queryFieldName, FacetSubQueryType subQueryType, boolean labelQuery, FacetSort sort, int cutoff, int maxFacets, String termPrefix) {
		super(name, fieldName, labelFactory, queryFieldName, subQueryType, labelQuery, sort, cutoff, maxFacets);
		this.termPrefix = termPrefix;
	}

	@Override
	public boolean accept(String x) {
		return x.startsWith(termPrefix);
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/CapitalizingFacetLabelFactory.java
public class CapitalizingFacetLabelFactory implements FacetLabelFactory {
	private final FacetLabelFactory facetLabelFactory;

	public CapitalizingFacetLabelFactory(FacetLabelFactory facetLabelFactory) {
		super();
		this.facetLabelFactory = facetLabelFactory;
	}

	@Override
	public String getFacetLabel(String text) throws Exception {
		String raw = facetLabelFactory.getFacetLabel(text);
		StringBuilder result = new StringBuilder();
		boolean sow = true;
		for (int i = 0; i < raw.length(); ++i) {
			char c = raw.charAt(i);
			if (sow) {
				c = Character.toUpperCase(c);
			}
			result.append(c);
			sow = Character.isWhitespace(c);
		}
		return result.toString();
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/FacetSpecification.java
public abstract class FacetSpecification implements Filter<String> {
	private final String name;
	private final String fieldName;
	private final FacetLabelFactory labelFactory;
	private final String queryFieldName;
	private final FacetSubQueryType subQueryType;
	private final boolean labelQuery;
	private final FacetSort sort;
	private final int cutoff;
	private final int maxTerms;

	/**
	 * Creates a facet specification.
	 * @param name facet category name
	 * @param fieldName name of the field of terms
	 * @param sort sort criterion
	 * @param cutoff minimal score for a facet term
	 * @param maxTerms maximal number of facet terms
	 */
	protected FacetSpecification(String name, String fieldName, FacetLabelFactory labelFactory, String queryFieldName, FacetSubQueryType subQueryType, boolean labelQuery, FacetSort sort, int cutoff, int maxTerms) {
		super();
		this.name = name;
		this.fieldName = fieldName;
		this.labelFactory = labelFactory;
		this.queryFieldName = queryFieldName;
		this.subQueryType = subQueryType;
		this.labelQuery = labelQuery;
		this.sort = sort;
		this.cutoff = cutoff;
		this.maxTerms = maxTerms;
	}

	/**
	 * Returns the facet category name.
	 * @return
	 */
	public String getName() {
		return name;
	}

	/**
	 * Returns the field name of facet terms.
	 * @return
	 */
	public String getFieldName() {
		return fieldName;
	}

	public FacetLabelFactory getLabelFactory() {
		return labelFactory;
	}

	public String getQueryFieldName() {
		return queryFieldName;
	}

	public FacetSubQueryType getSubQueryType() {
		return subQueryType;
	}

	public boolean isLabelQuery() {
		return labelQuery;
	}

	/**
	 * Returns the sort criterion of facet terms.
	 * @return
	 */
	public FacetSort getSort() {
		return sort;
	}

	/**
	 * Returns the minimal score of facet terms.
	 * @return
	 */
	public int getCutoff() {
		return cutoff;
	}

	/**
	 * Returns the maximal number of facet terms.
	 * @return
	 */
	public int getMaxTerms() {
		return maxTerms;
	}
	
	/**
	 * Sort the specified list of facet terms and crop the specified list according to limits provided to the constructor.
	 * @param list
	 */
	public void sortAndApplyLimits(List<FacetTerm> list) {
		int len = list.size();
		if (len <= 1) {
			return;
		}
		Collections.sort(list, sort);
		cropList(list, maxTerms);
		FacetTerm last = list.get(list.size() - 1);
		long lowest = sort.getCriterion(last);
		if (cutoff > lowest) {
			int tailIndex = getTailIndex(list);
			cropList(list, tailIndex);
		}
	}
	
	public String getSubQuery(FacetTerm facetTerm) {
		return subQueryType.getSubQuery(queryFieldName, labelQuery ? facetTerm.getLabel() : facetTerm.getTerm());
	}
	
	private static void cropList(List<FacetTerm> list, int newLen) {
		int len = list.size();
		if (len > newLen) {
			List<FacetTerm> subList = list.subList(newLen, len);
			subList.clear();
		}
	}
	
	private int getTailIndex(List<FacetTerm> list) {
		int lo = 0;
		int hi = list.size();
		while (lo < hi) {
			int mid = (lo + hi) / 2;
			FacetTerm info = list.get(mid);
			long value = sort.getCriterion(info);
			if (value >= cutoff) {
				lo = mid + 1;
			}
			else {
				hi = mid;
			}
		}
		return lo;
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/facet/ExpansionFacetLabelFactory.java
public class ExpansionFacetLabelFactory implements FacetLabelFactory {
	private final TextExpander textExpander;

	public ExpansionFacetLabelFactory(TextExpander textExpander) {
		super();
		this.textExpander = textExpander;
	}

	@Override
	public String getFacetLabel(String text) throws Exception {
		TextExpansionResult textExpansionResult = textExpander.searchCanonical(text);
		String label;
		if (textExpansionResult.any()) {
			label = textExpansionResult.getLabel();
		}
		else {
			label = text;
		}
		return label;
	}
}
alvisir-core/src/main/java/fr/inra/maiage/bibliome/alvisir/core/expand/NullTextExpander.java
public enum NullTextExpander implements TextExpander, TextExpansionResult {
	INSTANCE;

	@Override
	public TextExpansionResult searchExpansion(String text) throws IOException, ExpanderException {
		return this;
	}

	@Override
	public TextExpansionResult searchCanonical(String text) throws IOException, ExpanderException {
		return this;
	}

	@Override
	public ExplanationFactory getExplanationFactory(String type) throws ExpanderException {
		return null;
	}

	@Override
	public void setExplanationFactory(String type, Properties properties) throws ExpanderException {
	}

	@Override
	public boolean any() {
		return false;
	}

	@Override
	public boolean next() throws IOException, ExpanderException {
		return false;
	}

	@Override
	public String getType() {
		return null;
	}

	@Override
	public String getCanonical() {
		return null;
	}

	@Override
	public String[] getSynonyms() {
		return null;
	}

	@Override
	public String getLabel() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public String getProperty(String key) {
		return null;
	}
}


Question: package fr.inra.maiage.bibliome.alvisir.core;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.xml.sax.SAXException;
import fr.inra.maiage.bibliome.alvisir.core.expand.ExpanderException;
import fr.inra.maiage.bibliome.alvisir.core.expand.NullTextExpander;
import fr.inra.maiage.bibliome.alvisir.core.expand.QueryNodeExpanderFactory;
import fr.inra.maiage.bibliome.alvisir.core.expand.TextExpander;
import fr.inra.maiage.bibliome.alvisir.core.expand.index.IndexBasedTextExpander;
import fr.inra.maiage.bibliome.alvisir.core.facet.CapitalizingFacetLabelFactory;
import fr.inra.maiage.bibliome.alvisir.core.facet.ExpansionFacetLabelFactory;
import fr.inra.maiage.bibliome.alvisir.core.facet.FacetLabelFactory;
import fr.inra.maiage.bibliome.alvisir.core.facet.FacetSort;
import fr.inra.maiage.bibliome.alvisir.core.facet.FacetSpecification;
import fr.inra.maiage.bibliome.alvisir.core.facet.FacetSubQueryType;
import fr.inra.maiage.bibliome.alvisir.core.facet.LowerCaseFacetLabelFactory;
import fr.inra.maiage.bibliome.alvisir.core.facet.PrefixFacetSpecification;
import fr.inra.maiage.bibliome.alvisir.core.facet.RegexGroupFacetLabelFactory;
import fr.inra.maiage.bibliome.alvisir.core.facet.UpperCaseFacetLabelFactory;
import fr.inra.maiage.bibliome.alvisir.core.index.NormalizationOptions;
import fr.inra.maiage.bibliome.util.Strings;
import fr.inra.maiage.bibliome.util.xml.XMLUtils;





/**
 * Search configuration XML serializer.
 * @author rbossy
 *
 */
public class SearchConfigXMLSerializer {
	/**
	 * Returns a fresh search object using settings in the specified XML file.
	 * @param path path to the XML file.
	 * @return
	 * @throws IOException
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 * @throws ExpanderException 
	 * @throws SearchConfigException 
	 */
	public static final SearchConfig getSearch(String path) throws IOException, SAXException, ParserConfigurationException, ExpanderException, SearchConfigException {
		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
		Document doc = docBuilder.parse(path);
		String basedir = new File(path).getParent();
		return getSearch(basedir, doc);
	}

	/**
	 * Returns a fresh search object using settings in the specified DOM document.
	 * @param doc DOM document.
	 * @return
	 * @throws IOException
	 * @throws ExpanderException 
	 * @throws SearchConfigException 
	 */
	public static final SearchConfig getSearch(String basedir, Document doc) throws IOException, ExpanderException, SearchConfigException {
		return getSearch(basedir, doc.getDocumentElement());
	}
	
	private static void checkAttribute(Element elt, String attribute) throws SearchConfigException {
		if (!elt.hasAttribute(attribute)) {
			throw new SearchConfigException("missing attribute " + attribute + " in " + elt.getTagName());
		}
	}
	
	private static String getFileAttribute(String basedir, Element elt, String attribute) {
		String value = elt.getAttribute(attribute);
		File f = new File(value);
		if (!f.isAbsolute()) {
			return new File(basedir, value).getAbsolutePath();
		}
		return value;
	}

	/**
	 * Returns a fresh search object using settings in the specified DOM element.
	 * @param elt DOM element.
	 * @return
	 * @throws IOException
	 * @throws ExpanderException 
	 * @throws SearchConfigException 
	 */
	public static final SearchConfig getSearch(String basedir, Element elt) throws IOException, ExpanderException, SearchConfigException {
		SearchConfig result = new SearchConfig();
		checkAttribute(elt, AlvisIRConstants.XML_SEARCH_INDEX_DIR);
		checkAttribute(elt, AlvisIRConstants.XML_SEARCH_DEFAULT_FIELD);
		AlvisIRIndex index = new AlvisIRIndex(getFileAttribute(basedir, elt, AlvisIRConstants.XML_SEARCH_INDEX_DIR));
		TextExpander textExpander = NullTextExpander.INSTANCE;
		if (elt.hasAttribute(AlvisIRConstants.XML_SEARCH_EXPANDER_INDEX_DIR)) {
			IndexReader expanderIndexReader = openIndexReader(getFileAttribute(basedir, elt, AlvisIRConstants.XML_SEARCH_EXPANDER_INDEX_DIR));
Answer: 			textExpander = new IndexBasedTextExpander(expanderIndexReader);