/* * 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.apache.usergrid.services; import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.shiro.subject.Subject; import org.apache.usergrid.persistence.Entity; import org.apache.usergrid.persistence.EntityRef; import org.apache.usergrid.persistence.Query; import org.apache.usergrid.persistence.Results; import org.apache.usergrid.persistence.Schema; import org.apache.usergrid.persistence.SimpleEntityRef; import org.apache.usergrid.persistence.exceptions.EntityNotFoundException; import org.apache.usergrid.persistence.exceptions.UnexpectedEntityTypeException; import org.apache.usergrid.persistence.Query.Level; import org.apache.usergrid.security.shiro.principals.AdminUserPrincipal; import org.apache.usergrid.security.shiro.principals.ApplicationPrincipal; import org.apache.usergrid.security.shiro.principals.PrincipalIdentifier; import org.apache.usergrid.security.shiro.utils.SubjectUtils; import org.apache.usergrid.services.ServiceResults.Type; import org.apache.usergrid.services.exceptions.ForbiddenServiceOperationException; import org.apache.usergrid.services.exceptions.ServiceResourceNotFoundException; import static org.apache.usergrid.persistence.Schema.TYPE_APPLICATION; import static org.apache.usergrid.utils.ClassUtils.cast; public class AbstractCollectionService extends AbstractService { private static final Logger logger = LoggerFactory.getLogger( AbstractCollectionService.class ); public AbstractCollectionService() { declareMetadataType( "indexes" ); } public AbstractCollectionService(ServiceRequest serviceRequest){ setServiceManager( serviceRequest.getServices() ); } @Override public Entity getEntity( ServiceRequest request, UUID uuid ) throws Exception { if ( !isRootService() ) { return null; } Entity entity = em.get(new SimpleEntityRef(getEntityType(), uuid)); if ( entity != null ) { entity = importEntity( request, entity ); } return entity; } @Override public Entity getEntity( ServiceRequest request, String name ) throws Exception { if ( !isRootService() ) { return null; } String nameProperty = Schema.getDefaultSchema().aliasProperty( getEntityType() ); if ( nameProperty == null ) { nameProperty = "name"; } Entity entity = em.getUniqueEntityFromAlias( getEntityType(), name, false); if ( entity != null ) { entity = importEntity( request, entity ); } return entity; } private EntityRef loadFromId( ServiceContext context, UUID id ) throws Exception { EntityRef entity = null; if ( !context.moreParameters() ) { entity = em.get( new SimpleEntityRef( getEntityType(), id) ); entity = importEntity( context, ( Entity ) entity ); } else { entity = em.get( new SimpleEntityRef( getEntityType(), id) ); } if ( entity == null ) { if (logger.isTraceEnabled()) { logger.trace("miss on entityType: {} with uuid: {}", getEntityType(), id); } String msg = "Cannot find entity associated with uuid: " + id; throw new EntityNotFoundException( msg ); } return entity; } private ServiceResults getItemById( ServiceContext context, UUID id, boolean skipPermissionCheck ) throws Exception { EntityRef entity = loadFromId( context, id ); validateEntityType( entity, id ); if ( !skipPermissionCheck ) { checkPermissionsForEntity( context, entity ); } // check ownership based on graph if ( !em.isCollectionMember( context.getOwner(), context.getCollectionName(), entity ) ) { // the entity is already loaded in the scope of the owner and type ( collection ) so it must exist at this point // if for some reason it's not a member of the collection, it should be and read repair it if( context.getOwner().getType().equals(TYPE_APPLICATION) ){ logger.warn( "Edge missing for entity id {} with owner {}. Executing edge read repair to create new edge in " + "collection {}", id, context.getOwner(), context.getCollectionName()); em.addToCollection( context.getOwner(), context.getCollectionName(), entity); // do a final check to be absolutely sure we're good now before returning back to the client // TODO : Keep thinking if the double-check read after repair is necessary. Favoring stability here if ( !em.isCollectionMember( context.getOwner(), context.getCollectionName(), entity ) ) { logger.error( "Edge read repair failed for entity id {} with owner {} in collection {}", id, context.getOwner(), context.getCollectionName()); throw new ServiceResourceNotFoundException( context ); } } // if not head application, then we can't assume the ownership is meant to be there else{ throw new ServiceResourceNotFoundException( context ); } } List<ServiceRequest> nextRequests = context.getNextServiceRequests( entity ); return new ServiceResults( this, context, Type.COLLECTION, Results.fromRef( entity ), null, nextRequests ); } @Override public ServiceResults getItemById( ServiceContext context, UUID id ) throws Exception { return getItemById( context, id, false ); } @Override public ServiceResults getItemByName( ServiceContext context, String name ) throws Exception { // just get the UUID and then getItemById such that same results are being returned in both cases // don't use uniqueIndexRepair on read only logic UUID entityId = em.getUniqueIdFromAlias( getEntityType(), name, false); if ( entityId == null ) { if (logger.isTraceEnabled()) { logger.trace("Miss on entityType: {} with name: {}", getEntityType(), name); } String msg = "Cannot find entity with name: "+name; throw new EntityNotFoundException( msg ); } return getItemById( context, entityId, false); } @Override public ServiceResults getItemsByQuery( ServiceContext context, Query query ) throws Exception { checkPermissionsForCollection( context ); int count = 1; Level level = Level.REFS; if ( !context.moreParameters() ) { count = 0; level = Level.ALL_PROPERTIES; } if ( context.getRequest().isReturnsTree() ) { level = Level.ALL_PROPERTIES; } query = new Query( query ); query.setResultsLevel( level ); query.setLimit( query.getLimit( count ) ); if ( !query.isReversedSet() ) { query.setReversed( isCollectionReversed( context ) ); } /* * if (count > 0) { query.setMaxResults(count); } */ Results r = em.searchCollection( context.getOwner(), context.getCollectionName(), query ); List<ServiceRequest> nextRequests = null; if ( !r.isEmpty() ) { if ( !context.moreParameters() ) { importEntities( context, r ); } nextRequests = context.getNextServiceRequests( r.getRefs() ); } return new ServiceResults( this, context, Type.COLLECTION, r, null, nextRequests ); } @Override public ServiceResults getCollection( ServiceContext context ) throws Exception { checkPermissionsForCollection( context ); if ( getCollectionSort( context ) != null ) { return getItemsByQuery( context, new Query() ); } if (logger.isTraceEnabled()) { logger.trace("Limiting collection to {}", Query.DEFAULT_LIMIT); } int count = Query.DEFAULT_LIMIT; Results r = em.getCollection( context.getOwner(), context.getCollectionName(), null, count, Level.ALL_PROPERTIES, isCollectionReversed( context ) ); importEntities( context, r ); /* * if (r.isEmpty()) { throw new ServiceResourceNotFoundException(request); } */ return new ServiceResults( this, context, Type.COLLECTION, r, null, null ); } @Override public ServiceResults putItemById( ServiceContext context, UUID id ) throws Exception { if ( context.moreParameters() ) { return getItemById( context, id, true ); } checkPermissionsForEntity( context, id ); Entity item = em.get( id ); if ( item != null ) { validateEntityType( item, id ); if( context.getOwner().getType().equals(TYPE_APPLICATION)) { // this will repair any missing edges em.addToCollection(context.getOwner(), context.getCollectionName(), item); } updateEntity( context, item, context.getPayload() ); item = importEntity( context, item ); } else { String entityType = getEntityType(); item = em.create( id, entityType, context.getPayload().getProperties() ); } return new ServiceResults( this, context, Type.COLLECTION, Results.fromEntity( item ), null, null ); } @Override public ServiceResults putItemByName( ServiceContext context, String name ) throws Exception { if ( context.moreParameters() ) { return getItemByName( context, name ); } // use unique index repair here before any write logic if there are problems Entity entity = em.getUniqueEntityFromAlias( getEntityType(), name, true); if ( entity == null ) { // null entity ref means we tried to put a non-existing entity // before we create a new entity for it, we should check for permission checkPermissionsForCollection(context); Map<String, Object> properties = context.getPayload().getProperties(); if ( !properties.containsKey( "name" ) || !( ( String ) properties.get( "name" ) ).trim().equalsIgnoreCase( name ) ) { properties.put( "name", name ); } entity = em.create( getEntityType(), properties ); } else { entity = importEntity( context, entity ); checkPermissionsForEntity( context, entity ); if( context.getOwner().getType().equals(TYPE_APPLICATION)) { // this will repair any missing edges em.addToCollection(context.getOwner(), context.getCollectionName(), entity); } updateEntity( context, entity ); } return new ServiceResults( this, context, Type.COLLECTION, Results.fromEntity( entity ), null, null ); } @Override public ServiceResults putItemsByQuery( ServiceContext context, Query query ) throws Exception { checkPermissionsForCollection( context ); if ( context.moreParameters() ) { return getItemsByQuery( context, query ); } query = new Query( query ); query.setResultsLevel( Level.ALL_PROPERTIES ); query.setLimit( 1000 ); if ( !query.isReversedSet() ) { query.setReversed( isCollectionReversed( context ) ); } Results r = em.searchCollection( context.getOwner(), context.getCollectionName(), query ); if ( r.isEmpty() ) { throw new ServiceResourceNotFoundException( context ); } updateEntities( context, r ); return new ServiceResults( this, context, Type.COLLECTION, r, null, null ); } @Override public ServiceResults postCollectionSettings( ServiceRequest serviceRequest ) throws Exception { setServiceManager( serviceRequest.getServices() ); ServiceContext context = serviceRequest.getAppContext(); checkPermissionsForCollection( context ); //TODO: write rest test for these line of codes Subject currentUser = SubjectUtils.getSubject(); Object currentUserPrincipal =currentUser.getPrincipal(); Map collectionSchema = null; if(currentUserPrincipal instanceof AdminUserPrincipal) { AdminUserPrincipal adminUserPrincipal = ( AdminUserPrincipal ) currentUserPrincipal; collectionSchema = em.createCollectionSettings( context.getCollectionName(), adminUserPrincipal.getUser().getEmail(), context.getProperties() ); } else if(currentUserPrincipal instanceof ApplicationPrincipal){ collectionSchema = em.createCollectionSettings( context.getCollectionName(), "app credentials", context.getProperties() ); } else if ( currentUserPrincipal instanceof PrincipalIdentifier ) { collectionSchema = em.createCollectionSettings( context.getCollectionName(), "generic credentials", context.getProperties() ); } return new ServiceResults( this, context, Type.COLLECTION, Results.fromData( collectionSchema ), null, null ); } @Override public ServiceResults getCollectionSettings( ServiceRequest serviceRequest ) throws Exception { setServiceManager( serviceRequest.getServices() ); ServiceContext context = serviceRequest.getAppContext(); context.setAction( ServiceAction.GET ); checkPermissionsForCollection( context ); Object collectionSchema = em.getCollectionSettings( context.getCollectionName() ); return new ServiceResults( this, context, Type.COLLECTION, Results.fromData( collectionSchema ), null, null ); } @Override public ServiceResults postCollection( ServiceContext context ) throws Exception { checkPermissionsForCollection( context ); if ( context.getPayload().isBatch() ) { List<Entity> entities = new ArrayList<Entity>(); List<Map<String, Object>> batch = context.getPayload().getBatchProperties(); if (logger.isTraceEnabled()) { logger.trace("Attempting to batch create {} entities in collection {}", batch.size(), context.getCollectionName()); } final Map<String, Boolean> nameValues = new HashMap<>(batch.size()); for ( Map<String, Object> p : batch ) { // track unique name value in the batch to identify if duplicates are trying to be created String name = (String) p.get("name"); if( name !=null && nameValues.get(name) !=null ){ logger.warn("Batch contains more than 1 entity with the same name: {}", name); }else{ nameValues.put(name, true); } if (logger.isTraceEnabled()) { logger.trace("Creating entity [{}] in collection [{}]", p, context.getCollectionName()); } Entity item = null; try { item = em.createItemInCollection( context.getOwner(), context.getCollectionName(), getEntityType(), p ); } catch ( Exception e ) { logger.error("Entity [{}] unable to be created in collection [{}] due to [{} - {}]", p, context.getCollectionName(), e.getClass().getSimpleName(), e.getMessage()); // move on as we can't block the whole batch if only 1 failed continue; } if (logger.isTraceEnabled()) { logger.trace("Successfully created entity [{}] in collection [{}]", p, context.getCollectionName()); } item = importEntity( context, item ); entities.add( item ); } return new ServiceResults( this, context, Type.COLLECTION, Results.fromEntities( entities ), null, null ); } Entity item = em.createItemInCollection( context.getOwner(), context.getCollectionName(), getEntityType(), context.getProperties() ); item = importEntity( context, item ); return new ServiceResults( this, context, Type.COLLECTION, Results.fromEntity( item ), null, null ); } @Override public ServiceResults putCollection( ServiceContext context ) throws Exception { return postCollection( context ); } @Override public ServiceResults postItemsByQuery( ServiceContext context, Query query ) throws Exception { if ( context.moreParameters() ) { return super.postItemsByQuery( context, query ); } return postCollection( context ); } @Override public ServiceResults postItemById( ServiceContext context, UUID id ) throws Exception { if ( context.moreParameters() ) { return getItemById( context, id, true ); } checkPermissionsForEntity( context, id ); Entity entity = em.get( new SimpleEntityRef( this.getEntityType(), id) ); if ( entity == null ) { throw new ServiceResourceNotFoundException( context ); } validateEntityType( entity, id ); entity = importEntity( context, entity ); em.addToCollection( context.getOwner(), context.getCollectionName(), entity ); return new ServiceResults( null, context, Type.COLLECTION, Results.fromEntity( entity ), null, null ); } @Override public ServiceResults postItemByName( ServiceContext context, String name ) throws Exception { if ( context.moreParameters() ) { return super.postItemByName( context, name ); } // use unique index repair here before any write logic if there are problems Entity entity = em.getUniqueEntityFromAlias( getEntityType(), name, true); if ( entity == null ) { throw new ServiceResourceNotFoundException( context ); } return postItemById( context, entity.getUuid() ); } protected boolean isDeleteAllowed( ServiceContext context, Entity entity ) { return true; } protected void prepareToDelete( ServiceContext context, Entity entity ) { if ( !isDeleteAllowed( context, entity ) ) { throw new ForbiddenServiceOperationException( context ); } } @Override public ServiceResults deleteItemById( ServiceContext context, UUID id ) throws Exception { checkPermissionsForEntity( context, id ); if ( context.moreParameters() ) { return getItemById( context, id ); } Entity item = em.get( new SimpleEntityRef( this.getEntityType(), id) ); if ( item == null ) { throw new ServiceResourceNotFoundException( context ); } validateEntityType( item, id ); item = importEntity( context, item ); prepareToDelete( context, item ); em.removeFromCollection( context.getOwner(), context.getCollectionName(), item ); return new ServiceResults( this, context, Type.COLLECTION, Results.fromEntity( item ), null, null ); } @Override public ServiceResults deleteItemByName( ServiceContext context, String name ) throws Exception { if ( context.moreParameters() ) { return getItemByName( context, name ); } // use unique index repair here before any write logic if there are problems Entity entity = em.getUniqueEntityFromAlias( getEntityType(), name, true); if ( entity == null ) { throw new ServiceResourceNotFoundException( context ); } entity = importEntity( context, entity ); checkPermissionsForEntity( context, entity ); prepareToDelete( context, entity ); em.removeFromCollection( context.getOwner(), context.getCollectionName(), entity ); return new ServiceResults( this, context, Type.COLLECTION, Results.fromEntity( entity ), null, null ); } @Override public ServiceResults deleteItemsByQuery( ServiceContext context, Query query ) throws Exception { checkPermissionsForCollection( context ); if ( context.moreParameters() ) { return getItemsByQuery( context, query ); } query = new Query( query ); query.setResultsLevel( Level.ALL_PROPERTIES ); query.setLimit( query.getLimit() ); if ( !query.isReversedSet() ) { query.setReversed( isCollectionReversed( context ) ); } Results r = em.searchCollection( context.getOwner(), context.getCollectionName(), query ); importEntities( context, r ); for ( Entity entity : r ) { prepareToDelete( context, entity ); } for ( Entity entity : r ) { em.removeFromCollection( context.getOwner(), context.getCollectionName(), entity ); } return new ServiceResults( this, context, Type.COLLECTION, r, null, null ); } @Override public ServiceResults getServiceMetadata( ServiceContext context, String metadataType ) throws Exception { if ( "indexes".equals( metadataType ) ) { Set<String> indexes = cast( em.getCollectionIndexes( context.getOwner(), context.getCollectionName() ) ); return new ServiceResults( this, context.getRequest().withPath( context.getRequest().getPath() + "/indexes" ), context.getPreviousResults(), context.getChildPath(), Type.GENERIC, Results.fromData( indexes ), null, null ); } return null; } private void validateEntityType( EntityRef item, UUID id ) throws UnexpectedEntityTypeException { if ( !getEntityType().equalsIgnoreCase( item.getType() ) ) { throw new UnexpectedEntityTypeException( "Entity " + id + " is not the expected type, expected " + getEntityType() + ", found " + item .getType() ); } } }