Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.jboss.resteasy.annotations.GZIP;
Expand Down Expand Up @@ -1205,15 +1206,31 @@ private void addTeacherInformation(final Map<String, String> info, final Registe
info.put("teacherEmail", teacher.getEmail());
info.put("teacherId", teacher.getId().toString());
info.put("teacherSchoolId", teacher.getSchoolId() != null ? teacher.getSchoolId() : "");
info.put("teacherSchoolName", teacher.getSchoolOther() != null ? teacher.getSchoolOther() : "");
info.put("teacherSchoolName", findUsersSchoolName(teacher));
}

private String findUsersSchoolName(RegisteredUserDTO user) {
return Optional.ofNullable(user.getSchoolOther())
.or(() -> Optional.ofNullable(user.getSchoolId())
.flatMap(this::findSchoolNameById))
.orElse("");
}

private Optional<String> findSchoolNameById(String schoolId) {
try {
return Optional.ofNullable(schoolListReader.findSchoolById(schoolId).getName());
} catch (Exception e) {
log.warn("Failed to find school with ID: {}", schoolId, e);
return Optional.empty();
}
}

private void addStudentInformation(final Map<String, String> info, final RegisteredUserDTO student) {
info.put("student_user_id", student.getId().toString());
info.put("student_given_name", student.getGivenName() != null ? student.getGivenName() : "");
info.put("student_family_name", student.getFamilyName() != null ? student.getFamilyName() : "");
info.put("student_email", student.getEmail() != null ? student.getEmail() : "");
info.put("student_school_name", student.getSchoolOther() != null ? student.getSchoolOther() : "");
info.put("student_school_name", findUsersSchoolName(student));
info.put("student_school_urn", student.getSchoolId() != null ? student.getSchoolId() : "");

addStudentContextInformation(info, student);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
import uk.ac.cam.cl.dtg.segue.dao.associations.PgAssociationDataManager;
import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapperUtils;
import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager;
import uk.ac.cam.cl.dtg.segue.dao.schools.PgSchoolLookup;
import uk.ac.cam.cl.dtg.segue.dao.schools.SchoolListReader;
import uk.ac.cam.cl.dtg.segue.dao.userbadges.IUserBadgePersistenceManager;
import uk.ac.cam.cl.dtg.segue.dao.userbadges.PgUserBadgePersistenceManager;
Expand Down Expand Up @@ -1214,14 +1215,16 @@ private static IsaacSymbolicLogicValidator getSymbolicLogicValidator(final Prope
* We want this to be a singleton as otherwise it may not be threadsafe for loading into same SearchProvider.
*
* @param provider The search provider.
* @param schoolFallbackLookup Fallback lookup for schools not in search index.
* @return schoolList reader
*/
@Inject
@Provides
@Singleton
private SchoolListReader getSchoolListReader(final ISearchProvider provider) {
private SchoolListReader getSchoolListReader(final ISearchProvider provider,
final PgSchoolLookup schoolFallbackLookup) {
if (null == schoolListReader) {
schoolListReader = new SchoolListReader(provider);
schoolListReader = new SchoolListReader(provider, schoolFallbackLookup);
log.info("Creating singleton of SchoolListReader");
}
return schoolListReader;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Copyright 2025 Isaac Computer Science
* <br>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* <br>
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* <br>
* 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 uk.ac.cam.cl.dtg.segue.dao.schools;

import static uk.ac.cam.cl.dtg.util.LogUtils.sanitiseExternalLogValue;

import com.google.inject.Inject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.ac.cam.cl.dtg.isaac.dos.users.School;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.database.PostgresSqlDb;

/**
* Temporary fallback lookup for schools that are no longer in Elasticsearch.
* This queries the schools_2022 table in the database to resolve school names
* for users who registered with schools that have since been removed from the
* current school list.
*/
public class PgSchoolLookup {
private static final Logger log = LoggerFactory.getLogger(PgSchoolLookup.class);

private final PostgresSqlDb database;

/**
* Constructor for PgSchoolLookup.
*
* @param database - the postgres datasource to use
*/
@Inject
public PgSchoolLookup(final PostgresSqlDb database) {
this.database = database;
}

/**
* Look up a school by URN from the schools_2022 database table.
* This is a fallback for schools that no longer exist in the current Elasticsearch index.
*
* @param schoolUrn - the URN of the school to look up
* @return a School object if found, or null if not found
* @throws SegueDatabaseException if there is a database error
*/
public School findSchoolById(final String schoolUrn) throws SegueDatabaseException {
if (schoolUrn == null || schoolUrn.isEmpty()) {
return null;
}

String query = "SELECT urn, school_name FROM schools_2022 WHERE urn = ?";

try (Connection conn = database.getDatabaseConnection();
PreparedStatement pst = conn.prepareStatement(query)) {
pst.setString(1, schoolUrn);

try (ResultSet results = pst.executeQuery()) {
if (results.next()) {
School school = new School();
school.setUrn(results.getString("urn"));
school.setName(results.getString("school_name"));
school.setClosed(true);
school.setDataSource(School.SchoolDataSource.GOVERNMENT_UK);
log.debug("Found school {} in fallback database table schools_2022",
sanitiseExternalLogValue(schoolUrn));
return school;
}
}
} catch (SQLException e) {
String errorMsg = String.format("Error looking up school with URN %s from schools_2022 table",
sanitiseExternalLogValue(schoolUrn));
log.error(errorMsg, e);
throw new SegueDatabaseException(errorMsg, e);
}

log.debug("School with URN {} not found in fallback database table schools_2022",
sanitiseExternalLogValue(schoolUrn));
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.util.Lists;
import com.google.inject.Inject;
import jakarta.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import org.elasticsearch.ElasticsearchStatusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.ac.cam.cl.dtg.isaac.dos.users.School;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.search.BasicSearchParameters;
import uk.ac.cam.cl.dtg.segue.search.ISearchProvider;
import uk.ac.cam.cl.dtg.segue.search.SegueSearchException;
Expand All @@ -53,19 +55,23 @@ public class SchoolListReader {
private static final Logger log = LoggerFactory.getLogger(SchoolListReader.class);

private final ISearchProvider searchProvider;
private final PgSchoolLookup schoolFallbackLookup;

private final ObjectMapper mapper = getSharedBasicObjectMapper();

private final String dataSourceModificationDate;

/**
* SchoolListReader constructor.
* SchoolListReader constructor with database fallback for missing schools.
*
* @param searchProvider - search provider that can be used to put and retrieve school data.
* @param schoolFallbackLookup - optional fallback for schools not found in search index (can be null).
*/
@Inject
public SchoolListReader(final ISearchProvider searchProvider) {
public SchoolListReader(final ISearchProvider searchProvider,
@Nullable final PgSchoolLookup schoolFallbackLookup) {
this.searchProvider = searchProvider;
this.schoolFallbackLookup = schoolFallbackLookup;

String modificationDate;
try {
Expand All @@ -79,6 +85,16 @@ public SchoolListReader(final ISearchProvider searchProvider) {
dataSourceModificationDate = modificationDate;
}

/**
* SchoolListReader constructor without database fallback.
* Used by ETL module where database access is not available.
*
* @param searchProvider - search provider that can be used to put and retrieve school data.
*/
public SchoolListReader(final ISearchProvider searchProvider) {
this(searchProvider, null);
}

/**
* findSchoolByNameOrPostCode.
*
Expand Down Expand Up @@ -146,7 +162,8 @@ public School findSchoolById(final String schoolURN) throws UnableToIndexSchools
SCHOOL_URN_FIELDNAME.toLowerCase() + "." + UNPROCESSED_SEARCH_FIELD_SUFFIX, schoolURN, null).getResults();

if (matchingSchoolList.isEmpty()) {
return null;
// Fallback to database lookup for schools that are no longer in the current index
return findSchoolByIdFromFallback(schoolURN);
}

if (matchingSchoolList.size() > 1) {
Expand All @@ -157,6 +174,34 @@ public School findSchoolById(final String schoolURN) throws UnableToIndexSchools
return mapper.readValue(matchingSchoolList.get(0), School.class);
}

/**
* Fallback lookup for schools not found in Elasticsearch.
* This queries the schools_2022 database table for schools that have been
* removed from the current school list but are still referenced by user accounts.
*
* @param schoolURN - the URN of the school to look up
* @return the School if found in the fallback, or null if not found
*/
private School findSchoolByIdFromFallback(final String schoolURN) {
if (schoolFallbackLookup == null) {
log.debug("No fallback lookup configured, returning null for school URN: {}",
sanitiseExternalLogValue(schoolURN));
return null;
}

try {
School fallbackSchool = schoolFallbackLookup.findSchoolById(schoolURN);
if (fallbackSchool != null) {
log.info("Found school {} in fallback database table (not in current Elasticsearch index)",
sanitiseExternalLogValue(schoolURN));
}
return fallbackSchool;
} catch (SegueDatabaseException e) {
log.error("Error looking up school {} from fallback database", sanitiseExternalLogValue(schoolURN), e);
return null;
}
}


/**
* Ensure School List has been generated.
Expand Down
Loading
Loading