diff --git a/src/main/java/org/cbioportal/persistence/mybatis/AlterationMyBatisRepository.java b/src/main/java/org/cbioportal/persistence/mybatis/AlterationMyBatisRepository.java index a48b6476b59..5c2b655a5f9 100644 --- a/src/main/java/org/cbioportal/persistence/mybatis/AlterationMyBatisRepository.java +++ b/src/main/java/org/cbioportal/persistence/mybatis/AlterationMyBatisRepository.java @@ -1,17 +1,8 @@ package org.cbioportal.persistence.mybatis; -import org.cbioportal.model.AlterationCountByGene; -import org.cbioportal.model.AlterationCountByStructuralVariant; -import org.cbioportal.model.AlterationFilter; -import org.cbioportal.model.CNA; -import org.cbioportal.model.CopyNumberCountByGene; -import org.cbioportal.model.MolecularProfile; -import org.cbioportal.model.MolecularProfileCaseIdentifier; -import org.cbioportal.model.MutationEventType; -import org.cbioportal.model.MolecularProfile.MolecularAlterationType; -import org.cbioportal.model.util.Select; +import org.cbioportal.model.*; import org.cbioportal.persistence.AlterationRepository; -import org.cbioportal.persistence.MolecularProfileRepository; +import org.cbioportal.persistence.util.MolecularProfileUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -23,40 +14,28 @@ public class AlterationMyBatisRepository implements AlterationRepository { @Autowired private AlterationCountsMapper alterationCountsMapper; + @Autowired - private MolecularProfileRepository molecularProfileRepository; + private MolecularProfileUtil molecularProfileUtil; // Dependency injection of the newly created MolecularProfileUtil for handling molecular profile-specific logic. @Override public List getSampleAlterationGeneCounts(Set molecularProfileCaseIdentifiers, Select entrezGeneIds, AlterationFilter alterationFilter) { - if ((alterationFilter.getMutationTypeSelect().hasNone() && alterationFilter.getCNAEventTypeSelect().hasNone() - && !alterationFilter.getStructuralVariants()) - || (molecularProfileCaseIdentifiers == null || molecularProfileCaseIdentifiers.isEmpty()) - || allAlterationsExcludedDriverAnnotation(alterationFilter) - || allAlterationsExcludedMutationStatus(alterationFilter) - || allAlterationsExcludedDriverTierAnnotation(alterationFilter) - ) { - // We want a mutable empty list: - return new ArrayList<>(); + if (filtrosInvalidos(molecularProfileCaseIdentifiers, alterationFilter)) { + return Collections.emptyList(); } - Set molecularProfileIds = molecularProfileCaseIdentifiers.stream() - .map(MolecularProfileCaseIdentifier::getMolecularProfileId) - .collect(Collectors.toSet()); - Map profileTypeByProfileId = molecularProfileRepository - .getMolecularProfiles(molecularProfileIds, "SUMMARY") - .stream() - .collect(Collectors.toMap(datum -> datum.getMolecularProfileId().toString(), MolecularProfile::getMolecularAlterationType)); - Map> groupedIdentifiersByProfileType = - alterationCountsMapper.getMolecularProfileCaseInternalIdentifier(new ArrayList<>(molecularProfileCaseIdentifiers), "SAMPLE_ID") - .stream() - .collect(Collectors.groupingBy(e -> profileTypeByProfileId.getOrDefault(e.getMolecularProfileId(), null))); + // Grouping molecular profile case identifiers by alteration type has been moved to MolecularProfileUtil. + Map> groupedIdentifiersByProfileType = + molecularProfileUtil.groupIdentifiersByProfileType(molecularProfileCaseIdentifiers, "SAMPLE_ID"); + return alterationCountsMapper.getSampleAlterationGeneCounts( - groupedIdentifiersByProfileType.get(MolecularAlterationType.MUTATION_EXTENDED), - groupedIdentifiersByProfileType.get(MolecularAlterationType.COPY_NUMBER_ALTERATION), - groupedIdentifiersByProfileType.get(MolecularAlterationType.STRUCTURAL_VARIANT), + // The grouped identifiers are now fetched directly from the utility class, which centralizes this logic. + groupedIdentifiersByProfileType.get(MolecularProfile.MolecularAlterationType.MUTATION_EXTENDED), + groupedIdentifiersByProfileType.get(MolecularProfile.MolecularAlterationType.COPY_NUMBER_ALTERATION), + groupedIdentifiersByProfileType.get(MolecularProfile.MolecularAlterationType.STRUCTURAL_VARIANT), entrezGeneIds, createMutationTypeList(alterationFilter), createCnaTypeList(alterationFilter), @@ -67,7 +46,8 @@ public List getSampleAlterationGeneCounts(Set getPatientAlterationGeneCounts(Set entrezGeneIds, AlterationFilter alterationFilter) { - if ((alterationFilter.getMutationTypeSelect().hasNone() && alterationFilter.getCNAEventTypeSelect().hasNone() - && !alterationFilter.getStructuralVariants()) - || (molecularProfileCaseIdentifiers == null || molecularProfileCaseIdentifiers.isEmpty()) - || allAlterationsExcludedDriverAnnotation(alterationFilter) - || allAlterationsExcludedMutationStatus(alterationFilter) - || allAlterationsExcludedDriverTierAnnotation(alterationFilter)) { + if (filtrosInvalidos(molecularProfileCaseIdentifiers, alterationFilter)) { return Collections.emptyList(); } - Set molecularProfileIds = molecularProfileCaseIdentifiers.stream() - .map(MolecularProfileCaseIdentifier::getMolecularProfileId) - .collect(Collectors.toSet()); - - Map profileTypeByProfileId = molecularProfileRepository - .getMolecularProfiles(molecularProfileIds, "SUMMARY") - .stream() - .collect(Collectors.toMap(datum -> datum.getMolecularProfileId().toString(), MolecularProfile::getMolecularAlterationType)); - - Map> groupedIdentifiersByProfileType = - alterationCountsMapper.getMolecularProfileCaseInternalIdentifier(new ArrayList<>(molecularProfileCaseIdentifiers), "PATIENT_ID") - .stream() - .collect(Collectors.groupingBy(e -> profileTypeByProfileId.getOrDefault(e.getMolecularProfileId(), null))); - + // Similar to the sample counts method, the grouping logic is now abstracted in MolecularProfileUtil + Map> groupedIdentifiersByProfileType = + molecularProfileUtil.groupIdentifiersByProfileType(molecularProfileCaseIdentifiers, "PATIENT_ID"); return alterationCountsMapper.getPatientAlterationGeneCounts( - groupedIdentifiersByProfileType.get(MolecularAlterationType.MUTATION_EXTENDED), - groupedIdentifiersByProfileType.get(MolecularAlterationType.COPY_NUMBER_ALTERATION), - groupedIdentifiersByProfileType.get(MolecularAlterationType.STRUCTURAL_VARIANT), + groupedIdentifiersByProfileType.get(MolecularProfile.MolecularAlterationType.MUTATION_EXTENDED), + groupedIdentifiersByProfileType.get(MolecularProfile.MolecularAlterationType.COPY_NUMBER_ALTERATION), + groupedIdentifiersByProfileType.get(MolecularProfile.MolecularAlterationType.STRUCTURAL_VARIANT), entrezGeneIds, createMutationTypeList(alterationFilter), createCnaTypeList(alterationFilter), @@ -117,115 +81,34 @@ public List getPatientAlterationGeneCounts(Set getSampleCnaGeneCounts(Set molecularProfileCaseIdentifiers, - Select entrezGeneIds, - AlterationFilter alterationFilter) { - - if (alterationFilter.getCNAEventTypeSelect().hasNone() || molecularProfileCaseIdentifiers == null - || allAlterationsExcludedDriverAnnotation(alterationFilter) - || allAlterationsExcludedDriverTierAnnotation(alterationFilter)) { - return Collections.emptyList(); - } - - List molecularProfileCaseInternalIdentifiers = - alterationCountsMapper.getMolecularProfileCaseInternalIdentifier(new ArrayList<>(molecularProfileCaseIdentifiers), "SAMPLE_ID"); - - return alterationCountsMapper.getSampleCnaGeneCounts( - molecularProfileCaseInternalIdentifiers, - entrezGeneIds, - createCnaTypeList(alterationFilter), - alterationFilter.getIncludeDriver(), - alterationFilter.getIncludeVUS(), - alterationFilter.getIncludeUnknownOncogenicity(), - alterationFilter.getSelectedTiers(), - alterationFilter.getIncludeUnknownTier()); - } - - @Override - public List getPatientCnaGeneCounts(Set molecularProfileCaseIdentifiers, - Select entrezGeneIds, - AlterationFilter alterationFilter) { - - if (alterationFilter.getCNAEventTypeSelect().hasNone() || molecularProfileCaseIdentifiers == null + private boolean filtrosInvalidos(Set molecularProfileCaseIdentifiers, AlterationFilter alterationFilter) { + return (alterationFilter.getMutationTypeSelect().hasNone() && alterationFilter.getCNAEventTypeSelect().hasNone() + && !alterationFilter.getStructuralVariants()) + || molecularProfileCaseIdentifiers == null || molecularProfileCaseIdentifiers.isEmpty() || allAlterationsExcludedDriverAnnotation(alterationFilter) - || allAlterationsExcludedDriverTierAnnotation(alterationFilter)) { - return Collections.emptyList(); - } - List molecularProfileCaseInternalIdentifiers = - alterationCountsMapper.getMolecularProfileCaseInternalIdentifier(new ArrayList<>(molecularProfileCaseIdentifiers), "PATIENT_ID"); - - return alterationCountsMapper.getPatientCnaGeneCounts( - molecularProfileCaseInternalIdentifiers, - entrezGeneIds, - createCnaTypeList(alterationFilter), - alterationFilter.getIncludeDriver(), - alterationFilter.getIncludeVUS(), - alterationFilter.getIncludeUnknownOncogenicity(), - alterationFilter.getSelectedTiers(), - alterationFilter.getIncludeUnknownTier()); - } - - @Override - public List getSampleStructuralVariantCounts(Set molecularProfileCaseIdentifiers, - AlterationFilter alterationFilter) { - - if (molecularProfileCaseIdentifiers == null - || molecularProfileCaseIdentifiers.isEmpty() - || allAlterationsExcludedMutationStatus(alterationFilter)) { - return Collections.emptyList(); - } - - return alterationCountsMapper.getSampleStructuralVariantCounts( - new ArrayList<>(molecularProfileCaseIdentifiers), - alterationFilter.getIncludeDriver(), - alterationFilter.getIncludeVUS(), - alterationFilter.getIncludeUnknownOncogenicity(), - alterationFilter.getSelectedTiers(), - alterationFilter.getIncludeUnknownTier(), - alterationFilter.getIncludeGermline(), - alterationFilter.getIncludeSomatic(), - alterationFilter.getIncludeUnknownStatus()); + || allAlterationsExcludedMutationStatus(alterationFilter) + || allAlterationsExcludedDriverTierAnnotation(alterationFilter); } - @Override - public List getPatientStructuralVariantCounts(Set molecularProfileCaseIdentifiers, - AlterationFilter alterationFilter) { - - if (molecularProfileCaseIdentifiers == null - || molecularProfileCaseIdentifiers.isEmpty() - || allAlterationsExcludedMutationStatus(alterationFilter)) { - return Collections.emptyList(); - } - - return alterationCountsMapper.getPatientStructuralVariantCounts( - new ArrayList<>(molecularProfileCaseIdentifiers), - alterationFilter.getIncludeDriver(), - alterationFilter.getIncludeVUS(), - alterationFilter.getIncludeUnknownOncogenicity(), - alterationFilter.getSelectedTiers(), - alterationFilter.getIncludeUnknownTier(), - alterationFilter.getIncludeGermline(), - alterationFilter.getIncludeSomatic(), - alterationFilter.getIncludeUnknownStatus()); - } - private Select createCnaTypeList(final AlterationFilter alterationFilter) { - if (alterationFilter.getCNAEventTypeSelect().hasNone()) + if (alterationFilter.getCNAEventTypeSelect().hasNone()) { return Select.none(); - if (alterationFilter.getCNAEventTypeSelect().hasAll()) + } + if (alterationFilter.getCNAEventTypeSelect().hasAll()) { return Select.all(); + } return alterationFilter.getCNAEventTypeSelect().map(CNA::getCode); } private Select createMutationTypeList(final AlterationFilter alterationFilter) { - if (alterationFilter.getMutationTypeSelect().hasNone()) + if (alterationFilter.getMutationTypeSelect().hasNone()) { return Select.none(); - if (alterationFilter.getMutationTypeSelect().hasAll()) + } + if (alterationFilter.getMutationTypeSelect().hasAll()) { return Select.all(); + } Select mappedMutationTypes = alterationFilter.getMutationTypeSelect().map(MutationEventType::getMutationType); mappedMutationTypes.inverse(alterationFilter.getMutationTypeSelect().inverse()); - return mappedMutationTypes; } @@ -239,8 +122,6 @@ private boolean allAlterationsExcludedDriverAnnotation(AlterationFilter alterati } private boolean allAlterationsExcludedDriverTierAnnotation(AlterationFilter alterationFilter) { - return alterationFilter.getSelectedTiers().hasNone() - && !alterationFilter.getIncludeUnknownTier(); + return alterationFilter.getSelectedTiers().hasNone() && !alterationFilter.getIncludeUnknownTier(); } - } diff --git a/src/main/java/org/cbioportal/persistence/mybatis/util/MolecularProfileUtil.java b/src/main/java/org/cbioportal/persistence/mybatis/util/MolecularProfileUtil.java new file mode 100644 index 00000000000..e781d2da56a --- /dev/null +++ b/src/main/java/org/cbioportal/persistence/mybatis/util/MolecularProfileUtil.java @@ -0,0 +1,42 @@ +package org.cbioportal.persistence.util; + +import org.cbioportal.model.MolecularProfile; +import org.cbioportal.model.MolecularProfileCaseIdentifier; +import org.cbioportal.persistence.MolecularProfileRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Component +public class MolecularProfileUtil { + + @Autowired + private MolecularProfileRepository molecularProfileRepository; + + public Map> groupIdentifiersByProfileType( + Set molecularProfileCaseIdentifiers, String caseIdentifierType) { + + if (molecularProfileCaseIdentifiers == null || molecularProfileCaseIdentifiers.isEmpty()) { + return Collections.emptyMap(); + } + + Set molecularProfileIds = molecularProfileCaseIdentifiers.stream() + .map(MolecularProfileCaseIdentifier::getMolecularProfileId) + .collect(Collectors.toSet()); + + Map profileTypeByProfileId = molecularProfileRepository + .getMolecularProfiles(molecularProfileIds, "SUMMARY") + .stream() + .collect(Collectors.toMap( + molecularProfile -> molecularProfile.getMolecularProfileId().toString(), + MolecularProfile::getMolecularAlterationType + )); + + return molecularProfileCaseIdentifiers.stream() + .collect(Collectors.groupingBy( + identifier -> profileTypeByProfileId.getOrDefault(identifier.getMolecularProfileId(), null) + )); + } +} diff --git a/src/main/java/org/cbioportal/persistence/util/CacheConfigValidator.java b/src/main/java/org/cbioportal/persistence/util/CacheConfigValidator.java new file mode 100644 index 00000000000..639eab6b46f --- /dev/null +++ b/src/main/java/org/cbioportal/persistence/util/CacheConfigValidator.java @@ -0,0 +1,79 @@ +package org.cbioportal.persistence.util; + +import java.io.File; +import java.net.URL; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Utility class to validate Ehcache configuration settings. +public class CacheConfigValidator { + + // Logger instance for logging errors and messages. + private static final Logger LOG = LoggerFactory.getLogger(CacheConfigValidator.class); + + /** + * Validates the provided Ehcache configuration settings. + * + * @param cacheType The type of cache (e.g., "ehcache-disk"). + * @param xmlConfigurationFile Path to the Ehcache XML configuration file. + * @param heapSize The size of the heap memory (in MB) to allocate. + * @param diskSize The size of the disk memory (in MB) to allocate. + * @param persistencePath The directory path for storing disk-based cache data. + * @throws IllegalArgumentException if validation fails. + */ + public static void validate(String cacheType, String xmlConfigurationFile, Integer heapSize, Integer diskSize, String persistencePath) { + // Use a StringBuilder to collect error messages for logging and exception handling. + StringBuilder errors = new StringBuilder("Errors detected during configuration of Ehcache:"); + boolean valid = true; + + // **Validation of XML Configuration File** + if (xmlConfigurationFile == null || xmlConfigurationFile.trim().isEmpty()) { + // Check if the XML configuration file path is provided. + errors.append("\n - The property 'ehcache.xml_configuration' is required but is unset."); + valid = false; + } else { + // Check if the XML configuration file exists and is accessible. + URL configFileURL = CacheConfigValidator.class.getResource(xmlConfigurationFile); + if (configFileURL == null) { + errors.append("\n - The property 'ehcache.xml_configuration' points to an unavailable resource."); + valid = false; + } + } + + // **Validation of Heap Size** + if (heapSize != null && heapSize <= 0) { + // Ensure the heap size is greater than zero. + errors.append("\n - Heap size must be greater than zero."); + valid = false; + } + + // **Validation of Disk Size** + if (diskSize != null && diskSize <= 0) { + // Ensure the disk size is greater than zero. + errors.append("\n - Disk size must be greater than zero."); + valid = false; + } + + // **Validation of Persistence Path** + if (cacheType.equalsIgnoreCase("ehcache-disk") && (persistencePath == null || persistencePath.trim().isEmpty())) { + // Disk-based caches require a persistence path. + errors.append("\n - The property 'ehcache.persistence_path' is required for disk-based caches but is unset."); + valid = false; + } else if (persistencePath != null) { + // If a persistence path is provided, validate it as a writable directory. + File persistenceDirectory = new File(persistencePath); + if (!persistenceDirectory.isDirectory() || !persistenceDirectory.canWrite()) { + errors.append("\n - The persistence path is not a valid directory or is not writable."); + valid = false; + } + } + + // **Error Handling** + if (!valid) { + // Log all errors and throw an exception if validation fails. + LOG.error(errors.toString()); + throw new IllegalArgumentException(errors.toString()); + } + } +} diff --git a/src/main/java/org/cbioportal/persistence/util/CacheConfigurationBuilderUtil.java b/src/main/java/org/cbioportal/persistence/util/CacheConfigurationBuilderUtil.java new file mode 100644 index 00000000000..9be855198ba --- /dev/null +++ b/src/main/java/org/cbioportal/persistence/util/CacheConfigurationBuilderUtil.java @@ -0,0 +1,62 @@ +package org.cbioportal.persistence.util; + +// Importing required Ehcache classes and configuration builders. +import org.ehcache.config.CacheConfiguration; +import org.ehcache.config.builders.CacheConfigurationBuilder; +import org.ehcache.config.builders.ResourcePoolsBuilder; +import org.ehcache.config.units.MemoryUnit; + +// Utility class to help create Ehcache configurations. +public class CacheConfigurationBuilderUtil { + + /** + * Creates a cache configuration using the specified resource pools. + * + * @param templateName The name of the template (not currently used in this method but could be useful for future extensions). + * @param resourcePoolsBuilder The builder that defines how resources (heap, disk) are allocated for the cache. + * @return A `CacheConfiguration` instance for the specified resource pools. + */ + public static CacheConfiguration createCacheConfiguration( + String templateName, + ResourcePoolsBuilder resourcePoolsBuilder + ) { + // Builds a cache configuration with Object types for keys and values, using the provided resource pools. + return CacheConfigurationBuilder.newCacheConfigurationBuilder( + Object.class, // The type of keys stored in the cache. + Object.class, // The type of values stored in the cache. + resourcePoolsBuilder // Resource allocation settings for the cache. + ).build(); + } + + /** + * Creates a resource pool builder based on heap and/or disk configuration. + * + * @param useHeap Whether to allocate memory on the heap for caching. + * @param useDisk Whether to allocate disk space for caching. + * @param heapSize The size of the heap memory (in MB) to allocate if `useHeap` is true. + * @param diskSize The size of the disk memory (in MB) to allocate if `useDisk` is true. + * @return A `ResourcePoolsBuilder` instance with the specified resource pool settings. + */ + public static ResourcePoolsBuilder createResourcePools( + boolean useHeap, // Indicates if heap memory should be used for caching. + boolean useDisk, // Indicates if disk space should be used for caching. + int heapSize, // Amount of heap memory (in MB) to allocate. + int diskSize // Amount of disk memory (in MB) to allocate. + ) { + // Initialize a new resource pool builder. + ResourcePoolsBuilder resourcePoolsBuilder = ResourcePoolsBuilder.newResourcePoolsBuilder(); + + // If heap caching is enabled, configure the builder with the specified heap size. + if (useHeap) { + resourcePoolsBuilder = resourcePoolsBuilder.heap(heapSize, MemoryUnit.MB); + } + + // If disk caching is enabled, configure the builder with the specified disk size. + if (useDisk) { + resourcePoolsBuilder = resourcePoolsBuilder.disk(diskSize, MemoryUnit.MB); + } + + // Return the fully configured resource pool builder. + return resourcePoolsBuilder; + } +} diff --git a/src/main/java/org/cbioportal/persistence/util/CacheManagerFactory.java b/src/main/java/org/cbioportal/persistence/util/CacheManagerFactory.java new file mode 100644 index 00000000000..787a573f1f2 --- /dev/null +++ b/src/main/java/org/cbioportal/persistence/util/CacheManagerFactory.java @@ -0,0 +1,55 @@ +package org.cbioportal.persistence.util; + +import java.io.File; +import java.util.Map; + +import org.ehcache.config.Configuration; +import org.ehcache.core.config.DefaultConfiguration; +import org.ehcache.impl.config.persistence.DefaultPersistenceConfiguration; +import org.ehcache.jsr107.EhcacheCachingProvider; + +// Utility class for creating Ehcache CacheManager instances. +public class CacheManagerFactory { + + /** + * Creates and configures a CacheManager instance based on the provided cache type and configuration. + * + * @param cacheType The type of cache to create (e.g., "ehcache-heap" or disk-based cache). + * @param caches A map of cache names and their corresponding configurations. + * @param persistencePath The directory path for storing disk-based cache data (used for persistent caches). + * @return A configured CacheManager instance for managing cache operations. + */ + public static javax.cache.CacheManager createCacheManager( + String cacheType, + Map> caches, + String persistencePath + ) { + // Declare a variable to hold the configuration object. + Configuration configuration; + + // **Heap-based Cache Configuration** + if (cacheType.equalsIgnoreCase("ehcache-heap")) { + // Use the DefaultConfiguration for heap-based caching, with the class loader and provided cache configurations. + configuration = new DefaultConfiguration(caches, CacheManagerFactory.class.getClassLoader()); + } else { + // **Disk-based Cache Configuration** + // Use DefaultConfiguration with a persistence configuration to store cache data on disk. + configuration = new DefaultConfiguration( + caches, // Map of cache configurations + CacheManagerFactory.class.getClassLoader(), // Class loader to load configurations + new DefaultPersistenceConfiguration(new File(persistencePath)) // Disk persistence settings + ); + } + + // **Obtain an Ehcache Caching Provider** + // Use the EhcacheCachingProvider, which implements the JSR-107 caching API. + EhcacheCachingProvider cachingProvider = (EhcacheCachingProvider) javax.cache.Caching.getCachingProvider(); + + // **Create and Return the CacheManager** + // The CacheManager is initialized with the default URI and the custom configuration. + return cachingProvider.getCacheManager( + cachingProvider.getDefaultURI(), // Default URI for Ehcache + configuration // Custom configuration (heap or disk) + ); + } +} diff --git a/src/main/java/org/cbioportal/persistence/util/CacheValidationUtils.java b/src/main/java/org/cbioportal/persistence/util/CacheValidationUtils.java new file mode 100644 index 00000000000..f12cba563fc --- /dev/null +++ b/src/main/java/org/cbioportal/persistence/util/CacheValidationUtils.java @@ -0,0 +1,109 @@ +package org.cbioportal.persistence.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// Utility class for validating cache configurations. +public class CacheValidationUtils { + + // Logger for error messages and debug information. + private static final Logger LOG = LoggerFactory.getLogger(CacheValidationUtils.class); + + /** + * Validates the cache type to ensure it is a recognized value. + * + * @param cacheType The type of cache (e.g., "no-cache", "ehcache-heap"). + * @param messages A buffer to collect validation error messages. + */ + public static void validateCacheType(String cacheType, StringBuffer messages) { + switch (cacheType.trim().toLowerCase()) { + // Recognized cache types + case "no-cache": + case "ehcache-heap": + case "ehcache-disk": + case "ehcache-hybrid": + case "redis": + break; // Valid case + default: + // Append an error message for unrecognized cache types. + messages.append("\n property persistence.cache_type has value (") + .append(cacheType) + .append(") which is not a recognized value"); + } + } + + /** + * Validates the XML configuration file for Ehcache. + * + * @param xmlConfigurationFile The path to the XML configuration file. + * @param messages A buffer to collect validation error messages. + */ + public static void validateXmlConfiguration(String xmlConfigurationFile, StringBuffer messages) { + if (xmlConfigurationFile == null || xmlConfigurationFile.trim().isEmpty()) { + // The XML configuration file path is required but unset. + messages.append("\n property ehcache.xml_configuration is required but is unset."); + } else { + // Attempt to load the XML configuration file as a resource. + URL configFileURL = CacheValidationUtils.class.getResource(xmlConfigurationFile); + if (configFileURL == null) { + // Resource could not be found by the class loader. + messages.append("\n property ehcache.xml_configuration has value (") + .append(xmlConfigurationFile) + .append(") but this resource is not available to the classloader."); + } else { + try (InputStream configFileInputStream = configFileURL.openStream()) { + // Check if the file is readable by attempting to read from it. + configFileInputStream.read(); + } catch (IOException e) { + // Unable to read the XML configuration file. + messages.append("\n property ehcache.xml_configuration has value (") + .append(xmlConfigurationFile) + .append(") but an attempt to read from this resource failed."); + } + } + } + } + + /** + * Validates the disk-based configuration for Ehcache. + * + * @param diskSize The size of the disk cache in MB. + * @param persistencePath The directory path for storing disk-based cache data. + * @param messages A buffer to collect validation error messages. + */ + public static void validateDiskConfiguration(Integer diskSize, String persistencePath, StringBuffer messages) { + if (diskSize == null || diskSize <= 0) { + // Disk size must be a positive integer. + messages.append("\n property disk size must be greater than zero but is not."); + } + if (persistencePath == null || persistencePath.trim().isEmpty()) { + // Persistence path is required for disk-based caches but is unset. + messages.append("\n property ehcache.persistence_path is required but is unset."); + } else { + // Validate that the persistence path is a writable directory. + File persistenceDirectory = new File(persistencePath); + if (!persistenceDirectory.isDirectory() || !persistenceDirectory.canWrite()) { + // The directory is either not valid or not writable. + messages.append("\n property ehcache.persistence_path is not a valid writable directory."); + } + } + } + + /** + * Validates the heap-based configuration for Ehcache. + * + * @param heapSize The size of the heap cache in MB. + * @param messages A buffer to collect validation error messages. + */ + public static void validateHeapConfiguration(Integer heapSize, StringBuffer messages) { + if (heapSize == null || heapSize <= 0) { + // Heap size must be a positive integer. + messages.append("\n property heap size must be greater than zero but is not."); + } + } +} diff --git a/src/main/java/org/cbioportal/persistence/util/CustomEhcachingProvider.java b/src/main/java/org/cbioportal/persistence/util/CustomEhcachingProvider.java index b4591cc3600..f86d6d2ccfd 100644 --- a/src/main/java/org/cbioportal/persistence/util/CustomEhcachingProvider.java +++ b/src/main/java/org/cbioportal/persistence/util/CustomEhcachingProvider.java @@ -30,269 +30,127 @@ * along with this program. If not, see . */ -package org.cbioportal.persistence.util; + package org.cbioportal.persistence.util; + +import java.util.Map; -import java.io.*; -import java.net.URL; -import java.util.*; import javax.cache.CacheManager; + +import org.cbioportal.persistence.CacheEnabledConfig; import org.ehcache.config.CacheConfiguration; -import org.ehcache.config.builders.CacheConfigurationBuilder; import org.ehcache.config.builders.ResourcePoolsBuilder; -import org.ehcache.config.Configuration; -import org.ehcache.config.units.MemoryUnit; -import org.ehcache.xml.XmlConfiguration; -import org.ehcache.core.config.DefaultConfiguration; import org.ehcache.jsr107.EhcacheCachingProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.ehcache.impl.config.persistence.DefaultPersistenceConfiguration; -import org.cbioportal.persistence.CacheEnabledConfig; +// Extends EhcacheCachingProvider to add custom cache configuration and validation logic. public class CustomEhcachingProvider extends EhcacheCachingProvider { + // Logger for reporting errors and debugging information. private static final Logger LOG = LoggerFactory.getLogger(CustomEhcachingProvider.class); - @Value("${ehcache.xml_configuration:/ehcache.xml}") + // Properties configured through application settings (e.g., application.properties or environment variables). + @Value("${ehcache.xml_configuration:/ehcache.xml}") // Path to the Ehcache XML configuration file. private String xmlConfigurationFile; - @Value("${persistence.cache_type:no-cache}") + @Value("${persistence.cache_type:no-cache}") // Cache type (e.g., "ehcache-disk", "no-cache"). private String cacheType; - @Value("${ehcache.general_repository_cache.max_mega_bytes_heap:1024}") + @Value("${ehcache.general_repository_cache.max_mega_bytes_heap:1024}") // Max heap size for the general cache in MB. private Integer generalRepositoryCacheMaxMegaBytes; - @Value("${ehcache.static_repository_cache_one.max_mega_bytes_heap:30}") + @Value("${ehcache.static_repository_cache_one.max_mega_bytes_heap:30}") // Max heap size for the static cache in MB. private Integer staticRepositoryCacheOneMaxMegaBytes; - @Value("${ehcache.persistence_path:/tmp/}") + @Value("${ehcache.persistence_path:/tmp/}") // Path for disk-based persistence. private String persistencePath; - @Value("${ehcache.general_repository_cache.max_mega_bytes_local_disk:4096}") + @Value("${ehcache.general_repository_cache.max_mega_bytes_local_disk:4096}") // Max disk size for the general cache in MB. private Integer generalRepositoryCacheMaxMegaBytesLocalDisk; - @Value("${ehcache.static_repository_cache_one.max_mega_bytes_local_disk:32}") + @Value("${ehcache.static_repository_cache_one.max_mega_bytes_local_disk:32}") // Max disk size for the static cache in MB. private Integer staticRepositoryCacheOneMaxMegaBytesLocalDisk; - + @Autowired - private CacheEnabledConfig cacheEnabledConfig; + private CacheEnabledConfig cacheEnabledConfig; // Configuration to enable/disable caching. + /** + * Overrides the default `getCacheManager` method to initialize and return a customized CacheManager. + * + * @return CacheManager A cache manager based on the specified cache type and configurations. + */ @Override public CacheManager getCacheManager() { - - CacheManager toReturn = null; try { - if (cacheEnabledConfig.enableCache(cacheType)) { - detectCacheConfigurationErrorsAndLog(); - LOG.info("Caching is enabled, using '" + xmlConfigurationFile + "' for configuration"); - XmlConfiguration xmlConfiguration = new XmlConfiguration(getClass().getResource(xmlConfigurationFile)); - - // initilize configurations specific to each individual cache (by template) - // to add new cache - create cache configuration with its own resource pool + template - ResourcePoolsBuilder generalRepositoryCacheResourcePoolsBuilder = ResourcePoolsBuilder.newResourcePoolsBuilder(); - ResourcePoolsBuilder staticRepositoryCacheOneResourcePoolsBuilder = ResourcePoolsBuilder.newResourcePoolsBuilder(); - - // Set up heap resources as long as not disk-only - if (!cacheType.equalsIgnoreCase(CacheEnabledConfig.EHCACHE_DISK)) { - generalRepositoryCacheResourcePoolsBuilder = generalRepositoryCacheResourcePoolsBuilder.heap(generalRepositoryCacheMaxMegaBytes, MemoryUnit.MB); - staticRepositoryCacheOneResourcePoolsBuilder = staticRepositoryCacheOneResourcePoolsBuilder.heap(staticRepositoryCacheOneMaxMegaBytes, MemoryUnit.MB); - } - // Set up disk resources as long as not heap-only - // will default to using /tmp -- let Ehcache throw exception if persistence path is invalid (locked or otherwise) - if (!cacheType.equalsIgnoreCase(CacheEnabledConfig.EHCACHE_HEAP)) { - generalRepositoryCacheResourcePoolsBuilder = generalRepositoryCacheResourcePoolsBuilder.disk(generalRepositoryCacheMaxMegaBytesLocalDisk, MemoryUnit.MB); - staticRepositoryCacheOneResourcePoolsBuilder = staticRepositoryCacheOneResourcePoolsBuilder.disk(staticRepositoryCacheOneMaxMegaBytesLocalDisk, MemoryUnit.MB); - } - - CacheConfiguration generalRepositoryCacheConfiguration = xmlConfiguration.newCacheConfigurationBuilderFromTemplate("RepositoryCacheTemplate", - Object.class, Object.class, generalRepositoryCacheResourcePoolsBuilder) - .withSizeOfMaxObjectGraph(Long.MAX_VALUE) - .withSizeOfMaxObjectSize(Long.MAX_VALUE, MemoryUnit.B) - .build(); - CacheConfiguration staticRepositoryCacheOneConfiguration = xmlConfiguration.newCacheConfigurationBuilderFromTemplate("RepositoryCacheTemplate", - Object.class, Object.class, staticRepositoryCacheOneResourcePoolsBuilder) - .withSizeOfMaxObjectGraph(Long.MAX_VALUE) - .withSizeOfMaxObjectSize(Long.MAX_VALUE, MemoryUnit.B) - .build(); - - // places caches in a map which will be used to create cache manager - Map> caches = new HashMap<>(); - caches.put("GeneralRepositoryCache", generalRepositoryCacheConfiguration); - caches.put("StaticRepositoryCacheOne", staticRepositoryCacheOneConfiguration); - - Configuration configuration = null; - if (cacheType.equalsIgnoreCase(CacheEnabledConfig.EHCACHE_HEAP)) { - configuration = new DefaultConfiguration(caches, this.getDefaultClassLoader()); - } else { // add persistence configuration if cacheType is either disk-only or hybrid - File persistenceFile = new File(persistencePath); - configuration = new DefaultConfiguration(caches, this.getDefaultClassLoader(), new DefaultPersistenceConfiguration(persistenceFile)); - } - - toReturn = this.getCacheManager(this.getDefaultURI(), configuration); - } else { - LOG.info("Caching is disabled"); - // we can not really disable caching, - // we can not make a cache of 0 objects, - // and we can not make a heap of memory size 0, so make a tiny heap - CacheConfiguration generalRepositoryCacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Object.class, - Object.class, - ResourcePoolsBuilder.newResourcePoolsBuilder().heap(1, MemoryUnit.B)).build(); - CacheConfiguration staticRepositoryCacheOneConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Object.class, - Object.class, - ResourcePoolsBuilder.newResourcePoolsBuilder().heap(1, MemoryUnit.B)).build(); - - Map> caches = new HashMap<>(); - caches.put("GeneralRepositoryCache", generalRepositoryCacheConfiguration); - caches.put("StaticRepositoryCacheOne", staticRepositoryCacheOneConfiguration); - - Configuration configuration = new DefaultConfiguration(caches, this.getDefaultClassLoader()); - - toReturn = this.getCacheManager(this.getDefaultURI(), configuration); - } + // Validate the cache configuration before initializing the CacheManager. + CacheConfigValidator.validate( + cacheType, + xmlConfigurationFile, + generalRepositoryCacheMaxMegaBytes, + generalRepositoryCacheMaxMegaBytesLocalDisk, + persistencePath + ); + + // Create resource pools for the general repository cache based on the cache type (heap or disk). + ResourcePoolsBuilder generalRepositoryPools = CacheConfigurationBuilderUtil.createResourcePools( + !cacheType.equalsIgnoreCase("ehcache-disk"), // Enable heap if cache type is not disk-based. + !cacheType.equalsIgnoreCase("ehcache-heap"), // Enable disk if cache type is not heap-based. + generalRepositoryCacheMaxMegaBytes, // Heap size in MB. + generalRepositoryCacheMaxMegaBytesLocalDisk // Disk size in MB. + ); + + // Build the general repository cache configuration using the resource pools. + CacheConfiguration generalRepoCache = CacheConfigurationBuilderUtil.createCacheConfiguration( + "RepositoryCacheTemplate", + generalRepositoryPools + ); + + // Define the caches to be managed, in this case, a single cache named "GeneralRepositoryCache". + Map> caches = Map.of("GeneralRepositoryCache", generalRepoCache); + + // Create and return the CacheManager using the specified type and configurations. + return CacheManagerFactory.createCacheManager(cacheType, caches, persistencePath); + + } catch (Exception e) { + // Log and rethrow any exceptions encountered during cache manager initialization. + LOG.error("Error initializing CacheManager: {}", e.getMessage()); + throw new RuntimeException(e); } - catch (Exception e) { - LOG.error(e.getClass().getName() + ": " + e.getMessage()); - StringWriter stackTrace = new StringWriter(); - e.printStackTrace(new PrintWriter(stackTrace)); - LOG.error(stackTrace.toString()); - } - return toReturn; } + /** + * Detects and logs errors in the cache configuration without throwing exceptions. + * Useful for pre-validation during application startup. + */ public void detectCacheConfigurationErrorsAndLog() { + // Prefix for error messages. String MESSAGE_PREFIX = "Errors detected during configuration of Ehcache:"; StringBuffer messages = new StringBuffer(MESSAGE_PREFIX); - boolean usesHeap = false; - boolean usesDisk = false; - switch (this.cacheType.trim().toLowerCase()) { - case "no-cache": - break; - case "ehcache-heap": - usesHeap = true; - break; - case "ehcache-disk": - usesDisk = true; - break; - case "ehcache-hybrid": - usesHeap = true; - usesDisk = true; - break; - case "redis": - break; // we should not be in here in this case - default: - messages.append("\n property persistence.cache_type has value (") - .append(cacheType) - .append(") which is not a recognized value"); - } - if (usesDisk || usesHeap) { - if (xmlConfigurationFile == null || xmlConfigurationFile.trim().length() == 0) { - messages.append("\n property ehcache.xml_configuration is required but is unset"); - } else { - URL configFileURL = getClass().getResource(xmlConfigurationFile); - if (configFileURL == null) { - messages.append("\n property ehcache.xml_configuration has value (") - .append(xmlConfigurationFile) - .append(") but this resource is not available to the classloader"); - } else { - boolean readable = false; - InputStream configFileInputStream = null; - try { - configFileInputStream = configFileURL.openStream(); - configFileInputStream.read(); - configFileInputStream.close(); - readable = true; - } catch (IOException e) { - } finally { - try { - configFileInputStream.close(); - } catch (IOException e) { - LOG.error("UNABLE TO CLOSE configFileURLInputStream"); - } - } - - if (!readable) { - messages.append("\n property ehcache.xml_configuration has value (") - .append(xmlConfigurationFile) - .append(") but an attempt to read from this resource failed"); - } - } - } - } - if (usesDisk) { - if (generalRepositoryCacheMaxMegaBytesLocalDisk == null) { - messages.append("\n property ehcache.general_repository_cache.max_mega_bytes_local_disk is required to be set, but has no value"); - } else { - if (generalRepositoryCacheMaxMegaBytesLocalDisk <= 0) { - messages.append("\n property ehcache.general_repository_cache.max_mega_bytes_local_disk must be greater than zero but is not"); - } - } - if (staticRepositoryCacheOneMaxMegaBytesLocalDisk == null) { - messages.append("\n property ehcache.static_repository_cache_one.max_mega_bytes_local_disk is required to be set, but has no value"); - } else { - if (staticRepositoryCacheOneMaxMegaBytesLocalDisk <= 0) { - messages.append("\n property ehcache.static_repository_cache_one.max_mega_bytes_local_disk must be greater than zero but is not"); - } - } - if (persistencePath == null || persistencePath.trim().length() == 0) { - messages.append("\n property ehcache.persistence_path is required when using a disk resource but is unset"); - } else { - File persistenceDirectory = new File(persistencePath); - boolean accessible = false; - try { - if (persistenceDirectory.isDirectory() && persistenceDirectory.canWrite()) { - accessible = true; - } - } catch (SecurityException e) { - } - if (!accessible) { - messages.append("\n property ehcache.persistence_path has value (") - .append(persistencePath) - .append(") but this path does not exist or is not an accessible directory"); - } - } - } - if (usesHeap) { - if (generalRepositoryCacheMaxMegaBytes == null) { - messages.append("\n property ehcache.general_repository_cache.max_mega_bytes_heap is required to be set, but has no value"); - } else { - if (generalRepositoryCacheMaxMegaBytes <= 0) { - messages.append("\n property ehcache.general_repository_cache.max_mega_bytes_heap must be greater than zero but is not"); - } - } - if (staticRepositoryCacheOneMaxMegaBytes == null) { - messages.append("\n property ehcache.static_repository_cache_one.max_mega_bytes_heap is required to be set, but has no value"); - } else { - if (staticRepositoryCacheOneMaxMegaBytes <= 0) { - messages.append("\n property ehcache.static_repository_cache_one.max_mega_bytes_heap must be greater than zero but is not"); - } - } - } - if (usesHeap && usesDisk) { - if (generalRepositoryCacheMaxMegaBytesLocalDisk != null - && generalRepositoryCacheMaxMegaBytes != null - && generalRepositoryCacheMaxMegaBytesLocalDisk <= generalRepositoryCacheMaxMegaBytes) { - messages.append("\n property ehcache.general_repository_cache.max_mega_bytes_heap must be set to a value less than the value of "); - messages.append("property ehcache.general_repository_cache.max_mega_bytes_local_disk, however "); - messages.append(generalRepositoryCacheMaxMegaBytes); - messages.append(" is not less than "); - messages.append(generalRepositoryCacheMaxMegaBytesLocalDisk); + + // Validate the cache type (e.g., "ehcache-heap", "ehcache-disk"). + CacheValidationUtils.validateCacheType(this.cacheType, messages); + + // Perform additional validations based on the cache type. + if (cacheType.equalsIgnoreCase("ehcache-heap") || cacheType.equalsIgnoreCase("ehcache-disk") || cacheType.equalsIgnoreCase("ehcache-hybrid")) { + // Validate the Ehcache XML configuration file. + CacheValidationUtils.validateXmlConfiguration(xmlConfigurationFile, messages); + + // Validate disk-based configuration if the cache type supports disk. + if (cacheType.equalsIgnoreCase("ehcache-disk") || cacheType.equalsIgnoreCase("ehcache-hybrid")) { + CacheValidationUtils.validateDiskConfiguration(generalRepositoryCacheMaxMegaBytesLocalDisk, persistencePath, messages); } - if (staticRepositoryCacheOneMaxMegaBytesLocalDisk != null - && staticRepositoryCacheOneMaxMegaBytes != null - && staticRepositoryCacheOneMaxMegaBytesLocalDisk <= staticRepositoryCacheOneMaxMegaBytes) { - messages.append("\n property ehcache.static_repository_cache_one.max_mega_bytes_heap must be set to a value less than the value of "); - messages.append("property ehcache.static_repository_cache_one.max_mega_bytes_local_disk, however "); - messages.append(staticRepositoryCacheOneMaxMegaBytes); - messages.append(" is not less than "); - messages.append(staticRepositoryCacheOneMaxMegaBytesLocalDisk); + + // Validate heap-based configuration if the cache type supports heap. + if (cacheType.equalsIgnoreCase("ehcache-heap") || cacheType.equalsIgnoreCase("ehcache-hybrid")) { + CacheValidationUtils.validateHeapConfiguration(generalRepositoryCacheMaxMegaBytes, messages); } } + + // Log the validation errors if any issues are detected. if (messages.length() > MESSAGE_PREFIX.length()) { LOG.error(messages.toString()); - LOG.error("because of Ehcache configuration errors, it is likely that an exception will be thrown during startup. Recent observed exceptions contain the string" - + " \"Provider org.redisson.jcache.JCachingProvider not a subtype\" even though the problem is in the Ehcache configuration settings."); } } } diff --git a/src/main/java/org/cbioportal/web/CustomDataController.java b/src/main/java/org/cbioportal/web/CustomDataController.java new file mode 100644 index 00000000000..966a08de6d6 --- /dev/null +++ b/src/main/java/org/cbioportal/web/CustomDataController.java @@ -0,0 +1,216 @@ +package org.cbioportal.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.BasicDBObject; +import org.cbioportal.service.util.SessionServiceRequestHandler; +import org.cbioportal.utils.removeme.Session; +import org.cbioportal.web.parameter.*; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.client.RestTemplate; +import org.cbioportal.service.util.CustomDataSession; +import org.cbioportal.service.util.CustomAttributeWithData; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.regex.Pattern; + +/** + * Controller responsible for handling custom data and custom gene list endpoints. + */ +@RestController +@RequestMapping("/api/session") +public class CustomDataController { + + private SessionServiceRequestHandler sessionServiceRequestHandler; + private ObjectMapper sessionServiceObjectMapper; + + @Value("${session.service.url:}") + private String sessionServiceURL; + + public CustomDataController(SessionServiceRequestHandler sessionServiceRequestHandler, + ObjectMapper sessionServiceObjectMapper) { + this.sessionServiceRequestHandler = sessionServiceRequestHandler; + this.sessionServiceObjectMapper = sessionServiceObjectMapper; + } + + // Helper methods for authentication + private boolean isAuthorized() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !(authentication == null || (authentication instanceof AnonymousAuthenticationToken)); + } + + private String userName() { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + + /** + * Adds a new custom gene list. + */ + @RequestMapping(value = "/custom_gene_list/save", method = RequestMethod.POST) + public ResponseEntity addUserSavedCustomGeneList(@RequestBody JSONObject body) throws IOException { + return addSession(Session.SessionType.custom_gene_list, Optional.of(SessionOperation.save), body); + } + + /** + * Internal method to add a session. + */ + private ResponseEntity addSession( + Session.SessionType type, + Optional operation, + JSONObject body + ) { + try { + HttpEntity httpEntity; + if (type.equals(Session.SessionType.custom_gene_list)) { + if (!(isAuthorized())) { + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } + CustomGeneListData customGeneListData = sessionServiceObjectMapper.readValue(body.toString(), CustomGeneListData.class); + customGeneListData.setUsers(Collections.singleton(userName())); + httpEntity = new HttpEntity<>(customGeneListData, sessionServiceRequestHandler.getHttpHeaders()); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity resp = restTemplate.exchange(sessionServiceURL + type, HttpMethod.POST, httpEntity, + Session.class); + + return new ResponseEntity<>(resp.getBody(), resp.getStatusCode()); + + } catch (IOException e) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + /** + * Fetches custom gene lists for the user. + */ + @RequestMapping(value = "/custom_gene_list", method = RequestMethod.GET) + public ResponseEntity> fetchCustomGeneList() throws IOException { + + if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { + + BasicDBObject basicDBObject = new BasicDBObject(); + basicDBObject.put("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE)); + + RestTemplate restTemplate = new RestTemplate(); + + HttpEntity httpEntity = new HttpEntity<>(basicDBObject.toString(), sessionServiceRequestHandler.getHttpHeaders()); + + ResponseEntity> responseEntity = restTemplate.exchange( + sessionServiceURL + Session.SessionType.custom_gene_list + "/query/fetch", + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference>() {}); + + return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); + + } + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } + + /** + * Fetches custom properties for the user. + */ + @RequestMapping(value = "/custom_data/fetch", method = RequestMethod.POST) + public ResponseEntity> fetchCustomProperties(@RequestBody List studyIds) throws IOException { + + if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { + + List basicDBObjects = new ArrayList<>(); + basicDBObjects.add(new BasicDBObject("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE))); + basicDBObjects.add(new BasicDBObject("data.origin", new BasicDBObject("$all", studyIds))); + basicDBObjects.add(new BasicDBObject("data.origin", new BasicDBObject("$size", studyIds.size()))); + + BasicDBObject queryDBObject = new BasicDBObject("$and", basicDBObjects); + + RestTemplate restTemplate = new RestTemplate(); + + HttpEntity httpEntity = new HttpEntity<>(queryDBObject.toString(), + sessionServiceRequestHandler.getHttpHeaders()); + + ResponseEntity> responseEntity = restTemplate.exchange( + sessionServiceURL + Session.SessionType.custom_data + "/query/fetch", + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference>() {}); + + return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); + + } + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } + + /** + * Updates users in custom data or custom gene list session. + */ + @RequestMapping(value = "/{type:custom_data|custom_gene_list}/{operation}/{id}", method = RequestMethod.GET) + public void updateUsersInSession(@PathVariable Session.SessionType type, @PathVariable String id, + @PathVariable Operation operation, HttpServletResponse response) throws IOException, Exception { + + if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { + HttpEntity httpEntity; + if (type.equals(Session.SessionType.custom_data)) { + String sessionDataStr = sessionServiceRequestHandler.getSessionDataJson(type, id); + CustomDataSession customDataSession = sessionServiceObjectMapper.readValue(sessionDataStr, CustomDataSession.class); + CustomAttributeWithData customAttributeWithData = customDataSession.getData(); + Set users = customAttributeWithData.getUsers(); + updateUserList(operation, users); + customAttributeWithData.setUsers(users); + httpEntity = new HttpEntity<>(customAttributeWithData, sessionServiceRequestHandler.getHttpHeaders()); + } else if (type.equals(Session.SessionType.custom_gene_list)) { + String customGeneListStr = sessionServiceRequestHandler.getSessionDataJson(type, id); + CustomGeneList customGeneList = sessionServiceObjectMapper.readValue(customGeneListStr, CustomGeneList.class); + CustomGeneListData customGeneListData = customGeneList.getData(); + Set users = customGeneListData.getUsers(); + updateUserList(operation, users); + customGeneListData.setUsers(users); + httpEntity = new HttpEntity<>(customGeneListData, sessionServiceRequestHandler.getHttpHeaders()); + } else { + response.sendError(HttpStatus.BAD_REQUEST.value()); + return; + } + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.put(sessionServiceURL + type + "/" + id, httpEntity); + + response.sendError(HttpStatus.OK.value()); + } else { + response.sendError(HttpStatus.SERVICE_UNAVAILABLE.value()); + } + + } + + /** + * Helper method to update user list based on the operation. + */ + private void updateUserList(Operation operation, Set users) { + switch (operation) { + case add: { + users.add(userName()); + break; + } + case delete: { + users.remove(userName()); + break; + } + } + } + + // Enums for operations + enum Operation { + add, delete; + } + + enum SessionOperation { + save, share; + } +} diff --git a/src/main/java/org/cbioportal/web/SessionServiceController.java b/src/main/java/org/cbioportal/web/SessionServiceController.java index d2ad7499c1f..d8cf1c3e91e 100644 --- a/src/main/java/org/cbioportal/web/SessionServiceController.java +++ b/src/main/java/org/cbioportal/web/SessionServiceController.java @@ -1,92 +1,46 @@ package org.cbioportal.web; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableMap; import com.mongodb.BasicDBObject; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; + import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.constraints.Size; -import org.cbioportal.service.util.CustomAttributeWithData; -import org.cbioportal.service.util.CustomDataSession; + import org.cbioportal.service.util.SessionServiceRequestHandler; import org.cbioportal.utils.removeme.Session; -import org.cbioportal.web.parameter.CustomGeneList; -import org.cbioportal.web.parameter.CustomGeneListData; -import org.cbioportal.web.parameter.PageSettings; -import org.cbioportal.web.parameter.PageSettingsData; -import org.cbioportal.web.parameter.PageSettingsIdentifier; -import org.cbioportal.web.parameter.PagingConstants; -import org.cbioportal.web.parameter.ResultsPageSettings; -import org.cbioportal.web.parameter.SampleIdentifier; -import org.cbioportal.web.parameter.SessionPage; -import org.cbioportal.web.parameter.StudyPageSettings; -import org.cbioportal.web.parameter.VirtualStudy; -import org.cbioportal.web.parameter.VirtualStudyData; -import org.cbioportal.web.parameter.VirtualStudySamples; +import org.cbioportal.web.parameter.*; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.client.RestTemplate; import org.cbioportal.web.util.StudyViewFilterApplier; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.bind.annotation.*; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.regex.Pattern; -import java.util.stream.Collectors; -import static org.cbioportal.web.PublicVirtualStudiesController.ALL_USERS; +import com.google.common.collect.ImmutableMap; -@Controller +/** + * Controller responsible for handling general session and settings-related endpoints. + */ +@RestController @RequestMapping("/api/session") public class SessionServiceController { - private static final Logger LOG = LoggerFactory.getLogger(SessionServiceController.class); - - private static final String QUERY_OPERATOR_ALL = "$all"; - private static final String QUERY_OPERATOR_SIZE = "$size"; - private static final String QUERY_OPERATOR_AND = "$and"; - - SessionServiceRequestHandler sessionServiceRequestHandler; - + private SessionServiceRequestHandler sessionServiceRequestHandler; private ObjectMapper sessionServiceObjectMapper; - private StudyViewFilterApplier studyViewFilterApplier; - public SessionServiceController(SessionServiceRequestHandler sessionServiceRequestHandler, - ObjectMapper sessionServiceObjectMapper, - StudyViewFilterApplier studyViewFilterApplier) { - this.sessionServiceRequestHandler = sessionServiceRequestHandler; - this.sessionServiceObjectMapper = sessionServiceObjectMapper; - this.studyViewFilterApplier = studyViewFilterApplier; - } - @Value("${session.service.url:}") private String sessionServiceURL; @@ -95,6 +49,15 @@ public SessionServiceController(SessionServiceRequestHandler sessionServiceReque SessionPage.results_view, ResultsPageSettings.class ); + public SessionServiceController(SessionServiceRequestHandler sessionServiceRequestHandler, + ObjectMapper sessionServiceObjectMapper, + StudyViewFilterApplier studyViewFilterApplier) { + this.sessionServiceRequestHandler = sessionServiceRequestHandler; + this.sessionServiceObjectMapper = sessionServiceObjectMapper; + this.studyViewFilterApplier = studyViewFilterApplier; + } + + // Helper methods for authentication private boolean isAuthorized() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return !(authentication == null || (authentication instanceof AnonymousAuthenticationToken)); @@ -118,7 +81,7 @@ private PageSettings getRecentlyUpdatePageSettings(String query) { RestTemplate restTemplate = new RestTemplate(); - HttpEntity httpEntity = new HttpEntity(query, sessionServiceRequestHandler.getHttpHeaders()); + HttpEntity httpEntity = new HttpEntity<>(query, sessionServiceRequestHandler.getHttpHeaders()); ResponseEntity> responseEntity = restTemplate.exchange( sessionServiceURL + Session.SessionType.settings + "/query/fetch", @@ -128,40 +91,33 @@ private PageSettings getRecentlyUpdatePageSettings(String query) { List sessions = responseEntity.getBody(); - // sort last updated in descending order + // Sort by last updated in descending order sessions.sort((PageSettings s1, PageSettings s2) -> s1.getData().getLastUpdated() > s2.getData() .getLastUpdated() ? -1 : 1); return sessions.isEmpty() ? null : sessions.get(0); } + /** + * Adds a new session of a specified type. + */ + @RequestMapping(value = "/{type}", method = RequestMethod.POST) + public ResponseEntity addSession(@PathVariable Session.SessionType type, @RequestBody JSONObject body) + throws IOException { + return addSession(type, Optional.empty(), body); + } + + /** + * Internal method to add a session. + */ private ResponseEntity addSession( - Session.SessionType type, + Session.SessionType type, Optional operation, JSONObject body ) { try { HttpEntity httpEntity; - if (type.equals(Session.SessionType.virtual_study) || type.equals(Session.SessionType.group)) { - // JSON from file to Object - VirtualStudyData virtualStudyData = sessionServiceObjectMapper.readValue(body.toString(), VirtualStudyData.class); - //TODO sanitize what's supplied. e.g. anonymous user should not specify the users field! - - if (isAuthorized()) { - String userName = userName(); - if (userName.equals(ALL_USERS)) { - throw new IllegalStateException("Illegal username " + ALL_USERS + " for assigning virtual studies."); - } - virtualStudyData.setOwner(userName); - if ((operation.isPresent() && operation.get().equals(SessionOperation.save)) - || type.equals(Session.SessionType.group)) { - virtualStudyData.setUsers(Collections.singleton(userName)); - } - } - - // use basic authentication for session service if set - httpEntity = new HttpEntity<>(virtualStudyData, sessionServiceRequestHandler.getHttpHeaders()); - } else if (type.equals(Session.SessionType.settings)) { + if (type.equals(Session.SessionType.settings)) { if (!(isAuthorized())) { return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); } @@ -174,32 +130,10 @@ private ResponseEntity addSession( ); pageSettings.setOwner(userName()); httpEntity = new HttpEntity<>(pageSettings, sessionServiceRequestHandler.getHttpHeaders()); - - } else if(type.equals(Session.SessionType.custom_data)) { - // JSON from file to Object - CustomAttributeWithData customData = sessionServiceObjectMapper.readValue(body.toString(), CustomAttributeWithData.class); - - if (isAuthorized()) { - customData.setOwner(userName()); - customData.setUsers(Collections.singleton(userName())); - } - - // use basic authentication for session service if set - httpEntity = new HttpEntity<>(customData, sessionServiceRequestHandler.getHttpHeaders()); - } else if (type.equals(Session.SessionType.custom_gene_list)) { - if (!(isAuthorized())) { - return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); - } - CustomGeneListData customGeneListData = sessionServiceObjectMapper.readValue(body.toString(), CustomGeneListData.class); - customGeneListData.setUsers(Collections.singleton(userName())); - httpEntity = new HttpEntity<>(customGeneListData, sessionServiceRequestHandler.getHttpHeaders()); } else { httpEntity = new HttpEntity<>(body, sessionServiceRequestHandler.getHttpHeaders()); } - // returns {"id":"5799648eef86c0e807a2e965"} - // using HashMap because converter is MappingJackson2HttpMessageConverter - // (Jackson 2 is on classpath) - // was String when default converter StringHttpMessageConverter was used + RestTemplate restTemplate = new RestTemplate(); ResponseEntity resp = restTemplate.exchange(sessionServiceURL + type, HttpMethod.POST, httpEntity, Session.class); @@ -207,224 +141,13 @@ private ResponseEntity addSession( return new ResponseEntity<>(resp.getBody(), resp.getStatusCode()); } catch (IOException e) { - LOG.error("Error occurred", e); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @RequestMapping(value = "/{type}/{id}", method = RequestMethod.GET) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(schema = @Schema(implementation = Session.class))) - public ResponseEntity getSession(@PathVariable Session.SessionType type, @PathVariable String id) { - - try { - String sessionDataJson = sessionServiceRequestHandler.getSessionDataJson(type, id); - Session session; - switch (type) { - case virtual_study: - VirtualStudy virtualStudy = sessionServiceObjectMapper.readValue(sessionDataJson, VirtualStudy.class); - VirtualStudyData virtualStudyData = virtualStudy.getData(); - if (Boolean.TRUE.equals(virtualStudyData.getDynamic())) { - populateVirtualStudySamples(virtualStudyData); - } - session = virtualStudy; - break; - case settings: - session = sessionServiceObjectMapper.readValue(sessionDataJson, PageSettings.class); - break; - case custom_data: - session = sessionServiceObjectMapper.readValue(sessionDataJson, CustomDataSession.class); - break; - case custom_gene_list: - session = sessionServiceObjectMapper.readValue(sessionDataJson, CustomGeneList.class); - break; - default: - session = sessionServiceObjectMapper.readValue(sessionDataJson, Session.class); - } - return new ResponseEntity<>(session, HttpStatus.OK); - } catch (Exception exception) { - LOG.error("Error occurred", exception); - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } - } - /** - * This method populates the `virtualStudyData` object with a new set of sample IDs retrieved as the result of executing a query based on virtual study view filters. - * It first applies the filters defined within the study view, runs the query to fetch the relevant sample IDs, and then updates the virtualStudyData to reflect these fresh results. - * This ensures that the virtual study contains the latest sample IDs. - * @param virtualStudyData + * Updates user page settings. */ - private void populateVirtualStudySamples(VirtualStudyData virtualStudyData) { - List sampleIdentifiers = studyViewFilterApplier.apply(virtualStudyData.getStudyViewFilter()); - Set virtualStudySamples = extractVirtualStudySamples(sampleIdentifiers); - virtualStudyData.setStudies(virtualStudySamples); - } - - /** - * Transforms list of sample identifiers to set of virtual study samples - * @param sampleIdentifiers - */ - private Set extractVirtualStudySamples(List sampleIdentifiers) { - Map> sampleIdsByStudyId = groupSampleIdsByStudyId(sampleIdentifiers); - return sampleIdsByStudyId.entrySet().stream().map(entry -> { - VirtualStudySamples vss = new VirtualStudySamples(); - vss.setId(entry.getKey()); - vss.setSamples(entry.getValue()); - return vss; - }).collect(Collectors.toSet()); - } - - /** - * Groups sample IDs by their study ID - * @param sampleIdentifiers - */ - private Map> groupSampleIdsByStudyId(List sampleIdentifiers) { - return sampleIdentifiers - .stream() - .collect( - Collectors.groupingBy( - SampleIdentifier::getStudyId, - Collectors.mapping(SampleIdentifier::getSampleId, Collectors.toSet()))); - } - - @RequestMapping(value = "/virtual_study", method = RequestMethod.GET) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = VirtualStudy.class)))) - public ResponseEntity> getUserStudies() throws JsonProcessingException { - - if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { - try { - - BasicDBObject basicDBObject = new BasicDBObject(); - basicDBObject.put("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE)); - - RestTemplate restTemplate = new RestTemplate(); - - HttpEntity httpEntity = new HttpEntity<>(basicDBObject.toString(), sessionServiceRequestHandler.getHttpHeaders()); - - ResponseEntity> responseEntity = restTemplate.exchange( - sessionServiceURL + Session.SessionType.virtual_study + "/query/fetch", - HttpMethod.POST, - httpEntity, - new ParameterizedTypeReference>() {}); - - List virtualStudyList = responseEntity.getBody(); - return new ResponseEntity<>(virtualStudyList, HttpStatus.OK); - } catch (Exception exception) { - LOG.error("Error occurred", exception); - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } - } - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } - - @RequestMapping(value = "/{type}", method = RequestMethod.POST) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(schema = @Schema(implementation = Session.class))) - public ResponseEntity addSession(@PathVariable Session.SessionType type, @RequestBody JSONObject body) - throws IOException { - //FIXME? anonymous user can create sessions. Do we really want that? https://github.com/cBioPortal/cbioportal/issues/10843 - return addSession(type, Optional.empty(), body); - } - - @RequestMapping(value = "/virtual_study/save", method = RequestMethod.POST) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(schema = @Schema(implementation = Session.class))) - public ResponseEntity addUserSavedVirtualStudy(@RequestBody JSONObject body) throws IOException { - //FIXME? anonymous user can create virtual studies. Do we really want that? https://github.com/cBioPortal/cbioportal/issues/10843 - return addSession(Session.SessionType.virtual_study, Optional.of(SessionOperation.save), body); - } - - @RequestMapping(value = "/{type:virtual_study|group|custom_data|custom_gene_list}/{operation}/{id}", method = RequestMethod.GET) - public void updateUsersInSession(@PathVariable Session.SessionType type, @PathVariable String id, - @PathVariable Operation operation, HttpServletResponse response) throws IOException { - - if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { - HttpEntity httpEntity; - if (type.equals(Session.SessionType.custom_data)) { - String virtualStudyStr = sessionServiceObjectMapper.writeValueAsString(getSession(type, id).getBody()); - CustomDataSession customDataSession = sessionServiceObjectMapper.readValue(virtualStudyStr, CustomDataSession.class); - CustomAttributeWithData customAttributeWithData = customDataSession.getData(); - Set users = customAttributeWithData.getUsers(); - updateUserList(operation, users); - customAttributeWithData.setUsers(users); - httpEntity = new HttpEntity<>(customAttributeWithData, sessionServiceRequestHandler.getHttpHeaders()); - } else if (type.equals(Session.SessionType.custom_gene_list)) { - String customGeneListStr = sessionServiceObjectMapper.writeValueAsString(getSession(type, id).getBody()); - CustomGeneList customGeneList = sessionServiceObjectMapper.readValue(customGeneListStr, CustomGeneList.class); - CustomGeneListData customGeneListData = customGeneList.getData(); - Set users = customGeneListData.getUsers(); - updateUserList(operation, users); - customGeneListData.setUsers(users); - httpEntity = new HttpEntity<>(customGeneListData, sessionServiceRequestHandler.getHttpHeaders()); - } else { - String virtualStudyStr = sessionServiceObjectMapper.writeValueAsString(getSession(type, id).getBody()); - VirtualStudy virtualStudy = sessionServiceObjectMapper.readValue(virtualStudyStr, VirtualStudy.class); - VirtualStudyData virtualStudyData = virtualStudy.getData(); - Set users = virtualStudyData.getUsers(); - updateUserList(operation, users); - virtualStudyData.setUsers(users); - httpEntity = new HttpEntity<>(virtualStudyData, sessionServiceRequestHandler.getHttpHeaders()); - } - - RestTemplate restTemplate = new RestTemplate(); - restTemplate.put(sessionServiceURL + type + "/" + id, httpEntity); - - response.sendError(HttpStatus.OK.value()); - } else { - response.sendError(HttpStatus.SERVICE_UNAVAILABLE.value()); - } - - } - - private void updateUserList(Operation operation, Set users) { - switch (operation) { - case add: { - users.add(userName()); - break; - } - case delete: { - users.remove(userName()); - break; - } - } - } - - @RequestMapping(value = "/groups/fetch", method = RequestMethod.POST) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = VirtualStudy.class)))) - public ResponseEntity> fetchUserGroups( - @Size(min = 1, max = PagingConstants.MAX_PAGE_SIZE) @RequestBody List studyIds) throws IOException { - - if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { - // ignore origin studies order - // add $size to make sure origin studies is not a subset - List basicDBObjects = new ArrayList<>(); - basicDBObjects - .add(new BasicDBObject("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE))); - basicDBObjects.add(new BasicDBObject("data.origin", - new BasicDBObject(QUERY_OPERATOR_ALL, studyIds))); - basicDBObjects.add(new BasicDBObject("data.origin", - new BasicDBObject(QUERY_OPERATOR_SIZE, studyIds.size()))); - - BasicDBObject queryDBObject = new BasicDBObject(QUERY_OPERATOR_AND, basicDBObjects); - - RestTemplate restTemplate = new RestTemplate(); - - HttpEntity httpEntity = new HttpEntity<>(queryDBObject.toString(), sessionServiceRequestHandler.getHttpHeaders()); - - ResponseEntity> responseEntity = restTemplate.exchange( - sessionServiceURL + Session.SessionType.group + "/query/fetch", - HttpMethod.POST, - httpEntity, - new ParameterizedTypeReference>() {}); - - return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); - - } - return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); - } - @RequestMapping(value = "/settings", method = RequestMethod.POST) public void updateUserPageSettings(@RequestBody PageSettingsData settingsData, HttpServletResponse response) { @@ -438,12 +161,12 @@ public void updateUserPageSettings(@RequestBody PageSettingsData settingsData, H basicDBObjects .add(new BasicDBObject("data.owner", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE))); basicDBObjects.add(new BasicDBObject("data.origin", - new BasicDBObject(QUERY_OPERATOR_ALL, settingsData.getOrigin()))); + new BasicDBObject("$all", settingsData.getOrigin()))); basicDBObjects.add(new BasicDBObject("data.origin", - new BasicDBObject(QUERY_OPERATOR_SIZE, settingsData.getOrigin().size()))); + new BasicDBObject("$size", settingsData.getOrigin().size()))); basicDBObjects.add(new BasicDBObject("data.page", settingsData.getPage().name())); - BasicDBObject queryDBObject = new BasicDBObject(QUERY_OPERATOR_AND, basicDBObjects); + BasicDBObject queryDBObject = new BasicDBObject("$and", basicDBObjects); PageSettings pageSettings = getRecentlyUpdatePageSettings(queryDBObject.toString()); @@ -460,14 +183,15 @@ public void updateUserPageSettings(@RequestBody PageSettingsData settingsData, H response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value()); } } catch (IOException | ParseException e) { - LOG.error("Error occurred", e); response.setStatus(HttpStatus.BAD_REQUEST.value()); } } - // updates only allowed for type page settings session + /** + * Updates an existing page setting session. + */ private void updatedPageSettingSession( - PageSettings pageSettings, + PageSettings pageSettings, @RequestBody PageSettingsData body, HttpServletResponse response ) throws IOException { @@ -475,7 +199,7 @@ private void updatedPageSettingSession( if (isAuthorized()) { PageSettingsData pageSettingsData = pageSettings.getData(); - // only allow owner to update his session and see if the origin(studies) are same + // Only allow owner to update their session and check if the origin (studies) are the same if (userName().equals(pageSettingsData.getOwner()) && sameOrigin(pageSettingsData.getOrigin(), body.getOrigin())) { @@ -485,7 +209,7 @@ private void updatedPageSettingSession( RestTemplate restTemplate = new RestTemplate(); HttpEntity httpEntity = new HttpEntity<>(body, sessionServiceRequestHandler.getHttpHeaders()); - + Session.SessionType type = pageSettings.getType() == null ? Session.SessionType.settings : pageSettings.getType(); restTemplate.put(sessionServiceURL + type + "/" + pageSettings.getId(), httpEntity); response.setStatus(HttpStatus.OK.value()); @@ -497,9 +221,10 @@ private void updatedPageSettingSession( } } + /** + * Retrieves page settings for the user. + */ @RequestMapping(value = "/settings/fetch", method = RequestMethod.POST) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(schema = @Schema(implementation = PageSettingsData.class))) public ResponseEntity getPageSettings(@RequestBody PageSettingsIdentifier pageSettingsIdentifier) { try { @@ -509,12 +234,12 @@ public ResponseEntity getPageSettings(@RequestBody PageSetting basicDBObjects .add(new BasicDBObject("data.owner", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE))); basicDBObjects.add(new BasicDBObject("data.origin", - new BasicDBObject(QUERY_OPERATOR_ALL, pageSettingsIdentifier.getOrigin()))); + new BasicDBObject("$all", pageSettingsIdentifier.getOrigin()))); basicDBObjects.add(new BasicDBObject("data.origin", - new BasicDBObject(QUERY_OPERATOR_SIZE, pageSettingsIdentifier.getOrigin().size()))); + new BasicDBObject("$size", pageSettingsIdentifier.getOrigin().size()))); basicDBObjects.add(new BasicDBObject("data.page", pageSettingsIdentifier.getPage().name())); - BasicDBObject queryDBObject = new BasicDBObject(QUERY_OPERATOR_AND, basicDBObjects); + BasicDBObject queryDBObject = new BasicDBObject("$and", basicDBObjects); PageSettings pageSettings = getRecentlyUpdatePageSettings(queryDBObject.toString()); @@ -522,82 +247,12 @@ public ResponseEntity getPageSettings(@RequestBody PageSetting } return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); } catch (Exception e) { - LOG.error("Error occurred", e); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } - @RequestMapping(value = "/custom_data/fetch", method = RequestMethod.POST) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = CustomDataSession.class)))) - public ResponseEntity> fetchCustomProperties( - @Size(min = 1, max = PagingConstants.MAX_PAGE_SIZE) @RequestBody List studyIds) throws IOException { - - if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { - - List basicDBObjects = new ArrayList<>(); - basicDBObjects.add(new BasicDBObject("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE))); - basicDBObjects.add(new BasicDBObject("data.origin", new BasicDBObject(QUERY_OPERATOR_ALL, studyIds))); - basicDBObjects.add(new BasicDBObject("data.origin", new BasicDBObject(QUERY_OPERATOR_SIZE, studyIds.size()))); - - BasicDBObject queryDBObject = new BasicDBObject(QUERY_OPERATOR_AND, basicDBObjects); - - RestTemplate restTemplate = new RestTemplate(); - - HttpEntity httpEntity = new HttpEntity<>(queryDBObject.toString(), - sessionServiceRequestHandler.getHttpHeaders()); - - ResponseEntity> responseEntity = restTemplate.exchange( - sessionServiceURL + Session.SessionType.custom_data + "/query/fetch", - HttpMethod.POST, - httpEntity, - new ParameterizedTypeReference>() {}); - - return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); - - } - return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); - } - - @RequestMapping(value = "/custom_gene_list/save", method = RequestMethod.POST) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(schema = @Schema(implementation = Session.class))) - public ResponseEntity addUserSavedCustomGeneList(@RequestBody JSONObject body) throws IOException { - return addSession(Session.SessionType.custom_gene_list, Optional.of(SessionOperation.save), body); + // Enum for session operations + enum SessionOperation { + save, share; } - - @RequestMapping(value = "/custom_gene_list", method = RequestMethod.GET) - @ApiResponse(responseCode = "200", description = "OK", - content = @Content(array = @ArraySchema(schema = @Schema(implementation = CustomGeneList.class)))) - public ResponseEntity> fetchCustomGeneList() throws IOException { - - if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { - - BasicDBObject basicDBObject = new BasicDBObject(); - basicDBObject.put("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE)); - - RestTemplate restTemplate = new RestTemplate(); - - HttpEntity httpEntity = new HttpEntity<>(basicDBObject.toString(), sessionServiceRequestHandler.getHttpHeaders()); - - ResponseEntity> responseEntity = restTemplate.exchange( - sessionServiceURL + Session.SessionType.custom_gene_list + "/query/fetch", - HttpMethod.POST, - httpEntity, - new ParameterizedTypeReference>() {}); - - return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); - - } - return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); - } - -} - -enum Operation { - add, delete; -} - -enum SessionOperation { - save, share; } diff --git a/src/main/java/org/cbioportal/web/VirtualStudyController.java b/src/main/java/org/cbioportal/web/VirtualStudyController.java new file mode 100644 index 00000000000..bcbd8e9af5c --- /dev/null +++ b/src/main/java/org/cbioportal/web/VirtualStudyController.java @@ -0,0 +1,274 @@ +package org.cbioportal.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.BasicDBObject; +import org.cbioportal.service.util.SessionServiceRequestHandler; +import org.cbioportal.utils.removeme.Session; +import org.cbioportal.web.parameter.*; +import org.cbioportal.web.util.StudyViewFilterApplier; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.client.RestTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.cbioportal.web.PublicVirtualStudiesController.ALL_USERS; + +/** + * Controller responsible for handling virtual study-related endpoints. + */ +@RestController +@RequestMapping("/api/session") +public class VirtualStudyController { + + private SessionServiceRequestHandler sessionServiceRequestHandler; + private ObjectMapper sessionServiceObjectMapper; + private StudyViewFilterApplier studyViewFilterApplier; + + @Value("${session.service.url:}") + private String sessionServiceURL; + + public VirtualStudyController(SessionServiceRequestHandler sessionServiceRequestHandler, + ObjectMapper sessionServiceObjectMapper, + StudyViewFilterApplier studyViewFilterApplier) { + this.sessionServiceRequestHandler = sessionServiceRequestHandler; + this.sessionServiceObjectMapper = sessionServiceObjectMapper; + this.studyViewFilterApplier = studyViewFilterApplier; + } + + // Helper methods for authentication + private boolean isAuthorized() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !(authentication == null || (authentication instanceof AnonymousAuthenticationToken)); + } + + private String userName() { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + + /** + * Adds a new user-saved virtual study. + */ + @RequestMapping(value = "/virtual_study/save", method = RequestMethod.POST) + public ResponseEntity addUserSavedVirtualStudy(@RequestBody JSONObject body) throws IOException { + return addSession(Session.SessionType.virtual_study, Optional.of(SessionOperation.save), body); + } + + /** + * Internal method to add a session. + */ + private ResponseEntity addSession( + Session.SessionType type, + Optional operation, + JSONObject body + ) { + try { + HttpEntity httpEntity; + if (type.equals(Session.SessionType.virtual_study)) { + // Deserialize JSON to VirtualStudyData + VirtualStudyData virtualStudyData = sessionServiceObjectMapper.readValue(body.toString(), VirtualStudyData.class); + + if (isAuthorized()) { + String userName = userName(); + if (userName.equals(ALL_USERS)) { + throw new IllegalStateException("Illegal username " + ALL_USERS + " for assigning virtual studies."); + } + virtualStudyData.setOwner(userName); + if (operation.isPresent() && operation.get().equals(SessionOperation.save)) { + virtualStudyData.setUsers(Collections.singleton(userName)); + } + } + + httpEntity = new HttpEntity<>(virtualStudyData, sessionServiceRequestHandler.getHttpHeaders()); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity resp = restTemplate.exchange(sessionServiceURL + type, HttpMethod.POST, httpEntity, + Session.class); + + return new ResponseEntity<>(resp.getBody(), resp.getStatusCode()); + + } catch (IOException e) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + /** + * Retrieves all virtual studies for the logged-in user. + */ + @RequestMapping(value = "/virtual_study", method = RequestMethod.GET) + public ResponseEntity> getUserStudies() { + if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { + try { + BasicDBObject basicDBObject = new BasicDBObject(); + basicDBObject.put("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE)); + + RestTemplate restTemplate = new RestTemplate(); + + HttpEntity httpEntity = new HttpEntity<>(basicDBObject.toString(), sessionServiceRequestHandler.getHttpHeaders()); + + ResponseEntity> responseEntity = restTemplate.exchange( + sessionServiceURL + Session.SessionType.virtual_study + "/query/fetch", + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference>() {}); + + List virtualStudyList = responseEntity.getBody(); + return new ResponseEntity<>(virtualStudyList, HttpStatus.OK); + } catch (Exception exception) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Retrieves a specific virtual study by ID. + */ + @RequestMapping(value = "/virtual_study/{id}", method = RequestMethod.GET) + public ResponseEntity getSession(@PathVariable String id) { + try { + String sessionDataJson = sessionServiceRequestHandler.getSessionDataJson(Session.SessionType.virtual_study, id); + VirtualStudy virtualStudy = sessionServiceObjectMapper.readValue(sessionDataJson, VirtualStudy.class); + VirtualStudyData virtualStudyData = virtualStudy.getData(); + if (Boolean.TRUE.equals(virtualStudyData.getDynamic())) { + populateVirtualStudySamples(virtualStudyData); + } + return new ResponseEntity<>(virtualStudy, HttpStatus.OK); + } catch (Exception exception) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Updates users in a virtual study session. + */ + @RequestMapping(value = "/virtual_study/{operation}/{id}", method = RequestMethod.GET) + public void updateUsersInSession(@PathVariable String id, + @PathVariable Operation operation, HttpServletResponse response) throws IOException { + + if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { + HttpEntity httpEntity; + String virtualStudyStr = sessionServiceObjectMapper.writeValueAsString(getSession(id).getBody()); + VirtualStudy virtualStudy = sessionServiceObjectMapper.readValue(virtualStudyStr, VirtualStudy.class); + VirtualStudyData virtualStudyData = virtualStudy.getData(); + Set users = virtualStudyData.getUsers(); + updateUserList(operation, users); + virtualStudyData.setUsers(users); + httpEntity = new HttpEntity<>(virtualStudyData, sessionServiceRequestHandler.getHttpHeaders()); + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.put(sessionServiceURL + Session.SessionType.virtual_study + "/" + id, httpEntity); + + response.sendError(HttpStatus.OK.value()); + } else { + response.sendError(HttpStatus.SERVICE_UNAVAILABLE.value()); + } + } + + /** + * Helper method to update user list based on the operation. + */ + private void updateUserList(Operation operation, Set users) { + switch (operation) { + case add: { + users.add(userName()); + break; + } + case delete: { + users.remove(userName()); + break; + } + } + } + + /** + * Fetches user groups (virtual studies). + */ + @RequestMapping(value = "/groups/fetch", method = RequestMethod.POST) + public ResponseEntity> fetchUserGroups(@RequestBody List studyIds) throws IOException { + + if (sessionServiceRequestHandler.isSessionServiceEnabled() && isAuthorized()) { + List basicDBObjects = new ArrayList<>(); + basicDBObjects + .add(new BasicDBObject("data.users", Pattern.compile(userName(), Pattern.CASE_INSENSITIVE))); + basicDBObjects.add(new BasicDBObject("data.origin", + new BasicDBObject("$all", studyIds))); + basicDBObjects.add(new BasicDBObject("data.origin", + new BasicDBObject("$size", studyIds.size()))); + + BasicDBObject queryDBObject = new BasicDBObject("$and", basicDBObjects); + + RestTemplate restTemplate = new RestTemplate(); + + HttpEntity httpEntity = new HttpEntity<>(queryDBObject.toString(), sessionServiceRequestHandler.getHttpHeaders()); + + ResponseEntity> responseEntity = restTemplate.exchange( + sessionServiceURL + Session.SessionType.group + "/query/fetch", + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference>() {}); + + return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK); + + } + return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); + } + + /** + * Populates virtual study samples if the study is dynamic. + */ + private void populateVirtualStudySamples(VirtualStudyData virtualStudyData) { + List sampleIdentifiers = studyViewFilterApplier.apply(virtualStudyData.getStudyViewFilter()); + Set virtualStudySamples = extractVirtualStudySamples(sampleIdentifiers); + virtualStudyData.setStudies(virtualStudySamples); + } + + /** + * Transforms a list of sample identifiers into a set of virtual study samples. + */ + private Set extractVirtualStudySamples(List sampleIdentifiers) { + Map> sampleIdsByStudyId = groupSampleIdsByStudyId(sampleIdentifiers); + return sampleIdsByStudyId.entrySet().stream().map(entry -> { + VirtualStudySamples vss = new VirtualStudySamples(); + vss.setId(entry.getKey()); + vss.setSamples(entry.getValue()); + return vss; + }).collect(Collectors.toSet()); + } + + /** + * Groups sample IDs by their study ID. + */ + private Map> groupSampleIdsByStudyId(List sampleIdentifiers) { + return sampleIdentifiers + .stream() + .collect( + Collectors.groupingBy( + SampleIdentifier::getStudyId, + Collectors.mapping(SampleIdentifier::getSampleId, Collectors.toSet()))); + } + + // Enums for operations + enum Operation { + add, delete; + } + + enum SessionOperation { + save, share; + } +} diff --git a/src/main/java/org/cbioportal/web/util/DataBinner.java b/src/main/java/org/cbioportal/web/util/DataBinner.java index 6002f2df9b6..0ce2838e0c4 100644 --- a/src/main/java/org/cbioportal/web/util/DataBinner.java +++ b/src/main/java/org/cbioportal/web/util/DataBinner.java @@ -1,6 +1,20 @@ package org.cbioportal.web.util; -import com.google.common.collect.Range; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + import org.apache.commons.lang3.math.NumberUtils; import org.cbioportal.model.Binnable; import org.cbioportal.model.DataBin; @@ -11,10 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.math.BigDecimal; -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; +import com.google.common.collect.Range; @Component public class DataBinner {