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

Java tutorial

Introduction

Here is the source code for org.jasig.ssp.service.impl.PersonServiceBulkCoachLookupIntegrationTest.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 java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.UUID;

import org.apache.commons.lang.StringUtils;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Matcher;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.jasig.ssp.model.ObjectStatus;
import org.jasig.ssp.model.Person;
import org.jasig.ssp.service.ObjectNotFoundException;
import org.jasig.ssp.service.PersonService;
import org.jasig.ssp.transferobject.CoachPersonLiteTO;
import org.jasig.ssp.util.service.stub.StubPersonAttributesService;
import org.jasig.ssp.util.service.stub.Stubs;
import org.jasig.ssp.util.service.stub.Stubs.PersonFixture;
import org.jasig.ssp.util.sort.PagingWrapper;
import org.jasig.ssp.util.sort.SortingAndPaging;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import static org.hamcrest.Matchers.array;
import static org.jasig.ssp.util.service.stub.Stubs.PersonFixture.ADVISOR_0;
import static org.jasig.ssp.util.service.stub.Stubs.PersonFixture.JAMES_DOE;
import static org.jasig.ssp.util.service.stub.Stubs.PersonFixture.COACH_1;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "../service-testConfig.xml", "../stubPersonAttributesService-testConfig.xml" })
@TransactionConfiguration
@Transactional
public class PersonServiceBulkCoachLookupIntegrationTest {

    @Autowired
    protected transient SecurityServiceInTestEnvironment securityService;

    @Autowired
    protected transient StubPersonAttributesService personAttributesService;

    @Autowired
    protected transient PersonService personService;

    @Autowired
    protected transient SessionFactory sessionFactory;

    protected transient List<String> origCoachUsernames;

    /**
     * Setup the security service with the administrator.
     */
    @Before
    public void setUp() {
        securityService.setCurrent(new Person(Person.SYSTEM_ADMINISTRATOR_ID));
        Collection<String> rawCoachUsernames = personAttributesService.getCoachUsernames();
        if (rawCoachUsernames != null) {
            origCoachUsernames = new ArrayList<String>(rawCoachUsernames);
        }
    }

    @After
    public void tearDown() {
        personAttributesService.setCoachUsernames(origCoachUsernames);
        securityService.setCurrent(new Person());
    }

    @Test
    public void testGetAllAssignedCoachesLite() {

        final Collection<CoachPersonLiteTO> expected = Lists.newArrayList(coachPersonLiteTOFor(COACH_1),
                coachPersonLiteTOFor(ADVISOR_0));

        final PagingWrapper<CoachPersonLiteTO> result1 = personService.getAllAssignedCoachesLite(SortingAndPaging
                .createForSingleSortWithPaging(ObjectStatus.ACTIVE, 0, 1000, "lastName", "ASC", "lastName"));

        assertCoachPersonLiteTOCollectionsEqual(expected, result1.getRows());
        // zero b/c the request specified no pagination, so impl skips total
        // result size calculation
        assertEquals(2, result1.getResults());

        // now prove that getAllAssignedCoachesLite() doesn't lazily
        // create/return new coaches by creating a fixture where it could do so,
        // run the same method again, then checking that we get the exact
        // same results as before
        final Set<String> newExternalCoachUsernames = addCoachesToExternalDataAndAttributeService(5);

        final PagingWrapper<CoachPersonLiteTO> result2 = personService.getAllAssignedCoachesLite(SortingAndPaging
                .createForSingleSortWithPaging(ObjectStatus.ACTIVE, 0, 1000, "lastName", "ASC", "lastName"));

        assertCoachPersonLiteTOCollectionsEqual(expected, result2.getRows());
        // zero b/c the request specified no pagination, so impl skips total
        // result size calculation
        assertEquals(2, result2.getResults());
    }

    private CoachPersonLiteTO coachPersonLiteTOFor(Stubs.PersonFixture personFixture) {
        return new CoachPersonLiteTO(personFixture.id(), personFixture.firstName(), personFixture.lastName(),
                personFixture.primaryEmailAddress(), null, personFixture.departmentName(),
                personFixture.workPhone(), personFixture.photoUrl());
    }

