/*
* Copyright 2012 Shared Learning Collaborative, LLC
*
* 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.dashboard.manager.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import org.slc.sli.dashboard.entity.Config.Data;
import org.slc.sli.dashboard.entity.EdOrgKey;
import org.slc.sli.dashboard.entity.GenericEntity;
import org.slc.sli.dashboard.entity.util.GenericEntityComparator;
import org.slc.sli.dashboard.entity.util.GenericEntityEnhancer;
import org.slc.sli.dashboard.manager.ApiClientManager;
import org.slc.sli.dashboard.manager.UserEdOrgManager;
import org.slc.sli.dashboard.util.CacheableUserData;
import org.slc.sli.dashboard.util.Constants;
import org.slc.sli.dashboard.util.DashboardException;
import org.slc.sli.dashboard.util.SecurityUtil;
import org.slc.sli.dashboard.web.util.TreeGridDataBuilder;
/**
* Retrieves and applies necessary business logic to obtain institution data
*
* @author syau
*
*/
public class UserEdOrgManagerImpl extends ApiClientManager implements UserEdOrgManager {
private GenericEntity getParentEducationalOrganization(String token, GenericEntity edOrgOrSchool) {
return getApiClient().getParentEducationalOrganization(token, edOrgOrSchool);
}
private List<GenericEntity> getParentEducationalOrganizations(String token, List<GenericEntity> edOrgOrSchool) {
return getApiClient().getParentEducationalOrganizations(token, edOrgOrSchool);
}
protected boolean isEducator() {
return !SecurityUtil.isNotEducator();
}
/**
* read token. Then, find district name associated with school.
*
* @param token
* token-id, it is also using token as key value for cache.
* @return District name
*/
@Override
@CacheableUserData
public EdOrgKey getUserEdOrg(String token) {
GenericEntity edOrg = null;
// For state-level ed-org - need to take default config, so keep state
// ed org
if (!isEducator()) {
String id = getApiClient().getId(token);
GenericEntity staff = getApiClient().getStaffWithEducationOrganization(token, id, null);
if (staff != null) {
GenericEntity staffEdOrg = (GenericEntity) staff.get(Constants.ATTR_ED_ORG);
if (staffEdOrg != null) {
@SuppressWarnings("unchecked")
List<String> edOrgCategories = (List<String>) staffEdOrg.get(Constants.ATTR_ORG_CATEGORIES);
if (edOrgCategories != null && !edOrgCategories.isEmpty()) {
for (String edOrgCategory : edOrgCategories) {
if (edOrgCategory.equals(Constants.STATE_EDUCATION_AGENCY)) {
edOrg = staffEdOrg;
break;
} else if (edOrgCategory.equals(Constants.LOCAL_EDUCATION_AGENCY)) {
edOrg = staffEdOrg;
break;
}
}
}
}
}
}
// otherwise get school's parent ed-org
if (edOrg == null) {
// get list of school
List<GenericEntity> schools = getMySchools(token);
if (schools != null && !schools.isEmpty()) {
// read first school
GenericEntity school = schools.get(0);
// read parent organization
edOrg = getParentEducationalOrganization(getToken(), school);
if (edOrg == null) {
throw new DashboardException(
"No data is available for you to view. Please contact your IT administrator.");
}
}
}
// create ed-org key and save to cache
if (edOrg != null) {
return new EdOrgKey(edOrg.getId());
}
return null;
}
/**
* Get user's schools. Cache the results so we don't have to make the call
* twice.
*
* @return
*/
private List<GenericEntity> getSchools(String token) {
return getApiClient().getSchools(token, null);
}
/**
* Get user's associated schools. Cache the results so we don't have to make the call
* twice.
*
* @return
*/
private List<GenericEntity> getMySchools(String token) {
return getApiClient().getMySchools(token);
}
/**
* Returns the institutional hierarchy visible to the user with the given
* auth token as a list of generic entities, with the ed-org level flattened
* This assumes there are no cycles in the education organization hierarchy
* tree.
*
* @return
*/
private List<GenericEntity> getUserInstHierarchy(String token) {
// Find all the schools first.
List<GenericEntity> schools = getMySchools(token);
if (schools == null) {
return Collections.emptyList();
}
// This maps ids from educational organisations to schools reachable
// from it via the "child" relationship
Map<String, Set<GenericEntity>> schoolReachableFromEdOrg = new HashMap<String, Set<GenericEntity>>();
// This just maps ed org ids to ed org objects.
Map<String, GenericEntity> edOrgIdMap = new HashMap<String, GenericEntity>();
for (GenericEntity school : schools) {
String parentEdOrgId = (String) school.get(Constants.ATTR_PARENT_EDORG);
if (parentEdOrgId != null) {
if (!schoolReachableFromEdOrg.keySet().contains(parentEdOrgId)) {
schoolReachableFromEdOrg.put(parentEdOrgId, new HashSet<GenericEntity>());
}
schoolReachableFromEdOrg.get(parentEdOrgId).add(school);
}
}
// traverse the ancestor chain from each school and find ed orgs that
// the school is reachable from
List<GenericEntity> edOrgs = getParentEducationalOrganizations(token, schools);
while (!edOrgs.isEmpty()) {
for (GenericEntity edOrg : edOrgs) {
String parentEdOrgId = (String) edOrg.get(Constants.ATTR_PARENT_EDORG);
String edOrgId = edOrg.getId();
// if parentEdOrgId exists, you are not the top organization
if (parentEdOrgId != null) {
// insert ed-org id to - edOrg mapping
edOrgIdMap.put(edOrgId, edOrg);
// insert ed-org - school mapping into the reverse map
if (!schoolReachableFromEdOrg.keySet().contains(parentEdOrgId)) {
schoolReachableFromEdOrg.put(parentEdOrgId, new HashSet<GenericEntity>());
}
Set<GenericEntity> reachableSchool = schoolReachableFromEdOrg.get(edOrgId);
if (reachableSchool != null) {
schoolReachableFromEdOrg.get(parentEdOrgId).addAll(reachableSchool);
}
}
}
// next in the ancestor chain
edOrgs = getParentEducationalOrganizations(token, edOrgs);
}
// build result list
List<GenericEntity> retVal = new ArrayList<GenericEntity>();
for (String edOrgId : schoolReachableFromEdOrg.keySet()) {
GenericEntity obj = new GenericEntity();
try {
GenericEntity edOrgEntity = edOrgIdMap.get(edOrgId);
// if edOrgEntity is null, it may be API could not return entity
// because of error code 403.
if (edOrgEntity != null) {
obj.put(Constants.ATTR_NAME, edOrgEntity.get(Constants.ATTR_NAME_OF_INST));
obj.put(Constants.ATTR_ID, edOrgEntity.get(Constants.ATTR_ID));
// convert school ids to the school object array and sort based on the name of
// the institution
Set<GenericEntity> reachableSchools = new TreeSet<GenericEntity>(new GenericEntityComparator(
Constants.ATTR_NAME_OF_INST, String.class));
reachableSchools.addAll(schoolReachableFromEdOrg.get(edOrgId));
obj.put(Constants.ATTR_SCHOOLS, reachableSchools);
retVal.add(obj);
}
} catch (Exception e) {
throw new RuntimeException("error creating json object for " + edOrgId);
}
}
Collection<GenericEntity> orphanSchools = findOrphanSchools(schools, schoolReachableFromEdOrg);
// insert a dummy edorg for all orphan schools.
if (!orphanSchools.isEmpty()) {
insertSchoolsUnderDummyEdOrg(retVal, orphanSchools);
}
// Sort the Districts based on the District Name
Collections.sort(retVal, new GenericEntityComparator(Constants.ATTR_NAME, String.class));
return retVal;
}
// ------------- helper functions ----------------
private static Collection<GenericEntity> findOrphanSchools(List<GenericEntity> schools,
Map<String, Set<GenericEntity>> schoolReachableFromEdOrg) {
Vector<GenericEntity> orphanSchools = new Vector<GenericEntity>();
for (int i = 0; i < schools.size(); i++) {
GenericEntity s = schools.get(i);
boolean isOrphan = true;
for (Set<GenericEntity> reachableSchools : schoolReachableFromEdOrg.values()) {
if (reachableSchools.contains(s)) {
isOrphan = false;
break;
}
}
if (isOrphan) {
orphanSchools.add(s);
}
}
return orphanSchools;
}
// Insert schools into the list under a "dummy" ed-org
private static List<GenericEntity> insertSchoolsUnderDummyEdOrg(List<GenericEntity> retVal,
Collection<GenericEntity> schools) {
try {
GenericEntity obj = new GenericEntity();
obj.put(Constants.ATTR_NAME, DUMMY_EDORG_NAME);
obj.put(Constants.ATTR_SCHOOLS, schools);
retVal.add(obj);
} catch (Exception e) {
throw new RuntimeException("error creating json object for dummy edOrg");
}
return retVal;
}
/**
* Override from UserEdOrgManager.
* Signature is pre-defined by the architect.
*/
@Override
@CacheableUserData
public GenericEntity getUserInstHierarchy(String token, Object key, Data config) {
List<GenericEntity> entities = getUserInstHierarchy(token);
GenericEntity entity = new GenericEntity();
entity.put(Constants.ATTR_ROOT, entities);
return entity;
}
/**
* Override from UserEdOrgManager.
* Signature is pre-defined by the architect.
*/
@Override
public GenericEntity getUserCoursesAndSections(String token, Object schoolIdObj, Data config) {
String schoolId = (String) schoolIdObj;
List<GenericEntity> entities = getApiClient().getCoursesSectionsForSchool(token, schoolId);
GenericEntity entity = new GenericEntity();
entity.put(Constants.ATTR_ROOT, entities);
return entity;
}
/**
* Get list of subjects, courses, and sections for a school.
* Pass out a flattened structure with parent/child relationships defined.
*
* @param token
* @return
*/
@Override
@SuppressWarnings("unchecked")
public GenericEntity getUserSectionList(String token, Object schoolIdObj, Data config) {
String schoolId = (String) schoolIdObj;
List<GenericEntity> courses = getApiClient().getCoursesSectionsForSchool(token, schoolId);
// set any null subjects to Miscellaneous
for (GenericEntity course : courses) {
if (course.getString(Constants.ATTR_SUBJECTAREA) == null) {
course.put(Constants.ATTR_SUBJECTAREA, "Miscellaneous");
}
}
// sort courses by subject area
Collections.sort(courses, new CourseSubjectComparator());
Map<String, GenericEntity> subjects = new LinkedHashMap<String, GenericEntity>();
int subjectIndex = 0;
// do some restructuring
for (GenericEntity course : courses) {
String subjectArea = course.getString(Constants.ATTR_SUBJECTAREA);
GenericEntity subject = null;
// add/get subject entity
if (!subjects.containsKey(subjectArea)) {
subject = new GenericEntity();
subject.put(Constants.ATTR_NAME, subjectArea);
subject.put(Constants.ATTR_ID, String.valueOf(++subjectIndex));
List<GenericEntity> c = new ArrayList<GenericEntity>();
subject.put(Constants.ATTR_COURSES, c);
subjects.put(subjectArea, subject);
} else {
subject = subjects.get(subjectArea);
}
course.put(Constants.ATTR_NAME, course.getString(Constants.ATTR_COURSE_TITLE));
course.remove(Constants.ATTR_LINKS);
((List<GenericEntity>) subject.get(Constants.ATTR_COURSES)).add(course);
List<GenericEntity> sections = (List<GenericEntity>) course.get(Constants.ATTR_SECTIONS);
if (sections != null && sections.size() > 0) {
for (GenericEntity section : sections) {
section.put(Constants.ATTR_NAME, section.getString(Constants.ATTR_SECTION_NAME));
section.remove(Constants.ATTR_LINKS);
}
}
}
// call the tree grid builder to format/structure the data
List<String> subLevels = new ArrayList<String>();
subLevels.add(Constants.ATTR_COURSES);
subLevels.add(Constants.ATTR_SECTIONS);
List<GenericEntity> entities = TreeGridDataBuilder.build(new ArrayList<GenericEntity>(subjects.values()),
subLevels);
GenericEntity entity = new GenericEntity();
entity.put(Constants.ATTR_ROOT, entities);
return entity;
}
@Override
@SuppressWarnings("unchecked")
public GenericEntity getStaffInfo(String token) {
String id = getApiClient().getId(token);
GenericEntity staffEntity = getApiClient().getStaffWithEducationOrganization(token, id, null);
if (staffEntity == null) {
staffEntity = new GenericEntity();
}
return staffEntity;
}
private class CourseSubjectComparator implements Comparator<GenericEntity> {
@Override
public int compare(GenericEntity course1, GenericEntity course2) {
// compare subject area
String subject1 = course1.getString(Constants.ATTR_SUBJECTAREA);
String subject2 = course2.getString(Constants.ATTR_SUBJECTAREA);
if (subject1 == null) {
return -1;
}
if (subject2 == null) {
return 1;
}
int i = subject1.compareToIgnoreCase(subject2);
if (i != 0) {
return i;
}
// compare course title
String courseTitle1 = course1.getString(Constants.ATTR_COURSE_TITLE);
String courseTitle2 = course2.getString(Constants.ATTR_COURSE_TITLE);
if (courseTitle1 == null) {
return -1;
}
if (courseTitle2 == null) {
return 1;
}
return courseTitle1.compareToIgnoreCase(courseTitle2);
}
}
@Override
@SuppressWarnings("unchecked")
public GenericEntity getSchoolInfo(String token, Object schoolIdObj, Data config) {
// get school entity
GenericEntity school = getApiClient().getSchool(token, (String) schoolIdObj);
// convert grade strings
List<String> gradesOffered = school.getList("gradesOffered");
List<String> gradesOfferedCode = new ArrayList<String>();
for (String gradeOffered : gradesOffered) {
gradesOfferedCode.add(GenericEntityEnhancer.convertGradeLevel(gradeOffered));
}
school.put("gradesOfferedCode", gradesOfferedCode);
return school;
}
}