org.slc.sli.api.resources.generic.service.DefaultResourceService.java Source code

Java tutorial

Introduction

Here is the source code for org.slc.sli.api.resources.generic.service.DefaultResourceService.java

Source

/*
 * Copyright 2012-2013 inBloom, Inc. and its affiliates.
 *
 * 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.slc.sli.api.resources.generic.service;

import java.net.URI;
import java.util.*;

import javax.annotation.PostConstruct;
import javax.ws.rs.core.Response;
import javax.xml.bind.DatatypeConverter;

import org.apache.commons.lang3.StringUtils;
import org.slc.sli.common.constants.EntityNames;
import org.slc.sli.common.util.datetime.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;

import org.slc.sli.api.constants.Constraints;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;

import org.slc.sli.api.config.AssociationDefinition;
import org.slc.sli.api.config.EntityDefinition;
import org.slc.sli.api.constants.ResourceConstants;
import org.slc.sli.api.constants.ResourceNames;
import org.slc.sli.api.criteriaGenerator.GranularAccessFilter;
import org.slc.sli.api.criteriaGenerator.GranularAccessFilterProvider;
import org.slc.sli.api.migration.ApiSchemaAdapter;
import org.slc.sli.api.model.ModelProvider;
import org.slc.sli.api.representation.EntityBody;
import org.slc.sli.api.resources.generic.PreConditionFailedException;
import org.slc.sli.api.resources.generic.representation.Resource;
import org.slc.sli.api.resources.generic.representation.ServiceResponse;
import org.slc.sli.api.resources.generic.util.ResourceHelper;
import org.slc.sli.api.selectors.LogicalEntity;
import org.slc.sli.api.selectors.UnsupportedSelectorException;
import org.slc.sli.api.service.EntityNotFoundException;
import org.slc.sli.api.service.query.ApiQuery;
import org.slc.sli.api.util.SecurityUtil;
import org.slc.sli.aspect.ApiMigrationAspect.MigratePostedEntity;
import org.slc.sli.aspect.ApiMigrationAspect.MigrateResponse;
import org.slc.sli.common.constants.ParameterConstants;
import org.slc.sli.domain.CalculatedData;
import org.slc.sli.domain.NeutralCriteria;
import org.slc.sli.domain.NeutralQuery;
import org.slc.sli.modeling.uml.ClassType;

/**
 * Default implementation of the resource service.
 *
 * @author srupasinghe
 * @author jstokes
 * @author pghosh
 */

@Component("defaultResourceService")
public class DefaultResourceService implements ResourceService {

    private static final Logger LOG = LoggerFactory.getLogger(DefaultResourceService.class);

    @Autowired
    private LogicalEntity logicalEntity;

    @Autowired
    private ResourceHelper resourceHelper;

    @Autowired
    private ModelProvider provider;

    @Autowired
    private ResourceServiceHelper resourceServiceHelper;

    @Autowired
    private GranularAccessFilterProvider granularAccessFilterProvider;

    @Autowired
    private ApiSchemaAdapter adapter;
    private Map<String, String> endDates = new HashMap<String, String>();

    private static final String POST = "POST";
    private static final String GET = "GET";
    private static final String PUT = "PUT";

    @PostConstruct
    public void init() {
        endDates.put(ResourceNames.STUDENT_SCHOOL_ASSOCIATIONS, "exitWithdrawDate");

        endDates.put(ResourceNames.STUDENT_COHORT_ASSOCIATIONS, "endDate");
        endDates.put(ResourceNames.STUDENT_PROGRAM_ASSOCIATIONS, "endDate");
        endDates.put(ResourceNames.STUDENT_SECTION_ASSOCIATIONS, "endDate");
    }

    /**
     * @author jstokes
     */
    protected static interface ServiceLogic {
        public ServiceResponse run(final Resource resource, EntityDefinition definition);
    }

    protected ServiceResponse handle(final Resource resource, ServiceLogic logic) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        ServiceResponse serviceResponse = logic.run(resource, definition);