    @Test
    public void testGetAllAssignedCoaches() throws ObjectNotFoundException {

        // basically the same as testGetAllAssignedCoachesLite() except
        // we expect Persons instead
        final Collection<UUID> expected = Lists.newArrayList(ADVISOR_0.id(), COACH_1.id());

        final PagingWrapper<Person> result1 = personService.getAllAssignedCoaches(null);

        assertPersonCollectionsHaveSameIds(expected, result1.getRows());
        // zero b/c the request specified no pagination, so impl skips total
        // result size calculation
        assertEquals(0, result1.getResults());

        // now prove that getAllAssignedCoaches() doesn't lazily
        // create/return new coaches by creating a fixture where it could do so,
        // run the same method again, then checking that we get the exact
        // same results as before
        final Set<String> newExternalCoachUsernames = addCoachesToExternalDataAndAttributeService(5);

        final PagingWrapper<Person> result2 = personService.getAllAssignedCoaches(null);

        assertPersonCollectionsHaveSameIds(expected, result2.getRows());
        // zero b/c the request specified no pagination, so impl skips total
        // result size calculation
        assertEquals(0, result2.getResults());

    }

    @Test
    public void testGetAllCurrentCoachesLite() throws ObjectNotFoundException {

        final Collection<CoachPersonLiteTO> expected = Lists.newArrayList(coachPersonLiteTOFor(ADVISOR_0),
                coachPersonLiteTOFor(COACH_1));

        final SortedSet<CoachPersonLiteTO> result1 = personService.getAllCurrentCoachesLite(null);

        assertCoachPersonLiteTOCollectionsEqual(expected, result1);

        final Set<String> newExternalCoachUsernames = addCoachesToExternalDataAndAttributeService(2);

        final SortedSet<CoachPersonLiteTO> result2 = personService.getAllCurrentCoachesLite(null);

        assertCoachPersonLiteTOCollectionsEqual(expected, result2);

    }

    @Test
    public void testGetAllCurrentCoaches() throws ObjectNotFoundException {

        // unlike the getAllAssignedCoaches()/getAllAssignedCoachesLite()
        // pair, getAllCurrentCoaches()/getAllCurrentCoachesLite() have
        // significantly different behavior. Specifically the non-lite version,
        // tested here, *does* lazily create and return new Persons. The lite
        // version does not.
        final SortedSet<Person> result1 = personService.getAllCurrentCoaches(null);

        assertPersonCollectionsHaveSameIds(Lists.newArrayList(ADVISOR_0.id(), COACH_1.id()), result1);

        final Set<String> newExternalCoachUsernames = addCoachesToExternalDataAndAttributeService(2);

        final SortedSet<Person> result2 = personService.getAllCurrentCoaches(null);

        assertPersonCollectionsHaveSameIds(
                Lists.newArrayList(personService.personFromUsername("bulk_coach_001").getId(),
                        personService.personFromUsername("bulk_coach_002").getId(), ADVISOR_0.id(), COACH_1.id()),
                result2);
    }

    @Test
    public void testGetAllCurrentCoachesLiteFiltersDuplicates() throws ObjectNotFoundException {
        final Person jamesDoe = person(JAMES_DOE);
        final Person advisor0 = person(ADVISOR_0);
        jamesDoe.setCoach(advisor0);
        personService.save(jamesDoe);
        sessionFactory.getCurrentSession().flush();

        final SortedSet<CoachPersonLiteTO> result = personService.getAllCurrentCoachesLite(null);
        assertEquals(2, result.size());
    }

    @Test
    public void testGetAllCurrentCoachesFiltersDuplicates() throws ObjectNotFoundException {
        final Person jamesDoe = person(JAMES_DOE);
        final Person advisor0 = person(ADVISOR_0);
        jamesDoe.setCoach(advisor0);
        personService.save(jamesDoe);
        sessionFactory.getCurrentSession().flush();

        final SortedSet<Person> result = personService.getAllCurrentCoaches(null);
        assertEquals(2, result.size());
    }

    @Test
    public void testGetAllCurrentCoachesLiteFiltersDuplicatesByIdNotName() throws ObjectNotFoundException {
        final String duplicatePersonSchoolId = ADVISOR_0.schoolId() + "_foo";
        this.createExternalPerson(duplicatePersonSchoolId, ADVISOR_0.username() + "_foo", ADVISOR_0.firstName(), // everything else the same
                ADVISOR_0.lastName(), ADVISOR_0.middleName(), ADVISOR_0.primaryEmailAddress());

        // this should create the person record
        Person duplicatePerson = personService.getBySchoolId(duplicatePersonSchoolId, true);
        assertNotNull(duplicatePerson); // sanity check
        final Person jamesDoe = person(JAMES_DOE);
        jamesDoe.setCoach(duplicatePerson);
        personService.save(jamesDoe);
        sessionFactory.getCurrentSession().flush();

        final SortedSet<CoachPersonLiteTO> result = personService.getAllCurrentCoachesLite(null);
        assertEquals(3, result.size());
    }

