org.jasig.ssp.service.impl.WatchStudentServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.ssp.service.impl.WatchStudentServiceImpl.java

Source

/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.ssp.service.impl;

import com.google.common.collect.Maps;
import org.hibernate.exception.ConstraintViolationException;
import org.jasig.ssp.dao.DirectoryPersonSearchDao;
import org.jasig.ssp.dao.ObjectExistsException;
import org.jasig.ssp.dao.WatchStudentDao;
import org.jasig.ssp.factory.PersonSearchRequestTOFactory;
import org.jasig.ssp.factory.WatchStudentTOFactory;
import org.jasig.ssp.model.ObjectStatus;
import org.jasig.ssp.model.Person;
import org.jasig.ssp.model.PersonSearchRequest;
import org.jasig.ssp.model.PersonSearchResult2;
import org.jasig.ssp.model.WatchStudent;
import org.jasig.ssp.model.reference.ProgramStatus;
import org.jasig.ssp.service.AbstractPersonAssocAuditableService;
import org.jasig.ssp.service.ObjectNotFoundException;
import org.jasig.ssp.service.PersonSearchService;
import org.jasig.ssp.service.PersonService;
import org.jasig.ssp.service.SecurityService;
import org.jasig.ssp.service.WatchStudentService;
import org.jasig.ssp.service.jobqueue.AbstractJobExecutor;
import org.jasig.ssp.service.jobqueue.AbstractPersonSearchBasedJobExecutor;
import org.jasig.ssp.service.jobqueue.AbstractPersonSearchBasedJobQueuer;
import org.jasig.ssp.service.jobqueue.BasePersonSearchBasedJobExecutionState;
import org.jasig.ssp.service.jobqueue.JobExecutionResult;
import org.jasig.ssp.service.jobqueue.JobService;
import org.jasig.ssp.service.reference.ConfigService;
import org.jasig.ssp.transferobject.ImmutablePersonIdentifiersTO;
import org.jasig.ssp.transferobject.WatchStudentTO;
import org.jasig.ssp.transferobject.form.BulkWatchChangeJobSpec;
import org.jasig.ssp.transferobject.form.BulkWatchChangeRequestForm;
import org.jasig.ssp.transferobject.form.BulkWatchChangeTO;
import org.jasig.ssp.transferobject.jobqueue.JobTO;
import org.jasig.ssp.util.csvwriter.CaseloadCsvWriterHelper;
import org.jasig.ssp.util.sort.PagingWrapper;
import org.jasig.ssp.util.sort.SortingAndPaging;
import org.jasig.ssp.web.api.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
 * WatchStudent service implementation
 */
