/*
* Copyright (c) 2008-2012, Hazel Bilisim Ltd. All Rights Reserved.
*
* 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 com.hazelcast.query;
import com.hazelcast.core.MapEntry;
import com.hazelcast.impl.Record;
import com.hazelcast.nio.Data;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import static com.hazelcast.nio.IOUtil.toObject;
public class MapIndexService {
private final ConcurrentMap<Long, Record> records = new ConcurrentHashMap<Long, Record>(10000, 0.75f, 1);
private final Index indexValue;
private final Map<Expression, Index> mapIndexes = new ConcurrentHashMap<Expression, Index>(4, 0.75f, 1);
private final Object indexTypesLock = new Object();
private volatile boolean hasIndexedAttributes = false;
@SuppressWarnings("VolatileArrayField")
private volatile byte[] indexTypes = null;
private final AtomicInteger size = new AtomicInteger(0);
public MapIndexService(boolean valueIndexed) {
indexValue = (valueIndexed) ? new Index(null, false, -1) : null;
}
public void remove(Record record) {
Record existingRecord = records.remove(record.getId());
if (existingRecord != null) {
size.decrementAndGet();
}
}
public void index(Record record) {
final Long recordId = record.getId();
if (record.isActive()) {
final Record anotherRecord = records.putIfAbsent(recordId, record);
if (anotherRecord != null) {
record = anotherRecord;
} else {
size.incrementAndGet();
}
} else {
remove(record);
}
if (indexValue != null) {
Long newValueIndex = -1L;
if (record.isActive() && record.hasValueData()) {
newValueIndex = (long) record.getValueData().hashCode();
}
indexValue.index(newValueIndex, record);
}
Long[] indexValues = record.getIndexes();
if (indexValues != null && hasIndexedAttributes) {
byte[] indexTypes = record.getIndexTypes();
if (indexTypes == null || indexValues.length != indexTypes.length) {
throw new IllegalArgumentException("index and types don't match " + Arrays.toString(indexTypes));
}
Collection<Index> indexes = mapIndexes.values();
for (Index index : indexes) {
if (indexValues.length > index.getAttributeIndex()) {
Long newValue = indexValues[index.getAttributeIndex()];
index.index(newValue, record);
}
}
}
}
public Collection<Record> getOwnedRecords() {
return records.values();
}
public Long[] getIndexValues(Object value) {
if (hasIndexedAttributes) {
int indexCount = mapIndexes.size();
Long[] newIndexes = new Long[indexCount];
if (value instanceof Data) {
value = toObject((Data) value);
}
Collection<Index> indexes = mapIndexes.values();
for (Index index : indexes) {
int attributedIndex = index.getAttributeIndex();
newIndexes[attributedIndex] = index.extractLongValue(value);
}
byte[] _indexTypes = indexTypes;
if (_indexTypes == null || _indexTypes.length != indexCount) {
synchronized (indexTypesLock) {
_indexTypes = indexTypes;
if (_indexTypes == null || _indexTypes.length != indexCount) {
_indexTypes = new byte[indexCount];
for (Index index : indexes) {
int attributedIndex = index.getAttributeIndex();
_indexTypes[attributedIndex] = index.getIndexType();
}
indexTypes = _indexTypes;
}
}
}
return newIndexes;
}
return null;
}
public byte[] getIndexTypes() {
return indexTypes;
}
public Index addIndex(Expression expression, boolean ordered, int attributeIndex) {
Index index = mapIndexes.get(expression);
if (index == null) {
if (size() > 0) {
StringBuilder sb = new StringBuilder("Index can only be added before adding entries!");
sb.append("\n");
sb.append("Add indexes first and only once then put entries.");
throw new RuntimeException(sb.toString());
}
if (attributeIndex == -1) {
attributeIndex = mapIndexes.size();
}
index = new Index(expression, ordered, attributeIndex);
mapIndexes.put(expression, index);
indexTypes = null;
//todo build the indexes
hasIndexedAttributes = true;
}
return index;
}
public Set<MapEntry> doQuery(QueryContext queryContext) {
boolean strong = false;
Set<MapEntry> results;
Predicate predicate = queryContext.getPredicate();
try {
if (predicate != null && mapIndexes != null && predicate instanceof IndexAwarePredicate) {
List<IndexAwarePredicate> lsIndexAwarePredicates = new ArrayList<IndexAwarePredicate>();
IndexAwarePredicate iap = (IndexAwarePredicate) predicate;
strong = iap.collectIndexAwarePredicates(lsIndexAwarePredicates, mapIndexes);
if (strong) {
Set<Index> setAppliedIndexes = new HashSet<Index>(1);
iap.collectAppliedIndexes(setAppliedIndexes, mapIndexes);
if (setAppliedIndexes.size() > 0) {
for (Index index : setAppliedIndexes) {
if (strong) {
strong = index.isStrong();
}
}
}
}
int indexAwarePredicateCount = lsIndexAwarePredicates.size();
if (indexAwarePredicateCount == 1) {
IndexAwarePredicate indexAwarePredicate = lsIndexAwarePredicates.get(0);
Set<MapEntry> sub = indexAwarePredicate.filter(queryContext);
if (sub == null || sub.size() == 0) {
return null;
} else {
return sub;
}
} else if (indexAwarePredicateCount > 0) {
IndexAwarePredicate indexAwarePredicateFirst = lsIndexAwarePredicates.get(0);
Set<MapEntry> subFirst = indexAwarePredicateFirst.filter(queryContext);
if (subFirst != null && subFirst.size() < 11) {
strong = true;
Set<MapEntry> resultSet = new HashSet<MapEntry>(subFirst);
for (int i = 1; i < lsIndexAwarePredicates.size(); i++) {
IndexAwarePredicate p = lsIndexAwarePredicates.get(i);
Iterator<MapEntry> it = resultSet.iterator();
while (it.hasNext()) {
Record record = (Record) it.next();
if (!p.apply(record)) {
it.remove();
}
}
}
return resultSet;
} else if (subFirst != null) {
List<Set<MapEntry>> lsSubResults = new ArrayList<Set<MapEntry>>(indexAwarePredicateCount);
lsSubResults.add(subFirst);
Set<MapEntry> smallestSet = subFirst;
for (int i = 1; i < indexAwarePredicateCount; i++) {
IndexAwarePredicate p = lsIndexAwarePredicates.get(i);
Set<MapEntry> sub = p.filter(queryContext);
if (sub == null) {
strong = false;
} else if (sub.size() == 0) {
strong = true;
return null;
} else {
if (sub.size() < smallestSet.size()) {
smallestSet = sub;
}
lsSubResults.add(sub);
}
}
if (smallestSet == null) {
return null;
}
results = new HashSet<MapEntry>(smallestSet.size());
Iterator<MapEntry> it = smallestSet.iterator();
smallestLoop:
while (it.hasNext()) {
MapEntry entry = it.next();
for (Set<MapEntry> sub : lsSubResults) {
if (!sub.contains(entry)) {
continue smallestLoop;
}
}
results.add(entry);
}
return results;
}
}
}
// no matching condition yet!
// return everything.
return new SingleResultSet(records);
} finally {
queryContext.setStrong(strong);
}
}
public Map<Expression, Index> getIndexes() {
return mapIndexes;
}
public boolean hasIndexedAttributes() {
return hasIndexedAttributes;
}
public Index getValueIndex() {
return indexValue;
}
public boolean containsValue(Data value) {
if (indexValue != null) {
Set<MapEntry> results = indexValue.getRecords((long) value.hashCode());
if (results == null || results.size() == 0) return false;
for (MapEntry entry : results) {
Record record = (Record) entry;
if (record.containsValue(value)) {
return true;
}
}
} else {
for (Record record : records.values()) {
if (record.containsValue(value)) {
return true;
}
}
}
return false;
}
public Index[] getIndexesInOrder() {
if (mapIndexes.size() == 0) return null;
Index[] indexes = new Index[mapIndexes.size()];
for (Index index : mapIndexes.values()) {
indexes[index.getAttributeIndex()] = index;
}
return indexes;
}
public void appendState(StringBuffer sbState) {
byte[] _indexTypes = indexTypes;
sbState.append("\nIndex- records: " + records.size() + ", mapIndexes:"
+ mapIndexes.size() + ", indexTypes:" + ((_indexTypes == null) ? 0 : _indexTypes.length));
for (Index index : mapIndexes.values()) {
index.appendState(sbState);
}
}
public void clear() {
mapIndexes.clear();
records.clear();
size.set(0);
}
public int size() {
return size.get();
}
}