package com.bls.mongodb.dao; import com.bls.core.Identifiable; import com.bls.core.geo.Location; import com.bls.core.place.Place; import com.bls.core.user.User; import com.bls.dao.CommonDao; import com.bls.mongodb.core.MongodbMappableIdentifiableEntity; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.mongodb.BasicDBObject; import com.mongodb.DB; import org.mongojack.Aggregation; import org.mongojack.DBQuery; import org.mongojack.JacksonDBCollection; import org.mongojack.internal.MongoJackModule; import java.time.DayOfWeek; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** * Generic mongodb dao operations and mapping to/from core model * * @param <M> Entity type for mongodb mapping entity * @param <I> Entity type for core entity * @param <K> Key type for used inside core entity type */ public abstract class CommonMongodbDao<M extends MongodbMappableIdentifiableEntity, I extends Identifiable<K>, K> implements CommonDao<I> { private final static ObjectMapper MAPPER = MongoJackModule.configure(new ObjectMapper()) .configure(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS, false) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true).registerModule(new JodaModule()); protected final JacksonDBCollection<M, String> dbCollection; public CommonMongodbDao(final DB db) { dbCollection = provideMongodbCollection(db, getMongodbCollectionName(), getMongodbModelType(), String.class); } private JacksonDBCollection<M, String> provideMongodbCollection(final DB db, final String mongoCollectionName, final Class<M> mongodbModelType, final Class<String> keyType) { return JacksonDBCollection.wrap(db.getCollection(mongoCollectionName), mongodbModelType, keyType, MAPPER); } /** @return Type of mondodb entity mapping */ protected abstract Class<M> getMongodbModelType(); /** @return mongodb collection name - default to mongo model class tyoe name */ protected String getMongodbCollectionName() { return getMongodbModelType().getCanonicalName(); } protected I convert2coreModel(final M mongodbEntity) { final I coreEntity = (I) mongodbEntity.getCoreEntity(); coreEntity.setId(checkNotNull((K) mongodbEntity.id, "Missing mongodb generated id")); return coreEntity; } protected M convert2mongodbModel(I coreEntity) { final M mongodbEntity; try { mongodbEntity = getMongodbModelType().newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalStateException(e); } mongodbEntity.coreEntity = coreEntity; if (coreEntity.getId() != null) { mongodbEntity.id = String.valueOf(coreEntity.getId()); } return mongodbEntity; } @Override public List<I> findAll(final User owner) { final BasicDBObject owning = new BasicDBObject("owner.id", new BasicDBObject("$eq", owner.getId())); final List<M> mongodbEntities = dbCollection.find(owning).toArray(); final List<I> coreEntities = Lists.newArrayListWithCapacity(mongodbEntities.size()); coreEntities.addAll(mongodbEntities.stream().map(this::convert2coreModel).collect(Collectors.toList())); return coreEntities; } /** * Returns collection of POIs around {@param location}, within {@param radius} range. * Results may be optionally limited by tags if it's not empty. * Optional {@param user} indicates if the user is logged in or not. * Optional {@param page} enables pagination. Otherwise all matching POIs are returned. * Optional {@param pageSize} specifies number of results per page. (by default set in configuration file) * * @param location {@link Location} * @param radius meters * @param tags collection of tags, may be empty * @param name optional * @param street optional * @param subcat optional * @param paid optional * @param open optional * @param user optional {@link User} used to determine POI owner * @param page optional - specifies a page to return * @param pageSize specifies page size * @return List of POIs matching criteria. */ public List<I> find(final Location location, final Long radius, final Collection<String> tags, final Optional<String> name, final Optional<String> street, final Collection<Place.Subcategory> subcat, final Optional<Boolean> paid, final Optional<Boolean> open, final Optional<User> user, final Optional<Integer> page, final Integer pageSize) { final BasicDBObject query = createQuery(location, radius, tags, name, street, subcat, paid, open, user, page, pageSize); final Aggregation<M> queryWithPaging = addPagingAggregation(query, page, pageSize); final List<M> mongodbEntities = dbCollection.aggregate(queryWithPaging).results(); final List<I> coreEntities = Lists.newArrayListWithCapacity(mongodbEntities.size()); coreEntities.addAll(mongodbEntities.stream().map(this::convert2coreModel).collect(Collectors.toList())); return coreEntities; } private BasicDBObject createQuery(final Location location, final Long radius, final Collection<String> tags, final Optional<String> name, final Optional<String> street, final Collection<Place.Subcategory> subcat, final Optional<Boolean> paid, final Optional<Boolean> open, final Optional<User> user, final Optional<Integer> page, final Integer pageSize) { BasicDBObject additionalQuery = new BasicDBObject(); if (!tags.isEmpty()) { additionalQuery.append("tags", new BasicDBObject("$in", tags)); } if(name.isPresent()){ additionalQuery.append("name", new BasicDBObject("$eq", name.get())); } if(street.isPresent()){ additionalQuery.append("address.street", new BasicDBObject("$eq", street.get())); } if(!subcat.isEmpty()){ additionalQuery.append("subcategory", new BasicDBObject("$in", subcat)); } if(paid.isPresent()){ additionalQuery.append("paid", new BasicDBObject("$eq", paid.get())); } if(open.isPresent() && open.get().equals(true)){ DayOfWeek currentDay = LocalDateTime.now().getDayOfWeek(); String currentTime = LocalTime.now().toString(); additionalQuery.append("opening.day", new BasicDBObject("$eq", currentDay)); additionalQuery.append("opening.open", new BasicDBObject("$lt", currentTime)); additionalQuery.append("opening.close", new BasicDBObject("$gt", currentTime)); } if (user.isPresent()) { additionalQuery.append("owner.id", new BasicDBObject("$eq", user.get().getId())); } else { additionalQuery.append("owner", new BasicDBObject("$eq", null)); } BasicDBObject geoNearParams = new BasicDBObject(); double[] near = {location.getLongitude(), location.getLatitude()}; geoNearParams.append("near", near); geoNearParams.append("spherical", "true"); geoNearParams.append("maxDistance", Math.toRadians(metersToDegrees(radius))); geoNearParams.append("distanceField", "dist"); geoNearParams.append("query", additionalQuery); if (page.isPresent()) { geoNearParams.append("limit", (page.get() + 1) * pageSize); } return new BasicDBObject("$geoNear", geoNearParams); } public Float metersToDegrees(Long radiusInMeters) { checkArgument(radiusInMeters != null, "Radius is required"); return radiusInMeters.floatValue() / 111119.99965975954f; } private Aggregation<M> addPagingAggregation(final BasicDBObject query, final Optional<Integer> page, final int pageSize) { final Aggregation<M> aggregation; final int countToSkip = page.isPresent() ? page.get() * pageSize : 0; if (countToSkip > 0) { final BasicDBObject skip = new BasicDBObject("$skip", countToSkip); aggregation = new Aggregation<>(getMongodbModelType(), query, skip); } else { aggregation = new Aggregation<>(getMongodbModelType(), query); } return aggregation; } @Override public Optional<I> findById(final String coreEntityId) { final M mongodbEntity = dbCollection.findOneById(coreEntityId); return mongodbEntity != null ? Optional.of(convert2coreModel(mongodbEntity)) : Optional.absent(); } @Override public Optional<I> findOneByField(String field, String value) { final BasicDBObject fieldVal = new BasicDBObject(field, new BasicDBObject("$eq", value)); final M mongodbEntity = dbCollection.findOne(fieldVal); return mongodbEntity != null ? Optional.of(convert2coreModel(mongodbEntity)) : Optional.absent(); } @Override public I create(final I coreEntity) { return convert2coreModel(dbCollection.insert(convert2mongodbModel(coreEntity)).getSavedObject()); } @Override public I update(final I coreEntity) { return convert2coreModel(dbCollection.save(convert2mongodbModel(coreEntity)).getSavedObject()); } @Override public void delete(final I coreEntity) { dbCollection.removeById(String.valueOf(checkNotNull(coreEntity.getId(), "Missing entity id"))); } @Override public void deleteById(final String coreEntityId) { dbCollection.removeById(String.valueOf(checkNotNull(coreEntityId, "Missing id"))); } @Override public void deleteAll() { dbCollection.remove(DBQuery.empty()); } }