@Service
public class WatchStudentServiceImpl extends AbstractPersonAssocAuditableService<WatchStudent>
        implements WatchStudentService, InitializingBean {

    private static final Logger OUTER_CLASS_LOGGER = LoggerFactory.getLogger(PersonProgramStatusServiceImpl.class);

    private static final ThreadLocal<Logger> CURRENT_LOGGER = new ThreadLocal<Logger>();

    public static final String BULK_WATCH_CHANGE_JOB_EXECUTOR_NAME = "bulk-watch-executor";
    private static final String BULK_WATCH_CHANGE_BATCH_SIZE_CONFIG_NAME = "watch_bulk_change_batch_size";
    private static final String BULK_WATCH_CHANGE_MAX_DLQ_SIZE_CONFIG_NAME = "watch_bulk_change_max_dlq_size";
    private static final String BULK_WATCH_CHANGE_FAIL_ON_DLQ_OVERFLOW_CONFIG_NAME = "watch_bulk_change_fail_on_dlq_overflow";
    // could be created or deleted watch IDs
    private static final String WATCH_ID_FIELD_NAME = "watchId";

    @Autowired
    private transient WatchStudentDao dao;
    @Autowired
    protected transient WatchStudentTOFactory watchStudentTOFactory;
    @Autowired
    private transient DirectoryPersonSearchDao directoryPersonDao;
    @Autowired
    private transient PersonService personService;
    @Autowired
    private transient SecurityService securityService;
    @Autowired
    private transient PersonSearchRequestTOFactory personSearchRequestFactory;
    @Autowired
    private transient PersonSearchService personSearchService;
    @Autowired
    private transient JobService jobService;
    @Autowired
    private transient ConfigService configService;
    @Autowired
    private transient PlatformTransactionManager transactionManager;

    private static class BulkWatchChangeJobExecutionState extends BasePersonSearchBasedJobExecutionState {
        public int personsSkippedCount; // b/c they're external or already have the requested status
    }

    private AbstractJobExecutor<BulkWatchChangeJobSpec, BulkWatchChangeJobExecutionState> bulkJobExecutor;

    private AbstractPersonSearchBasedJobQueuer<BulkWatchChangeRequestForm, BulkWatchChangeJobSpec> bulkJobQueuer;

    @Override
    protected WatchStudentDao getDao() {
        return dao;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        initBulkWatchChangeJobExecutor();
        initBulkWatchChangeJobQueuer();
    }

    @Override
    public WatchStudent get(UUID watcherId, UUID studentId) {
        return dao.getStudentWatcherRelationShip(watcherId, studentId);
    }

    @Override
    public PagingWrapper<PersonSearchResult2> watchListFor(ProgramStatus programStatus, Person person,
            SortingAndPaging sAndP) {
        PersonSearchRequest form = new PersonSearchRequest();
        form.setWatcher(person);
        form.setProgramStatus(programStatus);
        form.setSortAndPage(sAndP);
        return directoryPersonDao.search(form);
    }

    @Override
    public WatchStudent create(final WatchStudent obj)
            throws ObjectNotFoundException, ValidationException, ObjectExistsException {
        try {
            return super.create(obj);
        } catch (final ConstraintViolationException e) {
            if (e.getConstraintName().equalsIgnoreCase("watch_student_watcher_id_student_id_key")) {
                final LinkedHashMap<String, UUID> lookupKeys = Maps.newLinkedHashMap();
                lookupKeys.put("watcherId", obj.getPerson().getId());
                lookupKeys.put("studentId", obj.getStudent().getId());
                throw new ObjectExistsException(WatchStudent.class.getName(), lookupKeys, e);
            }
            throw new ObjectExistsException("Constraint violation trying to store a WatchStudent record", e);
        }
    }

    @Override
    public void delete(final UUID id) throws ObjectNotFoundException {
        final WatchStudent current = getDao().get(id);
        dao.delete(current);
    }

    @Override
    // Would really rather make this non-transactional, but @Transactional is being set
    // well above us in the inheritance hierarchy so the best we can do is readOnly
    @Transactional(readOnly = true)
    public void exportWatchListFor(PrintWriter writer, ProgramStatus programStatus, Person person,
            SortingAndPaging buildSortAndPage) throws IOException {
        PersonSearchRequest form = new PersonSearchRequest();
        form.setWatcher(person);
        form.setProgramStatus(programStatus);
        form.setSortAndPage(buildSortAndPage);
        directoryPersonDao.exportableSearch(new CaseloadCsvWriterHelper(writer), form);
    }

    @Override
    public Long watchListCountFor(ProgramStatus programStatus, Person person, SortingAndPaging buildSortAndPage) {
        PersonSearchRequest form = new PersonSearchRequest();
        form.setWatcher(person);
        form.setProgramStatus(programStatus);
        form.setSortAndPage(buildSortAndPage);
        return directoryPersonDao.getCaseloadCountFor(form, buildSortAndPage);
    }

    @Override
    @Transactional(rollbackFor = { ObjectNotFoundException.class, IOException.class, ValidationException.class })
    public JobTO changeInBulk(BulkWatchChangeRequestForm form)
            throws IOException, ObjectNotFoundException, ValidationException {
        return bulkJobQueuer.enqueueJob(form);
    }

    /**
     * Intentionally private b/c we do not want this to participate in this service's "transactional-by-default"
     * public interface. Its transaction is managed by the {@code JobExecutor} assumed to be invoking it.
     *
     */
    private Map<String, ?> changeSingleFromBulkRequest(ImmutablePersonIdentifiersTO personIds,
            BulkWatchChangeRequestForm coreSpec) throws ValidationException, ObjectNotFoundException {
        final Map<String, UUID> rslt = Maps.newLinkedHashMap();
        final UUID targetPersonId = personIds.getId();
        final UUID watcherId = coreSpec.getWatchSpec().getWatcherId();
        final BulkWatchChangeTO.BulkWatchOperation operation = coreSpec.getWatchSpec().getOperation();
        if (targetPersonId == null) {
            getCurrentLogger().debug(
                    "Skipping Watch change [{}] for target person [{}] and watcher [{}] "
                            + "because that person has no operational record.",
                    new Object[] { operation, personIds, watcherId });
            return rslt;
        }
        WatchStudent watch = this.get(watcherId, targetPersonId);
        if (watch == null || watch.getObjectStatus() != ObjectStatus.ACTIVE) {
            if (operation == BulkWatchChangeTO.BulkWatchOperation.UNWATCH) {
                getCurrentLogger().debug("Skipping UNWATCH operation for target person [{}] and watcher [{}] "
                        + "because the change would be redudant.", personIds, watcherId);
                watch = null;
            } else {
                if (watch != null) {
                    watch.setObjectStatus(ObjectStatus.ACTIVE);
                    // Was hard to know whether to implement as a pure skip or just freshen the date b/c we don't
                    // actually do anything with the date. Decided to go ahead and freshen so its a little bit easier
                    // to correlate watch records with watch requests
                    watch.setWatchDate(new Date());
                    watch = save(watch);
                } else {
                    final WatchStudentTO watchTO = new WatchStudentTO();
                    watchTO.setStudentId(targetPersonId);
                    watchTO.setWatcherId(watcherId);
                    watchTO.setWatchDate(new Date());
                    // will raise ObjectNotFoundException if either target or watcher not found
                    watch = watchStudentTOFactory.from(watchTO);
                    watch = this.create(watch);
                }
            }
        } else {
            if (operation == BulkWatchChangeTO.BulkWatchOperation.UNWATCH) {
                this.delete(watch.getId());
            } else {
                // Was hard to know whether to implement as a pure skip or just freshen the date b/c we don't actually
                // do anything with the date. Decided to go ahead and freshen so its a little bit easier to correlate
                // watch records with watch requests. (Same as above)
                watch.setWatchDate(new Date());
                watch = save(watch);
            }
        }
        rslt.put(WATCH_ID_FIELD_NAME, watch == null ? null : watch.getId());
        return rslt;
    }

    private void initBulkWatchChangeJobExecutor() {
        this.bulkJobExecutor = new AbstractPersonSearchBasedJobExecutor<BulkWatchChangeJobSpec, BulkWatchChangeJobExecutionState>(
                BULK_WATCH_CHANGE_JOB_EXECUTOR_NAME, jobService, transactionManager, null, personSearchService,
                personSearchRequestFactory, configService) {
            private final Logger logger = LoggerFactory
                    .getLogger(WatchStudentServiceImpl.this.getClass().getName() + ".BulkWatchChangeJobExecutor");

            /**
             * The actual 'important' override... all the rest of the overrides are mostly boilerplate.
             */
            @Override
            protected Map<String, ?> executeForSinglePerson(ImmutablePersonIdentifiersTO personIds,
                    BulkWatchChangeJobSpec executionSpec, BulkWatchChangeJobExecutionState executionState,
                    UUID jobId) throws ValidationException, ObjectNotFoundException {
                return changeSingleFromBulkRequest(personIds, executionSpec.getCoreSpec());
            }

            @Override
            protected JobExecutionResult<BulkWatchChangeJobExecutionState> executeJobDeserialized(
                    BulkWatchChangeJobSpec executionSpec, BulkWatchChangeJobExecutionState executionState,
                    UUID jobId) {
                // TODO this copy pasted all over in these executor subclasses... abstract somehow
                try {
                    WatchStudentServiceImpl.this.setCurrentLogger(logger);
                    return super.executeJobDeserialized(executionSpec, executionState, jobId);
                } finally {
                    WatchStudentServiceImpl.this.setCurrentLogger(null);
                }
            }

            @Override
            protected void recordSuccessful(ImmutablePersonIdentifiersTO personIds, Map<String, ?> results,
                    BulkWatchChangeJobSpec executionSpec, BulkWatchChangeJobExecutionState executionState,
                    UUID jobId) {
                super.recordSuccessful(personIds, results, executionSpec, executionState, jobId);
                if (results.get(WATCH_ID_FIELD_NAME) == null) {
                    executionState.personsSkippedCount++;
                }
            }

            @Override
            protected BulkWatchChangeJobSpec deserializeJobSpecWithCheckedExceptions(String jobSpecStr)
                    throws Exception {
                return getObjectMapper().readValue(jobSpecStr, BulkWatchChangeJobSpec.class);
            }

            @Override
            protected BulkWatchChangeJobExecutionState deserializeJobStateWithCheckedExceptions(String jobStateStr)
                    throws Exception {
                return getObjectMapper().readValue(jobStateStr, BulkWatchChangeJobExecutionState.class);
            }

            @Override
            protected String decorateProcessingCompleteLogMessage(String baseMsg,
                    BulkWatchChangeJobExecutionState executionState) {
                return new StringBuilder(baseMsg).append(" Persons skipped : [")
                        .append(executionState.personsSkippedCount).append("].").toString();
            }

            @Override
            protected BulkWatchChangeJobExecutionState newJobExecutionState() {
                return new BulkWatchChangeJobExecutionState();
            }

            @Override
            protected Logger getCurrentLogger() {
                return WatchStudentServiceImpl.this.getCurrentLogger();
            }

            @Override
            protected String getPageSizeConfigName() {
                return BULK_WATCH_CHANGE_BATCH_SIZE_CONFIG_NAME;
            }

            @Override
            protected String getDlqSizeConfigName() {
                return BULK_WATCH_CHANGE_MAX_DLQ_SIZE_CONFIG_NAME;
            }

            @Override
            protected String getFailOnSlqOverflowConfigName() {
                return BULK_WATCH_CHANGE_FAIL_ON_DLQ_OVERFLOW_CONFIG_NAME;
            }
        };
        this.jobService.registerJobExecutor(this.bulkJobExecutor);
    }

    private void initBulkWatchChangeJobQueuer() {
        if (this.bulkJobExecutor == null) {
            // programmer error
            throw new IllegalStateException("Bulk Watch changing JobExecutor not yet initialized");
        }
        this.bulkJobQueuer = new AbstractPersonSearchBasedJobQueuer<BulkWatchChangeRequestForm, BulkWatchChangeJobSpec>(
                this.bulkJobExecutor, securityService, personSearchRequestFactory, personSearchService) {
            @Override
            protected BulkWatchChangeRequestForm validateJobRequest(BulkWatchChangeRequestForm jobRequest)
                    throws ValidationException {
                validateBulkWatchChangeRequest(jobRequest);
                return jobRequest;
            }

            @Override
            protected BulkWatchChangeJobSpec newJobSpec(BulkWatchChangeRequestForm jobRequest,
                    Person currentSspPerson, PersonSearchRequest searchRequest, long searchResultCount) {
                return new BulkWatchChangeJobSpec(jobRequest);
            }
        };
    }

    private void validateBulkWatchChangeRequest(BulkWatchChangeRequestForm jobRequest) throws ValidationException {
        final BulkWatchChangeTO watchSpec = jobRequest.getWatchSpec();
        if (watchSpec == null) {
            throw new ValidationException("Must specify a target watch state");
        }
        final BulkWatchChangeTO.BulkWatchOperation operation = watchSpec.getOperation();
        if (operation == null) {
            throw new ValidationException("Must specify a watch change operation");
        }
        if (watchSpec.getWatcherId() == null) {
            throw new ValidationException("Must specify a watcher ID");
        }
        Person watcher = null;
        try {
            watcher = personService.get(watchSpec.getWatcherId());
        } catch (ObjectNotFoundException e) {
            // nothing to do
        }
        if (watcher == null) {
            throw new ValidationException("Watcher ID [" + watchSpec.getWatcherId() + "] not on file");
        }
    }

    private Logger getCurrentLogger() {
        return CURRENT_LOGGER.get() == null ? OUTER_CLASS_LOGGER : CURRENT_LOGGER.get();
    }

    private void setCurrentLogger(Logger logger) {
        CURRENT_LOGGER.set(logger);
    }

    private static enum EmailVolume {
        SINGLE, BULK
    }

}