/* * Copyright 2015 JBoss, by Red Hat, Inc * * Licensed 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.uberfire.ext.metadata.backend.lucene.index; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicInteger; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexableField; import org.uberfire.commons.lifecycle.PriorityDisposableRegistry; import org.uberfire.ext.metadata.backend.lucene.fields.FieldFactory; import org.uberfire.ext.metadata.engine.Index; import org.uberfire.ext.metadata.engine.MetaIndexEngine; import org.uberfire.ext.metadata.engine.MetaModelStore; import org.uberfire.ext.metadata.model.KCluster; import org.uberfire.ext.metadata.model.KObject; import org.uberfire.ext.metadata.model.KObjectKey; import org.uberfire.ext.metadata.model.KProperty; import org.uberfire.ext.metadata.model.schema.MetaObject; import org.uberfire.ext.metadata.model.schema.MetaProperty; import org.uberfire.ext.metadata.model.schema.MetaType; import static org.uberfire.commons.validation.Preconditions.checkCondition; import static org.uberfire.commons.validation.Preconditions.checkNotNull; public class LuceneIndexEngine implements MetaIndexEngine { private final FieldFactory fieldFactory; private final MetaModelStore metaModelStore; private final LuceneIndexManager indexManager; private final Map<KCluster, AtomicInteger> batchMode = new ConcurrentHashMap<KCluster, AtomicInteger>(); private final Collection<Runnable> beforeDispose = new ArrayList<Runnable>(); public LuceneIndexEngine(final FieldFactory fieldFactory, final MetaModelStore metaModelStore, final LuceneIndexManager indexManager) { this.fieldFactory = checkNotNull("fieldFactory", fieldFactory); this.metaModelStore = checkNotNull("metaModelStore", metaModelStore); this.indexManager = checkNotNull("indexManager", indexManager); PriorityDisposableRegistry.register(this); } @Override public boolean freshIndex(final KCluster cluster) { final Index index = indexManager.get(cluster); return (index == null || index.freshIndex()) && !batchMode.containsKey(cluster); } @Override public void startBatch(final KCluster cluster) { final AtomicInteger batchStack = batchMode.get(cluster); if (batchStack == null) { batchMode.put(cluster, new AtomicInteger()); } else { if (batchStack.get() < 0) { batchStack.set(1); } else { batchStack.incrementAndGet(); } } } @Override public void index(final KObject object) { updateMetaModel(object); final LuceneIndex index = indexManager.indexOf(object); index.indexDocument(object.getId(), newDocument(object)); commitIfNotBatchMode(index.getCluster()); } private Document newDocument(final KObject object) { final Document doc = new Document(); doc.add(new StringField("id", object.getId(), Field.Store.YES)); doc.add(new StringField("type", object.getType().getName(), Field.Store.YES)); doc.add(new TextField("key", object.getKey(), Field.Store.YES)); doc.add(new StringField("cluster.id", object.getClusterId(), Field.Store.YES)); doc.add(new StringField("segment.id", object.getSegmentId(), Field.Store.YES)); final StringBuilder allText = new StringBuilder(object.getKey()).append('\n'); for (final KProperty<?> property : object.getProperties()) { final IndexableField[] fields = fieldFactory.build(property); for (final IndexableField field : fields) { doc.add(field); if (field instanceof TextField && !(property.getValue() instanceof Boolean)) { allText.append(field.stringValue()).append('\n'); } } } //Only create a "full text" entry if required if (object.fullText()) { doc.add(new TextField(FULL_TEXT_FIELD, allText.toString().toLowerCase(), Field.Store.NO)); } return doc; } @Override public void index(final KObject... objects) { for (final KObject object : objects) { index(object); } } @Override public void rename(final KObjectKey from, final KObject to) { checkNotNull("from", from); checkNotNull("to", to); checkCondition("renames are allowed only from same cluster", from.getClusterId().equals(to.getClusterId())); final LuceneIndex index = indexManager.indexOf(from); index.rename(from.getId(), newDocument(to)); commitIfNotBatchMode(index.getCluster()); } @Override public void delete(KCluster cluster) { indexManager.delete(cluster); } @Override public void delete(final KObjectKey objectKey) { final LuceneIndex index = indexManager.indexOf(objectKey); index.deleteIfExists(objectKey.getId()); commitIfNotBatchMode(index.getCluster()); } @Override public void delete(final KObjectKey... objectsKey) { final Map<LuceneIndex, List<String>> execution = new HashMap<LuceneIndex, List<String>>(); for (final KObjectKey key : objectsKey) { final LuceneIndex index = indexManager.indexOf(key); final List<String> ids = execution.get(index); if (ids == null) { execution.put(index, new ArrayList<String>() {{ add(key.getId()); }}); } else { ids.add(key.getId()); } } for (final Map.Entry<LuceneIndex, List<String>> entry : execution.entrySet()) { entry.getKey().deleteIfExists(entry.getValue().toArray(new String[entry.getValue().size()])); } } @Override public void commit(final KCluster cluster) { final Index index = indexManager.get(cluster); if (index == null) { return; } final AtomicInteger batchStack = batchMode.get(cluster); if (batchStack != null) { int value = batchStack.decrementAndGet(); if (value <= 0) { index.commit(); batchMode.remove(cluster); } } else { index.commit(); } } private synchronized void commitIfNotBatchMode(final KCluster cluster) { final AtomicInteger batchStack = batchMode.get(cluster); if (batchStack == null || batchStack.get() <= 0) { commit(cluster); } } @Override public void dispose() { if (!beforeDispose.isEmpty()) { for (final Runnable activeDispose : beforeDispose) { activeDispose.run(); } } } @Override public int priority() { return 50; } @Override public void beforeDispose(final Runnable callback) { this.beforeDispose.add(checkNotNull("callback", callback)); } private void updateMetaModel(final KObject object) { final MetaObject metaObject = metaModelStore.getMetaObject(object.getType().getName()); if (metaObject == null) { metaModelStore.add(newMetaObect(object)); } else { for (final KProperty property : object.getProperties()) { final MetaProperty metaProperty = metaObject.getProperty(property.getName()); if (metaProperty == null) { metaObject.addProperty(newMetaProperty(property)); } else { metaProperty.addType(property.getValue().getClass()); if (property.isSearchable()) { metaProperty.setAsSearchable(); } } } metaModelStore.update(metaObject); } } private MetaObject newMetaObect(final KObject object) { final Set<MetaProperty> properties = new HashSet<MetaProperty>(); for (final KProperty<?> property : object.getProperties()) { properties.add(newMetaProperty(property)); } return new MetaObject() { private final Map<String, MetaProperty> propertyMap = new ConcurrentHashMap<String, MetaProperty>() {{ for (final MetaProperty property : properties) { put(property.getName(), property); } }}; @Override public MetaType getType() { return object.getType(); } @Override public Collection<MetaProperty> getProperties() { return propertyMap.values(); } @Override public MetaProperty getProperty(final String name) { return propertyMap.get(name); } @Override public void addProperty(final MetaProperty metaProperty) { if (!propertyMap.containsKey(metaProperty.getName())) { propertyMap.put(metaProperty.getName(), metaProperty); } } }; } private MetaProperty newMetaProperty(final KProperty<?> property) { return new MetaProperty() { private boolean isSearchable = property.isSearchable(); private Set<Class<?>> types = new CopyOnWriteArraySet<Class<?>>() {{ add(property.getValue().getClass()); }}; @Override public String getName() { return property.getName(); } @Override public Set<Class<?>> getTypes() { return types; } @Override public boolean isSearchable() { return isSearchable; } @Override public void setAsSearchable() { this.isSearchable = true; } @Override public void addType(final Class<?> type) { types.add(type); } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (!(obj instanceof MetaProperty)) { return false; } return ((MetaProperty) obj).getName().equals(getName()); } @Override public int hashCode() { return getName().hashCode(); } }; } }