/*
* 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.exoplatform.services.jcr.impl.core.query.lucene;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.FilterIndexReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.index.TermPositions;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Version;
import org.exoplatform.services.jcr.impl.core.query.lucene.directory.DirectoryManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* <code>IndexMigration</code> implements a utility that migrates a Jackrabbit
* 1.4.x index to version 1.5. Until version 1.4.x, indexes used the character
* '\uFFFF' to separate the name of a property from the value. As of Lucene
* 2.3 this does not work anymore. See LUCENE-1221. Jackrabbit {@literal >=} 1.5 uses
* the character '[' as a separator. Whenever an index is opened from disk, a
* quick check is run to find out whether a migration is required. See also
* JCR-1363 for more details.
*/
public class IndexMigration
{
/**
* The logger instance for this class.
*/
private static final Logger log = LoggerFactory.getLogger("exo.jcr.component.core.IndexMigration");
/**
* Checks if the given <code>index</code> needs to be migrated.
*
* @param index the index to check and migration if needed.
* @param directoryManager the directory manager.
* @throws IOException if an error occurs while migrating the index.
*/
public static void migrate(PersistentIndex index, DirectoryManager directoryManager) throws IOException
{
Directory indexDir = index.getDirectory();
log.debug("Checking {} ...", indexDir);
ReadOnlyIndexReader reader = index.getReadOnlyIndexReader();
try
{
if (IndexFormatVersion.getVersion(reader).getVersion() >= IndexFormatVersion.V3.getVersion())
{
// index was created with Jackrabbit 1.5 or higher
// no need for migration
log.debug("IndexFormatVersion >= V3, no migration needed");
return;
}
// assert: there is at least one node in the index, otherwise the
// index format version would be at least V3
TermEnum terms = reader.terms(new Term(FieldNames.PROPERTIES, ""));
try
{
Term t = terms.term();
if (t.text().indexOf('\uFFFF') == -1)
{
log.debug("Index already migrated");
return;
}
}
finally
{
terms.close();
}
}
finally
{
reader.release();
index.releaseWriterAndReaders();
}
// if we get here then the index must be migrated
log.debug("Index requires migration {}", indexDir);
String migrationName = index.getName() + "_v2.3";
if (directoryManager.hasDirectory(migrationName))
{
directoryManager.delete(migrationName);
}
Directory migrationDir = directoryManager.getDirectory(migrationName);
try
{
IndexWriter writer =
new IndexWriter(migrationDir, new IndexWriterConfig(Version.LUCENE_36, new JcrStandartAnalyzer()));
try
{
IndexReader r = new MigrationIndexReader(IndexReader.open(index.getDirectory()));
try
{
writer.addIndexes(new IndexReader[]{r});
writer.close();
}
finally
{
r.close();
}
}
finally
{
writer.close();
}
}
finally
{
migrationDir.close();
}
directoryManager.delete(index.getName());
if (!directoryManager.rename(migrationName, index.getName()))
{
throw new IOException("failed to move migrated directory " + migrationDir);
}
log.info("Migrated " + index.getName());
}
//---------------------------< internal helper >----------------------------
/**
* An index reader that migrates stored field values and term text on the
* fly.
*/
private static class MigrationIndexReader extends FilterIndexReader
{
public MigrationIndexReader(IndexReader in)
{
super(in);
}
public Document document(int n, FieldSelector fieldSelector) throws CorruptIndexException, IOException
{
Document doc = super.document(n, fieldSelector);
Fieldable[] fields = doc.getFieldables(FieldNames.PROPERTIES);
if (fields != null)
{
doc.removeFields(FieldNames.PROPERTIES);
for (int i = 0; i < fields.length; i++)
{
String value = fields[i].stringValue();
value = value.replace('\uFFFF', '[');
doc.add(new Field(FieldNames.PROPERTIES, value, Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS));
}
}
return doc;
}
public TermEnum terms() throws IOException
{
return new MigrationTermEnum(in.terms());
}
public TermPositions termPositions() throws IOException
{
return new MigrationTermPositions(in.termPositions());
}
private static class MigrationTermEnum extends FilterTermEnum
{
public MigrationTermEnum(TermEnum in)
{
super(in);
}
public Term term()
{
Term t = super.term();
if (t == null)
{
return t;
}
if (t.field().equals(FieldNames.PROPERTIES))
{
String text = t.text();
return t.createTerm(text.replace('\uFFFF', '['));
}
else
{
return t;
}
}
TermEnum unwrap()
{
return in;
}
}
private static class MigrationTermPositions extends FilterTermPositions
{
public MigrationTermPositions(TermPositions in)
{
super(in);
}
public void seek(Term term) throws IOException
{
if (term.field().equals(FieldNames.PROPERTIES))
{
char[] text = term.text().toCharArray();
text[term.text().indexOf('[')] = '\uFFFF';
super.seek(term.createTerm(new String(text)));
}
else
{
super.seek(term);
}
}
public void seek(TermEnum termEnum) throws IOException
{
if (termEnum instanceof MigrationTermEnum)
{
super.seek(((MigrationTermEnum)termEnum).unwrap());
}
else
{
super.seek(termEnum);
}
}
}
}
}