    @Test
    public void testGetAllCurrentCoachesFiltersDuplicatesByIdNotName() throws ObjectNotFoundException {
        final String duplicatePersonSchoolId = ADVISOR_0.schoolId() + "_foo";
        this.createExternalPerson(duplicatePersonSchoolId, ADVISOR_0.username() + "_foo", ADVISOR_0.firstName(), // everything else the same
                ADVISOR_0.lastName(), ADVISOR_0.middleName(), ADVISOR_0.primaryEmailAddress());

        // this should create the person record
        Person duplicatePerson = personService.getBySchoolId(duplicatePersonSchoolId, true);
        assertNotNull(duplicatePerson); // sanity check
        final Person jamesDoe = person(JAMES_DOE);
        jamesDoe.setCoach(duplicatePerson);
        personService.save(jamesDoe);
        sessionFactory.getCurrentSession().flush();

        final SortedSet<Person> result = personService.getAllCurrentCoaches(null);
        assertEquals(3, result.size());
    }

    /**
     * Ignored b/c it doesn't assert anything, it just demonstrates the
     * performance (and behavioral) differences between
     * {@link PersonService#getAllCurrentCoaches(java.util.Comparator)}
     * and {@link PersonService#getAllCurrentCoachesLite(java.util.Comparator)}.
     * There are (at least) three problems with the former...
     *
     * <ol>
     *     <li>It lazily creates person records it hasn't encountered before,
     *     and</li>
     *     <li>It looks up {@link Person}s one-by-one, and</li>
     *     <li>Person lookups are just very expensive</li>
     * </ol>
     *
     * <p>So if the total number of coaches returned from
     * {@link org.jasig.ssp.service.PersonAttributesService#getCoaches()} is
     * large (anywhere into the 100s),
     * {@link PersonService#getAllCurrentCoaches(java.util.Comparator)} is
     * unsuitable for invocation in the request cycle, <em>even if all
     * the referenced coaches have already been created as {@link Person}
     * records</em>.</p>
     *
     * <p>{@link PersonService#getAllCurrentCoachesLite(java.util.Comparator)}
     * is faster, but partly b/c it doesn't make any attempt to lazily create
     * new {@link Person}s. So it doesn't run the risk of exceptionally long
     * runtimes when first invoked. But it does so at the cost of potentially
     * not returning a completely up-to-date view of all coaches.
     * <a href="https://issues.jasig.org/browse/SSP-470">SSP-470</a> combats
     * this by moving the {@link Person} creation into a background job.</p>
     *
     * <p>This test demonstrates the performance gradient by causing
     * {@link org.jasig.ssp.service.PersonAttributesService#getCoaches()} to
     * return 500 coach usernames the {@link PersonService} hasn't seen before,
     * then making a series of calls to the methods of interest. At this
     * writing (Nov 20, 2012), in an all-local development env, the numbers
     * looked like this (execution time in
     * {@link org.jasig.ssp.service.PersonAttributesService#getCoaches()} is
     * effecively negiligible b/c this test stubs that service):</p>
     *
     * <ol>
     *     <li>{@link PersonService#getAllCurrentCoachesLite(java.util.Comparator)} (returns 1 record): 55ms</li>
     *     <li>{@link PersonService#getAllCurrentCoaches(java.util.Comparator)} (returns 501 records): 29504ms</li>
     *     <li>{@link PersonService#getAllCurrentCoaches(java.util.Comparator)} (returns 501 records): 15428ms</li>
     *     <li>{@link PersonService#getAllCurrentCoachesLite(java.util.Comparator)} (returns 501 records): 59ms</li>
     * </ol>
     *
     * <p>Keep in mind again that
     * {@link PersonService#getAllCurrentCoachesLite(java.util.Comparator)}
     * doesn't make any of the lazy-creation promises of
     * {@link PersonService#getAllCurrentCoaches(java.util.Comparator)}, so
     * the comparison isn't completely fair. But the calls to the latter
     * are sufficiently slow that it would be nice to find a way to
     * drop them both down... maybe through a combination of bulk db reads
     * and writes and by simplifying the object graph returned with all
     * {@link Person} lookups.</p>
     *
     */
    @Test
    @Ignore
    public void testLiteCoachLookupMuchFasterButPotentiallyIncomplete() {
        int externalCoachQuota = 500;
        Set<String> addedCoachUsernames = addCoachesToExternalDataAndAttributeService(externalCoachQuota);

        long started = new Date().getTime();
        final PagingWrapper<CoachPersonLiteTO> allCoachesLite1 = personService.getAllCoachesLite(null);
        long ended = new Date().getTime();
        System.out.println("Lite Person lookups, no external Persons created yet: " + (ended - started) + "ms ("
                + allCoachesLite1.getResults() + " total records returned)");

        started = new Date().getTime();
        final SortedSet<Person> lazyCreatedCoaches1 = personService.getAllCurrentCoaches(null);
        ended = new Date().getTime();
        System.out.println("Full Person lookups, lazy Person record creation: " + (ended - started) + "ms ("
                + externalCoachQuota + " lazy records, " + lazyCreatedCoaches1.size() + " total records returned)");

        started = new Date().getTime();
        final SortedSet<Person> lazyCreatedCoaches2 = personService.getAllCurrentCoaches(null);
        ended = new Date().getTime();
        System.out.println("Full Person lookups, all Persons already created: " + (ended - started) + "ms ("
                + lazyCreatedCoaches2.size() + " total records returned)");

        started = new Date().getTime();
        final PagingWrapper<CoachPersonLiteTO> allCoachesLite2 = personService.getAllCoachesLite(null);
        ended = new Date().getTime();
        System.out.println("Lite Person lookups, all Persons already created: " + (ended - started) + "ms ("
                + allCoachesLite2.getResults() + " total records returned)");
    }

