ApplicationStartup.java
/*
* Copyright 2019 Global Crop Diversity Trust
*
* 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.gringlobal.spring;
import static org.gringlobal.service.LanguageService.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.persistence.NonUniqueResultException;
import javax.transaction.Transactional;
import com.querydsl.jpa.impl.JPAQueryFactory;
import liquibase.changelog.DatabaseChangeLog;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.genesys.blocks.oauth.model.OAuthRole;
import org.genesys.blocks.oauth.persistence.OAuthClientRepository;
import org.genesys.blocks.oauth.service.OAuthClientService;
import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.security.serialization.Permissions;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.service.RepositoryService;
import org.gringlobal.custom.liquibase.ProgrammableChangeSet;
import org.gringlobal.custom.liquibase.ProgrammableLiquibase;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryExtra;
import org.gringlobal.model.InventoryMaintenancePolicy;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.QInventoryExtra;
import org.gringlobal.model.QInventoryMaintenancePolicy;
import org.gringlobal.model.QInventoryViability;
import org.gringlobal.model.QSysUser;
import org.gringlobal.model.SeedInventoryExtra;
import org.gringlobal.model.Site;
import org.gringlobal.model.SysUser;
import org.gringlobal.model.community.CommunityAppSettings;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.community.QSecuredAction;
import org.gringlobal.model.community.SecuredAction;
import org.gringlobal.model.community.SecurityAction;
import org.gringlobal.model.notification.NotificationSchedule;
import org.gringlobal.model.security.UserRole;
import org.gringlobal.notification.KPINotifications;
import org.gringlobal.notification.action.NotificationMessageEmailSender;
import org.gringlobal.persistence.InventoryMaintenancePolicyRepository;
import org.gringlobal.persistence.SiteRepository;
import org.gringlobal.persistence.SysGroupRepository;
import org.gringlobal.persistence.community.SecuredActionRepository;
import org.gringlobal.service.AccessionService;
import org.gringlobal.service.AppSettingsService;
import org.gringlobal.service.CodeValueService;
import org.gringlobal.service.DataviewServices.SysDataviewService;
import org.gringlobal.service.InventoryExtraService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.JasperReportService;
import org.gringlobal.service.LanguageService;
import org.gringlobal.service.NotificationScheduleService;
import org.gringlobal.service.TableServices.SysTableFieldService;
import org.gringlobal.service.TransientMessageService;
import org.gringlobal.service.UserService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.google.common.collect.Lists;
/**
* Run things at startup and after application context is initialized.
*/
@Component
@DependsOn({ "currentApplicationContext" })
@Slf4j
public class ApplicationStartup implements InitializingBean {
public static final String SID_GROUP_ADMINS = "GROUP_ADMINS";
private static final String CHANGESET_AUTHOR = "GGCE"; // DO NOT MODIFY
@Value("${default.oauthclient.clientId}")
private String defaultOAuthClientId;
@Value("${default.oauthclient.clientSecret}")
private String defaultOAuthClientSecret;
@Value("${base.url}")
private String baseUrl;
@Value("${frontend.url}")
private String frontendUrl;
@Autowired
private DataSource dataSource;
@Autowired
private JPAQueryFactory jpaQueryFactory;
@Autowired
private SecuredActionRepository securedActionRepository;
@Autowired
private OAuthClientRepository oauthClientRepository;
@Autowired
private OAuthClientService oauthService;
@Autowired
private InventoryMaintenancePolicyRepository inventoryMaintPolicyRepository;
@Autowired
private UserService userService;
@Autowired
private CustomAclService aclService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private SysGroupRepository sysGroupRepository;
@Autowired
private SiteRepository siteRepository;
@Autowired
private CodeValueService codeValueService;
@Autowired
private AppSettingsService appSettingsService;
@Autowired
private AccessionService accessionService;
@Autowired
private InventoryService inventoryService;
@Autowired
private LanguageService languageService;
@Autowired
private TransientMessageService transientMessageService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private InventoryExtraService inventoryExtraService;
@Autowired
private NotificationScheduleService notificationScheduleService;
@Autowired
private SysDataviewService sysDataviewService;
@Autowired
private SysTableFieldService sysTableFieldService;
@Autowired
private OAuthClientService oAuthClientService;
/**
* Things to run immediately
*/
@Override
public void afterPropertiesSet() throws Exception {
startup();
}
/**
* Startup.
*/
@Transactional
void startup() throws Exception {
ensure1OAuthClient();
ensureSystemInventoryPolicy();
ensureAuthoritySIDs();
ensureAcl();
ensureGGCESecurity();
ensureCommunityCodeValues(); // Add missing community CodeValues
ensureCommunityAppSettings(); // Add missing community AppSettings
// check cooperator for sysUsers
checkSysUserCooperator();
/**
* One-time upgrades
*/
var stopWatch = StopWatch.createStarted();
DatabaseChangeLog databaseChangeLog = new DatabaseChangeLog();
databaseChangeLog.setLogicalFilePath("ApplicationStartup"); // DO NOT MODIFY
// To run on every startup, set alwaysRun to true.
// To re-run a change once (after code update), update the #id.
// IMPORTANT: ID + filePath + author make a changeset unique!
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1662789283001", "ApplicationStartup.createRepositoryFolder_AIA", this::createRepositoryFolder_AIA, "Create default folder for AccessionInvAttach", "add '/AIA' repository folder", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1662789284001", "ApplicationStartup.addSysLangMCPD", this::addSysLangMCPD, "Add MCPD language", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1724397839000", "ApplicationStartup.upgradeDataviewsForGGCE", this::upgradeDataviewsForGGCE, "Upgrade GG dataviews to GGCE", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1662789284003", "ApplicationStartup.installJasperTemplate_InventoryLabels", this::installJasperTemplate_InventoryLabels, "Install inventoryLabels.jrxml", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
// November 2022
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1669369126182", "ApplicationStartup.generateSeedInventoryExtra", this::generateSeedInventoryExtra, "Generate SeedInventoryExtra for last completed InventoryViability", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
// May 2023
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1683170549000", "ApplicationStartup.ensureAccessionAndInventoryNumbers", this::ensureAccessionAndInventoryNumbers, "Generate accessionNumbers and inventoryNumbers", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
// July 2023
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1688985547646", "ApplicationStartup.ensureSystemInventories", this::ensureSystemInventories, "Generate missing system inventories for accessions", "", "", "", true, CHANGESET_AUTHOR, databaseChangeLog));
// April 2024
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1712052420998", "ApplicationStartup.convertScheduledMethodsToNotificationSchedule", this::convertScheduledMethodsToNotificationSchedule, "Convert scheduled notification methods to NotificationSchedule", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
// May 2024
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1715090120046", "ApplicationStartup.addPermissionOnViabilityAttachmentFolderForAllUsers", this::addPermissionOnViabilityAttachmentFolderForAllUsers, "Add permissions on ViabilityAttachment folder for all users", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
// June 2024
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1717826756000", "ApplicationStartup.addActionNotificationProcessor", this::addActionNotificationProcessor, "Register action notifications processor", "", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
// Nov 2024
databaseChangeLog.addChangeSet(new ProgrammableChangeSet("1730741847000", "ApplicationStartup.addOrUpdateFieldBookClient", this::addOrUpdateFieldBookClient, "Register Fieldbook app", "Add or update client for Fieldbook app", "", "", false, CHANGESET_AUTHOR, databaseChangeLog));
ProgrammableLiquibase programmableLiquibase = new ProgrammableLiquibase(databaseChangeLog);
programmableLiquibase.setDataSource(dataSource);
stopWatch.split();
log.warn("Prepared programmable Liquibase changes in {}ms", stopWatch.getSplitTime());
stopWatch.unsplit();
programmableLiquibase.doUpgrades();
stopWatch.stop();
log.warn("Executed programmable Liquibase changes in {}ms", stopWatch.getTime(TimeUnit.MILLISECONDS));
checkParentSystemInventory();
}
private void addSysLangMCPD() {
try {
asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> languageService.ensureSysLang(MCPD_IETF_TAG, "MCPD", "MCPD", null)));
} catch (Throwable e) {
log.error("Could not create SysLang MCPD: {}", e.getMessage(), e);
}
}
private void ensureAccessionAndInventoryNumbers() throws Exception {
asAdmin(() -> {
accessionService.assignMissingAccessionNumbers();
inventoryService.assignMissingInventoryNumbers();
return true;
});
}
private void ensureSystemInventories() throws Exception {
asAdmin(() -> {
inventoryService.ensureSystemInventories();
return true;
});
}
/**
* Make sure we have all the required CodeValues
*/
private void ensureCommunityCodeValues() {
try {
asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> {
CommunityCodeValues.listCommunityCodeValues().forEach((ccv) -> codeValueService.ensureCodeValue(ccv.groupName, ccv.value, ccv.title, ccv.description));
return true;
}));
} catch (Throwable e) {
log.error("Could not set up CodeValue: {}", e.getMessage(), e);
}
}
/**
* Make sure we have all the required AppSettings
*/
private void ensureCommunityAppSettings() {
try {
asAdmin(() -> {
CommunityAppSettings.listCommunityAppSettings().forEach((setting) -> {
try {
appSettingsService.ensureAppSetting(setting.categoryTag, setting.name, setting.value);
} catch (IncorrectResultSizeDataAccessException | NonUniqueResultException e) {
log.warn("Could not set up AppSetting {} with categoryTag={}: {}", setting.name, setting.categoryTag, e.getMessage());
}
});
return true;
});
} catch (Throwable e) {
log.error("Could not set up AppSetting: {}", e.getMessage(), e);
}
}
private void ensureGGCESecurity() {
try {
asAdmin(() -> {
var groupAdmins = aclService.ensureAuthoritySid(SID_GROUP_ADMINS);
// Each security action requires an entry
for (SecurityAction action : SecurityAction.values()) {
var securedAction = securedActionRepository.getByActionAndSite(action.name(), null);
if (securedAction == null) {
log.warn("Registering Security action {}", action);
securedAction = new SecuredAction();
securedAction.setAction(action.name());
securedActionRepository.save(securedAction);
aclService.createOrUpdatePermissions(securedAction, groupAdmins); // this doesn't do much for groupAdmins
}
}
return true;
});
} catch (Throwable e) {
log.error("Could not set up ACL: {}", e.getMessage(), e);
}
// Remove invalid actions
try {
asAdmin(() -> {
var validActions = Stream.of(SecurityAction.values()).map(Enum::name).collect(Collectors.toList());
var invalidActions = securedActionRepository.findAll(QSecuredAction.securedAction.action.notIn(validActions));
invalidActions.forEach(invalidAction -> {
log.warn("Removing SecuredAction with invalid action code: {} site={}", invalidAction.getAction(), invalidAction.getSite());
});
securedActionRepository.deleteAll(invalidActions);
return true;
});
} catch (Throwable e) {
log.error("Could not delete invalid actions", e);
}
// GROUP_ADMINS needs all permissions
try {
asAdmin(() -> {
var groupAdmins = aclService.ensureAuthoritySid(SID_GROUP_ADMINS);
// Each security action requires an entry
for (SecurityAction action : SecurityAction.values()) {
var securedAction = securedActionRepository.getByActionAndSite(action.name(), null);
if (securedAction != null) {
log.info("Allowing {} all permissions on Security action {}", groupAdmins.getSid(), action);
aclService.setPermissions(securedAction, groupAdmins, new Permissions().grantAll());
}
}
return true;
});
} catch (Throwable e) {
log.error("Could not set up ACL: {}", e.getMessage(), e);
}
}
private void ensureAcl() {
try {
asAdmin(() -> {
// Sites
aclService.createOrUpdatePermissions(Site.ACL_CLASS_IDENTITY);
aclService.makePubliclyReadable(Site.ACL_CLASS_IDENTITY, true);
siteRepository.findAll().forEach((site) -> {
// SysUser user = userService.getUserForCooperator(site.getOwnedBy());
// FIXME Should be introduced method getUserForAclSid() ???
SysUser user = null;
if (site.getOwnedBy() instanceof SysUser) {
user = (SysUser) site.getOwnedBy();
}
if (user != null) {
log.warn("User {} owns site {} coop={}", user, site.getSiteShortName(), site.getOwnedBy().getId());
aclService.createOrUpdatePermissions(site, user);
} else {
log.warn("Site {} owner acl_sid.id={} has no SysUser", site.getSiteShortName(), site.getOwnedBy().getId());
}
});
return true;
});
} catch (Throwable e) {
log.error("Could not set up ACL: {}", e.getMessage(), e);
}
try {
asAdmin(() -> {
// Inventory maintenance policies
aclService.createOrUpdatePermissions(InventoryMaintenancePolicy.ACL_CLASS_IDENTITY);
aclService.makePubliclyReadable(InventoryMaintenancePolicy.ACL_CLASS_IDENTITY, true);
inventoryMaintPolicyRepository.findAll().forEach((inventoryMaintPolicy) -> {
// SysUser user = userService.getUserForCooperator(inventoryMaintPolicy.getOwnedBy());
// FIXME Should be introduced method getUserForAclSid() ???
SysUser user = null;
if (inventoryMaintPolicy.getOwnedBy() instanceof SysUser) {
user = (SysUser) inventoryMaintPolicy.getOwnedBy();
}
if (user != null) {
log.warn("User {} owns inventory maintenance policy {} acl_sid.id={}", user, inventoryMaintPolicy.getMaintenanceName(), inventoryMaintPolicy.getOwnedBy().getId());
aclService.createOrUpdatePermissions(inventoryMaintPolicy, user);
} else {
log.warn("Inventory maintenance policy {} owner acl_sid.id={} of has no SysUser", inventoryMaintPolicy.getMaintenanceName(), inventoryMaintPolicy.getOwnedBy().getId());
// Use admin as owner
aclService.createOrUpdatePermissions(inventoryMaintPolicy);
}
});
return true;
});
} catch (Throwable e) {
log.error("Could not set up ACL: {}", e.getMessage(), e);
}
}
// @Autowired
// private SiteLocationRepository siteLocationRepository;
// @Autowired
// private SiteLocationService siteLocationService;
// private void makeSiteLocations() {
// LOG.warn("Generating sites from existing Inventory");
// try {
// if (siteLocationRepository.count() < 100) {
// Long count = asAdmin(() -> {
// return siteLocationService.generateSiteLocations();
// });
// LOG.warn("Added {} new SiteLocations from Inventory records", count);
// }
// } catch (Throwable e) {
// LOG.warn("Could not migrate Inventory locations to SiteLocation: {}", e.getMessage(), e);
// }
// }
private void createRepositoryFolder_AIA() {
try {
asAdmin(() -> {
// ensure AIA folder has write permissions
RepositoryFolder folder = repositoryService.ensureFolder(Paths.get("/AIA"));
AclSid roleUser = aclService.ensureAuthoritySid("ROLE_USER");
Permissions writePermissions = new Permissions().grantAll();
aclService.setPermissions(folder, roleUser, writePermissions);
// customAclService.makePubliclyReadable(folder, true);
return folder;
});
} catch (Throwable e) {
log.error("Could not ensure default folders: {}", e.getMessage(), e);
}
}
private void ensureSystemInventoryPolicy() {
InventoryMaintenancePolicy sysInvMaintPol = inventoryMaintPolicyRepository.findOne(QInventoryMaintenancePolicy.inventoryMaintenancePolicy.maintenanceName.eq("SYSTEM"))
.orElse(null);
if (sysInvMaintPol == null) {
log.info("Inventory maintenance policy named SYSTEM is missing");
try {
asAdmin(() -> {
InventoryMaintenancePolicy policy = new InventoryMaintenancePolicy();
policy.setFormTypeCode(Inventory.SYSTEM_INVENTORY_FTC);
policy.setManagementTypeCode(CommunityCodeValues.MANAGEMENT_TYPE_SYSTEM.value);
policy.setMaintenanceName("SYSTEM");
policy.setIsAutoDeducted("N");
return inventoryMaintPolicyRepository.save(policy);
});
log.warn("Registered an Inventory maintenance policy named SYSTEM.");
} catch (Throwable e) {
log.error("Could not create SYSTEM inventory maintenance policy: {}", e.getMessage(), e);
}
}
}
private void ensureAuthoritySIDs() {
try {
TransactionHelper.executeInTransaction(false, () -> {
return asAdmin(() -> {
// Ensure system-wide user roles are defined
for (UserRole systemRole : UserRole.values()) {
log.info("Ensuring SID for role {}", systemRole.getAuthority());
aclService.ensureAuthoritySid(systemRole.getAuthority());
}
// Ensure SysGroups are registered as SIDs
sysGroupRepository.findAll().forEach(sysGroup -> {
aclService.ensureAuthoritySid(sysGroup.getAuthority());
});
return true;
});
});
} catch (Throwable e) {
log.error("Could not ensure SID authorities: {}", e.getMessage(), e);
}
}
/**
* Ensure 1 OAuth client.
*/
private void ensure1OAuthClient() {
String frontendOrigin = null;
boolean isLocalFrontend = false;
try {
var frontend = new URL(frontendUrl);
if (frontend.getPort() != frontend.getDefaultPort() && frontend.getPort() != -1) {
frontendOrigin = String.format("%s://%s:%s", frontend.getProtocol(), frontend.getHost(), frontend.getPort());
} else {
frontendOrigin = String.format("%s://%s", frontend.getProtocol(), frontend.getHost());
}
isLocalFrontend = StringUtils.equalsIgnoreCase("localhost", frontend.getHost())
|| StringUtils.equalsIgnoreCase("127.0.0.1", frontend.getHost())
// TODO IPv6
;
} catch (Exception e) {
log.error("Invalid frontend URL {}", frontendUrl, e);
return;
}
String baseUrlOrigin = baseUrl;
try {
var base = new URL(baseUrlOrigin);
if (base.getPort() != base.getDefaultPort() && base.getPort() != -1) {
baseUrlOrigin = String.format("%s://%s:%s", base.getProtocol(), base.getHost(), base.getPort());
} else {
baseUrlOrigin = String.format("%s://%s", base.getProtocol(), base.getHost());
}
} catch (Exception e) {
log.error("Invalid base URL for origin {}", baseUrl, e);
}
OAuthClient existingDefaultClient = oauthClientRepository.findByClientId(defaultOAuthClientId);
if (oauthClientRepository.count() == 0 || existingDefaultClient == null) {
log.warn("Creating default OAuth client {}", defaultOAuthClientId);
OAuthClient client = new OAuthClient();
client.setActive(true);
client.setClientId(defaultOAuthClientId);
client.setClientSecret(passwordEncoder.encode(defaultOAuthClientSecret));
client.getAuthorizedGrantTypes().add("authorization_code");
client.getAuthorizedGrantTypes().add("client_credentials");
client.getRoles().add(OAuthRole.CLIENT);
client.getRoles().add(OAuthRole.TRUSTED_CLIENT);
client.getScope().add("read");
client.getScope().add("write");
client.getScope().add("trust");
client.setTitle("Default OAuth client");
client.setDescription("This OAuth client was automatically created by the system.");
client.getRegisteredRedirectUri().add(frontendUrl);
client.getRegisteredRedirectUri().add((baseUrl.startsWith("http://localhost") ? baseUrl.replace("http://localhost", "http://127.0.0.1") : baseUrl) + "/login/oauth2/code/local"); // for testing
client.getRegisteredRedirectUri().add((baseUrl.startsWith("http://localhost") ? baseUrl.replace("http://localhost", "http://127.0.0.1") : baseUrl) + "/swagger-ui/oauth2-redirect.html");
client.getAllowedOrigins().add(baseUrlOrigin);
client.getAllowedOrigins().add(frontendOrigin);
client.getRegisteredRedirectUri().add(frontendUrl);
// Add local development environment when on localhost
if (isLocalFrontend) {
client.getAllowedOrigins().add("http://127.0.0.1:3000");
client.getRegisteredRedirectUri().add("http://127.0.0.1:3000");
// The https version
client.getAllowedOrigins().add("https://127.0.0.1:3433");
client.getRegisteredRedirectUri().add("https://127.0.0.1:3443");
}
client = oauthClientRepository.save(client);
} else {
log.info("Updating default configuration for OAuth client {}", defaultOAuthClientId);
OAuthClient client = existingDefaultClient;
if (client != null) {
client.getAllowedOrigins().add(baseUrlOrigin);
client.getAllowedOrigins().add(frontendOrigin);
client.getRegisteredRedirectUri().add(frontendOrigin);
client.getRegisteredRedirectUri().add((baseUrl.startsWith("http://localhost") ? baseUrl.replace("http://localhost", "http://127.0.0.1") : baseUrl) + "/login/oauth2/code/local"); // for testing
client.getRegisteredRedirectUri().add((baseUrl.startsWith("http://localhost") ? baseUrl.replace("http://localhost", "http://127.0.0.1") : baseUrl) + "/swagger-ui/oauth2-redirect.html");
// Add local development environment when on localhost
if (isLocalFrontend) {
client.getAllowedOrigins().add("http://127.0.1:3000");
client.getAllowedOrigins().add("https://127.0.1:3443");
client.getRegisteredRedirectUri().add("http://127.0.0.1:3000");
client.getRegisteredRedirectUri().add("https://127.0.0.1:3443");
}
client.getAuthorizedGrantTypes().remove("implicit");
client.getAuthorizedGrantTypes().remove("password");
client.getRoles().add(OAuthRole.TRUSTED_CLIENT);
oauthService.updateClient(client.getId(), client.getVersion(), client);
client = oauthClientRepository.findByClientId(defaultOAuthClientId);
log.warn("Default OAuth client updated id={} redirect={} origins={}", client.getId(), client.getRedirect(), client.getOrigins());
}
}
}
/**
* Ensure KSU Fieldbook app client.
*/
private void addOrUpdateFieldBookClient() {
try {
var fieldbookClient = asAdmin(() -> {
OAuthClient client = oAuthClientService.getClient("fieldbook");
if (client == null) {
log.info("Trying to create 'fieldbook' OAuth client");
client = new OAuthClient();
client.setActive(false); // Deactivated by default
client.setClientId("fieldbook");
client.setClientSecret(null);
client.getAuthorizedGrantTypes().add("authorization_code");
client.getAuthorizedGrantTypes().add("implicit");
client.getRoles().add(OAuthRole.CLIENT);
client.getScope().add("read");
client.getScope().add("write");
client.setTitle("FieldBook App");
client.setDescription("OAuth client for KSU Field Book app.");
var redirectUrls = client.getRegisteredRedirectUri(); // this is picked up by the Fieldbook app (manifest.xml)
redirectUrls.add("fieldbook://app/auth");
redirectUrls.add("https://phenoapps.org/field-book");
redirectUrls.add("https://fieldbook.phenoapps.org");
client = oAuthClientService.addClient(client);
} else {
// Update existing client with correct configuration for FieldBook
client.setClientSecret(null); // No secret
client.getAuthorizedGrantTypes().clear();
client.getAuthorizedGrantTypes().add("authorization_code");
var redirectUrls = client.getRegisteredRedirectUri(); // this is picked up by the Fieldbook app (manifest.xml)
redirectUrls.add("fieldbook://app/auth");
redirectUrls.add("https://phenoapps.org/field-book");
redirectUrls.add("https://fieldbook.phenoapps.org");
client = oAuthClientService.updateClient(client.getId(), client.getVersion(), client);
}
return client;
});
fieldbookClient = oAuthClientService.getClient("fieldbook");
log.warn("Updated Fieldbook app client: {}", fieldbookClient);
} catch (Exception e) {
log.error("Could not update Fieldbook app client", e);
}
}
private void upgradeDataviewsForGGCE() {
try {
asAdmin(() -> {
sysTableFieldService.generateMapping("acl_sid", "id");
sysTableFieldService.generateMapping("accession", "site_id");
sysTableFieldService.generateMapping("accession", "exploration_id");
sysTableFieldService.generateMapping("inventory", "site_id");
sysTableFieldService.generateMapping("inventory", "barcode");
sysTableFieldService.generateMapping("inventory", "production_location_geography_id");
sysTableFieldService.generateMapping("inventory_maint_policy", "management_type_code");
sysTableFieldService.generateMapping("sys_lang", "is_enabled");
return true;
});
} catch (Exception e) {
log.error("Could not: {}", e.getMessage(), e);
}
var ggceDataviews = List.of(
// ACL
"ggce_aclsid_lookup", "sys_user", "get_sys_user", "validate_login", "cooperator_lookup", "big_cooperator_lookup",
// model updates
"get_accession", "get_inventory"
// code values
, "get_code_value", "get_code_value_lang"
// don't forget to include new dataviews!
, "get_inventory_maint_policy", "get_site"
// sys_lang with is_enabled
, "get_sys_language"
// sys_matrix_input fixed for code_lang lookups
, "sys_matrix_input"
// New CT Attachment Wizard: attach_wizard_get_accession_inv_attach_filepath -- GGCE /AIA/...
, "attach_wizard_get_accession_inv_attach_filepath"
// Old CT Attachment Wizard -- GGCE /AIA/....
, "inventory_attach_wizard_get_filepath"
); // WARNING: If you change this, then update the changeLogId above to run it!
for (var ggceDv : ggceDataviews) {
try {
asAdmin(() -> {
try (InputStream ggceDefinition = ApplicationStartup.class.getResourceAsStream("/dataviews/" + ggceDv + ".dataviewxml")) {
if (ggceDefinition == null) {
log.warn("Custom Dataview definition for {} not found at /dataviews/{}/.dataviewxml", ggceDv, ggceDv);
return null;
}
var existingDataview = sysDataviewService.load(ggceDv);
if (existingDataview != null) {
log.warn("Updating Dataview: {}", ggceDv);
return sysDataviewService.updateDataviewFromXML(ggceDefinition, existingDataview);
} else {
log.warn("Importing Dataview: {}", ggceDv);
return sysDataviewService.registerDataviewFromXML(ggceDefinition);
}
}
});
} catch (Exception e) {
log.error("Could not register {} dataview: {}", ggceDv, e.getMessage(), e);
}
}
}
private void checkSysUserCooperator() {
var userWithoutCooperator = jpaQueryFactory.selectFrom(QSysUser.sysUser)
.where(QSysUser.sysUser.cooperator().isNull().and(QSysUser.sysUser.isEnabled.eq("Y"))).limit(100).fetch();
for (var user : userWithoutCooperator) {
transientMessageService.addAdminAlert(
"sysUserNullCooperator-" + user.getId(),
"\"" + user.getSid() + "\" is missing their Cooperator record."
);
}
var repeatCooperatorIds = jpaQueryFactory.from(QSysUser.sysUser).select(QSysUser.sysUser.cooperator().id)
.where(QSysUser.sysUser.isEnabled.eq("Y"))
.groupBy(QSysUser.sysUser.cooperator().id)
.having(QSysUser.sysUser.cooperator().id.count().gt(1))
.fetch();
if (!repeatCooperatorIds.isEmpty()) {
transientMessageService.addAdminAlert(
"sysUserSameCooperators",
"Each active user must have a unique Cooperator record for the Curator Tool to function properly."
);
var userWithDupeCoop = jpaQueryFactory.selectFrom(QSysUser.sysUser)
.where(QSysUser.sysUser.cooperator().id.in(repeatCooperatorIds)).limit(100).fetch();
for (var user : userWithDupeCoop) {
transientMessageService.addAdminAlert(
"sysUserSameCooperators-" + user.getId(),
"\"" + user.getSid() + "\" is sharing the Cooperator record with ID=" + user.getCooperator().getId() + "."
);
}
}
var usersWithoutSite = jpaQueryFactory.selectFrom(QSysUser.sysUser)
.where(QSysUser.sysUser.isEnabled.eq("Y").and(QSysUser.sysUser.cooperator().site().isNull()))
.limit(100).fetch();
if (!usersWithoutSite.isEmpty()) {
transientMessageService.addAdminAlert(
"sysUserNullSite",
"Some active users are using Cooperators that without a Site record. This will break the Curator Tool."
);
for (var user : usersWithoutSite) {
transientMessageService.addAdminAlert(
"sysUserNullSite-" + user.getId(),
"\"" + user.getSid() + "\"'s Cooperator record does not specify the Site."
);
}
}
}
private void installJasperTemplate_InventoryLabels() {
addJasperTemplate("/reports/Inventory/InventoryLabels.jrxml", "InventoryLabels.jrxml", "Inventory QR labels - 3 columns", null, Inventory.class);
}
/**
* Install stock PDF templates
* @param resourcePath
* @param targetFilename
* @param title
* @param description
* @param targetClass
* @throws IOException
* @throws FileNotFoundException
* @throws InvalidRepositoryFileDataException
* @throws InvalidRepositoryPathException
*/
private void addJasperTemplate(String resourcePath, String targetFilename, String title, String description, Class<?> targetClass) {
var reportMetadata = new RepositoryFile();
reportMetadata.setCreated("support@ggce.genesys-pgr.org");
reportMetadata.setLicense("CC-BY-NC");
try (var reportInputStream = Files.newInputStream(Path.of(getClass().getResource(resourcePath).toURI()))) {
reportMetadata.setTitle(title);
reportMetadata.setDescription(StringUtils.defaultIfBlank(description, "Stock template"));
var folderPath = Path.of(JasperReportService.REPORT_PATH, targetClass.getSimpleName());
asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> {
repositoryService.ensureFolder(folderPath);
RepositoryFile file;
try {
file = repositoryService.getFile(folderPath, targetFilename);
log.info("PDF template {} already exists in {}", targetFilename, folderPath);
} catch (NoSuchRepositoryFileException e) {
file = repositoryService.addFile(folderPath, targetFilename, null, reportInputStream, reportMetadata);
log.warn("Added PDF template {} to {}", targetFilename, folderPath);
}
return file;
}));
} catch (Exception e) {
log.warn("Could not add '': {} {}", resourcePath, e.getMessage());
}
}
private void generateSeedInventoryExtra() {
try {
QInventoryViability qViability = new QInventoryViability("inventoryViability");
QInventoryViability qViability2 = new QInventoryViability("inventoryViability2");
QInventoryViability qViability3 = new QInventoryViability("inventoryViability3");
asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> jpaQueryFactory.delete(QInventoryExtra.inventoryExtra).execute())); // Delete any existing extras.
var lastViabilityList = jpaQueryFactory.selectFrom(qViability)
.where(qViability.percentViable.isNotNull()
.and(qViability.inventory().formTypeCode.ne(Inventory.SYSTEM_INVENTORY_FTC)) // No system inventories, please
// where percentViable is not null and max(testedDate) and max(inventoryViability.id)
.and(qViability.id.eq(
jpaQueryFactory
.from(qViability2)
.select(qViability2.id.max())
.where(
qViability2.percentViable.isNotNull()
.and(qViability2.testedDate.eq(
jpaQueryFactory
.from(qViability3)
.select(qViability3.testedDate.max())
.where(
qViability3.percentViable.isNotNull()
.and(qViability3.inventory().eq(qViability2.inventory()))
)
))
.and(qViability2.inventory().eq(qViability.inventory()))
)
))
)
.fetch();
List<InventoryExtra> extraForSave = new ArrayList<>();
lastViabilityList.forEach(inventoryViability -> {
SeedInventoryExtra seedInventoryExtra = new SeedInventoryExtra();
seedInventoryExtra.setInventory(inventoryViability.getInventory());
seedInventoryExtra.setLastViability(inventoryViability);
extraForSave.add(seedInventoryExtra);
});
log.warn("Saving {} SeedInventoryExtra", extraForSave.size());
asAdmin(() -> TransactionHelper.executeInTransaction(false, () -> inventoryExtraService.create(extraForSave)));
} catch (Throwable e) {
log.warn("Could not generate inventory extra with last viability");
throw new RuntimeException(e);
}
}
private void checkParentSystemInventory() {
QInventory inventory1 = new QInventory("inventory1");
QInventory inventory2 = new QInventory("inventory2");
var inventoryWithSystemParent = jpaQueryFactory.select(inventory1.id).from(inventory1)
.innerJoin(inventory1.parentInventory(), inventory2)
.where(inventory2.formTypeCode.eq(Inventory.SYSTEM_INVENTORY_FTC))
.orderBy(inventory1.id.asc())
.limit(1).fetch();
if (!inventoryWithSystemParent.isEmpty()) {
transientMessageService.addAdminAlert(
"inventoriesWithSystemParent", "Using SYSTEM inventory as a parent inventory is discouraged."
);
}
}
private void convertScheduledMethodsToNotificationSchedule() throws Exception {
asAdmin(() -> {
// KPINotifications.sendAllKPIs
NotificationSchedule sendAllKPIsNotification = new NotificationSchedule();
sendAllKPIsNotification.setGenerator(KPINotifications.class.getName());
sendAllKPIsNotification.setMethod("sendAllKPIs");
sendAllKPIsNotification.setCron("0 0 12 ? * MON-FRI");
sendAllKPIsNotification.setIsActive("Y");
sendAllKPIsNotification.setTimezone(ZoneId.systemDefault().getId());
notificationScheduleService.createFast(sendAllKPIsNotification);
return true;
});
}
private void addActionNotificationProcessor() throws Exception {
asAdmin(() -> {
// KPINotifications.sendAllKPIs
NotificationSchedule processorSchedule = new NotificationSchedule();
processorSchedule.setGenerator(NotificationMessageEmailSender.class.getName());
processorSchedule.setMethod("pendingMessageSender");
processorSchedule.setCron("0 0/30 7-17 * * MON-FRI");
processorSchedule.setIsActive("Y");
processorSchedule.setTimezone(ZoneId.systemDefault().getId());
processorSchedule.setTitle("Action notifications");
processorSchedule = notificationScheduleService.createFast(processorSchedule);
notificationScheduleService.subscribe(processorSchedule, List.of("GROUP_ADMINS"));
return true;
});
}
private void addPermissionOnViabilityAttachmentFolderForAllUsers() throws Exception {
asAdmin(() -> {
var folder = repositoryService.ensureFolder(Paths.get("/inventoryViability"));
aclService.setPermissions(folder, aclService.getAuthoritySid(UserRole.USER.getAuthority()), new Permissions().grantAll());
return true;
});
}
private <T> T asAdmin(Callable<T> callable) throws Exception {
UserDetails administrator = userService.loadUserByUsername("administrator");
List<GrantedAuthority> authorities = Lists.newArrayList(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
authorities.addAll(administrator.getAuthorities());
Authentication authentication = new UsernamePasswordAuthenticationToken(administrator, null, authorities);
return TransactionHelper.asUser(authentication, callable);
}
}