        return serviceResponse;
    }

    @Override
    @MigrateResponse
    public ServiceResponse getEntitiesByIds(final Resource resource, final String idList, final URI requestURI) {

        return handle(resource, new ServiceLogic() {
            @Override
            public ServiceResponse run(final Resource resource, EntityDefinition definition) {
                final int idLength = idList.split(",").length;

                if (idLength > Constraints.MAX_MULTIPLE_UUIDS) {
                    String errorMessage = "Too many GUIDs: " + idLength + " (input) vs "
                            + Constraints.MAX_MULTIPLE_UUIDS + " (allowed)";
                    LOG.error(errorMessage);
                    throw new PreConditionFailedException(errorMessage);
                }

                final List<String> ids = Arrays.asList(idList.split(","));

                ApiQuery apiQuery = resourceServiceHelper.getApiQuery(definition, requestURI);

                apiQuery.addCriteria(new NeutralCriteria("_id", "in", ids));
                apiQuery.setLimit(0);
                apiQuery.setOffset(0);

                // final/resulting information
                List<EntityBody> finalResults = null;
                finalResults = (List<EntityBody>) getEntityBodies(apiQuery, definition, resource.getResourceType());

                if (idLength == 1 && finalResults.isEmpty()) {
                    throw new EntityNotFoundException(ids.get(0));
                }

                //inject error entities if needed
                finalResults = injectErrors(definition, ids, finalResults);

                return new ServiceResponse(adapter.migrate(finalResults, definition.getResourceName(), GET),
                        idLength);
            }
        });
    }

    @Override
    @MigrateResponse
    public ServiceResponse getEntities(final Resource resource, final URI requestURI,
            final boolean getAllEntities) {

        return handle(resource, new ServiceLogic() {
            @Override
            public ServiceResponse run(final Resource resource, final EntityDefinition definition) {
                try {
                    Iterable<EntityBody> entityBodies = null;
                    final ApiQuery apiQuery = resourceServiceHelper.getApiQuery(definition, requestURI);
                    addAdditionalCriteria(apiQuery);
                    addGranularAccessCriteria(definition, apiQuery);

                    if (apiQuery.isCountOnly()) {
                        entityBodies = new ArrayList<EntityBody>();
                    } else {
                        if (getAllEntities) {
                            entityBodies = SecurityUtil
                                    .sudoRun(new SecurityUtil.SecurityTask<Iterable<EntityBody>>() {

                                        @Override
                                        public Iterable<EntityBody> execute() {
                                            Iterable<EntityBody> entityBodies = null;
                                            try {
                                                entityBodies = logicalEntity.getEntities(apiQuery,
                                                        resource.getResourceType());
                                            } catch (UnsupportedSelectorException e) {
                                                entityBodies = definition.getService().list(apiQuery);
                                            }

                                            return entityBodies;
                                        }
                                    });
                        } else {
                            entityBodies = getEntityBodies(apiQuery, definition, definition.getResourceName());
                        }
                    }
                    long count = getEntityCount(definition, apiQuery);

                    return new ServiceResponse(
                            adapter.migrate((List<EntityBody>) entityBodies, definition.getResourceName(), GET),
                            count);
                } catch (NoGranularAccessDatesException e) {
                    List<EntityBody> entityBodyList = Collections.emptyList();
                    return new ServiceResponse(entityBodyList, 0);
                }
            }
        });
    }

    protected void addAdditionalCriteria(final NeutralQuery apiQuery) {
        // no-op
    }

    protected long getEntityCount(EntityDefinition definition, ApiQuery apiQuery) {
        long count = 0;

        if (definition.getService() == null) {
            return count;
        }

        if (apiQuery == null) {
            return definition.getService().count(new NeutralQuery());
        }

        int originalLimit = apiQuery.getLimit();
        int originalOffset = apiQuery.getOffset();
        apiQuery.setLimit(0);
        apiQuery.setOffset(0);

        if (SecurityUtil.isStaffUser()) {
            count = definition.getService().countBasedOnContextualRoles(apiQuery);
        } else {
            count = definition.getService().count(apiQuery);
        }

        apiQuery.setLimit(originalLimit);
        apiQuery.setOffset(originalOffset);

        return count;
    }

    @Override
    @MigratePostedEntity
    public String postEntity(final Resource resource, EntityBody entity) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);
        List<String> entityIds = new ArrayList<String>();
        if (SecurityUtil.isStaffUser()) {
            entityIds = definition.getService()
                    .createBasedOnContextualRoles(adapter.migrate(entity, definition.getResourceName(), POST));
        } else {
            entityIds = definition.getService().create(adapter.migrate(entity, definition.getResourceName(), POST));
        }
        return StringUtils.join(entityIds.toArray(), ",");
    }

    @Override
    @MigratePostedEntity
    public void putEntity(Resource resource, String id, EntityBody entity) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        EntityBody copy = new EntityBody(entity);
        copy.remove(ResourceConstants.LINKS);

        List<EntityBody> migratedCopies = adapter.migrate(copy, definition.getResourceName(), PUT);
        if (migratedCopies.size() != 1) {
            throw new IllegalStateException("Error occurred while processing entity body.");
        }

        definition.getService().update(id, migratedCopies.get(0), SecurityUtil.isStaffUser());
    }

    @Override
    @MigratePostedEntity
    public void patchEntity(Resource resource, String id, EntityBody entity) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        EntityBody copy = new EntityBody(entity);
        copy.remove(ResourceConstants.LINKS);

        //if isStaffUser == true then enforce contextual roles, otherwise do not
        definition.getService().patch(id, copy, SecurityUtil.isStaffUser());
    }

    @Override
    public void deleteEntity(Resource resource, String id) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        if (SecurityUtil.isStaffUser()) {
            definition.getService().deleteBasedOnContextualRoles(id);
        } else {
            definition.getService().delete(id);
        }
    }

    @Override
    public String getEntityType(Resource resource) {
        return resourceHelper.getEntityDefinition(resource).getType();
    }

    @Override
    public CalculatedData<String> getCalculatedData(Resource resource, String id) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        return definition.getService().getCalculatedValues(id);
    }

    @Override
    public CalculatedData<Map<String, Integer>> getAggregateData(Resource resource, String id) {
        EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        return definition.getService().getAggregates(id);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    @MigrateResponse
    public ServiceResponse getEntities(final Resource base, final String id, final Resource resource,
            final URI requestURI) {
        final EntityDefinition definition = resourceHelper.getEntityDefinition(resource);

        if (isAssociation(base)) {
            final String key = "_id";
            return getAssociatedEntities(base, base, id, resource, key, requestURI);
        }

        try {
            final String associationKey = getConnectionKey(base, resource);
            List<EntityBody> entityBodyList = new ArrayList<EntityBody>(); // DS-1046 - initialize in case we don't fill this in
            List<String> valueList = Arrays.asList(id.split(","));

            final ApiQuery apiQuery = resourceServiceHelper.getApiQuery(definition, requestURI);

            addGranularAccessCriteria(definition, apiQuery);

            //Mongo blows up if we have multiple $in or equal criteria for the same key.
            //To avoid that case, if we do have duplicate keys, set the value for that
            //criteria to the intersection of the two critiera values
            boolean skipIn = false;
            for (NeutralCriteria crit : apiQuery.getCriteria()) {
                if (crit.getKey().equals(associationKey) && (crit.getOperator().equals(NeutralCriteria.CRITERIA_IN)
                        || crit.getOperator().equals(NeutralCriteria.OPERATOR_EQUAL))) {
                    skipIn = true;
                    Set valueSet = new HashSet();
                    if (crit.getValue() instanceof Collection) {
                        valueSet.addAll((Collection) crit.getValue());
                    } else {
                        valueSet.add(crit.getValue());
                    }
                    valueSet.retainAll(valueList);
                    crit.setValue(valueSet);
                }
            }

            if (!skipIn) {
                apiQuery.addCriteria(new NeutralCriteria(associationKey, "in", valueList));
            }

            // if we are doing only a count, don't execute the query that returns the body list
            // DS-1046
            if (!apiQuery.isCountOnly()) {
                entityBodyList = (List<EntityBody>) getEntityBodies(apiQuery, definition,
                        definition.getResourceName());
            }

            long count = getEntityCount(definition, apiQuery);
            return new ServiceResponse(adapter.migrate(entityBodyList, definition.getResourceName(), GET), count);
        } catch (NoGranularAccessDatesException e) {
            List<EntityBody> entityBodyList = Collections.emptyList();
            return new ServiceResponse(entityBodyList, 0);
        }
    }

    @Override
    public ServiceResponse getEntities(Resource base, String id, Resource association, Resource resource,
            URI requestUri) {
        final String associationKey = getConnectionKey(base, association);
        return getAssociatedEntities(base, association, id, resource, associationKey, requestUri);
    }

    private boolean isAssociation(Resource base) {
        boolean isAssociation = false;

        final EntityDefinition baseEntityDef = resourceHelper.getEntityDefinition(base);
        ClassType baseEntityType = provider.getClassType(StringUtils.capitalize(baseEntityDef.getType()));

        if (baseEntityType.isAssociation()) {
            isAssociation = true;
        } else if (baseEntityDef instanceof AssociationDefinition) {
            isAssociation = true;
        }

        return isAssociation;
    }

    @SuppressWarnings("unchecked")
    private ServiceResponse getAssociatedEntities(final Resource base, final Resource association, final String id,
            final Resource resource, final String associationKey, final URI requestUri) {
        List<String> valueList = Arrays.asList(id.split(","));
        final List<String> filteredIdList = new ArrayList<String>();

        final EntityDefinition finalEntity = resourceHelper.getEntityDefinition(resource);
        final EntityDefinition assocEntity = resourceHelper.getEntityDefinition(association);
        final EntityDefinition baseEntity = resourceHelper.getEntityDefinition(base);

        String resourceKey = getConnectionKey(association, resource);
        String key = "_id";

        try {
            final ApiQuery apiQuery = resourceServiceHelper.getApiQuery(assocEntity);

            addGranularAccessCriteria(assocEntity, apiQuery);

            apiQuery.setLimit(0);
            apiQuery.addCriteria(new NeutralCriteria(associationKey, "in", valueList));
            if (association.getResourceType().equals(ResourceNames.STUDENT_SCHOOL_ASSOCIATIONS) && requestUri
                    .getPath().matches("^/api/rest/[^/]+/schools/[^/]+/studentSchoolAssociations/students")) {
                apiQuery.addOrQuery(new NeutralQuery(new NeutralCriteria(ParameterConstants.EXIT_WITHDRAW_DATE,
                        NeutralCriteria.CRITERIA_EXISTS, false)));
                apiQuery.addOrQuery(new NeutralQuery(
                        new NeutralCriteria(ParameterConstants.EXIT_WITHDRAW_DATE, NeutralCriteria.CRITERIA_GTE,
                                DateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd")))));
            }

            Iterable<EntityBody> entityList;
            if (SecurityUtil.isStaffUser()) {
                entityList = assocEntity.getService().listBasedOnContextualRoles(apiQuery);
            } else {
                entityList = assocEntity.getService().list(apiQuery);
            }

            for (EntityBody entityBody : entityList) {
                List<String> filteredIds = entityBody.getValues(resourceKey);
                if ((filteredIds == null) || (filteredIds.isEmpty())) {
                    key = resourceKey;
                    if (associationKey.equals("_id")) {
                        filteredIdList.addAll(valueList);
                        break;

                    } else {
                        resourceKey = "id";
                        filteredIds = entityBody.getValues(resourceKey);
                    }
                }
                filteredIdList.addAll(filteredIds);
            }

            List<EntityBody> entityBodyList;
            final ApiQuery finalApiQuery = resourceServiceHelper.getApiQuery(finalEntity, requestUri);

            addGranularAccessCriteria(finalEntity, finalApiQuery);

            //skip this when someone is querying on public entities
            if (!(EntityNames.isPublic(finalEntity.getType()) && finalApiQuery.getCriteria().size() > 1)) {

                //Mongo blows up if we have multiple $in or equal criteria for the same key.
                //To avoid that case, if we do have duplicate keys, set the value for that
                //criteria to the intersection of the two critiera values
                boolean skipIn = false;
                for (NeutralCriteria crit : finalApiQuery.getCriteria()) {
                    if (crit.getKey().equals(key) && (crit.getOperator().equals(NeutralCriteria.CRITERIA_IN)
                            || crit.getOperator().equals(NeutralCriteria.OPERATOR_EQUAL))) {
                        skipIn = true;
                        Set<Object> valueSet = new HashSet<Object>();
                        if (crit.getValue() instanceof Collection) {
                            valueSet.addAll((Collection<Object>) crit.getValue());
                        } else {
                            valueSet.add(crit.getValue());
                        }
                        valueSet.retainAll(filteredIdList);
                        crit.setValue(valueSet);
                    }
                }

                if (!skipIn) {
                    finalApiQuery.addCriteria(new NeutralCriteria(key, "in", filteredIdList));
                }

            }

            // if we're only getting the count of this query, just create an empty list, otherwise
            // execute the query to get the bodies - DS-1046, providing an API for retrieving counts only.
            if (finalApiQuery.isCountOnly()) {
                entityBodyList = new ArrayList<EntityBody>();
            } else {
                entityBodyList = (List<EntityBody>) getEntityBodies(finalApiQuery, finalEntity,
                        finalEntity.getResourceName());
            }

            long count = getEntityCount(finalEntity, finalApiQuery);

            return new ServiceResponse(adapter.migrate(entityBodyList, finalEntity.getResourceName(), GET), count);
        } catch (NoGranularAccessDatesException e) {
            List<EntityBody> entityBodyList = Collections.emptyList();
            return new ServiceResponse(entityBodyList, 0);
        }
    }

    private boolean finalEntityReferencesAssociation(EntityDefinition finalEntity, EntityDefinition assocEntity,
            String referenceField) {
        return !assocEntity.getReferenceFields().containsKey(referenceField)
                && finalEntity.getReferenceFields().containsKey(referenceField);
    }

    private String getConnectionKey(final Resource fromEntity, final Resource toEntity) {
        final EntityDefinition toEntityDef = resourceHelper.getEntityDefinition(toEntity);
        final EntityDefinition fromEntityDef = resourceHelper.getEntityDefinition(fromEntity);

        ClassType fromEntityType = provider.getClassType(StringUtils.capitalize(fromEntityDef.getType()));
        ClassType toEntityType = provider.getClassType(StringUtils.capitalize(toEntityDef.getType()));

        return provider.getConnectionPath(fromEntityType, toEntityType);
    }

    protected List<EntityBody> injectErrors(EntityDefinition definition, final List<String> ids,
            List<EntityBody> finalResults) {
        int idLength = ids.size();

        if (idLength > 1) {
            Collections.sort(finalResults, new Comparator<EntityBody>() {
                @Override
                public int compare(EntityBody o1, EntityBody o2) {
                    return ids.indexOf(o1.get("id")) - ids.indexOf(o2.get("id"));
                }
            });

            int finalResultsSize = finalResults.size();

            // loop if results quantity does not matched requested quantity
            for (int i = 0; finalResultsSize != idLength && i < idLength; i++) {

                String checkedId = ids.get(i);

                boolean checkedIdMissing = false;

                try {
                    checkedIdMissing = !(finalResults.get(i).get("id").equals(checkedId));
                } catch (IndexOutOfBoundsException ioobe) {
                    checkedIdMissing = true;
                }

                // if a particular input ID is not present in the results at the appropriate
                // spot
                if (checkedIdMissing) {

                    Map<String, Object> errorResult = new HashMap<String, Object>();

                    // try individual lookup to capture specific error message (type)
                    try {
                        definition.getService().get(ids.get(i));
                    } catch (EntityNotFoundException enfe) {
                        errorResult.put("type", "Not Found");
                        errorResult.put("message", "Entity not found: " + checkedId);
                        errorResult.put("code", Response.Status.NOT_FOUND.getStatusCode());
                    } catch (AccessDeniedException ade) {
                        errorResult.put("type", "Forbidden");
                        errorResult.put("message", "Access DENIED: " + ade.getMessage());
                        errorResult.put("code", Response.Status.FORBIDDEN.getStatusCode());
                    } /* catch (Exception e) {
                      errorResult.put("type", "Internal Server Error");
                      errorResult.put("message", "Internal Server Error: " + e.getMessage());
                      errorResult.put("code", Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
                      } */

                    finalResults.add(i, new EntityBody(errorResult));
                }
            }
        }

        return finalResults;
    }

    private void addGranularAccessCriteria(EntityDefinition entity, ApiQuery apiQuery)
            throws NoGranularAccessDatesException {

        if (granularAccessFilterProvider.hasFilter()) {
            GranularAccessFilter filter = granularAccessFilterProvider.getFilter();

            if (entity.getType().equals(filter.getEntityName())) {

                if (filter.isNoSessionsFoundForSchoolYear()) {
                    throw new NoGranularAccessDatesException();
                }

                NeutralQuery dateQuery = filter.getNeutralQuery();
                for (NeutralCriteria criteria : dateQuery.getCriteria()) {
                    apiQuery.addCriteria(criteria);
                }
                for (NeutralQuery dateOrQuery : dateQuery.getOrQueries()) {
                    apiQuery.addOrQuery(dateOrQuery);
                }
            }
        }
    }

    private List<EntityBody> getTimeFilteredAssociations(List<EntityBody> associations, EntityDefinition baseEntity,
            EntityDefinition assocEntity) {
        List<EntityBody> filtered = new ArrayList<EntityBody>(associations);
        if (!baseEntity.getResourceName().equals(ResourceNames.STUDENTS)) {
            for (EntityBody associationEntity : associations) {
                if ((this.endDates.keySet().contains(assocEntity.getResourceName())
                        && !isCurrent(assocEntity, associationEntity))) {
                    filtered.remove(associationEntity);
                }
            }
        }

        return filtered;
    }

    private boolean isCurrent(EntityDefinition def, EntityBody body) {
        String now = DateUtils.getFormattedDate(DateUtils.DateUtilFormat.DATE_YYYY_MM_dd, new Date());

        String assocEnd = (String) body.get(this.endDates.get(def.getResourceName()));

        // Absent end date means association is 'current'
        if (assocEnd == null) {
            assocEnd = "6999-12-12"; // infinity
        }

        return now.compareTo(assocEnd) < 0;
    }

    private Iterable<EntityBody> getEntityBodies(ApiQuery apiQuery, EntityDefinition definition, String type) {

        Iterable<EntityBody> entityBodies = null;

        try {
            entityBodies = logicalEntity.getEntities(apiQuery, type);
        } catch (UnsupportedSelectorException e) {
            if (SecurityUtil.isStaffUser()) {
                entityBodies = definition.getService().listBasedOnContextualRoles(apiQuery);
            } else {
                entityBodies = definition.getService().list(apiQuery);
            }
        }
        return entityBodies;
    }

    /**
     * Indicates that for a valid granular access query, no sessions and hence
     * no beginDate or endDate were found.
     */
    private class NoGranularAccessDatesException extends Exception {
        private static final long serialVersionUID = 1L;
    }

}