    private Set<String> addCoachesToExternalDataAndAttributeService(int quota) {
        Set<String> usernames = Sets.newHashSet();
        if (quota <= 0) {
            return usernames;
        }
        for (int i = 1; i <= quota; i++) {
            String paddedIdx = StringUtils.leftPad("" + i, 3, "0");
            String username = "bulk_coach_" + paddedIdx;
            createExternalPerson("bulk_coach_school_id_" + paddedIdx, username, "BulkCoach",
                    "BulkCoach" + paddedIdx, "" + i, "bulkcoach" + paddedIdx + "@school.edu");
            usernames.add(username);
        }
        personAttributesService.getCoachUsernames().addAll(usernames);
        sessionFactory.getCurrentSession().flush();
        return usernames;
    }

    public void createExternalPerson(final String schoolId, final String username, final String firstName,
            final String lastName, final String middleName, final String primaryEmailAddress) {
        final Session session = sessionFactory.getCurrentSession();
        final SQLQuery sqlQuery = session.createSQLQuery("insert into external_person (school_id,"
                + "username, first_name, last_name, middle_name," + "primary_email_address) values (?,?,?,?,?,?)");
        sqlQuery.setString(0, schoolId).setString(1, username).setString(2, firstName).setString(3, lastName)
                .setString(4, middleName).setString(5, primaryEmailAddress);
        sqlQuery.executeUpdate();
    }

    private void assertPersonCollectionsHaveSameIds(Collection<UUID> expected, Collection<Person> actual) {
        assertArrayEquals(uuidCollectionToUuidAray(expected), personCollectionToUuidAray(actual));
    }

    private UUID[] uuidCollectionToUuidAray(Collection<UUID> uuidCollection) {
        return uuidCollection.toArray(new UUID[uuidCollection.size()]);
    }

    private UUID[] personCollectionToUuidAray(Collection<Person> personCollection) {
        UUID[] uuidArray = new UUID[personCollection.size()];
        int i = 0;
        for (Person person : personCollection) {
            uuidArray[i] = person.getId();
            i++;
        }
        return uuidArray;
    }

    private void assertCoachPersonLiteTOCollectionsEqual(Collection<CoachPersonLiteTO> expected,
            Collection<CoachPersonLiteTO> actual) {
        CoachPersonLiteTO[] actualArray = actual.toArray(new CoachPersonLiteTO[actual.size()]);
        assertThat(actualArray, array(matchersFor(expected)));
    }

    private Matcher<CoachPersonLiteTO>[] matchersFor(Collection<CoachPersonLiteTO> coachPersonLiteTOs) {
        Matcher<CoachPersonLiteTO>[] matchers = new Matcher[coachPersonLiteTOs.size()];
        int i = 0;
        for (CoachPersonLiteTO coachPersonLiteTO : coachPersonLiteTOs) {
            matchers[i++] = matcherFor(coachPersonLiteTO);
        }
        return matchers;
    }

    private Matcher<CoachPersonLiteTO> matcherFor(final CoachPersonLiteTO coachPersonLiteTO) {
        return new CustomTypeSafeMatcher<CoachPersonLiteTO>(coachPersonLiteTO.toString()) {
            @Override
            protected boolean matchesSafely(CoachPersonLiteTO item) {
                return item.equalsAllFields(coachPersonLiteTO);
            }
        };
    }

    private Person person(Stubs.PersonFixture personFixture) throws ObjectNotFoundException {
        return Stubs.person(personFixture, personService);
    }

}