/*
* 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.
*
* Contributions from 2013-2017 where performed either by US government
* employees, or under US Veterans Health Administration contracts.
*
* US Veterans Health Administration contributions by government employees
* are work of the U.S. Government and are not subject to copyright
* protection in the United States. Portions contributed by government
* employees are USGovWork (17USC ยง105). Not subject to copyright.
*
* Contribution by contractors to the US Veterans Health Administration
* during this period are contractually contributed under the
* Apache License, Version 2.0.
*
* See: https://www.usa.gov/government-works
*
* Contributions prior to 2013:
*
* Copyright (C) International Health Terminology Standards Development Organisation.
* Licensed under the Apache License, Version 2.0.
*
*/
package sh.isaac.api;
//~--- JDK imports ------------------------------------------------------------
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.BiConsumer;
//~--- non-JDK imports --------------------------------------------------------
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.hk2.api.MultiException;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.ServiceLocatorFactory;
import org.glassfish.hk2.runlevel.RunLevelController;
//~--- JDK imports ------------------------------------------------------------
import com.sun.javafx.application.PlatformImpl;
//~--- non-JDK imports --------------------------------------------------------
import gov.va.oia.HK2Utilities.HK2RuntimeInitializer;
import sh.isaac.api.DatabaseServices.DatabaseValidity;
import sh.isaac.api.constants.Constants;
import sh.isaac.api.util.HeadlessToolkit;
//~--- classes ----------------------------------------------------------------
/**
* The Class LookupService.
*
* @author kec
* @author <a href="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</a>
*/
@SuppressWarnings("restriction")
public class LookupService {
/** The Constant LOG. */
private static final Logger LOG = LogManager.getLogger();
/** The looker. */
private static volatile ServiceLocator looker = null;
/** The fx platform up. */
private static volatile boolean fxPlatformUp = false;
/** The Constant DATABASE_SERVICES_STARTED_RUNLEVEL. */
public static final int DATABASE_SERVICES_STARTED_RUNLEVEL = 2;
/** The Constant ISAAC_DEPENDENTS_RUNLEVEL. */
public static final int ISAAC_DEPENDENTS_RUNLEVEL = 5;
/** The Constant ISAAC_STARTED_RUNLEVEL. */
public static final int ISAAC_STARTED_RUNLEVEL = 4;
/** The Constant METADATA_STORE_STARTED_RUNLEVEL. */
public static final int METADATA_STORE_STARTED_RUNLEVEL = -1;
/** The Constant WORKERS_STARTED_RUNLEVEL. */
public static final int WORKERS_STARTED_RUNLEVEL = -2;
/** The Constant SYSTEM_STOPPED_RUNLEVEL. */
public static final int SYSTEM_STOPPED_RUNLEVEL = -3;
/** The Constant STARTUP_LOCK. */
private static final Object STARTUP_LOCK = new Object();
/** The discovered validity value. */
private static DatabaseValidity discoveredValidityValue = null;
//~--- methods -------------------------------------------------------------
/**
* Stop all core isaac service, blocking until stopped (or failed).
*/
public static void shutdownIsaac() {
setRunLevel(WORKERS_STARTED_RUNLEVEL);
// Fully release any system locks to database
System.gc();
}
/**
* Stop all system services, blocking until stopped (or failed).
*/
public static void shutdownSystem() {
if (isInitialized()) {
setRunLevel(SYSTEM_STOPPED_RUNLEVEL);
looker.shutdown();
ServiceLocatorFactory.getInstance()
.destroy(looker);
looker = null;
}
}
/**
* This is automatically done when a typical ISAAC pattern is utilized. This method is only exposed as public for obscure use cases where
* we want to utilize the javaFX task API, but are not looking to start up HK2 and other various ISAAC services.
*/
public static void startupFxPlatform() {
if (!fxPlatformUp) {
LOG.debug("FxPlatform is not yet up - obtaining lock");
synchronized (STARTUP_LOCK) {
LOG.debug("Lock obtained, starting fxPlatform");
if (!fxPlatformUp) {
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
if (GraphicsEnvironment.isHeadless()) {
LOG.info("Installing headless toolkit");
HeadlessToolkit.installToolkit();
}
LOG.debug("Starting JavaFX Platform");
PlatformImpl.startup(() -> {
// No need to do anything here
});
fxPlatformUp = true;
}
}
}
}
/**
* Start all core isaac services, blocking until started (or failed).
*/
public static void startupIsaac() {
try {
// Set run level to startup database and associated services running on top of database
setRunLevel(DATABASE_SERVICES_STARTED_RUNLEVEL);
// Validate that databases and associated services directories uniformly exist and are uniformly populated during startup
validateDatabaseFolderStatus();
// If database is validated, startup remaining run levels
setRunLevel(ISAAC_STARTED_RUNLEVEL);
setRunLevel(ISAAC_DEPENDENTS_RUNLEVEL);
} catch (final Exception e) {
// Will inform calling routines that database is corrupt
throw e;
} finally {
// Regardless of successful or failed startup, reset database and lucene services' validityCalculated flag for next startup attempt
get().getAllServiceHandles(DatabaseServices.class).forEach(handle -> {
if (handle.isActive()) {
handle.getService()
.clearDatabaseValidityValue();
}
});
}
}
/**
* start all core isaac services in a background thread, returning immediately.
* @param callWhenStartComplete (optional) - if provided, a call back will be provided
* notifying of successfully start of ISAAC, or providing the Exception, if the startup sequence failed.
*/
public static void startupIsaac(BiConsumer<Boolean, Exception> callWhenStartComplete) {
LOG.info("Background starting ISAAC services");
final Thread backgroundLoad = new Thread(() -> {
try {
startupIsaac();
LOG.info("Background start complete - runlevel now " +
getService(RunLevelController.class).getCurrentRunLevel());
if (callWhenStartComplete != null) {
callWhenStartComplete.accept(isIsaacStarted(), null);
}
} catch (final Exception e) {
LOG.warn("Background start failed - runlevel now " +
getService(RunLevelController.class).getCurrentRunLevel(),
e);
if (callWhenStartComplete != null) {
callWhenStartComplete.accept(false, e);
}
}
},
"Datastore init thread");
backgroundLoad.start();
}
/**
* Start the Metadata services (without starting ISAAC core services), blocking until started (or failed).
*/
public static void startupMetadataStore() {
if (getService(RunLevelController.class).getCurrentRunLevel() < METADATA_STORE_STARTED_RUNLEVEL) {
setRunLevel(METADATA_STORE_STARTED_RUNLEVEL);
}
}
/**
* Start the WorkExecutor services (without starting ISAAC core services), blocking until started (or failed).
*/
public static void startupWorkExecutors() {
if (getService(RunLevelController.class).getCurrentRunLevel() < WORKERS_STARTED_RUNLEVEL) {
setRunLevel(WORKERS_STARTED_RUNLEVEL);
}
}
/**
* Validate database folder status.
*/
/*
* Check database directories. Either all must exist or none may exist. Inconsistent state suggests database
* corruption
*/
private static void validateDatabaseFolderStatus() {
discoveredValidityValue = null;
get().getAllServiceHandles(DatabaseServices.class).forEach(handle -> {
if (handle.isActive()) {
if (discoveredValidityValue == null) {
// Initial time through. All other database directories and lucene directories must have same state
discoveredValidityValue = handle.getService()
.getDatabaseValidityStatus();
LOG.info("First database service handler (" +
handle.getActiveDescriptor().getImplementation() +
") has database validity value: " + discoveredValidityValue);
} else {
// Verify database directories have same state as identified in first time through
LOG.info("Comparing database validity value for Provider " +
handle.getActiveDescriptor().getImplementation() +
" to see if consistent at startup. Status: " +
handle.getService().getDatabaseValidityStatus());
if (discoveredValidityValue != handle.getService().getDatabaseValidityStatus()) {
// Inconsistency discovered
throw new RuntimeException("Database Corruption Observed: Provider " +
handle.getActiveDescriptor().getImplementation() +
" has inconsistent database validity value prior to startup");
}
}
}
});
}
//~--- get methods ---------------------------------------------------------
/**
* Gets the current run level.
*
* @return the current run level
*/
public static int getCurrentRunLevel() {
return getService(RunLevelController.class).getCurrentRunLevel();
}
/**
* Gets the.
*
* @return the {@link ServiceLocator} that is managing this ISAAC instance
*/
public static ServiceLocator get() {
if (looker == null) {
synchronized (STARTUP_LOCK) {
if (looker == null) {
startupFxPlatform();
final ArrayList<String> packagesToSearch = new ArrayList<>(Arrays.asList("sh.isaac",
"org.ihtsdo",
"org.glassfish",
"com.informatics"));
final boolean readInhabitantFiles = Boolean.valueOf(System.getProperty(Constants.READ_INHABITANT_FILES,
"false"));
if (System.getProperty(Constants.EXTRA_PACKAGES_TO_SEARCH) != null) {
final String[] extraPackagesToSearch = System.getProperty(Constants.EXTRA_PACKAGES_TO_SEARCH)
.split(";");
packagesToSearch.addAll(Arrays.asList(extraPackagesToSearch));
}
try {
final String[] packages = packagesToSearch.toArray(new String[] {});
LOG.info("Looking for HK2 annotations " + (readInhabitantFiles ? "from inhabitant files"
: "skipping inhabitant files") + "; and scanning in the packages: " +
Arrays.toString(packages));
final ServiceLocator temp = HK2RuntimeInitializer.init("ISAAC", readInhabitantFiles, packages);
if (looker != null) {
final RuntimeException e =
new RuntimeException(
"RECURSIVE Lookup Service Reference! Ensure that there are no static variables " +
"objects utilizing the LookupService during their init!");
e.printStackTrace();
throw e;
}
looker = temp;
LOG.info("HK2 initialized. Identifed " + looker.getAllServiceHandles((criteria) -> {
return true;
}).size() + " services");
} catch (IOException | ClassNotFoundException | MultiException e) {
throw new RuntimeException(e);
}
try {
LookupService.startupWorkExecutors();
} catch (final Exception e) {
final RuntimeException ex =
new RuntimeException(
"Unexpected error trying to come up to the work executors level, possible classpath problems!",
e);
ex.printStackTrace(); // We are in a world of hurt if this happens, make sure this exception makes it out somewhere, and doesn't get eaten.
throw ex;
}
}
}
}
return looker;
}
/**
* Checks if initialized.
*
* @return true, if initialized
*/
public static boolean isInitialized() {
return looker != null;
}
/**
* Checks if isaac started.
*
* @return true, if isaac started
*/
public static boolean isIsaacStarted() {
return isInitialized() ? getService(RunLevelController.class).getCurrentRunLevel() >= ISAAC_STARTED_RUNLEVEL
: false;
}
/**
* Find a service by name, and automatically fall back to any service which implements the contract if the named service was not available.
*
* @param <T> the generic type
* @param contractOrService May not be null, and is the contract or concrete implementation to get the best instance of
* @param name May be null (to indicate any name is ok), and is the name of the implementation to be returned
* @return the named service if possible
*/
public static <T> T getNamedServiceIfPossible(Class<T> contractOrService, String name) {
T service = null;
if (StringUtils.isEmpty(name)) {
service = get().getService(contractOrService);
} else {
service = get().getService(contractOrService, name);
if (service == null) {
service = get().getService(contractOrService);
}
}
LOG.debug("LookupService returning {} for {} with name={}", ((service != null) ? service.getClass()
.getName()
: null), contractOrService.getName(), name);
return service;
}
//~--- set methods ---------------------------------------------------------
/**
* Sets the run level.
*
* @param runLevel the new run level
*/
public static void setRunLevel(int runLevel) {
final int current = getService(RunLevelController.class).getCurrentRunLevel();
if (current > runLevel) {
get().getAllServiceHandles(OchreCache.class).forEach(handle -> {
if (handle.isActive()) {
LOG.info("Clear cache for: " + handle.getActiveDescriptor().getImplementation());
handle.getService()
.reset();
}
});
get().getAllServices(OchreCache.class).forEach((cache) -> {
cache.reset();
});
}
getService(RunLevelController.class).proceedTo(runLevel);
}
//~--- get methods ---------------------------------------------------------
/**
* Return the highest ranked service that implements the requested contract or implementation class.
*
* @param <T> the generic type
* @param contractOrImpl the contract or impl
* @return the service
* @see ServiceLocator#getService(Class, Annotation...)
*/
public static <T> T getService(Class<T> contractOrImpl) {
final T service = get().getService(contractOrImpl, new Annotation[0]);
LOG.debug("LookupService returning {} for {}", ((service != null) ? service.getClass()
.getName()
: null), contractOrImpl.getName());
return service;
}
/**
* Find the best ranked service with the specified name. If no service with the specified name is available,
* this returns null (even if there is a service with another name [or no name] which would meet the contract)
*
* @param <T> the generic type
* @param contractOrService May not be null, and is the contract or concrete implementation to get the best instance of
* @param name May not be null or empty
* @return the service
* @see ServiceLocator#getService(Class, String, Annotation...)
*/
public static <T> T getService(Class<T> contractOrService, String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("You must specify a service name to use this method");
}
final T service = get().getService(contractOrService, name, new Annotation[0]);
LOG.debug("LookupService returning {} for {} with name={}", ((service != null) ? service.getClass()
.getName()
: null), contractOrService.getName(), name);
return service;
}
/**
* Return true if and only if any service implements the requested contract or implementation class.
*
* @param contractOrImpl the contract or impl
* @return true, if successful
* @see ServiceLocator#getService(Class, Annotation...)
*/
public static boolean hasService(Class<?> contractOrImpl) {
return get().getServiceHandle(contractOrImpl, new Annotation[0]) != null;
}
/**
* Return true if and only if there is a service with the specified name. If no service with the specified name is available,
* this returns false (even if there is a service with another name [or no name] which would meet the contract)
*
* @param contractOrService May not be null, and is the contract or concrete implementation to get the best instance of
* @param name May not be null or empty
* @return true, if successful
* @see ServiceLocator#getService(Class, String, Annotation...)
*/
public static boolean hasService(Class<?> contractOrService, String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("You must specify a service name to use this method");
}
return get().getServiceHandle(contractOrService, name, new Annotation[0]) != null;
}
}