/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jackrabbit.core.query.lucene; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.regex.Pattern; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.ValueFormatException; import org.apache.jackrabbit.core.value.InternalValue; import org.apache.lucene.document.Document; import org.apache.lucene.document.Fieldable; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.search.Query; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>Util</code> provides various static utility methods. */ public class Util { /** * The logger instance for this class. */ private static final Logger log = LoggerFactory.getLogger(Util.class); /** * Disposes the document <code>old</code>. Closes any potentially open * readers held by the document. * * @param old * the document to dispose. */ public static void disposeDocument(Document old) { for (Fieldable f : old.getFields()) { try { if (f.readerValue() != null) { f.readerValue().close(); } else if (f instanceof LazyTextExtractorField) { LazyTextExtractorField field = (LazyTextExtractorField) f; field.dispose(); } } catch (IOException ex) { log.warn("Exception while disposing index document: " + ex); } } } /** * Returns <code>true</code> if the document is ready to be added to the * index. That is all text extractors have finished their work. * * @param doc * the document to check. * @return <code>true</code> if the document is ready; <code>false</code> * otherwise. */ public static boolean isDocumentReady(Document doc) { for (Fieldable f : doc.getFields()) { if (f instanceof LazyTextExtractorField) { LazyTextExtractorField field = (LazyTextExtractorField) f; if (!field.isExtractorFinished()) { return false; } } } return true; } /** * Depending on the index format this method returns a query that matches * all nodes that have a property with a given <code>name</code>. * * @param name * the property name. * @param version * the index format version. * @return Query that matches all nodes that have a property with the given * <code>name</code>. */ public static Query createMatchAllQuery(String name, IndexFormatVersion version, PerQueryCache cache) { if (version.getVersion() >= IndexFormatVersion.V2.getVersion()) { // new index format style return new JackrabbitTermQuery(new Term(FieldNames.PROPERTIES_SET, name)); } else { return new MatchAllQuery(name, cache); } } /** * Creates an {@link IOException} with <code>t</code> as its cause. * * @param t * the cause. */ public static IOException createIOException(Throwable t) { IOException ex = new IOException(t.getMessage()); ex.initCause(t); return ex; } /** * Depending on the type of the <code>reader</code> this method either * closes or releases the reader. The reader is released if it implements * {@link ReleaseableIndexReader}. * * @param reader * the index reader to close or release. * @throws IOException * if an error occurs while closing or releasing the index * reader. */ public static void closeOrRelease(IndexReader reader) throws IOException { if (reader instanceof ReleaseableIndexReader) { ((ReleaseableIndexReader) reader).release(); } else { reader.close(); } } /** * Returns a comparable for the internal <code>value</code>. * * @param value * an internal value. * @return a comparable for the given <code>value</code>. * @throws RepositoryException * if retrieving the <code>Comparable</code> fails. */ public static Comparable getComparable(InternalValue value) throws RepositoryException { switch (value.getType()) { case PropertyType.BINARY: return null; case PropertyType.BOOLEAN: return value.getBoolean(); case PropertyType.DATE: return value.getDate().getTimeInMillis(); case PropertyType.DOUBLE: return value.getDouble(); case PropertyType.LONG: return value.getLong(); case PropertyType.DECIMAL: return value.getDecimal(); case PropertyType.NAME: return value.getName().toString(); case PropertyType.PATH: return value.getPath().toString(); case PropertyType.URI: case PropertyType.WEAKREFERENCE: case PropertyType.REFERENCE: case PropertyType.STRING: return value.getString(); default: return null; } } /** * Returns a comparable for the internal <code>value</code>. * * @param value * an internal value. * @return a comparable for the given <code>value</code>. * @throws ValueFormatException * if the given <code>value</code> cannot be converted into a * comparable (i.e. unsupported type). * @throws RepositoryException * if an error occurs while converting the value. */ public static Comparable getComparable(Value value) throws ValueFormatException, RepositoryException { switch (value.getType()) { case PropertyType.BOOLEAN: return value.getBoolean(); case PropertyType.DATE: return value.getDate().getTimeInMillis(); case PropertyType.DOUBLE: return value.getDouble(); case PropertyType.LONG: return value.getLong(); case PropertyType.DECIMAL: return value.getDecimal(); case PropertyType.NAME: case PropertyType.PATH: case PropertyType.URI: case PropertyType.WEAKREFERENCE: case PropertyType.REFERENCE: case PropertyType.STRING: return value.getString(); default: throw new RepositoryException("Unsupported type: " + PropertyType.nameFromValue(value.getType())); } } /** * Compares values <code>c1</code> and <code>c2</code>. If the values have * differing types, then the order is defined on the type itself by calling * <code>compareTo()</code> on the respective type class names. * * @param c1 * the first value. * @param c2 * the second value. * @return a negative integer if <code>c1</code> should come before * <code>c2</code><br> * a positive integer if <code>c1</code> should come after * <code>c2</code><br> * <code>0</code> if they are equal. */ public static int compare(Comparable c1, Comparable c2) { if (c1 == c2) { return 0; } else if (c1 == null) { return -1; } else if (c2 == null) { return 1; } else if (c1.getClass() == c2.getClass()) { return c1.compareTo(c2); } else { // differing types -> compare class names String name1 = c1.getClass().getName(); String name2 = c2.getClass().getName(); return name1.compareTo(name2); } } /** * Compares two arrays of Comparable(s) in the same style as * {@link #compare(Value[], Value[])}. * * The 2 methods *have* to work in the same way for the sort to be * consistent */ public static int compare(Comparable<?>[] c1, Comparable<?>[] c2) { if(c1 == null && c2 == null){ return 0; } if (c1 == null) { return -1; } if (c2 == null) { return 1; } for (int i = 0; i < c1.length && i < c2.length; i++) { int d = compare(c1[i], c2[i]); if (d != 0) { return d; } } return c1.length - c2.length; } /** * Compares two arrays of Value(s) in the same style as * {@link #compare(Comparable[], Comparable[])}. * * The 2 methods *have* to work in the same way for the sort to be * consistent */ public static int compare(Value[] a, Value[] b) throws RepositoryException { if(a == null && b == null){ return 0; } if (a == null) { return -1; } if (b == null) { return 1; } for (int i = 0; i < a.length && i < b.length; i++) { int d = compare(a[i], b[i]); if (d != 0) { return d; } } return a.length - b.length; } /** * Compares the two values. If the values have differing types, then an * attempt is made to convert the second value into the type of the first * value. * <p> * Comparison of binary values is not supported. * * @param v1 * the first value. * @param v2 * the second value. * @return result of the comparison as specified in * {@link Comparable#compareTo(Object)}. * @throws ValueFormatException * if the given <code>value</code> cannot be converted into a * comparable (i.e. unsupported type). * @throws RepositoryException * if an error occurs while converting the value. */ public static int compare(Value v1, Value v2) throws ValueFormatException, RepositoryException { Comparable c1 = getComparable(v1); Comparable c2; switch (v1.getType()) { case PropertyType.BOOLEAN: c2 = v2.getBoolean(); break; case PropertyType.DATE: c2 = v2.getDate().getTimeInMillis(); break; case PropertyType.DOUBLE: c2 = v2.getDouble(); break; case PropertyType.LONG: c2 = v2.getLong(); break; case PropertyType.DECIMAL: c2 = v2.getDecimal(); break; case PropertyType.NAME: if (v2.getType() == PropertyType.URI) { String s = v2.getString(); if (s.startsWith("./")) { s = s.substring(2); } // need to decode try { c2 = URLDecoder.decode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RepositoryException(e); } } else { c2 = v2.getString(); } break; case PropertyType.PATH: case PropertyType.REFERENCE: case PropertyType.WEAKREFERENCE: case PropertyType.URI: case PropertyType.STRING: c2 = v2.getString(); break; default: throw new RepositoryException("Unsupported type: " + PropertyType.nameFromValue(v2.getType())); } return compare(c1, c2); } /** * Creates a regexp from <code>likePattern</code>. * * @param likePattern * the pattern. * @return the regular expression <code>Pattern</code>. */ public static Pattern createRegexp(String likePattern) { // - escape all non alphabetic characters // - escape constructs like \<alphabetic char> into \\<alphabetic char> // - replace non escaped _ % into . and .* StringBuffer regexp = new StringBuffer(); boolean escaped = false; for (int i = 0; i < likePattern.length(); i++) { if (likePattern.charAt(i) == '\\') { if (escaped) { regexp.append("\\\\"); escaped = false; } else { escaped = true; } } else { if (Character.isLetterOrDigit(likePattern.charAt(i))) { if (escaped) { regexp.append("\\\\").append(likePattern.charAt(i)); escaped = false; } else { regexp.append(likePattern.charAt(i)); } } else { if (escaped) { regexp.append('\\').append(likePattern.charAt(i)); escaped = false; } else { switch (likePattern.charAt(i)) { case '_': regexp.append('.'); break; case '%': regexp.append(".*"); break; default: regexp.append('\\').append(likePattern.charAt(i)); } } } } } return Pattern.compile(regexp.toString(), Pattern.DOTALL); } /** * Returns length of the internal value. * * @param value * a value. * @return the length of the internal value or <code>-1</code> if the length * cannot be determined. */ public static long getLength(InternalValue value) { if (value.getType() == PropertyType.NAME || value.getType() == PropertyType.PATH) { return -1; } else { try { return value.getLength(); } catch (RepositoryException e) { log.warn("Unable to determine length of value. {}", e.getMessage()); return -1; } } } }