/* * Copyright 2015 the original author or authors. * * 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.springframework.data.aerospike.core; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.support.PropertyComparator; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.aerospike.convert.AerospikeData; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; import org.springframework.data.aerospike.mapping.AerospikeMappingContext; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.mapping.AerospikePersistentProperty; import org.springframework.data.aerospike.mapping.AerospikeSimpleTypes; import org.springframework.data.aerospike.mapping.BasicAerospikePersistentEntity; import org.springframework.data.aerospike.repository.query.AerospikeQueryCreator; import org.springframework.data.aerospike.repository.query.Query; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.keyvalue.core.IterableConverter; import org.springframework.data.keyvalue.core.KeyValueCallback; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.comparator.CompoundComparator; import com.aerospike.client.AerospikeClient; import com.aerospike.client.AerospikeException; import com.aerospike.client.Bin; import com.aerospike.client.Info; import com.aerospike.client.Key; import com.aerospike.client.Operation; import com.aerospike.client.Record; import com.aerospike.client.ScanCallback; import com.aerospike.client.Value; import com.aerospike.client.cluster.Node; import com.aerospike.client.policy.RecordExistsAction; import com.aerospike.client.policy.ScanPolicy; import com.aerospike.client.policy.WritePolicy; import com.aerospike.client.query.Filter; import com.aerospike.client.query.IndexType; import com.aerospike.client.query.KeyRecord; import com.aerospike.client.query.ResultSet; import com.aerospike.client.query.Statement; import com.aerospike.client.task.IndexTask; import com.aerospike.helper.query.KeyRecordIterator; import com.aerospike.helper.query.Qualifier; import com.aerospike.helper.query.QueryEngine; /** * Primary implementation of {@link AerospikeOperations}. * * @author Oliver Gierke * @author Peter Milne */ public class AerospikeTemplate implements AerospikeOperations { private static final MappingAerospikeConverter DEFAULT_CONVERTER = new MappingAerospikeConverter(); private static final AerospikeExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new DefaultAerospikeExceptionTranslator(); private final MappingContext<BasicAerospikePersistentEntity<?>, AerospikePersistentProperty> mappingContext; private final AerospikeClient client; private final MappingAerospikeConverter converter; private final String namespace; private final QueryEngine queryEngine; private AerospikeExceptionTranslator exceptionTranslator; private WritePolicy insertPolicy; private WritePolicy updatePolicy; /** * Creates a new {@link AerospikeTemplate} for the given * {@link AerospikeClient}. * * @param client must not be {@literal null}. */ public AerospikeTemplate(AerospikeClient client, String namespace) { Assert.notNull(client, "Aerospike client must not be null!"); Assert.notNull(namespace, "Namespace cannot be null"); Assert.hasLength(namespace); this.client = client; this.converter = DEFAULT_CONVERTER; this.exceptionTranslator = DEFAULT_EXCEPTION_TRANSLATOR; this.namespace = namespace; this.mappingContext = new AerospikeMappingContext(); this.insertPolicy = new WritePolicy(this.client.writePolicyDefault); this.updatePolicy = new WritePolicy(this.client.writePolicyDefault); this.insertPolicy.recordExistsAction = RecordExistsAction.CREATE_ONLY; this.updatePolicy.recordExistsAction = RecordExistsAction.UPDATE_ONLY; this.queryEngine = new QueryEngine(this.client); loggerSetup(); } private void loggerSetup() { final Logger log = LoggerFactory.getLogger(AerospikeQueryCreator.class); com.aerospike.client.Log .setCallback(new com.aerospike.client.Log.Callback() { @Override public void log(com.aerospike.client.Log.Level level, String message) { switch (level) { case INFO: log.info("AS:" + message); break; case DEBUG: log.debug("AS:" + message); break; case ERROR: log.error("AS:" + message); break; case WARN: log.warn("AS:" + message); break; } } }); } @Override public <T> void createIndex(Class<T> domainType, String indexName, String binName, IndexType indexType) { IndexTask task = client.createIndex(null, this.namespace, domainType.getSimpleName(), indexName, binName, indexType); task.waitTillComplete(); } @Override public void save(Object objectToInsert) { save(objectToInsert, null); } @Override public void save(Object objectToInsert, WritePolicy policy) { Assert.notNull(objectToInsert, "Object to insert must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToInsert, data); Key key = data.getKey(); Bin[] bins = data.getBinsAsArray(); client.put(policy, key, bins); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } public <T> void insertAll(Collection<? extends T> objectsToSave) { for (T element : objectsToSave) { if (element == null) { continue; } insert(element); } } @Override public <T> T insert(T objectToInsert) { return insert(objectToInsert, null); } @Override public <T> T insert(T objectToInsert, WritePolicy policy) { Assert.notNull(objectToInsert, "Object to insert must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToInsert, data); Key key = data.getKey(); Bin[] bins = data.getBinsAsArray(); client.put(policy == null ? this.insertPolicy : policy, key, bins); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } return null; } @Override public void update(Object objectToUpdate) { update( objectToUpdate, null); } @Override public void update(Object objectToUpdate, WritePolicy policy) { Assert.notNull(objectToUpdate, "Object to update must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToUpdate, data); Key key = data.getKey(); Bin[] bins = data.getBinsAsArray(); client.put(policy == null ? this.updatePolicy : policy, key, bins); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @Override public void delete(Class<?> type) { try { ScanPolicy scanPolicy = new ScanPolicy(); scanPolicy.includeBinData = false; final AtomicLong count = new AtomicLong(); client.scanAll(scanPolicy, namespace, type.getSimpleName(), new ScanCallback() { @Override public void scanCallback(Key key, Record record) throws AerospikeException { if (client.delete(null, key)) count.addAndGet(1); /* * after 10,000 records delete, return print the * count. */ if (count.get() % 10000 == 0) { System.out.println("Deleted " + count.get()); } } }, new String[] {}); System.out.println("Deleted " + count + " records from set " + type.getSimpleName()); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @Override public <T> T delete(Serializable id, Class<T> type) { Assert.notNull(id, "Id must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); data.setID(id); data.setSetName(AerospikeSimpleTypes.getColletionName(type)); this.client.delete(null, data.getKey()); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } return null; } @Override public <T> T delete(T objectToDelete) { Assert.notNull(objectToDelete, "Object to delete must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToDelete, data); this.client.delete(null, data.getKey()); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } return null; } @Override public <T> List<T> findAll(final Class<T> type) { // TODO returning a list is dangerous because // the list is unbounded and could contain billions of elements // we need to find another solution final List<T> scanList = new ArrayList<T>(); Iterable<T> results = findAllUsingQuery(type, null, (Qualifier[])null); Iterator<T> iterator = results.iterator(); try { while (iterator.hasNext()) { scanList.add(iterator.next()); } } finally { ((EntityIterator<T>) iterator).close(); } return scanList; } @Override public <T> T findById(Serializable id, Class<T> type) { Assert.notNull(id, "Id must not be null!"); try { AerospikePersistentEntity<?> entity = converter.getMappingContext() .getPersistentEntity(type); Key key = new Key(this.namespace, entity.getSetName(), id.toString()); Record record = this.client.get(null, key); return mapToEntity(key, type, record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @SuppressWarnings("unchecked") @Override public <T> Iterable<T> aggregate(Filter filter, Class<T> outputType, String module, String function, List<Value> arguments) { Assert.notNull(outputType, "Output type must not be null!"); AerospikePersistentEntity<?> entity = converter.getMappingContext() .getPersistentEntity(outputType); Statement statement = new Statement(); if (filter != null) statement.setFilters(filter); statement.setSetName(entity.getSetName()); statement.setNamespace(this.namespace); ResultSet resultSet = null; if (arguments != null && arguments.size() > 0) resultSet = this.client.queryAggregate(null, statement, module, function, arguments.toArray(new Value[0])); else resultSet = this.client.queryAggregate(null, statement); return (Iterable<T>) resultSet; } /** * Configures the {@link AerospikeExceptionTranslator} to be used. * * @param exceptionTranslator can be {@literal null}. */ public void setExceptionTranslator( AerospikeExceptionTranslator exceptionTranslator) { this.exceptionTranslator = exceptionTranslator == null ? DEFAULT_EXCEPTION_TRANSLATOR : exceptionTranslator; } @Override public String getSetName(Class<?> entityClass) { AerospikePersistentEntity<?> entity = converter.getMappingContext() .getPersistentEntity(entityClass); return entity.getSetName(); } @Override public <T> Iterable<T> findAll(Sort sort, Class<T> type) { // TODO Auto-generated method stub return null; } @SuppressWarnings("unused") private static boolean typeCheck(Class<?> requiredType, Object candidate) { return candidate == null ? true : ClassUtils.isAssignable(requiredType, candidate.getClass()); } public boolean exists(Query<?> query, Class<?> entityClass) { if (query == null) { throw new InvalidDataAccessApiUsageException( "Query passed in to exist can't be null"); } Iterator<?> iterator = (Iterator<?>) find(query, entityClass).iterator(); return iterator.hasNext(); } /* * (non-Javadoc) * * @see * org.springframework.data.keyvalue.core.KeyValueOperations#execute(org. * springframework.data.keyvalue.core.KeyValueCallback) */ @Override public <T> T execute(KeyValueCallback<T> action) { Assert.notNull(action, "KeyValueCallback must not be null!"); try { return action.doInKeyValue(null); } catch (RuntimeException e) { throw e; } } /* * (non-Javadoc) * * @see * org.springframework.data.aerospike.core.AerospikeOperations#count(org. * springframework.data.aerospike.repository.query.Query, java.lang.Class) */ @SuppressWarnings("rawtypes") @Override public int count(Query<?> query, Class<?> type) { Assert.notNull(query, "Query must not be null!"); Assert.notNull(type, "Type must not be null!"); int i = 0; Iterator iterator = (Iterator<?>) find(query, type).iterator(); for (; iterator.hasNext(); ++i) iterator.next(); return i; } /* * (non-Javadoc) * * @see * org.springframework.data.aerospike.core.AerospikeOperations#find(org. * springframework.data.aerospike.repository.query.Query, java.lang.Class) */ @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public <T> Iterable<T> find(Query<?> query, Class<T> type) { Assert.notNull(query, "Query must not be null!"); Assert.notNull(type, "Type must not be null!"); List<Qualifier> qualifiers = null; Filter secondaryFilter = null; qualifiers = query.getQueryObject(); if (qualifiers != null && qualifiers.size() > 0) { secondaryFilter = qualifiers.get(0).asFilter(); if (secondaryFilter != null) { qualifiers.remove(0); } } final Iterable<T> results = findAllUsingQuery(type, secondaryFilter, qualifiers.toArray(new Qualifier[qualifiers.size()])); List<?> returnedList = IterableConverter.toList(results); if(results!=null && query.getSort()!=null){ Comparator comparator = aerospikePropertyComparator(query); Collections.sort(returnedList, comparator); } return (Iterable<T>) returnedList; } @SuppressWarnings({ "rawtypes", "unchecked" }) public Comparator<?> aerospikePropertyComparator(Query<?> query ) { if (query == null || query.getSort() == null) { return null; } CompoundComparator compoundComperator = new CompoundComparator(); for (Order order : query.getSort()) { if (Direction.DESC.equals(order.getDirection())) { compoundComperator.addComparator(new PropertyComparator(order.getProperty(), true, false)); }else { compoundComperator.addComparator(new PropertyComparator(order.getProperty(), true, true)); } } return compoundComperator; } /* * (non-Javadoc) * * @see org.springframework.data.aerospike.core.AerospikeOperations# * getMappingContext() */ @Override public MappingContext<?, ?> getMappingContext() { return this.mappingContext; } public String getNamespace() { return namespace; } /* * (non-Javadoc) * * @see * org.springframework.data.keyvalue.core.KeyValueOperations#findInRange( * int, int, org.springframework.data.domain.Sort, java.lang.Class) */ @Override public <T> Iterable<T> findInRange(int offset, int rows, Sort sort, Class<T> type) { Assert.notNull(type, "Type for count must not be null!"); final long rowCount = rows; final AtomicLong count = new AtomicLong(0); final Iterable<T> results = findAllUsingQuery(type, null, (Qualifier[])null); final Iterator<T> iterator = results.iterator(); /* * skip over offset */ for (int skip = 0; skip < offset; skip++) { if (iterator.hasNext()) iterator.next(); } /* * setup the iterable litimed by 'rows' */ Iterable<T> returnList = new Iterable<T>() { @Override public Iterator<T> iterator() { return new Iterator<T>() { @Override public boolean hasNext() { if (count.get() == rowCount) { ((EntityIterator<T>) iterator).close(); return false; } else { return iterator.hasNext(); } } @Override public T next() { if (count.addAndGet(1) <= rowCount) { return iterator.next(); } else { return null; } } @Override public void remove() { } }; } }; return (Iterable<T>) returnList;// TODO:create a sort } @Override public long count(Class<?> type) { Assert.notNull(type, "Type for count must not be null!"); AerospikePersistentEntity<?> entity = converter.getMappingContext() .getPersistentEntity(type); return count(type, entity.getSetName()); } /* * (non-Javadoc) * * @see * org.springframework.data.keyvalue.core.KeyValueOperations#count(java.lang * .Class) */ @Override public long count(Class<?> type, String setName) { Assert.notNull(type, "Type for count must not be null!"); Node[] nodes = client.getNodes(); int replicationCount = 2; int nodeCount = nodes.length; int n_objects = 0; for (Node node : nodes) { String infoString = Info.request(node, "sets/" + this.namespace + "/" + setName); String n_objectsString = infoString.substring( infoString.indexOf("=") + 1, infoString.indexOf(":")); n_objects = Integer.parseInt(n_objectsString); } return (nodeCount > 1) ? n_objects / replicationCount : n_objects; } protected <T> Iterable<T> findAllUsingQuery(Class<T> type, Filter filter, Qualifier... qualifiers) { final Class<T> classType = type; Statement stmt = new Statement(); stmt.setNamespace(this.namespace); stmt.setSetName(this.getSetName(type)); Iterable<T> results = null; final KeyRecordIterator recIterator = this.queryEngine.select( this.namespace, this.getSetName(type), filter, qualifiers); results = new Iterable<T>() { @Override public Iterator<T> iterator() { return new EntityIterator<T>(classType, converter, recIterator); } }; return results; } public class EntityIterator<T> implements CloseableIterator<T> { private KeyRecordIterator keyRecordIterator; private MappingAerospikeConverter converter; private Class<T> type; public EntityIterator(Class<T> type, MappingAerospikeConverter converter, KeyRecordIterator keyRecordIterator) { this.converter = converter; this.type = type; this.keyRecordIterator = keyRecordIterator; } @Override public boolean hasNext() { return this.keyRecordIterator.hasNext(); } @Override public T next() { KeyRecord keyRecord = this.keyRecordIterator.next(); return mapToEntity(keyRecord.key, type, keyRecord.record); } @Override public void close() { try { keyRecordIterator.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void remove() { } } @SuppressWarnings("unchecked") @Override public <T> T prepend(T objectToPrependTo, String fieldName, String value) { Assert.notNull(objectToPrependTo, "Object to prepend to must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToPrependTo, data); Record record = this.client.operate(null, data.getKey(), Operation.prepend(new Bin(fieldName, value)), Operation.get(fieldName)); return mapToEntity(data.getKey(), (Class<T>) objectToPrependTo.getClass(), record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @SuppressWarnings("unchecked") @Override public <T> T prepend(T objectToPrependTo, Map<String, String> values) { Assert.notNull(objectToPrependTo, "Object to prepend to must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToPrependTo, data); Operation[] ops = new Operation[values.size() + 1]; int x = 0; for (Map.Entry<String, String> entry : values.entrySet()) { Bin newBin = new Bin(entry.getKey(), entry.getValue()); ops[x] = Operation.prepend(newBin); x++; } ops[x] = Operation.get(); Record record = this.client.operate(null, data.getKey(), ops); return mapToEntity(data.getKey(), (Class<T>) objectToPrependTo.getClass(), record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @SuppressWarnings("unchecked") @Override public <T> T append(T objectToAppendTo, Map<String, String> values) { Assert.notNull(objectToAppendTo, "Object to append to must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToAppendTo, data); Operation[] ops = new Operation[values.size() + 1]; int x = 0; for (Map.Entry<String, String> entry : values.entrySet()) { Bin newBin = new Bin(entry.getKey(), entry.getValue()); ops[x] = Operation.append(newBin); x++; } ops[x] = Operation.get(); Record record = this.client.operate(null, data.getKey(), ops); return mapToEntity(data.getKey(), (Class<T>) objectToAppendTo.getClass(), record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @SuppressWarnings("unchecked") @Override public <T> T append(T objectToAppendTo, String binName, String value) { Assert.notNull(objectToAppendTo, "Object to append to must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToAppendTo, data); Record record = this.client.operate(null, data.getKey(), Operation.append(new Bin(binName, value)), Operation.get(binName)); return mapToEntity(data.getKey(), (Class<T>) objectToAppendTo.getClass(), record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @SuppressWarnings("unchecked") public <T> T add(T objectToAddTo, Map<String, Long> values) { Assert.notNull(objectToAddTo, "Object to add to must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToAddTo, data); Operation[] operations = new Operation[values.size() + 1]; int x = 0; for (Map.Entry<String, Long> entry : values.entrySet()) { Bin newBin = new Bin(entry.getKey(), entry.getValue()); operations[x] = Operation.add(newBin); x++; } operations[x] = Operation.get(); Record record = this.client.operate(null, data.getKey(), operations); return mapToEntity(data.getKey(), (Class<T>) objectToAddTo.getClass(), record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } @SuppressWarnings("unchecked") public <T> T add(T objectToAddTo, String binName, int value) { Assert.notNull(objectToAddTo, "Object to add to must not be null!"); try { AerospikeData data = AerospikeData.forWrite(this.namespace); converter.write(objectToAddTo, data); Record record = this.client.operate(null, data.getKey(), Operation.add(new Bin(binName, value)), Operation.get()); return mapToEntity(data.getKey(), (Class<T>) objectToAddTo.getClass(), record); } catch (AerospikeException o_O) { DataAccessException translatedException = exceptionTranslator .translateExceptionIfPossible(o_O); throw translatedException == null ? o_O : translatedException; } } private <T> T mapToEntity(Key key, Class<T> type, Record record) { if(record == null) { return null; } AerospikeData data = AerospikeData.forRead(key, record); T readEntity = converter.read(type, data); AerospikePersistentEntity<?> entity = mappingContext.getPersistentEntity(type); if (entity.hasVersionProperty()) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity, readEntity); accessor.setProperty(entity.getVersionProperty(), data.getRecord().generation); } return readEntity; } private ConvertingPropertyAccessor getPropertyAccessor(AerospikePersistentEntity<?> entity, Object source) { PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); return new ConvertingPropertyAccessor(accessor, converter.getConversionService()); } }